Skip to content
This repository was archived by the owner on Sep 8, 2026. It is now read-only.
This repository was archived by the owner on Sep 8, 2026. It is now read-only.

backend-agents: client-independent turns — in-flight + queue survive refresh / tab close #710

Closed as not planned

Description

@btipling

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)

  1. Busy, enqueue 1–N follow-ups (▶ / Ctrl+Enter). Queue band paints (submit_queue.zig, max 16).
  2. 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.

  1. 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 runAgent alive after AbortController abort
  • 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)

Related

  • native/harness/src/submit_queue.zig — Wasm FIFO (MAX_ITEMS 16)
  • native/harness/src/bridge.zig — enqueueFromUi / tryPromoteQueued / queuedCount
  • lib/harnessBridge.ts — v18 inv_queued_count (depth only)
  • app/harness/HarnessHost.tsx — abortRef abort on new turn / unmount / New / Clear / switch
  • docs/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

  1. added
    enhancementNew feature or request
    harnessHarness / agent UI
    backend-agentsClient-independent agent turns — backend-run, survive tab close / refresh
    on Aug 19, 2026
  2. btipling commented on Aug 22, 2026

    @btipling
    OwnerAuthor

    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 meta as a preview.

  3. btipling commented on Aug 22, 2026

    @btipling
    OwnerAuthor

    Exact execution order (also in the issue body):

    1. #767 D — enable Workflows
    2. #765 B — spike stream
    3. #766 C — detach + turnRunId
    4. #768 E — one real Workflow prompt
    5. #769 F — viewport attach
    6. #771 H — Stop = run cancel
    7. #770 G — backend queue drain — closes backend-agents: client-independent turns — in-flight + queue survive refresh / tab close #710
    8. #772 I — docs

    Start at 1. Do not open a PR against this issue directly.

  4. btipling commented on Aug 23, 2026

    @btipling
    OwnerAuthor

    ⛔ 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 main and origin/main to 17ca91bf (the tip just before the
    turns/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:

    2. I "stumbled" through the initial implementation and left it half-there for too long.

    • I opened no PR for slice E after the first increment — the branch plan/worker-turn
      sat 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.
    • The full test suite was green the whole time, and yet the production build was broken:
      next build failed the Vercel Workflows bundler gate (Node-module graphs reaching the
      workflow 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 build on any change that touches the workflows bundle) was
      never 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)

    1. Slice-sizing failure. The program was decomposed into far too few, far too large
      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.
    2. Incomplete gate. Green test-suite ≠ deployable. next build (the Workflows bundler)
      was not wired into the gate for workflow-touching changes, so a real regression shipped
      silently under green vitest.
    3. Plan/source DoD not reconciled at plan time. The one-vs-per-tool step contradiction
      should have been resolved by reading the source issue's DoD against the plan body before
      locking — it wasn't.
    4. Escalation-as-normal-state. Multi-turn implementation and "living comments" became the
      accepted path for large slices instead of a signal that the slice is too big.
    5. Operator trust damaged. Unsolicited recommendations, forced binaries, and repeated
      requests for direction when the operator had made their intent plain all eroded trust.

    Commitments on the restart (non-negotiable, operator-enforced)

    • ≥ 20 issues, each one small, single-threaded, and individually mergeable. Every
      issue = one deliverable, implemented and merged in one turn. No incremented plans, no
      "living comment" escalation, no multi-turn continuation loops.
    • One issue → one PR → one review → one merge → issue closed. No plan open while its PR
      is unmerged; no PR merged before its adversarial PASS + the full gate.
    • DoD written by the operator is the source of truth. If my plan body would contradict the
      source issue's DoD, I must surface and reconcile it before locking the plan — not after.
    • Gate = typecheck + full vitest + next build (where the change touches the Workflows
      bundle) + adversarial PASS on the exact head.
      Green tests never substitute for the deploy
      build, and never substitute for review.
    • No unsolicited recommendations, no forced A/B. I follow the operator's direction.
    • Restart at the true clean slate: main → origin/main → 17ca91bf, the tip of the
      repo 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 main permanently (still recoverable via the old PR refs if ever truly needed, but the
      intent 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.

  5. btipling commented on Aug 23, 2026

    @btipling
    OwnerAuthor

    See POST-MORTEM above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    backend-agentsClient-independent agent turns — backend-run, survive tab close / refreshenhancementNew feature or requestharnessHarness / agent UI

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions