Status: Living
Last updated: 2026-07-29 — the Phase-2.6 rewrite triaged every open item and the 2026-07-19 full-project review added the deliberately-unscheduled block at the end.
Lifecycle states, because this file mixes three:
- [ ]= unscheduled and actionable · Scheduled → 2.6.X / → 2.5.5.X = owned by that workstream, still unchecked until the PR that lands it · DONE / DECLINED = closed history, written as a plain bullet, never a checkbox.
- Related: current.md, README.md, phases/phase-0-foundations.md, phases/phase-2.5.5-hardening-and-remediation.md
A holding pen so confirmed-but-deferred findings don't get lost. Every item here was adversarially confirmed by a comprehensive review (the Phase-0 97-agent workflow, or a later per-PR review pass) but was deliberately not fixed in that pass — either because it needs a maintainer decision, is below the bar for its pass, or is an optimization whose risk/benefit favors waiting. None block a shipped milestone. Pick them up opportunistically (most fit naturally into the work that first touches the file) or in a dedicated hardening pass.
Severity is the review's verified rating. Check an item off in the PR that resolves it.
- ⚠ Supported floor (Node 20.12, EOL) →
>=22— DONE (Phase 2.6.F Step 1, commit367e4f5). Node 20 was EOL (2026-04-30) with nobetter-sqlite3prebuild (forcing a C++ source build). Both halves shipped together in Step 1: (A).nvmrc22 → 24 (Active LTS); (B) the supported floor 20.12 →>=22— a breaking release for publishedrelavium(pre-1.0 → a 0.x MINOR bump, e.g.0.2.0, at publish — not a SemVer-major) that restoredbetter-sqlite3prebuild coverage and unlocked ink 7, and superseded ADR-0021 via ADR-0067 (better-sqlite3re-affirmed over node:sqlite).vitest5 /eslint10 remain deferred to their own PRs (see the 2.6.F deferral entries below). Analysis: phases/node-runtime-upgrade.md. (package.json engines + .nvmrc + pnpm-workspace catalog @types/node + tech-stack.md + ci.yml floor leg)
- ⚠ OpenAI Sora 2 + Videos API DEPRECATED — provider shutdown 2026-09-24 (affects 1.AH A3). OpenAI
announced the Sora 2 video models (
sora-2,sora-2-pro,sora-2-2025-10-06,sora-2-2025-12-08,sora-2-pro-2025-10-06) + thevideos.*API shut down 2026-09-24. The 1.AH A3 OpenAI/Sora async-video adapter targets exactly these (videos.create/retrieve/downloadContent+pollMediaJobSora). Impact is narrow: only the Sora adapter arm + its tests; the engine async-job LRO (ADR-0045), the sharedencode/decodeMediaJobIdcodec, the generative-seam conformance, and the Gemini/Veo (A4) adapter are provider-agnostic and unaffected. No live exposure today — A3 is not runtime-reachable until the Phase-2 hostmedia_surfacewiring. Action (maintainer call, before 2026-09-24): when OpenAI announces a replacement video model/API, retarget the A3 arm; otherwise disable/remove the Sora arm (thegenerateMediavideodispatch +pollMediaJobSora+ their tests) leaving the seam + Veo intact. Low priority until a replacement lands or the date nears. (packages/llm/src/adapters/openai.ts; 1.AH/Phase-2) - Host-side SSRF enforcement in
EgressCapability.fetch(DNS resolve + connect-by-validated-IP + per-hop redirect re-validation) — the shared SSRF range-primitive (1.AE) covers the policy half (literal format checks on URLs and hostnames); the mechanism half — resolving a hostname to its IP, validating the IP against the same range block, pinning the connection to that IP (connect-by-validated-IP), and re-validating on every redirect hop — belongs to the host-sideEgressCapability.fetch(already defined inpackages/core/src/tools/types.ts). When the desktop or CLI surface implements that fetch hook, it must apply these runtime checks. The currentassertHttpsBaseUrlandrefineInFlightMediaPartURL validation are construction-time / seam-ingestion-time policy; they catch malformed URLs but cannot catch DNS rebinding or a public hostname resolving to a private IP. Scope split (resolving the earlier "Phase 2" framing): the media url-carrier mechanism is pulled into 1.AF on a new bytes-shaped media-egress capability (ADR-0043); the CLI toolEgressCapability.fetchlanded in 2.5.E (ADR-0057) —apps/cli/src/engine/tool-host/egress.tsover the sharedconnectValidatedconnect-by-validated-IP mechanism (packages/db/src/safe-egress.ts), with the Host/:authority-header strip; the desktop surface's fetch hook still lands when the desktop implements it. (packages/core/src/tools/types.ts; security-review.md; media → 1.AF/ADR-0043; CLI tool → 2.5.E/ADR-0057; desktop → surface fetch hook) - MCP SDK network transport — upgrade to connect-by-validated-IP (ADR-0053 §2). 2.R ships pre-connect host validation as the floor for the
http(Streamable HTTP) /websocketMCP transports — the@modelcontextprotocol/sdkopens its own socket, architecturally distinct from theEgressCapability.fetchhook above. When the SDK transport exposes an injectablefetch/dialer hook, upgrade to connect-by-validated-IP: resolve DNS → validate the IP against the shared range-block primitive → connect to that IP, re-validating on each redirect hop — closing the residual DNS-rebind window. The dialer + redirect re-validation MUST enforce the authoredhost:port(ADR-0053 §3 / SEC-EGRESS-3), not just the host: anallow_local_endpointserver is permitted exactly its declaredhost:port, so a resolved/redirected target on a different port of the same permitted-private host (:6379/:5432/:22/the Docker socket) must be re-blocked. (2.R's pre-connect floor is host:port-safe by construction — the SDK dials exactly the one authored url — so this constraint binds the dialer, not the floor.) Each MCP network mechanism gets a dedicated security-review pass when it lands. (packages/mcp/src; ADR-0053 §2/§3; ADR-0043 mechanism) - MCP
stdiospawn — import-trust/consent gate +npxdependency pinning (ADR-0052 §2). Spawning a declaredstdioMCP server runs arbitrary local code / annpx-installed package. 2.R treats a server declared in the user's own committed YAML as author trust; the imported/shared untrusted workflow case is out of baseline scope. When the import/share path matures, gate the first spawn of a server from an untrusted-provenance.relavium.yamlbehind explicit consent, and pin thenpxpackage version/integrity for the built-in auto-install servers. Scheduled → 2.6.B (the authoring/import path this consent gate protects matures there). (packages/mcp/src; apps/cli; ADR-0052 §2; ADR-0029 trust model) - MCP host boundary — strip
McpConnectError.causefrom--json/ event output (2.R Step 3, ADR-0052 §2). Resolved in the 2.R Step 3 host wiring:startMcpClientFailLoud(apps/cli/src/engine/mcp-servers.ts) wraps anMcpErrorinto a typedCliErrorwhose message is the secret-free MCP summary with no{ cause }attached, and the top-level--jsonrenderer (apps/cli/src/process/render-error.ts) serializes only{ type, code, message }— nevercause. Regression-locked byrun.test.ts(expect(err.cause).toBeUndefined()). (apps/cli; packages/mcp/src/errors.ts; 2.R Step 3) - MCP network transport — header-based auth (ADR-0052 §6). 2.R injects
{{secrets.*}}only into a stdio child'senv; the network (http/websocket) specs carry only{ url }, so a network server'senvis rejected at parse (fail-closed, never silently dropped). When network MCP servers need credentials, add a host-resolved auth-header field (e.g.Authorization: 'Bearer {{secrets.<name>}}') wired through the SDK transport'srequestInit/headers, resolved from the same isolatedmcp-secret:*namespace and never logged/serialized. Scheduled → 2.6.I. (packages/mcp/src/sdk-http.ts; apps/cli/src/engine/mcp-servers.ts; ADR-0052 §6) - MCP follow-ups (non-security). A durable cross-invocation tool-list cache (mcp-integration.md ~1h per-
(command,args), with a transport-covering key) — 2.R re-runs discovery per process (ADR-0052 §3); and a generalizedSecretResolverseam beyond the 2.Rmcp-secret:*keychain namespace (ADR-0052 §6); and reconciling thetypes.tsToolId"register dynamically" comment to "host-side assembled" when 2.R touchespackages/core; and mid-call abort propagation — the engine'sAbortSignalLikeis not forwarded to the in-flight MCPtools/call(the SDK transport wants a DOMAbortSignal), so a turn cancel tears the connection down but does not cancel an in-flight call (@relavium/mcpmanager.ts). The tool-list-cache + mid-call-abort halves are scheduled → 2.6.I (the rest stays opportunistic). (packages/mcp/src; packages/core; Phase-3) - Streaming media triad (
media_start/media_delta/media_end) — host-deferred (ADR-0046 §4). 1.AG Section B delivers inline media-out through the non-streaminggenerate()path (the in-flightmediaContentPartis de-inlined at#emitDurable). The streaming triad stays RESERVED: its Node de-inline needs a host hook reaching the pure adapter (the output twin ofresolveForEgress, sincemedia_endis handle-only and the adapter has noMediaStore) or the desktop Rust CAS (ADR-0032). Acceptable because a media-output turn is typically terminal/single-shot, so token-streaming its short accompanying text is low value against a frozen-seam change. Wire at 1.AH (the progressive-preview surface). (packages/llm/src/types.ts seam; 1.AH) - OpenAI agentic image-gen via the Responses API — wire deferred (ADR-0046 §3).
The normalized delivery shape is defined (a
providerExecuted: truetool_resultcarrying a normalizedmediapart — ADR-0031 §4.3/#7) and 1.AG Section B wires the two Chat-Completions inline cases (GeminiresponseModalities→inlineData, OpenAI inline audio →audio). OpenAI image output is not a Chat-Completions modality (modalitiesistext/audioonly); agentic image-gen is the Responses APIimage_generationbuilt-in tool — a separate request surface the Chat-Completions adapter does not call. Wire the Responses-API path (request +image_generation_calloutput-item parse → the already-defined providerExecuted media shape) when a Phase-1 model needs it. (packages/llm/src/adapters/openai.ts; post-1.AG) - Per-model
media_surfacelookup — wired by the CLI (✅ PR #52, 2.S; verified + checked off 2026-07-08). Section C addedAgentRunnerDeps.resolveMediaSurface?(model) → MediaSurface(the inline-vs-generative routing discriminator, default'chat'; tests inject it). The production wiring — the host readingmodel_catalog.media_surfaceand supplying the lookup — landed with 2.S:media-wiring.tssuppliescatalog.resolveMediaSurface(themodel_catalogprojection) andbuild-engine.tsthreads it intoAgentRunnerDeps, so a generative model routes by its catalog surface on the CLI. The sibling D15/D17/D8 host-wiring items were checked off at PR #52; this one was missed then. The desktop/VS Code surfaces reuse the same injectable port (Phase-3/Phase-4). (apps/cli/src/engine/media-wiring.ts + build-engine.ts; PR #52) - Verified generative model rates +
MODEL_PRICINGrows (1.AG Section C → 1.AH). The Section C cost mechanism (pre-egress estimate + the one realizedcost:updated) reusesestimateMediaCost/mediaCost, which degrade to 0 on a missing rate (H4). No generative model rows were added toMODEL_PRICING— fabricating billing rates is a mis-bill risk and the existing rows are all "verified". Add the generative rows (gpt-image-1, Imagen, OpenAI-TTS, Sora, Veo) with verifiedmediaOutputRates(image per-image; audio/video per-second) when the catalog/pricing-page values are confirmed; until then a generative call is correctly gated/folded at 0 cost (no mis-bill). Test follow-up: the realized-cost vertical (realizedMediaCost→ a non-zerocost:updated) cannot be exercised end-to-end until a generative model carries a rate; the cost MATH is unit-tested viamediaCostagainst a constructed rate, and a non-zero dispatch assertion lands with these rows. (packages/llm/src/pricing.ts; verified rates → 1.AH) - Rate-carrying media representation for raw PCM (1.AH A1 known-limitation). OpenAI TTS
pcmis headerless 16-bit LE PCM at 24 kHz, but the seam'sMediaMimeTypeSchemaforbids MIME parameters, so the bareaudio/L16cannot carry;rate=24000(RFC 2586's default is 8 kHz) — a consumer of anaudio/L16part must assume 24 kHz. Mirrors the pre-existing chat-audiopcm16 → audio/L16convention; the self-describing containers (mp3/opus/aac/ flac/wav) are unaffected. A rate-carrying media representation (or dropping bare-PCM from the offered set) is the fix. Low priority — opt-in niche format only. (packages/shared/src/content.ts MediaMimeTypeSchema; packages/llm/src/adapters/openai.ts; low · 1.AH/Phase-2) - Node-retry of a PARKED media job — deferred (1.AG Section D, ADR-0045 §3 "MAY re-dispatch").
The node-retry budget (1.S) wraps the executor
#dispatch; an async media job parks AFTER dispatch returns, and the engine's out-of-band poll loop settles a deadline/poll failure asnode:failed(retryable for a timeout) directly — it does NOT re-enter the node-retry wrapper, so a parked media job is not automatically re-dispatched (a fresh paid submit) on a retryable timeout. ADR-0045 §3 makes this a MAY ("the loop itself never silently re-submits"), so the current behavior is conformant; re-dispatching a parked-then-failed media node through the node-retry budget is an additive refinement. (packages/core/src/engine/engine.ts #pollMediaJob; post-1.AG) - Durable realized cost for a FAILED/CANCELLED paid media job — ✅ Done (PR #52, 2.S Part 3).
node:failed,run:cancelled, ANDrun:failednow each snapshotcumulativeCostMicrocentsonto the durable terminal (the#emitMediaJobCostfold runs just before the terminal), so a durable-log reader reconstructs a billed-but-failed/cancelled media job's cost from the persisted log, not only the livecost:updatedstream. The checkpoint fold reads cost only fromnode:completed, so resume is unaffected (ADR-0045 §5). (packages/core/src/engine/engine.ts; shared/run-event.ts; PR #52) - Per-modality pre-egress media cost estimate (A6) — ADR-0028's governor is token-based and
cannot price a media-gen call. Add a
[defaults].media_cost_estimateconfig default (the media analogue ofmax_tokens_estimate, in config-spec.md) and a per-model media rate inpricing.ts/model_catalog; the governor estimatesunits × ratepre-egress. Decided in ADR-0044 (disjoint cost class folded into the existingmax_cost_microcentscap — no new cap dimension; a distinct media count/bytes cap is deferred as additive). (config.ts; pricing.ts; database-schema.md; wired at 1.AF) -
partialRefpartial-write semantics (A3, reserved) —media_delta.partialRefships in the frozen triad (1.AD) but is reserved, host-implementation-defined; theMediaStorecontract defines onlyput(completeBytes). Specify append-vs-per-delta-put semantics when the surface that renders progressive previews lands. (1.AH / Phase E) -
read_mediaworkspaceauthz scope kind (A8, reserved) —read_mediaauthz is a generichandle → allowedScopes: Set<Scope>withScope = { kind:'session', id }today; the{ kind:'workspace', id }kind is reserved (documented, not implemented) so cross-session / shared-asset reads are an additive scope kind, no handle-model migration. Implement only when a shared-asset feature has a real consumer. (reserved; ADR-0044 ships thesessionkind at 1.AF, defers theworkspacekind) -
MediaStoreretention/GC +media_objectstable (defaulted) — per-distinct-referencerefcount+last_referenced_at+ grace window, separate from the 90-dayrun_eventsprune; GC owner is the host (Rust desktop / filesystem CLI). Decided in ADR-0042 — amedia_referencesrefcount junction + a 7-day-default grace window + a terminal-state sweep; lands with the table at 1.AF. (database-schema.md; 1.AF) - Retire the
visionderived alias (OQ6 default) —CapabilityFlags.visionis kept as a derived alias ofmedia.input.imagefor live consumers (db.supports_vision, adaptersupports.vision); schedule removal once those migrate tomedia.input.image. (types.ts; a later cleanup)
- Conformance test additions — image-in, audio-in, pdf-in per provider — the 1.AE media input
wiring (OpenAI
image_url/input_audio, Anthropicimage/documentblocks, GeminiinlineData) is unit-tested but the conformance replay fixtures don't yet exercise media-in scenarios. Add recorded fixture-replay conformance tests per provider: image-in (all three), audio-in (OpenAI, Gemini), pdf-in (Anthropic, Gemini). Requires real API calls to record fixtures, then replay — do in a follow-up after the PR lands. (packages/llm/src/conformance/; 1.AE follow-up) -
mediaUnitsmapping (OpenAI audio tokens, others nil for now) —Usage.mediaUnitsships as an optional field (ADR-0031 decision #4) but no adapter populates it yet. OpenAI reportscompletion_tokens_details.audio_tokenswhich maps to{ modality: 'audio', direction: 'output', units: n, unit: 'count' }(the RAW token count — audio_tokens are tokens, not seconds); Anthropic and Gemini report no media-specific billing counters in their current usage shapes. Wire OpenAI audio-token billing at 1.AF when the engine surfaces usage to the session; leave Anthropic/Gemini nil until those providers add billing counters. Report the rawaudio_tokenscount (no fabricated tokens→seconds conversion) to avoid mis-billing (ADR-0044). (packages/llm/src/adapters/openai.ts; 1.AF) - OpenAI reasoning-model capability matrix (
OPENAI_REASONING_CAPS) — o1/o3/o4-mini have a separate media capability surface (no audio input, restricted tools, etc.). The currentopenai.tsuses oneOPENAI_SUPPORTSmatrix for all GPT models; reasoning models need their own matrix selected by model id, paralleling how DeepSeek already hasDEEPSEEK_SUPPORTS. Wire when reasoning-model media support is specified. (packages/llm/src/adapters/openai.ts; 1.AF/1.AG) - Handle and URL media source resolution in adapters (
1.AFMediaStore integration) —handlesources (media://sha256-<hex>) andurlsources are accepted at the seam boundary but skipped (not wired) in the adapter content-building functions (toOpenAiUserContent,toAnthropicContentBlocks,toGeminiMediaPart) with// handle: resolved at egress by MediaStore (1.AF)/// url/handle audio: deferred to 1.AFcomments. The engine'sdeInlineMedia+MediaStorecontract resolves handles at egress; URL sources are fetched throughEgressCapability.fetch(the SSRF mechanism half). Wire when the engine plumbing lands (1.AF). (packages/llm/src/adapters/{openai,anthropic,gemini}.ts; 1.AF)
The 1.AF P4 engine-pure policy (read_media tool + scope-set authz + Range gate, the D15 load-check, the D17 cost governor, the save_to write port, the media-reference store) landed and is fully tested. The comprehensive 46-agent review confirmed the host/surface wiring below is not yet present — so D12/D15/D17 are inert end-to-end until a host (CLI/desktop, 1.AH/Phase-2) wires them. Recorded here so the roadmap is not read as "live end-to-end." None is a defect in the landed policy; each is the deferred mechanism/wiring half. (1.AF is ✅ Done — all PRs merged #33/#34/#35/#36, 2026-06-20; the items below remain:
read_media(D12) was deferred to 2.M, then split into a dedicated, security-reviewed follow-up (maintainer-approved, 2026-06-26) — the 2.M chat REPL shipped without it (it is engine/db + cross-surface security-sensitive work usable by bothrunandchat, orthogonal to the REPL). D15/D17/D8 are now wired by the CLI (✅ PR #52, 2026-06-25, checked off below) — the desktop/VS Code surfaces reuse the same injectable ports (Phase-3/Phase-4); thesave_tomulti-feeder semantics remain.)
-
read_mediahostMediaReadAccessimpl + base64 encoder (D12 mechanism) — there is no host factory that bridgesMediaReferenceStore.describe()+MediaStore.readRange()(which returnsUint8Array) into theMediaReadAccessthe tool needs (whosereadRangereturns an in-flight base64MediaSource). Until a host provides one,read_mediacannot be invoked successfully. (packages/db; read_media D12 follow-up) -
read_mediasession-scope population (D12 authz data, ADR-0044 §1) — nothing writessession/workspacemedia_referencesrows (the only writer,createMediaReferencePort, writesrunrefs only), sodescribe().allowedScopesis always[]and every read denies. The input-transfer scope-population at the node/session boundary is unimplemented. (packages/core engine input-transfer + AgentSession; read_media D12 follow-up) -
ctx.mediaRead/ctx.requestingScopenot wired into the dispatch context — the AgentRunner + AgentSession buildToolDispatchContextwithout these, soread_mediaalways throwsToolUnavailableErrorin the engine path (fail-closed, no leak). (packages/core/src/engine/{agent-runner,agent-session}.ts; read_media D12 follow-up) -
validateWorkflowWithCatalog(D15) — wired by the CLI loader (✅ PR #52).runandgatecall it post-parse viaassertWorkflowCatalogValid(the shareddrive.tshelper) over the DBmodel_catalog, so an incapable / malformed-generative authoredoutput_modalitiesfails fast at LOAD (exit 2) on a fresh run AND a resume — not only at the runtime FallbackChain pre-skip. The desktop/VS Code loaders reuse the sameWorkflowModelCatalogprojection (phase-3/phase-4). (apps/cli; PR #52) -
[defaults].media_cost_estimate→AgentRunnerDeps.mediaCostEstimate(D17) — wired by the CLI (✅ PR #52).config/resolve.tsreads the key andmedia-wiring.ts/build-engine.tsthread it into the engine, so the media cost governor uses the configured per-modality estimate (falling back toDEFAULT_MEDIA_UNIT_ESTIMATEonly when unset). (apps/cli; PR #52) -
resolveForEgress(D8) — wired by the CLI host (✅ PR #52).build-engine.tsinjects theFilesystemMediaStore.resolveForEgressre-materialization hook, so a durable handle in a transcript message is re-materialized on the failover/egress path rather than sent unchanged. (apps/cli; PR #52) -
save_tomulti-feeder output semantics — an output node with several feeders captures a record;save_torequires exactly one media handle across it (0/>1 → node failure). Document the "which handle" contract + add a mixed-feeder test. (low · workflow-yaml-spec.md + packages/core; 1.AH) -
save_toaccepts only therun.idnamespace at LOAD time — a non-run.idref insave_to(e.g.{{ run.outputs[...] }}) parses, creates a spurious DAG edge, and fails only at runtime. Add a load-time check restrictingsave_totorun.idso the author gets an immediate error. (low · packages/core load path) - CAS-orphan crash window for
save_to— ✅ Done (PR #52). The host media GC's CAS-orphan sweep (runHostMediaGcstep 3, ADR-0042 §4) deletes row-less CAS bytes (a crash betweenputandrecordObject) — gated on no other active run AND anorphanMinAgeMsage-guard so a concurrent writer's fresh blob is never swept. (apps/cli host GC; PR #52) - Clean-terminal media-reclaim retry — ✅ Done (PR #52). The host media GC's clean-terminal reclaim-retry
(
runHostMediaGcstep 1) re-attemptsremoveRunReferencesfor every settled (terminal/gone) run whose inline#reclaimRunMediawas dropped by a crash — never the current run (the engine reclaims it inline) and never a paused run (its media must survive a resume). (apps/cli host GC; PR #52) -
save_tourl double-fetch — aurl-sourced media part in a save_to output is fetched twice (the save_to de-inline + the node:completed emit de-inline; the put dedupes the bytes). Thread one de-inlined result into both paths to fetch once. (low · packages/core/src/engine/engine.ts#performSaveTo) -
save_toresumer-cwd vs original-run project root — ✅ Done (PR #53).runnow persists the run's cwd toruns.project_rootat run-start (threaded throughopenHistoryStore→RunHistoryStoreDeps.projectRoot), andloadRunSnapshotreturns it;gatere-jailssave_tounder that ORIGINAL root (snapshot.projectRoot ??the resumer's cwd for a pre-column run), so a run started in dir A and resumed from B writes its deliverables under A. Therealpath+commonpathjail holds under either root. (apps/cli + @relavium/db; PR #53) - Host-GC orchestration is CLI-local (2.S) —
runHostMediaGc(the 3 ordered steps: clean-terminal reclaim-retry, grace-window byte reclaim, CAS-orphan sweep + theorphanMinAgeMsconcurrent-writer age-guard) is host-agnostic but lives inapps/cli/src/engine/media-gc.ts, so the Phase-3 desktop / Phase-6 cloud hosts can't reuse it. When a 2nd host wires media GC, promote the pure orchestration to@relavium/db(or a shared host-helper) and pin the mechanism in adocs/reference/home so the hosts can't drift. (med · apps/cli → @relavium/db; Phase-3+) -
[defaults].media_gc_grace_dayswired (✅ PR #53). Added to the config Zod schema;config/resolve.tsresolves it (last-writer-wins) and normalizes DAYS → ms (mediaGcGraceMs);run/gatethread it intosweepHostMediaBestEffort'sgraceMs. Absent ⇒ the built-inDEFAULT_MEDIA_GC_GRACE_MS(7-day) default (ADR-0042 §4c). (apps/cli + shared config; PR #53) - Keychain no-raw-key IPC test (ADR-0044 §4 acceptance gate) — ADR-0044 §4 makes "the keychain bridge never returns a raw key from an IPC command" an explicit 1.AF test deliverable, bundled with the media IPC/byte-delivery review surface. That IPC surface is the desktop/Tauri command layer, which is unbuilt at 1.AF — there is no keychain IPC command to assert against yet — so the test is deferred to the 1.AH host bridge that introduces it. The Node-side keychain seam (ADR-0006) exists, but the no-raw-key-over-IPC gate is meaningful only once the IPC command exists. Owner: 1.AH (the keychain/media IPC bridge). Recorded so the ADR-0044 §4 acceptance is not silently dropped. (apps/desktop keychain IPC + a direct test; 1.AH)
- Surface
Usage.mediaUnitsoncost:updated(the disjoint per-unit observability axis) — ADR-0031 A6 / ADR-0044 §3 intend the per-unit media usage (image per-count, audio/video per-second; a token-based provider's audio asunit:'count') to be observable on thecost:updatedevent. Realized media spend already folds intocumulativeCostMicrocents(D17), but the per-unit counts are not surfaced:CostUpdatedEventSchemalives in@relavium/shared, which cannot importMediaUnitsEntryfrom@relavium/llm(the layering forbids shared→llm). Surfacing it requires relocatingMediaUnitsEntrySchemato@relavium/shared(llm re-exports it;UsageSchemakeeps using it) — a seam-shape move that wants its own PR. The canonical docs (sse-event-schema.md) now reflect the current state (field deferred). (low · @relavium/shared seam move + run-event.ts + agent-turn.ts emit; a later PR)
2026-06-19 — second (Sonnet) review pass on PR #35. A full re-review (9 dimensions, double-verified) on top of the first review's fixes confirmed 0 blockers/highs in reachable code; ~17 small fixes landed in the follow-up commit. The items below are the deferred remainder — a read_media result-shape contract that the 1.AH wiring must resolve coherently (it touches the inert read_media path, so fixing it in isolation now risks conflicting with the 1.AH host design), plus one test-injection gap.
-
read_mediaresult must be schema-conforming for a multi-turn message (1.AH read_media contract).read_mediareturns a{ type:'media', source:{ kind:'base64', data } }MediaPart placed intool_result.result; on the next LLM callLlmMessageSchema.superRefinerunscontainsInlineMediaBytesover the tool-result part and rejects inline base64 — so a wired read_media would break the turn. The result should carry a handle (durable form, resolved on egress by the seam), not inline base64. Defer with the 1.AH host wiring (it co-decides base64-vs-handle forMediaReadAccess): (a) read_media returns a handle source; (b) narrowMediaReadAccess.readRangefromPromise<MediaSource>to the chosen base64/ handle form (today the wide type permits aurl/handlesource a host could mis-return → I3/SSRF surface); (c) threadAbortSignalLikeintoMediaReadAccess.describe/readRange(the only host-delegated path with no cancellation, unlikeMediaStore.readRange). (Sonnet review HIGH/MEDIUM, latent — read_media is inert; packages/core/src/tools/builtins.ts + types.ts; 1.AH) - Budget-governor media-cost block/warn/fail path has no non-zero-estimate test. No shipped model
carries a
mediaOutputRatesrow, soestimateMediaCostalways returns 0 and the governor's media-drivenwarn/fail/pausearm is never exercised end-to-end (the units×rate math IS covered inmediaCost/estimateMediaCostunit tests). Add coverage when a media-priced model lands (or refactor the governor to accept an injectable estimator). (low test gap · packages/core/src/engine/budget-governor.test.ts)
A hardening-analysis pass over the seam / fallback / byte-delivery / i18n surfaces (validated against the current ADRs) produced these recorded rulings and deferrals. Each stands on Relavium's own architecture.
- Score-threshold / partial-success fallback — DECIDED out-of-scope for Phase 1 (Y2).
FallbackChain(1.K) routing is binary classified-retryable-vs-fatal only (phase-1 1.K acceptance); a cross-provider quality judgment is a provider-superior decision that ADR-0011's capability-gated lowest-common-denominator seam explicitly fences out. Quality/score fallback stays an author/node concern (a judge /conditionnode + branch in the DAG), not anLLMProvider-seam or 1.K concern — do not fold it into 1.K under any framing. Promote to a separate candidate-ADR only if a concrete multi-workflow demand for engine-native quality-fallback appears, and even then it must sit above the seam, never amend theLLMProvidercontract. (in-1.K scope: the fallback trigger is a typedLlmError/run-event, never a string-sentinel — see phase-1 1.K acceptance.) (ADR-0011; 1.K) - Per-host/per-provider TLS-verify granularity — DEFERRED draft-proposal (MD-TLS). The stance today is a single global never-disable (security-review.md), which is strictly safer. Per-host granular TLS (for a self-signed / private-CA local gateway) reintroduces the MITM surface and would need its own ADR + opt-on/opt-out tests. Decided: keep the global never-disable stance; revisit a per-host opt-IN only when a real private-CA self-hosted consumer lands (the BYOK custom-baseURL opt-in-local path already covers the realistic local-endpoint case). (security-review.md)
- Run-submission idempotency / request-dedup (open — evaluate carefully). Distinct from the content-addressed
media cache and any managed-mode metering
request_id: should an identical run-create request be de-duplicated so a double-submit does not start two runs? Open: a Phase-1 engine run-create hook vs a surface concern. Low-stakes; recorded so it is not lost. Scheduled → 2.6.H (the Home can start a workflow there, making double-submit real). (WorkflowEngine run-create; 1.N) - i18n CI key-parity + data/code separation (Phase 2+ surface). When the desktop / CLI / VS Code
surfaces add i18n: a CI test that fails on a missing/extra translation key (parity), zero conditional
logic in translation data (data ≠ code), and a dead/unused-string lint. Recorded now; lands with the
Phase-2/3/4 surface i18n work (no consumer yet). Scheduled → 2.6.L (the CLI
en+trcatalog is the first consumer). (adocs/standards/entry or skill; Phases 2–4) - Pre-egress token-estimate accuracy — watch item (1.AC). The ADR-0028 governor blocks on
worstCaseNextEstimate(maxTokens)from[defaults].max_tokens_estimate. Record the open question: does the estimate need provider-accurate token counting (from the seam's model meta / usage feedback) to avoid systematic over/under-blocking, or is the declared estimate enough? No change now — re-evaluate with real 1.AC telemetry. (1.AC; ADR-0028) - Configurable sub-100% budget warning threshold (ADR-0028 amendment).
budget:warningtoday is emitted only when a pre-egress estimate would already exceed the cap, on theon_exceed: warnpath (thresholdPctreports the observed spent/limit fraction at that point). A user-facing early-warning threshold (e.g.warn_at_pct: 80) requires amending ADR-0028 to add both a config default and a per-workflowbudget.warn_at_pctfield, plus a decision on whether it throttles/queues subsequent egresses or only surfaces a one-time advisory event. Deferred until there is concrete surface demand or telemetry showing operators need an earlier signal. (1.AC; ADR-0028; config-spec.md; workflow-yaml-spec.md)
A comprehensive multi-dimensional pre-merge review of 1.L2 (PR #15, merged 2026-06-12) confirmed the engine sound and folded every actionable finding. Two cross-layer forward-obligations were deliberately deferred (each marked in a code comment); recorded here so the 1.M / 1.R implementers see them. The security-critical deferrals — re-tainting
run.outputsfor a secret-derived node output, and carrying resolved-interpolation provenance into the untrusted-content-as-data boundary — are already 1.O acceptance criteria (phase-1 §1.O), so they are not duplicated here.
- Frozen
ctxcheckpoint transport must usestructuredClone(→ ctx-threading, not 1.R).resolveContextreturns anObject.freezed null-prototype map so a__proto__/constructorcontext key is a safe own property. That guard is in-memory only: persisting/transporting it viaJSON.stringify→JSON.parse(esp. with a reviver, or merged into{}) can re-materialize__proto__as a real setter. NB (2026-06-15): ctx-threading landed (the ctx-threading PR) and RE-RESOLVESctxat run start AND on resume — it is never carried inCheckpointState, so there is nothing to transport. This obligation therefore stays dormant; it only becomes live if a future revision decides to CHECKPOINTctx(instead of re-resolving it), at which point that transport MUST usestructuredClone(never a JSON round-trip) and pin it with a test. (packages/core/src/interpolation/resolve.ts)
2026-06-14 1.O pre-implementation review. ADR-0039 scopes the same-provider signed-reasoning replay to Anthropic signed (non-redacted) thinking — the case 1.O's headline acceptance needs. Two harder per-provider cases are explicitly deferred (recorded here, not shipped half-built) because each needs a canonical opaque-continuation carrier (a seam-shape addition tracked against ADR-0030).
- Anthropic
redacted_thinkingreplay — the inbound fold drops the opaquedata({ type: 'reasoning', text: '', redacted: true }), so a redacted block can never be lowered back. Faithful replay needs the canonical reasoningContentPartto carry an opaque continuation payload; until then aredactedpart is carried as-is and not replayed, and redacted-thinking continuations are out of 1.O scope. (high · packages/llm/src/adapters/anthropic.ts:126-127, packages/shared/src/content.ts:447-450; ADR-0030 follow-up) - Gemini part-level
thoughtSignaturereplay — Gemini carries the continuity signature on anyPartincluding afunctionCall; the adapter drops it (mapContentreads only name/args) and the canonicaltool_callpart has no field for it, so Gemini 3 function-calling continuations cannot replay it (and can themselves 400). Needs a continuation-metadata carrier on the canonicaltool_call/reasoningparts plus adapter capture/replay. (high · packages/llm/src/adapters/gemini.ts:193-198, packages/shared/src/content.ts:419-441; ADR-0030 follow-up) -
output_schemadeep JSON-Schema conformance — 1.O validates anagentnode'soutput_schemanode-side but parse-as-JSON only (the seam'sresponseFormatis a request hint; a schema-violating-but-valid JSON output, e.g.{"wrong":true}for a{ n: number }schema, currently passes ascompleted). 1.P shares this gap for thetransformnode's optionaloutput_schema— the sandbox guarantees the result is JSON-serializable but does not check it against the declared schema. Deep conformance needs a JSON-Schema validator (Zod cannot consume an arbitrary JSON-Schema), which is a new runtime dependency requiring an ADR. (medium · packages/core/src/engine/agent-runner.ts, packages/core/src/engine/node-handlers/transform.ts; error-handling.md) - Per-attempt model attribution for
agent:token/agent:reasoning—cost:updatedis always per-attempt-accurate, but the two mid-stream eventsagent:token.modelandagent:reasoning.model(EA6, 2.5.H) useactiveModel(updated from the succeeding attempt record, which fires after the stream), so a cross-model pre-content failover attributes that turn's tokens/reasoning to the prior model (reasoning arrives before text, so it shares the same window). A precise fix needs aFallbackChainonAttemptStart/attributed-stream hook (a seam change). (low · packages/core/src/engine/agent-turn.ts; packages/llm/src/fallback-chain.ts) - Multi-tool result ordering in the turn core —
dispatchToolCallsappends tool-result messages in dispatch-completion order; for v1.0 (single tool call pertool_usestop) this is moot — and 1.V now reuses the core on that single-tool path. A parallel-tool provider should order by the accumulator'stoolOrder; re-home to whatever future parallel-tool work first enables it. (low · packages/core/src/engine/agent-turn.ts; future parallel-tool)
2026-06-14 (PR #18 final review follow-ups). Confirmed by the multi-dimensional pre-merge review; non-blocking, recorded so they aren't dropped.
- Parse-time
run.outputs/read_filegate on system-bound fields — 1.O assemblessystemfrom authored text only (secure), butsystem_prompt_appendis collected as a{{ … }}reference site (collect.ts) so the contract implies dispatch resolution. A future PR that admits trusted{{ inputs }}/{{ ctx }}in system fields must add a parse-time gate rejecting untrustedrun.outputs/read_filereferences there (analogous to the secret-taint gate — do not drop the field fromnodeReferenceSites, which would remove the existing secret-leak protection). A pinning test already asserts an untrustedrun.outputsvalue never reaches the system string. Scheduled → 2.6.D. (medium · packages/core/src/interpolation/analyze.ts, collect.ts; SEC-1) - Multimodal tool-result through the adjacent-message + redaction paths — all 1.O coverage exercises
text/JSON tool args + content; confirm image/media tool-result blocks survive the Anthropic adjacent-role
merge (no dropped blocks / no double-merge with
stripReasoningParts) and the redaction path. (low · packages/llm/src/adapters/anthropic.ts; 1.AF) - Checkpoint/resume of a mid-tool-loop turn — whether a run paused/resumed between tool dispatches
reconstructs the message history (assistant turn + partial tool results) consistently. NB (2026-06-15):
1.R (PR #22) resumes only at GATE boundaries; a crash mid-tool-loop is non-resumable → reconciled to
run:failed(a started-but-unfinished node re-runs frompending). Faithful mid-loop resume needs persisted agent message history (CheckpointStatecarries none today) → Phase-2. (medium · 1.R/Phase-2)
2026-06-14 1.P implementation + pre-merge review. The six non-agent handlers (condition / transform / fan_out / fan_in / input / output) landed behind the 1.N seam. A comprehensive multi-dimensional review confirmed 18 findings; all blocker/high/medium/low/nit items were folded in the 1.P PR — including a BLOCKER secret-leak (the
inputhandler emitted rawsecret-typed inputs into events; fixed by threadingsecretInputNamesontoNodeExecContextand masking in the input handler + the expression scope). The items below are the deliberately-deferred forward work (maintainer-approved), recorded so they aren't lost.
- True
wait_firstearly-cancellation of losing branches —merge_strategy: firstis implemented executor-only: the engine still waits for all branches to settle, then thefan_inhandler takes the first bybranchNodeIdsorder. Genuine early-cancel (abort the still-running sibling branches the moment the first settles) needs an engine-owned per-branch cancellation primitive — the current single run-wideAbortSignalcannot cancel one branch without cancelling the run, and a handler cannot cancel sibling vertices. The engine authors already flagged this as a "1.P refinement" (engine.ts:26-28). Promote to a scoped 1.N/engine change (possibly an ADR) only when a real workflow needs it. (low · packages/core/src/engine/engine.ts, packages/core/src/engine/node-handlers/fan-in.ts; run-plan.md §fan-in) -
secret-typed input flowing into an agent prompt (1.O parallel to the 1.P fix) — the AgentRunner resolves{{ inputs.<name> }}in aprompt_templateagainst the rawRunScope(agent-runner.ts), so asecret-typed input interpolates raw into a USER message sent to the provider. This is provider egress the author opted into (not an event-payload leak, so it does not violate the events rule the 1.P fix enforces), but whether asecret-typed input should be silently interpolated into a prompt — vs masked / rejected at parse — is a policy call. Decided (2026-06-21, maintainer): REJECT AT PARSE — a{{ inputs.<secret_name> }}reference inside aprompt_templateis a parse-time error with a clear message (secure-by-default; surfaces author intent explicitly rather than silently egressing or silently masking). Resolved — verified already-satisfied during 2.D (2026-06-22). The decided policy was already enforced by the 1.L2 parse-layer taint analysis:collectReferencescovers the agent node'sprompt_template(collect.ts),analyzeSecretTaintflags any tainted reference reaching model-visible text, andparseWorkflowturns a non-empty result into aWorkflowSecretLeakError(secret_interpolation) before aWorkflowDefinitionis ever produced — so a run never starts on such a file. Covered by tests for the direct case (analyze.test.ts"rejects a secret-typed input interpolated directly into a prompt"), the transitive-via-contextcase, and$ref/registry agents (analyzeResolvedAgentTaint). 2.D maderelavium runthe first liveprompt_templateconsumer and confirmed the guard holds end-to-end; no new code was required. (low → 2.D · packages/core/src/interpolation/analyze.ts + collect.ts + parser.ts; security-review.md)
2026-06-15 1.S implementation (ADR-0040). The above-chain node-retry budget (Part A — the run loop re-dispatches a whole node on a retryable failure, with backoff, bounded by
retry.max, applied to agent/condition/transform/merge nodes) landed. Part B is deferred:
- retry-from-node — re-run a settled run from a chosen node (ADR-0040 Part B) → Phase-2. Deferred
because the in-memory engine cannot satisfy the design intent simultaneously: re-running on the same
runId(so the host dedups completed-upstream side effects viarunId+nodeId+retryCount) would append a second terminal event to a settled run, breaking the exactly-one-terminal invariant (ADR-0036) and the 1.R Checkpointer fold; a newrunIdkeeps a single terminal but loses upstream side-effect dedup. Reconciling both needs the real persistent store + a run-attempt model (a re-run row referencing the original) — Phase-2, which already owns the surface trigger. The in-run budget (Part A) is the landed 1.S deliverable. (medium · packages/core/src/engine/engine.ts; ADR-0040 Part B; Phase-2)
2026-06-15 1.R/1.Q implementation + two pre-merge review passes (PR #22). The derived
Checkpointer
- cross-process
resumeFromCheckpointand thehuman_in_the_loopgate (suspend/resume + one-shotsetTimertimeout port) landed; both review rounds' findings were folded in the PR. The items below are the deliberately-deferred forward work — each is a Phase-2 concern that needs real persistence and/or a store-level guarantee the in-memory reference cannot provide, recorded so it isn't lost.
- Re-arm a still-pending gate's timeout on cross-process rehydration —
resumeFromCheckpointapplies the target gate's decision immediately, but a remaining pending gate (multi-gate run, crash-while-paused) is rehydrated without re-arming its timer, so its deadline is lost until the next restart. The data needed (timeoutAction+expiresAt) is now persisted onhuman_gate:paused(PR #22), so no backfill — Phase-2 crash-reconciliation re-arms from the log against a real clock. Scheduled → 2.6.K. (low · packages/core/src/engine/engine.ts#seedFromCheckpoint; Phase-2) - Content-level workflow-identity guard on resume —
resumeFromCheckpointcompares the surrogateworkflowId(catches resuming a different workflow →workflow_mismatch), but not a same-slug, edited-content workflow. The stronger guard rides on the frozenruns.workflow_definition_snapshotcolumn (database-schema.md) — a Phase-2 persistence concern wired with the realRunStore, not the event-derived in-memory state. Scheduled → 2.6.H. (low · packages/core/src/engine/engine.ts; Phase-2) - Cross-process concurrent gate-resolve (TOCTOU) — idempotent re-delivery holds within a process
(
#resolvedGates) and across processes once the prior process'shuman_gate:resumedis persisted (the checkpoint reconstructsresolvedGateIds). The residual window — two processes loading the same still-pending gate before either persists — is closed by a store-level uniqueness constraint onhuman_gate:resumedper(runId, gateId), a Phase-2 SQLite/cloud-store guarantee, not the in-memory reference. Scheduled → 2.6.H. (low · checkpoint.ts/engine.ts; Phase-2 store)
Found while fixing 2.6.C's F1 (the
/modelsreseat blanking the alt-screen viewport). Deliberately scoped OUT of that fix, and recorded here so it is tracked work rather than a discovery that gets lost.
relavium chat-resume <sessionId> restores the model's context from history.db, but opens on an empty
viewport — the prior conversation is never repainted. This is not the 2.6.F regression F1 is: it has been
true in every mode and every version, because nothing anywhere projects session_messages into rendered
TranscriptEntrys — that projection has simply never been written. Inline and alt-screen behave identically.
It shares F1's root (SessionViewSeed.transcript, now the seam 2.6.C adds) but not its cause, and fixing it
needs machinery F1 does not:
- a DB →
TranscriptEntryprojection (session_messagesrows → the{role, text}/{role:'assistant', text, summary}union), including what to do with rows a/compactor/trimdropped; - a decision on whether the inline renderer should repaint history on resume too — which would be a genuine behaviour change, not a regression fix (today it prints nothing, and that is consistent).
Why it was not folded into 2.6.C: F1 is a regression on the just-shipped default surface and ships as a hotfix; this
is a long-standing UX gap that would have added a DB read path and an inline behaviour change to a view-only fix.
The seam it needs (SessionViewSeed.transcript + carriesSeedTranscript) is already in place after 2.6.C Step 2,
so the remaining work is the projection and the inline decision.
Scheduled → 2.6.G (the /agents > Sessions browser owns resume-in-place). The earlier "2.6.C's natural sibling or 2.6.G, whichever reaches it first" hedge is spent: 2.6.C shipped 2026-07-13 without it.
Raised while investigating a "
/modelsreseat forgets tool calls" report (2.6.C / PR #75). The investigation's findings are restated in full below — every claim carries its ownfile:line, so this entry stands alone. Recorded here — deliberately not actioned — pending a maintainer call on sequencing and risk.
The premise behind the original report is wrong: the engine drops tool_use/tool_result pairs at every
turn boundary, model switch or not — a deliberate design cut (ADR-0062 §6, deferred to Phase 3 by ADR-0059).
This is the same gap already tracked below as "Faithful cross-turn transcript (tool + reasoning history)"
in the AgentSession (1.V) follow-ups section. This entry adds one new idea surfaced during the analysis —
gating the eventual fix behind a default-off config toggle — plus the risks that come with it.
The idea: ship cross-turn tool-call carry behind a default-off [chat] config toggle (e.g.
carry_tool_history), the same opt-in-then-flip pattern ADR-0068 already established for the mouse-wheel
default. Note the toggle really only has one meaningful axis, not several: reasoning/signature can never
be carried regardless of any toggle (ADR-0030 — a structural, cryptographic replay boundary, not a
preference), and @-mention file content already carries correctly today (fixed in 8ba7737). So "which
data types carry across a model switch" reduces to a single boolean, not a multi-way settings panel.
What the toggle would gate (confirmed touchpoints — no schema migration needed):
DurableContentPart(packages/shared/src/content.ts:626-660) already hastool_call/tool_resultarms;session_messages.content_parts(packages/db/src/schema.ts:475) already round-trips them. Thetool_calls/tool_call_idcolumns are write-only denormalized metadata never read back (packages/db/src/session-store.tsfromSessionMessageRow) — not a blocker.- The persister (
apps/cli/src/chat/persister.tsappendText, lines 118-131) writes text only; it never touchesagent:tool_call/agent:tool_resultevents. reconstructSessionState(packages/core/src/engine/session-resume.ts:110-141,textOf43-48) flattens every non-text part to text on replay — would need to splice paired assistant/toolLlmMessages back in instead.agent-session.tsbuilds#messagestext-only today (declaration :363, pushes at :548/:575-576) with an explicit deferral comment at :567-574 — this is the actual splice point.
Why the toggle doesn't remove the prerequisite risk. The three 🔴 gaps the analysis found are independent, already-live bugs today, not side effects of tool-carry:
- No pre-egress context-window check —
#maybeAutoCompact(agent-session.ts:619) runs after the request already went out; nothing checks the outbound request against the window before sending. - Overflow kills the turn outright — a 400 (
bad_request,packages/llm/src/llm-error.ts:43) isn't inRETRYABLE_KINDS(:15-20), so FallbackChain never engages; there's nocontext_overflowkind to catch it. - The budget gate only prices output tokens —
estimateMaxNextCost(packages/llm/src/budget-estimator.ts:16-26) ignores a growing input history entirely.
A default-off toggle narrows who hits these (opt-in users only), not whether they're real. And the opt-in population — power users, and especially the future VS Code coding-assistant surface where remembering a file a tool read 3 turns back is the actual point — is exactly the segment most likely to run long sessions, and therefore most likely to hit an overflow. Shipping the toggle before the three fixes trades a vague "model forgot" complaint for a harder "turn died mid-conversation with no recovery" complaint, for the very users who opted in.
Sequencing implication: fix the three prerequisite bugs first (independent value regardless of tool-carry); design tool-carry from the start behind the default-off toggle so it can follow immediately after, rather than waiting for a strictly separate later window. A new ADR is required either way — one that supersedes ADR-0062 §6 (ADR-0059 needs no change; it already says "deferred to Phase 3").
Also open: the toggle's discoverability depends on a /settings surface, which doesn't exist yet (no
workstream scaffolds it today). Don't advertise "change this in /settings" in the model-switch notice
(apps/cli/src/chat/repl-info.ts modelSwitchNotice, lines 171-177) until that surface is real — pending a
maintainer call on whether to point at a config.toml key in the interim, or say nothing until /settings
ships.
Home: Phase 3 (or a later Phase-2.6 workstream) — natural sibling to the AgentSession (1.V) "Faithful cross-turn transcript" item just below; whichever lands first should absorb the other.
Raised while answering a question about editing the agent used by a Home-opened chat. The headline gap is already tracked at phase-2.6-conversational-authoring.md §2.6.G — recorded here anyway because the investigation surfaced a narrower residual that 2.6.G's current task description does not clearly cover, so it doesn't get lost when that workstream starts.
Confirmed today: apps/cli/src/home/drive-home.tsx:510 launches every Home chat with
agentRef: undefined — i.e. always the built-in buildDefaultChatAgent (apps/cli/src/chat/default-agent.ts:
fixed system prompt, a 3-tool read-only grant, no temperature). There is no picker, no --agent-equivalent
affordance, and none of the registered chat slash commands
(apps/cli/src/commands/repl-commands.ts — help/exit/cancel/export/workflows/cost/doctor/mode/effort/
thinking/compact/trim/clear/models/scrollback/edit/copy) shows or edits the bound agent's full config. The
only way to bind a custom .agent.yaml today is relavium chat --agent <ref> from a shell — a path Home
never takes.
The "pick a different agent" half is already scheduled — do not re-file it. 2.6.G's task list says, almost
verbatim: "/agents browser (Home + chat): tabs Defined | Sessions. Defined: the agent catalog with
'start a chat with this agent' (closing the Home's built-in-agent-only gap)." That is this exact gap, already
named and owned.
The narrower residual 2.6.G's current description does not clearly cover: a read-only view of the
full config of the agent already bound to the current session — system prompt, tool grant, temperature,
reasoning_effort, provider — not just picking a different agent for a new chat. 2.6.G's Sessions tab is
described only as "recent + in-progress sessions... a detail view (transcript summary, cost, model
attribution)" — model attribution (which model/provider) is there, but system prompt / tools / temperature /
reasoning_effort are not mentioned. Today there is nowhere at all — Home or the terminal chat command — to
inspect that for a session already in flight.
This should stay read-only, not become an edit surface. resolveChatAgent's doc comment is explicit that
one agent binds a session for its whole lifetime by design — "ADR-0024 — one agent per session, no
mid-session switching." So the fix here is a view, not a live-mutate command: "show me what I'm actually
talking to" (worth having even by itself, e.g. after a /clear reseat or a --agent invocation where the
resolved config isn't obvious), not "let me tweak the running agent." Changing agents mid-session is already
correctly out of scope by design, and 2.6.G's "start a chat with this agent" (a new session) is the
sanctioned way to switch.
Home: 2.6.G — when that workstream is scoped, confirm the Sessions-tab detail view (or a lightweight
/agent / /whoami-style command available in both Home and the terminal chat command) explicitly
includes system prompt, tool grant, temperature, and reasoning_effort for the current session, not only
model/provider/cost. If it's deliberately left out, that should be a stated decision, not a silent gap.
2026-06-16 — 1.V
AgentSession(ADR-0024) + 1.AC budget governor (ADR-0028) merged in PR #26 (after two pre-merge review passes + a Sonnet multi-dimensional review). The in-memoryAgentSessionentry point landed — multi-turnstart/sendMessage/cancelover the shared turn core (runAgentTurn), the hard turn cap →turn_limit, session-wide cost, emission via an injectedSessionEventSink. The deferrals below were decided while building it; each has a clear later home, recorded so it isn't lost. The still-open follow-ons are 1.W (wire theSessionEventSinkonto theRunEventBus+ per-sessionsequenceNumber/SessionHandle), 1.X (session persistence + the durableSessionMessageschema), resume 1.Y, export 1.Z, and the deferred cost-event persistence (below) — those are workstreams, tracked in phase-1-engine-and-llm.md, not deferred items.
- Faithful cross-turn transcript (tool + reasoning history) → 1.X/1.Z. 1.V appends only the final
assistant text across turns: the turn core keeps the within-turn
tool_use/tool_resultpairs internal (so the transcript carries no orphanedtool_useand stays protocol-valid), and reasoning is dropped (asignatureis a within-turn same-provider replay token — ADR-0030/0039 — that must not span turns). Carrying the full per-turn tool/reasoning history needs the turn core to expose its intermediate messages (runAgentTurncopies its input and returns only final content) — revisit when 1.X persistence / 1.Z export needs faithful turns, onceagent-turn.tsis settled. (medium · packages/core/src/engine/agent-session.ts + agent-turn.ts; 1.X/1.Z) - Session budget pause/resume (1.V × 1.AC).
AgentSessionthreads the ADR-0028preEgresshook as a pass-through but does not handle aBudgetPauseError: a non-AgentTurnErrorthrow rolls the user message back and re-raises (a session has no pause/resume gate machinery in 1.V). The run path maps a budget pause to apausednode outcome via the human-gate seam; a budgeted session needs the analogous suspend/resume lifecycle. Wire it when sessions gain a budget (surface phases). Scheduled → 2.6.K (with the EA4-ride sibling below). (medium · packages/core/src/engine/agent-session.ts; ADR-0028) - Per-session tool narrowing (ADR-0029 narrow-only). 1.V grants the bound agent's
toolsverbatim; a session cannot yet narrow them per-session (it may only ever narrow, never widen). Add a session-level narrow when a surface needs to restrict a session's tools below the agent's grant. (low · packages/core/src/engine/agent-session.ts; ADR-0029) -
[chat].max_turnssurface wiring — RESOLVED in 2.5.G S11 (PR #66). The hard turn cap is an engine-API knob in 1.V (SessionDeps.maxTurns, finite default 50); the CLI now maps the[chat].max_turnsconfig default ontoSessionDeps.maxTurns(config/resolve.tsmax_turns→maxTurns, threaded throughsession-host.ts, enforced by asession-host.test.tspin). (config-spec.md + surfaces) - Session
output_schema. 1.V ignoresagent.output_schema(a chat session is free-form text); structured output stays a workflow concern. If a session ever needs it, lower it toresponseFormat+ validate node-side (as the AgentRunner does for anagentnode). (low · packages/core/src/engine/agent-session.ts) - Session
{{ctx.*}}prompt interpolation (surfaced by 2.Qagent run --input).AgentSession.#runTurnpasses the agent'ssystem_promptverbatim — it does NOTresolveTemplateit against#context.variablesthe way the workflowAgentRunnerinterpolates anagentnode's prompt. Sorelavium agent run --input k=v(2.Q) carries the variables inSessionContext(visible onsession:started) but a{{ctx.k}}placeholder in the agent's prompt is sent to the model literally. Wire aresolveTemplatepass over the session prompt against aRunScopebuilt fromcontext.variables. Governed by ADR-0060 (Proposed, Phase-2.6 / workstream 2.6.D) — it is NOT a plainresolveTemplatereuse: the safe implementation requires a new per-variable provenance/taint marker onSessionContext(today a flat record) so--input-derived (untrusted) values can never reach thesystemposition, plus the ADR's mandatory security review of the session-prompt taint path before Accept. 2.5-close decision (2026-07-08): the phase-2.5 close-plan (Step 13, Batch C) proposed landing this here; the maintainer chose to DEFER to Phase 2.6 / 2.6.D — where ADR-0060 is finalized (Proposed→Accepted) with its taint-provenance marker + security review — rather than pull an unaccepted, security-critical Phase-2.6 ADR forward into the consolidation close-out. Only the siblingAgentParseErrorline/col half of Step 13 landed. (medium · packages/core/src/engine/agent-session.ts; ADR-0060)
2026-07-03 2.5.D (ADR-0061, Accepted). The
@-mention file injection (dir-navigable completion + fs-jailed reader + nonce-fenced, size/line-bounded untrusted injection) shipped. These bounded pieces were deliberately deferred (each is additive; the confidentiality floor + jail + injection framing hold without them):
- Advisory
.gitignore/.relaviumignorecompletion trim — DONE (2.5-close Step 15, Batch E; PR #69). A dependency-free, ReDoS-safe in-house matcher (gitignore.ts) folds the workspace-root.gitignore+.relaviumignoreinto the@-mention candidate filter (comments/blanks,!negation, dir-only/, anchoring,*/**/?globs; a LINEAR two-pointer glob matcher — no regex, so no backtracking/ReDoS on a crafted pattern), complementing the fixedNOISE_DIRSset. A UX/privacy nicety, NOT a security control — the confidentiality floor + listing-gate remain the authoritative fs-capability enforcement. Documented subset limits: nested per-dir ignore files +[a-z]char classes deferred (they only UNDER-hide, never a security gap). (apps/cli/src/render/tui/gitignore.ts + mention.ts; ADR-0061) -
@-glob / directory expansion. Single-file injection ships;@src/**/*.tsglob / whole-directory expansion is deferred (ADR-0061). Scheduled → 2.6.E. (low · apps/cli/src/render/tui/mention.ts) -
@-mention of a binary / media file. The reader fail-closes on a binary file (parity withread_file); a durable media-handle injection path (ADR-0031) is a follow-up. (low · apps/cli/src/render/tui/mention.ts) - Strip Unicode bidi/format controls at the shared display boundary — DONE (2.5-close Step 14, D-5; PR #69). The
shared
stripTerminalControls(chat-projection.ts) now strips the Trojan-Source reordering family (U+202A–202E, U+2066–2069, LRM/RLM/ALM) at every display boundary; ZWJ/ZWNJ preserved; the source uses\uescapes (no literal bidi bytes). (apps/cli/src/render/tui/chat-projection.ts)
2026-07-02 2.5.E (ADR-0057, Accepted). The reseat-less mode system + per-tool approval + mid-turn abort + the host arms shipped. These bounded pieces were deliberately deferred (each is additive, none blocks the mode system's security guarantees):
-
[c]reject-with-typed-reason approval prompt — DONE (2.5-close Step 14, D-1; PR #69). A[c]at the approval prompt opens a keyboard-owning reason-input sub-mode (bothrelavium chat+ the Home); on submit it rejects with the sanitized + 300-char-bounded reason via the existingToolApprovalDecision.reject.reasonseam. The floor is unchanged. (apps/cli/src/render/tui/chat-input.ts + chat-ink.tsx + home-controller.ts) - Conversationally recover from a SCOPE denial in chat — DONE (2.5-close Step 14, D-3; PR #69). The
recoverableflag moved to the baseToolDispatchError; exactly twotool_denieds opt in —ToolPolicyError('media_scope_denied')and the fs pure scope-tier escape (FsScopeDeniedErrorfromassertInScope) — so on therecoverToolFailuressurfaces the model is fed the denial and adapts to an in-bounds path. The confidentiality / protected-path / symlink / SSRF / user / guardrail denials stay FATAL. (packages/core/src/tools/errors.ts; apps/cli/src/engine/tool-host/fs.ts; ADR-0057) - Plain / non-TTY non-interactive approval policy — DONE (2.5.E "High 9" + consolidated in 2.5-close Step 14,
D-2; PR #69). A non-interactive driver (plain non-TTY /
--json/ one-shotagent run) uses the one canonicalnonInteractiveApprovalPrompt— every governed dispatch is DENIED (never a hang, never an auto-approve). (apps/cli/src/chat/chat-mode.ts; apps/cli/src/commands/chat.ts + agent-run.ts) - Approval-consent-line zero-width hardening (2.5-close Step 14 security-review, optional). The shared render
floor strips the Trojan-Source REORDERING controls everywhere (the CVE-2021-42574 vector is closed), but the
highest-trust surface — the approval consent line's target (
formatApprovalTarget→sanitizeInline) — still passes NON-reordering zero-width chars (ZWSP U+200B / word-joiner U+2060 / BOM U+FEFF), which the provider-URL echo already strips. Running the stricter zero-width superset on the consent-line target would be defense-in-depth. Not a CVE gap (those chars cannot reorder a command to masquerade). Scheduled → 2.6.M (with tool-render v2). (low · apps/cli/src/render/tui/chat-projection.ts) - Extract the
[c]reason-capture to a shared pure reducer (2.5-close Step 14 review, test-parity). The reason-capture keystroke glue is duplicated inline inChatApp(chat-ink.tsx) and the Home controller; the shared primitives (reduceApprovalKey,sanitizeApprovalReason,reduceEditorMotion) are unit-tested and the Home path has an integration test, but the ChatApp inline copy has no direct test (matching the existing no-ChatApp-integration boundary). Extracting the capture step to one pure reducer (like the mention/effort submodes) would let both surfaces test the same function. Scheduled → 2.6.M (with tool-render v2). (low · apps/cli/src/render/tui/chat-ink.tsx + home-controller.ts) - Live
web_search/http_requestegress credential resolver.assembleToolEnvaccepts anegressCredentialResolverand the egress arm attaches it host-side as a Bearer, but the chat/Home session-host does not yet wire it to the keychain — so aweb_searchneeding a provider key currently 401s (surfaced, never a crash). Wire the provider-key resolver through when the chat surface needs authenticated egress. Scheduled → 2.6.M (theweb_searchactivation). (low · apps/cli/src/chat/session-host.ts) - Session-level budget pause/resume (rides the EA4 machine). The EA4 pause/resume state landed for mid-turn
abort + approval; the ADR-0028 session budget
pause_for_approvalcan now ride the same machine (today a chat cost-cap trip settles the turn loudly asbudget_exceeded— the REPL is the approval gate). See also the 1.V session-budget follow-up above. Scheduled → 2.6.K. (medium · apps/cli/src/chat + agent-session.ts) -
relavium budget resumeCLI command (2.5-close Step 15 / Batch E — DEFERRED to a focused follow-up). The engine ALREADY supports resuming a budget-paused run (engine.resume(runId, budgetGateId, decision), budget-governor.ts / checkpoint.tsisBudgetGate), andrelavium gatedeliberately EXCLUDES budget gates (selectGatefilters!isBudgetGate), naming this the "budget resumesurface." The remaining work is the documented CLI command — a new manifest entry + dispatch handler + a command core that ~90% overlapsgate.ts's resume machinery (so the clean form extracts a shared resume core rather than duplicating). Low, dependency-free. Why deferred (maintainer call, 2026-07-08): it modifies the security-sensitivegate.tscross-process resume path and is coupled to the secret-re-provide follow-up below (both refactor that path), so both are best landed together with fresh context rather than at the tail of the 2.5-close session. Scheduled → 2.6.K (that focused follow-up). (low · apps/cli/src/commands/{gate,budget}.ts + manifest.ts + dispatch.ts; ADR-0028) -
project-tierextraRootsallowlist (carried from 2.5.A). Theprojectfs tier behaves as workspace-only until the path-allowlist lands (it can only NARROW the jail, never open a hole). Scheduled → 2.6.M (the[chat].extra_rootsconfig key is the missing source). (low · apps/cli/src/engine/tool-host/assemble.ts) - fs hard-link aliasing — the pnpm virtual-store read exemption (accepted residual, ADR-0057 review record).
The hard-link aliasing READ guard (
st.nlink > 1⇒ refused) is disabled ONLY for pnpm'snode_modules/.pnpm/…virtual store (isPnpmStorePath), so dependency-source reads work on Linux (where pnpm hard-links). The bounded residual: a compromised dependency (a malicious postinstall, or a hard-link path-traversal in the extractor — the node-tar CVE class) could plant a cross-boundary hard link UNDERnode_modules/.pnpm/that a later read would follow; the same actor already has local RCE, and the sensitive-read floor still refuses a NAMED secret store even there. A future opt-out (allow_aliased_readsconfig, or resolving the inode's other name against a tool-known pnpm store root) would let a stricter deployment disable even this. (low · apps/cli/src/engine/tool-host/fs.ts) - Target-scoped approval cache + a per-tool preview target (ADR-0057 review elevation). The once/always
ApprovalCacheis keyed by tool id only, so an[a]lwaysgrant blankets every allowlisted target of that tool for the session (awrite_filealways covers every non-protected path; anhttp_requestalways covers everyallowedDomainshost). It is bounded (enforcePolicy's allowlists + the fs protected-paths floor still gate each dispatch) and is the documented accept-edits semantics — but keying by(toolId, target)(a path prefix for fs, a host for egress, a server for mcp) would makealwaystrack what the user actually reviewed. Pairs with: surface the MCP server/tool inToolActionPreview(todaymcp_call/web_searchreturn a BLANK preview, so F3 correctly forbids caching theiralways) — a structured{mcpServer,mcpTool}preview would turn the blank-check downgrade into a real, reviewable, cacheable per-server grant. Scheduled → 2.6.M. (medium · apps/cli/src/chat/chat-mode.ts + packages/core/src/tools/{types,registry,builtins}.ts + run-event.ts) - fs
.relaviumsensitive-read/write segment vs. the~/.relavium/tmpsandboxed root (latent). Both the read floor (SENSITIVE_READ_DIR_SEGMENTS) and the write floor (PROTECTED_DIR_SEGMENTS) match a.relaviumsegment anywhere, so they would refuse the sanctionedtmpDirscratch root — inert today (no call site wirestmpDir). Resolve (home-anchored match, or exclude the wired tmp root) before any caller passestmpDir. Scheduled → 2.6.M / 2.6.N — promoted from latent to a prerequisite: 2.6.N's central ephemeral artifact root (~/.relavium/artifacts/<sessionId>/) needs tool read/write into a~/.relavium/subdir, so the home-anchored fix (protect the secrets-bearing root + project.relavium/, exclude the wired sanctioned scratch subroot) must land with theextra_roots/tmpDirwiring. (low→prereq · apps/cli/src/engine/tool-host/fs.ts + assemble.ts)
2026-06-22 2.D (
relavium run) implementation. The CLI was wired to@relavium/core— the first real engine consumer. The planned scope-splits it leans on (richinkTUI → 2.E; finalized--jsonenvelope → 2.F; interactive gate prompt +relavium gateresume → 2.G; durable run history → 2.H; provider keys from the OS keychain → 2.C) are tracked as their own workstreams in phases/phase-2-cli.md and summarized in the### relavium runImplementation status note in ../reference/cli/commands.md, so they are not duplicated here. The one item below is an unscheduled security follow-up with no numbered workstream yet.
- CLI
ToolHost— the built-in host capabilities are wired for the CLI; the DESKTOP surface remains. 2.D originally built the tool registry with an emptyToolHost(createToolRegistry({ tools: BUILTIN_TOOLS, host: {} })). For the CLI this is now closed: 2.5.A wired thefs+processarms and 2.5.E wired theegress+osarms viaassembleToolEnv, all behind the fail-closed approval floor (see the RESOLVED block below, ADR-0055 + ADR-0057). What remains: the desktop/Tauri surface has no tool host wired yet (Phase 3), and its host-sideEgressCapability.fetchSSRF hardening tracks with the SSRF item above. Until the desktop host is wired, a desktop workflow calling a capability-backed built-in tool surfaces a clean "tool unavailable" failure, never a half-implemented or unsafe execution. (medium · desktop tool host; Phase 3; security-review.md; egress → the SSRF item above) - VS Code docs claim a SQLCipher
history.db"same store as CLI" — contradicts ADR-0050. The Phase-4 VS Code docs (reference/vscode/extension-api.md L46/67, phases/phase-4-vscode.md L376/378) still carry the pre-ADR-0050 assumption that the extension host opens the SQLCipherhistory.dbthat is "the same store as CLI/desktop". Per ADR-0050 the CLI store isbetter-sqlite3unencrypted and a SQLCipher file cannot be the same physical file; there is no cross-surface shared session/run store until a Phase-3/4 ADR reconciles it. Reword the VS Code at-rest posture once the cross-host physical-store decision lands. (low · Phase 4 forward-design docs; blocked on the cross-host store decision; surfaced during the 2.5.J encrypted-wording sweep) - Run-resume reconstruction reads (
loadRun+loadRunEvents+loadStepExecutions) are separate reads at the caller level — the same torn-read class 2.5.I S1 fixed for sessionloadFull. A concurrent writer committing between them could yield a run row + event/step reads from different snapshots. Lower impact than the session case: run history is event-sourced and the checkpoint fold tolerates partial state, so a torn read self-heals on the next fold. If tightened, wrap the caller-level reconstruction in one read transaction (asloadFullnow does). Scheduled → 2.6.H. (low · packages/db run-history-store consumers + the resume caller; surfaced during 2.5.I S1 review) - The CLI chat persister writes a turn non-atomically — messages then session totals in separate
auto-committed statements.
apps/cli/src/chat/persister.tsappends the user + assistant messages and thenupdateSessions the running totals as separate writes, so the DB legitimately passes through a state where a turn's messages are present but its totals are stale.sessionStore.loadFull's read transaction (2.5.I S1) guarantees snapshot consistency (both reads see one DB snapshot) but not turn atomicity — a snapshot can observe messages ahead of their totals. To make "totals always match the returned messages" hold, wrap each turn's message-appends +updateSessionin one host-sidedb.transaction(BEGIN IMMEDIATE). Bounded, host-side. Scheduled → 2.6.H. (low · apps/cli/src/chat/persister.ts; surfaced during 2.5.I S2 review) -
relavium runmaps anyrun:pausedto exit 3 (gate-paused); revisit when media host-wiring lands.run.tsreturnsEXIT_CODES.gatePaused(3) for anyrun:paused, which is correct in 2.D because a human gate is the onlyrun:pausedsource (nomediaStore/media-job host is wired, so a media-only park — a validrun:pausedcarryingpendingMediaJobNodeIdsand no gates, perRunPausedEventSchema/ 1.AG §D — can never be emitted). When the media host capability lands (the same surface as the deferred media-egress work, ~2.S), a media-only park would be reported as "gate-paused" with no gate; at that point decide whether exit 3 (and the rendered message) should distinguish a gate park from a media park. Scheduled → 2.6.K. (low · apps/cli/src/commands/run.ts; media host-wiring / 2.S) -
relavium budget resume <runId> [--approve | --abort]is documented but has no numbered workstream. commands.md (canonical) specifies it as the non-interactive operator path for a run suspended at a budget cap (budget:paused,on_exceed: pause_for_approval— ADR-0028), but no Phase-2 workstream implements it. It reuses 2.G's cross-process resume substrate (a budget pause resolves through the same checkpoint reload- resume path as a human gate, behind a budget-specific command + flags), so it is a small follow-up once 2.G lands — candidate home: alongside 2.I, or its own short workstream. Deliberately out of 2.G (a distinct ADR-0028 surface, not in 2.G's acceptance). Scheduled → 2.6.K (single tracking point: the Batch-E entry above). (low · apps/cli/src/commands/; ADR-0028)
- Re-provide
secret-typed inputs on cross-process resume. The durablerun:started.inputsare masked (asecretinput is persisted as{ secret: true, ref }, never plaintext — ADR-0006/0036), so a fresh-processrelavium gateresume cannot restore the real value. 2.G fails closed (exit 2) when a restored input is aMaskedSecret(assertNoMaskedSecretInputs, gate.ts) rather than resume with a broken value. The proper fix lets the operator re-supply the secret on resume (e.g.relavium gate <runId> --secret token=…read from stdin likeprovider set-key, or a keychain/env re-resolution keyed by the inputref) so a secret-bearing run becomes resumable. Until then the fail-closed + the commands.md note stand. 2.5-close Step 15 / Batch E status (maintainer call, 2026-07-08): selected IN by D8 but DEFERRED to a focused follow-up — this RELAXES a fail-closed security guarantee (allow-with-re-provisioning), demands the stdin-not-argv secret discipline (provider set-keypattern)- a mandatory security-review pass, and is coupled to the
budget resumecommand above (both refactor thegate.tsresume path). Best landed together with fresh context, not at the tail of the 2.5-close session. Scheduled → 2.6.K (that focused follow-up). (medium · apps/cli/src/commands/gate.ts; ADR-0006)
- a mandatory security-review pass, and is coupled to the
The PR #48 review (7-dimension multi-agent pass) returned 0 blocker / 0 major; the fix-now items (the catalog special-file guard + the doc/contract corrections) and the cheap test/code nits landed in the PR. The items below were verified-real but graded fix-followup — none blocks merge.
- History-read query indexes — DONE (2.5.B, migration 0005). The
(created_at DESC, id DESC) WHERE deleted_at IS NULLpartial index landed on bothruns(idx_runs_created) andagent_sessions(idx_agent_sessions_updated), solistRuns/listActiveRuns/listSessionsorder off the index instead of aUSE TEMP B-TREEfilesort, and the 2.5.B Home reads bound to an indexed top-N ({ limit }onlistRuns/listSessions). Still open (cloud-scale only): offset/cursor pagination for the read commands (relavium list/loadLatestRunPerWorkflowstill return the full set) — genuinely unneeded at single-user CLI scale; add a cursor API before the desktop/cloud surfaces drive these reads at volume. (low → scale · packages/db/src/run-history-store.ts; database-schema.md) -
AgentParseErrorline/column — DONE (2.5-close Step 13,fix(core); PR #69).agentSyntaxErrorFrom(the agent sibling of parser.ts'ssyntaxErrorFrom) now threadsLineCounterpositions into a positionedYAMLParseError: optional 1-basedline/columnfields (parity withWorkflowSyntaxError) plus a folded(source — line L, column C)locator in the message so the position reaches every.message-surfacing consumer. The echoed YAML rule is secret-free (prettyErrors: false; verified across a 14-case + 12-case adversarial sweep — no authored key/value ever rides the message). (packages/core/src/agent-parser.ts) -
AgentParseErrordiagnostics are invisible on thechat --agent/agent runsurfaces (surfaced by the 2.5-close Step 13 Sonnet review, 2026-07-08).resolveChatAgent(agent-source.ts) deliberately surfaces the RAWAgentParseError— pinned byagent-source.test.ts"surfaces an invalid .agent.yaml as a field-named AgentParseError (not a silent default or CliError)" — and neitherbuildChatSessionnoragent-run.tscatches it, so a malformed-but-existing.agent.yamlreaches the top-levelrenderError/toUserFacingand is reduced to exit 1 + the generic "An unexpected internal error occurred" (human AND--json). So the field-named, position-enriched diagnostic Step 13 built only actually reaches a user vialist --agents/create/import(catalog.ts/authoring.ts, which DO catch it). Pre-existing (predates Step 13; the diff only changed message CONTENT, not this catch-layer gap) and it conflicts with the deliberateisCliError === falsetest above, so re-tagging is a cross-surface DESIGN decision, not a mechanical fix — deferred out of thefix(core)close-out. Fix options (need a maintainer call on which): (a) wrap theAgentParseErrorinto aCliError('invalid_invocation', err.message, { cause })atresolveChatAgent/ theagent-run/chatcallers (revising the pinned test), or (b) teach the top-levelrenderError/toUserFacingto render a typedAgentParseErroras an exit-2 invocation fault. Also relativize thesourcelabel at thechat/agent runcall site (agent-source.tspasses the ABSOLUTEsource.path, unlike the catalog's workspace-relativerel; no secret leak — it is the user-typed path — but it contradicts the parser docstring's "workspace-relative"). Scheduled → 2.6.B (the conversational-authoring loop needs these diagnostics visible). (medium · apps/cli/src/chat/agent-source.ts + session-host.ts + commands/agent-run.ts + run.ts) - Residual read-command test pins. A few low-risk coverage gaps remain after the PR's test additions:
pendingHumanGateswithexpiresAtpresent and with multiple simultaneous gates;list --jsonno-project stderr + invalid-entryerrormachine-contract; thestatus"no node activity" fallback. The production code is correct (verified); these are operator-surface pins. (low · apps/cli/src/**/.test.ts; testing.md)* - Phase-doc structural views still omit
gate list. The §2.I heading + acceptance now namegate list, but the two Mermaid graphs and the from-scratch wave/dependency tables describe the original plan node and were left unchanged (they are the plan, not a live tracker). Foldgate listin if those diagrams are ever regenerated. (nit · docs/roadmap/phases/phase-2-cli.md)
2026-06-28 2.5.A (ADR-0055). The shared CLI tool-environment factory landed the
fs+processToolHostarms behind oneassembleToolEnv({ profile, fsScopeTier, workspaceDir })seam, the advertise-filter, thetool_unavailable(EA1) fail-closed backstop, and real failed-turn usage (EA2). This directly advanced the 2.D "CLIToolHostis fail-closed" item above. Thefs/processhalves were wired + security-reviewed in 2.5.A; theegressandoshalves + the write-capable chat tier landed in 2.5.E (ADR-0057) behind the per-tool approval floor (see the resolved items below + the Phase 2.5.E follow-ups section). The 2.5.A items were confirmed by the PR #60 review passes and deliberately not taken in-PR — none blocked the milestone.
-
egress+oshost arms wired (governed) — RESOLVED in 2.5.E. Landed in 2.5.E (ADR-0057, PR #63, merged 2026-07-03):apps/cli/src/engine/tool-host/egress.ts(over the sharedconnectValidatedconnect-by-validated-IP mechanism, Host/:authority-header-strip) +os.ts, wired by thechat-read-writefactory profile as governed classes on the fail-closed approval floor (denied inask, prompt inaccept-edits). (apps/cli/src/engine/tool-host/; security-review.md) - Project-tier path-allowlist (
extraRoots) not yet passed by the factory. Theprojecttier therefore behaves as workspace-only (it can only narrow the jail, never open a hole —project==sandboxed-minus-tmp). It did not land in 2.5.E — carried forward under the Phase 2.5.E follow-ups entry above (single tracking point). (low · apps/cli/src/engine/tool-host/assemble.ts; ADR-0057; built-in-tools.md fs-tier note) - Write-capable chat — RESOLVED in 2.5.E. Landed in 2.5.E (ADR-0057, PR #63, merged 2026-07-03): the
relavium chatdefault profile is now the full-capabilitychat-read-writehost —write_fileis wired and gated by the per-tool approval floor (denied in the defaultaskmode astool_denied, nottool_unavailable); a declaredfulltier is still clamped toprojectfor the chat surface (an unjailed read exfiltrates~/.ssh/~/.aws, a write-capable chat shares that risk). (apps/cli/src/chat/session-host.ts; ADR-0057) - Profile-unaware advertise-filter — DONE (2.5-close Step 15, Batch E; PR #69).
wiredToolIdsnow takes a{ readOnly }option: on a read-only host a WRITE-class (fsWrite) tool is no longer advertised (itsfsarm is wired but always denies the write — an always-denied advertisement), the advertise-side complement to the read-only fs arm's dispatch refusal. The default (read-write) is unchanged, so the livechat-read-writesession host is inert. (apps/cli/src/engine/tool-host/assemble.ts) - Residual fs TOCTOU on the PARENT directory (no
openatin Node). The read path (readJailedFile) and the write paths (append + temp/rename) all open the FINAL component withO_NOFOLLOW, so a final-component symlink swapped in after the jail'srealpathfails closed. The unclosable residual is a swap of a PARENT directory component to an out-of-jail symlink between therealpathand the open — Node exposes noopenat/openat2(nor LinuxRESOLVE_BENEATH) to pin the parent by fd, so a pure-path open re-walks the (possibly swapped) parents. The window is narrowed to the gap betweenjailExisting/jailWriteTarget'srealpathand the immediately-following open. Additionally,O_NOFOLLOWis0on Windows, so even the final-component guard there rests on the pre-oplstatalone (the non-race case). Reads are bounded, and the write arm now serves the approval-gatedchat-read-writechat as well as the author-trusted workflow-run path; the ADR-0057 mandatory security review explicitly re-assessed and accepted this Windows-only parent-swap residual for the write-capable surface (the protected-paths refusal + fs jail still hold). Close the gap with a nativeopenat-based helper (or a Rust-side resolver) if an untrusted-read surface raises the bar. (low · apps/cli/src/engine/tool-host/fs.ts) - Deliberate non-fixes from the PR #60 excellence review (recorded, not bugs). Each was weighed and
skipped with a reason: (a) no host-arm memoization (fs scope-checker, process base-env, exec cache) —
each would cache a security-relevant
realpathon an I/O-dominated cold path, defeating the per-call re-resolution that catches a mid-session symlink swap; (b) no tool-name prefix on the shared fs helper errors (jailExisting/assertInScope/lexicalTarget) — they back read/write/list, so a single prefix would needtoolNamethreading for marginal gain (write-only messages are prefixed); (c) the genericguarded()catch-all stays reason-only (the I3 boundary). Re-open only if a concrete need appears. (nit · apps/cli/src/engine/tool-host/) - Two transitively-covered test gaps — DONE (2.5-close Step 11, Batch A; PR #69). Both now have an
explicit pin: the chat-session-dispatches-
git_statuse2e was ADDED this round (session-host.test.ts — "dispatches git_status through the process arm end-to-end", asserting the tool_result folded back with a clean{"exitCode":0}); the failed-turn real-token persister fold was CONFIRMED already explicitly pinned (persister.test.ts — "flushes the running cost on a failed turn so a resumed budget governor sees the true spend"). (apps/cli/src/chat/session-host.test.ts + persister.test.ts; testing.md)
-
z.unknown()payload presence —agent:tool_call.toolInput,node:completed.output,human_gate:resumed.payloadvalidate even when absent. Decide presence per field (force the key via a.superRefinehasOwnProperty check, or document absence is OK) and add accept/reject tests. (minor · run-event.ts:64,93,124) Deferred 2026-06-07: the obvious per-member.superRefineis infeasible — these events are members ofz.discriminatedUnion, which rejects aZodEffectsmember; the correct fix adds thehasOwnPropertycheck to the existing outerRunEventSchema.superRefine(where the runId/sessionId cross-check already lives). Low value, left for the consumer that needs the guarantee.
- dist-resolution packaging test — the migration runner is tested only from
src/; add a smoke test that imports builtdist/index.jsand runsrunMigrations(the path consumers use). (minor · packages/db/src/client.test.ts) Deferred 2026-06-07: fragile as a Vitest unit test (it must importdist/which only exists afterturbo run build, so it would skip or fail depending on run order). Belongs in a dedicated post-build packaging-smoke step, not the unit suite.
- Local
format:checkvia turbo — CI now runsturbo run format:check; consider routing the rootformat:checknpm script through turbo too so local + CI share the cache. (minor · package.json:21) Deferred 2026-06-07: low-value cache nit that needs a task rename to avoid turbo recursion (//#format:checkis bound by name to theformat:checkscript); CI already runs through turbo, so only the local cache-share is missing. Not worth the rename churn now. - Enable the live-nightly conformance lane — the per-provider conformance suite runs in
fixture mode on every PR, but the scheduled live-API lane is still reserved/commented in
ci.yml(the "enable with the first provider adapter" TODO), and the adapters have now landed (PR #9, M1).1.Jaccepted M1 with the live lane explicitly pending keys; to actually exercise it, uncomment theschedule:lane and add the provider API keys (ANTHROPIC_API_KEY/OPENAI_API_KEY/GEMINI_API_KEY/DEEPSEEK_API_KEY) as CI secrets. Until then live coverage is a known gap. Decided (2026-06-21, maintainer): defer to Phase-2 workstream 2.K — enable the live-nightly lane together with the 2.K regression harness rather than as a pre-Phase-2 ops chore; fixture coverage holds the line until then. Update (2026-07-08): 2.K has shipped; this is now a Phase-2.6 in-window maintenance obligation (see phase-2.6 §"In-window maintenance obligations") — enable when CI provider keys are available. (minor → keys · ci.yml, packages/llm/src/conformance/.conformance.test.ts)* - Leakwatch secret-scanning CI gate — CI has no secret-scan step. The HodeTech standard
scanner is Leakwatch (never gitleaks); the blocking
ci.ymlstep is wired once a distribution path for the binary onto Actions runners exists (private release / action). Until then scanning runs locally with the installed binary, and test fixtures keep building any key-shaped strings viajoin()so no contiguous key literal ever sits in the tree. Exceptions, when the gate lands, are documented per finding — never blanket-ignored. (blocked → distribution path · ci.yml, security-review.md) - Dependency-bump cooling window — adopt a "no same-day upgrades" posture for runtime dependency bumps: a freshly published version waits a cooling period before entering the lockfile (supply-chain compromise of a new release is typically detected within days), with a documented security-exception path (a CVE fix may skip the window, recorded in the PR). pnpm 9 has no native knob for this; enforce as review policy now and revisit native enforcement (e.g. a minimum-release-age setting) when the toolchain moves to a pnpm major that has one. (policy now, tooling later · pnpm-workspace.yaml, architectural-principles.md)
2026-07-07 2.5.H (EA6 + live-turn feedback). The reasoning-event emit + the TUI reasoning/latency render shipped. The bounded pieces below were deliberately deferred (each additive, none blocks the feature):
- CLI render-layer (ink component) test harness. The CLI has no component-render test (no
ink-testing-library/react-test-renderer) by design — all logic lives in pure reducers/formatters that ARE unit-tested. But this leaves the React prop-plumbing / render-cadence layer untestable: the 2.5.H Home live-timer fix (threading the clock as anow: () => numberFUNCTION so the per-frameChatRegionre-reads it, vs a frozen number captured at the parentRootApp's render) is verified only by tracing + the type-shape guard, not by a regression test. Adding a harness is a first-of-its-kind test-architecture decision forapps/cli(a new devDependency; possibly an ADR) — track it, then a smoke test could tick a fake clock across twostore.tick()-driven renders and assert the displayed elapsed advances. Scheduled → 2.6.F (behind the full-screen renderer + harness ADR). *(low · apps/cli/src/render/tui/home-app.tsx- chat-ink.tsx; testing.md "every bug fix lands with a regression test")*
- Compact abort hint during token streaming — DONE (2.5-close Step 12, Batch B; PR #69). A pure
streamingAbortHint(busy)renders a compact dimEsc to stopline beneath the streaming CONTENT (a STATUS line already carries its inline hint), so the abort affordance persists for the whole turn (EA7). (apps/cli/src/render/tui/chat-projection.ts + chat-ink.tsx) - Bound the EXPANDED reasoning panel by rendered LINES, not just chars — DONE (2.5-close Step 12, Batch B; PR #69).
formatReasoningPanelnow tails the expanded body to the lastMAX_REASONING_PANEL_LINES(12) RENDERED rows (each logical line counts asceil(len/columns)wrapped rows; a single over-budget line is head-sliced), so a full 4000-char buffer cannot wrap into a flickering, screen-filling panel on a short terminal. The live width is threaded via acolumnsprop (ChatApp readsprocess.stdout?.columns; the Home passes its resize-trackedsize.cols). (apps/cli/src/render/tui/chat-projection.ts + chat-ink.tsx + home-app.tsx) - Allow
Ctrl+T//thinkingduring a pending approval — DONE (2.5-close Step 14, D-4; PR #69).reduceApprovalKeywhitelists exactly the view-only reasoning toggle (Ctrl-without-metat→toggle-reasoning, a pure store repaint with zero session/approval/decision effect) through the fail-closed swallow, so a user can expand the thinking to inform the decision; every other key (mode cycle, edits, the most-permissive approve/reject chord) stays swallowed. (apps/cli/src/render/tui/chat-input.ts)
The CLI e2e suite opens and MIGRATES the developer's real ~/.relavium/history.db (2.6.C spin-off, 2026-07-13)
DONE 2026-07-13 (PR #76 review fold).
regression.e2e.test.tsnow redirectsHOME/USERPROFILEto a freshmkdtempSyncdir in a suite-widebeforeEach(restored + removed inafterEach), so the full-shellrun(argv)case migrates a throwaway db, never the developer's real history. A guard in the same hook refuses to proceed if the redirect fails to take (resolveHomeDir({}) === realHome), so a future test cannot silently re-acquire the default path. The evidence and rationale below are kept for the record.
Found while diagnosing a red CI run during 2.6.C (PR #75). Verified, not inferred — see the evidence below.
apps/cli/src/harness/regression.e2e.test.ts drives the real CLI shell (run(argv('run', …, '--json'), io))
without pointing it at a database, so the run resolves the default path and opens
~/.relavium/history.db — the developer's actual chat history. It does not merely read it: it runs
migrations against it.
Evidence. Executing that one test file with HOME pointed at an empty directory creates
$HOME/.relavium/history.db with all 11 migrations applied. Under a real HOME those migrations land on
real data. (The sibling test at :315 does this correctly — mkdtempSync + an explicit dbPath — so the
isolation exists; this path just does not use it.)
Why this is worth fixing rather than tolerating. It is not a hypothetical: during 2.6.C the coupling
actively hid a bug from CI and converted it into damage to real data instead. A migration was re-cut
while in development, which changes its journal timestamp; drizzle replays such a migration, so
CREATE TABLE session_costs ran a second time against the table it had itself created. On CI this is
invisible — a fresh runner has no ~/.relavium/history.db, so nothing had been applied and nothing could
conflict. The failure surfaced only on the maintainer's machine, against a 3.3 MB database of real
sessions. A test suite that writes to real user data both damages it and blinds CI to the damage.
Fix: give the failing path the same isolation the sibling already has — a temp dir + an explicit db path — and, as a floor, make the e2e harness refuse to run against the default history path at all, so a future test cannot silently re-acquire it.
Home: a chore pass, or whichever workstream next touches the CLI harness.
2026-06-14 (PR #18 review). Verified Sonar findings in already-merged code (1.L/1.L2/1.T/0.x), outside the 1.O diff — kept out of the 1.O feature PR (a behaviour-preserving refactor of merged, tested code is its own change, not feature scope). Pick these up in a dedicated
chore: sonar cleanuppass. The 1.O-diff findings (thetryParseJsonfence regex → string ops, and the#nodeEmitduplicate cases → fallthrough) were fixed in PR #18; they are not listed here.
-
2026-07-29 Sonar sweep — ~30 findings in already-merged code, outside the Wave 0 diff. Recorded per this section's standing policy: a behaviour-preserving refactor of merged, tested code is its own change, not feature scope. Highest-value first:
chat-ink.tsx:1151cognitive complexity 91 → 15 (the same god-file 2.5.5.I declines to decompose — hand it to 2.6.M's render-v2),bounding.ts:165/:180two regexes at complexity 38 and 57,references.ts:154a regex with super-linear backtracking (the only finding here with a runtime-safety edge — it parses{{ }}filter arguments, so treat it as 2.5.5.A scope, not cosmetic),agent-session.ts:545complexity 17,create-prompter.ts:70a nested ternary, threeRunApp.tsxarray-index keys, two'never' is overriddenunion types, two.some()→.includes(),openai.ts:481nested template literals,node.ts:60String.raw, plus ~10 test-only nits (toHaveLength, parameterised tests,test.skip()). Not actioned, with reasons: the--ignore-scriptsfindings on all four workflows are declined —pnpm.onlyBuiltDependencies(rootpackage.json) already allowlists the only two packages permitted to run install scripts, and adding the flag would break both while weakening nothing else; theci.ymlreserved-lane TODO was deleted in Wave 0 rather than deferred (it was superseded bymodels-catalog.yml's live nightly lane). (chore · a dedicatedchore: sonar cleanuppass) -
Duplicated SQL literal in the initial migration (0.x) — Sonar flags a 4× literal in the generated drizzle migration. Migrations are append-only / generated (never hand-edited), so this is informational — only act if the literal recurs in the schema source a future migration regenerates. (critical-by-Sonar / likely won't-fix · packages/db/drizzle/0000_organic_the_santerians.sql:118)
Intentional — not a defect (do not "fix"; recorded so Sonar's generic suggestion isn't re-litigated):
bounding.tsusescharCodeAtdeliberately for WTF-8 lone-surrogate byte counting (and the matching test asserts surrogate pairs per UTF-16 unit) —codePointAtwould merge pairs and break the pinned tests.type ToolId = stringis a deliberate semantic domain alias for readability, not a redundant alias.
Items explicitly marked out-of-scope in phase-2.6-conversational-authoring.md that had no prior tracking entry in this file. Recorded 2026-07-08 so they don't get lost. Each maps to a concrete later phase or decision gate.
-
Dynamic runtime
invoke_workflowfrom a workflow agent node. Deferred past Phase 2.6 by decision D57 (2026-07-26): a running workflow's agent node selecting a sub-workflow at runtime by model judgment is the unauthored composition 2.6.P's authored surface == executable surface invariant exists to prevent, and it has no cost-governance boundary. 2.6.P ships the authoredsubworkflownode only. Owner: a dedicated ADR + security review covering target selection, the nested-run event namespace, and cost/resource governance — before any implementation. (medium ·packages/coreengine + aninvoke_workflowtool) -
File-snapshot undo (opencode-style revert of message + file changes). Phase 2.6.E ships conversation-level
/rewind//forkonly — reverting the file changes a message made requires an engine-level file-snapshot mechanism (tracking which tool calls modified which files at which conversation point). Deferred to Phase 3 — desktop or a dedicated follow-up ADR. (medium · packages/core engine) -
Workflow-run
egress/osarms inbuild-engine.ts. The chat surface hasegressandoswired behind the per-tool approval floor (2.5.E). The workflow-run path (build-engine.ts) deliberately wires onlyfs+process— theegress/osarms are not threaded into the workflowToolHostfactory. Revisiting this boundary requires its own ADR (the tool-environment factory design in ADR-0055 / ADR-0057 was scoped to chat-first). Owner: a dedicated ADR before any implementation; likely Phase 3+ when workflows gain the full tool surface. (medium · apps/cli/src/engine/build-engine.ts + assemble.ts) -
Multi-pane dashboard. A split-pane or multi-tab dashboard layout for the Home (e.g. chat on the left, run monitor on the right) is explicitly desktop canvas territory (ADR-0007 — the CLI is not an IDE shell). The desktop app (Phase 3) owns the visual multi-pane experience; the CLI stays single-viewport. (medium · apps/desktop)
-
pluginToolSource loader. TheToolSourcetype already defines'builtin' | 'mcp' | 'plugin'(packages/core/src/tools/types.ts), but thepluginvariant has no loader, no resolver, and no runtime wiring. Plugins loaded from npm packages or user-supplied JS/TS files need a sandboxed execution environment and an installation/security model distinct from both built-in tools and MCP. Deferred until there is concrete demand beyond the existing built-in + MCP tool surface (both of which are sufficient for Phase-2.6 toolbelt parity). (medium · packages/core/src/tools; Phase 4 — VS Code / post-Phase-3)
-
vitest5 +eslint10 toolchain migrations. The ADR-0067 floor bump to Node>=22makesvitest5 (needs>=22.12) eligible;eslint10 (needs>=20.19) was already reachable. Both are explicitly out of Step-scope — each is its own independent migration PR with its own breakage risk, never riding the governed floor bump (node-runtime-upgrade.md §5/§8). Not Phase-2.6 work; pick up when the toolchain is bumped on its own track. (med · pnpm-workspace catalog + configs) -
Mouse click / drag / text-selection / copy-on-select / hover / URL-open. ADR-0068 ships wheel-only mouse (DECSET 1000+1006, codes 64/65) in 2.6.F. Full mouse — click-to-position, click-to-expand, drag text-selection + copy-on-select (pbcopy/wl-copy/OSC-52), hover, Cmd/Ctrl-click URL/file-open — and per-terminal scroll-speed normalization pull in motion tracking (1002/1003), hit-test geometry, a clipboard bridge, and per-terminal quirks. Deferred to Phase 3 — desktop / a later CLI polish PR. (med · apps/cli/src/render/tui + a clipboard bridge)
-
Flip the mouse-wheel default OFF → ON-with-opt-out. ADR-0068 ships wheel-scroll opt-in (
[preferences].mousedefault off) for the first release — keyboard PgUp/PgDn covers the core need and mouse capture disables native copy-on-select. After real-terminal validation (SSH/tmux/VS Code/iTerm2/Warp) with the 2.6.F harness, flip the default to on-with---no-mouse(the field norm). A tracked 2.6.F follow-up, not a defect. (low · apps/cli config default + validation matrix) -
Step 4b: bound / scroll the LIVE REGION when it alone exceeds the terminal height. The alt-screen chat bounds the whole tree to the terminal
rowswith the transcript viewport flex-growing above a FIXED live region (prompt / approval / warnings / footer). When the live region ALONE is taller than the terminal (a huge pasted multi-line prompt, or a big approval block + warnings on a short terminal), the viewport measures height ≤ 0 and renders nothing (acceptable), but the fixed live region then overflows theheight={rows}container — and the observed failure mode is WORSE than a clean top-clip (Step-4b-1 Sonnet review, empirical): atrows1–2 two distinct fixed status lines visually MERGE onto one terminal row (character-level overlap), and atrows3 a whole fixed line (the compose prompt) silently vanishes — the alt buffer has no scrollback to reach the lost content. Long-term give the live region its own bounded/scrollable box or guarantee the viewport a minimum height. (low · apps/cli/src/render/tui/chat-ink.tsx ChatView layout) -
Step 4b-2: harden
displayWidthso it never UNDER-counts vs ink (grapheme-aware) — DONE (Step 4b-2).displayWidth+wrapLogicalLinenow segment withIntl.Segmenter(Node 22, ADR-0067) + measure per grapheme cluster: a VS16❤️/ enclosing keycap1️⃣counts 2 (was 1 — the dangerous under-count), a ZWJ family counts 2 (was 4), a flag/skin-tone counts 2, and a cluster is never split mid-glyph — so the 1-DisplayLine-==-1-real-row invariant holds under the persisted-offset scroll. Pinned in viewport.test.ts. -
isWidestill under-counts a few non-emoji EAW=Wide BMP punctuation code points (Step-4b-2 Sonnet review, NIT). The per-code-pointisWidetable misses a handful of East-Asian-Width=Wide BMP points immediately adjacent to ranges it already handles: U+2329/232A angle brackets, U+268A–268F (Yijing monogram/digram symbols), and the U+4DC0–4DFF Yijing Hexagram Symbols block (just past the 0x4DBF CJK-Ext-A cutoff). These render 2 cells but count 1 — the same UNDER-count class the emoji-presentation fix closed, only for rare glyphs no chat realistically emits. Safe-direction bias means over-counting is fine, so this is cosmetic. Fold the missing ranges into the deferredIntl.Segmenter/EAW wrap-cache hardening below rather than a one-off patch. (low · apps/cli/src/render/tui/viewport.ts) -
Step 4b-3: memoize the transcript wrap so an append/resize doesn't re-segment all of history — DONE (Step 4b-3).
wrapTranscript(chat-projection.ts) memoizes per ENTRY in aWeakMap<TranscriptEntry, { cols, lines }>keyed on the immutable, append-only entry object: an append is O(history) map lookups + ONEwrapEntry(the new entry), a resize replaces each entry's single cached wrap, and it NEVER thrashes (holds exactly the live entries, GC-reclaimable) —viewport.tswrapTextis pure again. (The first cut was a fixed-size per-line LRU; the Step-4b-3 Opus review showed it thrashed to a 0% hit rate once a session exceeded the cache size, so it was replaced.) Pinned in chat-projection.test.ts (incremental append + same-object-on-hit + a >8192-entry repeatability case). -
Step 4b-3: route mid-session raw-
ionotices through the CURRENT session's view store so they survive alt mode — DONE (Step-4b-3 Sonnet fold). The budget-caponBudgetWarning(fires mid-turn) and the/clear/reseat MCP-skipped diagnostic were writing via rawio.writeErrWHILE the hoisted alt buffer was entered, so ink's next frame overwrote them → LOST on the default full-screen path. Fixed with a file-privateliveSessionNoticepointer thatdriveOneSessionsets to the live session'sstore.noticefor its lifetime (a REPL runs one session at a time) and clears in its finally: all fouronBudgetWarningwiring sites now route through it (falling back to rawioonly when no session is live), so the warning renders as a transcript notice. The re-drive MCP-skipped diagnostic (which fires between sessions, before the sink is live) routes to the fresh session'sstore.noticedirectly via the newmcpSkippedLineshelper. Pinned by a live-routing test (break-verified) + the fallback test + amcpSkippedLinesunit test. -
Step 4b-3: keep the alt buffer entered across a
/clear//models-reseat re-drive (inter-session flicker) — DONE (Step 4b-3). Fixed by the HOIST (chosen over DEC-2026, which cannot span a primary↔alt switch):driveInknow passes the ink render optionalternateScreen:false(ink toggles the buffer no more — it still full-screen-renders via log-update), and the hoistedrunReplLoop(withHoistedAltScreen) enters DECSET-1049 ONCE above the per-session loop, clears between re-drives, and exits ONCE, so a/clear/ reseat no longer flips the terminal. Exit-safety net (ink's own 1049-exit is now inert): idempotentrestore()on the finally + aprocess.on('exit')net (the second-SIGINT force quit) + explicit SIGTERM/SIGHUP/SIGQUIT handlers. Verified against ink 7.1.0's compiled build; unit-tested (withHoistedAltScreen, 10 cases); real-TTY signal validation (double-Ctrl-C,kill -TERM) is a manual PR-time check. -
Step 4b-3: on an EXTERNAL SIGTERM/SIGHUP/SIGQUIT, unmount the live ink instance BEFORE the alt-exit (avoid the final-frame dump on the primary buffer). With the ink render option
alternateScreen:false(Step 4b-3), the hoist's signal handleralt.restore()s (DECRST-1049 → primary) thenprocess.exit(128+signo);process.exitis intercepted by ink's signal-exit, which fires ink'sunmount→ ink renders its FINAL frame onto stdout — now the PRIMARY buffer — leaving a screenful of transcript above the recovered shell prompt (Step-4b-3 Opus review). COSMETIC (the terminal IS recovered: buffer + cursor + raw mode + bracketed paste all restored; external-signal-only, not a keyboard path). The clean fix is to thread the current session'sinstance.unmountup to the hoist (driveHome parity) and call it BEFOREalt.restore(), so ink's final frame lands on the alt buffer that is then discarded — a small cross-seam wiring (ChatDriveContextonInstance→ a loop ref → the signal handler). (low · apps/cli/src/{commands/chat.ts,render/tui/chat-ink.tsx}) -
relavium runTUI → full-screen + retained scrollable run-history. ADR-0068 scopes the 2.6.F full-screen renderer to the Home +chat; therelavium runRunAppstays inline (nouseInput→ kernelCtrl-C → SIGINTcooperative cancel preserved). Making it full-screen + giving it a retained, scrollable per-node token history requires the COOKED→RAW cancel rework (an in-process SIGINT handler, asChatApphas) — the single riskiest cancel change, deliberately kept out of 2.6.F. Owner: a focused follow-up (candidate 2.6.G run-detail browser, or its own PR) with a real-TTY cancel test. (med · apps/cli/src/render/tui/{ink-renderer.ts,RunApp.tsx,run-view-model.ts}) -
Bracketed-paste teardown symmetry — drop the redundant Home
DISABLE_BRACKETED_PASTEwrite (Step-2 Sonnet review). ink 7'sApphas an unconditional unmount-cleanup that writesESC[?2004lon EVERY unmount (verified against ink 7.1.0 source), so the Home's manual defensivewriteControl(DISABLE_BRACKETED_PASTE)on the signal/exit teardown (drive-home.tsx) is redundant, and the standaloneChatAppcorrectly relies on ink's cleanup with no manual write (an asymmetry, not a bug). When 2.6.F Step 4 re-introduceswriteControlfor the alt-screen (DECSET 1049) control writes, resolve this cleanly: drop the redundant Home paste-DISABLE (and itshome-inputDISABLE_BRACKETED_PASTEexport + the drive-home test assertion) so both surfaces rely on ink's unmount cleanup uniformly. Low; deferred to ride Step 4'swriteControlrework rather than churn it twice. (low · apps/cli/src/home/drive-home.tsx + render/tui/home-input.ts; Step 2.6.F-4) -
The Anthropic adapter's non-streaming
generate()cannot carry a largemax_tokens. The SDK refuses a non-streaming request whose cap implies a >10-minute generation (empirically:claude-opus-4-5is accepted at ~21 000 and refused at ~24 000), and every Anthropic row in the catalog has amaxOutputTokensof 32 000 or more. So the ADR-0071 §7 clamp — which holds the cap AT the ceiling — cannot bring a large authored cap back under that threshold:generate()still fails, just with the SDK's message instead of a provider 400. Reachable only through the public seam (LlmProvider.generate, the conformance harness,validateProviderKey), because the engine's own agent turn takesstream(); the seam's other callers all pass a tiny cap. Resolve by either capping the non-streaming Anthropic request under the SDK threshold, or failing with a typed, actionablebad_requestnamingstream()rather than surfacing an SDK string. (medium · packages/llm/src/adapters/anthropic.ts; found by the 2.6.Q Step-5 Opus review) -
Cache WRITES are billed at the flat rate, never a context tier's.
ratesFor(packages/llm/src/cost-tracker.ts) moves input, output and cache-read onto the tier a prompt lands in (ADR-0071 §11), but cache-write stays on the flatcacheWritePerMtokMicrocents— because models.dev's tier schema publishesinput,outputandcache_readand nocache_write, so a per-tier write rate is not a number we have. Scaling one from the input tier's multiple would be a guess on a money path, which is the thing this ADR exists to stop. The exposure is a cache-write-heavy prompt above the 272k threshold on the fourgpt-5.6variants (the only shipped models with both a cache-write rate and a tier). Resolve by extendingCatalogPriceTier+ the upstream schema if models.dev starts publishing it, or by asking upstream to. (medium · packages/llm/src/cost-tracker.ts + catalog/models-dev-schema.ts; found by the 2.6.Q Step-6 Sonnet review) -
Auto-open a PR for additive catalog drift. ADR-0071 §9 wants new models to "merge automatically" while a moved shipped-model price stays a red human-reviewed check. The red check ships (
.github/workflows/models-catalog.ymlweekly-catalog-checkrunspnpm sync:models, red ONLY on a moved/vanished shipped-model price; additive drift is green). The automatic half — a bot PR that runspnpm sync:modelsand commits the additive diff — is deferred because it needs a third-partycreate-pull-requestaction pinned to a verified commit SHA (the repo pins every action by SHA; inventing one unseen is the supply-chain risk rule 3 forbids). Add it as a second job once the SHA is verified. Until then a maintainer runspnpm sync:modelslocally when the weekly check goes red — which also forces the--accept-price-changeshuman decision on a moved price, exactly as §9 intends. (low · .github/workflows/models-catalog.yml; ADR-0071 §9) -
Per-model capability gating:
tool_call/attachment+ a withheld-parameter notice. The 2.6.Q capability gating (ADR-0071 §12) carries all four per-modelrequestCapabilitiesin the catalog and WITHHOLDS the two safe silent param-drops —temperatureandstructured_output— at the adapters.tool_callandattachmentchange request MEANING (dropping tools leaves the agent unable to act; dropping an image input changes what the model sees), so silently withholding them is wrong: they need a LOUDER signal — a config/authoring-time validation, or a gate-level notice like the effort gate (ADR-0066 §6). The catalog data is already parsed and carried, so this is the wiring only. Likewise, a user-facing notice for the two silent withholds (so a droppedtemperatureis said out loud, §6's standard) is a follow-up — the correctness fix (no 400) shipped first. (low · packages/llm/src/adapters/ + a gate-level check; ADR-0071 §12; found by the 2.6.Q capability review)* -
Surface a user↔catalog price DIVERGENCE on the ongoing surfaces, not only at set-time. ADR-0071 §5 promises a user price that diverges from the catalog is surfaced in the
/modelspicker and/cost, not just echoed bymodels pricingwhen it is set. Today only the set-time echo is wired: the picker and/costshow the effective (user) price without flagging that it OVERRIDES a different catalog price. Add a divergence marker to both so a stale/wrong override is visible on the surfaces a user actually reads, per §5's intent. (low · apps/cli/src/render/tui/model-picker-view.tsx + the /cost breakdown; ADR-0071 §5; found by the 2.6.Q review)
2026-07-19 — full-project review. A 377-finding multi-agent code review at HEAD
f88b0e8was adversarially verified down to 373 confirmed findings — one agent per finding tried to refute it against the code, a second audited its materiality — with the toolchain green throughout (lint, typecheck, all 226 test files passing): every finding below is a defect a green build cannot catch. The bulk (176 of the 205 triaged work items) now live in phases/phase-2.5.5-hardening-and-remediation.md. Sixteen of the review's findings turned out to duplicate items already recorded in this file — each already carries an accurateScheduled →marker (2.6.B/2.6.H/2.6.I/2.6.K/2.6.M/2.6.Q) or an accurate "no owner yet" note, and the review's own triage confirmed none needed a new entry or a marker change. The items below are the ones the triage deliberately left unscheduled: small enough to pick up opportunistically, or genuinely unowned by any phase or workstream.
-
Annotate the
FallbackChaincost-underreporting pinning tests as a KNOWN GAP, not a deliberate contract.fallback-chain.test.ts's "records a content-free success with no usage when the stream omits a stop chunk" (line 984) and its sibling (line 1000) asserttrace[0]?.usage/trace[0]?.costaretoBeUndefined()when a provider stream ends without a usage chunk — literally encoding the known spend-under-reporting gap (the two failure paths named in current.md's 2.6.C carry-overs, filed against 2.6.Q/ADR-0070 §3) as the CORRECT, asserted behavior, with nothing in the suite signaling that the assertion itself is a product gap rather than a design choice. Add a comment on both tests marking them as pinning a known gap (with a pointer to the 2.6.Q fix) so a future engineer editing this file understands it is not a green test to preserve as-is. Independently shippable now — the comment-only annotation does not need 2.6.Q's 7 open maintainer decisions to land, unlike the underlying fix. (low · packages/llm/src/fallback-chain.test.ts:984,1000; #292) -
Outbound MCP ("workflow-as-MCP-server") has no owner. mcp-integration.md documents an
import { createMcpAdapter } from '@relavium/core/mcp'API as shipped, tagged "Status: Stable" — neithercreateMcpAdapternor a/mcpsubpath exists anywhere in the repo (packages/core/package.jsonexports only.;packages/mcpimplements inbound tool-consumption only, nolisten()/server capability). ADR-0034 and ADR-0052 both call outbound MCP "a later workstream," but no phase or workstream ID actually claims it. The doc-honesty half (rewrite the section as unimplemented/roadmap-only, strike the fabricated sample, drop it from "Stable") is scheduled → 2.5.5.F; the implementation — a realcreateMcpAdapterturning an agent/workflow into an MCP server, firing the already-schema-acceptedmcp_calltrigger — needs no later-phase substrate (packages/mcp,packages/core, and the trigger schema all exist today) and simply has no home. (medium · packages/mcp, packages/core, docs/reference/shared-core/mcp-integration.md; #141) -
Agent memory policy (
memory: none|window|summary) has no owner. agent-yaml-spec.md documentsmemory.type(none/window/summary) as operational behavior, andpackages/shared/src/agent.ts'sMemorySchemavalidates it onAgentSchema.memory, so an authored.agent.yamlwithmemory: { type: window, window_size: 10 }parses cleanly — but nothing inpackages/core/src/engineorapps/cli/srcconsumes the field anywhere; the unrelated[chat].max_messages/ ADR-0062 auto-compaction is a separate, session-level mechanism. A user configures a windowing/summary policy and silently gets the full unbounded transcript every turn, with no warning. The doc fix (mark the field reserved/inert, thenode-types.mdloop/best_of_npattern) is scheduled → 2.5.5.F; wiringagent.memoryinto the turn core is unowned and needs no later-phase substrate. (medium · packages/core/src/engine, packages/shared/src/agent.ts, docs/reference/contracts/agent-yaml-spec.md; #142)
-
Sandbox
Math.randomfallback re-check narrowness — DECLINED, accepted residual.packages/core/src/expression/sandbox.ts:219-225re-checkstypeof Math.random === 'function'after the neutralizingdelete, not the wider "is not undefined"; a hypothetical future QuickJS variant that leftMath.randomas some non-function, non-undefined value after a failed delete would slip past this narrower guard. Declined:Object.freeze(Math)two lines later is the real backstop regardless of what valueMath.randomholds, the currently-pinned QuickJS variant is fully covered bysandbox.test.ts(lines 112-116, 533-545), and the finding's own analysis concludes no action is needed until the QuickJS variant changes. (polish · packages/core/src/expression/sandbox.ts:219; #84) -
Alt-screen
restore()'s idempotent latch-after-success design — DECLINED, verified strength, not a defect.apps/cli/src/render/alt-screen.ts:88-109sets its idempotence latch only AFTER the terminal-restore write succeeds, so a transient EIO/EPIPE on a half-dead TTY during one teardown net (finally /process.on('exit')/ a signal handler) leaves the terminal recoverable by the NEXT net rather than being permanently marked "restored" after a failed write. Declined: the review confirmed this is the correct pattern, not a bug — recorded here as the bar (suspend.ts'ssuspendFullScreenalready matches it) the rest of the codebase should be held to, not as an item to fix. (polish · apps/cli/src/render/alt-screen.ts:88; #67) -
Full-repo
knipdead-export inventory — DECLINED, no further action beyond its two spin-offs. A full-repoknippass surfaced 3 unused-file hits (2 false positives — ESLint fixture configs consumed via ESLint's file-path API, not a TS import), 7 "unused dependency" hits inapps/cli/package.json(all false positives — declared for ADR-0051's tsup bundle closure, not directly imported), 39 "unused" function/const exports, and 112 "unused" exported types. Manual verification of all 39 function/const exports found every one butisDispatchableIdis used internally within its own declaring file (an unnecessaryexport, not dead code); the 112 types are near-entirely...Deps/...Options/...ArgsDI interfaces exported for documentation, a standard low-cost pattern here. Declined: the finding's own conclusion is no action beyond the two genuine hits it already spun off (isDispatchableId, the@relavium/llm/adapterssubpath export) — already tracked under the quality dead-code cleanup work; the remaining ~150 hits are confirmed false positives, not deferred work. (polish · packages/db/src/media-write.ts; #245)