Skip to content

feat(llm,core,shared,db): 1.AG — media output generation (Phase D, ADR-0045/0046)#37

Merged
cemililik merged 25 commits into
mainfrom
development
Jun 21, 2026
Merged

feat(llm,core,shared,db): 1.AG — media output generation (Phase D, ADR-0045/0046)#37
cemililik merged 25 commits into
mainfrom
development

Conversation

@cemililik

@cemililik cemililik commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

What this is

1.AG — Output generation (Phase D, 1.m6): media output across the @relavium/llm seam, built on the 1.AF media substrate. Both inline media-out (a chat model emitting media in a turn) and separate-endpoint generation (generateMedia) — synchronous (gpt-image-1) and the engine-owned async long-running-operation loop (Sora/Veo-shaped jobs: park → poll → checkpoint → resume → cancel). Governed by two new ADRs:

  • ADR-0045 — the async media-job LRO (durable media_job:submitted, the derived pendingMediaJobs checkpoint slot, re-attach-on-resume, host-timer poll cadence, deadline→timeout, cancel→abort→sweep, the one realized cost addend).
  • ADR-0046 — inline media-out via the existing chain.generate(); the streaming triad host-deferred to 1.AH.

How it was built — five sections, each dual-reviewed

Section Scope
A Contracts & shapes pollMediaJob seam signal param, media_job:submitted event, pendingMediaJobs slot, content_filter ErrorCode, model_catalog.media_surface, poll-cadence config defaults
B Inline media-out Gemini responseModalities + OpenAI inline audio → generate(); the runner de-inlines media parts to media:// handles
C Sync generateMedia + routing the AgentRunner generative dispatch fork, the OpenAI gpt-image-1 adapter, the pre-egress gate + the lone realized cost addend
D Async media-job LRO the engine-owned poll/checkpoint/resume/cancel loop (MJ-1 re-attach, AG-A-FC-3 gate-vs-media disambiguation, deadline→retryable-timeout, cancel→terminal sweep)
E Acceptance + conformance + docs the cancel-sweep acceptance test, generative conformance (sync OpenAI + a stub async seam-contract suite), the reserved→wired doc reconcile

Each section ran the same loop: develop → Opus adversarial review → fix → Sonnet adversarial review → fix — all rounds 0 blocker/high.

Final pre-PR review (the whole workstream)

A comprehensive full-diff pass (18 dimension/area/integration finders → dedup → 2 perspective+model-diverse adversarial verifiers per finding) caught two real cross-section bugs no per-section review could see, now fixed with regression tests:

  • H1media_job:submitted was missing from RESUMABLE_LAST_TYPES: a crash in the submit→pause window reconciled the run to run:failed, orphaning a paid, still-generating provider LRO (the checkpoint fold already treated it as resumable — the two classifications disagreed).
  • H2resumeFromCheckpoint leaked the constructor-armed poll/run-timeout timers when a resume misuse-guard threw, so an orphan timer kept polling the provider for a rejected run (and a retry could double-attach the same jobId). Fixed via a new RunExecution.abandon().

A focused re-verification of the fix round returned GO (both HIGH fixes independently reproduced pre-fix / confirmed post-fix; regression sweep clean). The remaining low/nit findings were fixed or recorded as 1.AH deferrals — notably no generative MODEL_PRICING rows were fabricated (the mis-bill risk Section C deliberately avoided).

Invariants held (verified clean)

I3 durable-boundary byte-safety (all three media-out paths de-inline to media://sha256-… handles) · ADR-0011 seam purity (no vendor type crosses the seam; opaque jobId) · engine purity (zero platform imports; all timing via injected setTimer) · secret redaction (keyFor-throw → fixed secret-free error on both sync and async paths) · exactly-one-terminal · exactly-one realized cost addend across every terminal path.

Deferred to 1.AH (recorded in deferred-tasks.md)

The real Sora/Veo pollMediaJob adapters + OpenAI-TTS/Gemini-Imagen sync adapters, the per-model media_surface host lookup, verified generative pricing rows, and the streaming media triad. No generative model is runtime-reachable in Phase 1 (the engine mechanism + the OpenAI-image adapter are proven via injected-surface tests + a conforming stub).

Validation

pnpm turbo run lint typecheck test build16/16 green (core 846 tests, llm 381+8 skipped). Leakwatch — 0 findings. The 1.AG roadmap row is intentionally NOT marked ✅ Done — it flips on merge (Roadmap-Done-After-Merge).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added async long-running media generation for agent nodes with polling, durable checkpoint resume/reattach, and best-effort cancellation.
    • Introduced model “media surface” routing for chat vs generative media flows.
    • Updated inline media output to use non-streaming generation for media parts.
  • Bug Fixes
    • Enforced non-retryable failures when requested media isn’t returned and improved content_filter fatal handling.
    • Improved media cost rounding consistency.
  • Documentation
    • Updated ADRs, roadmap/status, contracts (including SSE run events), and config/media capability references.
  • Tests
    • Expanded end-to-end, engine, adapter, checkpoint, and conformance coverage for inline and async media.

cemililik and others added 20 commits June 20, 2026 00:25
…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>
…5), Accepted

The async media-job poll/checkpoint/resume/cancel LRO ADR the 1.AG contract
mandates (ADR-0031 A5). Pins, across 7 decisions: media_surface-driven
inline-vs-generative routing on the agent node (no new node type); a durable
media_job:submitted run-event folded into a derived pendingMediaJobs checkpoint
slot (no media_jobs table); re-attach-not-restart on resume (re-poll the persisted
opaque jobId, never re-submit); an additive pollMediaJob signal param + best-effort
local cancel + the one terminal sweep; a pre-egress estimate that gates only + one
realized addend at done (request-derived, no frozen-shape touch); content_filter as
a first-class fatal ErrorCode + no cross-provider failover for an in-flight job +
the poll cadence carved out of deterministic replay; and reuse of the one SSRF
egress / opaque jobId / strip-raw discipline, with strict poll/deadline config
(5s→30s backoff, 30-min deadline).

Amends ADR-0031 (wires the reserved generators + the signal param), ADR-0036 (a new
non-terminal suspension state + the submit event through the one #emitDurable choke),
and ADR-0040 (re-attach overrides the re-run-from-pending default for the async-media
node; the wall-clock poll is carved out of deterministic replay). Dated amendment
notes added to all three; README index updated.

Three independent ADR reviews addressed: corrected the ADR-0014 filename (3 refs),
reframed the A.6 attribution to the checkpoint model, pinned the deadline→timeout→
provider_unavailable mapping, the pendingMediaJobs nodeId-keyed cardinality + the
abandoned-pre-done-no-orphan rule, the estimate-gates-only/realized-single-addend
cost model, the exact poll formula + config key names, the cancel done-race
clarification, the @relavium/shared land-time obligations list, and the
amendment-note obligation.

Refs: ADR-0031, ADR-0036, ADR-0040, ADR-0042, ADR-0043, ADR-0044

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…& shapes (ADR-0045)

The foundational contracts the rest of 1.AG (inline media-out, sync/async
generateMedia) binds to — shapes only, no behavior yet (mirrors how 1.AD landed
the seam shape before its consumers).

- shared: `media_job:submitted` durable RunEvent (jobId/provider/model/modality/
  startedAt/deadlineAt) + the union/RUN_EVENT_TYPES/count-test in lock-step (20→21);
  `content_filter` first-class fatal ErrorCode; `[defaults].media_job_poll_initial_ms/
  poll_max_ms/deadline_ms` (5s/30s/30min).
- llm: `pollMediaJob(jobId, key, signal?)` — additive signal param so cancel aborts
  the in-flight poll (the only change to the 1.AD-frozen seam, made while un-implemented).
- core: `pendingMediaJobs` CheckpointState slot derived from the run_events fold —
  media_job:submitted parks the node (paused, keyed by nodeId, latest-submit-wins) and
  its terminal clears it (no media_jobs table; ADR-0003); `codeForLlmError(content_filter)
  → content_filter` (was collapsing to `validation`). +3 checkpoint fold tests.
- db: `model_catalog.media_surface` ('chat'|'generative', default 'chat') + migration 0004.
- docs: sse-event-schema (the event + content_filter taxonomy), database-schema
  (media_surface), config-spec (the three poll/deadline defaults), error-handling
  (content_filter fatal), llm-provider-seam (the pollMediaJob signal).

Validation: lint/typecheck/test/build 16/16, prettier clean, Leakwatch 0.
Refs: ADR-0045, ADR-0031, ADR-0036, ADR-0040

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rified-valid)

Opus multi-dimensional review of Section A (0 blockers/highs; 16/16 green): fixed
the confirmed findings, deferred the two that are genuinely Section-D obligations.

- CF-1 (doc drift): agent-runner.md chain-exhausted-LlmError table still mapped
  content_filter → validation; split it out to content_filter (the other canonical
  homes were already updated).
