Skip to content

feat(chat): migrate chat protocols onto pipelines - #476

Draft
Menci wants to merge 192 commits into
feat/pipeline-non-chatfrom
feat/pipeline-chat
Draft

Menci wants to merge 192 commits into
feat/pipeline-non-chatfrom
feat/pipeline-chat

Conversation

@Menci

@Menci Menci commented Aug 16, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #475; remains draft until that dependency lands.

Moves the four chat protocols, count-token operations, Responses compaction and WebSocket transport onto immutable fact pipelines and provider-owned stages. Native/translated paths preserve rule order, immutable inputs and unchanged object identity. Each stage owns a module, including hosted tools and their sibling helper directories.

Responses loops retain each actual billable observation. Search/image calls launch independent runs with their own input facts, prologue, settlement and diagnostics. Local warmups/probes have no billable inference rows; reported zero differs from called-but-unreported usage. Chat Completions accepts clean EOF, with read faults/cancellation preserving observed quantities. Lazy hosted image formatter faults retain their original cause and failed diagnostics; finite readers transfer ownership before native reads.

Client recording uses @floway-dev/dump directly after protocol-error and WebSocket metadata projection. The gateway adapter retains attribution, drain, HTTP measurement and subrequests. Shared metadata, store and wire contracts move to dump; final gateway facades/exports are removed, while SQL persistence schemas remain in the adapter. Migration 0086 deletes obsolete edge records/files. The inspector reads completed run facts and literal protocol frames; live writer/reader integration is in #565.

Read Chat Completions first, then Responses state/hosted loops, child runs, final client recording and legacy diagnostic closure.

Validation at 3c89c152a: all eight exact-head CI checks passed, including full tests, typechecks, genuine installers, repository checks and Web build/output assertions. Focused checks passed 1283 tests plus the final portable/storage regression rerun. Independent AST comparison confirmed unchanged metadata schemas, codec, wire conversion, subrequest creation, response measurement and drain/metadata assembly; ten migrated chat modules changed imports only.

Menci added 30 commits August 16, 2026 22:07
…ols share

The first commit of the chat migration. Four source protocols, ten translation
directions between them, and one array of stages every chain runs — this is the
space they run over and the first three entries of that array.

The key layout says two things the context object could not. Each protocol has
its own request and response key, so a translation consumes the source and
provides the target — and `compose` then refuses a chain that could re-enter its
own protocol, which is `ctx.targetApi === <self>` made structural. And `ingress.*`
is what the client asked for, so `wantsStream` is four disjoint keys rather than
one: a translated request must not inherit the target protocol's answer to a
question the client never asked.

That guard turns out to be two things wearing one expression. "Do not run on a
re-entered request" is now structural and no stage asks it. "Which protocol was
chosen" stays a live question, because a vendor normalizer genuinely needs to
know — it becomes a declared fact read through `needs`, which is visibility
rather than removal. So the five guards are not deleted by the migration; they
change what they read.

The rewrites themselves are transcribed, not redesigned: they already worked. What
is new is that a stage returns the record it hands on instead of assigning to
`ctx.payload`. Today's code already writes its map conditionally, which is the
convention that keeps a 49-message conversation costing three objects, so the
tests assert identity rather than equality — a payload no flag touched comes back
as the same object, and a flag that fires but changes nothing changes nothing.

Gemini is source-only. Nothing in `packages/translate/src/` targets it, so by the
ruling it contributes one pipeline rather than two: a source role and no target
role.
The chat interceptors were written against route.candidate, the key the selector
split replaced. They reach for the live candidate only to read its enabled
flags, and the selector carries those as data — so they now read what travels in
the record and the tests need no candidate stub at all.

The shared chat stages arrive with them: resolveChatCandidates enumerates,
narrows to the wires a source protocol can reach, and orders by affinity, which
can refuse outright when a turn's own state requires two upstreams at once.
The reference chat chain, and what it establishes for the three families that
follow: the edge decides whether the client sees the frames or the one object
they add up to, which is where the stream-to-value collection belongs — the
upstream speaks SSE whatever the client asked for, so folding is the edge's own
work rather than a second reading.

