Status: Living
Last updated: 2026-06-19
- Related: current.md, README.md, phases/phase-0-foundations.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.
2026-06-07 hardening pass + maintainer decisions: the built-package items (shared / db / llm / root tooling / docs) were re-verified against current code and 29 were resolved, then the seven open decisions were ruled:
$ref(keep the door open — schema now accepts the union, engine resolves the file), config strictness (.strict()),engine-strict(enforce), branded ids (plain strings stay — code-style note), and turboinputs(keep the safe default) are all settled and checked off. Still open: theLICENSE(HodeTech is drafting its own commercial license), blocked work (live-nightly keys, non-Anthropic pricing — needs the live pages), and three explicitly deferred items (each annotated with why: thez.unknown()presence check, the dist packaging smoke test, and localformat:checkvia turbo).2026-06-08 multimodal decision pass: first-class multimodal I/O is now fully designed and decided — the analysis (multimodal-io-design-2026-06-07.md), ADR-0031 (seam) + ADR-0032 (desktop Rust de-inline) landed, nine maintainer decisions (A1–A9) ruled, and implementation scheduled as the 1.AD–1.AH sub-spine (1.m6). The item is checked off; its not-yet-coded pieces are carried as the seven multimodal forward-obligations below (SSRF primitive, async-job ADR, media cost estimate,
partialRefsemantics,workspaceauthz scope, retention/GC table,vision-alias retirement) so nothing is lost.
2026-06-10 engine/tooling review pass (landed in PR #12, merged 2026-06-11): a review of the engine, tool, and CI surfaces against the current contracts produced a small set of additions, recorded in their sections below: the tool-output size gate + spill-to-disk (1.T), conformance tool-loop / cache-hit scenarios (1.F follow-up), a token-estimate accuracy watch item (1.AC), the Leakwatch CI gate (deferred pending a distribution path), and a dependency-bump cooling window (pending a pnpm major). The same pass settled three decisions outside this file: the MCP client dependency and scheduling (ADR-0034 / workstream 2.R), the reserved
on_erroredge kind (workflow-yaml-spec.md), and theturn_limitErrorCode(constants.ts + sse-event-schema.md). It also landed a CI engine dependency-allowlist guard (tools/engine-deps/check.mjs) and the pnpm install-script allowlist.
- Workflow
agents:$refsupport — the workflow YAML spec allows anagents:entry to be a$refto an external.agent.yaml, butWorkflowSpecSchema.agentsaccepted inlineAgentSchemaonly. Decided (2026-06-07): keep the door open —agents:now acceptsz.union([AgentSchema, AgentRefSchema])({ $ref },.strict()); the duplicate-id check skips$refentries; file/path resolution + path-traversal/SSRF hardening stay the engine's job (the pure/sync shared schema never reads files). Code now matches the spec. (workflow.ts) - Branded id types —
runId/nodeId/gateId/workflowId/agentIdare all plainstring. Decided: plain strings stay (deliberate) — recorded in code-style-typescript.md §Naming; validation is at the Zod boundary and branding adds cross-seam friction for little payoff. Revisit via an ADR if a real id-mixup bug class appears. (minor · packages/shared/src/run.ts, node.ts) -
LICENSEfile + rootlicensefield — the public repo has neither. Decided (2026-06-07): HodeTech will author its own commercial/proprietary license — left open until that license text is drafted (do NOT drop in anUNLICENSED/OSS placeholder in the meantime). When ready, add theLICENSEfile + the rootpackage.json"license"string. (nit → pending the drafted license · package.json, repo root) -
node:started.nodeTypeenum vs free string — currently an unconstrainednonEmptyString. Decide whether the SSE event should carry the engine node-type enum (addENGINE_NODE_TYPESto constants andz.enum(...)) or stay free-string for forward-compat, and record the choice. (nit · packages/shared/src/run-event.ts:47) -
MaskedSecretnamed contract —run:started.inputsdocuments secret masking only in a comment. Export aMaskedSecrettype/schema ({ secret: true; ref: string }) so the masked shape is a named contract every surface renders. (nit · run-event.ts:39) -
composite/project references (reconcile 0.B) — phase-0-foundations.md 0.B calls forcomposite/project-reference tsconfig fields that were not implemented. Either add apackages/*library base withcomposite: true+references(db → shared) and build viatsc -b, or record that turbo^build-ordering is the deliberate final design and update the 0.B callout. (minor · tsconfig.base.json, packages//tsconfig.json)* - First-class multimodal I/O (the
visionflag is only the tip) —vision: trueis set for Anthropic / OpenAI / Gemini, yetContentParthad no media arm, so media could only reach a provider through theproviderOptionsescape hatch in a vendor-specific shape. DONE (2026-06-08): analysis + ADRs landed, decisions ruled, scheduled. The design analysis is multimodal-io-design-2026-06-07.md (three-perspective adversarial review, 8 blocking issues resolved); the binding records are ADR-0031 (the seam amendment — mediaContentPart/StreamChunkarms,CapabilityFlags.mediawithinput{image,audio,video,document}+outputCombinations,Usage.mediaUnits,LlmRequest.outputModalities, reservedgenerateMedia?/pollMediaJob?, theMediaStore/deInlineMedia/handle model) and ADR-0032 (desktop Rust-side de-inline). Nine maintainer decisions (A1–A9) are recorded in ADR-0031's Maintainer decisions table. Implementation is scheduled as the 1.AD–1.AH sub-spine (1.m6, phase-1) — 1.AD (seam shape) lands before 1.K/1.O; 1.AE–1.AH are additive. The residual forward-obligations below carry the not-yet-coded pieces so nothing is lost. (content.ts; the adapters'*_SUPPORTS; llm-provider-seam.md)
- Media-arm integrity metadata (Y3) — DECIDED 2026-06-09 (ADR-0031 amended), land at 1.AD. The
durable form (
DurableMediaPart) carries an optionalbyteLength?+ audio/videodurationMs?, host-populated at thedeInlineMediaboundary; nochecksum(themedia://sha256-<hex>handle IS the sha256); nowidth/heightin Phase A (render-only). Must ship in the 1.AD seam shape (before 1.K/1.O exhaustive consumers) — adding a union-arm field later is breaking.byteLengthis what the byte-delivery Range check bounds against. (ADR-0031 "Amended 2026-06-09"; multimodal-io-design §3.2; 1.AD) ✅ Landed at 1.AD (PR #11, 2026-06-10):byteLength?/durationMs?ship onDurableMediaPartonly (the in-flight arm stays lean — parse-stripped, tested), with thedurationMs-is-audio/video-only rule enforced on both the standalone schema and the durable union. - Shared SSRF range-primitive (the
url-carrier precondition) — the one shared HTTPS-only / block-private-loopback-link-local-metadata-CGNAT / DNS-resolution + per-hop-redirect-revalidation / IPv4-mapped-IPv6-decode primitive thatassertHttpsBaseUrl(openai.ts) is the best-effort placeholder for. security-review.md mandates one primitive across all egress paths; the mediaurlcarrier (input + provider-returned output) is gated feature-flag-OFF until it lands. Landing (1.AE, PR #32):isPrivateOrLocalHost(),extractHttpsHost(),urlHasCredentials()shipped in@relavium/shared(pure sync, platform-free) with 40+ SSRF tests.assertHttpsBaseUrlin openai.ts delegates to the shared functions +new URL()normalization.MEDIA_URL_SOURCE_ENABLEDflipped totruewith per-URL SSRF validation at the seam boundary (refineInFlightMediaPart). The host-side DNS/connect enforcement for the media url path is pulled into 1.AF on a new bytes-shaped media-egress capability (ADR-0043); the general tool/MCP fetch enforcement lands with that surface hook (see below). (security-review.md; openai.ts; 1.AE) - 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 general tool/MCPEgressCapability.fetchenforcement still lands when the desktop/CLI surface implements that fetch hook. (packages/core/src/tools/types.ts; security-review.md; media → 1.AF/ADR-0043; tool/MCP → surface fetch hook) - Async media-job ADR (
generateMedia/pollMediaJobbehavior, A5) — the seam shape is reserved now (1.AD); the engine-owned poll / checkpoint / resume / cancel loop for minute-scale LROs (Sora/Veo) — in the run loop (1.N) + checkpointer (1.R), reusingLlmErrorclassification — gets its own ADR written at 1.AG (Phase D). Highest behavioral complexity in the multimodal design. (1.AG) - 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. (matrix row stays ◇ until the PR merges.)
-
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; 1.AH) -
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; 1.AH) -
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; 1.AH) -
validateWorkflowWithCatalog(D15) is called by no production loader — exported + tested, but no parse/load path invokes it, so authoredoutput_modalitiesare not load-validated (the runtime FallbackChain pre-skip — now wired onto the request — is the only backstop). A host should call it post-parse with the DBmodel_catalog. (CLI/desktop load path; 1.AH/Phase-2) -
[defaults].media_cost_estimate→AgentRunnerDeps.mediaCostEstimate(D17) — the config key + the dep both exist but nothing reads the config and threads it intocreateAgentNodeExecutor, sobuildMediaUnitsEstimatealways uses the built-inDEFAULT_MEDIA_UNIT_ESTIMATE. (host executor construction; 1.AH/Phase-2) -
resolveForEgress(D8) not wired in the engine path — the FallbackChain re-materialization hook is never injected by the AgentRunner, so a durable handle in a transcript message is sent unchanged (no-op); the D7/D8 failover re-materialization is inert until the host wiresresolveForEgress. (packages/core/src/engine/agent-runner.ts; 1.AH/Phase-2) -
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—#performSaveToputs bytes (CAS) before the node:completed emit records themedia_objectsrow; a crash between them leaves row-less CAS bytes thatreclaimExpired(which keys off rows) can never reclaim. Needs a host CAS-orphan sweep. (low · packages/db host GC; 1.AH/Phase-2) - Clean-terminal media-reclaim has no retry —
#reclaimRunMediais best-effort at the terminal; a transient async-host rejection on a cleanly-completed run leaves therunrefs (resume short-circuits on a terminal checkpoint). Consider a host periodic sweep keyed on terminal run events. (low · host GC; 1.AH/Phase-2) -
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) - 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. (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). (a
docs/standards/entry or skill; Phases 2–4) - Tool-output size/token gate + spill-to-disk (1.T). Today only the event
outputSummaryis truncated; the tool result handed back to the model has no bound, so one oversizedread_file/http_request/ MCP result can blow the context window (a cost/DoS surface ADR-0028's pre-egress governor cannot see, since the damage lands in the next request). Add to theToolRegistrydispatch path (1.T): a byte/token ceiling per tool result with an explicit truncation marker, and for over-threshold output (e.g. >2000 lines / >50KB) spill the full output to a workspace-scoped file and hand the model a bounded preview + the path (readable via the normal FS-scope-tiered tools). Behavior belongs in built-in-tools.md when implemented. (1.T; built-in-tools.md) ✅ Landed in 1.T:boundForModel(bounding.ts) applies a byte+line ceiling, emits a head/tail preview + explicit truncation marker, and spills the full text to the host's run-scopedoutputStore(handle in the marker); applied inregistry.dispatch(returnstruncated) under the one cancellation-precedence ladder. Documented in tool-registry.md §result-bounding-and-spill-to-file. - Cumulative cost is not restored on cross-process resume (cost-event persistence) — 1.AC/1.R. Done
(maintainer-approved, the node:completed-carry variant).
cost:updatedis streamed (#nodeEmit→ bus), not persisted, so thereconstructCheckpointStatefold never saw it — a resumed run'scumulativeCostMicrocents(and the governor) restarted near 0. Fix: the durablenode:completednow carries an optionalcumulativeCostMicrocents(run-event.ts) — a snapshot of the run-wide running total at the node boundary, populated by the engine (#completeNode) and folded on resume with a monotonicMath.maxthat reconciles with the existingbudget:paused.spentMicrocentsrestore (checkpoint.ts). So a run paused at any gate (plain human OR budget) now resumes with the right spend; a gate-less crashed-mid-run is reconciled torun:failed(not resumed), so its cost-loss is moot. Chosen over persistingcost:updated(which would add hot-path durable writes + a delivery-ordering change): zero new events, one additive forward-compatible field, folds at boundaries the store already persists — no ADR needed. Pinned by a checkpoint.test.ts unit test (plain-human-gate restore) + the 1.U flagship harness (post-resumerun:completed.totalCostMicrocentsreflects the pre-gate cost). (packages/core/src/engine/engine.ts#completeNode+ checkpoint.ts; packages/shared/src/run-event.ts; 1.AC/1.R) - 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.
- Structured-default reference flow (→ 1.M). Resolved in 1.M. Boundary decided and
pinned: a structured input default is opaque data, never template-interpolated — only string
defaults carry templates. A
{{ … }}nested in a structured default (default: { token: '{{secrets.x}}' }) is therefore neither resolved nor taint-scanned, and is not a leak vector (resolveTemplateis single-pass, so{{inputs.cfg | json}}emits the literal{{secrets.x}}, not a resolved secret). Pinned byanalyze.test.ts("treats a STRUCTURED input default as opaque data"). (packages/core/src/interpolation/analyze.ts) - 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) - DeepSeek surviving-reasoning replay — Confirmed correct + locked (engine-hardening pass). The
OpenAI-compatible adapter CAPTURES
reasoning_contentinbound (mapContent→ areasoningpart) but intentionally drops it on egress (toOpenAiMessageslowers only text + tool_call parts; openai.ts:256-260, "reasoning is ephemeral and never replayed, ADR-0030"). For DeepSeek this is the CORRECT direction:reasoning_contentis output-only — the API 400s if it is echoed back in an input message, and deepseek-reasoner does not need prior reasoning to continue. So no seam-shape carrier is needed (unlike the Anthropic-redacted / Gemini-thoughtSignature items above). Pinned by an openai.test.ts lock-test (a prior-turn reasoning part never reaches the request body). (packages/llm/src/adapters/openai.ts; ADR-0030/0039) -
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—cost:updatedis always per-attempt-accurate, butagent:token.modelusesactiveModel(updated from the succeeding attempt record, which fires after the stream), so a cross-model pre-content failover attributes that turn's tokens to the prior model. 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) - Per-attempt pre-egress budget gate (1.AC) — closed by 1.AC (PR #26). The precise per-egress budget
check now rides the
FallbackChainpre-attempt hook, so every attempt — including a failover to a pricier model — is capped; the loop-topawaitPreEgressinrunAgentTurnadds the zero-egress-on-cancel guard + primary-model early check (the intentional double gate). (closed · ADR-0028; ADR-0038; 1.AC, PR #26) - 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) - Secret-into-
run.outputsruntime taint (ADR-0029(c) follow-up) — anagentnode cannot launder a secret intorun.outputs(it emits LLM text only), so this is not 1.O's to own; it belongs to thetransform/ sandbox node (1.P / 1.AB) that can return a secret-derived value. 1.O's only obligation is to refuse a tainted{{ run.outputs[…] }}reference if such a marker reaches it; the static parse-timeanalyzeSecretTaintgate covers the authored template graph. Record as a scoped ADR-0029 amendment when 1.P/1.AB lands. (medium · packages/core/src/interpolation/analyze.ts; ADR-0029(c), 1.P/1.AB) ✅ Closed at the source by 1.P (PR #20):buildExpressionScope(scope.ts) maskssecret-typed inputs out of the sandbox scope, so atransform/condition/ fan_inmerge_fnreads the{ secret, ref }marker — never the raw secret — and therefore cannot derive a secret value to launder intorun.outputs. The vector is cut at the read, so no runtime taint on the output is needed. (The only remaining secret-into-egress path is the agent prompt — tracked separately below as a 1.O policy item, and it is provider egress, not an event-payload leak.)
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. (medium · packages/core/src/interpolation/analyze.ts, collect.ts; SEC-1) - Concurrent-agent dispatch coverage — N agent nodes run in parallel under
max_parallel, each callingrunAgentTurnagainst the sharedToolRegistry. Verified reentrant (per-call locals; each node builds its ownFallbackChain+CostTracker), but there is no concurrency test. Add one: two agent vertices dispatching the same tool in parallel, asserting gap-free per-node event sequences and no cross-node cost/emit bleed. (low · packages/core/src/engine/agent-runner.ts; enginemax_parallel) ✅ Added (hardening pass):agent-runner.e2e.test.ts"runs two agent nodes concurrently against the shared executor" —max_parallel: 2, two agent vertices on one executor instance, asserting a gap-free global sequence and that each node'sagent:tokenevents carry their ownnodeId(no cross-node bleed). - Combined tool-loop DoS bound (turns × corrections) — Done (engine-hardening pass). The
"product" framing was imprecise: the bounds are NOT multiplicative.
maxToolTurnsis the worst-case egress ceiling (≤maxToolTurns + 1provider calls);maxToolCorrectionsis a monotonic sub-budget that can only end the turn EARLY withtool_failed(a genuine round never resets it). Documented onAgentTurnLimits(agent-turn.ts) and pinned by an interleaving test (correctable / genuine / correctable / correctable →tool_failedat turn 4, far undermaxToolTurns), asserting corrections accumulate across the interleaved genuine round and egress stays bounded. (low · packages/core/src/engine/agent-turn.ts) - 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) - Workflow-context (
ctx.*) threading into expression/agent scope — thecondition/transform/merge_fnsandbox scope (and the AgentRunner's promptRunScope) boundctx: {}— the authoredcontext:namespace was not resolved/threaded (a{{ctx.key}}template resolved, but a barectx.keyJS-expression read saw{}). ✅ Fixed (the ctx-threading PR, 2026-06-15): the engine resolves the workflowcontext:once at run start (a new#resolveContextOrFailstep using injectedWorkflowEngineDeps.resolverCapabilities, with avalidationfailure path that closes the run), threads the frozenctx.*via the newNodeExecContext.ctxseam field, and both consumers (buildExpressionScope, the AgentRunner'sresolvePrompt) read it;ctxis re-resolved on resume (not checkpointed). Pinned by engine e2e + a transform unit test. (packages/core/src/engine/engine.ts, node-handlers/scope.ts, agent-runner.ts) -
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. Evaluate alongside the secret-handling story; if masked, reusemaskSecretInputs. (low · packages/core/src/engine/agent-runner.ts; security-review.md) - Reject a plain (handle-less) edge whose
fromis aconditionnode (1.M validation) — aconditionroutes only viabranches[].target_node/default(materialized edges); a separately-authored plain edgefrom: <condition>(no:handle) makes its target a dependent that the handler'sselectednever names, so the run loop skip-propagates it — a silently-dead downstream rather than a parse error. Add a structural validation indag.ts(validateStructuralEdge) rejecting a handle-less edge out of a condition (reuseinvalid_handle, or acondition_requires_handlekind). Pre-existing 1.M edge-validation gap, not a 1.P handler defect. (low · packages/core/src/dag.ts; workflow-yaml-spec.md §edges) ✅ Fixed (2026-06-14 hardening pass):validateStructuralEdgerejects a handle-less edge from aconditionwith aninvalid_handleissue (no existing fixture/spec used one — the spec routes viabranches+nodeId:whenhandles); pinned bydag.test.tsand documented in workflow-yaml-spec.md §edges.
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. (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. (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. (low · checkpoint.ts/engine.ts; Phase-2 store)
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). (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. The hard turn cap is an engine-API knob in 1.V (SessionDeps.maxTurns, finite default 50); mapping the[chat]config default onto it is a surface task (the CLI/desktop read[chat]and passmaxTurns). It is deliberately not a Phase-1[chat]field. (low · config-spec.md + surfaces; Phase 2+) - 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)
-
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. - Standalone
MergeNodeSchemagap —merge_strategy:customwithoutmerge_fnonly fails atWorkflowSchemalevel (a discriminated-union member can't carry the cross-field rule). Document the partial node-level validation and add anode.test.tscase pinning the gap as intentional. (minor · node.ts:85-92, workflow.ts:104-113) - O(n²) duplicate-id check in
AgentSchema— usesindexOf-in-filterwhileworkflow.tsuses an O(n)Set. Reuse a sharedreportDuplicateshelper so both schemas share the single O(n) implementation. (nit · agent.ts:109-110) - Per-provider temperature ranges — the shared
temperatureSchemais the provider-agnostic[0, 2]envelope, but Anthropic accepts only[0, 1]. Enforce/clamp the provider's real range in the@relavium/llmadapter (Phase 1, where request validation lives) so aprovider: anthropic+temperature > 1agent fails fast — without coupling the shared contract to a provider's current API limit. (review · agent.ts, common.ts) - Config-schema strictness parity —
GlobalConfigSchema/ProjectConfigSchema/ChatConfigSchemawere not.strict(), so a typo in a committedconfig.toml/project.tomlkey was silently dropped — asymmetric with the authored-YAML strictness (ADR-0023). Decided: fail loud — all three (and their nestedpreferences/defaultsobjects) are now.strict(); a typo'd config key is rejected at parse. (minor · packages/shared/src/config.ts) - Codify
ContentPart/StopReasoncanonical home in the seam doc — both are intended to be owned by@relavium/sharedand re-exported by the@relavium/llmseam, never imported by shared from llm (which would invert the package dependency).StopReasonalready lives in@relavium/shared(constants.ts, used bysession:turn_completed);ContentPartlands whenSessionMessageSchema/AgentSessionSchemado (1.V/1.X). The seam doc (llm-provider-seam.md) still shows both only as local TS shapes with no ownership/re-export statement — annotate them there so the seam doc and theconstants.ts/run-event.tscomments stay aligned. (nit → 1.A/1.V · llm-provider-seam.md) -
AgentSchemainput_schema/output_schema— agent-yaml-spec.md lists both as optional agent-level fields ("purely additive metadata"), and namesAgentSchemaas their validator, butAgentSchemais.strict()and declares neither — so an authored agent using a spec-sanctionedoutput_schemais rejected at parse. Pre-existing (agent.ts untouched by 1.L.0, which scopesoutput_schemato the agent/transform nodes only); add both asOutputSchemaSchema.optional()(the node.ts JSON-Schema-subset map) + a test, or amend the spec. (low · agent.ts, agent-yaml-spec.md) - Input-validation type-compatibility —
WorkflowInput.validationaccepts any key regardless of the inputtype(e.g.format/max_lengthon anumber,min/maxon astring). Bound-ordering (min ≤ max,min_length ≤ max_length) is enforced; the per-type key matrix is not, because the contract (workflow-yaml-spec.md) only shows two examples and doesn't define which keys are legal perInputType. Specify that matrix, then add aWorkflowInputSchema.superRefine((type, validation) => …). (minor · workflow.ts, workflow-yaml-spec.md) - Verify the non-Anthropic prices in
pricing.ts(at 1.G/1.H) — the OpenAI / Gemini / DeepSeek rows were best-known placeholders (Anthropic confirmed via claude-api). Done 2026-06-11: verified against each provider's live pricing page, which revealed five of the six non-Anthropic models were deprecated/shut down — retired and replaced with current models (gpt-4o→gpt-5.5, gpt-4o-mini→gpt-5.4-mini, gemini-2.0-flash→gemini-2.5-flash, gemini-1.5-pro→gemini-2.5-pro), DeepSeek prices corrected (deepseek-chat/-reasoner now distinct, ctx 1M / 384K out, deprecating 2026-07-24), and Claude Fable 5 added; Opus 4.8 / Sonnet 4.6 / Haiku 4.5 re-confirmed unchanged. (packages/llm/src/pricing.ts) -
model_catalogcache-write column (at the seeder) —ModelPricingcarriescacheWritePerMtokMicrocents(Anthropic charges one), butmodel_catalog(database-schema.md) has onlyinput/output/cached_input_*_per_mtok_microcents. When the catalog seeder lands, either add acache_write_cost_per_mtok_microcentscolumn or knowingly drop the cache-write price from the DB projection (pricing.tsstays the source of truth either way). (nit · database-schema.md)
- Coverage glob is cwd-sensitive + no enforced threshold —
vitest.config.ts'scoverage.include: ['packages/*/src/**/*.ts']is repo-root-relative, so a package-scoped run (pnpm --filter @relavium/llm exec vitest --coverage) reports a false 0%; coverage is only accurate from the repo root. Make the glob cwd-tolerant (or document root-only) and add the testing.md ≥90% line+branch threshold for the engine packages (packages/core,packages/llm) — per-area, since surfaces are smoke-only. (minor · vitest.config.ts) - Coverage floor fires only on a repo-root run + is not a CI gate — Done (engine-hardening
pass, advisory). Added a repo-ROOT
pnpm coverageCI job (ci.yml) — a root run is exactly what makes the root-relative per-glob thresholds (packages/core|llm/src/**) authoritative, so the package-scoped cwd gap (residue 1) is moot. The job is advisory (a separate, non-required job likepeer-dep-gate) so it surfaces a regression without blocking merge while the core-package branch margin is thin (90.29% vs the 90% floor); promote it to a required check once that margin is confirmed stable under CI's Node 22. The cwd-sensitivity itself stays documented at the thresholds block (a single glob cannot fix it without wrongly binding shared/db runs). (minor · ci.yml; vitest.config.ts) - Column-level schema fidelity —
client.test.tsproves only that table names exist. Add aPRAGMA table_info(<table>)assertion per table (name/type/notnull/dflt/pk) against an expected fixture, or snapshot0000_*.sqlbyte-for-byte. (minor · packages/db/src/client.test.ts) - Negative FK test — insert a
step_executionsrow with a non-existentrun_idand assert/FOREIGN KEY constraint failed/i, provingforeign_keys = ONactually rejects. (minor · packages/db/src/client.test.ts) - 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. - In-memory
journal_modeassertion — if/when asserting the WAL no-op for:memory:, assert itsjournal_modeis'memory'. (nit · client.test.ts:50-53) - Edge
from-handle grammar — the handle is permissive (uppercase/spaces/repeated colons). Decide + pin the grammar (a:empty handle rejects; decide ona:UPPER/a:a:b) and tighten the regex if needed. (minor · edge.ts:14-19, edge.test.ts) - Condition/transform invariants — add tests: reject
default:'Not Kebab'; acceptwhen:'foo'/when:7; reject emptytransform/expression. (minor · node.test.ts) -
record()non-object reject — assertRunSchema.safeParse({ ...run, inputs: 'x' })rejects, pinning the record boundary. (nit · run.test.ts, run-event.test.ts) - Round-trip fixture verbatim — the workflow no-drift fixture paraphrases multi-line prompts; transcribe them verbatim from the spec or soften the "verbatim" claim. (nit · workflow.test.ts)
- Conformance: tool-loop + cache-hit recorded scenarios (1.F follow-up) — Done
(engine-hardening pass). Both landed as recorded scenarios across all four provider suites: (1) a
multi-turn tool loop — a new
replayFetchSequence(+ areplayForsingle-vs-sequence router; the Gemini transport indexes per call) drives two generate() calls against one adapter, so turn 2 exercises the adapter lowering atool_resultmessage back onto the provider's wire (the call → result → continuation path every agent node runs); and (2) a prompt-cache-hit assertion —ConformanceExpectations.textGenerategained an optionalcacheReadTokens, asserted in the textGenerate test (DeepSeek's fixture already recordsprompt_cache_hit_tokens: 4→ net input 8, cacheRead 4 folds into the one canonicalUsage). The provider-quirk fixture bank can still grow opportunistically as new quirks are met. (packages/llm conformance)
- Turbo task
inputs—lint/typecheck/testdeclare noinputs, so turbo hashes every file (over-invalidation). Decided: keep the safe default (hash-all) — scopinginputsrisks a stale-cache false pass (a changed file outside the input set served as "cached green"), which is worse than slower cache. Recorded as deliberate; revisit if CI cache time becomes a real cost. (minor · turbo.json:19-30) -
incrementaltsconfig — no.tsbuildinforeuse; everytscrecompiles from scratch. Addincremental: true(gitignoredtsBuildInfoFile, listed in turbooutputs). (minor · tsconfig.base.json) - Typecheck the config files — root/package-root
*.config.ts(drizzle/vitest) are neither typechecked nor linted. Add atsconfig.tools.json+typecheck:toolsstep, or document the gap as an accepted boundary. (minor · drizzle.config.ts, vitest.config.ts) - Concurrency head-ref grouping —
mainis now protected from cancellation, but a same-repo branch push and its open PR still run CI under separate groups. Considergroup: ci-${{ github.workflow }}-${{ github.head_ref || github.ref }}to collapse them. (minor · ci.yml:19-27) -
engine-strict=true—engineswas advisory. Decided: enforce —engine-strict=trueadded to.npmrc, so an unsupported Node/pnpm fails install fast (clear message) instead of surfacing as confusing errors later. (minor · .npmrc, package.json) - 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. (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-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.
-
readBracketcognitive complexity (1.L2) — Sonar 17 > 15; extract the numeric-index vs quoted-key branches into helpers. (critical · packages/core/src/interpolation/path.ts:96) ✅ Fixed: extractedreadQuotedKey+readNumericIndex;readBracketis now a thin dispatcher that delegates to them (cognitive complexity well under the threshold). -
splitTopLevelcognitive complexity (1.L) — Sonar 16 > 15; extract the quote/bracket depth-tracking into a small state helper. (critical · packages/core/src/interpolation/references.ts:217) ✅ Fixed: extracted aSplitState+splitStep/splitStepOutsideQuotepair; the loop body is one call. -
String.rawfor regex-escape literals (1.L test) — useString.rawinstead of escaping\in the interpolation reference fixtures. (minor · packages/core/src/interpolation/references.test.ts:181-190) ✅ Fixed: both escaped-quote fixtures now useString.raw(template + expected value); tests still green. - Negated condition in the glob matcher (1.T) — Sonar "unexpected negated condition"; flip the
branch for readability if it does not obscure the backtracking logic. (minor · packages/core/src/tools/registry.ts:387)
✅ Fixed:
star !== -1→ the positive valid-index checkstar >= 0. - 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.
- Node-runtime row in tech-stack.md —
runbooks/local-dev-setup.mddefers the Node version to tech-stack.md, which states none. Add a row (.nvmrc= dev/CI 22; supported floor 20.11 perengines). (minor · tech-stack.md) - WAL single-writer wording — soften database-schema.md "concurrent read performance" to
make the single-writer constraint explicit so engine authors design
run_eventswrites around one writer. (minor · database-schema.md) -
vitest.config.tsinclude comment — the stated rationale is inaccurate; rewrite it to the real reason (pin to*.test.tsso a stray*.spec.tssurfaces). (minor · vitest.config.ts:16-18) -
constants.tsheader overstatement — clarify that providers/execution-modes are consumed byz.enum, while event names/node types are a parallel authoritative list the unions re-declare and tests pin. (nit · constants.ts) -
RetrySchemacross-dep note — note at thenode.tsimport thatRetrySchemais owned byagent.tsand the dependency is one-way (agent.ts must never import node.ts). (nit · node.ts:1-4) -
cumulativeCostMicrocentscomment — append the run-scope "running total for the whole run" note to match the spec. (nit · run-event.ts:84) - Per-variant event-type export consolidation — 3 inline + 10 in a trailing block; either co-locate all inline or annotate the trailing block so it isn't read as exhaustive. (nit · run-event.ts)
- Shipped source maps reference
../src— publisheddist/*.mappoint atsrc/, which isn't infiles. Either add"src"tofilesor dropdeclarationMap/sourceMapfrom the*.build.json. Bounded byprivate: truefor now. (nit · tsconfig.base.json, package.jsonfiles)