- AG-A-DB-1 (db convention): model_catalog.media_surface now uses the shared
  `MEDIA_SURFACES` const + `.$type<MediaSurface>()` (the value-set source the sibling
  enum columns' checks use). A DB CHECK is DEFERRED with a recorded reason: drizzle
  cannot ADD a CHECK to a *new* column on an existing table without a table-rebuild
  migration whose INSERT…SELECT references the not-yet-existing column (the sibling
  checks live in the initial CREATE; this is an ALTER ADD). Value integrity holds via
  the type + the Zod/shared boundary.
- MJ-2 (fold robustness): applyMediaJobEvent never resurrects an already-settled node
  (a stale/out-of-order submit after a terminal is ignored) + 4 fold tests (skipped
  clear-path, the stale-submit guard, multi-node independence).
- F3 (test coverage): media_job:submitted reject-matrix cases (bad provider/modality/
  non-datetime); new constants.test.ts pins content_filter ∈ ERROR_CODES ∧ ∉ RETRYABLE.
- nits: ADR-0045 §2 prose now uses the wire names jobId/provider; sse-event-schema TS
  interface types provider as the closed union.

Deferred to Section D (recorded): MJ-1 (#seedFromCheckpoint re-attach + #handleIdle
park-awareness for pending media jobs) and AG-A-FC-3 (resume disambiguates a paused
node via pendingMediaJobs vs pendingGates) — both need the re-attach wiring that lands
with the LRO behavior.

Validation: lint/typecheck/test/build 16/16, prettier clean, Leakwatch 0, no migration drift.
Refs: ADR-0045

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ps (verified-valid)

Sonnet multi-dimensional review of Section A (0 blockers; deeper than the Opus
round): fixed the confirmed findings, deferred the genuinely-later ones.

HIGH:
- CapabilityFlags.media.surface projection (the named ADR-0045 §1 land-time
  obligation): add `surface?: MediaSurface` to MediaCapabilitiesSchema (absent ⇒
  'chat'), set 'chat' on all four adapter CAPS + their tests, and the seam doc.
  Resolves the inconsistency where @relavium/shared already advertised the projection.
- Doc RunEvent union: add `| MediaJobSubmittedEvent` to the sse-event-schema type block.

MEDIUM/LOW:
- Export CheckpointPendingMediaJob from the @relavium/core barrel (was asymmetric
  with CheckpointPendingGate); narrow its provider/modality to LlmProviderId /
  MediaBilledModality (closed types, imported from @relavium/shared — no purity leak).
- MEDIA_JOB_POLL_DEFAULTS const in @relavium/shared (single home for 5000/30000/1.8e6
  so Section D reads `config ?? DEFAULTS` instead of magic numbers).
- config: a cross-field refine (media_job_poll_max_ms >= media_job_poll_initial_ms);
  config.test exercises the three keys + the refine.
- Tests: node:failed media-job clear-set fold; content_filter LlmError → fatal
  content_filter ErrorCode (agent-turn); content_filter on the event parse path;
  reject-matrix empty/missing jobId + non-datetime startedAt.
- Docs: node-types agent_config count/duration_seconds (generative models); seam doc
  reserved→shape-final; corrected the schema CHECK-deferral root cause (SQLite ADD
  COLUMN can't carry a CHECK); run-event startedAt/deadlineAt comments (submit-time +
  use Date.parse).

Deferred to Section C (recorded): the model_catalog→CapabilityFlags surface read path
+ the agent_config count/duration_seconds → MediaGenRequest wiring (land with the
generateMedia routing). Section D obligations (re-attach seed, idle park-awareness)
unchanged. A handful of cosmetic nits (test-name precision, pre-existing doc table
ordering) intentionally skipped — the lock-step count-test covers the contract.

Validation: lint/typecheck/test/build 16/16, prettier clean, Leakwatch 0, no drift.
Refs: ADR-0045, ADR-0031

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…g triad host-deferred (1.AG Section B)

The maintainer's decision (2026-06-20) for how Phase-1 inline media-out de-inlines
base64→handle on Node. The agent turn is stream-only and StreamChunk.media_end is
handle-only (1.AD-frozen) — a pure adapter cannot deliver its base64 media into the
stream. Decision: a media-output turn routes through the existing generate() seam
method (LlmResult.content carries an in-flight media part, de-inlined at the
#emitDurable choke point, reusing 1.AF) — no frozen-shape change, no new host hook.
The FallbackChain gains a generate() path; the streaming media triad
(media_start/delta/end) stays reserved/deferred to the host (1.AH).

Amends ADR-0031 (pins §5.1's "more common" non-streaming path) and ADR-0038 (the
turn loop gains a single-shot generate() branch). Amendment notes added to both;
README index updated.

Refs: ADR-0031, ADR-0038, ADR-0042, ADR-0045

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ise (review follow-up)

Three independent reviews flagged the same factual error: ADR-0046 said the
FallbackChain "has no generate() path / gains a generate() method / was
stream-only". In fact FallbackChain.generate() has existed since 1.K
(fallback-chain.ts:295) with the full skip/select/failover/cost/classification it
describes — and ADR-0038 (which this amends) already acknowledges generate(). The
stream-only thing is the agent-TURN loop, not the chain. Corrected the Context and
Decision §2 to the right framing (the turn gains a routing branch that calls the
chain's EXISTING generate()), matching the ADR-0038 amendment note this ADR added.
Not a decision change — a wrong-premise fix on a same-day, not-yet-coded ADR.

Also: noted generate() preserves same-provider reasoning replay (beginEntry,
ADR-0039); marked the produced-vs-output_modalities comparison an additive
refinement of ADR-0031 §5.3; and strengthened the tool+media one-shot rationale
(the tool loop is built around stream()).

Refs: ADR-0031, ADR-0038, ADR-0039

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route a media-output agent turn through the FallbackChain's existing non-streaming
generate() (present since 1.K, never invoked by the stream-only turn loop) per
ADR-0046. The settled LlmResult.content carries in-flight base64 media that the
engine de-inlines to media:// handles at the #emitDurable choke point (1.AF) — no
new seam shape, no new host hook, the I3 invariant inherited intact.

Adapters (response + request, both paths):
- gemini: mapContent parses response inlineData → an in-flight base64 media part;
  buildGeminiRequest lowers non-text outputModalities → responseModalities.
- openai: mapContent parses response audio → the transcript as a text part PLUS the
  audio as a base64 media part (outputAudioMime maps the requested format → MIME);
  buildCommonBody lowers an audio outputModality → modalities + audio (voice
  any-string, format narrowed to the SDK union, providerOptions-merged).

Engine:
- agent-turn: generateOneTurn (single-shot, terminal — no tool loop, since the tool
  loop is built around stream() and generate() is one round-trip) + a
  requestsMediaOutput routing branch; shared throwMappedChainError maps a chain
  failure symmetrically whether it arrives as a stream error chunk or a thrown
  generate() LlmProviderError (.llmError.cause preserves budget/turn errors).
- agent-runner: a turn that produced media parts now outputs { text, media } (ahead
  of output_schema) so #emitDurable de-inlines the media to handles — the
  previously-missing integration link (the runner returned result.text only, which
  dropped the media).

Also: removed a raw NUL byte embedded in fallback-chain.ts's egress-sidecar cache
key (from 1.AF P3) — replaced with the runtime-identical unicode escape; the raw
byte made the file read as binary to grep/rg. Docs: seam-doc delivery-path note;
deferred-tasks records the streaming-triad (1.AH) and the OpenAI Responses-API
agentic-image-gen deferrals.

Tests: gemini inlineData + responseModalities; openai audio parse + outputAudioMime
+ request round-trip; agent-turn routing (generate-not-stream, text-stays-streaming,
error-mapping); agent-runner media-output shape. Full toolchain + build green, seam
fence clean, Leakwatch 0.

Refs: ADR-0046, ADR-0031, ADR-0042

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…out)

Adversarial Opus review (6 dimensions → refute-verify → toolchain): 0 blocker/high/
medium, 4 low + 5 nit confirmed. All addressed:

- [low] A media-output turn returning a tool_use stop is now a LOUD `tool_failed`
  failure (agent-turn), not a silent completion with empty output — a media turn is
  single-shot/terminal (ADR-0046), generate() can't run a tool round.
- [low] A media-requesting node that produced NO media now FAILS `validation`
  (agent-runner) — the declared-capability pre-skip can't catch a capable model that
  returns text, so detect the silent degradation here (ADR-0046 produced-vs-requested).
- [low] Gemini mapContent now SKIPS a mimeType-less inlineData part (symmetric with the
  empty-data skip) instead of defaulting to 'application/octet-stream' — that default
  would HARD-FAIL the de-inline (mediaModalityOf undefined → run:failed).
- [low] Added the end-to-end proof: an agent node (output_modalities [text,image]) →
  generate() → { text, media } → engine de-inline → media:// handle, gap-free, no
  base64 on the durable stream (m2-e2e-harness).
- [nit] Fixed an orphaned JSDoc (the GEMINI_RESPONSE_MODALITY const split
  buildGeminiRequest's doc comment); documented video->VIDEO as a type-exhaustiveness
  entry unreachable via the gate; forward-note on requestsMediaOutput for the Section-C
  media_surface conjunct; documented the media-over-output_schema precedence in
  workflow-yaml-spec.md.

Tests added: gemini mimeType-less skip; agent-turn tool_use→tool_failed; agent-runner
no-media→validation; m2 e2e agent media-out through the engine. Full toolchain + build
green, seam fence clean, Leakwatch 0.

Refs: ADR-0046

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…a-out)

Adversarial Sonnet review (4 dimensions → refute-verify → toolchain): 0 blocker/high,
2 medium, 7 low, 10 nit. Confirmed findings addressed (2 contradictory verdicts on the
tool_use code adjudicated by redesign):

- [medium] A media-output turn is now offered NO tools (buildRequest omits them — a
  single-shot generate() has no tool loop). A tool_use stop is therefore a genuine
  provider protocol anomaly, mapped to `provider_unavailable` (matching the stream
  path's identical guard) — resolving the tool_failed-vs-provider_unavailable split.
- [medium] Both adapters now reject a non-text outputModalities on the STREAM path
  (assertNoStreamingMediaOutput): media-out is generate()-only, the streaming triad is
  host-deferred (ADR-0046 §4), and the streaming fold drops media — so a stream() media
  request would silently lose it. Fail loud instead.
- [low] Gemini inline media-out strips MIME parameters to the bare type — Gemini audio
  output uses a parameterized mime (audio/L16;codec=pcm;rate=24000) that
  MediaMimeTypeSchema rejects, which would have hard-failed the de-inline.
- [low] Added generate()-path coverage: cross-provider failover, budget-exceeded cause
  unwrap, pre-aborted-signal cancel (zero egress); strengthened the cost:updated
  assertion (model + attemptNumber); reqCapturingProvider returns media so the
  output_modalities-lowering test stays on the success path.
- [nit] mediaParts narrowed via a type predicate; outputAudioMime computed only for an
  audio turn; transcript/content mutual-exclusion comment; aac MIME test; e2e
  handle-ref-format assertion; deferred-tasks cross-ref for the Section-C media_surface
  conjunct.

Accepted (no action — established codebase idiom, reviewer concurred): the per-file
`isRecord` guard duplication.

Full toolchain + build green (813 core tests), seam fence clean, Leakwatch 0.

Refs: ADR-0046

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rface routing

Route a media_surface:'generative' agent node to the seam's separate-endpoint
generateMedia (gpt-image-1, Imagen, OpenAI TTS) instead of the inline generate()/
stream() turn, per ADR-0045 §1/§5/§6. SYNC one-round-trip: the resolved { media }
de-inlines to a media:// handle at #emitDurable exactly like the inline path.

Engine (agent-runner):
- A routing fork in executeAgent reads the resolved model's media_surface (a new
  host-injected AgentRunnerDeps.resolveMediaSurface lookup, default 'chat'; production
  catalog wiring is 1.AH) — 'generative' dispatches generateMedia, 'chat' the normal turn.
- executeGenerativeMedia: ONE provider, NO chain failover (provider-bound, §6); the
  pre-egress budget gate runs first (the authored volume estimate, gate-only); the result
  becomes the { text:'', media } node output; exactly ONE realized cost:updated is emitted
  (request volume × per-model rate via the shared cost() fold, degrade-to-0 on a missing
  rate, H4). jobId (async LRO) → internal (Section D); a content-policy refusal maps through
  the SAME codeForLlmError taxonomy as the chat path (content_filter stays content_filter).
- requestsMediaOutput (agent-turn) now only ever runs for chat models (the generative fork
  is upstream); codeForLlmError is exported for the shared classification.

Adapter (openai): generateMedia SYNC for gpt-image-1 (images.generate → base64 PNG media);
a non-image modality / DeepSeek throws a typed capability error; a no-data response → a
typed bad_request. (OpenAI-TTS audio + Gemini-Imagen + the verified pricing rows + the
host surface-lookup wiring are bounded 1.AH follow-ups — deferred-tasks.md.)

Schema (shared): AgentNodeSchema += count (images/call) + duration_seconds (audio/video
length), inline on the agent node → MediaGenRequest at dispatch.

Tests: schema (count/duration); agent-runner generative dispatch (routing, output shape,
one cost:updated, pre-egress gate→budget_exceeded, generateMedia-undefined→internal,
jobId→internal, modality validation, content_filter mapping); OpenAI generateMedia (image
round-trip, modality/DeepSeek rejection, no-data bad_request); m2 e2e — a generative agent
node through the full engine → generateMedia → de-inline → media:// handle, no base64,
one cost:updated. Docs: workflow-yaml-spec, llm-provider-seam, deferred-tasks. Full
toolchain + build green, seam clean, Leakwatch 0.

Refs: ADR-0045, ADR-0031

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial Opus review (6 dimensions → refute-verify → toolchain): 0 blocker/high,
4 medium, 9 low, 9 nit. Confirmed findings addressed:

- [med] Empty resolved prompt now fails validation BEFORE the gate/egress (the seam's
  MediaGenRequest.prompt is nonEmptyString; the chat path tolerates empty, generative
  cannot) — no confusing provider bad_request.
- [med] mediaCost now rounds the addend to an integer micro-cent (Math.round) — a
  fractional duration_seconds × per-second rate would otherwise break the integer invariant.
- [med] Added cost-math + count/duration→MediaGenRequest mapping tests (non-zero +
  rounding via a constructed rate; the dispatch maps node.count→req.count).
- [low] A seam UnsupportedCapabilityError (non-image modality / DeepSeek) now classifies as
  validation with its message, not an opaque engine 'internal' (LlmConfigError branch).
- [low] keyFor throw on the generative path is now redacted to a fixed secret-free
  provider_auth failure (matching the chat path's #resolveKey) — no secret leak.
- [low] OpenAI generateMedia rejects count > 1 (single-artifact SYNC seam — never bill-N-
  deliver-1); honors a requested output format (req.mimeType → output_format + result MIME);
  an image content-policy refusal maps to content_filter (the documented taxonomy).
- [low] realizedMediaCost degrades ANY pricing throw to 0 (not just UnknownModelError) so a
  pricing-layer fault can never fail an already-paid generation.
- [nit] Defense-in-depth: a produced media modality that mismatches the request fails
  validation; generativeUnits documents the no-host-default rationale; the e2e now asserts
  MediaGenResult.raw is absent from the durable stream (§7 strip-on-sink).

Tests added: empty-prompt, count/duration mapping, neither-media-nor-jobId, cancel-before-
egress, UnsupportedCapability→validation, keyFor→provider_auth redaction, produced-modality
mismatch; openai count>1 rejection, output_format honoring, content_filter mapping; cost
rounding. Deferred-tasks records the multi-image + image-knob-passthrough bounded follow-ups.
Full toolchain + build green, seam clean, Leakwatch 0.

Refs: ADR-0045

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial Sonnet review (4 dimensions → refute-verify → toolchain): 0 blocker/high,
3 medium, 7 low, 7 nit. Two valuable latent forward-compat catches + the rest:

- [med] validateWorkflowWithCatalog (D15) would WRONGLY reject every generative node
  once host-wired — a generative model's outputCombinations is empty (chat-surface only).
  Now skips the inline output-combination load-check for a media_surface:'generative'
  model (its output set is the generateMedia modality). + a regression test.
- [med] The generative pre-egress gate now passes maxTokens:0 — a generative call emits no
  tokens, so once generative pricing rows land the gate must not add a spurious token
  addend on top of the media estimate. + a test asserting maxTokens:0 + the media estimate.
- [med] Documented resolveMediaSurface in its canonical home (agent-runner.md AgentRunnerDeps
  + the generative fork step), per rule 8 (the code docstring is not the SSOT).
- [low] OpenAI content_policy_violation / moderation_blocked now classify as content_filter
  in mapOpenAiApiError (chat + image paths) — + classification tests. count>1 test asserts
  the bad_request kind. image/jpg alias maps to jpeg (no silent PNG mismatch).
- [nit] cost:updated docs note the zero-token, no-attemptNumber generative variant; the
  media-undefined guard documents its refine-bypass defense; the e2e drops two unsafe `as`
  casts for a runtime type-guard (firstMediaRef); the cost:updated test asserts nodeId/model;
  a duration_seconds→durationSeconds audio mapping test; the jobId test title trimmed.

deferred-tasks records the realized-cost non-zero dispatch test as a 1.AH (with-rates)
follow-up. Full toolchain + build green, seam clean, Leakwatch 0.

Refs: ADR-0045

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…int/resume/cancel)

The engine-owned async media-job loop (Sora/Veo, minute-scale LROs) per ADR-0045
§2/§3/§4/§5. A generative node whose generateMedia resolves { jobId } (not { media })
PARKS for the engine to poll — reusing the human-gate suspend/resume machinery.

Handoff (executor → engine):
- New NodeOutcome `media_job` arm (node-executor.ts) + MediaJobSubmission; executeGenerativeMedia's
  jobId arm now hands the job off (was a Section-C `internal` stub). NodeExecutor gains an optional
  pollMediaJob delegate (provider + credential resolution stays in the executor; the dispatcher forwards it).

Engine (engine.ts):
- #settleMediaJobParked: status 'paused' (slot-free, keyed by nodeId — the AG-A-FC-3 disambiguator),
  record #pendingMediaJobs, emit durable media_job:submitted, arm the first poll via the INJECTED
  host.setTimer (never ambient setTimeout — purity).
- #pollMediaJob loop: deadline → retryable provider_unavailable timeout; pending → exp-backoff re-arm
  (capped at pollMaxMs); done → the lone realized cost:updated (§5) + de-inline the media to a handle +
  node:completed; failed → codeForLlmError → node:failed. The run abort signal is threaded into the poll,
  so a cancel aborts the in-flight request (§4) + #settle disarms the poll timers + the terminal sweep runs.
- MJ-1: #handleIdle treats a non-empty #pendingMediaJobs as PARKED (not a stall-fail); #seedFromCheckpoint
  re-attaches each parked job (re-poll the persisted opaque jobId, NEVER re-call generateMedia — overrides
  the running-at-crash-re-runs default; units recomputed from the node config).
- AG-A-FC-3: resumeFromCheckpoint accepts gateId+decision (gate) OR neither (media-only re-attach via the
  new beginResumeMediaJobs); a run parked on BOTH resumes both. run:paused relaxed to carry
  pendingMediaJobNodeIds (a media-only park has 0 gates).

No Phase-1 adapter implements pollMediaJob / returns a jobId; the loop is proven with a STUB async provider
+ the manual-timer harness — the real Sora/Veo adapters + node-retry-of-a-parked-job are 1.AH/post-1.AG
deferrals (deferred-tasks.md). 5 e2e tests: park→poll(pending→done)→handle; content_filter failure; cross-
process re-attach (no re-submit); cancel-aborts-poll → run:cancelled; AG-A-FC-3 both-park. Docs:
sse-event-schema (run:paused media fields), deferred-tasks. Full toolchain + build green, seam clean,
engine pure, Leakwatch 0.

Refs: ADR-0045

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ia LRO)

Applies the verified findings from the Section-D Opus adversarial review round
(0 blocker/0 high). Engine fixes already landed in the prior Section-D commit;
this round closes the remaining test-coverage gaps and the schema/doc hardening:

- engine: hang-prevention default arm on an unrecognized job state, cost emit on
  fail/cancel/settle, #pauseEpisode reset on clear, an outer try/catch backstop
  around #pollMediaJob, the beginResumeMediaJobs empty-pending guard, and output
  text:'' parity with the inline path.
- shared(run-event): relax RunPausedEventSchema (a media-only park carries no
  gateIds) and move the "at least one suspension reason" invariant to a
  union-level superRefine; tests updated for the relaxed shape.
- shared(constants): soften the MEDIA_JOB_POLL_DEFAULTS docstring — the engine
  uses the defaults directly; the [defaults].media_job_* config overrides exist
  and validate but host-wiring is 1.AH.
- core(e2e): add the deadline-timeout re-attach test (resume past the deadline
  short-circuits to a retryable timeout, never re-polls), the deterministic
  in-flight poll cancel/abort test, and the pending×N backoff-re-arm test.
- core(unit): add the pollMediaJob delegate redaction test — a missing
  pollMediaJob → failed(unknown); a keyFor throw → a secret-free failed(auth).

Refs: ADR-0045

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dia LRO)

