RUGitHubInstall

Tech debt

Known gaps and deferred decisions, tracked so they don’t get lost between releases.

agent/lib/repeat-guard.ts temporarily stops repeated failed tool calls at the model boundary; remove it when eve itself stops a turn on a repeated invalid tool call.

1. Approval prompts (eve tools.approval + Telegram HITL)

eve ships a native tool-approval flow (human-in-the-loop confirmation before a tool runs). Iva doesn’t wire it up yet — every tool call executes unattended. Adopting it means designing the Telegram side of the approval prompt (inline buttons, timeout, what happens to the turn while waiting) before it’s worth turning on. Deferred deliberately, not an oversight.

2. Bridge UI wizards → native HITL

The /model, /think and related menu flows in the Bridge (scripts/poller/) are hand-rolled multi-step wizards predating eve’s native human-in-the-loop primitives. They should eventually move onto the same mechanism as item 1 instead of maintaining a parallel bespoke UI layer.

3. Cross-imports from scripts/lib into agent/ — CLOSED

eve rebuilds agent/ at service start, so a specifier there that resolves into scripts/ drags operational code into the bundle — the failure behind the 0.3.14 crash loop (issue #176). RESOLVED: there are none left. scripts/authored-tree-guard.test.ts asserts the exact list of edges out of agent/, and every one of them leads into the shared packages/ (@iva/* or a relative path there), so a new specifier out of agent/ is red on sight. The guard scans production files only: tests never reach the bundle eve rebuilds, so a specifier in a *.test.ts cannot drag scripts/ into it.

Moved to their canonical home in agent/lib: telegram-acceptance, run-status, settings, i18n, telegram-format, security-gate, telegram-reply-context, telegram-reset-route, telegram-turn-start, schedule-runner, the write half of usage, then core-cap, core-clamp, card-text, schedule-migration and the health poll now in agent/lib/eve-health.ts. scripts/ consumers reach them through the #lib/ alias instead of the other way around.

What made the last ten closable is the seam, not a move. iva has to work on an install whose agent/ is missing or half-written — that is the state repair.sh exists for (ADR-0003) — so every module those processes load stays in scripts/, every module the authored tree needs lives in agent/lib, and neither side reaches the other while loading. “Those processes” is wider than scripts/cli/*: the guard’s load-time walk stops at a child process, so every separate node run is walked as its own entrypoint. The systemd units are read out of deploy/ rather than listed by hand — a hand-written list forgot the nightly Brain unit (deploy/iva-brain.service) once, which is exactly how a unit gets silently coupled to the tree — and the three runs no unit starts are named in the guard: the setup wizard (install.sh and iva config → scripts/setup.mjs), the vault template copy install.sh runs before eve has ever built the tree, and the updater’s second half, which the previous version spawns inside the version it just fetched. One unit is exempt on purpose: the Telegram bridge renders Iva’s own UI (#lib/i18n.ts, #lib/run-status.ts) and runs beside iva.service, which is what builds the tree. Three shapes, in descending order of preference:

4. Evals

The #evals/* import alias is declared in package.json but unused. eve ships a native eve/evals module — adopt it before adding product-level skill evals.

5. Discovery guardrails are not part of the release check

npx eve info prints what eve actually discovered in agent/ — the instructions dir, the skill, tool, subagent and schedule counts, and a Diagnostics line — so a skill or tool that quietly stopped being discovered shows up as a smaller count. Nothing runs it: npm run build spawns npm run build:core (eve build) from scripts/build.ts and reads none of that back, and no other script in package.json calls eve info at all. So the wiring can rot between releases and the first place it surfaces is a user’s install. There is no CI to hang the check on (ADR-0004); the fix is either a pre-release habit of reading npx eve info or a script that asserts the expected counts.

6. sessionTimeoutMs: false — CLOSED

Closed 2026-09-02: agent/agent.ts sets a 24-hour session lifetime. A day of active chat grew one durable session to ~6400 events and a 220–660 s replay per turn, past the 240 s workflow ceiling; the upstream lifetime is the retirement story.

Original note:

Disabled in agent/agent.ts to preserve eve 0.27’s behavior (no auto-expiry) after the 0.28 default changed to a 30-day session lifetime. This was the safe choice for existing self-hosted installs with long-lived Telegram/rollup sessions, but it opts out of a framework-owned cleanup mechanism. Revisit deliberately once Iva has its own session-retirement story, rather than leaving the override in place indefinitely.

7. Opt-in UI for the scheduled Notices — CLOSED

/menu → 🔔 Notices (scripts/lib/menu/notices.ts) switches the nightly memory reports (memoryReports.enabled) and Writes on her own — Watch and the Brief, which replaced the morning Report (proactive.enabled, ADR-0020) — so neither needs a raw settings.json edit, and Insight (proactive.insightTimes): a switch and three times. All three keys are read at fire time, so a tap applies on the next tick with no restart. The rules the screen enforces: ADR-0007, ADR-0020 and ADR-0022.

8. TypeScript-only Node source

The repository migration is complete. New Node.js source and tests must be TypeScript; JavaScript modules must not be added. Five permanent, logic-free .mjs entry shims keep externally installed paths stable: bin/iva.mjs and scripts/{telegram-poll,check-update,setup,init-vault}.mjs. All implementation belongs in the TypeScript modules behind those shims.

9. Upstream feature request: catch-up for missed schedule runs

If the box is down when an eve schedule would have fired, the run is simply skipped — there’s no catch-up on next start, unlike systemd’s Persistent=true timers. Worth filing as a feature request against vercel/eve.

Workaround implemented here: agent/lib/schedule-migration.ts, run fire-and-forget from agent/instrumentation.ts on every server start, replaces Persistent=true for the single memory-night schedule. It compares the last recorded success (data/rollup-status.json) against the most recent timezone-aware scheduled point and runs it once when stale and still inside the 20-hour grace window — home-grown and specific to this app, not a general answer other eve apps could reuse. Superseded if/when eve grows a native catch-up story.

10. Rollup-turn workarounds for vercel/eve#1450

Closed (T96). A parked session no longer resumes: every night turn creates its own session, and the turn’s deadline is the abort signal of its model request (scripts/memory/night-call.ts), not a timer race. The Promise.race timeout is gone.

11. Cron/name metadata duplicated across schedules, migration, and the menu

The same 5 schedule names + cron expressions used to be hand-maintained in three places: agent/schedules/*.ts (the actual cron strings), agent/lib/schedule-migration.ts’s PERIOD_SCHEDULE (hour/minute per period, for catch-up math), and scripts/lib/menu/crons.ts’s EVE_SCHEDULES (for the /menu → ⏰ display). Changing one schedule’s cadence meant remembering to update up to three files by hand; a missed one would make the menu display (or the catch-up math) silently wrong.

RESOLVED: the table lives once, in agent/lib/schedule-table.ts (SCHEDULE_CRON), and all three read it — the schedule files take their cron from it, the migration places its catch-up point with parseCron(), time of day and day constraint alike (it keeps only its own per-period grace window, which is catch-up policy, not schedule metadata), and the menu renders the entries in table order. agent/lib/schedule-table.test.ts cross-checks all three against the table — the migration through its behavior, by bisecting the point where a recorded success stops counting as stale and checking that instant against the cron — and fails if any cron expression reappears in another source file, so the copies cannot silently grow back.

12. Bundled Autograph was removed

The non-night Autograph engine now lives in the standalone Autograph repository. Iva keeps only the TypeScript seams its product uses: frontmatter, card sections, the nightly graph and the streaming description cleanup. Fixes to the standalone engine do not ship with Iva automatically.

13. Markdown parsing has one implementation

agent/lib/frontmatter.ts and agent/lib/card-store.ts are the canonical parsers. Their edge corpus lives under scripts/fixtures/ and the TypeScript property tests pin quoting, folded blocks, fenced headings and round trips.

Pair (a) had two more implementations until then, one per half of memory search: agent/tools/memory_search.ts (BM25 columns) and scripts/memory/embed-index.ts (the dense sidecar) each carried a private mini parser for the few scalars they index, with their own field lists. Duplicates inside one language — no golden fixtures held them, and both silently dropped folded/literal scalars (indexing the literal >-), block lists and every card written with CRLF. Worse, the copies were not identical, so in MEMORY_SEARCH_MODE=hybrid the two halves could rank different text for the same card. Both are gone: agent/lib/card-index.ts is now the single seam turning a card into indexable text (canonical parseFrontmatter, one META_FIELDS list, lists flattened), and both halves call it. scripts/memory-search-index.test.ts pins the FTS columns and the dense text side by side per card shape, and checks the halves still agree.

14. The inbound gate cannot replace Telegram message text

On a text message the pipeline can add context to the turn and nothing else: eve’s TelegramInboundResult is { auth, context } | null, so message.text is delivered to the model by the channel regardless of what sanitizeInbound found. Checked against the pinned eve 0.51.1. The only lever beside it is null, which drops the entire update: too blunt to spend on a false positive, and from 0.31.0 it no longer applies to an authorized message at all. So for Telegram text the gate is an annotation: the cleaned copy, the injection warning and the truncation note ride beside the original, and whether the payload is obeyed is up to the model. Everything the pipeline assembles itself — voice transcripts, captions, the vision description of an image — is genuinely filtered, because there is no channel-delivered original to compete with.

No workaround here, by the owner’s decision: faking one (re-sending a scrubbed copy, dropping the turn and replying by hand, editing session state behind the framework’s back) buys a partial guarantee at the cost of a second delivery path around the inbound pipeline — exactly the shape ADR-0005 exists to avoid. docs/security.md states the limit instead, in the section “Telegram text: an annotation, not a filter”.

The fix is upstream; the feature request is filed as vercel/eve#4605 (10.10.2026) and is small: let the inbound hook return the text the model will see, as one optional field beside context —

type TelegramInboundResult = {
  readonly auth: SessionAuthContext | null;
  readonly context?: readonly string[];
  readonly text?: string; // new: the body the model actually reads
} | null;

— where an omitted text keeps today’s behaviour, so it can ship in a patch release, and the replacement covers only the model-visible body, leaving logs and the raw update alone. Remove this item when a released eve lets the pipeline hand back that text and the pipeline uses it: that is what turns the sanitizer on Telegram text from a warning into a filter, and it retires the security doc’s section with it.

15. A rich message in a group is admitted only as a reply

The Bridge admits a group message carrying no text/caption only when it replies to the bot (ADR-0011). So a rich_message that addresses Iva with an @mention inside its blocks is dropped: the Bridge judges the envelope and never opens content, and the scripts/lib → agent/ import boundary (§3) is closed, so it cannot borrow the reader that would find the mention. Private chats are unaffected — there is nothing to address there.

Remove this item when Bot API exposes a plain-text projection of rich_message beside the blocks, or when eve upstream resolves the addressing before the update reaches the Bridge. Until then the workaround in a group is to reply to one of Iva’s messages.

16. Rollup stale-cursor workaround for vercel/eve#2461

Closed (T96). The bug (vercel/eve#2461) needs a resumed session: result() read from a lagging cursor. The night now creates a fresh session per turn and reads its stream from index 0, so the drain, the prompt nonce and the foreign-result check are removed.

The ai pin is gone: Iva used to override ai to 7.0.39 against eve 0.51.1’s peer ai ^7.0.82, so eve’s bundled @ai-sdk/code-mode (it imports experimental_toolCaller) did not link and any Workflow/code-mode tool would crash server start. package.json now asks for ai ^7.0.82 with no override. ADR-0013 rejected the workflow-tool route for Reminders (variant C) partly on that pin; the pin no longer stands in its way, the rest of the ADR’s reasoning does.

17. Insight follow-ups for the next wave

18. Update lock takeover residual windows

acquireUpdateLock serializes two new contenders reclaiming an abandoned owner by rename and owner.json comparison. The ownerless mkdir → owner.json publication window has a five-second grace, checked both before and after rename: two null contents alone cannot identify an abandoned directory (UpdateLock-ownerless.cfg, UpdateLock-ownerless-unsafe.cfg). A publisher stalled beyond the grace can lose its ownerless directory. A third contender can claim the temporarily empty path before restoration, and a legacy updater still deletes a successor by path (UpdateLock-three.cfg, UpdateLock-mixed.cfg). A crash while a live successor’s directory is displaced also leaves the original path available to another contender. A displaced live owner is kept in update.lock.stale-* and logged if restoration finds an occupied path; version sweep removes forgotten directories older than a day while retaining live owners. The mixed window lasts until old updaters are retired; the three-contender window remains part of the directory protocol.

Startup recovery uses the same chat-status lock with a two-second stale threshold; ordinary chat writers retain their usual thirty-second threshold. A killed writer’s fresh lock becomes reclaimable within two seconds. A live synchronous writer stalled beyond this recovery threshold can lose its lock (FileLock-sync-slow.cfg); normal writes must finish well within the threshold.

19. Daily-summary conflicts across nights

Closed by storing the original summary hash in the open day pass before the model call and the last published hash in cache.writtenSummaryHash before writing the summary. The published hash survives day completion. Subsequent passes compare against that publication, so a late entry preserves owner edits with absent, matching or mismatching body_hash, including description-only edits. Interrupted publication can finish its git commit after the raw day, model or instruction changes. The comparison uses the saved publication hash throughout recovery.

Three-night process tests in scripts/memory/night-process.test.ts assert the preserved bytes and “день ждёт владельца” on repeated nights. The message and the old-queue reminder name iva jobs skip memory-night <date> or restoring the summary. An explicit owner skip takes precedence over an open pass; the CLI writes it even when an ordinary completion marker already exists. Other days, CORE and periods continue to finish. The property test runs 100 cases with seed 20261020.

NightWriter.HumanEditWins covers open passes and edits after day completion, before a late entry opens the next pass, including closed 0.5.5 caches without a publication hash. PublishedSummaryResumes covers publication, interruption and continuation after input changes. Mutants that reread the expected hash, omit the published hash or compare with recomputed output produce witnesses. The model retains the filesystem stability assumption between the final comparison and atomic publication from ADR-0016.

Both pass.summaryHash and writtenSummaryHash are optional in cache version 1. With a closed old cache, an existing summary and a new pass, the day waits for the owner because its last publication hash is unknown. The summary stays intact. The 0.5.5 reader validates only version, date and integer through, so it accepts the added fields. Its writer preserves the top-level hash, but ignores it on rollback; the absent/matching body_hash cases regain the old behavior until upgraded again. После отката на 0.5.5 и обратно день с поздней репликой может ждать владельца; освобождается iva jobs skip.

20. Accepted empty-response limit

After three recoverable empty responses, a fourth ends the eve turn with turn.failed; background turns retain their owner-facing outcome (#338), and the owner can start another attempt from the parked chat turn. See ADR-0023.

View original Markdown ↗