The shared chat resolver narrows to the wires a source protocol can reach and
orders what is left by affinity, which can refuse a turn whose carried state
needs two upstreams at once. The interceptors run as ordinary stages between the
fork and the ending.

Metering reads the upstream's own usage off its own events as they pass, so a
streaming turn settles from the promise its last chunk resolves rather than in
the stage. The route still runs the existing surface.
The chain composed and nothing had run it. Doing so found the edge handing its
SSE view up without move(), which the handover gate refuses — a stream the run
never took ownership of would have escaped the record's own guarantee.

The five cases are the ones only running can state: frames written out when the
client asked to stream, the same frames folded into one object when it did not,
the upstream's forwardable headers kept and content-length dropped, a refusal
carrying the upstream's own status and words, and a dial nobody answered failing
over to the next candidate rather than ending the run.
The chain declared the services affinity needs and then read the client's own
payload straight out of the record, so the per-candidate rewrite was lost — a
turn carrying state for one upstream would have been sent to another unchanged.
The ending now asks for what affinity materialized, and records the candidate
that answered so a follow-up turn comes back to it.
A chat run's three extra services are all the live half of resolution: the
candidate, and the payload affinity materializes for it. Neither can enter the
record — move() would freeze the provider's own models cache, and materializing
is per candidate — so the resolver keeps them and the stage that dials asks back
by selector, which is the shape the shared services already use.

The seam splits rather than being wrapped. Building the chat context over an
already-built one would have minted a second dump accumulator, and the body can
only be taken once: takeRequestBody empties what it is given, so the second call
would hand the dump an empty buffer. What is shared is now the options and the
services, and each family builds the one context its run has.
Gemini contributes one pipeline rather than two, because nothing translates into
it, and it has no wire of its own: the ending translates the turn out to Chat
Completions, dials, and translates the answer back, metering on the dialect the
upstream actually spoke. Only that first wire is built — the Messages and
Responses ones, `:countTokens`, this family's interceptors and the affinity
egress are named in the header as absent rather than left implied.

Running the chain settled three things it had wrong. The stream handed to the
client is moved into the record like every other fact; the turn that is
translated is the one affinity materialized for the candidate, and the candidate
that answered is marked for the next turn; and the metered reading is converted
to billing metrics rather than cast to them, since a cast leaves a TokenUsage
where UsageQuantities is declared and the usage row it writes then carries no
metric at all.
A translation is two declarations: it consumes the source protocol's request
key and provides the target's, and coming back it consumes the target's response
key and provides the source's.

Saying it that way retires the runtime test. The source key is gone below the
handoff, so a stage needing it cannot be placed there and compose says so at
assembly — which is ctx.targetApi === <self> deleted and made structural. And
nothing below knows a translation happened: the target chain sees its own
protocol's keys and only those, so one chain serves a native request and a
translated one.

A refusal is handed to the pair as the upstream's own bytes, with the headers
that actually came back rather than a synthesized set — that is what lets a
context-window error become the shape Claude Code reads for auto-compaction. An
answer that arrived as a value has no pair to map it, so the handoff fails
rather than handing the client another protocol's object under its own key.
…oken categories

What an upstream reports is per token category and what is billed is per metric
name; the two are different shapes and a cast between them typechecks. The usage
row was therefore written with no metrics at all — the recorder looked for
input_tokens and found input.

Converting through the same function every other caller uses fixes it, and the
chain now asserts the quantities rather than only that a row was written.
The chain rendered failures through the shared envelope, which is the OpenAI
families' shape: a Gemini SDK reads error.status — the Google-RPC name — and
would have found it undefined, and error.code carries the HTTP status the
generic shape has no field for.

The mapping the replaced surface used already existed next door; it is now a
renderer the edge calls, so the two entry points that build this envelope agree
on one definition of it.
/v1/messages served through the chain Chat Completions established: an edge that
writes Anthropic's own named SSE events when the client asked to stream and
reassembles them into one message when it did not, settlement above the fork,
affinity-ordered candidates, and a native ending that dials callMessages.

