Skip to content

Daemon: stash.restore is all or nothing (#20706) - #21943

Merged
teamleaderleo merged 11 commits into
feat-cmux-nextfrom
split/20706-stash-restore-atomic
Oct 19, 2026
Merged

teamleaderleo merged 11 commits into
feat-cmux-nextfrom
split/20706-stash-restore-atomic

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

stash.restore reopens members one at a time and only then commits their removal from the stash. When member N failed, members 1..N-1 stayed open and also stayed in the stash, so a retry opened them a second time. A restore is now all or nothing. If any member can't reopen, or the commit fails, every surface this call created is closed again and every member stays in the stash. The restore returns the original error, and a retry opens each page once.

  • Tracking what was opened. Surfaces are recorded the moment a reopen creates them (closed_history::recording_created/note_created). That catches a tab whose name or pin step then fails, and the starter tab of any workspace made for the pages (target new_workspace, or a here restore with no workspace at all).
  • How they close. The rollback closes them through the normal batch close as a session end, so nothing lands in Recently Closed, under a fresh key. It closes only surfaces that are still open. If another client closed one in the meantime and the batch refuses, it retries once without that surface. If the close still fails, it logs.
  • Starter close key. The new_workspace starter close now takes a fresh key per attempt. It used to derive the key from the request's key, so a retry after a failed attempt hit an idempotency conflict with the first attempt's receipt. That was the documented "known limitation". A replay of a committed restore returns before this point, so it's unaffected.
  • Spec. The stash.restore constraints and resource-api-v2.md now state the all-or-nothing contract. The SDK descriptors are regenerated.

Decision

Choice: roll back. The default rule is to roll back unless callers already depend on partial results, and none do.

Evidence (origin/feat-cmux-next after #21670):

  • The only references to stash.restore are the resource registry (resource.rs, wire_name.rs, page_access.rs, resource_router/owner.rs), the SDK descriptors (operation class only) and the app scope tables.
  • The generated app global types the result as StashRestoreResult = {stash_id, workspace_id, workspace_ids, screen_ids, tab_ids, remaining}, which has no per-member status.
  • MCP and the CLI deliberately leave stash ops out (cli/mcp/v2_tools.rs).
  • The macOS client and UI slices haven't landed, and no webview calls it.
  • The spec already promised a single commit: "Restored members leave the stash in the commit that stores the result."

Counterargument. A review subagent argued against rollback. Its points and what I did:

  1. Tabs created before a later step of their reopen fails would escape the rollback. Fixed: surfaces are recorded at creation, not when the reopen finishes.
  2. A batch close refuses the whole batch if one surface is already gone. Fixed: only surfaces still open are closed, with one retry.
  3. A workspace made for the pages, and its starter tab, would survive. Fixed: starter tabs are recorded too. A split pane made by split_closed_pane_again closes with its last tab, but the neighbor's split ratio may not come back exactly. That's cosmetic, and I accepted it.
  4. The derived starter-close key would make a failed new_workspace restore fail forever on that key. Fixed: the key is fresh per attempt.
  5. A retry loads pages 1..N-1 a second time, which can break one-time URLs. Not fixed. Stashed pages are ordinary saved URLs; one-time OAuth codes are already stripped by closed-history privacy. A partial result loads each page once, but it leaves the stash out of step with what's open unless every client handles per-member status.
  6. A member that always fails blocks "restore all", which is the strongest argument for a partial result. Not fixed. The client can restore the other members by id. A partial result would need a new result shape and would break the single-commit contract above, for callers that don't exist yet.

None of these makes a partial result better on correctness. Points 1 to 4 were real bugs in the first rollback version and are fixed here; 5 and 6 are tradeoffs, and I accept them.

Testing

  • cdff2a44 adds stash_restore_that_fails_partway_opens_nothing_and_keeps_every_member. It stashes three pages and breaks the middle member's record so it can't reopen. Then:
    • with targets here and new_workspace, the restore has to fail with tab and workspace counts unchanged and all 3 members still in the stash;
    • after the member is fixed, a retry has to open exactly 3 tabs and empty the stash.
  • The fix is 2312fe94 (rollback, starter key, spec) plus 233d0f2d (from the review: record surfaces at creation, skip ones already gone).
  • Red: at cdff2a44, hosted verify-cmux-tui-hosted.sh --filter stash_restore_that (run 39201789671) failed the test on Linux and macOS. Counts were (2, 1), expected (1, 1): the first page stayed open.
  • Green: at 233d0f2d, hosted --filter stash_ (run 39202184409) passed test (linux) and macOS lint and tests, 12/12 stash tests on each. The earlier run at 2312fe94 passed on Linux. Its macOS job never ran a test, because the runner hook refused while a fleet build waited for the host.
  • Final head: the hosted --filter stash_ run is in progress; I'll record the result here.
  • This Linux host can't build cmux-tui-core, so it compiles only in the hosted runs.
  • Merging current feat-cmux-next (eae2935) regenerated the SDK descriptors. It also regenerated cmux-app-host/generated/cmux-app.d.ts: base's copy lacks tab.update/pane fields (pinned, allow_tabs, marked_unread) that base's catalog already has, so gen-cmux-global.ts --check fails on base.
  • Locally: check-resource-api-boundary.py, check-spec-inventory.py, check-sdk-schema.py, sync-resource-api.py and gen-cmux-global.ts --check pass, and rustfmt --check is clean.

Freeze WINDOW areas: CORE (touches cmux-tui-core and the resource spec). No Cargo file changes, so not LOCK. The Air Blue coordinator merges this PR.

Changelog

Fixed: restoring a stash that fails partway no longer leaves pages open that a retry opens again; the restore is all or nothing.

🤖 Generated with Claude Code

teamleaderleo and others added 3 commits October 19, 2026 04:53
Regression for a restore whose second member cannot reopen: the first
page stays open and stays in the stash, so a retry opens it twice.
Covers target here and new_workspace. Fails on the current code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a member cannot reopen, or the restore's commit fails, the tabs
this call opened close again as a session end (never recorded in
Recently Closed) under a key of their own, together with the new
workspace's starter tab for target new_workspace. Every member stays in
the stash and the error is returned, so a retry opens each page once.
A rollback close that fails is logged.

The starter close now takes a fresh key per attempt instead of one
derived from the request's, so a retry after a failed attempt no longer
conflicts with the first attempt's receipt (a replay of a committed
restore returns before it).

The spec says so in the stash.restore constraints and resource-api-v2;
SDK descriptors are regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of the rollback:
- Surfaces are recorded the moment a reopen creates them
  (closed_history::recording_created / note_created), not when the
  reopen finishes, so a tab whose name or pin step then failed, and the
  starter tab of a workspace made for a page, close with the rest.
- The rollback closes only surfaces still open, and tries once more
  without any another client closed meanwhile, since a batch close
  refuses a surface that is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 19, 2026 04:59 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Oct 19, 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: 1e4f3bf2-3cf8-49c0-a1e6-6c4bd62f771c

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.

@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Receipt for 1f67cf6 (this PR's head; a new push replaces it)

  • Visual review: HQ feed card · gallery before/after runs
  • Checks: this head
  • 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=pr21943 -f ref=split/20706-stash-restore-atomic -f platform=macos -f backend_mode=local

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

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

@blacksmith-sh

This comment has been minimized.

…source API catalog hashes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 19, 2026 06:33 — with GitHub Actions Inactive
- regenerated cmux-tui/crates/cmux-app-host/generated/cmux-app.d.ts

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/39210649559
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-stash-restore-atomic

# Conflicts:
#	cmux-tui/bindings/cpp/.cmux-resource-api.json
#	cmux-tui/bindings/go/.cmux-resource-api.json
#	cmux-tui/bindings/java/.cmux-resource-api.json
#	cmux-tui/bindings/python/.cmux-resource-api.json
#	cmux-tui/bindings/rust/.cmux-resource-api.json
#	cmux-tui/bindings/typescript/.cmux-resource-api.json
#	cmux-tui/bindings/zig/.cmux-resource-api.json
@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 19, 2026 07:28 — with GitHub Actions Inactive
@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.

@blacksmith-sh

This comment has been minimized.

…-stash-restore-atomic

# Conflicts:
#	cmux-tui/crates/cmux-tui/src/cli/app.rs
#	cmux-tui/crates/cmux-tui/src/cli/app/tests.rs
@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 19, 2026 07:40 — with GitHub Actions Inactive
@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.

@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 19, 2026 19:33 — with GitHub Actions Inactive
@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable defect was established.

Summary

stash.restore now tracks pages and workspace starters, then closes them if reopening or the final commit fails.

  • Failed stash restores close opened pages and keep every member saved.

Acknowledged choices from teamleaderleo: closing a restored split may not recover the exact neighboring split ratio; retrying may load a URL again; one failing member blocks restoring the whole stash, while callers can select other member IDs. The description also explicitly states that a rollback close that still fails is logged.

Verification here was by code inspection; no tests were run.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Restore request] --> B{Committed result exists?}
  B -->|Yes| C[Return saved result]
  B -->|No| D[Track created pages and starters]
  D --> E[Reopen selected members]
  E -->|Success| F[Commit stash removal and result]
  F -->|Success| G[Return result]
  E -->|Failure| H[Close created pages still open]
  F -->|Failure| H
  H --> I[Return original error]
Loading

Reviews (1) · Last reviewed commit: "Merge remote-tracking branch 'origin/fea..." · Reviewed by Greptile

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Merging at 1f67cf6. A correctness review found nothing blocking, and the stash tests are green. The three reds don't come from this PR:

@teamleaderleo
teamleaderleo merged commit d8515a0 into feat-cmux-next Oct 19, 2026
@teamleaderleo
teamleaderleo deleted the split/20706-stash-restore-atomic branch October 19, 2026 20:49
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

teamleaderleo added a commit that referenced this pull request Oct 20, 2026
#22022)

* cmux-next: Swift daemon client for Stash (stash-v1), slice 3 of #20706

StashRecord and StateResourceClient+Stash call stash.create/list/restore/
delete/import (landed daemon side: #21670, #21943). DaemonCapabilities
gains stash-v1 in `optional`; daemon-capabilities.json follows. No UI:
the service, page, actions, sidebar item and New Tab link are slices 4-7.

BranchStateResourcesTests covers OneTab import, list and delete against
the same-tree daemon.

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

* chore(next): PR autofix at b048ed3

- regenerated Packages/macOS/CmuxNext/ci-target-graph.json

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/39212326369

* chore(next): PR autofix at c59b09e

- Packages/macOS/CmuxNext/ci-target-graph.json

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/39367796953

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: cmux-autofix[bot] <cmux-autofix[bot]@users.noreply.github.com>
teamleaderleo added a commit that referenced this pull request Oct 21, 2026
* cmux-next: Swift daemon client for Stash (stash-v1), slice 3 of #20706

StashRecord and StateResourceClient+Stash call stash.create/list/restore/
delete/import (landed daemon side: #21670, #21943). DaemonCapabilities
gains stash-v1 in `optional`; daemon-capabilities.json follows. No UI:
the service, page, actions, sidebar item and New Tab link are slices 4-7.

BranchStateResourcesTests covers OneTab import, list and delete against
the same-tree daemon.

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

* chore(next): PR autofix at b048ed3

- regenerated Packages/macOS/CmuxNext/ci-target-graph.json

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/39212326369

* cmux-next: Stash service and Stashed page, slice 4 of #20706

CmuxNextStash (StashService, StashHost) serves the Stashed page from this
Mac's daemon (stash-v1). CmuxNextHistory gets the page: StashPageModel,
StashPageView, StashPageHostView, StashedGroup, StashStrings and the
OneTab export parser. The daemon's stash changes on session.events now
reach the app (SessionStateMirror.Touched.stash, SessionStateStore
.stashChanges), so the page lists again when the CLI, MCP or another
window changes a stash. The App answers StashHost and registers the
Stashed top page; nothing opens it yet (actions, sidebar item and New Tab
link land in later slices).

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

* cmux-next: regenerate ci-target-graph.json on the merged head

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

* cmux-next: Stash review fixes

- An op's reload waits for the newest stash.list read, so a stash change
  event that starts a read mid-op no longer leaves the op returning with
  stale groups.
- OneTab titles are capped at 1024 UTF-8 bytes (the daemon's limit), not
  1024 characters, so a long non-ASCII title no longer refuses the import.
- StashService keeps its change watcher and cancels it in deinit.
- Rename the event-stream test to say what it covers.

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

* cmux-next: regenerate ci-target-graph.json on the merged head

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-74b7-74e2-a471-ad8e009b887f&host=cosmic-gray

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: cmux-autofix[bot] <cmux-autofix[bot]@users.noreply.github.com>

This branch was previously deployed

2 inactive deployments
artifacts — 1f67cf68 Deployed Oct 19, 2026 by teamleaderleo
cmux-tui-merge-tree — 1f67cf68 Deployed Oct 19, 2026 by teamleaderleo
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