Skip to content

fix(runs): replay worker retries only for the writing lease; fence retention handoffs - #21634

Merged
teamleaderleo merged 10 commits into
feat-cmux-nextfrom
split/21305-run-lease-fencing
Oct 20, 2026
Merged

teamleaderleo merged 10 commits into
feat-cmux-nextfrom
split/21305-run-lease-fencing

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

A Run worker can now retry an event or artifact it already wrote and get the stored row back, even after its lease expired, was replaced, or was cleared by its own terminal event. A stale lease holder can no longer get a success reply for someone else's work or overwrite the replacement worker's state.

What changed:

  • run_events and run_artifacts record which lease wrote each row: its generation and the SHA-256 of its token. The new migration is 20261019120000_run_writer_lease. It is additive with nullable columns, so it stands alone.

  • ingestRunEvent and recordRunArtifact apply the retry rule below before they check the lease. A request with a new id still needs the live lease.

  • Worker responses drop the writer columns.

  • scheduleAttemptRetention needs the worker's lease token and generation and a terminal attempt. A stale worker's finally block therefore can't overwrite the replacement's VM, workspace and terminal handles or its retention deadline. A terminal event clears only the lease expiry and keeps the token until this handoff consumes it.

  • Cancel during VM create no longer leaks the VM. A cancel that lands while createVm is in flight bumps the lease generation before the worker has stored the VM id. The worker's attempt.started ingest and its fenced scheduleAttemptRetention handoff were then both rejected, so no row pointed at the VM and the reaper never destroyed it. Now, when the fenced handoff is rejected, scheduleAttemptRetention still records the provider VM id for immediate reaping (terminalRetainUntil = now). It does this only when all of these hold:

    • the attempt is terminal and its lease generation is newer than the caller's;
    • the attempt has no VM id, retention deadline or cleanup lease;
    • no event for the attempt ever carried a providerVmId.

    Nothing else can own that VM, because create is idempotent per attempt. A VM the reaper already destroyed and cleared is never handed back to it. No other field of the fenced worker is written.

Carved from #21305 (commits bdbf27bd05, 4d9248ff18, f8b5cc631d, 585deed335). The durable Run schema and repository landed on feat-cmux-next in #21337, and web/services/runs hasn't changed there since. This PR is the lease-fencing delta on top of it, now rebased onto current origin/feat-cmux-next. main's separate Run implementation (#21444 and its follow-ups) has no worker leases, so nothing there supersedes this.

Decision

Rule: a request that reuses a stored event id or artifact name gets the stored row back (200, nothing written) only when the caller's lease token hash and generation match the lease that wrote that row and the request is identical:

  • for events: kind, attempt sequence and payload, compared by jsonb equality;
  • for artifacts: digest, size and URI.

Any other reuse is a 409:

  • run_event_conflict or run_artifact_conflict when the caller is the writer or holds the live lease;
  • otherwise run_lease_conflict.

A stale caller gets the same reply for a reused id as for a new one, so it learns nothing about stored rows. Rows with no recorded writer match nobody.

Cases, each a DB behavior test in web/tests/run-repository-db-behavior.test.ts:

Case Result
Same holder retries an identical terminal event after its expiry was cleared, and again after the retention handoff cleared the token Stored row, attempt stays succeeded, no new journal rows
Stale holder sends a different event (new id) after a replacement took the lease run_lease_conflict
Stale holder reuses an id the replacement wrote run_lease_conflict
The writer retries an identical non-terminal event after being replaced Stored row, no new journal rows
The writer retries the same id with different content run_event_conflict
Replacement holder sends an identical copy of the old lease's attempt-started run_event_conflict; attempt stays leased
Row with no recorded writer Nobody gets the stored row
Artifacts Same rule

Counterargument: I asked a review agent to argue against the first draft. These changes came out of it:

  • Dropped "the live holder may replay any identical row": that clause let a replacement worker's identical attempt-started return the old lease's row. The attempt then stayed leased while the new worker believed it had started.
  • Payload compared in SQL: jsonb equality, so key order and number formatting don't break a retry.
  • Writer columns on every lease-created row, including the attempt.artifact journal row.
  • Identity is token hash plus generation: all workers share the worker token, and generations are guessable.