Applies the verified findings from the Section-D Sonnet adversarial review
(0 blocker/0 high; 3 medium, 4 low, 4 nit — all confirmed, none refuted):

Medium:
- engine: add the missing `#schedule()` to `#pollMediaJob`'s outer-catch
  backstop so a settle-path fault drives `run:failed` instead of hanging the
  run at `run:paused` forever (M1), mirroring the `#onOutcome` invariant.
- engine: record `submittedAtMs` on the parked job (set at park, recomputed on
  resume from the persisted `startedAt`) and pass it as the completed node's
  `startedAtMs`, so `node:completed.durationMs` is the full async submit→done
  wall-clock instead of the ~0 of the synchronous settle (M2).
- shared(run-event): re-assert `pendingGateCount === gateIds.length` at the
  union-level superRefine — the count/list relaxation for a media-only park
  dropped the structural guarantee that the pair agrees (M3).

Low:
- engine: `#failNodeInternal` now transitions a `'paused'` vertex (a media park)
  to `'failed'`, not just a `'running'` one (L1).
- engine: a media-only cross-process resume no longer re-emits a duplicate
  `run:paused` — `#seedFromCheckpoint` marks the pause episode already-emitted
  when it re-attaches ≥1 job (L2).
- engine: `beginResumeMediaJobs` rejects a media-only resume of a run also
  parked on a gate (new `pending_gate_requires_decision`) rather than silently
  re-parking after the job settles (L3).
- core(agent-runner): `generativeUnits` multiplies `duration_seconds` by `count`
  for audio/video when both are authored (ADR-0045 §5), no longer dropping
  `count` (L4).

Nits: accurate `pollMediaJobThroughDeps` throw-contract docstring (N1); a raw
poll throw on a live job is now a retryable `provider_unavailable`, aligned with
the deadline path + forward-compatible with 1.AH node-retry (N2); corrected the
cancel-test comment (N3) and the dispatcher `.bind` comment (N4).

Tests: generativeUnits count×duration; durationMs > 0 on an async completion;
no duplicate run:paused on resume; the AG-A-FC-3 gate+media misuse guard; the
run:paused count/gateIds-mismatch reject. Docs: sse-event-schema run:paused row.

Refs: ADR-0045

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nce, doc reconcile

Closes the final 1.AG slice — the acceptance proof, the generative seam-contract
conformance, and the canonical-home doc reconcile. No production behavior change.

