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
Operator-visible: a coding turn that used to finish in 10–20 minutes on tab-owned POST /api/agent now takes ~1 hour on the durable path (POST /api/turns → turnWorkflow) after the D17 cutover.
Same work (same model, same sandbox tools). The hour is wall-clock tax on the Workflows loop, not slower DeepSeek.
Parent / umbrella:#794 Status: source bug — create-plan is a later turn. This will almost certainly split (≥2 issues) if a plan tries to land reconnect + persist cadence + step-arg size in one PR.
This is not covered by:
#872 / #880 — per-round tool batch (stopped N writeTurnSse Fluid steps per tool). Live tokens already use one held writer. The remaining hop is still one Function per model round + one per tool batch + one persist per cadence point.
#884 / #885 — persist oversize killing the turn. Different failure mode.
#794’s cost example (8 rounds + 20 tools + 1 persist = 29 steps) is already stale. Live runTurnLoop persists after the first tools model and after every successful batch.
Full world rebuild on every model and tool step.assembleDurableToolWorld runs inside both modelGenerateStep and toolExecuteStep: decrypt GH token, load HTTP instance, resolveAgentSandbox + new sandbox client, MCP connect, HTTP runner, persona + skills, and BYOK again in the model step. The turns route even says the POST probe client is closed after start() because the in-step assemble helper opens its OWN client per step VM. /api/agent did this once. A 20-round turn now does it ~40 times.
Growing transcript stuffed into step args (O(n²) event log). Architecture lock: step I/O is deltas; messages stay orchestrator-local. Live code passes the full reconstructed messages[] into every modelGenerateStep, and the full deltas + fold.checkpoint into every persistStep. Later rounds serialize a bigger conversation into the Workflows event log, then replay it on the next resume. Short turns stay fine; 10–20 min coding turns get slower as they go.
Mid-turn persist is its own step, every round.persistOnce(false) after the first tools model and after every batch — Blob PUT + Redis overlay + another replay. Detach/refresh needs some persist; it does not need a Function hop after every round.
modelGenerateStep still has default 3 retries.toolExecuteStep.maxRetries = 0 (correct — don’t replay mutations). modelGenerateStep and persistStep do not set it. SDK default is 3 retries. A long streamText that blips or hits a step timeout can re-run the whole LLM round.
No whole-turn wall clock. Old path died at 30 min. Workflows only cap per step at 1800s, loop at 512 steps. A turn that used to get killed at 30 min can now run to 60.
writeTurnSse / closeTurnSse are still 'use step'. Live tokens are correctly held inside the model/tool step (withDefaultStreamWriter); loop done/error still pay an extra hop. That is not the old N-writes-per-token bug.
How to confirm (one slow Production run)
Vercel → Observability → Workflows, one hour-long coding turn:
Step count — expect ~3× model rounds, not ~1×.
Time between steps vs step duration — large gaps = replay/queue.
assembleDurableToolWorld / sandbox / MCP at the start of every model and tool step.
invincible.turn.model logs — duplicate rounds with the same shape ⇒ retries.
If (2)+(3) dominate, it is the step tax + reconnect. If later steps get slower, it is (3) the O(n²) args. If a round’s LLM time appears twice, it is (5).
Likely seam (for a later plan — do not implement here)
Direction, not a locked design. Highest leverage first:
Cache sandbox/MCP/BYOK across steps in the same run, or pass a handle instead of reconnecting every model and tool step.
Persist on a timer / every N rounds / terminal only — not a 'use step' after every batch.
Pass deltas into steps (as locked); keep messages[] orchestrator-local. Stop sending full deltas into persist — persist can read Blob.
Set maxRetries = 0 (or 1) on modelGenerateStep.
Collapse loop done/close into the terminal persist step so they are not extra 'use step' calls.
Do not wrap runAgentStream / streamText+execute in one step (forbidden). Do not cut back to /api/agent. Do not cancel the run on F5. Do not change Wasm/protocol.
If this is too large for one PR (it is), split along the table above. Reconnect (2) and persist cadence (4) are the wall-clock fire; step-arg size (3) is the “gets worse as the turn goes” fire.
Not this
Per-token / per-tool writeTurnSse Fluid steps (already fixed; held writer inside the step)
Tool parallelism inside a round (reads still Promise.all; only bind-mutators + FS editors are serial)
The 1s TURN_START_MIN_INTERVAL_MS 429 (start only)
Raising or lowering MAX_WORKFLOW_STEPS (512) or maxDuration (1800)
⛔ No application code in this issue — source bug. create-plan is a later turn.
DoD
A representative durable coding turn (the class that used to be 10–20 min on /api/agent) is no longer ~1 hour. Wall clock is dominated by model + tool time, not step hops / reconnect / replay.
Observability on a long run: step count is not ~3× model rounds from persist+assemble hops; time-between-steps is not the majority of the hour.
assembleDurableToolWorld is not a cold sandbox+MCP handshake on every model and tool step of the same run.
Step args stay bounded (deltas), not the growing messages[] / full deltas / checkpoint on every persist.
modelGenerateStep does not silently 3×-retry a completed LLM round.
No /api/agent fallback. No wrapping runAgentStream in one step. No Wasm/protocol change. D18 unmount still detach. C15 409 still live-only.
Cloud ops: N/A for the source issue (no Production mutate here). Diagnosis is Observability on an existing Production run.
⛔ No application code here — source bug. Split the later plan if it is >1 turn.
Refs
Operator report: since D17 cutover, 10–20 min coding turns take ~1 hour
Umbrella / architecture lock: #794 (deltas, no mega-step, replay budget)
Summary
Operator-visible: a coding turn that used to finish in 10–20 minutes on tab-owned
POST /api/agentnow takes ~1 hour on the durable path (POST /api/turns→turnWorkflow) after the D17 cutover.Same work (same model, same sandbox tools). The hour is wall-clock tax on the Workflows loop, not slower DeepSeek.
Parent / umbrella: #794
Status: source bug —
create-planis a later turn. This will almost certainly split (≥2 issues) if a plan tries to land reconnect + persist cadence + step-arg size in one PR.This is not covered by:
writeTurnSseFluid steps per tool). Live tokens already use one held writer. The remaining hop is still one Function per model round + one per tool batch + one persist per cadence point.Expected
A durable coding turn’s wall clock is dominated by model + tool time, same as
/api/agent.modelGenerateStep/persistStepcall into the event log./api/agentcoding turn is still ~15–20 minutes after D17, not ~60.Actual (grounded on
main@98f1a0f)D17 (#811 / PR #841) cut
runPromptover toPOST /api/turns./api/agentis tests/JSON only.Before: one Fluid Function, one sandbox/MCP/BYOK setup, in-process model↔tool loop, whole-turn ceiling
maxDuration = 1800.After:
turnLoopdrives isolated'use step'Functions. A 20-round coding turn is ~60 Function hops:modelGenerateSteppersistStep(running) — user-linetoolExecuteStep(batch, #880)persistStep(running) againwriteTurnSse(done) +closeTurnSse#794’s cost example (8 rounds + 20 tools + 1 persist = 29 steps) is already stale. Live
runTurnLooppersists after the first tools model and after every successful batch.Grounded seams:
Replay between every step (platform). Vercel Workflows suspends the orchestrator and enqueues a replay on every
'use step'(vercel/workflow#1930). plan: backend-agents RESTART — client-independent turns (umbrella, ≥20 small issues + handoff) #794 already flagged replay slowing after 2k events / 1 GB. Observability → Workflows → time between steps is the tell: if the gap ≫ step CPU, this is the bulk of the hour.Full world rebuild on every model and tool step.
assembleDurableToolWorldruns inside bothmodelGenerateStepandtoolExecuteStep: decrypt GH token, load HTTP instance,resolveAgentSandbox+ new sandbox client, MCP connect, HTTP runner, persona + skills, and BYOK again in the model step. The turns route even says the POST probe client is closed afterstart()because the in-step assemble helper opens its OWN client per step VM./api/agentdid this once. A 20-round turn now does it ~40 times.Growing transcript stuffed into step args (O(n²) event log). Architecture lock: step I/O is deltas; messages stay orchestrator-local. Live code passes the full reconstructed
messages[]into everymodelGenerateStep, and the fulldeltas+fold.checkpointinto everypersistStep. Later rounds serialize a bigger conversation into the Workflows event log, then replay it on the next resume. Short turns stay fine; 10–20 min coding turns get slower as they go.Mid-turn persist is its own step, every round.
persistOnce(false)after the first tools model and after every batch — Blob PUT + Redis overlay + another replay. Detach/refresh needs some persist; it does not need a Function hop after every round.modelGenerateStepstill has default 3 retries.toolExecuteStep.maxRetries = 0(correct — don’t replay mutations).modelGenerateStepandpersistStepdo not set it. SDK default is 3 retries. A longstreamTextthat blips or hits a step timeout can re-run the whole LLM round.No whole-turn wall clock. Old path died at 30 min. Workflows only cap per step at 1800s, loop at 512 steps. A turn that used to get killed at 30 min can now run to 60.
writeTurnSse/closeTurnSseare still'use step'. Live tokens are correctly held inside the model/tool step (withDefaultStreamWriter); loopdone/errorstill pay an extra hop. That is not the old N-writes-per-token bug.How to confirm (one slow Production run)
Vercel → Observability → Workflows, one hour-long coding turn:
assembleDurableToolWorld/ sandbox / MCP at the start of every model and tool step.messages/deltas/checkpoint).invincible.turn.modellogs — duplicate rounds with the same shape ⇒ retries.If (2)+(3) dominate, it is the step tax + reconnect. If later steps get slower, it is (3) the O(n²) args. If a round’s LLM time appears twice, it is (5).
Likely seam (for a later plan — do not implement here)
Direction, not a locked design. Highest leverage first:
'use step'after every batch.messages[]orchestrator-local. Stop sending fulldeltasinto persist — persist can read Blob.maxRetries = 0(or 1) onmodelGenerateStep.done/closeinto the terminal persist step so they are not extra'use step'calls.Do not wrap
runAgentStream/streamText+executein one step (forbidden). Do not cut back to/api/agent. Do not cancel the run on F5. Do not change Wasm/protocol.If this is too large for one PR (it is), split along the table above. Reconnect (2) and persist cadence (4) are the wall-clock fire; step-arg size (3) is the “gets worse as the turn goes” fire.
Not this
writeTurnSseFluid steps (already fixed; held writer inside the step)Promise.all; only bind-mutators + FS editors are serial)TURN_START_MIN_INTERVAL_MS429 (start only)MAX_WORKFLOW_STEPS(512) ormaxDuration(1800)create-planis a later turn.DoD
/api/agent) is no longer ~1 hour. Wall clock is dominated by model + tool time, not step hops / reconnect / replay.assembleDurableToolWorldis not a cold sandbox+MCP handshake on every model and tool step of the same run.messages[]/ fulldeltas/ checkpoint on every persist.modelGenerateStepdoes not silently 3×-retry a completed LLM round./api/agentfallback. No wrappingrunAgentStreamin one step. No Wasm/protocol change. D18 unmount still detach. C15 409 still live-only.Refs
lib/workflows/turnLoop.ts(persistOnceafter first tools model + after every batch;MAX_WORKFLOW_STEPS = 512)lib/workflows/assembleDurableToolWorld.ts,lib/workflows/modelGenerateStep.ts,lib/workflows/toolExecuteStep.tslib/workflows/persistStep.ts(deltas+fold.checkpointas step args)lib/workflows/turnSseStep.ts(writeTurnSse/closeTurnSsestill'use step')toolExecuteStep.maxRetries = 0;modelGenerateStepunset (SDK default 3)app/api/turns/route.ts(probe client closed afterstart(); in-step assemble opens its own)