Three things the replaced serve/attempt/respond surface said are said here rather
than left implied. The turn that goes out is the one affinity materialized for
the candidate, and the candidate that answered is marked for the next turn. The
beta flags keep their typed transport path — read off the inbound headers and
dropped from what the provider is handed, so no header allowlist can admit them
and no other source protocol can leak them in. And a dial that never connected is
a failure value the fork moves past rather than a throw that ends the run.

Only the native wire is built. The translated ones, count_tokens and this
family's own interceptors are named in the header as absent rather than left
implied. What the header cannot say is that a pre-upstream refusal now renders in
the shared error envelope rather than Anthropic's, since renderErrorEnvelope is
what the edge writes; an upstream that refused in its own words is still handed
on in them.
Builds `/v1/responses`' own wire as a pipeline, on the chain Chat Completions
established: the edge that writes SSE frames or the one response object, settlement
above the fork, candidate narrowing ordered by affinity, failover, and an ending that
dials `callResponses`.

Two things this family states that its siblings do not. A provider answers with the
branch it actually ran, and one of them is not a stream — a compaction is a single
envelope that carries its own counts — so it rides at the response key's value arm
rather than at a key of its own or lowered into synthesized frames. And the client's
stream is terminated by `[DONE]` here rather than by whatever the upstream's stream
ended on, which is what the transport reads to know the turn is over; the ending stops
reading at the turn's terminal event, and a stream that never states one ends the run
with the sentence the replaced surface used.

Scope is one wire and one transport. The WebSocket entry, `/v1/responses/compact`, the
two translated wires, this family's interceptors and the stored-items membrane are each
a step of their own and are named in the file header rather than implied by absence.

Tests run the chain: frames written out, frames folded into one response, an envelope
served as itself, a refusal answered in the upstream's own status and words, and a dial
nobody answered failed over to the next candidate.
… the upstream said

Three families rendered a gateway-synthesized refusal through the OpenAI
envelope. A Gemini client reads error.status — the Google-RPC name — and a
Messages client reads a top-level type and a request_id; neither shape carries
those, so both would have found the fields missing.

The rule the envelope already encoded stays: an upstream that refused in its own
words is handed on in them, because those words are already what its client
reads. Only a refusal the gateway itself produced has no body to forward, and
that is the one case where the *client's* protocol decides the shape. That
distinction is now named once and each protocol supplies its own envelope.

A turn whose carried state needs two upstreams at once answers 400, as every
family did before the shared stage invented a 409 for it.

The three chat chains assemble alongside the six that came before them.
The seam wrote an SSE comment as keepalive, which is invisible to any client by
design — but Anthropic defines a ping event and its clients read one, so the two
are not the same wire byte. A family that has its own idle frame now names it
alongside the frames it is answering with, and Messages exports the one it
writes so the route that serves that chain cannot forget it.
The interceptors rewrite the request as a fact and the ending read the payload
back from the resolver, so all three stages mutated a copy nobody sent. Deleting
every one of them left the suite green.

Affinity's per-candidate payload now enters the record below the fork, where
there is a payload to speak of — each candidate has its own, and re-running the
suffix is what produces the next. Everything between that stage and the dial
rewrites a fact, and the ending sends the fact.

Four comments claimed the fact space deletes the ctx.targetApi guards. The
record retracts that: the claim rested on splitting the interceptor array by
role, which was rejected — 「这里没有 source 和 target 之分!这是同一个数组!」 —
and with one array the guards remain. What changed is that what they test is
declared rather than ambient, which is visibility, not deletion.
…teway made

A gateway-synthesized refusal reached the client as type: 'api_error' — which
says the gateway broke rather than that the request did — and Responses lost the
param and code that name which field was at fault and why routing failed.

A protocol's envelope carries more than a status and a sentence, and only
whoever refused knows those, so the refusal now carries what it would write and
the edge renders it. The upstream's own body still wins wherever there is one.

Which refusal it was travels with it: a turn that cannot be routed, a model no
wire reaches, and a model no upstream has are three different things to say.
Time to first token was written in one place no stage reaches, so every chat run
took the neutral branch and recorded no TTFT at all. It is measured where the
token is — the only place that can tell a generated frame from the envelope
around it.

A rate can depend on the service tier and on how much input there was. Both are
selector coordinates rather than quantities, and dropping them left the pricer
with an empty selector, so neither axis could ever apply. The reading now goes
through the same measurement the other families use and carries both.
…ng reads

The shared stages' header described a dispatch stage that picks a wire. No such
stage was built — every chain dials its own protocol — so what it now describes
is the payload entering the record below the fork, and what will decide a wire
once the translated ones land.

The selector is one statement about a candidate and was written twice; the chat
resolver uses the definition the shared one already had. ChatNarrowing.source
was declared and never read.

A handoff declared it needs the upstream's headers and then supplied [] when
they were absent, which would have hidden the assembly error that absence is.
The ending read its payload back from the resolver while everything between the
fork and the dial rewrites the request as a fact, so a rewrite would have gone
nowhere. Affinity's per-candidate payload now enters the record below the fork,
where there is one payload to speak of, and the ending sends the fact.

Time to first token was written in one place no stage on this chain reaches, so
every Messages run took the neutral branch and recorded no TTFT at all. It is
measured where the token is — the only place that can tell a generated frame
from the envelope around it.

A rate can depend on the service tier and on how much input there was. Both are
selector coordinates rather than quantities, and dropping them left the pricer
with an empty selector, so neither axis could ever apply. The reading now goes
through the same measurement the other families use and carries both.

A stream that ran out before message_stop was served as a whole answer, and
frames an upstream wrote after it went out to the client. The meter now stops
at the terminal event and fails a stream that never reached one, which is what
the protocol's own reassembly already did for the folded shape alone.

ChatNarrowing.source goes with the key it was declared under.
…e its first token

The ending read its payload back from the resolver while everything between the
fork and the dial rewrites the request as a fact, so a rewrite would have gone
nowhere. Affinity's per-candidate payload now enters the record below the fork,
where there is one payload to speak of, and the ending sends the fact.

Time to first token was written in one place no stage on this chain reaches, so
every Responses run took the neutral branch and recorded no TTFT at all. It is
measured where the token is — the only place that can tell a generated frame
from the envelope around it.

The two readings this chain already had right — pricing facts beside the
quantities, and a stream that fails rather than answering short — were carried
by no test, which is how the other chains came to disagree with them. They have
one each now.

ChatNarrowing.source goes with the key it was declared under.
…tates

The ending read its turn back from the resolver while everything between the
fork and the dial rewrites the request as a fact, so a rewrite would have gone
nowhere. Affinity's per-candidate turn now enters the record below the fork,
where there is one turn to speak of, and the ending translates the fact.

Time to first token was written in one place no stage on this chain reaches, so
every Gemini run took the neutral branch and recorded no TTFT at all. It is
measured where the token is, on the dialect the upstream spoke — the same one
the usage beside it is read on.

A rate can depend on the service tier and on how much input there was. Both are
selector coordinates rather than quantities, and dropping them left the pricer
with an empty selector, so neither axis could ever apply. The reading now goes
through the same measurement the other families use and carries both.

A wire that closed cleanly had not thereby finished the turn: with no finish
reason on any choice, nothing that came out of the translation said the answer
was over, and a client streaming was served those frames as a whole answer. The
folded shape already refused them, through the protocol's own reassembly; both
shapes now do, because the ending stops at the turn's terminal frame and fails
a stream that ran out before one.

ChatNarrowing.source goes with the key it was declared under.
The meter read to exhaustion, so an upstream that dropped mid-turn was served as
a whole answer and anything it sent after its terminator was forwarded. The
streaming path is where this has to be caught: nothing folds those frames, so
the collector's own check never sees them.

The terminator is written out before the read stops, because it is what a client
reads as the end.

The first test written for this passed with the guard deleted — it drove the
non-streaming path, where the collector throws the same sentence for its own
reasons. It now drains the stream, and both halves of the guard are covered.
# Conflicts:
#	packages/gateway/src/data-plane/pipeline/serve.ts
The chain existed and was tested against stub providers, but nothing reached it:
the route still built a chat context and ran the onion, so every property the
chain states was stated only under test. The handler is now the prologue and
epilogue the other migrated families use — read the ingress, open the chat
prologue against a scratchpad store, hand the payload over, and write what the
run answered with.