Acceptance (the 4th scenario's missing assertion):
- core(e2e): a CANCEL of a parked media job now asserts the terminal media sweep
  (#reclaimRunMedia fires reclaimRun([runId]) on the run:cancelled terminal), not
  just the terminal type — the cancel-path twin of the run:completed sweep test.
  The other three scenarios (sync image→handle; async checkpoint→restart→resume
  re-attach; content-policy→content_filter) were already asserted in B/C/D.

Generative conformance fixtures (@relavium/llm):
- spec.ts: an optional `mediaGenerate` arm (skipIf-gated like reasoning/toolLoop)
  asserting a SYNC generateMedia image normalizes to a base64 media part, no jobId;
  plus a cross-adapter `media.surface` projection check (a valid MediaSurface).
- fixtures/openai.ts: a recorded images.generate base64 reply drives the sync arm
  through the real OpenAI adapter offline (the one production generateMedia today).
- generative-seam.conformance.test.ts: a NEW stub-driven async seam-contract suite
  (no production pollMediaJob adapter — Sora/Veo are 1.AH) pinning the MediaGenResult
  exactly-one-of refine, an opaque jobId, the discriminated MediaJobStatus states,
  the additive abort signal, and content-policy→content_filter classification.

Doc reconcile (reserved→wired):
- llm-provider-seam.md: the generateMedia/pollMediaJob behavior is WIRED (sync
  Section C + the engine-owned async loop Section D), not "remaining 1.AG work";
  the code-fence caption + inline interface comment flipped from RESERVED.
- llm/types.ts: the same reserved→wired flip on the MediaGenRequest / MediaJobStatus
  / generateMedia / pollMediaJob doc-comments ("No Phase-1 adapter implements it"
  was false — OpenAI's sync generateMedia landed in Section C).
- deferred-tasks.md: ticked the A5 async-media-job ADR + engine-loop obligation
  (DONE, 1.AG A/C/D); the 1.AH vendor-adapter + host-wiring entries stay unchecked.
- roadmap: 1.AG marked "Sections A–E landed on development (pending PR merge)" —
  NOT ✅ Done (flips on merge, per the Roadmap-Done-After-Merge rule).

Refs: ADR-0045, ADR-0046

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Applies the verified findings from the Section-E Opus adversarial review
(0 blocker/0 high; 1 low, 1 nit — both confirmed):

- docs(deferred-tasks): revert the premature `[x]` tick on the A5 async media-job
  obligation — 1.AG is on `development`, not merged, so per the Roadmap-Done-After-
  Merge rule (and this file's own "check off in the PR that resolves it") it stays
  `[ ]` with a "landed on development, pending PR merge" note; ticks with the merged
  PR number when the 1.AG PR lands. Consistent with every other 1.AG status location.
- test(llm conformance): the sync generateMedia scenario now asserts the base64
  payload round-trips byte-for-byte (exact-value parity with the text/structured
  scenarios) — `expected.mediaGenerate` carries the canonical `data`. Defense-in-depth
  against a wrong-but-nonempty normalization regression (already caught by the OpenAI
  unit test, now also at the shared-suite level).

Refs: ADR-0045

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Applies the verified findings from the Section-E Sonnet adversarial review
(0 blocker/0 high; 2 nits — both confirmed; 2 refuted). The review independently
ground-truthed the cancel-sweep test as real, the sync conformance scenario as
running for OpenAI, and the Roadmap-Done-After-Merge rule as honored throughout.

- test(llm conformance): the async abort-signal seam-contract test now asserts the
  caller-owned signal is forwarded by IDENTITY (`signal === controller.signal`), not
  mere presence — faithfully falsifying an adapter that mints its own controller
  instead of threading the run's signal (A5 §4 cancel reaches the in-flight poll).
- docs(deferred-tasks): the A5 item's landed-scope summary now reads "Sections A–E"
  (consistent with current.md + phase-1-engine-and-llm.md), while noting the A5
  obligation itself was discharged in A/C/D. Still `[ ]`, pending PR merge.

Refs: ADR-0045

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nit)

Applies the verified findings from a comprehensive multi-dimensional pre-PR
review of the ENTIRE 1.AG workstream (18 finders → dedup → 2 perspective+model-
diverse adversarial verifiers per finding), plus a focused re-verification of
this fix round. The full-diff pass caught two real CROSS-SECTION bugs no
per-section review could see (both confirmed by both verifiers); the rest are
coverage/clarity. 0 blocker.

HIGH (real bugs on the async media-job LRO):
- H1 (core/execution-host): add `media_job:submitted` to RESUMABLE_LAST_TYPES.
  It persists in its own turn BEFORE the later run:paused, so a crash in that
  window left it as the durable last event → listInterruptedRuns reported
  resumable:false → reconcile() wrote a terminal run:failed → resumeFromCheckpoint
  short-circuited at the TERMINAL gate, ORPHANING a paid, still-generating
  provider LRO (the checkpoint fold already treats it as resumable, so the two
  classifications disagreed). Now reconcile leaves it for the re-attach path.
  Tests: listInterruptedRuns pins resumable:true; reconcile() leaves it untouched.
- H2 (core/engine): resumeFromCheckpoint leaked the constructor-armed media-poll
  (and run-timeout) timers when beginResume*/beginResumeMediaJobs threw a
  validation misuse (pending_gate_requires_decision / unknown_gate / run_not_paused)
  AFTER #seedFromCheckpoint armed them — the catch only dropped the run from #runs,
  so an orphan timer kept polling the provider for a run the caller saw rejected
  (and a retry could double-attach the same jobId). New RunExecution.abandon()
  disarms all timers + aborts in the catch. Regression: armedCount()===0 after the
  rejected resume (a dedicated timeout_ms-bearing workflow pins BOTH the media-poll
  and run-timeout disarm legs of abandon()).

LOW / NIT:
- L1 (core/e2e): assert the realized cost addend fires on the FAILED (content_filter)
  and CANCEL-sweep media paths — the exactly-one-addend invariant was untested on
  the non-happy terminals (a dropped #emitMediaJobCost would have been silent).
- L2 (core/agent-runner): post-await cancel re-check in executeGenerativeMedia — a
  cancel landing while generateMedia was in-flight now returns `cancelled` and
  suppresses the stray cost:updated, mirroring the inline/stream post-turn check.
- L3 (core/agent-runner): document the by-design fail-fast (the full fallback_chain
  resolves before the generative surface fork, so an unwired unused fallback fails
  loud — a misconfig regardless of surface).
- N1 (core/engine): document the unreachable non-agent units-recompute fallback.
- N2 (core/e2e): a test proving #applyMediaJobStatus's unrecognized-state default
  arm fails the node loud (no silent park-forever hang).
- N3 (llm/budget-estimator): mirror the per-entry Math.round so the pre-egress
  estimate and the realized addend agree to the micro-cent on fractional durations.
- M1, N4 (deferred-tasks): record the durable fail/cancel-cost reconstruction as a
  Phase-2 item (pre-existing cost:updated live-only trait, not 1.AG-introduced), and
  the 1.AH opaque-jobId/strip-raw conformance obligation for the Sora/Veo adapters.

L4 (retryable-but-not-retried async poll fault) and L5 (non-zero realized media
cost end-to-end) are confirmed PRE-EXISTING documented deferrals to 1.AH — no
generative MODEL_PRICING rows exist and fabricating a rate is the mis-bill risk
Section C deliberately avoided; the cost MATH is unit-tested against a constructed
rate, the WIRING is now pinned by the L1 assertions.

Refs: ADR-0045

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot 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

coderabbitai Bot commented Jun 20, 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: 97da971a-a4e3-4851-b432-bfb3584479dc

📥 Commits

Reviewing files that changed from the base of the PR and between 30915d9 and c34238f.

📒 Files selected for processing (3)
  • packages/core/src/engine/agent-runner.test.ts
  • packages/core/src/engine/engine.ts
  • packages/core/src/engine/m2-e2e-harness.e2e.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/core/src/engine/engine.ts
  • packages/core/src/engine/m2-e2e-harness.e2e.test.ts
  • packages/core/src/engine/agent-runner.test.ts

📝 Walkthrough

Walkthrough

This PR implements 1.AG engine sections A–E: async media-job loop (ADR-0045) and inline media-out routing (ADR-0046), wiring media-surface discriminator through database schema, shared constants, LLM adapters, agent-runner generative fork, agent-turn generate path, checkpoint reconstruction with pending media jobs, and a complete WorkflowEngine lifecycle for media-job park/poll/resume/cancel/settle with pre-egress budget gating, realized cost emission, and checkpoint re-attachment semantics. Includes comprehensive unit and end-to-end tests, conformance suite updates, and aligned documentation.

Changes

1.AG Engine Media Pipeline (Async Job Loop and Inline Media-Out)

Layer / File(s) Summary
ADRs, reference specs, roadmap, and status docs
docs/decisions/0045-async-media-job-..., docs/decisions/0046-inline-media-out-..., docs/decisions/0031/0036/0038/0040-... (amended), docs/reference/contracts/*, docs/reference/shared-core/*, docs/reference/desktop/database-schema.md, docs/standards/error-handling.md, docs/roadmap/*, CLAUDE.md, README.md
Adds ADR-0045 (async media-job loop with park/poll/resume/cancel semantics, durable state via media_job:submitted, cost gating/realization, content_filter taxonomy) and ADR-0046 (inline media-out via generate() with de-inline at #emitDurable, streaming triad deferred to host). Amends ADR-0031/0036/0038/0040 to reflect media seam changes. Updates all reference contracts, provider seam, agent-runner, node-types, error-handling docs. Updates roadmap/current status to mark 1.AF done and 1.AG sections A–E landed on development.
Shared constants, run events, node schema, and config defaults
packages/shared/src/constants.ts, packages/shared/src/constants.test.ts, packages/shared/src/run-event.ts, packages/shared/src/run-event.test.ts, packages/shared/src/node.ts, packages/shared/src/node.test.ts, packages/shared/src/config.ts, packages/shared/src/config.test.ts
Adds MEDIA_SURFACES ('chat' / 'generative'), MEDIA_JOB_POLL_DEFAULTS, content_filter as fatal non-retryable error code, media_job:submitted run-event schema with jobId/provider/model/modality/startedAt/deadlineAt. Updates run:paused to support media-only suspension with pendingMediaJobNodeIds and relaxes pendingGateCount to non-negative. Adds agent node count/duration_seconds volume knobs. Adds config defaults for media_job_poll_initial_ms, media_job_poll_max_ms, media_job_deadline_ms with cross-field validation.
Database schema migration and LLM seam contracts
packages/db/drizzle/0004_numerous_ultimatum.sql, packages/db/drizzle/meta/_journal.json, packages/db/drizzle/meta/0004_snapshot.json, packages/db/src/schema.ts, packages/llm/src/types.ts, packages/llm/src/cost-tracker.ts, packages/llm/src/cost-tracker.test.ts, packages/llm/src/budget-estimator.ts, packages/llm/src/fallback-chain.ts
Adds media_surface text NOT NULL DEFAULT 'chat' to model_catalog via SQL migration with Drizzle schema and snapshot. Extends MediaCapabilitiesSchema with optional surface field, updates pollMediaJob signature to accept optional signal?: AbortSignalLike for aborting in-flight polls. Rounds per-entry media costs to integer micro-cents in both mediaCost() and estimateMediaCost() to align realized with estimated. Fixes NUL-character escape in fallback-chain cache key.
LLM adapters: streaming guard, media output, generateMedia support
packages/llm/src/adapters/shared.ts, packages/llm/src/adapters/gemini.ts, packages/llm/src/adapters/gemini.test.ts, packages/llm/src/adapters/openai.ts, packages/llm/src/adapters/openai.test.ts, packages/llm/src/adapters/anthropic.ts, packages/llm/src/adapters/anthropic.test.ts
Introduces assertNoStreamingMediaOutput guard to prevent streaming requests with media-out modalities. Gemini: emits inlineData as base64 media parts, lowers outputModalities to responseModalities on generate. OpenAI: adds inline audio output parsing from message.audio, implements generateMedia for synchronous image generation mapping to base64, normalizes content-policy/moderation errors to content_filter code. All adapters annotate surface: 'chat' in capabilities. Includes comprehensive tests for round-trip normalization and error mapping.
LLM conformance: generative seam and media surface validation
packages/llm/src/conformance/generative-seam.conformance.test.ts, packages/llm/src/conformance/spec.ts, packages/llm/src/conformance/fixtures/openai.ts
Adds generative-seam conformance tests covering generateMedia result shape validation, MediaGenResult exactly-one-of (jobId vs media), polling state transitions (pending → done → failed), content_filter error classification as non-retryable, and abort-signal forwarding identity. Extends ConformanceExpectations/ConformanceFixtures with mediaGenerate for sync generateMedia. Adds media-surface capability value validation. Wires OpenAI image-generation fixture.
Core executor contracts and catalog validation
packages/core/src/engine/node-executor.ts, packages/core/src/engine/node-handlers/dispatcher.ts, packages/core/src/index.ts, packages/core/src/validate-catalog.ts, packages/core/src/validate-catalog.test.ts
Introduces MediaJobSubmission interface with jobId/provider/model/modality/units/deadlineMs. Extends NodeOutcome with kind: 'media_job' variant. Adds optional pollMediaJob(job, signal) to NodeExecutor interface. Wires dispatcher to conditionally forward pollMediaJob when agent handler is present. Re-exports CheckpointPendingMediaJob. Updates catalog validation to bypass output-combination checks for generative-surface models and require exactly one billed media modality.
Agent turn: media-output generate branch and error mapping
packages/core/src/engine/agent-turn.ts, packages/core/src/engine/agent-turn.test.ts
Exports codeForLlmError for media dispatch reuse. Omits tools in LLM requests when non-text outputModalities are requested. Introduces generateOneTurn non-streaming wrapper. Refactors error mapping through throwMappedChainError helper. Adds early media-output branch with requestsMediaOutput predicate that applies primary-model pre-egress budget gate, calls chain.generate(), validates stop-reason (throws provider_unavailable on tool_use), and maps failures. Includes tests for inline media routing, cost events, failover, budget exceed, cancellation, and content_filter classification.
AgentRunner: media-surface fork and executeGenerativeMedia
packages/core/src/engine/agent-runner.ts, packages/core/src/engine/agent-runner.test.ts
Extends AgentRunnerDeps with optional resolveMediaSurface(model) dependency. Routes to executeGenerativeMedia for 'generative' models, otherwise continues chat-turn. executeGenerativeMedia validates exactly one billed modality, requires non-empty prompt, verifies provider.generateMedia exists, computes authored units, applies pre-egress budget gate with maxTokens: 0, checks cancellation, resolves credentials with secret redaction on failure, calls provider, and returns either sync media parts or async media_job jobId. Adds pollMediaJobThroughDeps helper that re-resolves provider/credentials and gracefully handles missing/failing polling. Extracts inline media from chat-turn results, fails non-retryable validation when media requested but not produced. Exports generativeUnits/realizedMediaCost helpers. Comprehensive test coverage.
Checkpoint: pending media job reconstruction and settlement tracking
packages/core/src/engine/checkpoint.ts, packages/core/src/engine/checkpoint.test.ts
Introduces CheckpointPendingMediaJob interface storing opaque jobId, provider/model/modality, and submit/deadline timestamps. Extends CheckpointState with pendingMediaJobs array. Implements applyMediaJobEvent that folds media_job:submitted into reconstruction: marks node as paused, per-node latest-wins keying, defensive stale-event skipping (events for already-terminal nodes ignored). Clears pending jobs when nodes reach terminal settlement (completed/failed/skipped). Tests validate per-node isolation and stale-event idempotence.
WorkflowEngine: async media park/poll/resume/cancel lifecycle
packages/core/src/engine/engine.ts, packages/core/src/engine/engine.test.ts, packages/core/src/engine/errors.ts, packages/core/src/engine/execution-host.ts, packages/core/src/engine/execution-host.test.ts
Introduces ParkedMediaJob runtime record and per-run poll timer disarms. On checkpoint resume, re-attaches jobs by reconstructing metadata (deadline, units, submittedAtMs). Nodes parked on media_job outcomes emit durable media_job:submitted, record in-memory parked job, and arm poll timers. Idle handling emits run:paused with pendingMediaJobNodeIds when media jobs pending. Poll loop with deadline short-circuit, adapter fault classification, and exponential backoff. Terminal settlement disarms timers, emits realized cost addends for still-pending jobs, then clears state. Paused nodes transition to failed on internal failure. Updates resumeFromCheckpoint input to optional gate fields (both provided = gate resume, both omitted = media-only resume), adds assertValidResumeInput, introduces beginResumeMediaJobs, and routes correctly. Adds pending_gate_requires_decision error code. Updates crash reconciliation to classify media_job:submitted as resumable. Comprehensive engine tests.
M2 end-to-end harness: full media scenario coverage
packages/core/src/engine/m2-e2e-harness.e2e.test.ts
Adds media-specific test infrastructure: inline/generative/async media provider stubs with scripted responses and call-count tracking. Poll-timer driver concurrently consumes run events and fires armed timers until terminal settlement. In-memory MediaStore storing bytes with deterministic media://sha256-... handle generation. Media capability fixtures and workflow fixtures for inline-out, generative-out, concurrent gate+job scenarios. End-to-end tests validate inline routing and de-inline to handles with base64 exclusion, generative routing with raw diagnostic exclusion, async job submit/poll success and failure with content_filter terminal errors, cross-process checkpoint re-attach without re-submission, cancel behavior (abort in-flight polls, cleanup), concurrent gate+media resume (both advance), rejection of media-only resume without gate decision, deadline short-circuit to retryable timeout, abort signal propagation, loud failure on unknown poll states, and repeated pending polls with correct backoff/re-arming.

Sequence Diagram(s)

sequenceDiagram
  participant Host
  participant WorkflowEngine
  participant AgentRunner
  participant LlmAdapter
  participant RunStore

  Host->>WorkflowEngine: executeRun(workflow)
  WorkflowEngine->>AgentRunner: executeAgent(node)
  AgentRunner->>AgentRunner: resolveMediaSurface(model)
  alt surface is 'generative'
    AgentRunner->>LlmAdapter: generateMedia(request)
    LlmAdapter-->>AgentRunner: MediaGenResult {jobId}
    AgentRunner-->>WorkflowEngine: NodeOutcome {kind:'media_job'}
    WorkflowEngine->>RunStore: persist media_job:submitted
    WorkflowEngine->>WorkflowEngine: arm poll timer
    loop poll with backoff until terminal
      WorkflowEngine->>LlmAdapter: pollMediaJob(jobId, signal)
      LlmAdapter-->>WorkflowEngine: MediaJobStatus
    end
    WorkflowEngine->>RunStore: emit cost:updated addend
    WorkflowEngine->>RunStore: emit node:completed/failed
    WorkflowEngine-->>Host: terminal node event
  else surface is 'chat'
    AgentRunner->>LlmAdapter: generate(request with outputModalities)
    LlmAdapter-->>AgentRunner: LlmResult {content:[media part]}
    AgentRunner->>RunStore: de-inline media to media://sha256-...
    AgentRunner-->>WorkflowEngine: NodeOutcome {kind:'completed'}
    WorkflowEngine-->>Host: node completed event
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • HodeTech/Relavium#11: Introduced the LlmProvider multimodal seam contracts (generateMedia, pollMediaJob, outputModalities) that this PR fully implements and extends with the signal parameter and surface capability field.
  • HodeTech/Relavium#17: Established the human-gate checkpoint/resume machinery in checkpoint.ts and engine.ts that this PR extends with the parallel pendingMediaJobs park/resume/cancel path.
  • HodeTech/Relavium#18: Introduced the AgentRunner/agent-turn core execution path that this PR extends with the resolveMediaSurface fork, executeGenerativeMedia, and the inline media-out generateOneTurn branch.

Poem

🐇 Hoppity-hop through the media lane,
A generate() call, no streaming refrain.
Jobs park and poll till the pixels arrive,
content_filter blocked? Fatal — don't retry!
Resume, re-attach, the checkpoint holds tight,
Base64 de-inlined to a media:// bright. 🖼️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: introducing 1.AG media output generation (Phase D) governed by ADR-0045/0046, implementing async media-job and inline media-out capabilities. It directly matches the scope and main objective of this comprehensive changeset.
Docstring Coverage ✅ Passed Docstring coverage is 93.65% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check 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-assist gemini-code-assist Bot 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 Phase 1.AG (Output Generation) of the multimodal sub-spine, introducing support for both inline media-out (via a single-shot generate() call) and separate-endpoint generative media (via generateMedia and an engine-owned async poll/checkpoint/resume/cancel loop). It updates the database schema, SSE event schema, and workflow specs to support these new capabilities, alongside comprehensive end-to-end and unit tests. The review feedback correctly identifies a critical issue in the OpenAI adapter's generateMedia implementation, where the images.generate call incorrectly passes an unsupported output_format parameter and fails to specify response_format: 'b64_json', which is required to retrieve the expected base64 image data.

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.

Comment on lines +883 to +890
const outputFormat = imageOutputFormat(req.mimeType);
let response: OpenAI.ImagesResponse;
try {
response = await createClient(key).images.generate(
{
model: req.model,
prompt: req.prompt,
...(outputFormat === undefined ? {} : { output_format: outputFormat }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

There are two critical issues with this images.generate call against the official OpenAI API:

  1. OpenAI's images.generate endpoint does not support an output_format parameter. Passing it will result in a 400 Bad Request error.
  2. The default response_format for OpenAI's image generation is 'url'. Since the code expects base64 data (response.data?.[0]?.b64_json), you must explicitly request response_format: 'b64_json'. Otherwise, the call will always fail to retrieve the base64 image data.
        response = await createClient(key).images.generate(
          {
            model: req.model,
            prompt: req.prompt,
            response_format: 'b64_json',
          },
          isAbortSignal(req.signal) ? { signal: req.signal } : {},
        );

…:check)

The review-fix round (717bc97) edited five files whose new lines did not match
Prettier's wrapping; `pnpm run format:check` (a separate CI task not covered by
`turbo run lint typecheck test build`) failed on them. Whitespace-only reformat;
no behavior change — typecheck + test stay green, format:check now clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/reference/shared-core/agent-runner.md (1)

23-34: ⚠️ Potential issue | 🟡 Minor

The pre-egress budget gate statement is correct but should link to the canonical gate definition.

Line 34's claim that "a pre-egress budget gate runs first (gate-only, maxTokens: 0)" is accurate (confirmed in agent-runner.ts lines 456–460). However, per the documentation guideline ("link, don't restate"), this spec should provide a link to the gate's canonical definition (e.g., BudgetGovernor, or the ADR covering pre-egress gating) rather than inline the mechanism (maxTokens: 0). The rationale for the zero-token estimate (generative calls emit no tokens) belongs in implementation comments, not the spec. Add a link to clarify where readers can find the full gate contract and cost-accounting behavior.

🤖 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 `@docs/reference/shared-core/agent-runner.md` around lines 23 - 34, In the
Generative fork subsection describing the dispatch behavior for `'generative'`
models, the statement about the pre-egress budget gate currently inlines the
mechanism detail `maxTokens: 0` in the spec. Remove the inline mechanism
explanation and replace it with a link to the canonical gate definition (such as
BudgetGovernor or the relevant ADR covering pre-egress gating). Keep the
high-level claim that "a pre-egress budget gate runs first" but delegate the
implementation rationale (why it uses zero tokens) to implementation comments
rather than the specification.

Source: Coding guidelines

🧹 Nitpick comments (3)
packages/llm/src/conformance/generative-seam.conformance.test.ts (1)

48-51: ⚡ Quick win

Prefer interface for GenerativeProvider object shape.

Use an interface here to align with the TypeScript style standard used in this repo.

As per coding guidelines, “Prefer interface for defining object shapes in TypeScript (per docs/standards/code-style-typescript.md)”.

Proposed refactor
-type GenerativeProvider = LlmProvider & {
+interface GenerativeProvider extends LlmProvider {
   generateMedia: NonNullable<LlmProvider['generateMedia']>;
   pollMediaJob: NonNullable<LlmProvider['pollMediaJob']>;
-};
+}
🤖 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/conformance/generative-seam.conformance.test.ts` around
lines 48 - 51, The GenerativeProvider type alias should be converted to an
interface to align with the repository's TypeScript style standards. Replace the
type alias definition with an interface that extends LlmProvider and includes
the generateMedia and pollMediaJob properties as required (non-nullable) members
instead of using type intersection with NonNullable utility types.

Source: Coding guidelines

packages/core/src/engine/agent-runner.test.ts (1)

796-802: ⚡ Quick win

Extend the invariant test to include the “both present” invalid result.

This currently asserts only the neither media nor jobId guard. Add a sibling case for { media, jobId } so the full XOR invariant is regression-protected.

🤖 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/engine/agent-runner.test.ts` around lines 796 - 802, The
current test case only verifies the invariant guard for when neither media nor
jobId are present in the result (the empty object case). Add a sibling test case
immediately after this one that checks the opposite invalid scenario where both
media and jobId are present in the result, ensuring the full XOR invariant is
regression-protected. Both test cases should use createAgentNodeExecutor with a
generativeProvider configured with the respective invalid result objects and
verify that the executor correctly fails with an internal error in both cases.
packages/core/src/validate-catalog.test.ts (1)

39-50: ⚡ Quick win

Add the negative generative-shape case in this suite.

This covers the valid 'generative' + ['image'] path, but not invalid shapes (e.g., ['text','image'] or multiple media modalities). Add one failing case so catalog validation and runtime generative-shape rules cannot drift.

🤖 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/validate-catalog.test.ts` around lines 39 - 50, Add a
negative test case in the same suite to validate that invalid generative-shape
configurations are properly rejected. Create a new test using agentWorkflow with
invalid output_modalities (such as combining multiple incompatible modalities
like 'text' and 'image'), set up the generativeCaps CapabilityFlags with the
'generative' surface, and expect validateWorkflowWithCatalog to throw an error
for that invalid configuration. This ensures the catalog validation rules for
generative-surface models remain consistent and prevent invalid shapes from
being accepted.
🤖 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 `@docs/reference/contracts/workflow-yaml-spec.md`:
- Line 228: Replace the ~100-word "Inline delivery (1.AG Section B,
[ADR-0046]...)" subsection with a condensed statement that references ADR-0046
instead of restating the behavior. The replacement should briefly note that when
output_modalities requests media, a chat-surface model issues a single-shot
generate() with no tool access, then link to ADR-0046 Section 1 for the full
details on de-inlining media to handles. Keep the remaining subsections on
vocabulary constraints and generative routing intact.

In `@packages/core/src/engine/agent-runner.ts`:
- Around line 529-555: The code at the async LRO branch (the first if statement
checking `result.jobId !== undefined`) does not verify that media is absent
before treating the result as an async job. Add an explicit guard before this
async-job branch to ensure both jobId and media are not present simultaneously,
enforcing the XOR relationship. If both jobId and media exist, return a failed
result with an appropriate error message indicating the provider returned an
invalid result with both fields populated, ensuring media is not silently
discarded.

In `@packages/core/src/engine/m2-e2e-harness.e2e.test.ts`:
- Around line 1002-1004: In the pollMediaJob method, replace the unsafe double
cast `as unknown as MediaJobStatus` with a `@ts-expect-error` suppression
comment. Add the `@ts-expect-error` comment on the line above or inline before
the return value, then remove the cast entirely so the function simply returns
`{ state: 'frozen' }` directly, allowing TypeScript to suppress the type
mismatch error while keeping the test intention clear.

In `@packages/core/src/validate-catalog.ts`:
- Around line 36-43: The unconditional `continue` statement in the generative
media surface block bypasses all catalog-time validation for the
output_modalities, allowing invalid declarations (such as multiple media
modalities or text mixed with media) to pass and only fail at runtime. Add
validation logic before the `continue` statement that enforces the same rule as
runtime dispatch: ensure the generative model's output_modalities contains
exactly one billed media modality with no text. If this validation fails, raise
an appropriate error instead of allowing the validation check to be skipped.

In `@packages/db/drizzle/0004_numerous_ultimatum.sql`:
- Line 1: The migration file 0004_numerous_ultimatum.sql and its associated meta
artifacts are out of sync with the database schema. Navigate to the `@relavium/db`
package and run the pnpm db:generate command filtered to that workspace, then
commit all regenerated migration and meta artifact files to resolve the schema
drift error reported by CI.

In `@packages/shared/src/run-event.ts`:
- Around line 279-283: The MediaJobSubmittedEventSchema allows deadlineAt to be
earlier than startedAt, which can admit malformed durable events and break
timeout semantics during resume/replay. Add a validation constraint to the
schema definition (using a method like .refine()) that enforces deadlineAt is
always greater than or equal to startedAt to ensure timestamp ordering is always
valid.

---

Outside diff comments:
In `@docs/reference/shared-core/agent-runner.md`:
- Around line 23-34: In the Generative fork subsection describing the dispatch
behavior for `'generative'` models, the statement about the pre-egress budget
gate currently inlines the mechanism detail `maxTokens: 0` in the spec. Remove
the inline mechanism explanation and replace it with a link to the canonical
gate definition (such as BudgetGovernor or the relevant ADR covering pre-egress
gating). Keep the high-level claim that "a pre-egress budget gate runs first"
but delegate the implementation rationale (why it uses zero tokens) to
implementation comments rather than the specification.

---

Nitpick comments:
In `@packages/core/src/engine/agent-runner.test.ts`:
- Around line 796-802: The current test case only verifies the invariant guard
for when neither media nor jobId are present in the result (the empty object
case). Add a sibling test case immediately after this one that checks the
opposite invalid scenario where both media and jobId are present in the result,
ensuring the full XOR invariant is regression-protected. Both test cases should
use createAgentNodeExecutor with a generativeProvider configured with the
respective invalid result objects and verify that the executor correctly fails
with an internal error in both cases.

In `@packages/core/src/validate-catalog.test.ts`:
- Around line 39-50: Add a negative test case in the same suite to validate that
invalid generative-shape configurations are properly rejected. Create a new test
using agentWorkflow with invalid output_modalities (such as combining multiple
incompatible modalities like 'text' and 'image'), set up the generativeCaps
CapabilityFlags with the 'generative' surface, and expect
validateWorkflowWithCatalog to throw an error for that invalid configuration.
This ensures the catalog validation rules for generative-surface models remain
consistent and prevent invalid shapes from being accepted.

In `@packages/llm/src/conformance/generative-seam.conformance.test.ts`:
- Around line 48-51: The GenerativeProvider type alias should be converted to an
interface to align with the repository's TypeScript style standards. Replace the
type alias definition with an interface that extends LlmProvider and includes
the generateMedia and pollMediaJob properties as required (non-nullable) members
instead of using type intersection with NonNullable utility types.
🪄 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: 8719a371-a845-4873-ad0e-84183715136c

📥 Commits

Reviewing files that changed from the base of the PR and between 5d26e7b and 717bc97.

📒 Files selected for processing (64)
  • CLAUDE.md
  • README.md
  • docs/decisions/0031-llm-seam-shape-amendment-multimodal-io.md
  • docs/decisions/0036-run-loop-substrate-event-bus-and-execution-host.md
  • docs/decisions/0038-agentrunner-llm-call-boundary.md
  • docs/decisions/0040-node-retry-budget-above-the-chain.md
  • docs/decisions/0045-async-media-job-loop-poll-checkpoint-resume-cancel.md
  • docs/decisions/0046-inline-media-out-via-generate-streaming-triad-deferred.md
  • docs/decisions/README.md
  • docs/reference/contracts/config-spec.md
  • docs/reference/contracts/sse-event-schema.md
  • docs/reference/contracts/workflow-yaml-spec.md
  • docs/reference/desktop/database-schema.md
  • docs/reference/shared-core/agent-runner.md
  • docs/reference/shared-core/llm-provider-seam.md
  • docs/reference/shared-core/node-types.md
  • docs/roadmap/current.md
  • docs/roadmap/deferred-tasks.md
  • docs/roadmap/phases/phase-1-engine-and-llm.md
  • docs/standards/error-handling.md
  • packages/core/src/engine/agent-runner.test.ts
  • packages/core/src/engine/agent-runner.ts
  • packages/core/src/engine/agent-turn.test.ts
  • packages/core/src/engine/agent-turn.ts
  • packages/core/src/engine/checkpoint.test.ts
  • packages/core/src/engine/checkpoint.ts
  • packages/core/src/engine/engine.test.ts
  • packages/core/src/engine/engine.ts
  • packages/core/src/engine/errors.ts
  • packages/core/src/engine/execution-host.test.ts
  • packages/core/src/engine/execution-host.ts
  • packages/core/src/engine/m2-e2e-harness.e2e.test.ts
  • packages/core/src/engine/node-executor.ts
  • packages/core/src/engine/node-handlers/dispatcher.ts
  • packages/core/src/index.ts
  • packages/core/src/validate-catalog.test.ts
  • packages/core/src/validate-catalog.ts
  • packages/db/drizzle/0004_numerous_ultimatum.sql
  • packages/db/drizzle/meta/0004_snapshot.json
  • packages/db/drizzle/meta/_journal.json
  • packages/db/src/schema.ts
  • packages/llm/src/adapters/anthropic.test.ts
  • packages/llm/src/adapters/anthropic.ts
  • packages/llm/src/adapters/gemini.test.ts
  • packages/llm/src/adapters/gemini.ts
  • packages/llm/src/adapters/openai.test.ts
  • packages/llm/src/adapters/openai.ts
  • packages/llm/src/adapters/shared.ts
  • packages/llm/src/budget-estimator.ts
  • packages/llm/src/conformance/fixtures/openai.ts
  • packages/llm/src/conformance/generative-seam.conformance.test.ts
  • packages/llm/src/conformance/spec.ts
  • packages/llm/src/cost-tracker.test.ts
  • packages/llm/src/cost-tracker.ts
  • packages/llm/src/fallback-chain.ts
  • packages/llm/src/types.ts
  • packages/shared/src/config.test.ts
  • packages/shared/src/config.ts
  • packages/shared/src/constants.test.ts
  • packages/shared/src/constants.ts
  • packages/shared/src/node.test.ts
  • packages/shared/src/node.ts
  • packages/shared/src/run-event.test.ts
  • packages/shared/src/run-event.ts

Comment thread docs/reference/contracts/workflow-yaml-spec.md Outdated
Comment thread packages/core/src/engine/agent-runner.ts
Comment thread packages/core/src/engine/m2-e2e-harness.e2e.test.ts
Comment thread packages/core/src/validate-catalog.ts
Comment thread packages/db/drizzle/0004_numerous_ultimatum.sql
Comment thread packages/shared/src/run-event.ts
…— XOR, load-check, ordering

Verified each PR-review finding against current code; fixed the still-valid ones
minimally, skipped the invalid/advisory with a reason. Toolchain 16/16 green,
format:check clean, Leakwatch 0.

Correctness:
- core(agent-runner): enforce the MediaGenResult XOR explicitly at the executor —
  a result with BOTH media and jobId now fails `internal` instead of letting the
  async jobId branch silently DISCARD the media (+ a both-present regression test).
  Folded the result-handling + error-mapping into `buildGenerativeOutcome` /
  `mapGenerateMediaError`, which also cuts executeGenerativeMedia's complexity.
- core(validate-catalog): a `media_surface: 'generative'` node now load-checks the
  same one-billed-modality rule the runtime dispatch enforces (exactly one of
  image|audio|video, no text) before skipping the inline outputCombinations check —
  a malformed generative node fails fast at load, not only at runtime (+ neg test).
- shared(run-event): reject a `media_job:submitted` whose `deadlineAt < startedAt`
  (Date.parse-compared) via the union-level superRefine — a member `.refine()` would
  make a ZodEffects and break the discriminatedUnion (+ reject test).

Docs (canonical-home, rule 8):
- workflow-yaml-spec: condense the inline-delivery prose to link ADR-0046 §1 instead
  of restating the de-inlining behavior.
- agent-runner.md: drop the `maxTokens: 0` mechanism detail; link the ADR-0028
  pre-egress governor (the "why zero tokens" stays in the code comment).

Style / Sonar (advisory — the SonarCloud gate already passes):
- m2 test: replace the `as unknown as MediaJobStatus` double cast with `@ts-expect-error`.
- conformance: `GenerativeProvider` type alias → interface extending LlmProvider.
- agent-runner.test: de-negate the generativeProvider ternary (Sonar).
- engine: optional-chain the media-units fallback (Sonar).
- openai(adapter): extract the nested error-kind ternary into an if/else (Sonar).

Skipped (with reason):
- openai images.generate `output_format`/`response_format` (CodeRabbit): the adapter
  targets gpt-image-1, which DOES accept `output_format` and ALWAYS returns b64_json
  (it rejects `response_format`); that param applies to dall-e-2/3, not this path.
- DB migration drift: `pnpm --filter @relavium/db db:generate` reports no changes —
  the 0004 migration + meta are already in sync (CI's migration-sync step passed).
- The four remaining cognitive-complexity smells (buildCommonBody, gemini mapContent,
  resumeFromCheckpoint, executeAgent): advisory, the Sonar gate is green, and a
  sub-15 refactor of that mostly-pre-existing whole-function logic exceeds "minimal".

Refs: ADR-0045, ADR-0046

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cemililik

Copy link
Copy Markdown
Contributor Author

@coderabbitai review all

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

@cemililik Sure! I'll kick off a full review of all the changes in this PR.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot 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

♻️ Duplicate comments (1)
packages/db/drizzle/0004_numerous_ultimatum.sql (1)

1-1: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Regenerate Drizzle artifacts; CI still reports schema drift.

The DB pipeline is still failing with @relavium/db migration is out of sync with src/schema.ts. Please rerun generation for @relavium/db and commit all resulting drizzle SQL/meta artifacts together.

🤖 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/0004_numerous_ultimatum.sql` at line 1, The migration
file for adding the `media_surface` column to the `model_catalog` table is out
of sync with the schema definition in src/schema.ts, causing CI to report schema
drift. Regenerate all Drizzle artifacts for the `@relavium/db` package by running
the appropriate Drizzle generation command, which will update both the SQL
migration files and metadata artifacts. Commit all generated Drizzle files
together to ensure the migration and schema definition are synchronized.

Source: Pipeline failures

🤖 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.test.ts`:
- Around line 672-675: The test function starting with "gates pre-egress with
maxTokens:0 + the media estimate → budget_exceeded (no generateMedia egress)"
has a duplicate declaration of the `info` variable in the same scope, causing a
TypeScript compile error. Search through this test function and identify both
declarations of `let info`, then remove one of the duplicate declarations to
ensure only a single `info` variable declaration exists within the test scope.

In `@packages/core/src/engine/engine.ts`:
- Around line 1407-1411: The condition checking `vertex === undefined` in the
media job resume handler currently returns silently without clearing the job
from `#pendingMediaJobs`, leaving the parked media job in an inconsistent state.
Instead of returning when the vertex is missing, you should treat this as a
terminal failure by clearing the job from `#pendingMediaJobs` before returning,
ensuring the job is properly cleaned up and the run does not get stranded in a
paused state. This cleanup is particularly important in the checkpoint and
workflow-drift code paths.

In `@packages/core/src/engine/m2-e2e-harness.e2e.test.ts`:
- Around line 182-210: The pollMediaJob method in the asyncMediaProvider
function currently ignores the _jobId parameter, which means the test doesn't
validate that the correct job ID is being re-polled on resume. Add validation
logic to the pollMediaJob method to assert that the incoming _jobId matches the
expected job ID (which should be 'job-1', the jobId returned from
generateMedia). If the jobId doesn't match the expected value, have the method
reject with an error instead of silently proceeding. This ensures the test
properly covers the invariant that the persisted jobId is actually being
re-polled during checkpoint re-attachment.
- Around line 1005-1009: The `@ts-expect-error` comment in the `pollMediaJob`
function is currently three lines long, which violates the style guide
requirement for one-line justifications. Extract the detailed explanation (the
parts about out-of-union job state, closed-switch's default arm, and terminal
failure) as a separate regular comment block placed directly above the
`@ts-expect-error` directive. Then reduce the `@ts-expect-error` comment itself
to a single-line justification that briefly explains why the type error is being
suppressed.

In `@packages/core/src/validate-catalog.ts`:
- Around line 44-46: The filter operation in the billed constant uses an unsafe
type cast `as readonly string[]` on MEDIA_BILLED_MODALITIES, which weakens type
safety. Replace this unsafe cast by creating a type guard function that properly
checks if a modality is included in MEDIA_BILLED_MODALITIES without using the
`as` assertion, then use that type guard in the filter operation instead of the
cast.

In `@packages/llm/src/types.ts`:
- Around line 200-205: The comment describing the `surface` field in the
media-output routing documentation contains outdated information. The comment
claims that no Phase-1 adapter implements `generateMedia`, but the OpenAI
adapter in packages/llm/src/adapters/openai.ts actually does implement sync
`generateMedia` for image generation. Update the comment on the line that
discusses Phase-1 adapter implementations to accurately reflect that at least
the OpenAI adapter supports `generateMedia`, removing or correcting the
statement that none of them implement it.

---

Duplicate comments:
In `@packages/db/drizzle/0004_numerous_ultimatum.sql`:
- Line 1: The migration file for adding the `media_surface` column to the
`model_catalog` table is out of sync with the schema definition in
src/schema.ts, causing CI to report schema drift. Regenerate all Drizzle
artifacts for the `@relavium/db` package by running the appropriate Drizzle
generation command, which will update both the SQL migration files and metadata
artifacts. Commit all generated Drizzle files together to ensure the migration
and schema definition are synchronized.
🪄 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: 5d8f275a-7b27-4f55-817c-fa4afbbe43ad

📥 Commits

Reviewing files that changed from the base of the PR and between 5d26e7b and 123c6b9.

📒 Files selected for processing (64)
  • CLAUDE.md
  • README.md
  • docs/decisions/0031-llm-seam-shape-amendment-multimodal-io.md
  • docs/decisions/0036-run-loop-substrate-event-bus-and-execution-host.md
  • docs/decisions/0038-agentrunner-llm-call-boundary.md
  • docs/decisions/0040-node-retry-budget-above-the-chain.md
  • docs/decisions/0045-async-media-job-loop-poll-checkpoint-resume-cancel.md
  • docs/decisions/0046-inline-media-out-via-generate-streaming-triad-deferred.md
  • docs/decisions/README.md
  • docs/reference/contracts/config-spec.md
  • docs/reference/contracts/sse-event-schema.md
  • docs/reference/contracts/workflow-yaml-spec.md
  • docs/reference/desktop/database-schema.md
  • docs/reference/shared-core/agent-runner.md
  • docs/reference/shared-core/llm-provider-seam.md
  • docs/reference/shared-core/node-types.md
  • docs/roadmap/current.md
  • docs/roadmap/deferred-tasks.md
  • docs/roadmap/phases/phase-1-engine-and-llm.md
  • docs/standards/error-handling.md
  • packages/core/src/engine/agent-runner.test.ts
  • packages/core/src/engine/agent-runner.ts
  • packages/core/src/engine/agent-turn.test.ts
  • packages/core/src/engine/agent-turn.ts
  • packages/core/src/engine/checkpoint.test.ts
  • packages/core/src/engine/checkpoint.ts
  • packages/core/src/engine/engine.test.ts
  • packages/core/src/engine/engine.ts
  • packages/core/src/engine/errors.ts
  • packages/core/src/engine/execution-host.test.ts
  • packages/core/src/engine/execution-host.ts
  • packages/core/src/engine/m2-e2e-harness.e2e.test.ts
  • packages/core/src/engine/node-executor.ts
  • packages/core/src/engine/node-handlers/dispatcher.ts
  • packages/core/src/index.ts
  • packages/core/src/validate-catalog.test.ts
  • packages/core/src/validate-catalog.ts
  • packages/db/drizzle/0004_numerous_ultimatum.sql
  • packages/db/drizzle/meta/0004_snapshot.json
  • packages/db/drizzle/meta/_journal.json
  • packages/db/src/schema.ts
  • packages/llm/src/adapters/anthropic.test.ts
  • packages/llm/src/adapters/anthropic.ts
  • packages/llm/src/adapters/gemini.test.ts
  • packages/llm/src/adapters/gemini.ts
  • packages/llm/src/adapters/openai.test.ts
  • packages/llm/src/adapters/openai.ts
  • packages/llm/src/adapters/shared.ts
  • packages/llm/src/budget-estimator.ts
  • packages/llm/src/conformance/fixtures/openai.ts
  • packages/llm/src/conformance/generative-seam.conformance.test.ts
  • packages/llm/src/conformance/spec.ts
  • packages/llm/src/cost-tracker.test.ts
  • packages/llm/src/cost-tracker.ts
  • packages/llm/src/fallback-chain.ts
  • packages/llm/src/types.ts
  • packages/shared/src/config.test.ts
  • packages/shared/src/config.ts
  • packages/shared/src/constants.test.ts
  • packages/shared/src/constants.ts
  • packages/shared/src/node.test.ts
  • packages/shared/src/node.ts
  • packages/shared/src/run-event.test.ts
  • packages/shared/src/run-event.ts

Comment thread packages/core/src/engine/agent-runner.test.ts
Comment on lines +1407 to +1411
const job = this.#pendingMediaJobs.get(nodeId);
const vertex = this.#plan.vertices.get(nodeId);
if (this.#settled || job === undefined || vertex === undefined) {
return; // run terminal, job cleared, or vertex gone
}

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

Handle missing resume-plan vertex as a terminal failure, not a silent return.

At Line 1409, returning when vertex === undefined after timer disarm leaves the parked media job uncleared and un-rearmed. This can strand the run in a paused/inconsistent state in the checkpoint/workflow-drift path already tolerated during seeding.

💡 Suggested fix
-    if (this.#settled || job === undefined || vertex === undefined) {
-      return; // run terminal, job cleared, or vertex gone
-    }
+    if (this.#settled || job === undefined) {
+      return; // run terminal or job cleared
+    }
+    if (vertex === undefined) {
+      this.#emitMediaJobCost(nodeId, job);
+      this.#clearMediaJob(nodeId);
+      if (this.#failure === undefined && !this.#cancelling) {
+        this.#failure = {
+          nodeId,
+          error: {
+            code: 'internal',
+            message: 'checkpoint media job references a node absent from the resume plan',
+            retryable: false,
+          },
+        };
+        this.#abort.abort();
+      }
+      this.#schedule();
+      return;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const job = this.#pendingMediaJobs.get(nodeId);
const vertex = this.#plan.vertices.get(nodeId);
if (this.#settled || job === undefined || vertex === undefined) {
return; // run terminal, job cleared, or vertex gone
}
const job = this.#pendingMediaJobs.get(nodeId);
const vertex = this.#plan.vertices.get(nodeId);
if (this.#settled || job === undefined) {
return; // run terminal or job cleared
}
if (vertex === undefined) {
this.#emitMediaJobCost(nodeId, job);
this.#clearMediaJob(nodeId);
if (this.#failure === undefined && !this.#cancelling) {
this.#failure = {
nodeId,
error: {
code: 'internal',
message: 'checkpoint media job references a node absent from the resume plan',
retryable: false,
},
};
this.#abort.abort();
}
this.#schedule();
return;
}
🤖 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/engine/engine.ts` around lines 1407 - 1411, The condition
checking `vertex === undefined` in the media job resume handler currently
returns silently without clearing the job from `#pendingMediaJobs`, leaving the
parked media job in an inconsistent state. Instead of returning when the vertex
is missing, you should treat this as a terminal failure by clearing the job from
`#pendingMediaJobs` before returning, ensuring the job is properly cleaned up
and the run does not get stranded in a paused state. This cleanup is
particularly important in the checkpoint and workflow-drift code paths.

Comment thread packages/core/src/engine/m2-e2e-harness.e2e.test.ts
Comment thread packages/core/src/engine/m2-e2e-harness.e2e.test.ts
Comment thread packages/core/src/validate-catalog.ts Outdated
Comment thread packages/llm/src/types.ts
… Sonar threshold

Addresses the 4 SonarCloud cognitive-complexity smells on the 1.AG PR via
behavior-preserving cohesive extractions (control flow identical, only relocated):

- core(agent-runner): executeAgent (18→~5) — the post-runAgentTurn outcome logic
  moves into `buildChatTurnOutcome(node, result, hasOutputSchema)` (media-parts →
  produced-none → output_schema → text, order preserved; the output_schema check
  is a De Morgan flip to an early text return).
- core(engine): resumeFromCheckpoint (16→~4 on the validation arm) — the two
  gate-input throws move into the free `assertValidResumeInput(input)` (called
  before #runs.has, throws byte-identical; no `this`).
- llm(gemini): mapContent (19→~3) — the per-part if/else-if chain moves into
  `mapGeminiPart(part, ids)` using early returns; the inline-media branch RETURNS
  in every sub-case (incl. empty-bare-MIME → undefined) so a matched media part is
  consumed and never re-interpreted as text — the original else-if semantics.
- llm(openai): buildCommonBody (20→~10) — the response_format json branch →
  `toOpenAiResponseFormat` (typed to the `{type:'json'}` member, returns the
  non-undefined response_format) and the audio voice/format resolution →
  `resolveOpenAiAudio`.

Strictly behavior-preserving — verified by the full existing suites passing
unchanged (core 848, llm 381+8) plus an independent 4-agent adversarial
branch-by-branch equivalence check (0 findings, GO). Toolchain 16/16 green
(incl. the tsc build), format:check clean, Leakwatch 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cemililik and others added 2 commits June 20, 2026 23:50
…eanup, type-guard, comments

Verified each finding against current code; fixed the still-valid ones minimally,
skipped the invalid/duplicate with a reason. Toolchain 16/16 green, format:check
clean, Leakwatch 0.

Fixed:
- core(engine): #pollMediaJob now CLEARS the orphaned job (+ #schedule) when the
  parked vertex is gone from the plan (same-slug content drift on resume) instead
  of returning silently — a silent return stranded the run paused forever on a job
  that can never re-attach; clearing it lets the now-jobless idle settle the run.
- core(m2-e2e): asyncMediaProvider.pollMediaJob now asserts the re-polled jobId is
  the persisted 'job-1' (rejects a mismatch), covering the re-poll-the-persisted-id
  invariant — especially the cross-process resume re-attach (MJ-1).
- core(m2-e2e): the default-arm test's `@ts-expect-error` is reduced to a one-line
  justification (the rationale moved to a regular comment above the directive).
- core(validate-catalog): replace the `MEDIA_BILLED_MODALITIES as readonly string[]`
  cast with a real `isBilledModality` type guard (`m is MediaBilledModality`, via
  `.some` — no `as`).
- llm(types): correct the CapabilityFlags `media.surface` comment — `surface` is a
  per-model catalog property, not an adapter flag; the OpenAI adapter DOES implement
  sync generateMedia (gpt-image-1), yet its capability surface stays 'chat'.

Skipped (with reason):
- agent-runner.test "duplicate `let info`": no duplicate exists (only one decl at
  line 674); typecheck is green (a redeclaration would be TS2451) — invalid finding.
- DB migration drift (duplicate): `pnpm --filter @relavium/db db:generate` reports
  no changes — the 0004 migration + meta are in sync (CI's migration-sync passed).

Refs: ADR-0045
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…run:paused) + coverage pins

The Opus-consolidated final pre-merge review (18 Sonnet finders → Sonnet 2-lens
adversarial verify → Opus consolidate/synthesize over the full PR diff) returned
NO-GO on two HIGH defects — both cross-interactions of THIS PR's own earlier
review-round fixes, on explicitly non-negotiable merge-gate invariants:

- H1 (engine #pollMediaJob, the orphaned-vertex branch): a paid async media job
  whose parked node vanished from the plan (same-slug CONTENT drift on resume) was
  CLEARED without emitting its realized cost addend → zero-cost terminal, violating
  ADR-0045 §5 (exactly one addend on EVERY terminal path). Now emits the addend
  before clearing (the provider billed the submit regardless of the drift).
- H2 (engine #seedFromCheckpoint): the #pauseEpisode de-dup suppression (added to
  avoid a duplicate run:paused on resume) wrongly fired in the CRASH-IN-WINDOW case
  (media_job:submitted persisted, run:paused not — runStatus stays 'running'),
  suppressing the legitimate run:paused → a gap in the gap-free stream (ADR-0036)
  on the exact window this PR added media_job:submitted to RESUMABLE_LAST_TYPES to
  support. Now guarded on `cp.runStatus === 'paused'` (only suppress when the prior
  process actually announced the pause).

Regression tests + coverage pins (the verified should-fix set):
- core(m2-e2e): H1 — an orphaned-vertex resume (drifted workflow, same id, no 'work'
  node) asserts exactly one cost:updated for the orphaned node; H2 — a crash-in-window
  resume (store copied minus run:paused) asserts exactly one run:paused before
  terminal. Plus L1 cost-addend pins on the resume→done and deadline e2e paths.
- core(agent-runner.test): M1 — BudgetPauseError on the generative pre-egress gate →
  paused (the human-gate seam, not internal); M2 — a cancel landing DURING
  generateMedia (the post-await re-check) → cancelled with no stray cost:updated.

All other dimensions verified clean (I3, seam/engine purity, secret redaction, the
async state machine, schema/contracts, concurrency, the 4 complexity refactors).
Toolchain 16/16 green, format:check clean, Leakwatch 0. Verdict after these fixes: GO.

Refs: ADR-0045, ADR-0036
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cemililik
cemililik merged commit 713feec into main Jun 21, 2026
9 checks passed
@sonarqubecloud

Copy link
Copy Markdown

cemililik added a commit that referenced this pull request Jun 21, 2026
1.AG (media output generation, Phase D) merged to main via PR #37 (2026-06-21),
including the final pre-merge review's two HIGH fixes (orphaned-vertex cost
addend + crash-in-window run:paused). Flip its status everywhere, per the
Roadmap-Done-After-Merge rule:

- phase-1-engine-and-llm.md: the 1.AG dependency-table row (◇ → ✅ Done, PR #37),
  the 1.m6 summary row (1.AG ✅), and the narrative entry's Status line.
- current.md: 1.AG ✅ Done (PR #37); "only 1.AH remains" on the 1.m6 sub-spine.
- deferred-tasks.md: tick [x] the A5 async-media-job ADR + engine-loop obligation
  (PR #37).
- CLAUDE.md: both Status paragraphs note 1.AG Done; remaining Phase-1 work is 1.AH.
- README.md: 1.AG ✅; only 1.AH remains.
- llm-provider-seam.md: clarify the streaming media triad — non-streaming output
  landed at 1.AG (generate() + generative endpoint, ADR-0046); the streaming triad
  is deferred to 1.AH (ADR-0046 §4).

Docs-only; format:check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to 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