Skip to content

1.AF P4 — media access & spend governance (read_media, save_to, cost governor, retention/GC) - #35

Merged
cemililik merged 15 commits into
mainfrom
development
Jun 19, 2026
Merged

1.AF P4 — media access & spend governance (read_media, save_to, cost governor, retention/GC)#35
cemililik merged 15 commits into
mainfrom
development

Conversation

@cemililik

@cemililikcemililik commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

1.AF P4 — media access & spend governance (the P4 remainder)

Completes workstream 1.AF (engine media plumbing) P4 — the media access & spend boundaries on the ADR-0031 seam. Builds on PR #33 (P1+P2) and PR #34 (P3 + D13), both merged. Lands the engine-pure policy; the host mechanism/wiring half is 1.AH/surface (recorded in deferred-tasks.md §1.AF P4).

Design ADRs: 0042 (storage/retention) · 0043 (egress) · 0044 (access/spend).

What landed

  • D12 — read_media — the 13th built-in tool + one engine-pure scope-set authz primitive (scopeSetIncludes; owner-equality rejected, sha256-knowledge ≠ authorization) + the byte-delivery Range gate (validateByteRange, fail-closed) + the additive ToolPolicyDenyReason.media_scope_denied; the media_objects/media_references junction store (MediaReferenceStore + createMediaReferencePort), recording a produced handle's run reference at the #emitDurable choke point.
  • D11 — the terminal-state sweep (reclaim a run's run refs at run:completed|failed|cancelled) + reclaimExpired grace-window GC (soft-delete zero-ref objects past the configured grace).
  • D15validateWorkflowWithCatalog: the engine-loader output_modalitiesmedia.outputCombinations membership check (defers an unresolvable model; the runtime FallbackChain pre-skip — now wired onto the request — is the backstop).
  • D16 — save_toMediaWritePort (shared) + ExecutionHost.mediaWrite + the engine output-node orchestration (resolve {{ run.id }} → relative path, extract the single produced handle → MediaStore.get → write; fails the node, not best-effort) + the db createFilesystemMediaWrite fail-closed jail (realpath+commonpath, symlinks off, atomic wx temp + rename, reason-only MediaWriteError) + the new run.id interpolation namespace.
  • D17 — the per-modality media cost governor: ModelPricing.mediaOutputRates + mediaCost/estimateMediaCost (a disjoint addend folded into the existing max_cost_microcents cap, degrade-to-0 on a missing rate) + the widened PreEgressHook (outputModalities/mediaUnitsEstimate) + [defaults].media_cost_estimate + the model_catalog media-rate columns (migration 0003).

Reviews (both adversarial, multi-agent)

  • A dedicated P4 byte-delivery security review (two independent passes) — 0 blockers/highs, no traversal/symlink-escape/arbitrary-write reachable; 2 mediums + cheap lows fixed (dd03cc8).
  • A comprehensive 46-agent review of the whole P4 changeset (9 dimensions, every finding double-verified) — 0 blockers/highs in reachable code. Its real findings were fixed in e6e8545:
    • [high]output_modalities is now lowered onto the LlmRequest, so the FallbackChain output-combination pre-skip (the D15 runtime backstop) actually fires — previously an incapable model silently returned text.
    • [med]reconcile() best-effort reclaims a crashed run's media refs (no permanent orphan).
    • [med]boundForModel is media-aware — a read_media/media tool result never serializes base64 into agent:tool_result.outputSummary/spill (the I3 gap the emit choke point can't catch).
    • [low]read_media 0-byteLength whole-handle off-by-one; wx temp hardening; run.id / best-effort regression pins.

Deferred to 1.AH (recorded, not in this PR)

The host-wiring half: the D12 host MediaReadAccess impl + session-scope population, the D15 loader call, the D17 config/resolveForEgress wiring — so D12/D15/D17 are engine-policy-complete but inert end-to-end until a surface wires them. The keychain no-raw-key IPC test (no Phase-1 desktop surface; ADR-0044 §4).

Conformance

  • pnpm turbo run lint typecheck test build16/16 (786 core tests)
  • pnpm run format:check clean · Leakwatch 0
  • Engine purity (zero platform imports in packages/core) — save_to fs mechanism lives in @relavium/db
  • @relavium/llm seam holds (no vendor type crosses); secrets/I3 (no bytes/path/secret in events/logs)
  • Canonical-home docs updated (workflow-yaml-spec, security-review, config-spec, database-schema)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added read_media tool for controlled, scope-authorized byte-range access.
    • Added save_to for output nodes to persist media bytes using {{ run.id }} (with safer path handling).
    • Added workflow output_modalities validation against model capability sets.
    • Added project-level [defaults.media_cost_estimate] defaults for media-output unit estimates.
  • Bug Fixes / Improvements
    • Extended budget governance and cost estimation to incorporate media-unit estimates.
    • Improved handling and security redaction for inline media payloads in model-facing outputs.
  • Documentation
    • Updated config, workflow YAML, and security checklist references for media cost estimation and {{ run.id }} semantics.

cemililikand others added 13 commits June 19, 2026 15:36
…rship primitive
The engine-pure half of the read_media byte-delivery authz (ADR-0044 §1), landed before its
consumers (the D12 read_media tool + the 1.AH desktop command) like the rest of the seam shapes:
- ScopeSchema / Scope — a { kind: 'session' | 'workspace', id } authz scope (workspace reserved,
documented-not-implemented), keyed on the existing MEDIA_AUTHZ_SCOPE_KINDS.
- scopeSetIncludes(allowed, requesting) — grants read ONLY on exact kind+id membership in a handle's
allowedScopes (the host-read media_references session/workspace rows); owner-equality is not
expressible and an empty set fails closed. Knowing a sha256 is not authorization.
Reused by both surfaces so the authz is written once. +2 tests (schema accepts session/workspace,
rejects run/node + empty id; membership grants only on exact kind+id, fail-closed on empty).
The read_media tool itself + the ToolDispatchContext media-read delegate + media_scope_denied +
the media_references row-writing lifecycle land in the following D12 increments.
Refs: ADR-0044
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e-set authz + Range gate)
The 13th built-in (ADR-0044 §1). Engine-pure byte-delivery gate; the MediaStore + media_references
access is an injected ctx.mediaRead delegate (NOT a ToolHost capability arm — mirrors invokeAgent),
so the dispatch never touches raw bytes:
- tools/types: MediaReadAccess (describe(handle) -> { mimeType, byteLength, allowedScopes } + readRange
-> in-flight base64 MediaSource) + ToolDispatchContext.requestingScope + .mediaRead delegate.
- tools/errors: additive ToolPolicyDenyReason.media_scope_denied (no exhaustive switch breaks — reasons
are construction-site only).
- tools/builtins: read_media { handle, start?, end? } — dispatch does authz FIRST (scopeSetIncludes on
the handle's allowedScopes; deny -> media_scope_denied; never owner-equality), then validateByteRange
(whole-handle default; fail-closed BEFORE any host read), then the host readRange; returns an in-flight
media part. MEDIA_POLICY (read-only, no fs-scope/egress/gate; bypasses the Phase-2 ActionGuard).
+6 tests (authz grant whole-handle + explicit range; media_scope_denied; unknown handle; out-of-bounds
fails before the read; unavailable without the delegate). Built-in count 12 -> 13.
The host wiring (engine builds ctx.mediaRead from ExecutionHost.mediaStore + a media_references db port)
and the media_references row-writing lifecycle are the next D12 increment (D12c, paired with D11 GC).
Refs: ADR-0044, ADR-0029
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erences junction reference)
The Node/SQLite host reference for the media retention + authz junction (ADR-0042 §3-4 / ADR-0044 §1).
One junction serves both the refcount/terminal-sweep (run/node rows) and read_media authz (session/
workspace rows). Lives in @relavium/db (better-sqlite3); the pure engine never imports it — a host
wires it behind the engine reference-lifecycle port + the read_media MediaReadAccess delegate.
- recordObject(input): upsert media_objects, idempotent on the content-addressed handle (a conflict
just refreshes last_referenced_at, the GC cursor — bytes are identical).
- addReference(handle, scopeKind, scopeId): idempotent on the (handle, scopeKind, scopeId) unique index
(the refcount is the distinct-row count). The handle FK targets media_objects.handle, so a stray
reference fails closed at the FK.
- describe(handle): a LIVE handle's mimeType/byteLength + its session/workspace authz scopes ONLY
(run/node lifetime rows excluded — they never grant read); undefined for an unknown or deleted_at
(GC-reclaimed) handle, so read_media fails closed. Explicit kind-narrowing → Scope[] (no unsafe cast).
- removeRunReferences(runId): D11 terminal sweep — removes a run's run-kind rows; returns the count.
+6 tests (authz scopes filtered from lifetime rows; unknown/deleted → undefined; addReference +
recordObject idempotency; sweep removes only the run rows, leaves session authz intact).
The engine wiring (the reference-lifecycle port: record at the deInline choke point + the terminal
reclaim; the read_media ctx.mediaRead delegate assembly) is the next increment (D12c-engine + D11).
Refs: ADR-0042, ADR-0044
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ediaHandles (engine contracts)
The shared half of the media-reference lifecycle wiring (ADR-0042 §3-4):
- MediaReferencePort: the host lifecycle port the pure engine calls — recordRunMedia(meta, runId) at the
de-inline choke point + reclaimRun(runId) at the terminal sweep (D11). Documented best-effort + optional:
a record/reclaim failure is a retention concern (GC over/under-retention), NEVER an I3 or run-correctness
break, so it must never fail the run. (Session/workspace read-grants are the surface/AgentSession concern.)
- DurableMediaMeta: the produced-handle metadata mirror (handle/mimeType/modality/byteLength/durationMs?).
- collectDurableMediaHandles(value): cycle-safe, handle-deduped collection of the durable handle media
parts in an already-de-inlined value (mirrors the containsDurableUnsafeMedia stack walk). Skips a
non-handle source, a handle part missing byteLength, or an unknown modality.
+4 tests (metadata incl. durationMs; dedupe across array/Map/Set/cycle; skip base64/url/no-byteLength/
unknown-modality; empty). The db adapter (createMediaReferencePort) + the engine wiring (record at
#emitDurable, reclaim at terminal) are the next two increments.
Refs: ADR-0042, ADR-0044
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Store -> engine port adapter)
Adapts the MediaReferenceStore to the engine's MediaReferencePort (shared): recordRunMedia(meta, runId)
= recordObject(meta) + addReference(handle,'run',runId); reclaimRun(runId) = removeRunReferences(runId).
DurableMediaMeta is structurally MediaObjectInput, so the record forwards directly. A host wires this
behind ExecutionHost.mediaReferences; the pure engine calls only the shared port. +1 test.
Refs: ADR-0042, ADR-0044
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…at choke point, reclaim at terminal)
Wires the media-reference lifecycle into the engine (ADR-0042 §3-4), completing D12c + the D11 terminal
sweep:
- ExecutionHost.mediaReferences? (the shared MediaReferencePort) + createInMemoryHost option.
- #emitDurable: after the event is stamped, #recordProducedMedia(durable) records the run's reference for
every produced durable handle (collectDurableMediaHandles), and at a TERMINAL event #reclaimRunMedia()
runs the D11 sweep (release the run's run-kind refs; a session/workspace read-grant survives).
- Both go through #bestEffortMediaRef: a sync throw or async rejection is swallowed (.catch), so a
retention-port failure NEVER touches the I3 / gap-free / exactly-one-terminal guarantees — the run is
unaffected. No-op without the port or when an event carries no handle.
+2 engine tests (a produced handle is recorded for the run + reclaimed once at the terminal; a THROWING
port leaves the run completing normally with no byte leak).
The grace-window byte-GC (the periodic 0-ref reclaim past last_referenced_at) is a host-periodic db
method (D11-gc), landing next; the read_media ctx.mediaRead/requestingScope wiring is the surface/
AgentSession concern (session scope), recorded for 1.AH.
Refs: ADR-0042, ADR-0044
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nceStore
The grace-window byte-GC (ADR-0042 §4 step c): soft-deletes (sets deleted_at) every LIVE object that
now has ZERO references AND whose last_referenced_at is older than graceMs before now; returns the
reclaimed handles so the host can delete their CAS bytes. A host periodic job calls it with the
configured grace (default 7 days); the terminal sweep (removeRunReferences / the engine reclaim) is
what drops a handle to zero refs first. Single-connection select-then-update is consistent; the update
targets exactly the expired handles (not a re-run of the 0-ref filter, which would ignore the grace).
Idempotent (an already-deleted object is skipped). +1 test (still-referenced never reclaimed; not-yet-
expired skipped; zero-ref + past-grace reclaimed; idempotent).
Refs: ADR-0042
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…es load-check
The engine-loader capability pass (ADR-0044 §2): validates every agent node's authored output_modalities
against its resolved model's media.outputCombinations MEMBERSHIP, using a host-provided model->capabilities
catalog (WorkflowModelCatalog, sourced from the DB model_catalog). Runs as a separate pass because
WorkflowSchema.superRefine has no catalog (the core->llm parse-time dependency, not circular). Behavior:
- a requested combination must EXACTLY match one declared outputCombinations entry (a per-model combination
constraint, not a subset of their union);
- a model absent from the catalog is DEFERRED (no error — unresolvable at load; the runtime FallbackChain
per-modality pre-skip is the backstop, never a silent drop);
- an incapable model throws a field-named WorkflowValidationError listing each offending node (secret-free:
a model id + the modality set).
+5 tests (member passes; non-member throws field-named; unresolvable defers; no-modalities/no-model skipped;
exact-match required).
Refs: ADR-0044, ADR-0031
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…run.id namespace
Land save_to (ADR-0044 §2): the engine resolves an output node's save_to path
template and its single produced media handle to bytes, then writes via a
host-injected MediaWritePort — policy in the platform-pure engine, the
filesystem mechanism in @relavium/db.
- shared: MediaWritePort + MediaWriteResult — the host save_to write contract
(relativePath, bytes, signal) -> { bytesWritten }, not best-effort (a write
failure fails the node, unlike the retention MediaReferencePort).
- core/interpolation: a new run.id namespace (kind 'run'); RunScope.runId,
resolved only where a runId is in scope (today save_to) — a {{ run.id }}
elsewhere is a typed unresolved_reference, never an empty string. The lexer
lookahead stops run.identity/run.id_x from collapsing to run.id; run.outputs
stays the node namespace.
- core/engine: ExecutionHost.mediaWrite?; #applySaveTo/#performSaveTo run after
the output executor completes — de-inline the captured output to its handle
(deInlineMedia is non-mutating, so state.output keeps the raw in-flight form),
require exactly one handle, MediaStore.get -> MediaWritePort. save_to resolves
ONLY {{ run.id }} (no inputs/ctx/run.outputs into a filesystem path). Secret-
free typed failures (validation / internal); never echoes the path/bytes/host
reason (I3).
- db: createFilesystemMediaWrite(scopeRoot) — fail-closed jail (reject absolute/
drive/UNC/.., realpath the root, verify the deepest existing ancestor is in-
root BEFORE mkdir, refuse a final-component symlink, atomic temp+rename).
- docs: workflow-yaml-spec (save_to resolves run.id only) + security-review
(§Media byte delivery — the save_to write jail), both canonical homes.
Toolchain: lint/typecheck/test/build 16/16 green; prettier clean; Leakwatch 0.
Refs: ADR-0044, ADR-0031, ADR-0042
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nance
Add a disjoint media cost class to the ADR-0028 pre-egress governor + the
realized cost fold (ADR-0044 §3). Media bills per image / audio-second /
video-second, never in tokens, folded into the SAME max_cost_microcents cap —
no new cap dimension, no new event/error class.
- llm/pricing: ModelPricing.mediaOutputRates? (µ¢ per image/audio-sec/video-sec);
every shipped row leaves it undefined (no model prices media output yet).
- llm/cost-tracker: mediaCost(pricing, mediaUnits) — a disjoint addend folded into
cost(); prices only output-direction entries whose unit matches the modality's
billed unit (image=count, audio/video=second); a missing rate or a token-`count`
audio unit is observability-only (degrade-to-0, H4). Realized fold flows into
cumulativeCostMicrocents.
- llm/budget-estimator: estimateMediaCost(model, estimate) — Σ units×rate; unpriced
modality degrades to 0; UnknownModelError for an unlisted model (governor catches).
- core/agent-turn: widen PreEgressHook + AgentTurnParams with outputModalities? +
mediaUnitsEstimate?; awaitPreEgress forwards them (the chain's narrower PreAttemptHook
is unchanged — media is gated at the loop-top vs the primary model).
- core/budget-governor: evaluate/checkPreEgress fold estimateMediaCost into the
projection; the unbounded short-circuit + the UnknownModelError degrade-to-allow
stay before/around it.
- core/agent-runner: buildMediaUnitsEstimate(output_modalities, [defaults].media_cost_estimate)
— one entry per BILLED modality (text/document excluded), config count else the
built-in DEFAULT_MEDIA_UNIT_ESTIMATE; AgentRunnerDeps.mediaCostEstimate.
- shared/config: [defaults].media_cost_estimate (per-modality unit COUNT default).
- db: model_catalog gains nullable media_{image,audio,video}_cost_microcents columns
(the ModelPricing.mediaOutputRates projection) + migration 0003.
- docs: config-spec ([defaults.media_cost_estimate]) + database-schema (model_catalog
media-rate columns), the canonical homes.
Toolchain: lint/typecheck/test/build 16/16 green; prettier clean; Leakwatch 0.
Refs: ADR-0044, ADR-0028, ADR-0031
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apply the two MEDIUM findings + cheap LOWs from the P4 byte-delivery security
review (two independent adversarial passes — no blockers/highs; no traversal /
symlink-escape / arbitrary-write reachable):
- core/engine #performSaveTo: classify an InterpolationError (a bad save_to path
TEMPLATE — e.g. a non-run.id reference resolving to nothing) as `validation`,
not `internal`; a genuine write failure stays `internal`. Secret-free message.
- db/media-write: a reason-only MediaWriteError + a boundary that re-wraps any RAW
Node fs error (realpath/mkdir/writeFile/rename/lstat — whose message/path/dest
carry the absolute path) so the resolved path can never escape the port (the I3
self-defending contract, regardless of caller). Atomic-publish temp now reclaimed
in a `finally` on ANY non-success exit (writeFile/rename throw or cancel) — no
orphaned `.tmp`. Documented the (write-free) stray-empty-dir residual on a
symlinked-ancestor mkdir.
- core/agent-runner buildMediaUnitsEstimate: replace the `as MediaBilledModality`
cast with an `isBilledModality` type guard (narrow without a cast).
- Tests: save_to bad-template → validation; scope-root-missing → reason-only
MediaWriteError with no path leak.
Toolchain: lint/typecheck/test/build 16/16 green; prettier clean; Leakwatch 0.
Refs: ADR-0044
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bring the roadmap status current: PR #34 (P3 + P4/D13) merged; the P4 remainder
(D12, D11, D15, D16, D17 + the byte-delivery review + canonical-home docs) is
landed on `development`, pending merge. The keychain no-raw-key IPC test is
recorded as deferred to 1.AH (no Phase-1 desktop surface; ADR-0044 §4). The
matrix row stays ◇ until the PR merges (Roadmap-Done-After-Merge).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apply the real findings from the 46-agent review of the P4 changeset (9
dimensions, every finding double-verified; 0 blockers/highs in reachable code).
A — real defects fixed:
- A1 [high] output_modalities is now lowered onto the LlmRequest (buildRequest),
so the FallbackChain output-combination pre-skip — the runtime backstop the
D15 load-check defers to (ADR-0044 §2) — actually fires; previously an
incapable model silently returned text. + a skip-backstop test (incapable
model → chain exhausts → typed failure, never a text drop).
- A2 [med] reconcile() best-effort reclaims a crashed non-resumable run's media
refs (#bestEffortReclaim), so a crash no longer permanently orphans partial
media (ADR-0042 §4); retention failure never abandons reconciliation. + tests.
- A3 [med] boundForModel is media-aware: a read_media/media tool result's inline
base64 is redacted from the text path (toText), so it can never serialize into
agent:tool_result.outputSummary / the spill / the over-cap preview — the I3 gap
the emit-time deInlineMedia choke point cannot catch (a base64 substring inside
a flat string). The model-facing value keeps the bytes (rides the seam). + tests.
- A4 [low] read_media whole-handle read of a 0-byteLength handle returns an empty
source instead of tripping the default end = byteLength-1 = -1 range rejection. + tests.
B — cheap hardening:
- wx (O_CREAT|O_EXCL) on the save_to temp write — refuses a pre-existing symlink
at the temp path (defense-in-depth atop the unguessable UUID name).
- async-rejection arm of the run-loop best-effort media-ref port now tested
(no unhandled rejection); run.id no-spurious-edge / not-a-secret-source /
not-a-pre-run-violation regressions pinned in dag/analyze tests.
- documented the url-bearing save_to double-fetch tradeoff.
D — recorded the deferred HOST-WIRING half (D12 host MediaReadAccess + session-
scope population, the D15 loader call, the D17 config/resolveForEgress wiring) in
deferred-tasks.md §1.AF P4 + the roadmap, so D12/D15/D17 are not read as live
end-to-end — the engine policy is complete + tested; the surface wiring is 1.AH.
Toolchain: lint/typecheck/test/build 16/16 green (786 core tests); prettier
clean; Leakwatch 0.
Refs: ADR-0044, ADR-0042, ADR-0031, ADR-0028
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@sourcery-aisourcery-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @cemililik, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitaiBot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 29c9543f-74da-4cd3-ab8a-56745cb00df0

📥 Commits

Reviewing files that changed from the base of the PR and between e6e8545 and a6a4e55.

📒 Files selected for processing (18)
  • docs/roadmap/deferred-tasks.md
  • packages/core/src/engine/agent-runner.ts
  • packages/core/src/engine/agent-turn.ts
  • packages/core/src/engine/engine.test.ts
  • packages/core/src/interpolation/analyze.test.ts
  • packages/core/src/interpolation/references.ts
  • packages/core/src/interpolation/resolve.test.ts
  • packages/core/src/tools/bounding.test.ts
  • packages/core/src/tools/bounding.ts
  • packages/core/src/tools/builtins.test.ts
  • packages/core/src/tools/builtins.ts
  • packages/db/src/media-reference-store.test.ts
  • packages/db/src/media-reference-store.ts
  • packages/db/src/media-write.test.ts
  • packages/llm/src/pricing.ts
  • packages/shared/src/config.ts
  • packages/shared/src/content.test.ts
  • packages/shared/src/content.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/roadmap/deferred-tasks.md
🚧 Files skipped from review as they are similar to previous changes (13)
  • packages/llm/src/pricing.ts
  • packages/db/src/media-reference-store.test.ts
  • packages/shared/src/config.ts
  • packages/shared/src/content.test.ts
  • packages/core/src/engine/agent-runner.ts
  • packages/core/src/tools/bounding.ts
  • packages/core/src/interpolation/references.ts
  • packages/db/src/media-reference-store.ts
  • packages/core/src/interpolation/analyze.test.ts
  • packages/shared/src/content.ts
  • packages/core/src/engine/agent-turn.ts
  • packages/db/src/media-write.test.ts
  • packages/core/src/engine/engine.test.ts

📝 Walkthrough

Walkthrough

Phase 1 / P4 multimodal media delivery: adds {{ run.id }} as a first-class interpolation namespace, a read_media built-in tool with scope-set authz and inclusive byte-range gate, per-modality media cost governance wired through BudgetGovernor, a save_to write port backed by a symlink-averse jailed filesystem writer, durable media reference recording and terminal reclamation in the engine, catalog-aware output-modality validation, DB schema columns and migration for media cost rates, and corresponding docs/roadmap updates.

Changes

1.AF P4 Multimodal Media Delivery

Layer / File(s)Summary
Shared media auth contracts, host ports, and config schema
packages/shared/src/content.ts, packages/shared/src/content.test.ts, packages/shared/src/config.ts, packages/shared/src/config.test.ts
Defines ScopeSchema/Scope, scopeSetIncludes, DurableMediaMeta, MediaReferencePort, MediaWritePort, collectDurableMediaHandles, and the MediaCostEstimate config schema with strict Zod validation; all covered with unit tests.
{{ run.id }} interpolation namespace
packages/core/src/interpolation/references.ts, packages/core/src/interpolation/scope.ts, packages/core/src/interpolation/resolve.ts, packages/core/src/interpolation/references.test.ts, packages/core/src/interpolation/resolve.test.ts, packages/core/src/interpolation/analyze.test.ts, packages/core/src/dag.test.ts, docs/reference/contracts/workflow-yaml-spec.md
Adds run to ReferenceKind, introduces a RUN_ID regex with negative lookahead, adds RunScope.runId, resolves run.id to scope.runId (or unresolved_reference), and confirms the reference creates no DAG dependency edge.
Media pricing: ModelPricing rates, cost tracker, budget estimator, DB columns
packages/llm/src/pricing.ts, packages/llm/src/cost-tracker.ts, packages/llm/src/budget-estimator.ts, packages/llm/src/index.ts, packages/llm/src/cost-tracker.test.ts, packages/llm/src/budget-estimator.test.ts, packages/db/src/schema.ts, packages/db/drizzle/0003_cloudy_wind_dancer.sql, packages/db/drizzle/meta/*
Adds mediaOutputRates to ModelPricing, implements mediaCost as a disjoint addend in cost(), adds estimateMediaCost/MediaUnitsEstimate for pre-egress projection, and extends model_catalog with three nullable microcent columns plus migration and Drizzle snapshot.
read_media built-in tool: types, authz, range gate, base64 redaction
packages/core/src/tools/types.ts, packages/core/src/tools/errors.ts, packages/core/src/tools/builtins.ts, packages/core/src/tools/registry.test.ts, packages/core/src/tools/bounding.ts, packages/core/src/tools/bounding.test.ts, packages/core/src/tools/builtins.test.ts
Introduces MediaHandleInfo, MediaReadAccess, ToolDispatchContext.mediaRead/requestingScope, media_scope_denied deny reason, and readMediaTool with handle validation, scope authz, zero-byte short-circuit, and range gating; also redacts base64 from model-facing summaries.
Agent runner & turn: output_modalities forwarding and media cost governance
packages/core/src/engine/agent-runner.ts, packages/core/src/engine/agent-turn.ts, packages/core/src/engine/budget-governor.ts, packages/core/src/engine/agent-runner.test.ts, packages/core/src/engine/budget-governor.test.ts
Extends AgentRunnerDeps with mediaCostEstimate, adds DEFAULT_MEDIA_UNIT_ESTIMATE/buildMediaUnitsEstimate, threads outputModalities and mediaUnitsEstimate through AgentTurnParams and PreEgressHook into BudgetGovernor.evaluatePreEgress/checkPreEgress.
validateWorkflowWithCatalog catalog-aware output-modality validation
packages/core/src/validate-catalog.ts, packages/core/src/validate-catalog.test.ts, packages/core/src/index.ts
Adds WorkflowModelCatalog type and validateWorkflowWithCatalog that checks each agent node's output_modalities against the model's declared output combinations, throws WorkflowValidationError on mismatch, and is exported from the core public surface.
DB media-reference store and jailed filesystem media-write
packages/db/src/media-reference-store.ts, packages/db/src/media-reference-store.test.ts, packages/db/src/media-write.ts, packages/db/src/media-write.test.ts, packages/db/src/index.ts
Implements createMediaReferenceStore (idempotent object upsert, run-scoped reference tracking, scope-based describe, grace-window GC), createMediaReferencePort adapter, and createFilesystemMediaWrite with fail-closed realpath jail, symlink refusal, and atomic wx+rename publish.
ExecutionHost: mediaReferences and mediaWrite port wiring
packages/core/src/engine/execution-host.ts
Extends ExecutionHost interface and createInMemoryHost with optional mediaReferences and mediaWrite ports via conditional spreads.
Engine: save_to delivery, media reference lifecycle, and reconcile reclaim
packages/core/src/engine/engine.ts, packages/core/src/engine/engine.test.ts
Adds #applySaveTo/#performSaveTo to resolve save_to path templates, enforce exactly-one-handle semantics, and write via host mediaWrite; adds #recordProducedMedia/#reclaimRunMedia called from #emitDurable for terminal sweeps; adds #bestEffortReclaim in reconcile for crashed runs; 316-line test expansion covers all paths.
Docs and roadmap updates
docs/reference/contracts/config-spec.md, docs/reference/desktop/database-schema.md, docs/standards/security-review.md, docs/roadmap/current.md, docs/roadmap/deferred-tasks.md, docs/roadmap/phases/phase-1-engine-and-llm.md
Updates config-spec with [defaults.media_cost_estimate], database-schema with media cost column docs, security-review with save_to jailed-write checklist, and roadmap current/deferred/phase-1 files with P4 landing status and deferred host-wiring obligations.

Sequence Diagrams

sequenceDiagram
participant Engine
participant applySaveTo
participant resolveTemplate
participant collectDurableMediaHandles
participant MediaWritePort
participant MediaReferencePort
participant BudgetGovernor
rect rgba(100, 149, 237, 0.5)
note over Engine,BudgetGovernor: Pre-egress media cost check
Engine->>BudgetGovernor: checkPreEgress(model, maxTokens, mediaUnitsEstimate)
BudgetGovernor-->>Engine: allow | warn | fail
end
rect rgba(60, 179, 113, 0.5)
note over Engine,MediaReferencePort: Post-turn: save_to and media reference lifecycle
Engine->>applySaveTo: completed executor result
applySaveTo->>resolveTemplate: save_to template + run.id scope
resolveTemplate-->>applySaveTo: resolved relative path
applySaveTo->>collectDurableMediaHandles: de-inlined output value
collectDurableMediaHandles-->>applySaveTo: DurableMediaMeta[]
applySaveTo->>MediaWritePort: write(relativePath, bytes, signal)
MediaWritePort-->>applySaveTo: MediaWriteResult
applySaveTo-->>Engine: NodeSuccess | NodeFailure
Engine->>MediaReferencePort: recordRunMedia(runId, meta)
Engine->>MediaReferencePort: reclaimRun(runId) on terminal
end
Loading
sequenceDiagram
participant LLM
participant readMediaTool
participant scopeSetIncludes
participant validateByteRange
participant MediaReadAccess
participant bounding_toText
LLM->>readMediaTool: dispatch(handle, start?, end?)
readMediaTool->>MediaReadAccess: describe(handle)
MediaReadAccess-->>readMediaTool: MediaHandleInfo | undefined
readMediaTool->>scopeSetIncludes: allowedScopes vs requestingScope
scopeSetIncludes-->>readMediaTool: granted | ToolPolicyError
readMediaTool->>validateByteRange: (start, end, byteLength)
validateByteRange-->>readMediaTool: valid | ToolArgsInvalidError
readMediaTool->>MediaReadAccess: readRange(handle, range)
MediaReadAccess-->>readMediaTool: bytes
readMediaTool-->>LLM: MediaPart{mimeType, base64}
LLM->>bounding_toText: build summary
bounding_toText->>bounding_toText: redactInlineMediaForText (remove base64)
bounding_toText-->>LLM: byte-free descriptor
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • HodeTech/Relavium#7: This PR extends the CostTracker/ModelPricing foundation from that PR by adding mediaOutputRates and a mediaCost disjoint addend.
  • HodeTech/Relavium#11: This PR wires outputModalities/mediaUnitsEstimate through the LLM request and pre-egress budget seam shapes introduced in that PR.
  • HodeTech/Relavium#33: This PR extends the deInlineMedia/MediaStore plumbing from that PR by adding save_to byte writing and media-reference recording/reclamation.

Poem

🐇 Hoppity-hop through frames of light,
The rabbit jails each byte just right—
run.id resolves, the scope is clear,
No base64 shall leak from here!
Media costs are tallied neat,
Phase 2 is now a tasty treat. 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 72.34% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change—completing media access and spend governance (1.AF P4)—matching the PR's primary objective and scope.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch development

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assistgemini-code-assistBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the remaining Phase-1 engine media plumbing (1.AF P4), introducing the read_media built-in tool with scope-set authorization, the save_to write port with a fail-closed filesystem jail, terminal-state media reference reclamation, and catalog-aware workflow validation. Feedback on the changes highlights two key improvements: adding optional chaining to prevent a potential TypeError when accessing model capabilities in the catalog validator, and explicitly handling Map and Set instances in the inline media redactor to avoid data loss during text serialization.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

if (caps === undefined) {
continue; // unresolvable model — defer to the runtime FallbackChain pre-skip (never a silent drop)
}
if (!isOutputComboSupported(caps.media.outputCombinations, node.output_modalities)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If caps.media is undefined or null (for example, in older or custom model definitions), accessing caps.media.outputCombinations will throw a TypeError. Using optional chaining prevents potential runtime crashes.

Suggested change
if(!isOutputComboSupported(caps.media.outputCombinations,node.output_modalities)){
if(!isOutputComboSupported(caps.media?.outputCombinations??[],node.output_modalities)){

Comment on lines +80 to +104
function redactInlineMediaForText(value: unknown, seen: WeakSet<object>): unknown {
if (typeof value === 'string') {
return isBase64DataUri(value) ? '[base64 data URI omitted]' : value;
}
if (typeof value !== 'object' || value === null) {
return value;
}
if (seen.has(value)) {
return '[cyclic]';
}
seen.add(value);
if (Array.isArray(value)) {
return value.map((item) => redactInlineMediaForText(item, seen));
}
const record = value as Record<string, unknown>;
if (isCanonicalBase64Source(record)) {
const data = record['data'];
return { kind: 'base64', bytes: typeof data === 'string' ? data.length : 0 }; // drop the bytes
}
const out: Record<string, unknown> = {};
for (const [key, item] of Object.entries(record)) {
out[key] = redactInlineMediaForText(item, seen);
}
return out;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The redactInlineMediaForText function does not handle Map and Set instances. Since pushMediaWalkChildren explicitly supports walking Map and Set objects, these types can be present in the tool results. If they are not handled here, they will be treated as plain records, which converts them to empty objects {} and results in data loss in the text summary/spill/preview. Handling Map and Set explicitly ensures their contents are correctly redacted and preserved.

functionredactInlineMediaForText(value: unknown,seen: WeakSet<object>): unknown{if(typeofvalue==='string'){returnisBase64DataUri(value) ? '[base64 data URI omitted]' : value;}if(typeofvalue!=='object'||value===null){returnvalue;}if(seen.has(value)){return'[cyclic]';}seen.add(value);if(Array.isArray(value)){returnvalue.map((item)=>redactInlineMediaForText(item,seen));}if(valueinstanceofMap){constout=newMap();for(const[key,item]ofvalue.entries()){out.set(redactInlineMediaForText(key,seen),redactInlineMediaForText(item,seen));}returnout;}if(valueinstanceofSet){constout=newSet();for(constitemofvalue.values()){out.add(redactInlineMediaForText(item,seen));}returnout;}constrecord=valueasRecord<string,unknown>;if(isCanonicalBase64Source(record)){constdata=record['data'];return{kind: 'base64',bytes: typeofdata==='string' ? data.length : 0};// drop the bytes}constout: Record<string,unknown>={};for(const[key,item]ofObject.entries(record)){out[key]=redactInlineMediaForText(item,seen);}returnout;}

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
packages/db/src/media-reference-store.test.ts (1)

86-101: ⚡ Quick win

Add a regression case for “reclaimed then re-recorded” handles.

The suite currently validates reclaim behavior, but not reintroduction of the same handle afterward. Adding that case will lock the lifecycle contract and prevent silent authz regressions.

✅ Suggested test extension
 it('reclaimExpired soft-deletes only zero-ref objects past the grace window (D11 GC)', () => {
@@
expect(store.reclaimExpired(0)).toEqual([HANDLE]);
expect(store.describe(HANDLE)).toBeUndefined(); // soft-deleted
expect(store.reclaimExpired(0)).toEqual([]); // idempotent — already deleted
++ // Re-recording the same content-addressed handle should revive it for describe/authz.+ record();+ store.addReference(HANDLE, 'session', 's2');+ expect(store.describe(HANDLE)?.allowedScopes).toEqual([{ kind: 'session', id: 's2' }]);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/db/src/media-reference-store.test.ts` around lines 86 - 101, The
test validates the reclaim behavior and idempotency but does not verify what
happens when a reclaimed handle is re-recorded. After the final
expect(store.reclaimExpired(0)).toEqual([]) assertion that confirms idempotency,
add a regression case by re-recording the same HANDLE (call record() again),
then verify that store.describe(HANDLE) returns a defined value again and that
the handle can re-enter the normal lifecycle. This locks the contract and
prevents silent authorization regressions when handles transition from reclaimed
state back into active use.
packages/core/src/tools/builtins.test.ts (1)

373-375: ⚡ Quick win

Replace unsafe type assertion with a runtime type guard.

Line 374 uses an unsafe as cast; narrow err with instanceof before reading .reason.

As per coding guidelines, **/*.{ts,tsx} requires “no any types or unsafe as casts” and to “Use type guards instead of unsafe type assertions.”

Suggested change
 expect(err).toBeInstanceOf(ToolPolicyError);
- expect((err as ToolPolicyError).reason).toBe('media_scope_denied');+ if (!(err instanceof ToolPolicyError)) {+ throw err;+ }+ expect(err.reason).toBe('media_scope_denied');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/core/src/tools/builtins.test.ts` around lines 373 - 375, Replace the
unsafe type assertion `(err as ToolPolicyError).reason` with a proper runtime
type guard. Add an `instanceof ToolPolicyError` check to narrow the type of err
before accessing the `.reason` property. This ensures type safety without using
the unsafe `as` cast, following the coding guidelines that require type guards
instead of unsafe type assertions.

Source: Coding guidelines

packages/core/src/tools/bounding.ts (1)

94-101: ⚡ Quick win

Replace the unsafe type assertion with a type guard.

Line 94 uses value as Record<string, unknown>, which violates the strict TypeScript rule for unsafe assertions.

♻️ Suggested refactor
+function isRecord(value: unknown): value is Record<string, unknown> {+ return typeof value === 'object' && value !== null;+}+
function redactInlineMediaForText(value: unknown, seen: WeakSet<object>): unknown {
if (typeof value === 'string') {
return isBase64DataUri(value) ? '[base64 data URI omitted]' : value;
}
- if (typeof value !== 'object' || value === null) {+ if (!isRecord(value)) {
return value;
}
if (seen.has(value)) {
return '[cyclic]';
}
seen.add(value);
if (Array.isArray(value)) {
return value.map((item) => redactInlineMediaForText(item, seen));
}
- const record = value as Record<string, unknown>;+ const record = value;

As per coding guidelines, “TypeScript-first, strict. No any, no unsafe as. Use type guards instead of unsafe type assertions.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/core/src/tools/bounding.ts` around lines 94 - 101, The unsafe type
assertion `value as Record<string, unknown>` on line 94 violates strict
TypeScript guidelines. Replace this assertion with a proper type guard function
that validates whether the value is actually a Record before proceeding with
operations like isCanonicalBase64Source check and Object.entries iteration.
Create a type guard that checks if value is an object type, then use that guard
to conditionally execute the Record-related operations instead of relying on the
unsafe as keyword.

Source: Coding guidelines

packages/llm/src/pricing.ts (1)

50-54: ⚡ Quick win

Use a modality-derived mapped type for mediaOutputRates to prevent key drift.

Hard-coding modality keys here can silently diverge from the shared canonical media modality set over time. A mapped type keeps this contract synchronized at compile time.

♻️ Proposed refactor
+import type { MediaBilledModality } from '`@relavium/shared`';
...
- readonly mediaOutputRates?: {- readonly image?: number;- readonly audio?: number;- readonly video?: number;- };+ readonly mediaOutputRates?: Partial<Record<MediaBilledModality, number>>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/llm/src/pricing.ts` around lines 50 - 54, The mediaOutputRates
property in the pricing.ts file uses hard-coded modality keys (image, audio,
video) that can drift from the canonical media modality set over time. Replace
the explicit object type definition for mediaOutputRates with a mapped type that
derives from a shared canonical modalities type, ensuring the keys stay
synchronized at compile time. This mapped type approach will automatically
reflect any changes to the canonical modality set without requiring manual
updates to this interface.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/src/engine/agent-runner.ts`:
- Around line 137-140: The isBilledModality function uses an unsafe cast `as
readonly string[]` that removes literal type information from
MEDIA_BILLED_MODALITIES, violating TypeScript safety guidelines. Replace the
current implementation by creating a ReadonlySet from MEDIA_BILLED_MODALITIES
and using its .has() method instead of .includes() with the cast. This maintains
the literal types from the const-asserted array and eliminates the unsafe type
casting.
In `@packages/core/src/tools/bounding.ts`:
- Around line 115-123: The redaction call to redactInlineMediaForText is
currently executed outside the try-catch block, which means any thrown errors
from getters or proxies in the tool output during redaction will not be caught
and will cause the function to fail ungracefully. Move the logic that assigns to
the safe variable (the conditional check for containsInlineMediaBytes and the
call to redactInlineMediaForText) inside the try block so that any throws during
the redaction traversal are caught by the existing error handler and allow the
function to degrade safely to the "[unserializable]" fallback.
In `@packages/db/drizzle/0003_cloudy_wind_dancer.sql`:
- Around line 1-3: The Drizzle migration artifacts in the
0003_cloudy_wind_dancer.sql file are out of sync with the current schema
definition in src/schema.ts, causing CI to fail with schema-drift errors.
Regenerate all Drizzle migration files by running the database generation
command for the `@relavium/db` package, then commit both the resulting SQL
migration files and the Drizzle metadata files together to ensure the migration
state matches the current schema definition.
In `@packages/db/src/media-reference-store.ts`:
- Around line 86-89: The issue is that when a handle is re-recorded (already
exists in the database), the onConflictDoUpdate call on mediaObjects.handle only
refreshes the lastReferencedAt timestamp but does not clear the deletedAt field
that may have been set during garbage collection soft-deletion. This causes
describe() to keep returning undefined for reintroduced content. In the set
object of the onConflictDoUpdate call, add deletedAt: null alongside the
lastReferencedAt: ts update to ensure that soft-deleted handles are properly
restored when they are re-recorded.
In `@packages/db/src/schema.ts`:
- Around line 111-113: The monetary rate columns mediaImageCostMicrocents,
mediaAudioCostMicrocents, and mediaVideoCostMicrocents are defined as integer
fields without non-negative constraints, allowing them to persist negative
values which can undermine budget enforcement. Add a non-negative constraint
(such as .unsigned() or a check constraint ensuring values are greater than or
equal to 0) to each of these three integer column definitions in the schema.
Apply this constraint to all instances of these columns throughout the schema,
including the sections mentioned in lines 125-133.
In `@packages/shared/src/config.ts`:
- Around line 104-107: The ADR relative link path in the JSDoc comment block for
the media-output unit-count default is rooted incorrectly. Update the link
reference `../decisions/0044-media-access-governance-read-media-save-to-cost.md`
to use the correct relative path from the file location
packages/shared/src/config.ts to the actual decisions directory location.
Traverse up the appropriate number of directory levels from src/ to reach the
project root and then navigate to the decisions folder, ensuring the relative
link properly points to the ADR file.
---
Nitpick comments:
In `@packages/core/src/tools/bounding.ts`:
- Around line 94-101: The unsafe type assertion `value as Record<string,
unknown>` on line 94 violates strict TypeScript guidelines. Replace this
assertion with a proper type guard function that validates whether the value is
actually a Record before proceeding with operations like isCanonicalBase64Source
check and Object.entries iteration. Create a type guard that checks if value is
an object type, then use that guard to conditionally execute the Record-related
operations instead of relying on the unsafe as keyword.
In `@packages/core/src/tools/builtins.test.ts`:
- Around line 373-375: Replace the unsafe type assertion `(err as
ToolPolicyError).reason` with a proper runtime type guard. Add an `instanceof
ToolPolicyError` check to narrow the type of err before accessing the `.reason`
property. This ensures type safety without using the unsafe `as` cast, following
the coding guidelines that require type guards instead of unsafe type
assertions.
In `@packages/db/src/media-reference-store.test.ts`:
- Around line 86-101: The test validates the reclaim behavior and idempotency
but does not verify what happens when a reclaimed handle is re-recorded. After
the final expect(store.reclaimExpired(0)).toEqual([]) assertion that confirms
idempotency, add a regression case by re-recording the same HANDLE (call
record() again), then verify that store.describe(HANDLE) returns a defined value
again and that the handle can re-enter the normal lifecycle. This locks the
contract and prevents silent authorization regressions when handles transition
from reclaimed state back into active use.
In `@packages/llm/src/pricing.ts`:
- Around line 50-54: The mediaOutputRates property in the pricing.ts file uses
hard-coded modality keys (image, audio, video) that can drift from the canonical
media modality set over time. Replace the explicit object type definition for
mediaOutputRates with a mapped type that derives from a shared canonical
modalities type, ensuring the keys stay synchronized at compile time. This
mapped type approach will automatically reflect any changes to the canonical
modality set without requiring manual updates to this interface.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3ba5d197-b0c8-478e-9859-db16f2c01d63

📥 Commits

Reviewing files that changed from the base of the PR and between d0faf38 and e6e8545.

📒 Files selected for processing (51)
  • docs/reference/contracts/config-spec.md
  • docs/reference/contracts/workflow-yaml-spec.md
  • docs/reference/desktop/database-schema.md
  • docs/roadmap/current.md
  • docs/roadmap/deferred-tasks.md
  • docs/roadmap/phases/phase-1-engine-and-llm.md
  • docs/standards/security-review.md
  • packages/core/src/dag.test.ts
  • packages/core/src/engine/agent-runner.test.ts
  • packages/core/src/engine/agent-runner.ts
  • packages/core/src/engine/agent-turn.ts
  • packages/core/src/engine/budget-governor.test.ts
  • packages/core/src/engine/budget-governor.ts
  • packages/core/src/engine/engine.test.ts
  • packages/core/src/engine/engine.ts
  • packages/core/src/engine/execution-host.ts
  • packages/core/src/index.ts
  • packages/core/src/interpolation/analyze.test.ts
  • packages/core/src/interpolation/references.test.ts
  • packages/core/src/interpolation/references.ts
  • packages/core/src/interpolation/resolve.test.ts
  • packages/core/src/interpolation/resolve.ts
  • packages/core/src/interpolation/scope.ts
  • packages/core/src/tools/bounding.test.ts
  • packages/core/src/tools/bounding.ts
  • packages/core/src/tools/builtins.test.ts
  • packages/core/src/tools/builtins.ts
  • packages/core/src/tools/errors.ts
  • packages/core/src/tools/registry.test.ts
  • packages/core/src/tools/types.ts
  • packages/core/src/validate-catalog.test.ts
  • packages/core/src/validate-catalog.ts
  • packages/db/drizzle/0003_cloudy_wind_dancer.sql
  • packages/db/drizzle/meta/0003_snapshot.json
  • packages/db/drizzle/meta/_journal.json
  • packages/db/src/index.ts
  • packages/db/src/media-reference-store.test.ts
  • packages/db/src/media-reference-store.ts
  • packages/db/src/media-write.test.ts
  • packages/db/src/media-write.ts
  • packages/db/src/schema.ts
  • packages/llm/src/budget-estimator.test.ts
  • packages/llm/src/budget-estimator.ts
  • packages/llm/src/cost-tracker.test.ts
  • packages/llm/src/cost-tracker.ts
  • packages/llm/src/index.ts
  • packages/llm/src/pricing.ts
  • packages/shared/src/config.test.ts
  • packages/shared/src/config.ts
  • packages/shared/src/content.test.ts
  • packages/shared/src/content.ts

Comment threadpackages/core/src/engine/agent-runner.ts
Comment threadpackages/core/src/tools/bounding.ts Outdated
Comment on lines +1 to +3
ALTER TABLE `model_catalog` ADD `media_image_cost_microcents` integer;--> statement-breakpoint
ALTER TABLE `model_catalog` ADD `media_audio_cost_microcents` integer;--> statement-breakpoint
ALTER TABLE `model_catalog` ADD `media_video_cost_microcents` integer; No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Regenerate and recommit Drizzle artifacts — current migration set is out of sync.

CI is failing with explicit schema-drift errors for @relavium/db. Please rerun generation and commit the resulting SQL/meta files together so migration state matches src/schema.ts and derived checks.

pnpm --filter `@relavium/db` db:generate
🧰 Tools
🪛 GitHub Actions: CI / 2_lint · typecheck · test.txt

[error] 1-1: Migration out of sync: The committed @relavium/db migration is out of sync with src/schema.ts (or an upstream @relavium/shared enum the checks derive from). Regenerate and commit: pnpm --filter @relavium/db db:generate

🪛 GitHub Actions: CI / lint · typecheck · test

[error] 1-1: The committed @relavium/db migration is out of sync with src/schema.ts (or an upstream @relavium/shared enum the CHECKs derive from). Regenerate and commit: pnpm --filter @relavium/db db:generate

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/db/drizzle/0003_cloudy_wind_dancer.sql` around lines 1 - 3, The
Drizzle migration artifacts in the 0003_cloudy_wind_dancer.sql file are out of
sync with the current schema definition in src/schema.ts, causing CI to fail
with schema-drift errors. Regenerate all Drizzle migration files by running the
database generation command for the `@relavium/db` package, then commit both the
resulting SQL migration files and the Drizzle metadata files together to ensure
the migration state matches the current schema definition.

Source: Pipeline failures

Comment threadpackages/db/src/media-reference-store.ts Outdated
Comment on lines +111 to +113
mediaImageCostMicrocents: integer('media_image_cost_microcents'),
mediaAudioCostMicrocents: integer('media_audio_cost_microcents'),
mediaVideoCostMicrocents: integer('media_video_cost_microcents'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add non-negative DB checks for the new media rate columns.

media_*_cost_microcents are monetary rate fields, but they currently allow negative values. Negative persisted rates can underprice projected media cost and weaken budget enforcement.

Suggested fix
 export const modelCatalog = sqliteTable(
'model_catalog',
{
@@
mediaImageCostMicrocents: integer('media_image_cost_microcents'),
mediaAudioCostMicrocents: integer('media_audio_cost_microcents'),
mediaVideoCostMicrocents: integer('media_video_cost_microcents'),
@@
},
(t) => [
+ check(+ 'model_catalog_media_image_cost_non_negative',+ sql`${t.mediaImageCostMicrocents} is null or ${t.mediaImageCostMicrocents} >= 0`,+ ),+ check(+ 'model_catalog_media_audio_cost_non_negative',+ sql`${t.mediaAudioCostMicrocents} is null or ${t.mediaAudioCostMicrocents} >= 0`,+ ),+ check(+ 'model_catalog_media_video_cost_non_negative',+ sql`${t.mediaVideoCostMicrocents} is null or ${t.mediaVideoCostMicrocents} >= 0`,+ ),
uniqueIndex('idx_model_catalog_provider_model')
.on(t.providerId, t.modelId)
.where(sql`${t.deletedAt} is null`),

Also applies to: 125-133

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/db/src/schema.ts` around lines 111 - 113, The monetary rate columns
mediaImageCostMicrocents, mediaAudioCostMicrocents, and mediaVideoCostMicrocents
are defined as integer fields without non-negative constraints, allowing them to
persist negative values which can undermine budget enforcement. Add a
non-negative constraint (such as .unsigned() or a check constraint ensuring
values are greater than or equal to 0) to each of these three integer column
definitions in the schema. Apply this constraint to all instances of these
columns throughout the schema, including the sections mentioned in lines
125-133.

Comment threadpackages/shared/src/config.ts
cemililikand others added 2 commits June 19, 2026 20:16
…-valid only)
Verified each external/Sonar finding against current code; fixed the still-valid
ones (minimal), skipped the rest with reasons below.
Fixed:
- [correctness] media-reference-store recordObject now clears deleted_at on a
re-record (onConflictDoUpdate set deletedAt:null), so a GC-soft-deleted handle
is RESURRECTED when the same content-addressed bytes are produced again —
previously describe() stayed undefined and read_media denied live re-introduced
content. + a re-record-after-reclaim regression test.
- [robustness] bounding.toText now runs the media-redaction INSIDE the try, so a
throwing getter/proxy during the redaction walk degrades to '[unserializable]'
instead of escaping.
- [TS] agent-runner isBilledModality uses a ReadonlySet<string> (drops the
`as readonly string[]` cast); bounding redactor uses an isRecord guard (drops
the `as Record` cast); builtins.test narrows via `instanceof ToolPolicyError`
(drops the `as` cast).
- [drift-proof] pricing.ts mediaOutputRates is now a mapped type keyed by
MediaBilledModality (was a hand-maintained image/audio/video literal).
- [docs] config.ts ADR-0044 link fixed (../decisions → ../../../docs/decisions).
- [Sonar] references RUN_ID regex uses \w; media-write.test uses String.raw for
the Windows/UNC paths; content.test sort takes an explicit localeCompare
comparator (S2871).
Skipped (verified not actionable):
- Drizzle migration "drift": `pnpm --filter @relavium/db db:generate` reports
"No schema changes" — 0003 is in sync, no CI drift (false positive).
- schema.ts media-rate columns non-negative constraint: the sibling money columns
(input/output/cached microcents) carry no such CHECK either; rates are
catalog-seeded (verified, never negative), not user input; `.unsigned()` is not
a SQLite/Drizzle option — adding a check to only the 3 new columns would be
inconsistent and needs a migration for no real exploit surface.
- validate-catalog caps.media optional chaining: CapabilityFlags requires `media`
(Zod MediaCapabilitiesSchema at the seam), so it cannot be undefined for a valid
catalog result; `?.` would only mask a host contract violation.
- bounding redactor Map/Set handling: a Map/Set collapses to `{}` exactly as
JSON.stringify renders it (non-serializable) — no base64 leaks and no regression
vs the un-redacted JSON path, so no change needed for the I3 purpose.
Toolchain: lint/typecheck/test/build 16/16 green (786 core tests); prettier
clean; Leakwatch 0.
Refs: ADR-0044, ADR-0042
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… only)
A second multi-agent review pass (Sonnet, 9 dimensions, double-verified) on top
of the first review's fixes — 0 blockers/highs in reachable code. Salvaged its
findings and applied the still-valid ones (minimal); deferred a read_media
result-contract cluster to 1.AH and skipped the rest with reasons.
Fixed:
- [I3] bounding redaction now strips RAW BINARY BUFFERS (Uint8Array/ArrayBuffer)
too — a buffer tool result would otherwise JSON-serialize its decimal byte
values into the agent:tool_result.outputSummary/spill. The walk now runs for
every non-string result (so a buffer-only result is caught) and uses a
PLAIN-object guard so Date/Map/Set/class results render natively (not collapsed
to {}); the cycle-safe walk also makes a circular result serialize ('[cyclic]').
- [schema] read_media 0-byte whole-read now returns a HANDLE source, not
`{ kind:'base64', data:'' }` (which violates the base64 nonEmptyString contract
and breaks deInlineMedia's empty-decode).
- [defense] read_media `handle` arg is validated against MEDIA_HANDLE_PATTERN at
parse (engine-pure structural check); durableMediaMetaOf also rejects a
non-pattern ref before recording it.
- [governance] the FallbackChain per-attempt budget hook now carries the media
estimate (outputModalities/mediaUnitsEstimate) so a cross-provider FAILOVER is
media-priced, not token-only (ADR-0044 §3) — was silently dropped.
- [scalability] reclaimExpired chunks its `handle IN (…)` UPDATE under SQLite's
999-bound-parameter floor (a large GC sweep would otherwise throw at runtime).
- [parse] RUN_ID lookahead also excludes `-` so `{{run.id-x}}` is `unknown`, not
a mis-parsed kind:'run'.
- [bound] ScopeSchema.id gains .max(255) (matches the mimeType/name convention).
- [observability] redactor marker field renamed bytes→base64Length (it is the
base64 char length, ~1.33× the byte count); start/end LLM-visible schema gains
minimum:0.
- Tests: denied-scope on a 0-byte handle (authz BEFORE the short-circuit),
malformed-handle parse rejection, array-of-media + binary-buffer + Date
redaction, symlink-jail errors asserted as reason-only MediaWriteError (no path
leak), save_to template-failure message leaks no reference name, run.id
trailing-path → unresolved_reference; corrected a misleading analyze.test comment.
Deferred to 1.AH (recorded in deferred-tasks §1.AF P4) — the read_media result
must carry a HANDLE (not inline base64) to satisfy LlmMessageSchema on a
multi-turn call; narrow MediaReadAccess.readRange + thread AbortSignalLike — all
co-decided with the 1.AH host wiring (read_media is inert today). Plus the
budget-governor non-zero-media-estimate test gap.
Skipped (verified): drift (db:generate = no changes), the `as RunEventDraft`
cast (pre-existing/merged, Zod re-validates at #bus.next), the soft-delete
"orphan media_references" claim (a reclaimed handle is zero-ref by construction,
so no rows exist to orphan), a plain non-data:-URI base64 string (undetectable
without false positives; deInlineMedia is the primary gate), and unbounded
transcript / cyclic-marker cosmetics (by-design).
Toolchain: lint/typecheck/test/build 16/16 green; prettier clean; Leakwatch 0.
Refs: ADR-0044, ADR-0042, ADR-0031, ADR-0028
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cemililik
cemililik merged commit ebba5ce into mainJun 19, 2026
9 checks passed
@sonarqubecloud

Copy link
Copy Markdown

cemililik added a commit that referenced this pull request Jun 20, 2026
…surfaces
All 1.AF (engine media plumbing) PRs are merged to main (2026-06-20): P1+P2 (#33),
P3 + P4/D13 (#34), P4 remainder (#35), and the multi-agent + external review
follow-ups (#36). Flip the status markers from in-progress / pending-merge to Done
across CLAUDE.md, README.md, current.md, the phase-1 task entry + matrix row + the
1.m6 milestone row, and the deferred-tasks note.
The deferred host-wiring half (D12 MediaReadAccess + session-scope population, the
D15 loader call, the D17/resolveForEgress config) and the keychain no-raw-key IPC
test remain owned by 1.AH (already recorded in deferred-tasks.md). Remaining
Phase-1 work: 1.AG/1.AH.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cemililik added a commit that referenced this pull request Sep 1, 2026
… the transport
W4 Step 1 — CR-40, and the 2.5.5 connect-timeout queue (#35, G32, #205) plus the
orphaned-child half of #21. Two of the three measurements ADR-0088 opens with
become assertions here.
**A connect had no upper bound.** Measured: a transport whose `start()` never
resolves hung `connectSdkTransport` indefinitely. Passing the SDK a
`RequestOptions` would not have fixed it — `Client.connect` awaits
`transport.start()` FIRST and with no options at all, and `start()` is where the
hang lives. So the window opens before any I/O, the whole `client.connect()` is
raced against it, and the REMAINING time is handed to the SDK's own `timeout` so
its internal 60 s default can neither cut an authored 120 s window short nor
outlive one. Four named deadlines replace four inherited or absent ones: stdio
connect 120 s (a cold `npx` is the canonical example), network connect 30 s,
discovery 60 s for the whole paged walk, call 60 s.
**Cancellation was dropped.** Measured: an aborted `tools/call` was still pending
500 ms later. The capability closure took `(input)` and ignored its second
parameter, and `McpConnection.callTool(name, args)` had nowhere to put a signal.
Both round-trips now take an `AbortSignalLike`, bridged to a real `AbortSignal`
at the SDK fence — `@relavium/shared` keeps `types: []` rather than widening its
signal type to a DOM class.
**A signal does not run `finally`.** Measured with a host that spawns a child
exactly the way the SDK does: `SIGTERM` printed no teardown line and left the
child alive with `ppid 1`. `run` and `agent run` tear down in a `finally`, which
Node's default signal handling never unwinds. A cooperative guard on all four
signals reaps the children and exits 128+signo, bounded so a wedged transport
cannot turn Ctrl-C into a hung shell.
`connect_timeout_ms` is authored on both server schemas, ceiling 600 000 ms,
rejected above it rather than clamped (ADR-0086's shape). It is an inline
CONNECTION field, so it is refused alongside a by-name `ref` — no precedence rule
is needed because none is possible — and it enters the duplicate-declaration
fingerprint for the reason `allow_local_endpoint` already does. It is deliberately
NOT in the ADR-0084 consent digest: that answers "is this the same program", and a
timeout changes nothing about what executes.
**One of my own tests was hollow, and mutation caught it.** `raceDeadline`
originally took a `dispose` callback said to be load-bearing; removing its call
left every test green, because `connectSdkTransport`'s own `catch` already closes
the client on every failure path. The parameter had no owner, so it is gone rather
than left as a second mechanism nobody exercises — and the close assertion now
binds the `catch`, verified by removing it (2 red).
Every new test is break-verified: the race removed (6 red), the SDK timeout
handoff dropped (1 red), the catch's close removed (2 red), the signal forwarding
reverted (2 red), the guard's close removed (8 red), its latch removed (1 red).
`pnpm run ci` and `pnpm coverage` both exit 0; `deadlines.ts` is at 100%.
Refs: ADR-0088, ADR-0082, ADR-0084, ADR-0086
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@cemililik