Repository navigation
backend-agents: client-independent turns — in-flight + queue survive refresh / tab close #710
Description
Summary
Queued follow-ups (and the in-flight turn) must survive refresh and tab close. That is not a SessionStore dump of the Wasm FIFO. It is a backend-run agent: the turn is independent of the client; closing a tab does not abort it; the queue keeps draining on the server.
Label: backend-agents — this is the project-tracking issue for that product.
Status (2026-08-21): OPEN and unblocked. Plan: #764. #548 is memory, not turn ownership — do not wait on it. This issue closes when G (#770) merges. Do not stash the Wasm FIFO on
meta.
Execution order (strict — do not skip)
Work one of these at a time, in this order. #710 is not a first PR.
| Step | Issue | What | Unblocks |
|---|---|---|---|
| 1 | #767 D | Enable Vercel Workflows on Production + SDK smoke | preview/prod runs |
| 2 | #765 B | Spike: start / getRun / resumable AgentStreamEvent stream |
E |
| 3 | #766 C | meta.turnRunId + unmount = detach (do not abort the run) |
F |
| 4 | #768 E | Port one prompt into a Workflow (tools as steps, re-bind sandbox/skills) | F, H |
| 5 | #769 F | Viewport attach: GET stream + Blob hydrate + live thinking/tools/status | G |
| 6 | #771 H | Stop/Esc cancels the Workflow run (all viewports); fetch abort is detach | G |
| 7 | #770 G | Backend submit queue + child-run drain | closes this issue (#710) |
| 8 | #772 I | Living docs | done |
Parent plan (not a coding slice): #764.
No parallel slices until 1–2 are green. C (#766) is the only item that could overlap B; still do it as step 3 so detach ships before attach.
Goal
Today the product is a browser-owned turn:
Wasm enqueue / Send → host poll → POST /api/agent (AbortController in the tab)
→ tab close / refresh / unmount → abortRef.abort() → turn dies
→ submit_queue lives only in Wasm RAM → FIFO gone
Target:
Client is a viewport (Wasm canvas + host shell)
Turn owner is the backend (durable run, not a Function tied to one fetch)
→ close the tab: in-flight turn keeps going
→ reopen / refresh: see live Busy + remaining queue
→ queued follow-ups deliver in order without the original tab
Concrete operator outcome that started this issue: enqueue while Busy, refresh, the queue is still there and still runs. That only holds if the turn is also still there.
Observed (today)
- Busy, enqueue 1–N follow-ups (
▶/ Ctrl+Enter). Queue band paints (submit_queue.zig, max 16). - Refresh, New session, Clear, or session switch.
Actual: FIFO is gone. Docs: “Wasm-ephemeral. … Not stored on the cloud session” (docs/harness-limits.md Submit queue · Lifetime). Host queuedCount() is protocol v18 depth only — auto-continue must not steal the queue (harnessCanAutoContinue.ts). Nothing PUTs the items.
- Close the tab (or navigate) while Busy.
Actual: HarnessHost abortRef.current?.abort() on unmount / New / Clear / switch. The Vercel Function POST dies with the client. Partial transcript may persist; the rest of the turn and the queue do not continue.
Expected (this umbrella): refresh and tab close are viewport events, not turn-cancel. Cancel stays explicit (■ Stop / Escape, once those chords exist).
Why this is not “put the FIFO on meta”
Stashing string[16] on SessionSnapshot / Redis would restore the band after reload. It would not:
- keep the in-flight
runAgentalive afterAbortControllerabort - deliver queued items if no tab is open
- survive a closed laptop
That is a localStorage-shaped lie. Queue persist is a required outcome of backend-run turns, not a first PR.
Blocked on
| Blocker | Why |
|---|---|
| #548 agent-session (not started) | Durable agent mind (structured messages, notes, compaction, viewport). A backend-run turn that still folds 400/3.5M crumbs is a long-running lobotomy. Do not start this board until that track has actually begun (A1+ at minimum — lock in a later create-plan). |
| Backend-run turn execution (does not exist) | Inference must not be “one browser fetch to /api/agent”. Need a server-owned run (job / durable stream / worker) the canvas attaches to, not one it is. |
Do not sneak a meta.submitQueue onto #408 session PUTs as a preview. Feature-divide still holds: Wasm never talks to Redis or Blob; the host/backend owns persist. The queue’s source of truth moves off Wasm RAM onto the backend run.
Directional shape (not a plan — lock in create-plan after #548)
Steal the invariant, not a process:
| Concern | Today | This umbrella |
|---|---|---|
| Turn owner | Tab AbortController |
Backend run id, attach/detach |
| Queue owner | submit_queue.Q in Wasm |
Backend FIFO on that run/session |
| Promote | tryPromoteQueued in ui.frame() |
Server drains after Ready; canvas mirrors |
| Cancel | Host abort of this fetch | Explicit cancel of the run (all viewports) |
| Refresh | Wipe | Reattach: Busy + remaining queue |
| Tab close | Abort | Detach; run continues |
Caps (MAX_ITEMS 16, ITEM_BYTES = SUBMIT_CAP) stay until a plan changes them (cap governance).
Non-goals (this umbrella)
- Implementing anything in this issue
- A Wasm-only persist (localStorage / ring dump)
- Dual DOM chat / second composer
- Changing enqueue UX (harness: enqueue follow-up does not scroll so the new queued row is visible #696 scroll-into-view is a separate harness bug)
- Replacing session architecture — addressable per-user sessions with session-owned state (cwd, sandbox binding) #408 session ids / envelope / Blob
- Putting this on the agent session architecture — model-context memory, compaction, and viewport #548 agent-session board (project 3) — different product problem (model memory vs turn ownership)
- Starting before agent session architecture — model-context memory, compaction, and viewport #548 has a real phase in flight
Related
native/harness/src/submit_queue.zig— Wasm FIFO (MAX_ITEMS16)native/harness/src/bridge.zig—enqueueFromUi/tryPromoteQueued/queuedCountlib/harnessBridge.ts— v18inv_queued_count(depth only)app/harness/HarnessHost.tsx—abortRefabort on new turn / unmount / New / Clear / switchdocs/harness-limits.md— Submit queue · Lifetime (must change when this ships)docs/feature-divide.md— Wasm never talks to Redis/Blob; Stop/cancel is Wasm control + DOM abort today- #548 — agent-session umbrella (blocker)
- #696 — enqueue scroll (harness UX, not this)
Suggested next: nothing until #548 is underway. Then /create_plan this issue as its own parent with a Cloud ops path (the run must live in cloud infra, not the operator’s tab).
Activity
btipling commentedon Aug 22, 2026
Unblocked. The “wait for #548” hold is lifted — turn ownership is independent of agent-memory (#548 / #549). Memory still matters for what the model sees; it is not a reason to keep the tab as the runtime.
Comprehensive plan: #764
Lock: Vercel Workflows own the run (
start/getRun/ resumable stream). The canvas is a viewport. Queue drain is a child run per prompt (slice G #770) — that slice closes this issue.Do not stash the Wasm FIFO on
metaas a preview.btipling commentedon Aug 22, 2026
Exact execution order (also in the issue body):
turnRunIdStart at 1. Do not open a PR against this issue directly.
btipling commentedon Aug 23, 2026
⛔ POST-MORTEM — backend-agents (#710 umbrella) — ABANDONED / RESTARTED
Date: session reset
Decision by operator (btipling): close this umbrella and the entire backend-agents
slice family, hard-reset
mainandorigin/mainto17ca91bf(the tip just before theturns/strike — spike began), wipe the sandbox, and restart the whole program from scratch
with a minimum of 20 small, single-threaded issues. No multi-turn issue messiness.
This post-mortem is written before the issues are closed, as instructed. It is an honest,
unflinching accounting of where and how the implementation went wrong. It is not a defense.
What was being built (the intent)
Client-independent turns: a backend-owned turn that survives tab close / refresh — the run,
its in-flight events (thinking, tools, status), a viewport that can attach to a live run and
hydrate its state from Blob storage, and a backend submit queue that drains child runs. The
program was cut into a parent plan (#764) and slices B..I, and an umbrella (#710) tied them.
What actually went wrong — the hard truth
1. The biggest sin: I stuffed the entire backgrounding system into too few, oversized issues.
Instead of ~20 discrete, single-threaded, individually-mergeable issues, the program was
compressed into a small handful of giant multi-slice plans (#789, #791, #792) with a server
parent (#764) and an umbrella (#710). Each of those plans became a sprawling, multi-turn
implementation monster:
14 files, +2168/−163 and required five separate increments strung across many turns,
tracked in a "living planning comment" because the plan was too big to land in one go. That
is exactly the anti-pattern. A slice that big cannot be reviewed as one unit and cannot be
merged incrementally, so feedback arrived late and huge.
2. I "stumbled" through the initial implementation and left it half-there for too long.
plan/worker-turnsat pushed with a single freshness-ledger commit while the rest of the plan existed only in
a comment. The implement-plan deliverable (an open, mergeable PR) was not produced until
after five increments. This is how a "continue" loop becomes multiple turns of misdirection.
next buildfailed the Vercel Workflows bundler gate (Node-module graphs reaching theworkflow module: db/postgres, mcp/urlPolicy/node:dns, blobStore/node:crypto). A green vitest
gate was not enough and did not catch a genuine deploy-blocking regression. The
fixed build gate (run
next buildon any change that touches the workflows bundle) wasnever part of the correctness gate until review forced it.
3. Multi-turn planning and "living comment" escalation became normal — and it was wrong.
plan #791 and the F review explicitly required escalations with a living planning comment and
a running list of "remaining increments," which the operator then had to shepherd through
turn after turn. The user's directive — "if we even have multi-turn
issue again we'll start over" — names precisely this failure. It should never have
gotten to that state.
4. A spec contradiction was baked into the plan and never reconciled before locking.
Finding #3 in the PR #793 review exposed that the plan's wording ("your whole prompt = one
step") contradicted the source issue's wording ("each tool call = its own step"). That
discrepancy should have been caught and reconciled during plan creation, before any code. It
wasn't, and it produced a blocking design impasse and (worse) an attempt by me to force a
false binary (A vs B) and to lead with unsolicited recommendations instead of letting the
operator drive. That behavior is exactly wrong and is owned here.
5. PR feedback hell and review-driver rebuild.
Because the slice was so large, the adversarial review came back CONCERNS (3 Major + 2 Minor)
with real bugs: deploy failure, worker persisting undefined metadata, SSE stream hanging open,
a 409 that burned the rate-limit window, and the tools-per-step semantic mismatch. Fixing them
required a sizeable rewrite on an already-huge diff. Had the slice been small and single-
threaded, each of these would have been its own tiny PR caught in isolation.
6. Pipeline hygiene went stale.
Plans were HANDOFF-READY / IMPLEMENTED while their PRs sat unmerged; F was planned before E
merged; prerequisite gating (C→E→F) layered additional fragility onto an already fragile
process. The tracker never reflected a clean "one issue → one PR → merged → issue closed"
cycle.
Root causes (the genuinely systemic ones)
units. Everything downstream (late feedback, living comments, multi-turn continuation,
PR feedback hell, blocked-on-each-other plans) is a symptom of this first sin.
next build(the Workflows bundler)was not wired into the gate for workflow-touching changes, so a real regression shipped
silently under green vitest.
should have been resolved by reading the source issue's DoD against the plan body before
locking — it wasn't.
accepted path for large slices instead of a signal that the slice is too big.
requests for direction when the operator had made their intent plain all eroded trust.
Commitments on the restart (non-negotiable, operator-enforced)
issue = one deliverable, implemented and merged in one turn. No incremented plans, no
"living comment" escalation, no multi-turn continuation loops.
is unmerged; no PR merged before its adversarial PASS + the full gate.
source issue's DoD, I must surface and reconcile it before locking the plan — not after.
next build(where the change touches the Workflowsbundle) + adversarial PASS on the exact head. Green tests never substitute for the deploy
build, and never substitute for review.
main→origin/main→17ca91bf, the tip of therepo the moment before the turns/strike — spike began. The workflows-cloud-ops infra
(harness-backend: enable Vercel Workflows wiring + authed smoke + workflows-smoke GHA (plan #785) #786, already merged before the spike) remains; every commit of the spike → slice C stay
off
mainpermanently (still recoverable via the old PR refs if ever truly needed, but theintent is that they are gone).
Accountability
This was not one mistake; it was a chain of them, and each one amplified the next. The slice
that was too big made the implementation stumble; the stalled half-PR made it multi-turn; the
late adversarial review made it a rebuild; the plan/source contradiction made it an impasse. I
own all of it. The restart's small-issue discipline is the correction, and it is enforced by
the operator, not by me.
— the implementing agent (btipling's automation), accepting full blame for the botch.
btipling commentedon Aug 23, 2026
See POST-MORTEM above.