Open points it raised, not changed here:

  • Fixed per-attempt ids can block a replacement worker. A replacement worker that reuses ids such as attempt-failed with low attempt sequences collides with the old lease's rows and with per-attempt sequence uniqueness, so it can't record a terminal event for that attempt. This was already true before this PR. Fixing it needs ids and sequences scoped by generation, and that's a separate change.
  • Replies don't say the lease is gone. A stale writer whose retry succeeds isn't told its lease is gone; it finds out on its next renew. That's harmless to data.

Validation

  • Locally, on huge-blue with Postgres 16 in podman, migrated with bun scripts/db-migrate.mjs:
    • CMUX_DB_TEST=1 bun test tests/run-retention-db-behavior.test.ts:
      • Before the fix (e966878b7a7, test commit only), schedules cleanup for a VM whose create returned after cancellation fenced the worker fails with stale or expired attempt lease.
      • After the fix (d7fe72e9565), all 5 pass.
    • The guard case, does not hand a stale worker's VM back to retention after the reaper destroyed it, passes both before and after.
    • Full bun run test:db:behavior on head: exit 0, no failures.
    • tsgo --noEmit: no errors in web/services or web/tests/run-*. The only errors are from workers/ sources, which that partial copy didn't have. CI web-typecheck is green.
  • Commits put tests before fixes: 1c0ece6d38d (tests), then a8498dd0335 (fix); 99c25ae938e (tests), then 2ced057fdac (fix); e966878b7a7 (tests), then d7fe72e9565 (fix).

Proof

  • Not UI: web Run repository and DB behavior tests only (web/services/runs, web/tests, one additive migration). No app, webview or Swift code.
  • Runtime test: run-retention-db-behavior.test.ts (schedules cleanup for a VM whose create returned after cancellation fenced the worker, does not hand a stale worker's VM back to retention after the reaper destroyed it, plus the existing retention cases) and run-repository-db-behavior.test.ts (the lease-fencing retry cases), in web / web-db-migrations on d7fe72e9565: https://github.com/manaflow-ai/cmux/actions/runs/39344503727/job/118374018262

Changelog

Fixed: Cloud Run workers can safely retry their own events and artifacts, a replaced worker can no longer overwrite the new worker's state, and cancelling a Run while its VM is being created no longer leaks the VM.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: c287f4ad-3958-4e9e-a69b-dee7828e717a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-3pzvo created for this PR by the cmux coordinator.

@teamleaderleo
teamleaderleo marked this pull request as draft October 18, 2026 13:19
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

On hold behind the same run-attach-linux freeze as #21305. Several run-* fixes landed in web/services/runs today, and this slice changes retry semantics: a retried terminal event after the lease expiry was cleared now gets a 409 instead of the stored duplicate. The run owner should decide that, so it isn't going in as a split slice. Left open as a draft; no further pushes.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Blocked: it needs the run owner (the Huge Blue run lane, or Leo) to decide whether a worker retrying a terminal event after its lease expiry was cleared should get a 409, as this PR does, or the stored duplicate, as today. If 409 is right, this rebases onto feat-cmux-next and goes for review. If not, close it. Nothing else depends on it.

teamleaderleo and others added 2 commits October 19, 2026 04:53
A worker whose lease was replaced can still replay an event or artifact
it sent before, and can still schedule terminal retention over the
replacement worker's VM handles. These DB behavior tests expect both to
be rejected with RunLeaseConflictError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ingestRunEvent and recordRunArtifact now check the caller's lease before
returning a stored duplicate, so a worker whose lease was replaced can no
longer get a success reply for an event or artifact it sent earlier.

scheduleAttemptRetention now requires the worker's lease token and
generation and a terminal attempt. A terminal event keeps the lease token
(only its expiry is cleared) until this handoff consumes it, so a stale
worker's finally block cannot overwrite the replacement worker's VM,
workspace and terminal handles or its retention deadline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo pushed a commit to teamleaderleo/cmux that referenced this pull request Oct 19, 2026
…cx-q1lnl)

134 main commits since the last sync (3e7ed5a). Full rename detection
(merge.renameLimit=100000). manaflow-ai#21310 (0d5316d, export npm_config_prefix)
is now on main; it is a superset of feat's manaflow-ai#21311 copy, so the resume
workflows keep one version (main's). No Swift, Rust, Go or package manifest
change survives the merge (daemon/remote/go.mod unchanged).

Conflict choices:
- 209 modify/delete and 1 rename/delete in the legacy app, legacy CLI,
  cmuxTests, legacy Swift packages and Resources/Localizable.xcstrings: kept
  deleted.
- cmux.xcodeproj/project.pbxproj, cmux.xcworkspace/contents.xcworkspacedata,
  docs/cli-contract.md, tests/test-execution.toml: feat-cmux-next's side.
- ci-macos.yml, seed-derived-data.yml, restore-app-host-test-product.sh,
  package-test-lane.sh (CmuxTextActions), tests/test_ci_change_areas.py
  (app-host admission block): feat's side; main's rsync --no-times edits
  are for CmuxAgentJournal framework staging that feat deleted. Main's new
  merge-gate change-area test merged in.
- scripts/ci/compile-app-host-test-product.sh: main's logic (the legacy
  cmux target builds without the compilation cache on every Xcode); the
  test (test_ci_test_compilation_cache_seed.sh) keeps feat's one-scheme
  checks with main's every-Xcode loop.
- nightly.yml and tests/test_nightly_universal_build.sh: feat's side (feat
  already had manaflow-ai#21761's empty DerivedData start via manaflow-ai#21762, plus the
  nightly-next build-state prepare and its no-replay check; manaflow-ai#21544's
  promotion change was already on feat via manaflow-ai#21533). Feat's WRITER gate,
  live HOLD read, recovery guard, legacy-identity split, Mach-O floor,
  macOS 15 launch job and bash 3.2 parse hardening kept.
- scripts/gh-merge-green: main's version (multiple freeze tokens manaflow-ai#21584,
  merge-tree merged head manaflow-ai#21576, missing Swift path refusal manaflow-ai#21560, risk
  evidence manaflow-ai#21692) plus feat's "cmux-next Swift canary" required check,
  freeze expiry and owner/pr/head metadata, and the canonical merged path.
