feat(chat-routes): assembled chat-turn vertical + multimodal upload→parts (#188 Phase 1)#194
Merged
Merged
Conversation
tangletools
approved these changes
Jul 15, 2026
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 46c20543
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-07-15T19:16:14Z
drewstone
added a commit
that referenced
this pull request
Jul 17, 2026
…n, lifecycle, context-gate, turn-lock) (#200) Add five optional, domain-free seams to createChatTurnRoutes so a complex product turn-orchestrator can delete its hand-rolled generator and compose the vertical. Every seam is optional — omitting all of them reproduces today's exact behavior, and the existing #194 tests are untouched. handleChatTurn stays the engine; the seams only wrap its input, its producer stream, and its settle. - turnLock: async acquire before any side effect / release once when the turn settles (drain finish, short-circuit, or throw). - contextGate: pre-producer domain-readiness short-circuit with a product Response (distinct from authorize). - beforeTurn: observe the assembled producer input and optionally rewrite the prompt / prior messages. - lifecycle: deterministic onTurnStart then exactly one of onTurnComplete(finalText, usage, durationMs) / onTurnError(error, durationMs). - heartbeat: keepalive injected while the producer is quiet; window resets on every real event. - onRawEvent: the raw producer events, before the engine frames them (telemetry).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The wave-2 centerpiece from #188 Phase 1: one factory that assembles the server chat vertical every product re-wired by hand, plus the multimodal upload→parts middle. Zero new loop logic — every hop is owned by a module that already ships it.
Composition (hop → owning module)
/webparseJsonObjectBody+ new/chat-routes./wire(import-free wire contract,parseChatTurnParts, inline-part byte gate)authorizeseam (composable with/app-authguards; newguardResolutionadapter in/platform— every product wrote this try/catch by hand)/streamresolveChatTurn+ agent-runtimederiveExecutionIdstreamSandboxPrompt+ newcreateSandboxChatProducer(raw sidecar events → the client vocabulary/web-reactalready parses + persisted parts + usage receipt + non-renderable-ask auto-decline); router/openai-compat lane = product-supplied producerhandleChatTurn— verbatim, no wrapper logic/streamturn-buffer tap, wired by default (tee + drain underwaitUntil: the turn finishes and buffers after a client drop; legal skips this today and hand-rolls a 5s debounced partial-saver) + replay endpoint/chat-store-shaped store: user row on send (retry-safe viaturnIdreuse), assistant row with parts/model/usage on completion/interactionscreateInteractionAnswerRoute, composed asroutes.interactionscreateUploadRoute: ≤700 KiB → inlinedata:part; larger → base64 write through a structural sandbox sink (box.fs) + path-ref part (the ~1 MiB gateway body cap makes the two-step mandatory); hard per-file cap 413sChatComposer.onSendParts(text, parts)(additive;onSend(string)unchanged),ComposerFile.partdescriptor, file-only sendschatTurnRequestInit+ wire types re-exported through/web-reactchat-stream (browser-safe:./wireis import-free)Reference assembly:
examples/chat-app.md— auth factory + chat tables + turn routes + upload + composer in 117 code lines. This example is the seed of thecreate-agent-app --chatscaffold (#188 Phase 2).Composition-eval follow-ups folded in (coordinates with #193)
/stream→/chat-storeboundary: new validating projectiontoChatMessagePartsin/chat-store(the module that owns the stored vocabulary; the stream normalizers stay storage-agnostic). The assembly's persistence hops use it — no casts. A compile-time exhaustiveness check makes a newChatMessagePartkind unignorable.normalizePersistedPartpreviously returnednullforfile/image/step-start/step-finish/subtask(andgetPartKeymis-keyed them into the text lane) — step-finish usage receipts and file parts silently fell out of persisted transcripts. Both fixed at the source in/stream;createSandboxChatProducerpersists per-step receipts under unique keys so two receipts never merge.guardResolutionin/platform(thrown-Response guards →{ok,response}resolutions), used by the example instead of a per-product try/catch.tests/vertical/chat-routes-vertical.test.ts, rebased onto test(vertical): composition eval suite over the assembled chat modules (#188) #193): the mini-app's hand-rolled chat route replaced bycreateChatTurnRoutes+createSandboxChatProducerend-to-end — same app-auth guard (viaguardResolution), same chat store, real client parser (streamChatTurn); asserts the client vocabulary, persisted rows with typed parts + flattened receipt columns, buffered replay after the live stream is gone, and the 401 guard contract. The factory wiring is ~20 lines vs the mini-app's ~150-line bespoke pump.normalizePersistedPartreturningnull) is updated to consume the normalizer's new full-vocabulary projection — that suite stays green (17/17).Net LOC products get to delete
src/routes/api.chat.ts: 895 lines → an assembly call (~60 lines of seams: authorize, produce, billing inonTurnComplete); plus legal finally gets buffered replay instead of the partial-saver.chat-turn.ts: 1,058 lines → same shape; tax's hand-wired tee/drain/replay and its re-declaredturn_eventsDDL are the factory's defaults.onSendParts.Tests
tests/chat-routes/turn-routes.test.ts(11): fake producer streaming, turn marker + engine lifecycle framing, user/assistant persistence with parts+usage,turnIdretry no-double-insert, echoed file parts → producer prompt, 400/401/413 (inline-cap gate), buffer replay after a simulated client drop (cancel mid-stream → drain → full replay +turn_status: complete, coalesced rows), replay auth, interaction answer round-trip through a fake sidecar.tests/chat-routes/sandbox-producer.test.ts(6): delta + snapshot text, tool announce/settle, step-finish usage accumulation + per-step receipt persistence, file/image part persistence, renderable ask forwarding vs auto-decline, verbatim error/cancel forwarding.tests/chat-routes/upload.test.ts(7): inline-vs-sandbox decision, base64 sink write + path-ref shape,SANDBOX_REQUIRED/FILE_TOO_LARGE413s, auth short-circuit, hostile-filename sanitization, per-request uploadDir.tests/chat-store/parts-projection.test.ts(3): every storable kind survives the typed projection; junk dropped; extra fields (turnId) round-trip.tests/stream-normalizer.test.ts(+4): full-vocabulary projection, typed part-key lanes.onSendPartsemission (ready-only), file-only send, precedence +onSendback-compat.Full gate on a clean frozen-lockfile install:
pnpm typecheck+pnpm test+pnpm build(NODE_OPTIONS=--max-old-space-size=8192) all green.Closes the
createChatTurnRoutes()and multimodal-middle items of #188 Phase 1.🤖 Generated with Claude Code