Skip to content

fix(session): at startup, a local copy never overrides an SSH-owned workspace's own copy - #26098

Merged
Jinwoo-H merged 8 commits into
mainfrom
fix/ssh-startup-local-shadow
Oct 7, 2026
Merged

Jinwoo-H merged 8 commits into
mainfrom
fix/ssh-startup-local-shadow

Conversation

@Jinwoo-H

@Jinwoo-H Jinwoo-H commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​211 $\color{#cf222e}{\Huge{\mathbf{−}}}$​6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​205
Prod 5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​32 $\color{#cf222e}{\Huge{\mathbf{−}}}$​9 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​23

ELI5

Orca can hold two saved copies of an SSH workspace: an old one in the "local" bucket and the current one in that SSH host's own bucket. At startup the old copy won. Now, for a workspace whose repo lives on an SSH host, the SSH host's copy wins.

What Changed

Before: if the local bucket still held any terminal tab for an SSH workspace, startup used that stale copy and skipped the SSH bucket for the workspace. Users saw tabs they closed long ago come back as empty shells and lost live agent tabs. The SSH bucket's agent-resume records were left out of the restored session, so the first save deleted them from disk, and after a reboot those agents were no longer offered for resume.

After: the SSH bucket's tabs, active tab, layouts and agent-resume records are restored as saved. The next save writes the workspace to its SSH bucket, so the stray local tabs disappear from disk.

Mechanism: startup already folds each SSH bucket into the local session (adoptStrandedHostPartitionSession), filling gaps and skipping any workspace the local session holds terminal tabs for. Startup also already asks the repo list who owns each workspace an SSH bucket names. That check now also returns the workspaces it resolves to that bucket. Before the fold, the local rows for such a workspace are dropped, as long as the id is not claimed by another host and the SSH bucket holds tabs for it. The existing fold then runs unchanged and takes the workspace whole from the SSH bucket: tabs, layouts, agent-resume records, open files, browser tabs, even when a stale local tab shares an id with the live one.

  • Edge, kept on purpose: when the SSH bucket's tab row for the workspace is empty, local tabs still win (today's behavior). An empty row is not evidence the user closed them, so they are never wiped.
  • Ownership is per SSH target: a workspace the repo list places on one target is not affected by leftover rows another target's bucket holds for it.
  • Unsaved drafts: a local open file with an unsaved draft is kept when the folded result lacks that file, or holds it without a draft. A draft in the SSH bucket's own entry wins. Nothing else holds a draft, so this applies to every folded workspace.
  • The same owned-ids input is passed on main's upload path (persistedSessionForTarget), which publishes the session to the SSH host when the renderer sent none. Without it main could still upload the stale local copy to the host.

Unchanged: folder workspaces, repos the repo list cannot name yet, ids two hosts both claim, and workspaces the SSH bucket does not mention keep today's rule.

Why

The skip assumed the local copy was the live one. After #19572 the opposite is true for SSH workspaces: the renderer and the runtime both write an SSH workspace to its SSH bucket, so a local row for it is residue, either from builds before #19572 or written by relay reattach (#25616; #26088 stops that writer). Fixing only the writer leaves existing residue winning on every boot, so the read has to change.

Changing the one existing skip, rather than filtering the local copy out before the fold, keeps a single rule and reuses the fold's field handling (including its draft protection) instead of a second code path. Alternatives considered:

  • Keep the local copy but also carry the SSH bucket's resume fields. That fixes the lost records but still restores closed tabs and drops live ones.
  • Prefer the SSH bucket only when it has tabs. A second rule, and an SSH bucket that deliberately emptied its tabs would still lose to stale local tabs.
  • A one-time migration that rewrites stored data. More machinery; the normal save already removes the stray rows.

The open community PR #23394 targets the same rule with a broader carry-over approach.

Linked Issue

Part of #23390 — fixes the boot-time choice between the two copies (live tabs dropped, closed tabs restored). It does not change the separate request there to re-surface detached relay leases.
Part of #25616 — existing stray local rows no longer shadow the SSH bucket. #26088 stops new ones being written.

Visual Proof

N/A — this changes which saved copy startup restores, with no new UI. The Docker SSH e2e below is the before/after: on main the agent-resume record is gone from the SSH bucket after a restart; with this change it survives and the stray local tabs are cleaned up.

Testing

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

  • New e2e tests/e2e/ssh-startup-local-shadow.spec.ts (Docker SSH lane), run on macOS: connect to a real SSH target, open two terminals and quit. Then seed a stray local copy (one live tab under an old title, plus a long-closed tab) and an agent-resume record in the SSH bucket, and relaunch. Fails on main (resume record erased from the SSH bucket after the first save). Passes with this change (SSH tabs restored, resume record kept, stray local tabs gone from disk after a save).

  • Unit (workspace-session-ssh-partition-round-trip.test.ts): the SSH bucket's tabs, active tab and resume records win over a stray local copy; a stale local tab sharing the SSH tab's id does not keep its layout; a stray local copy's unsaved draft is kept beside, or over a clean copy of, the SSH bucket's own open files; tabs the SSH bucket keys by workspace key still supersede the local copy (all fail without the fix). Ownership suite: a leftover copy in a second SSH bucket does not remove the owner's rows, in both read orders; an SSH bucket with no tabs keeps local tabs; a local copy whose repo the repo list cannot name keeps today's behavior. Existing draft-protection and contested-id tests pass.

  • Unit (remote-workspace-target-session-export.test.ts): main's upload fallback publishes the SSH bucket's tabs, not a stray local copy (fails without the owned-ids input).

  • pnpm run test:e2e:terminal-layout-parity -- --base origin/main --head HEAD: PASS (9 scenarios, no differences). That suite has no SSH scenario yet, so it shows only that local layouts are unchanged.

  • pnpm tc, pnpm run check:code-quality:changed, targeted vitest (workspace-session, adoption, remote-workspace: 853 tests).

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • SSH: this is the SSH case. A workspace counts as SSH-owned only from the local repo catalog plus the SSH bucket naming it, so no network access and no connection state is involved. When the host is offline, the same copy is chosen.
  • Data safety: unsaved editor drafts in a stray local copy are kept, even when the SSH bucket has its own open files for that workspace. Rows for workspaces the catalog cannot attribute are never dropped.
  • Cross-platform: renderer-only logic, no paths or platform checks.
  • Backwards compatibility: reads existing stored data as-is. No schema or wire change.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

…rkspace's own copy

Rows the local partition holds for a workspace whose repo the catalog places on
an SSH target are residue (pre-#19572 builds, relay reattach). Startup used to
keep them whenever they held a tab and skip the SSH partition for that
workspace, dropping live tabs, restoring closed ones and erasing agent-resume
records on the first save. Boot hydration now drops those local rows (keeping
open files and visit recency) so adoption takes the SSH partition whole.
@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Startup hydration now passes catalog-resolved SSH workspace IDs to stranded-partition adoption. Adoption can select SSH state for those workspaces when local rows also exist, while rejecting empty host rows and retaining unsaved drafts missing from adopted open-file rows. Unit tests cover conflicting state and unresolved ownership. A Docker-backed restart test checks restored tabs and persisted partition state. The E2E configuration routes that test to the dedicated Docker SSH lane.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to 52b0f

Startup now prefers the SSH workspace's own session over a stale local copy. Some edge cases remain. The SSH tabs can stay hidden when the workspace key is stored in canonical form. An unsaved local edit can be lost when the SSH copy has the same file open without changes. Resolve these before merging.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.29% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 9 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing a stale local copy from overriding an SSH-owned workspace at startup.
Description check ✅ Passed The description is complete and detailed. It includes the user impact, implementation details, rationale, linked issues, testing evidence, platform considerations, and checklist status. It also explai…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · 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.

…stead of pre-filtering local

Replaces the sentinel-partition split with one rule in adoption: the base's
terminal tabs no longer keep out the partition the repo catalog places the
workspace on (contested ids keep today's behavior).

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0533df69-6a3b-4ed1-82a5-a312716881f3
📥 Commits

Reviewing files that changed from the base of the PR and between 693ac16 and 46a26d8.

📒 Files selected for processing (3)
  • src/renderer/src/lib/workspace-session-host-hydration.ts
  • src/renderer/src/lib/workspace-session-ssh-partition-round-trip.test.ts
  • src/shared/workspace-session-stranded-partition-adoption.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.

Comment thread src/shared/workspace-session-stranded-partition-adoption.ts Outdated
Comment thread src/shared/workspace-session-stranded-partition-adoption.ts Outdated
… unsaved local drafts survive

Addresses review: a stale local tab sharing an id with the SSH tab kept its
layout and resume records (fill-only), and a replaced open-files row dropped
local-only unsaved drafts.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8babc4eb-0455-43cf-bae7-b4308d9d03d5
📥 Commits

Reviewing files that changed from the base of the PR and between 46a26d8 and 92b2efc.

📒 Files selected for processing (2)
  • src/renderer/src/lib/workspace-session-ssh-partition-round-trip.test.ts
  • src/shared/workspace-session-stranded-partition-adoption.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

Comment thread src/shared/workspace-session-stranded-partition-adoption.ts Outdated
Addresses review: an owned workspace the SSH partition holds no tabs for keeps
today's rule, so its empty tab row cannot wipe the local tabs.

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Trigger the dedicated Docker-SSH job for… · ci-e2e-job-selection.mjs:27-28

config/scripts/ci-e2e-job-selection.mjs:27-28
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Trigger the dedicated Docker-SSH job for ssh-startup-local-shadow.spec.ts.

selectPrE2eSpecs includes the changed spec, but selectGeneralE2eSpecs removes it because it is Docker-SSH-only. The changed workspace-session files do not match either SSH source route, so ssh_source_changed is false. The dedicated job condition does not include this spec. Therefore, this PR has no CI job that invokes the runner for the new regression.

Suggested fix
       contains(inputs.test_files, 'tests/e2e/ssh-stale-resume-execution-host-scope.spec.ts') ||
+      contains(inputs.test_files, 'tests/e2e/ssh-startup-local-shadow.spec.ts') ||
       contains(inputs.test_files, 'tests/e2e/ssh-terminal-window-wake-stale-grid-repro.spec.ts') ||

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d0003feb-0324-4387-b3ba-56cd2dd3c903
📥 Commits

Reviewing files that changed from the base of the PR and between 92b2efc and 50ab7f1.

📒 Files selected for processing (2)
  • src/renderer/src/lib/workspace-session-ssh-partition-round-trip.test.ts
  • src/shared/workspace-session-stranded-partition-adoption.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/renderer/src/lib/workspace-session-ssh-partition-round-trip.test.ts
  • src/shared/workspace-session-stranded-partition-adoption.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

…ish path passes owned ids

Simplifies the rule: for a workspace the catalog places on this SSH host,
uncontested and with host tabs, the base's rows are dropped and the existing
gap-fill adoption runs unchanged; unsaved local drafts survive. One resolver
says which workspace each scoped entry belongs to, shared by the census, the
drop and adoption. The upload path (persistedSessionForTarget) now passes
owned ids too, so main cannot publish the stale local copy to the host.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a00de8eb-052d-4604-b088-6baf077fb038
📥 Commits

Reviewing files that changed from the base of the PR and between 50ab7f1 and 52b0f28.

📒 Files selected for processing (3)
  • src/main/ipc/remote-workspace-target-session-export.test.ts
  • src/main/ipc/remote-workspace-target-session-export.ts
  • src/shared/workspace-session-stranded-partition-adoption.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.

Comment thread src/shared/workspace-session-stranded-partition-adoption.ts
Comment thread src/shared/workspace-session-stranded-partition-adoption.ts Outdated
Comment thread src/shared/workspace-session-stranded-partition-adoption.ts
…s a clean host entry

Addresses review: a host tab row under a workspace key now counts toward
superseding the local copy, and a local unsaved draft for a path the host
holds clean is kept instead of dropped.
…g names

A workspace homed on one SSH target with leftover rows in another target's
partition no longer has the owner's adopted rows removed by the leftover's
pass.
@Jinwoo-H
Jinwoo-H merged commit e05c69f into main Oct 7, 2026
44 checks passed
Jinwoo-H added a commit that referenced this pull request Oct 8, 2026
…space, as before the core

#26098 (on main) relies on the window's first save erasing a stray local copy of
a workspace the repo catalog places on an SSH target. Window saves now keep
main's rows, so that copy stayed. In an owner partition main keeps only the
rows of workspaces homed there (or of unknown home); residue homed elsewhere is
the window's to keep or drop, as before. Fixes ssh-startup-local-shadow.
ph47s74x pushed a commit to mercurai/orca that referenced this pull request Oct 8, 2026
…orkspace's own copy (stablyai#26098)

* fix(session): at startup a local copy never overrides an SSH-owned workspace's own copy

Rows the local partition holds for a workspace whose repo the catalog places on
an SSH target are residue (pre-stablyai#19572 builds, relay reattach). Startup used to
keep them whenever they held a tab and skip the SSH partition for that
workspace, dropping live tabs, restoring closed ones and erasing agent-resume
records on the first save. Boot hydration now drops those local rows (keeping
open files and visit recency) so adoption takes the SSH partition whole.

* refactor(session): let adoption take a catalog-owned SSH workspace instead of pre-filtering local

Replaces the sentinel-partition split with one rule in adoption: the base's
terminal tabs no longer keep out the partition the repo catalog places the
workspace on (contested ids keep today's behavior).

* fix(session): an SSH-owned workspace's records win under shared keys; unsaved local drafts survive

Addresses review: a stale local tab sharing an id with the SSH tab kept its
layout and resume records (fill-only), and a replaced open-files row dropped
local-only unsaved drafts.

* fix(session): an SSH copy with no tabs never replaces local tabs

Addresses review: an owned workspace the SSH partition holds no tabs for keeps
today's rule, so its empty tab row cannot wipe the local tabs.

* refactor(session): drop a superseded local copy before adoption; publish path passes owned ids

Simplifies the rule: for a workspace the catalog places on this SSH host,
uncontested and with host tabs, the base's rows are dropped and the existing
gap-fill adoption runs unchanged; unsaved local drafts survive. One resolver
says which workspace each scoped entry belongs to, shared by the census, the
drop and adoption. The upload path (persistedSessionForTarget) now passes
owned ids too, so main cannot publish the stale local copy to the host.

* fix(session): match host tab rows by workspace id; a local draft beats a clean host entry

Addresses review: a host tab row under a workspace key now counts toward
superseding the local copy, and a local unsaved draft for a path the host
holds clean is kept instead of dropped.

* fix(session): scope catalog-owned ids to the ssh partition the catalog names

A workspace homed on one SSH target with leftover rows in another target's
partition no longer has the owner's adopted rows removed by the leftover's
pass.

(cherry picked from commit e05c69f)
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