- scripts/install-cmux-tui-client.sh: union of the header comments (main's
  total download budget manaflow-ai#16305 and feat's attestation retry text).
- web/data/cmux-shortcuts.ts: feat's generated table.
- web/services/vms/workflows.ts: feat's doc comment on requireUserVm.
- Run API: feat's version (manaflow-ai#21337, open lanes manaflow-ai#21634/manaflow-ai#21305 build on it).
  web/services/runs/*, web/app/api/runs/*, web/db/schema.ts run tables:
  feat's side. Main's parallel Run API (manaflow-ai#21444, manaflow-ai#21468, manaflow-ai#21473, manaflow-ai#21477,
  manaflow-ai#21484, manaflow-ai#21488, manaflow-ai#21492, manaflow-ai#21495, manaflow-ai#21467, manaflow-ai#21517, manaflow-ai#21573, manaflow-ai#21596) is NOT
  ported: its runs services, cron route and vercel.json cron, admission
  probe, run migrations 20261017150000/20261018000000/20261018110000/
  20261018120000 and their tests are removed here, and
  cloud-vm-dogfood.yml / dogfood-journey.mjs stay feat's (main's staging
  dogfood deploy changes manaflow-ai#21556-manaflow-ai#21617 and manaflow-ai#21424-manaflow-ai#21428 not ported).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo and others added 2 commits October 19, 2026 04:59
Covers the retry rule for reused event ids and artifact names. The
writer's identical retry gets the stored row after its lease was
replaced, after its own terminal event cleared the expiry, and after
the retention handoff cleared the token. A changed retry is a content
conflict. A stale holder gets a lease conflict. A replacement holder
never gets another lease's row, and a row with no recorded writer
matches nobody.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wrote it

Event and artifact rows now record the writing lease's generation and
the SHA-256 of its token (migration 20261019120000_run_writer_lease).
A request that reuses a stored id gets that row back, with no write,
only when the caller is that lease and the request is identical (kind,
attempt sequence and jsonb payload for events; digest, size and URI for
artifacts). Otherwise it is a 409: a content conflict for the writer or
the live holder, and a lease conflict for anyone else. A new id still
needs the live lease. Worker responses drop the writer columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo
teamleaderleo force-pushed the split/21305-run-lease-fencing branch from d002617 to 2ced057 Compare October 19, 2026 04:59
@teamleaderleo teamleaderleo changed the title fix(runs): fence stale worker retries and retention handoffs by lease fix(runs): replay worker retries only for the writing lease; fence retention handoffs Oct 19, 2026
@teamleaderleo
teamleaderleo marked this pull request as ready for review October 19, 2026 05:00
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Retry decision: a reused event id or artifact name gets the stored row back only for the lease that wrote it, and only for an identical request. Every other reuse is a 409. The rule, the cases and the counterargument are in the Decision section of the PR description. Head 2ced057fdac.

@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

all web/size checks green

@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review d7fe72e: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr21634 -f ref=split/21305-run-lease-fencing -f platform=macos -f backend_mode=local

Passes: CI passes on d7fe72e956.

CI passes on d7fe72e956 (run 39344503727 attempt 1).

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

CI fast guards passes on d7fe72e956 (run). This covers only the fast guards, not CI: CI's result is ci-status.

@github-actions

Copy link
Copy Markdown
Contributor

CI fast guards failed on 2ced057fda (https://github.com/manaflow-ai/cmux/actions/runs/39202232801). It does not block the merge; a red guard merged into main breaks it for every open PR.
The log named no failed step; see the run.

Agents: python3 scripts/ci/guard_attribution.py fix applies the mechanical fixes locally. This comment is updated in place on each push.

@blacksmith-sh

blacksmith-sh Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Found 1 test failure on Blacksmith runners:

Failure

Test View Logs
NEXT post-publication dependency contract/NEXT post-publication dependency contract View Logs

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

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

CI on 2ced057fdac: the changed code passes. The three red checks also fail on feat-cmux-next.

Passed

  • web-db-migrations: migrations applied twice on Postgres 16, then the DB behavior suite ran.
    • run-repository-db-behavior: 9 pass, 0 fail. That includes all six retry-rule cases in the Decision section.
    • run-retention-db-behavior: 3 pass, 0 fail.
    • runtime-promotion-db-behavior: passed.
  • web-typecheck: passed.

Red, also on feat-cmux-next (this PR touches none of their inputs)

  • Web tests (4/4), and Web status because of it. Two tests in vm-image-manifest.test.ts fail ("every default was baked…" and "every drift awaiting its rebake…") with a devbox source digest of d77b614b… where c6bdea67… is expected. Running bun test tests/vm-image-manifest.test.ts on a detached origin/feat-cmux-next (c181fb5ad13) gives the same two failures and the same digest. This PR changes no file under web/services/vms/images or web/scripts.
  • cmux-next checks. It fails on No literal state roots, Main-thread concurrency and Motion tokens. All three point at ErrorJournalService.swift, ErrorJournal.swift and SidebarCardTransition.swift, which this PR doesn't touch. They were base reds at this PR's base c77b58ee93e, cleared afterwards on feat-cmux-next by cmux-next: clear five base reds in cmux-next checks (ErrorJournal, browser popups) #21981 and cmux-next: mark the sidebar card's Core Animation objects as reviewed motion exceptions #21951.

release-compile is still pending.

@cursor

cursor Bot commented Oct 19, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The changes reach the durable Run worker and VM cleanup workflow.

Fix the late-created VM cleanup gap before merging.

Findings

  1. P1 Late-created VM escapes cleanup ▶
  2. P2 Comment promises unsupported replay ▶

Summary

This PR ties event and artifact retries to the lease that wrote each row.

  • Run workers can replay only the matching events and artifacts their lease wrote.
  • Terminal Run workers cannot overwrite a newer worker's retained VM.

A replacement worker's VM-less failure can still block cleanup of a late-created VM.

teamleaderleo explicitly defers the existing fixed-id/sequence collision issue and accepts successful retries not reporting lease loss. Rows without a recorded writer intentionally match nobody.

Review used code inspection; no tests were executed.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[First worker starts VM creation] --> B[Lease expires]
  B --> C[Replacement takes lease]
  C --> D[Same create key reports creation in progress]
  D --> E[Replacement records failure without VM id]
  E --> F[Terminal event sets retention deadline]
  A --> G[First worker receives VM id]
  G --> H[Event writes are fenced]
  H --> I[Retention handoff tries adoption]
  F --> I
  I --> J[Non-null deadline blocks adoption]
  J --> K[No VM id saved for cleanup]
Loading

Reviews (3) · Last reviewed commit: "Schedule cleanup for a VM created while ..." · Reviewed by Greptile

Comment thread web/db/schema.ts
externalId: text("external_id"),
kind: runEventKind("kind").notNull(),
payload: jsonb("payload").$type<Record<string, unknown>>().notNull().default(sql`'{}'::jsonb`),
/** Lease that wrote this event. A retry of it is answered only for this lease or the live holder. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Comment promises unsupported replay

This comment says the live holder can replay another lease's event, but assertStoredRetry rejects that request even when the content matches. Remove “or the live holder” so the schema documents the writer-only rule. The artifact comment points here too. Leaving this wording could lead a teammate to restore the behavior this PR deliberately removes.

Suggested change
/** Lease that wrote this event. A retry of it is answered only for this lease or the live holder. */
/** Lease that wrote this event. An identical retry is answered only for this lease. */

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines +365 to +367
eq(runAttempts.leaseToken, input.leaseToken),
eq(runAttempts.leaseGeneration, input.leaseGeneration),
inArray(runAttempts.status, ["succeeded", "failed", "cancelled"]),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Cancellation leaves a VM running

If the user cancels while createVm is still running, finalizeAttemptCancellation clears the lease before the VM is recorded. When creation returns, both event writes fail, and these new checks also reject the finally block's scheduleAttemptRetention call. That rejection is only logged. The VM stays running without a providerVmId or retention deadline on the attempt, so attempt cleanup cannot find it.

Keep the fence, but add a safe cleanup path for a VM created after its worker was cancelled.

A cancel during createVm fences the worker before it can store the VM id,
so its retention handoff was rejected and no row pointed at the VM. The
fenced handoff now records the id for immediate reaping when no lease ever
stored a VM for the attempt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gt(runAttempts.leaseGeneration, input.leaseGeneration),
inArray(runAttempts.status, ["succeeded", "failed", "cancelled"]),
isNull(runAttempts.providerVmId),
isNull(runAttempts.terminalRetainUntil),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Late-created VM escapes cleanup

adoptFencedAttemptVm rejects a late-created VM when a replacement worker failed without a VM. If the first worker's lease expires during createVm, a replacement can take the lease and receive VmCreateInProgressError for the same create key. Its attempt.failed event sets terminalRetainUntil even though providerVmId is still null.

When the first worker receives the VM, this null-deadline check blocks adoption. Its event writes are also fenced, so the attempt never records the VM and the reaper cannot find it. Allow this handoff for a terminal attempt that never recorded a VM, even when its terminal event already set a deadline.

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

teamleaderleo added a commit that referenced this pull request Oct 20, 2026
…suite

#21634 made a retry with the same event id and different content a 409; the
suite ported from #21305 still expected the first event back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Oct 21, 2026
… from #21305) (#22408)

* cmux-next: add the Run board webview and its Run API client

Rebuilt from #21305 onto the landed Run API (#21337, #21634). The board reads
a durable run's snapshot and replays its events by cursor, mints and redeems a
one-use attach ticket, and asks the App to open the worker terminal
(run.attach). Gallery entries: the base board, the Compare and Timeline
variants, and the top-two tournament judge.

Adapted to current lints: type-scale and layer tokens instead of raw sizes and
z-index, an output element for attach status, and a narrow fetch type so tests
pass plain functions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: open a redeemed Run attempt's terminal from the agent pane (run.attach)

Rebuilt from #21305. The pane decodes run.attach (ids, a live trusted-carrier
ws/wss endpoint, the worker session) and, with the person's fresh gesture in
the pane, asks the App to open the attempt's terminal as a remote-terminal tab.
The App opens it only on a connected remote worker daemon whose session is the
one the ticket names; the route is never dialed and no token is accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* web: reap retained Run attempt VMs and accept worker artifact manifests