Two decisions stay at the entry because only it can make them, and both are read
before any stage can rewrite what they are read from: whether the client asked to
stream, which the run has to be opened knowing, and whether it asked to be shown
the usage chunk metering asks the upstream for either way. A body that is not
JSON never enters a pipeline at all — there is no model to resolve and no attempt
to make — so it is answered in this protocol's own 400 envelope.

Wiring is what states what the chain does not carry yet, and two of them are
written down in failing tests rather than papered over. The affinity egress never
runs, so a turn hands back no `reasoning_opaque` and the next one cannot be pinned
to the upstream that served this one; `http_test.ts` says so twice and is left
saying it. Six of the nine interceptors are not stages — the usage-chunk request,
both usage normalizers and the three vendor normalizers — a refusal that reached
no upstream leaves the streamed-usage key unwritten, and the meter reports what
was billed without reporting whether the stream reached its terminator. Each of
those is the chain's own to close.
The chain existed and was tested against stub providers, but nothing reached it.
The generate entry is now the prologue and epilogue the other migrated families
use; `/v1/messages/count_tokens` stays where it is, because it is a second
operation over this protocol rather than another wire under its chain.

A stream is handed to the seam with this protocol's own idle frame. Anthropic
defines a `ping` event and its clients read one, so an idle connection is held
open with that rather than with an SSE comment no client sees — the chain names
the frame and the route passes it on. A body that is not JSON is answered in
Anthropic's own envelope, and a body carrying `anthropic_beta` or `betas` is
refused as before, both now recorded as the gateway refusals they are.

Wiring states what the chain does not carry yet. None of this family's five
interceptors is a stage, and the first of them is the one `http_test.ts` catches:
Claude Code's one-token model probe is answered by dialling an upstream instead of
by the gateway. The web-search shim, the billing-attribution scrub, the reasoning
and role rewrites, and the affinity egress are absent with it; so is the mid-stream
`error` event `respond.ts` wrote into a client's own stream. A refusal that reached
no upstream leaves the streamed-usage key unwritten, and the meter reports what was
billed without reporting whether the stream reached its terminator.
The chain existed and was tested against stub providers, but nothing reached it.
Both generate actions are now the prologue and epilogue the other migrated
families use; `:countTokens` stays where it is, because it is a second operation
over this protocol rather than another wire under its chain.

Gemini carries the model in the path rather than the body, so the id the run
resolves against is the one the route already split off the action segment, and
whether the turn streams is the action itself rather than a field. A body that is
not JSON is answered in the Google-RPC envelope this protocol's clients read.

Wiring states what the chain does not carry yet, and `http_test.ts` catches the
first: the chain has one wire, so a candidate reachable only over Messages or
Responses is dialled on Chat Completions anyway and the turn fails. None of the
four Gemini interceptors is a stage, the thought-signature rewrite and the affinity
egress on the way out are absent with them, and a stream that ends without a
terminal event ends the run as a throw rather than as the Google-RPC envelope
`respond.ts` wrote into the client's own stream. A refusal that reached no upstream
leaves the streamed-usage key unwritten, and the meter reports what was billed
without reporting whether the stream reached its terminator.
…sal settle at all

Two defects the non-chat families had already been through, both found by wiring
the routes.

Every chat chain handed its usage up as a bare list, so each handler adapted it
with failed: false — a stream that stopped before its terminator was recorded as
a turn that produced what it said it would. The meters now report both together,
which is what the seam has taken since the non-chat families moved, and the four
adapters delete.

A refusal never provided the streamed-usage key at all, so settlement's
still-reading test read undefined and deferred a turn that had no stream to wait
for: the run wrote no performance row. Refusing now says there is nothing still
to read, which is the same statement an ending makes when it did not stream.
…ages

Chat Completions had three of its interceptors as stages; the other three families
had none, so their chains ran nothing between the fork and the ending. Gemini's
thought suppression was among the missing, which is a client that never opted in
being shown thought parts.

Thirteen stages, one per (interceptor, protocol) pair because the payload types
differ, and one rule each: the role fold, the flag reading and the key removal are
written once, and a protocol contributes only the walk over its own items. Two of
them speak about the response — Gemini's thought suppression and the Responses
cache-token fold — and say so as a response-direction declaration, carrying what
they read on the way down in the stage's own closure, because a response-side
`needs` can only name what the ending provides.

