cmux-next: Run board, run.attach bridge and retention reaper (rebuilt from #21305)

Merged
TE
16d ago
+5129-28

Summary

#21305 can't merge into feat-cmux-next anymore (37 conflicting files). Most of its durable Run API landed in reworked form in #21337 and #21634, and its priority sidebar landed in #21234 and #21281. This PR rebuilds the parts that hadn't landed on the current architecture, and fixes one bug in the landed code that the rebuild would otherwise have switched on.

Run board (webview, gallery only). webviews/src/agent-session/acpmux/run-board/ holds the board plus its client for /api/runs. The client reads a run's snapshot, replays events from a cursor, and mints and redeems a one-use attach ticket. Gallery entries cover the base board, the Compare and Timeline variants, and the top-two tournament judge.

Compared with #21305, the board:

  • follows today's type-scale and layer tokens;
  • restarts its replay session when the run changes;
  • takes the session id from the Run view's placement;
  • disables Attach when the view says the attempt isn't attachable.

The board isn't mounted in the app. The agent page's CSP is connect-src 'none' and these routes need cmux.com auth, so mounting it means routing the fetches through native first.

run.attach (agent pane → App). The page sends run.attach with a redeemed ticket's ids, the worker session and a live ws/wss trusted-carrier endpoint. The App opens that terminal as a remote-terminal tab in the pane, and only under these conditions:

  • the person made a fresh gesture in the pane, used once, as tab.open requires to run a line;
  • the target is a Cloud machine whose daemon is already connected and whose session matches the ticket;
  • the target is never this Mac, an SSH host or a paired server.

The route is never dialed, and no token crosses the bridge. Nothing in the app sends run.attach yet (see above).

Retention reaper and artifact route (web).

  • Retention cron. The landed dogfood path keeps a finished attempt's VM attachable for a retention window, and its comment says a cron destroys the VM afterwards. No route called reapExpiredCloudVmAttempts, though. /api/cron/run-attempt-retention now runs it every minute, monitored like the sibling crons.
  • Promoted VMs are skipped. While building this, review found that claimExpiredAttemptCleanups would also claim an attempt that had been promoted to a persistent runtime. Promotion keeps the same VM and doesn't clear the retention window, so the cron would have destroyed that runtime's live VM. The claim now skips attempts that have a runtime_id.
  • Already-gone VMs count as cleaned. A VM the reaper finds already gone is marked cleaned. Before, it failed every minute and blocked the rows behind it.
  • Artifact route. POST /api/runs/[id]/attempts/[attemptId]/artifacts exposes the landed recordRunArtifact to workers.
  • Ported tests. #21305's tests for the run model, the provider view, worker auth and the live-database route suite come across. The route suite now expects a changed retry of the same event id to get a 409, which is #21634's rule.

Not rebuilt from #21305:

  • Landed elsewhere: the durable Run/Attempt/Event API, retention and cancellation, attach-ticket minting, typed provider views, lease fencing (#21337, #21634) and the priority sidebar projection (#21234, #21281).
  • Not landed, and not part of this PR:
    • the agent broker, VM host-lease transport, Huge Blue relay and attach tokens, and persistent-workspace lifecycle wiring (#21456, #21502, #21503, #21559, #21569, #21581, which merged into #21305's branch and never reached feat-cmux-next);
    • the off-main-actor PNG encode for window snapshots;
    • the Codex voice capability constants;
    • the orchestration lab gallery;
    • the sidebar motion debug menus;
    • the New Tab context changes, superseded by #22131's location row.

Testing

  • Webview tests: cd webviews && bun test src/agent-session/acpmux/run-board passes, 25/25. bunx --bun vp check src/agent-session/acpmux/run-board is clean.
  • Webview style checks: check:layers, check:type-scale, check:system-blue and check:browser-lanes pass. bun run check as a whole fails on this checkout, but only on base files this PR doesn't touch:
    • check:text-focus and check:dialog-keys fail on FileGrid, FilesUI, Screenshots and Browser Swift files;
    • oxfmt flags src/pages/settings/*;
    • the typecheck can't find the ungenerated pages/settings/generated/strings.json.
  • Web unit tests: cd web && bun test tests/run-view.test.ts tests/run-model.test.ts tests/run-worker-auth.test.ts tests/cron-auth.test.ts tests/backend-error-coverage.test.ts passes. bun run typecheck, bun run lint:complexity and bunx eslint on the changed web files are all clean.
  • Live Postgres (podman on huge-blue, migrations applied with scripts/db-migrate.mjs), with CMUX_DB_TEST=1 at ff6c6a5ee5a:
    • run-route-db-behavior 9/9;
    • run-repository-db-behavior 9/9;
    • run-retention-db-behavior 6/6;
    • run-attempt-promotion 7/7;
    • runtime-promotion-db-behavior 1/1.
  • Promoted-VM regression: the new test never reaps the VM of an attempt promoted to a persistent runtime failed with the old claim (Expected: false, Received: true, 5/6 at bfb7f1e1c5a) and passes with the fix.
  • Swift: xcrun swiftc -parse on every changed file passes, and scripts/cmux-next/prepush-gate.py reports no new violations. AgentPaneRunAttachTests was added; it covers decoding, refusal of a stale or untrusted endpoint, the single-use gesture gate and the missing-host reply.

Observed / Not run

Observed: everything listed under Testing.

Adopted from a desktop session with no live owner. Fixes since adoption: the expired-attempt cleanup claim only reaps ephemeral or runtime-less attempts, promoteAttemptToRuntime throws RuntimeAttemptConflictError while a cleanup lease is active, public import Foundation in AgentPaneRunAttach.swift, and size trims. Base merged at 8ae8fd5f2bd4 (clean).

At head 63031b46ce82:

  • Fleet cmux-ci compile-check --app passed (job 2222dd98a78664c0dbdc20f8).
  • Fleet run-swift-testing-suites.sh Packages/macOS/CmuxNext: AgentPaneRunAttachTests passed. The run also failed 36 suites this PR doesn't touch (Accounts, ClassicUpgradeSimulation, FolderNameIndexSpeed, AgentPaneSendPickedFolderPrompt gesture checks, among others), so that run as a whole is not green.
  • CI reds are base only: Web tests (1/4) fails "keyboard shortcuts docs > table aliases" and react-apps-check fails as on base. The CI Swift jobs were still queued at merge time.

Follow-up: runError in web/services/runs/http.ts doesn't map RuntimeAttemptConflictError to 409 yet.

Not run:

  • CI's cmux-next swift test hadn't run at merge time (queued).
  • The retention cron and the artifact route weren't exercised against a deployed backend.
  • run.attach hasn't been driven in a tagged build, because nothing in the app sends it yet.

Review: correctness review by a separate review subagent at eaca1abb783 (verdict: pass with fixes). Its findings are fixed in bfb7f1e1c5a..ff6c6a5ee5a; the same reviewer re-checked those at ff6c6a5ee5a (pass).
Runtime test: run-retention-db-behavior "never reaps the VM of an attempt promoted to a persistent runtime" plus the Run DB suites above, CMUX_DB_TEST=1 bun test --max-concurrency=1 <file> against Postgres 16 on huge-blue at ff6c6a5ee5a.

Changelog

none

Proof

Not UI: the Run board appears only in the webview gallery, whose advisory run captures it (agent-pane.run-board* entries). Nothing user-visible changes in the app.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Rebuilds the Run board webview and run.attach bridge from #21305 on the landed durable Run API, and adds the run-attempt retention reaper and worker artifact route. Fixes three reaper bugs that would have destroyed promoted runtimes' VMs and blocked cleanup rows.

Run board and attach bridge

  • The board reads a run's snapshot, replays events by cursor, and mints and redeems a one-use attach ticket; it's gallery-only, not mounted in the app.
  • run.attach opens the attempt's terminal as a remote-terminal tab only on a connected Cloud machine whose daemon session matches the ticket, with a fresh pane gesture; the route is never dialed and no token crosses the bridge.
  • CmuxNextApp stays under its size cap by joining the attach wiring onto fewer lines, with no behavior change.
  • run.attach's init imports Foundation publicly so the package builds with InternalImportsByDefault.

Retention reaper and artifact route

  • /api/cron/run-attempt-retention runs the landed reaper every minute, monitored like the sibling crons.
  • The reaper skips attempts promoted to a persistent runtime, not every attempt with a runtime: each launched attempt already owns an ephemeral runtime, so claiming all of them would reap every finished VM.
  • Promotion is refused while the reaper holds an attempt's cleanup lease, closing the race where promotion commits before the destroy and loses its VM.
  • A VM the reaper finds already gone counts as cleaned instead of failing every minute and starving older rows.
  • POST /api/runs/[id]/attempts/[attemptId]/artifacts exposes recordRunArtifact to workers.