The landed Run API schedules a short terminal retention window after an
attempt finishes and its comment says a cron reaper destroys the VM after it,
but no route called reapExpiredCloudVmAttempts, so retained VMs were never
destroyed by it. Add /api/cron/run-attempt-retention (monitored, every
minute) and the worker artifacts route over the landed recordRunArtifact.
Port #21305's tests for the run model, provider view, worker auth and the
live-database route suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(web): expect a changed event retry to conflict in the Run route suite

#21634 made a retry with the same event id and different content a 409; the
suite ported from #21305 still expected the first event back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(web): a promoted attempt's VM is not claimed by the retention reaper

Fails on the base: claimExpiredAttemptCleanups claims any finished attempt
whose retention expired, including one promoted to a persistent runtime, so
the retention cron would destroy the runtime's live VM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(web): keep promoted runtimes' VMs out of the retention reaper

claimExpiredAttemptCleanups now skips attempts linked to a runtime: promotion
hands the attempt's VM to a persistent runtime without clearing the
retention window, so the new cron would destroy it. A VM the reaper finds
already gone now counts as cleaned instead of failing every minute and
starving older rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cmux-next): poll the current run and gate Attach on the Run view

The board rebuilds its replay session when the run id changes, reads the
latest snapshot without a stale closure (vp check's exhaustive-deps), takes
the session id from the view's placement and disables Attach when the view
says the attempt is not attachable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cmux-next): run.attach opens only on a connected Cloud machine