What the interceptor form did by mutation is a rewrite now: the record is frozen,
so Gemini's three strippers cannot delete a field in place. Every rule writes
conditionally, so a turn it does not touch comes back by identity and the layer
costs what it actually changed. A `ctx.targetApi` guard is gone wherever array
position now says the same thing.

Four interceptors stay in the interceptor form. The Claude Code probe answers with
the Messages family's own response facts rather than rewriting a payload, and the
Responses compact shim, the Responses server-tool shim and the Messages web-search
shim each drive a turn of their own. Composing the stages into each family's chain
is the next step and is not here.
… chain runs it

The per-rule tests say what one stage does; nothing said that the array a family's
`pipeline.ts` will hold assembles at all. `compose` refuses declarations that do not
line up, so running the whole array between stages that declare what the real
neighbours declare — an edge that needs the answer, the headers and the billed set
on the way up, and an ending that answers with all three — is that check.

It also pins the one ordering that lives between stages rather than inside one: the
reasoning sentinel is the gateway's canonical form, and a vendor normalizer can only
put it on the wire in the vendor's shape because it runs after the stage that wrote
it.
Menci added 24 commits October 1, 2026 07:29
Integrate asynchronous scoped recording, resource/deferred drain before dump finalization, and exceptional settlement recovery. Preserve chat run-only dump identity, bind independent hosted/WebSocket runs, and keep count endpoints nonbillable on exceptional exits.
Bring deferred quota failures and pipeline failure boundaries into the chat stack while preserving provider-owned pipeline factories.
Keep terminal-based collectors from closing their recorded cursor prematurely, then exhaust that same cursor to publish a real stream.end. Verify completed HTTP generation and compaction dumps retain source-exhaustion proof.
Remove the duplicate buffered production write surface now that every run uses putRun. Prepared fixtures exercise the actual streaming contract through test-only support; detail reads still return the complete stored artifact.

Updated storage and frame documentation. Verified 62 affected tests, gateway typecheck and scoped lint, preserving retention, unique-file collision handling, staged leases and bytes-before-metadata publication.
Move event-id injection, keepalive sequence shifts, terminal buffering, and original failure envelopes into a declared projection stage. Socket delivery uses prepared JSON packets and selected dump frames match actual wire events. Keep transport keepalives excluded from the protocol record and native count read failures nonbillable.
Resolve the client stream named by run facts and retain its stream.end state. Normal Chat Completions EOF collects without an invented DONE; a terminal protocol frame alone cannot complete an interrupted recording. Display an incomplete-recording warning independently of protocol failure.

Render and copy already-projected Anthropic citation wire fields directly while retaining canonical non-stream reassembly. Remove retired target-capture helpers and refresh run-only metadata documentation. Verified 51 request UI tests, web typecheck, production build/output assertions and scoped lint.
Read admitted and hydrated requests through declared stage needs. Snapshot pure selected candidate payloads beside selectors, keep provider handles in the registry, and remove request-content cells and getter services. Pipeline constructors now compose static stages; Responses egress borrows its original client request at execution. Verify structural sharing, unsupported candidate exclusion, and dispatch-free projection.
Bring settlement-owned client JSON encoding and original quota persistence failures into the chat stack; preserve current run-only stage viewer fixtures.
Text and byte readers take responsibility for exhausting or failing the owned HTTP body before reading starts. A read rejection now propagates its original error without a second locked-stream cancellation failure; actual endpoint call facts remain available to settlement.

Verified 14 focused tests, owning provider typechecks and scoped lint. Native primitive read rejections separately exposed missing failure context and were handed to the core owner.
Release ownership to the native finite-body consumer before text or arrayBuffer reads begin. Preserve original read errors and known endpoint facts without adding cancellation failures from a locked body. Actual Codex401 and Claude200/403 operation chains cover the transfer.
Preserve internal exceptions, failure facts and stage markers instead of turning preparation faults into upstream refusals. Child diagnostics use status 500; the image tool boundary keeps its existing native terminal failure. Fixtures close both independent recordings before awaiting durable work.
Warmup and model probes retain diagnostic model attribution while providing no billable entities because no upstream was called. Preserve their local status and prove HTTP/WS handshakes record no usage or latency before one real continuation call.
Add the shared client JSON codec above every HTTP emitter while keeping canonical response objects in facts and streaming frames unchanged. Require response status from native calls, local answers and domain refusals instead of inventing success defaults. Verify original JSON serialization failures retain measured usage and count as failed requests.
Separate typed HTTP and WebSocket entry assemblies over shared model stages. Each edge now has its own declared entry/exit facts and serializer, without a generic framing default that could disagree with the actual stage array.
Remove the retired production result module and callable-model result types. The actual operation contracts remain provider-owned; fixture reply DTOs and their factory live in test-utils. Keep response cancellation as an HTTP helper for the active catalog and search callers.

Moved 54 fixture imports with identical non-import syntax trees and removed stale result-layer comments. Verified 2495 affected tests, 152 follow-up provider tests, ten package typechecks, frozen lock consistency and scoped lint. Test readers adopt finite body ownership before reading and preserve cleanup error chains.
Resolve the authoritative rendered stream for classic Completions alongside the chat client-frame fact. Inspect the stored stream tag so ordinary JSON extensions named stream cannot select a recording.

Verified 18 focused request UI tests, web typecheck and scoped lint, including classic collected output, stream.end proof and non-streaming JSON non-selection.
Await the scoped logger with the original error before projecting the native unavailable tool result. Preserve client semantics and verify the stage log retains the exact error and cause without introducing global error storage.
…eline-chat

# Conflicts:
#	README.md
#	packages/gateway/__tests__/dump/run-sink_test.ts
#	packages/gateway/__tests__/dump/test-fixtures.ts
#	packages/gateway/__tests__/repo/dump-store_test.ts
#	packages/gateway/__tests__/vitest.setup.ts
#	packages/gateway/src/dump/run-sink.ts
#	packages/gateway/src/dump/store-contract.ts
#	packages/gateway/src/repo/dump-store.ts
Remove obsolete future-charge assumptions and the claim that counting never dials upstream. Explain why transport proof cannot recover AI usage for native measurement failures.
Seed pre-migration SQLite records and their owned request, response and upstream-response files. Assert the real migration deletes all edge shapes, queues each file for collection, and preserves the complete run row and its owned file. Keep historical schema assertions separate from the retired runtime storage surface.
Retire the unreachable protocol-specific event viewer and its render/copy adapters. The actual stage viewer decodes recorded protocol frames directly; its citation regression verifies both visible source fields and copied frame content.

Keep generic event virtualization, search and collapse tests. Prune unused legacy edge columns from cold run detail reads while retaining the schema-required empty headers write. Verified 57 request UI tests, 16 dump repository tests, gateway/web typechecks and scoped lint.
Remove the retired edge-event DTO and synthetic timestamps from run decoding. Client collection consumes literal frames with their own recorded end marker, preserving EOF, truncation and citation behavior.
Own the emitted lifecycle and expose its deferred exception outcome. Join delayed emission with native or already measured JSON usage so formatter faults retain quantities once and propagate the original error through drain, scoped diagnostics and status 500 metadata. Preserve neutral undemanded images and the existing upstream-abort verdict.
Menci added 5 commits October 1, 2026 10:36
Give the finite Alpha Search reader ownership before it acquires the body lock, so cleanup preserves the original read failure. Exercise native body errors alongside transport failures and successful calls; retain actual call observations and independent child diagnostics.
Transfer finite error-body ownership before decoding, so resource cleanup cannot add a locked-stream failure. Verify the actual HTTP boundary preserves the original error and cause, records its failed verdict, and bills the called provider once.
Merge the dump package extraction and retain the run-only record contract. Delegate encoding and stream identity to the portable recorder while gateway retains admission, attribution, drain binding, HTTP measurement and child requests. Import dump contracts and helpers directly, remove the final gateway facades, and keep SQL-only schemas local.
Keep the final run-only gateway context directly typed while accepting the parent dump-boundary correction.
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