You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part 4 of 4 — upstream port chain: #880 → #881 → #883 → #884. This slice is BLOCKED until BOTH the #880 port (PR #115) AND the #883 port (PR #117) land on the fork. It is the last piece of the chain and must port LAST. Epic: #119. Upstream source: commit 53162d27 (PR #884 "Parallelize chat workflow startup", 6 files, +106/-95).
Why this matters
Every chat workflow run today pays a sequential startup tax. The current sequence in runAgentWorkflow is:
await claimActiveStream(chatId, workflowRunId) — a DB write/CAS round-trip (~10–50 ms)
Only after the claim resolves: convertMessages, persistInputMessages, resolveChatModelRuntime, resolveChatSandboxRuntime — all expensive, none dependent on the claim result
Upstream PR #884 removes the tax by hoisting the four startup promises before the claim so they begin executing concurrently with the claim DB round-trip. On the winning ("claimed") path, the already-in-flight promises are re-awaited via Promise.all (including activeStreamClaimPromise itself); fulfilled values arrive with near-zero extra latency. On a conflict, the hoisted promises are drained with Promise.allSettled before the stream closes, so no dangling async work or leaked rejections survive.
The POC (POC/upstream-884-parallel-startup/FINDINGS.md, 13/13 PASS) quantified the overlap: with the claim artificially delayed 100 ms, the hoisted resolveChatSandboxRuntime and persistInputMessages calls front-ran the claim's resolution by ~101–102 ms — i.e. the full claim latency reclaimed. User-visible effect: lower time-to-first-chunk (TTFC) on chat startup. On warm-sandbox paths the full claim round-trip is overlapped; on cold paths the overlap is proportionally smaller but still positive.
A second change is bundled in the same upstream commit: the { type: "start", messageId } chunk moves from resolveChatSandboxRuntime (which now runs before the claim) to claimActiveStream (which now receives writable + messageId and writes the start chunk itself on "claimed" and "error" outcomes, not on "conflict"). This is load-bearing for the conflict-safety guarantee — see the prerequisite note below and the regression risks section.
This slice lands after #117 (port #883, Workflow v5 transport). The v5 surface (workflow@5.0.0-beta.5, @workflow/ai@5.0.0-beta.4) is already the runtime when #884 is implemented. No new workflow API calls are introduced by #884 — it adds writable/messageId params to claimActiveStream (which is a "use step" function calling the existing claimChatActiveStreamId DB helper), and all other API calls (getWritable, start, getRun) are unchanged.
Single totally-ordered chunk-log invariant (from #117 reflection — MUST preserve)
From docs/plans/upstream-883-reflection.md §7 (Implications for #884):
"#883 actually raises the bar for #884: parallel work may fan out, but chunk emission into the durable stream must remain a single totally-ordered sequence."
The v5 transport counts chunks via a single global chunkIndex. The route's run.getReadable({ startIndex }) seeks into that one append-only log. If #884 ever introduced multiple independent writables or reordered chunks post-hoc, the client's chunkIndex would desync and resume would deliver duplicated or missing chunks.
Constraint for this port: all parallel computation (four hoisted promises) must funnel chunk emission into the single getWritable<UIMessageChunk>() in the correct sequence. The implementation achieves this naturally because the hoisted promises (convertMessages, persistInputMessages, resolveChatModelRuntime, resolveChatSandboxRuntime) produce results but do not write chunks independently — only claimActiveStream writes the start chunk, and the agent loop writes subsequent chunks after Promise.all resolves. This invariant must be verified explicitly in the behavior tests.
User/operator path protected
Chat user, normal run: first stream chunk is { type: "start", messageId } with the assistant id that matches the persisted assistant message, arriving with lower latency (TTFC reduced by the claim round-trip overlap).
Chat user, duplicate/racing request (conflict): the losing workflow exits silently — zero chunks, no message persistence into the abandoned stream — exactly as today.
Operator/observability: the fork-only workflow.skipped.active_stream_conflict event still fires on conflict, correlatable by chatId, workflowRunId, assistantId, requestId.
Managed-runtime user:runtimeMode, managedRuntime (profile id/version/run id), and sandboxName are still resolved from the runtime promise and propagated into the agent loop and usage record. workflow.runtime.resolved still carries managed-runtime and inference-profile fields.
Conflict-drain user (losing run):Promise.allSettled drains the hoisted promises before closing the stream. The client's stream closes silently. A slow hoisted promise (cold sandbox) can delay the conflict exit, bounded by the runtime cost.
Prerequisite: #880 must land FIRST — conflict-safety dependency
The hoist of resolveChatSandboxRuntime is unsafe before #880 strips sendStart from that function.
In the current fork, resolveChatSandboxRuntime contains a sendStart(assistantId) call (verified at chat-sandbox-runtime.ts:663) that writes { type: "start", messageId } directly to getWritable(). If #884 hoisted this function before the claim, a losing (conflict) run would race a start chunk onto the abandoned stream — breaking the "losing run stays silent" guarantee.
The #880 port (PR #115) strips sendStart and sendWorkspaceStatus from chat-sandbox-runtime.ts and moves the start chunk responsibility to claimActiveStream. Only after that strip is it safe to hoist. Confirm sendStart is absent from resolveChatSandboxRuntime before proceeding.
Behavior contract
Parallelization. When runAgentWorkflow runs with claimActiveStream delayed, resolveChatSandboxRuntime and persistInputMessages have already been invoked before the claim resolves (the hoisted promises front-run the claim). POC sub-goal 1: PASS (front-run by ~101.77 ms against a 100 ms delay).
Clean conflict drain. When the claim resolves to "conflict", the writable receives zero chunks, yet the hoisted promises were invoked and are drained via Promise.allSettled (a rejected hoisted promise is swallowed — 0 leaked unhandled rejections; verified 50× in the POC). The fork-only emitSessionEvent("workflow.skipped.active_stream_conflict") still fires, then closeStream(writable), then return. A slow winner (runtime ~2000 ms) bounds the conflict exit at ~2001 ms — it waits for the drain, it does not hang. POC sub-goals 2, 2b, 3: PASS.
Start chunk source shift. On a "claimed" run the first chunk is { type: "start", messageId }, written by claimActiveStream (NOT by resolveChatSandboxRuntime). On a "conflict" run there is no start chunk. On an "error" claim (non-fatal) the start chunk is still written and the workflow continues. POC sub-goals 5, 5b, 5c: PASS.
Synchronous assistant id. The assistant id is resolved synchronously as latestMessage.role === "assistant" ? latestMessage.id : (options.assistantId ?? generateIdAi()) — no "use step" wrapper. options.assistantId (passed from the HTTP handler) is the stable source; an assistant latestMessage.id still wins (resume/retry shape). POC sub-goals 6, 6b: PASS.
Single totally-ordered chunk log preserved. No parallel producer writes chunks independently; only claimActiveStream writes the start chunk and the sequential agent loop writes subsequent chunks. The v5 chunkIndex counter increments correctly across resume.
Product and design spec
No UI surface changes. The stream contract is unchanged from the client's perspective: the first chunk is still { type: "start", messageId }; only its producer moves from resolveChatSandboxRuntime to claimActiveStream. The intended product effect is a faster first token on chat startup (lower TTFC), with no change to the visible message sequence, workspace-status behavior (in this slice), or conflict semantics.
Integration spec (real paths + reconciliation)
Upstream source: commit 53162d27 (PR #884). Apply INTENT, not a blind diff-copy — the fork has diverged heavily (chat.ts: 2,241 lines fork vs ~1,326 upstream; chat.test.ts: 2,121 lines fork vs ~570 upstream; 12 mockImplementationOnce sites for resolveChatSandboxRuntime/claimActiveStream in the test file).
apps/web/app/api/chat/route.ts (380 lines fork)
Add generateId to the existing ai import block.
Pass assistantId: generateId() in the start() options object alongside the existing requestId, userId, requestUrl, authSession, maxSteps fields.
PRESERVE the fork-only requestId pass-through, the verified-build routing block (classifyVerifiedBuildTask, decideVerifiedBuildMode, startVerifiedBuildRun), and harness imports. Do NOT remove requestId.
NOTE: resolveChatSandboxRuntime drops assistantId, chatId, workflowRunId from its argument shape — those are stripped by the #880 port. Confirm #880 has landed before hoisting.
The fork emitSessionEvent call (currently at lines 1048–1060) STAYS in the conflict branch. The Promise.allSettled drain is NEW, added after the event emit.
Winning-path Promise.all — re-await the already-hoisted promises:
The hoisted promises are NOT re-created. activeStreamClaimPromise is included to surface any error. inputMessagesPersistPromise is included but its result is not destructured (side-effect only).
Preserve all managed-runtime variable assignments (lines 1144–1149): runtimeMode, runtimeSandboxName, managedRuntimeProfileId, managedRuntimeProfileVersion, managedRuntimeProfileRunId. Preserve all inference-profile assignments (lines 1140–1143): inferenceRoute, inferenceProfileId, inferenceProfileName, inferenceProvider. Preserve the workflow.runtime.resolved event emit.
claimActiveStream function signature: add optional writable?: WritableStream<UIMessageChunk> and messageId?: string parameters.
On "claimed" outcome: when both writable and messageId are provided, write { type: "start", messageId } via getWriter/write/finally-releaseLock.
On "error" outcome (non-fatal): same write — { type: "start", messageId } if both provided.
On "conflict" outcome: do NOT write any chunk.
PRESERVE all recordWorkflowUsage fork fields (requestId, runtimeMode, sandboxName, managed-runtime profile fields, inference fields, errorMessage). Do NOT delete them.
apps/web/app/workflows/chat-sandbox-runtime.ts — DEFERRED to #880
This file is NOT touched in this slice. The assistantId param removal, sendStart deletion, and sendWorkspaceStatus deletion are owned by the #880 port. Confirm #880 has completed before starting #884.
apps/web/app/api/chat/route.test.ts
Add generateId: () => "gen-id-1" to the ai module mock.
Add assistantId: "gen-id-1" to the expect.objectContaining assertion for startCalls[0][1].
resolveChatSandboxRuntime default mock: drop the assistantId start-chunk push → (_params) => Promise.resolve(createResolvedChatSandboxRuntime()). Remove params.assistantId reference from the default mock body.
claimActiveStream default mock: upgrade to async (_chatId?, _workflowRunId?, writable?, messageId?) — when both writable and messageId are provided, write the start chunk and return "claimed".
Mock-site updates (12 mockImplementationOnce sites for resolveChatSandboxRuntime + claimActiveStream):
All resolveChatSandboxRuntime mock sites: remove start-chunk pushes (writtenChunks.push({ type: "start", ... })). The start chunk now comes from claimActiveStream.
All claimActiveStream mock sites (current: () => Promise.resolve("conflict") / Promise.resolve("error")): update to the new 4-param signature; the conflict mocks write no chunk; the error mock writes the start chunk if writable+messageId provided.
Review and update test assertions that check writtenChunks[0] to account for the start chunk arriving from claimActiveStream rather than resolveChatSandboxRuntime.
KEEP:
runtimeMode/managedRuntime in the createResolvedChatSandboxRuntime() fixture (chat.ts still reads them post-hoist).
With #115 (#880 — sandbox provisioning): The resolveChatSandboxRuntime function post-#880 no longer calls sendStart or sendWorkspaceStatus internally, and its signature drops assistantId. The provisioning kick moves sandbox setup to session-create time, so by the time #884's hoist runs, a warm sandbox may already be ready — making the runtime promise resolve faster and the overall parallel overlap more valuable. The inputMessagesPersisted flag is reserved for a future resume flow (dead path in production now).
With #117 (#883 — Workflow v5): Post-#883, claimActiveStream is a "use step" function in the v5 runtime. The new writable/messageId params are plain JS values — no v5 API surface is introduced. The chunk written by claimActiveStream enters the single durable getWritable<UIMessageChunk>() log as chunk index 0, correctly indexed by the v5 transport's chunkIndex counter.
No push or PR to vercel-labs/open-agents or any Vercel Labs repository.
No change to the inputMessagesPersisted: true production path (dead path; implement the Promise.resolve() short-circuit but treat the true branch as untriggered in production).
Research and context sources
Upstream commit 53162d27 (reachable in this repo) — inspected directly via git show 53162d27. 6 files, +106/-95. Key hunks: route.tsgenerateId/assistantId; chat.ts promise hoist + Promise.allSettled conflict drain + synchronous assistant id + Promise.all with activeStreamClaimPromise; chat-post-finish.ts optional writable/messageId + start-chunk write on claimed/error; chat-sandbox-runtime.tssendStart/sendWorkspaceStatus/assistantId deletion (deferred to fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 here); test mock-site updates.
POC/upstream-884-parallel-startup/FINDINGS.md — 13/13 PASS. Proves: parallelization front-runs claim by ~102 ms; conflict drain emits 0 chunks and 0 leaked rejections (50× stress); slow winner bounds conflict exit at ~2001 ms; inputMessagesPersisted=true short-circuits; synchronous assistant id. Limitation: isolated spies, not real chat.ts/workflow runtime/DB.
docs/plans/upstream-883-reflection.md §7 — records the "single totally-ordered chunk log" invariant that fix: #879 honor guardrails embedded in the run's definitionSnapshot #884 must preserve (v5 chunkIndex is a global counter over all UI chunks; parallel producers must funnel into one ordered stream).
Fork files inspected:apps/web/app/workflows/chat.ts (2,241 lines; current startup sequence at lines 1036–1150 confirmed), apps/web/app/workflows/chat-post-finish.ts (600 lines; claimActiveStream at lines 294–327 confirmed, no writable/messageId params yet), apps/web/app/api/chat/route.ts (380 lines; start() call at lines 230–241 confirmed, no assistantId yet), apps/web/app/workflows/chat-sandbox-runtime.ts (confirmed sendStart and assistantId still present — fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 has NOT landed yet), apps/web/app/workflows/chat.test.ts (2,121 lines; 12 mock sites confirmed).
Confirm the four new behavior tests GREEN; full chat.test.ts + route.test.ts GREEN.
git diff --check and bun --bun run ci pass.
Capture observability + managed-runtime evidence (below) and integration verification.
Tests to add first (RED before implementing)
Write these RED before writing any implementation code. The TDD worker must commit the failing test output before the implementation commit.
Parallelization — with claimActiveStream delayed 100 ms via mockImplementationOnce, record call timestamps for resolveChatSandboxRuntime and persistInputMessages; assert both were called BEFORE the claim resolved. Fails today because promises are created after the await claimActiveStream(...).
Clean conflict drain — force claimActiveStream → "conflict" (no start-chunk write); assert writtenChunks is empty AND the hoisted promises were invoked (via call-count on spies) AND a rejecting hoisted promise leaks 0 unhandled rejections. Assert the fork workflow.skipped.active_stream_conflict event still fired.
Start chunk from claimActiveStream — on a claimed run assert writtenChunks[0] is { type: "start", messageId: <assistantId> } and the resolveChatSandboxRuntime spy pushed no start chunk. Assert the chunk-log invariant: writtenChunks remains a single ordered sequence starting from index 0.
assistantId from options — with makeOptions({ assistantId: "caller-provided-id" }) assert writtenChunks[0].messageId === "caller-provided-id" (synchronous resolution, no use-step delay). Assert assistantId in the conflict event payload also equals "caller-provided-id".
Test file: apps/web/app/workflows/chat.test.ts (add in a focused describe("parallel startup — #884") block). Route test: apps/web/app/api/chat/route.test.ts.
Observability and user feedback
workflow.skipped.active_stream_conflict still emitted on every conflict, correlatable by chatId, workflowRunId, assistantId (fork-only requestId preserved).
The start-chunk messageId matches the persisted assistant message id in the DB (both derived from generateId() in the route handler, passed via options.assistantId).
workflow.runtime.resolved payload still carries managed-runtime fields (runtimeMode, managedRuntime profile id/version/run id, sandboxName) and inference-profile fields (inferenceRoute, inferenceProfileId, inferenceProfileName, inferenceProvider).
New latency signal (optional, not required for DoD): log or event the elapsed time between workflow start and claim resolution, and between claim resolution and first agent step — to quantify TTFC improvement in production.
This slice touches the managed-runtime path → satisfy the Managed Runtime Proof Standard: user-visible status, runtime/sandbox attribution, logs/events/metadata, and final verification notes. Run a managed-runtime session post-port and confirm workflow.runtime.resolved shows the managed-runtime + inference fields.
Regression harness plan
Baseline GREEN before any change: bun test apps/web/app/workflows/chat.test.ts + apps/web/app/api/chat/route.test.ts.
Add the four behavior tests in a focused describe block; confirm RED; record failing output for the TDD audit trail red commit.
Implement; confirm GREEN; record passing output for the green commit.
Update the 12 existing mock sites to the new signatures and re-run the full suite. The managed-runtime, inference-profile, verified-build, Composio, background-agent, and observability tests must stay GREEN — the hoist must preserve the variable assignments they assert on.
Conflict smoke: two rapid POSTs to the same chat → one stream empty, fork conflict event emitted with correct fields, winner's start chunk normal.
Normal smoke: first client chunk { type: "start", messageId } matching the DB assistant message record.
Single-log smoke: after a full chat run, confirm writtenChunks contains exactly one start chunk at index 0 and no duplicates (validates the chunk-log invariant for v5 resume).
Final gate: bun --bun run ci.
Note: the POC harness (POC/upstream-884-parallel-startup/) is a reference, NOT a substitute for in-app tests — an integration test against real chat.ts + workflow runtime + DB is required.
TDD audit trail
Record in the PR: (1) failing-first output of the four behavior tests (RED commit hash), (2) smallest green diff that flips each (GREEN commit hash), (3) full chat.test.ts + route.test.ts pass after mock-site updates (REGRESSION commit hash), (4) git diff --check clean, (5) bun --bun run ci pass, (6) conflict + normal + single-log smoke evidence, (7) managed-runtime workflow.runtime.resolved payload proving runtime/inference fields survive the hoist.
generateId step-wrapper removal changes assistant-id determinism from a journaled step to synchronous resolution. Safe because options.assistantId (from the route handler) is now the stable source; generateIdAi() is the fallback only when no options.assistantId is provided (test path). Confirm replay/resume still yields the same id — an assistant latestMessage.id wins over options.assistantId for the resume/retry shape.
Conflict drain waits for a slow hoisted promise. On a cold-sandbox losing run, Promise.allSettled can delay the conflict exit by the sandbox connect cost. In production, after fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880, the heavy provisioning work moves to the provisioning workflow (session-create time), reducing the worst-case drain. Acceptable and bounded; document in the commit message.
inputMessagesPersisted is currently never true (dead path in production — reserved for a future resume flow). Implement the Promise.resolve() short-circuit but treat the true branch as untriggered in production tests.
Test-file divergence (chat.test.ts: 2,121 lines fork vs ~570 upstream; 12 mock sites) means no clean diff-apply. Each mock site must be reviewed and updated by hand. The managed-runtime, inference-profile, background-agent, observability, and Composio test blocks must stay GREEN after the mock-site updates.
Single totally-ordered chunk-log invariant (v5-specific risk): parallel hoisted promises must not emit chunks independently. The implementation avoids this naturally (hoisted promises return results, not streams), but verify with an explicit test assertion.
POC limitation: isolated spies do not prove the workflow "use step" journal/replay behavior, ambient getWritable() binding, or the live request cycle. The integration test against real chat.ts + workflow runtime + DB is mandatory before claiming the runtime path is proven.
Deploy and migration impact
No schema change, no migration. Latest migration is 0047_cheerful_thunderbolt_ross.sql; this slice adds no DB columns or tables.
New params are OPTIONAL (Options.assistantId?, Options.inputMessagesPersisted?, claimActiveStream(..., writable?, messageId?)) → old callers are unaffected → rollback-safe.
No client-visible API/transport contract change: the first chunk is still { type: "start", messageId }; only its producer moves.
Expected effect: improved TTFC (startup overlaps the claim DB round-trip, ~10–50 ms reclaimed on warm paths). Conflict-path drain may wait for a slow hoisted promise before closing — acceptable (client has already abandoned the stream).
chat.test.ts: four new behavior tests GREEN; all 12 mock sites updated; runtimeMode/managedRuntime fixture kept; data-workspace-status assertion unchanged; full suite GREEN.
Conflict smoke: two rapid POSTs → one stream empty, fork conflict event emitted with correct fields.
Normal smoke: first client chunk { type: "start", messageId } matching the DB record.
Single-log smoke: one start chunk at index 0, no duplicates (chunk-log invariant for v5).
Managed Runtime Proof Standard satisfied: workflow.runtime.resolved shows runtime + inference fields after the hoist; integration verification recorded.
PR opened on dennisonbertram/fork-open-agents with TDD audit trail (red/green/regression commit hashes + outputs) and observability evidence. Nothing pushed to vercel-labs/open-agents.
Assumptions and alternatives
Assumptions:
options.assistantId from the route handler is always a freshly generated id (via generateId() from ai) — not a user-supplied or replay-derived id in the normal POST path. Replay/resume uses latestMessage.id which wins over options.assistantId at the synchronous resolution site.
The inputMessagesPersisted: true short-circuit path is dead in production for this slice; it exists for future resume-flow use.
The conflict-drain wait (bounded by the slowest hoisted promise) is acceptable latency on the conflict exit path because the client has already abandoned the stream.
Not hoisting runtimePromise / only hoisting the cheap promises (e.g. only convertMessages + persistInputMessages): loses the primary latency win (sandbox connect is the expensive step). Rejected — the whole point is to overlap the expensive work.
Using Promise.race instead of Promise.allSettled for the conflict drain: would not guarantee all hoisted promises finish before the stream closes, risking dangling async work. Rejected in favor of Promise.allSettled.
Keeping the "use step" generateId wrapper: loses the synchronous assistant-id resolution and would require the start-chunk write to be deferred, complicating the ordering. Rejected — the synchronous form is simpler and safe given options.assistantId is the stable source.
Grooming clarifications (2026-05-31)
Provenance: This body was enriched by the issue groomer on 2026-05-31 using the following sources:
Upstream commit 53162d27 inspected directly via git show 53162d27 (commit is reachable in this repo's git history). Confirmed: 6 files, +106/-95; diff read in full. The diff matches the issue body's integration spec exactly. chat-sandbox-runtime.ts changes (lines 88–130 deleted in upstream) are confirmed to still be present in the fork — sendStart, sendWorkspaceStatus, and assistantId are all still present in the fork's chat-sandbox-runtime.ts at lines 187–205, 658, 663, 681. fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 has NOT landed on the fork yet.
Fork surfaces verified read-only:
chat.ts (2,241 lines): current startup sequence confirmed at lines 1036–1150. claimActiveStream called with 2 args (no writable/messageId). Promises created AFTER the claim. generateId step wrapper at line 274. assistantId resolved via await generateId() at line 1073. resolveChatSandboxRuntime called with { userId, sessionId, chatId, assistantId, workflowRunId } (includes assistantId — confirming pre-fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 signature).
chat-post-finish.ts (600 lines): claimActiveStream at lines 294–327, 2-param signature, no writable/messageId, no start-chunk write. Confirmed.
route.ts (380 lines): start() call at lines 230–241, no assistantId in options. Confirmed.
chat.test.ts (2,121 lines): 12 mockImplementationOnce sites for resolveChatSandboxRuntime/claimActiveStream confirmed. Default resolveChatSandboxRuntime spy at line 64 pushes start chunk from params.assistantId. Default claimActiveStream spy at line 68 is a bare () => Promise.resolve("claimed").
docs/plans/upstream-883-reflection.md §7 read in full — the "single totally-ordered chunk log" invariant extracted verbatim and recorded as a constraint in the behavior contract (item 5) and the definition-of-done.
Latest migration:0047_cheerful_thunderbolt_ross.sql. No migration required for this slice.
Mock site count: 12 mockImplementationOnce sites for resolveChatSandboxRuntime/claimActiveStream in chat.test.ts (confirmed via grep). The issue body's original "10+" estimate is now precise: 12.
Decisions made during grooming (documented, not product decisions):
Why this matters
Every chat workflow run today pays a sequential startup tax. The current sequence in
runAgentWorkflowis:await claimActiveStream(chatId, workflowRunId)— a DB write/CAS round-trip (~10–50 ms)convertMessages,persistInputMessages,resolveChatModelRuntime,resolveChatSandboxRuntime— all expensive, none dependent on the claim resultUpstream PR #884 removes the tax by hoisting the four startup promises before the claim so they begin executing concurrently with the claim DB round-trip. On the winning ("claimed") path, the already-in-flight promises are re-awaited via
Promise.all(includingactiveStreamClaimPromiseitself); fulfilled values arrive with near-zero extra latency. On a conflict, the hoisted promises are drained withPromise.allSettledbefore the stream closes, so no dangling async work or leaked rejections survive.The POC (
POC/upstream-884-parallel-startup/FINDINGS.md, 13/13 PASS) quantified the overlap: with the claim artificially delayed 100 ms, the hoistedresolveChatSandboxRuntimeandpersistInputMessagescalls front-ran the claim's resolution by ~101–102 ms — i.e. the full claim latency reclaimed. User-visible effect: lower time-to-first-chunk (TTFC) on chat startup. On warm-sandbox paths the full claim round-trip is overlapped; on cold paths the overlap is proportionally smaller but still positive.A second change is bundled in the same upstream commit: the
{ type: "start", messageId }chunk moves fromresolveChatSandboxRuntime(which now runs before the claim) toclaimActiveStream(which now receiveswritable+messageIdand writes the start chunk itself on"claimed"and"error"outcomes, not on"conflict"). This is load-bearing for the conflict-safety guarantee — see the prerequisite note below and the regression risks section.SDK v5 note (post-#117 target)
This slice lands after #117 (port #883, Workflow v5 transport). The v5 surface (
workflow@5.0.0-beta.5,@workflow/ai@5.0.0-beta.4) is already the runtime when #884 is implemented. No newworkflowAPI calls are introduced by #884 — it addswritable/messageIdparams toclaimActiveStream(which is a"use step"function calling the existingclaimChatActiveStreamIdDB helper), and all other API calls (getWritable,start,getRun) are unchanged.Single totally-ordered chunk-log invariant (from #117 reflection — MUST preserve)
From
docs/plans/upstream-883-reflection.md§7 (Implications for #884):The v5 transport counts chunks via a single global
chunkIndex. The route'srun.getReadable({ startIndex })seeks into that one append-only log. If #884 ever introduced multiple independent writables or reordered chunks post-hoc, the client'schunkIndexwould desync and resume would deliver duplicated or missing chunks.Constraint for this port: all parallel computation (four hoisted promises) must funnel chunk emission into the single
getWritable<UIMessageChunk>()in the correct sequence. The implementation achieves this naturally because the hoisted promises (convertMessages,persistInputMessages,resolveChatModelRuntime,resolveChatSandboxRuntime) produce results but do not write chunks independently — onlyclaimActiveStreamwrites thestartchunk, and the agent loop writes subsequent chunks afterPromise.allresolves. This invariant must be verified explicitly in the behavior tests.User/operator path protected
{ type: "start", messageId }with the assistant id that matches the persisted assistant message, arriving with lower latency (TTFC reduced by the claim round-trip overlap).workflow.skipped.active_stream_conflictevent still fires on conflict, correlatable bychatId,workflowRunId,assistantId,requestId.runtimeMode,managedRuntime(profile id/version/run id), andsandboxNameare still resolved from the runtime promise and propagated into the agent loop and usage record.workflow.runtime.resolvedstill carries managed-runtime and inference-profile fields.Promise.allSettleddrains the hoisted promises before closing the stream. The client's stream closes silently. A slow hoisted promise (cold sandbox) can delay the conflict exit, bounded by the runtime cost.Prerequisite: #880 must land FIRST — conflict-safety dependency
The hoist of
resolveChatSandboxRuntimeis unsafe before #880 stripssendStartfrom that function.In the current fork,
resolveChatSandboxRuntimecontains asendStart(assistantId)call (verified atchat-sandbox-runtime.ts:663) that writes{ type: "start", messageId }directly togetWritable(). If #884 hoisted this function before the claim, a losing (conflict) run would race astartchunk onto the abandoned stream — breaking the "losing run stays silent" guarantee.The #880 port (PR #115) strips
sendStartandsendWorkspaceStatusfromchat-sandbox-runtime.tsand moves thestartchunk responsibility toclaimActiveStream. Only after that strip is it safe to hoist. ConfirmsendStartis absent fromresolveChatSandboxRuntimebefore proceeding.Behavior contract
Parallelization. When
runAgentWorkflowruns withclaimActiveStreamdelayed,resolveChatSandboxRuntimeandpersistInputMessageshave already been invoked before the claim resolves (the hoisted promises front-run the claim). POC sub-goal 1: PASS (front-run by ~101.77 ms against a 100 ms delay).Clean conflict drain. When the claim resolves to
"conflict", the writable receives zero chunks, yet the hoisted promises were invoked and are drained viaPromise.allSettled(a rejected hoisted promise is swallowed — 0 leaked unhandled rejections; verified 50× in the POC). The fork-onlyemitSessionEvent("workflow.skipped.active_stream_conflict")still fires, thencloseStream(writable), thenreturn. A slow winner (runtime ~2000 ms) bounds the conflict exit at ~2001 ms — it waits for the drain, it does not hang. POC sub-goals 2, 2b, 3: PASS.Start chunk source shift. On a
"claimed"run the first chunk is{ type: "start", messageId }, written byclaimActiveStream(NOT byresolveChatSandboxRuntime). On a"conflict"run there is no start chunk. On an"error"claim (non-fatal) the start chunk is still written and the workflow continues. POC sub-goals 5, 5b, 5c: PASS.Synchronous assistant id. The assistant id is resolved synchronously as
latestMessage.role === "assistant" ? latestMessage.id : (options.assistantId ?? generateIdAi())— no"use step"wrapper.options.assistantId(passed from the HTTP handler) is the stable source; an assistantlatestMessage.idstill wins (resume/retry shape). POC sub-goals 6, 6b: PASS.Single totally-ordered chunk log preserved. No parallel producer writes chunks independently; only
claimActiveStreamwrites thestartchunk and the sequential agent loop writes subsequent chunks. The v5chunkIndexcounter increments correctly across resume.Product and design spec
No UI surface changes. The stream contract is unchanged from the client's perspective: the first chunk is still
{ type: "start", messageId }; only its producer moves fromresolveChatSandboxRuntimetoclaimActiveStream. The intended product effect is a faster first token on chat startup (lower TTFC), with no change to the visible message sequence, workspace-status behavior (in this slice), or conflict semantics.Integration spec (real paths + reconciliation)
Upstream source: commit
53162d27(PR #884). Apply INTENT, not a blind diff-copy — the fork has diverged heavily (chat.ts: 2,241 lines fork vs ~1,326 upstream; chat.test.ts: 2,121 lines fork vs ~570 upstream; 12mockImplementationOncesites forresolveChatSandboxRuntime/claimActiveStreamin the test file).apps/web/app/api/chat/route.ts(380 lines fork)generateIdto the existingaiimport block.assistantId: generateId()in thestart()options object alongside the existingrequestId,userId,requestUrl,authSession,maxStepsfields.requestIdpass-through, the verified-build routing block (classifyVerifiedBuildTask,decideVerifiedBuildMode,startVerifiedBuildRun), and harness imports. Do NOT removerequestId.apps/web/app/workflows/chat.ts(2,241 lines fork) — primary effortOptionstype additions:assistantId?: stringandinputMessagesPersisted?: boolean.requestId?(fork-only, used inemitSessionEventfor correlation).Remove step wrapper:
const generateId = async () => { "use step"; return generateIdAi(); }wrapper (currently at line 274). ResolveassistantIdsynchronously (contract item 4).Hoist four promises BEFORE the
claimActiveStreamcall:NOTE:
resolveChatSandboxRuntimedropsassistantId,chatId,workflowRunIdfrom its argument shape — those are stripped by the #880 port. Confirm #880 has landed before hoisting.Change
claimActiveStreamcall shape:Conflict branch — preserve fork event + add allSettled drain:
The fork
emitSessionEventcall (currently at lines 1048–1060) STAYS in the conflict branch. ThePromise.allSettleddrain is NEW, added after the event emit.Winning-path
Promise.all— re-await the already-hoisted promises:The hoisted promises are NOT re-created.
activeStreamClaimPromiseis included to surface any error.inputMessagesPersistPromiseis included but its result is not destructured (side-effect only).Preserve all managed-runtime variable assignments (lines 1144–1149):
runtimeMode,runtimeSandboxName,managedRuntimeProfileId,managedRuntimeProfileVersion,managedRuntimeProfileRunId. Preserve all inference-profile assignments (lines 1140–1143):inferenceRoute,inferenceProfileId,inferenceProfileName,inferenceProvider. Preserve theworkflow.runtime.resolvedevent emit.apps/web/app/workflows/chat-post-finish.ts(600 lines fork)claimActiveStreamfunction signature: add optionalwritable?: WritableStream<UIMessageChunk>andmessageId?: stringparameters."claimed"outcome: when bothwritableandmessageIdare provided, write{ type: "start", messageId }via getWriter/write/finally-releaseLock."error"outcome (non-fatal): same write —{ type: "start", messageId }if both provided."conflict"outcome: do NOT write any chunk.recordWorkflowUsagefork fields (requestId,runtimeMode,sandboxName, managed-runtime profile fields, inference fields,errorMessage). Do NOT delete them.apps/web/app/workflows/chat-sandbox-runtime.ts— DEFERRED to #880This file is NOT touched in this slice. The
assistantIdparam removal,sendStartdeletion, andsendWorkspaceStatusdeletion are owned by the #880 port. Confirm #880 has completed before starting #884.apps/web/app/api/chat/route.test.tsgenerateId: () => "gen-id-1"to theaimodule mock.assistantId: "gen-id-1"to theexpect.objectContainingassertion forstartCalls[0][1].apps/web/app/workflows/chat.test.ts(2,121 lines — highest effort)Default spy updates:
resolveChatSandboxRuntimedefault mock: drop theassistantIdstart-chunk push →(_params) => Promise.resolve(createResolvedChatSandboxRuntime()). Removeparams.assistantIdreference from the default mock body.claimActiveStreamdefault mock: upgrade toasync (_chatId?, _workflowRunId?, writable?, messageId?)— when bothwritableandmessageIdare provided, write the start chunk and return"claimed".Mock-site updates (12
mockImplementationOncesites forresolveChatSandboxRuntime+claimActiveStream):resolveChatSandboxRuntimemock sites: remove start-chunk pushes (writtenChunks.push({ type: "start", ... })). The start chunk now comes fromclaimActiveStream.claimActiveStreammock sites (current:() => Promise.resolve("conflict")/Promise.resolve("error")): update to the new 4-param signature; the conflict mocks write no chunk; the error mock writes the start chunk if writable+messageId provided.writtenChunks[0]to account for the start chunk arriving fromclaimActiveStreamrather thanresolveChatSandboxRuntime.KEEP:
runtimeMode/managedRuntimein thecreateResolvedChatSandboxRuntime()fixture (chat.ts still reads them post-hoist).data-workspace-statusassertion in its CURRENT orientation (do NOT invert or delete until fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 lands separately).Composition with dependencies
With #115 (#880 — sandbox provisioning): The
resolveChatSandboxRuntimefunction post-#880 no longer callssendStartorsendWorkspaceStatusinternally, and its signature dropsassistantId. The provisioning kick moves sandbox setup to session-create time, so by the time #884's hoist runs, a warm sandbox may already be ready — making the runtime promise resolve faster and the overall parallel overlap more valuable. TheinputMessagesPersistedflag is reserved for a future resume flow (dead path in production now).With #117 (#883 — Workflow v5): Post-#883,
claimActiveStreamis a"use step"function in the v5 runtime. The newwritable/messageIdparams are plain JS values — no v5 API surface is introduced. The chunk written byclaimActiveStreamenters the single durablegetWritable<UIMessageChunk>()log as chunk index 0, correctly indexed by the v5 transport'schunkIndexcounter.In scope
route.ts: importgenerateId, passassistantId: generateId().chat.ts:Optionsadditions; remove thegenerateIdstep wrapper; synchronousassistantIdresolution; hoist four promises before the claim; newclaimActiveStreamcall shape; conflict-branchPromise.allSettleddrain with preserved forkemitSessionEvent; winning-pathPromise.allre-await withactiveStreamClaimPromise; preserved managed-runtime + inference-profile assignments.chat-post-finish.ts: optionalwritable/messageIdonclaimActiveStream; start-chunk write onclaimed+error; preservedrecordWorkflowUsagefields.route.test.ts:generateIdmock +assistantIdassertion.chat.test.ts: default spy updates + 12 mock-site updates + four new behavior tests; fixture anddata-workspace-statusassertion preserved.Out of scope
resolveChatSandboxRuntimeis unsafe whilesendStartstill exists inside that function (pre-fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880). Default: land fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 → fix: #877 loop settings edits marked dirty and saved via one path #881 → feat: unblock authenticated production canary and wire journey harnesses (#866) #883 → fix: #879 honor guardrails embedded in the run's definitionSnapshot #884 in dependency order.chat-sandbox-runtime.tsedits (assistantId/sendStart/sendWorkspaceStatusremoval) — owned by the fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 port.data-workspace-statustest direction — happens with the fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 port.0047; this slice adds no DB columns or tables).vercel-labs/open-agentsor any Vercel Labs repository.inputMessagesPersisted: trueproduction path (dead path; implement thePromise.resolve()short-circuit but treat thetruebranch as untriggered in production).Research and context sources
53162d27(reachable in this repo) — inspected directly viagit show 53162d27. 6 files, +106/-95. Key hunks:route.tsgenerateId/assistantId;chat.tspromise hoist +Promise.allSettledconflict drain + synchronous assistant id +Promise.allwithactiveStreamClaimPromise;chat-post-finish.tsoptionalwritable/messageId+ start-chunk write on claimed/error;chat-sandbox-runtime.tssendStart/sendWorkspaceStatus/assistantIddeletion (deferred to fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 here); test mock-site updates.POC/upstream-884-parallel-startup/FINDINGS.md— 13/13 PASS. Proves: parallelization front-runs claim by ~102 ms; conflict drain emits 0 chunks and 0 leaked rejections (50× stress); slow winner bounds conflict exit at ~2001 ms;inputMessagesPersisted=trueshort-circuits; synchronous assistant id. Limitation: isolated spies, not realchat.ts/workflow runtime/DB.docs/plans/upstream-883-reflection.md§7 — records the "single totally-ordered chunk log" invariant that fix: #879 honor guardrails embedded in the run's definitionSnapshot #884 must preserve (v5chunkIndexis a global counter over all UI chunks; parallel producers must funnel into one ordered stream).apps/web/app/workflows/chat.ts(2,241 lines; current startup sequence at lines 1036–1150 confirmed),apps/web/app/workflows/chat-post-finish.ts(600 lines;claimActiveStreamat lines 294–327 confirmed, nowritable/messageIdparams yet),apps/web/app/api/chat/route.ts(380 lines;start()call at lines 230–241 confirmed, noassistantIdyet),apps/web/app/workflows/chat-sandbox-runtime.ts(confirmedsendStartandassistantIdstill present — fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 has NOT landed yet),apps/web/app/workflows/chat.test.ts(2,121 lines; 12 mock sites confirmed).docs/plans/active-plan.md— dependency order and fix: #879 honor guardrails embedded in the run's definitionSnapshot #884 digest.Agent todo checklist
sendStartis absent fromresolveChatSandboxRuntimeinchat-sandbox-runtime.ts. If not, STOP — this slice is blocked.bun test apps/web/app/workflows/chat.test.tsandapps/web/app/api/chat/route.test.ts— all pass.chat-post-finish.ts: add optionalwritable/messageId; start-chunk write on claimed + error; preserverecordWorkflowUsagefields.chat.ts:Optionsadditions; removegenerateIdstep wrapper; synchronousassistantId; hoist four promises before claim; newclaimActiveStreamcall shape; conflictPromise.allSettled+ preserved forkemitSessionEvent; winning-pathPromise.all; preserve managed-runtime + inference-profile assignments.route.ts: importgenerateId, passassistantId: generateId(); preserverequestId+ verified-build block.route.test.ts:generateIdmock +assistantId: "gen-id-1"assertion.chat.test.ts: update default spies + 12 mock-implementation sites; keepruntimeMode/managedRuntimefixture; keepdata-workspace-statusassertion as-is.chat.test.ts+route.test.tsGREEN.git diff --checkandbun --bun run cipass.Tests to add first (RED before implementing)
Write these RED before writing any implementation code. The TDD worker must commit the failing test output before the implementation commit.
Parallelization — with
claimActiveStreamdelayed 100 ms viamockImplementationOnce, record call timestamps forresolveChatSandboxRuntimeandpersistInputMessages; assert both were called BEFORE the claim resolved. Fails today because promises are created after theawait claimActiveStream(...).Clean conflict drain — force
claimActiveStream → "conflict"(no start-chunk write); assertwrittenChunksis empty AND the hoisted promises were invoked (via call-count on spies) AND a rejecting hoisted promise leaks 0 unhandled rejections. Assert the forkworkflow.skipped.active_stream_conflictevent still fired.Start chunk from
claimActiveStream— on a claimed run assertwrittenChunks[0]is{ type: "start", messageId: <assistantId> }and theresolveChatSandboxRuntimespy pushed no start chunk. Assert the chunk-log invariant:writtenChunksremains a single ordered sequence starting from index 0.assistantIdfromoptions— withmakeOptions({ assistantId: "caller-provided-id" })assertwrittenChunks[0].messageId === "caller-provided-id"(synchronous resolution, no use-step delay). AssertassistantIdin the conflict event payload also equals"caller-provided-id".Test file:
apps/web/app/workflows/chat.test.ts(add in a focuseddescribe("parallel startup — #884")block). Route test:apps/web/app/api/chat/route.test.ts.Observability and user feedback
workflow.skipped.active_stream_conflictstill emitted on every conflict, correlatable bychatId,workflowRunId,assistantId(fork-onlyrequestIdpreserved).messageIdmatches the persisted assistant message id in the DB (both derived fromgenerateId()in the route handler, passed viaoptions.assistantId).workflow.runtime.resolvedpayload still carries managed-runtime fields (runtimeMode,managedRuntimeprofile id/version/run id,sandboxName) and inference-profile fields (inferenceRoute,inferenceProfileId,inferenceProfileName,inferenceProvider).workflow.runtime.resolvedshows the managed-runtime + inference fields.Regression harness plan
bun test apps/web/app/workflows/chat.test.ts+apps/web/app/api/chat/route.test.ts.data-workspace-statustest in its current orientation (do not invert until fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880's PR lands separately).{ type: "start", messageId }matching the DB assistant message record.writtenChunkscontains exactly onestartchunk at index 0 and no duplicates (validates the chunk-log invariant for v5 resume).bun --bun run ci.POC/upstream-884-parallel-startup/) is a reference, NOT a substitute for in-app tests — an integration test against realchat.ts+ workflow runtime + DB is required.TDD audit trail
Record in the PR: (1) failing-first output of the four behavior tests (RED commit hash), (2) smallest green diff that flips each (GREEN commit hash), (3) full
chat.test.ts+route.test.tspass after mock-site updates (REGRESSION commit hash), (4)git diff --checkclean, (5)bun --bun run cipass, (6) conflict + normal + single-log smoke evidence, (7) managed-runtimeworkflow.runtime.resolvedpayload proving runtime/inference fields survive the hoist.Regression risks and concerns
Hoisting before fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 leaks a start chunk on conflict (load-bearing risk). Mitigation: hard-block on fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880; verify
sendStartis absent fromresolveChatSandboxRuntimebefore hoisting. The POC reproduced the leak in the PRE-fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 case; the POST-fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 case shows it vanishes.generateIdstep-wrapper removal changes assistant-id determinism from a journaled step to synchronous resolution. Safe becauseoptions.assistantId(from the route handler) is now the stable source;generateIdAi()is the fallback only when nooptions.assistantIdis provided (test path). Confirm replay/resume still yields the same id — an assistantlatestMessage.idwins overoptions.assistantIdfor the resume/retry shape.Conflict drain waits for a slow hoisted promise. On a cold-sandbox losing run,
Promise.allSettledcan delay the conflict exit by the sandbox connect cost. In production, after fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880, the heavy provisioning work moves to the provisioning workflow (session-create time), reducing the worst-case drain. Acceptable and bounded; document in the commit message.inputMessagesPersistedis currently nevertrue(dead path in production — reserved for a future resume flow). Implement thePromise.resolve()short-circuit but treat thetruebranch as untriggered in production tests.Test-file divergence (chat.test.ts: 2,121 lines fork vs ~570 upstream; 12 mock sites) means no clean diff-apply. Each mock site must be reviewed and updated by hand. The managed-runtime, inference-profile, background-agent, observability, and Composio test blocks must stay GREEN after the mock-site updates.
Single totally-ordered chunk-log invariant (v5-specific risk): parallel hoisted promises must not emit chunks independently. The implementation avoids this naturally (hoisted promises return results, not streams), but verify with an explicit test assertion.
POC limitation: isolated spies do not prove the workflow
"use step"journal/replay behavior, ambientgetWritable()binding, or the live request cycle. The integration test against realchat.ts+ workflow runtime + DB is mandatory before claiming the runtime path is proven.Deploy and migration impact
0047_cheerful_thunderbolt_ross.sql; this slice adds no DB columns or tables.Options.assistantId?,Options.inputMessagesPersisted?,claimActiveStream(..., writable?, messageId?)) → old callers are unaffected → rollback-safe.{ type: "start", messageId }; only its producer moves.Definition of done
sendStartandsendWorkspaceStatusabsent fromchat-sandbox-runtime.ts;resolveChatSandboxRuntimesignature no longer takesassistantId. feat: unblock authenticated production canary and wire journey harnesses (#866) #883 port confirmed landed (or proceeding explicitly justified).route.ts: importsgenerateId, passesassistantId: generateId();requestId+ verified-build block + harness imports intact.chat.tsOptions:assistantId?+inputMessagesPersisted?added;requestId?retained.chat.tsstartup: four promises hoisted before the claim; synchronousassistantIdresolution;generateIdstep wrapper removed;claimActiveStreamreceiveswritable+assistantId; conflict branch hasPromise.allSettleddrain + preserved forkemitSessionEvent; winning-pathPromise.allre-awaitsactiveStreamClaimPromise; managed-runtime + inference-profile assignments preserved.chat-post-finish.tsclaimActiveStream: optionalwritable/messageId; start-chunk write on claimed + error (not conflict);recordWorkflowUsagefork fields untouched.chat-sandbox-runtime.ts: untouched in this slice (deferred to fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880).chat.test.ts: four new behavior tests GREEN; all 12 mock sites updated;runtimeMode/managedRuntimefixture kept;data-workspace-statusassertion unchanged; full suite GREEN.route.test.ts:generateIdmock +assistantId: "gen-id-1"assertion GREEN.git diff --checkclean;bun --bun run cipasses.{ type: "start", messageId }matching the DB record.startchunk at index 0, no duplicates (chunk-log invariant for v5).workflow.runtime.resolvedshows runtime + inference fields after the hoist; integration verification recorded.dennisonbertram/fork-open-agentswith TDD audit trail (red/green/regression commit hashes + outputs) and observability evidence. Nothing pushed tovercel-labs/open-agents.Assumptions and alternatives
Assumptions:
options.assistantIdfrom the route handler is always a freshly generated id (viagenerateId()fromai) — not a user-supplied or replay-derived id in the normal POST path. Replay/resume useslatestMessage.idwhich wins overoptions.assistantIdat the synchronous resolution site.inputMessagesPersisted: trueshort-circuit path is dead in production for this slice; it exists for future resume-flow use.Alternatives considered:
runtimePromise/ only hoisting the cheap promises (e.g. onlyconvertMessages+persistInputMessages): loses the primary latency win (sandbox connect is the expensive step). Rejected — the whole point is to overlap the expensive work.Promise.raceinstead ofPromise.allSettledfor the conflict drain: would not guarantee all hoisted promises finish before the stream closes, risking dangling async work. Rejected in favor ofPromise.allSettled."use step"generateId wrapper: loses the synchronous assistant-id resolution and would require the start-chunk write to be deferred, complicating the ordering. Rejected — the synchronous form is simpler and safe givenoptions.assistantIdis the stable source.Grooming clarifications (2026-05-31)
Provenance: This body was enriched by the issue groomer on 2026-05-31 using the following sources:
Upstream commit
53162d27inspected directly viagit show 53162d27(commit is reachable in this repo's git history). Confirmed: 6 files, +106/-95; diff read in full. The diff matches the issue body's integration spec exactly.chat-sandbox-runtime.tschanges (lines 88–130 deleted in upstream) are confirmed to still be present in the fork —sendStart,sendWorkspaceStatus, andassistantIdare all still present in the fork'schat-sandbox-runtime.tsat lines 187–205, 658, 663, 681. fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 has NOT landed on the fork yet.Fork surfaces verified read-only:
chat.ts(2,241 lines): current startup sequence confirmed at lines 1036–1150.claimActiveStreamcalled with 2 args (nowritable/messageId). Promises created AFTER the claim.generateIdstep wrapper at line 274.assistantIdresolved viaawait generateId()at line 1073.resolveChatSandboxRuntimecalled with{ userId, sessionId, chatId, assistantId, workflowRunId }(includesassistantId— confirming pre-fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880 signature).chat-post-finish.ts(600 lines):claimActiveStreamat lines 294–327, 2-param signature, nowritable/messageId, no start-chunk write. Confirmed.route.ts(380 lines):start()call at lines 230–241, noassistantIdin options. Confirmed.chat.test.ts(2,121 lines): 12mockImplementationOncesites forresolveChatSandboxRuntime/claimActiveStreamconfirmed. DefaultresolveChatSandboxRuntimespy at line 64 pushesstartchunk fromparams.assistantId. DefaultclaimActiveStreamspy at line 68 is a bare() => Promise.resolve("claimed").docs/plans/upstream-883-reflection.md§7 read in full — the "single totally-ordered chunk log" invariant extracted verbatim and recorded as a constraint in the behavior contract (item 5) and the definition-of-done.Latest migration:
0047_cheerful_thunderbolt_ross.sql. No migration required for this slice.Mock site count: 12
mockImplementationOncesites forresolveChatSandboxRuntime/claimActiveStreaminchat.test.ts(confirmed via grep). The issue body's original "10+" estimate is now precise: 12.Decisions made during grooming (documented, not product decisions):
resolveChatSandboxRuntimearg shape in the hoist section notes the removal ofchatId/workflowRunId/assistantId(owned by fix: loop run detail page's live polling wedges — shows stale 'Running' snapshot long after the run failed #880) explicitly, since the fork currently passes all three.0047) explicitly.