daemon(machine:) also resolves SSH hosts and paired servers; the ticket names a
Cloud VM, and the page's fields are not verified natively, so the lookup is
now Cloud-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: keep CmuxNextApp from growing with run.attach

The size cap refuses any source growth in CmuxNextApp. The run.attach closure and its
onRunAttach hook are joined into fewer lines, and four one-statement functions and three
guard blocks in the same two files are written on one line each. No behavior change.

Agent-Session: 01a15ba2-c7ad-7850-9e71-5b51f1c9ed9f

* cmux-next: run.attach's public init imports Foundation publicly

Base now builds CmuxNextAgentPane with InternalImportsByDefault, so AgentPaneRunAttach's public
init(params:now: Date = Date()) failed to compile (fleet compile-check 3476a896 at 208bdff).

Agent-Session: 01a15ba2-c7ad-7850-9e71-5b51f1c9ed9f

* test(web): the retention reaper claims an attempt on its own ephemeral runtime (failing)

Review of #22408: launchCloudVmAttempt gives every attempt an ephemeral runtime, so skipping any
attempt with a runtime_id skips every real Run VM and none is ever destroyed. The promoted-VM
test now inserts its runtime as persistent explicitly.

Agent-Session: 01a15ba2-c7ad-7850-9e71-5b51f1c9ed9f

* fix(web): the retention reaper skips only attempts promoted to a persistent runtime

Every launched attempt is attached to its own ephemeral runtime before its VM exists, so the
claim now left-joins cloud_runtimes and keeps an attempt whose runtime is ephemeral; the row lock
stays on run_attempts (FOR UPDATE cannot lock the nullable side of an outer join).

Agent-Session: 01a15ba2-c7ad-7850-9e71-5b51f1c9ed9f

* test(web): promotion is refused while the reaper holds the attempt's cleanup lease (failing)

Review of #22408: a promotion that commits after the reaper's claim but before destroyVm still
loses its VM.

Agent-Session: 01a15ba2-c7ad-7850-9e71-5b51f1c9ed9f

* fix(web): refuse to promote an attempt while the reaper is cleaning up its VM

Agent-Session: 01a15ba2-c7ad-7850-9e71-5b51f1c9ed9f

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant