Skip to content

fix(GAP-03): stop offline reconnect from resurrecting server-closed tabs - #74

Open
svc-orca[bot] wants to merge 23 commits into
mainfrom
fix/gap-03-offline-client-reconciliation
Open

svc-orca[bot] wants to merge 23 commits into
mainfrom
fix/gap-03-offline-client-reconciliation

Conversation

@svc-orca

@svc-orca svc-orca Bot commented Oct 14, 2026

Copy link
Copy Markdown

ELI5

If you close a tab on the server while another Orca window is offline, that offline window used to bring the closed tab back when it reconnected. This change teaches reconnect to trust the server’s current tab list after a real offline gap, without treating a normal quit-and-relaunch as “offline” and wiping still-open tabs.

What Changed

Before

  • An offline client kept stale SSH partition rows on disk.
  • On reconnect/hydration it re-adopted those rows, so server-closed tabs reappeared.
  • A later write could also round-trip parked/declined rows back onto the host after a live snapshot had already answered.
  • An ordinary restart (still connected at last shutdown) could be mistaken for offline reconnect and drop live tabs.

After

  • Boot hydration declines confirmed-host worktrees with empty base tabs only when that SSH target was not connected at last shutdown (reconciledWorktreeIdsForHost + wasTargetConnectedAtLastShutdown).
  • Declined rows are parked (including tab/pane dependents), never deleted — leak, not kill.
  • Write path serializes per-host partition writes; pre-testimony writes that carry testimony-invalidatable parked shadow are discarded once live host testimony arrives.
  • Shadow withholding is limited to SSH terminal fields; non-terminal rows (e.g. editor drafts) survive.
  • Ordinary restart keeps still-open tabs; unknown shutdown list can infer connection from base remoteSessionIdsByTabId.

Mechanism

  • New/extended modules: host write queue, shadow-testimony filter, reconciliation attribution, stranded-partition parking, shared testimony predicate.
  • Persistence/hydration/contention wire those pieces; app-shell + mobile close bridge supply live testimony state to the queue.

Why

GAP-03 / stablyai#22038 (and related stablyai#21341): stale offline client cache must not override server closes on reconnect.

Alternatives considered:

  • Delete declined host rows on boot — rejected (SSH boundary: unverifiable ≠ dead; leak over kill).
  • Decline on every boot when catalog confirms host — rejected (ordinary restart false positive dropped live tabs).
  • Blind write-through of parked shadow until next boot — rejected (race re-injects closed tabs after live snapshot).

Linked Issue

Fixes stablyai#22038
Related: stablyai#21341, stablyai#22333 (documented concurrent-edit park boundary)

Visual Proof

N/A — session hydration/persistence behavior; no UI chrome change. Covered by unit tests + docker offline-reconnect harness (not run in this triage pass).

Testing

  • Automated tests added/updated
  • I manually tested these changes locally (agent triage; unit suite only)

Commands run in triage:

ORCA_BACKGROUND_LAUNCH=1 pnpm test \
  src/renderer/src/lib/workspace-session-host-write-queue.test.ts \
  src/renderer/src/lib/workspace-session-host-shadow-testimony.test.ts \
  src/renderer/src/lib/workspace-session-host-offline-reconnect.test.ts \
  src/renderer/src/lib/workspace-session-host-write-race.test.ts \
  src/renderer/src/lib/workspace-session-host-rapid-reconnect-cycles.test.ts \
  src/renderer/src/lib/remote-workspace-host-testimony.test.ts \
  src/renderer/src/lib/workspace-session-host-contention.test.ts
# → 7 files, 57 tests passed

Platforms exercised by tests: unit (Linux agent host). Docker e2e harness present (tests/e2e/gap-03-offline-reconnect-tab-resurrection.spec.ts) not executed in triage.

AI Disclosure

ACO structural triage + PR authoring via agent (xai grok) on worktree orca-gap03-tab-reopen.

Review

ACO triage (2026-10-14) summary

  • Graph: missing (rg-heuristic)
  • P0 open: none — race/decline/cycle paths closed by design + tests
  • P1: main persistedSessionForTarget still adopts without reconciledWorktreeIds (export ≠ boot decline)
  • P1: default carriesParkedShadowRows=true is a future-caller contract hazard (production sets explicit flag)
  • Branch was ~1542 commits behind origin/main at PR open — expect rebase/CI noise

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/remote session partition ownership is the core surface. Cross-platform: persistence paths are shared renderer/shared; Windows/macOS not separately re-run here.

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) — triage ran targeted unit tests only

@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 86a68557-eb1f-4865-84e9-6b0c26d0de9e

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.

@svc-orca

svc-orca Bot commented Oct 14, 2026

Copy link
Copy Markdown
Author

ACO structural triage — GAP-03 offline client reconciliation

Run: gap03-tab-reopen-2026-10-14 · prompt_version 1.2-quick · head e236c8f2380
Workspace: /data/projects/.worktrees/orca-gap03-tab-reopen · branch fix/gap-03-offline-client-reconciliation
Graph: .aco/graph.db missing · riley_kg unavailable → rg-heuristic (non-AST)
Verification: 7 unit files / 57 tests passed (write-queue, shadow-testimony, offline-reconnect, write-race, rapid-reconnect, testimony, contention)

Blast radius (high level)

Module Callers
write-queue persistence only
shadow-testimony persistence + contention (one-way; cycle broken)
reconciliation hydration only
hydration startup hydration
adoption/parking hydration; main persistedSessionForTarget (no reconciled decline)
testimony predicate write-queue, shadow, terminal authority

Findings checklist (requested changes)

P0

  • Pre-testimony write overwrites live snapshot — closed: discard only if carriesParkedShadowRows + live hostHasAnsweredForTarget (write-queue.ts)
  • Ordinary restart declines live SSH tabs — closed: empty reconciledWorktreeIds when wasConnectedAtLastShutdown (reconciliation.ts / hydration.ts)
  • Declined tab drops layout/session dependents — closed: parking walks tab/pane keys (stranded-partition-parking.ts)
  • Shadow wipes non-terminal fields — closed: SSH_TERMINAL_TESTIMONY_FIELDS only
  • contention ↔ shadow circular import — closed: contention → shadow only (tip commit)

P1 — still open / reviewer attention

  • Main export path lacks GAP-03 decline — src/main/ipc/remote-workspace-target-session-export.ts persistedSessionForTarget calls adoptStrandedHostPartitionSession with contested/foreign only; no reconciledWorktreeIds. Boot hydration declines; export/publish may still surface stale declined host rows. Decide: intentional export semantics vs share boot gate.
  • carriesParkedShadowRows default true — fail-safe over-discard if a future caller omits the flag. Production persistence sets explicit flag from hostsWithTestimonyInvalidatableShadow. Consider fail-closed default false + require opt-in, or a type-level required field.
  • Branch lag — ~1542 commits behind origin/main at open; rebase before merge; re-run targeted tests post-rebase.

P2

  • No AST graph in workspace (caller map is rg-only)
  • Docker e2e tests/e2e/gap-03-offline-reconnect-tab-resurrection.spec.ts not executed in this pass

Changes from triage

None (review-only; no remaining clear safe P0 patch).

Residual risk

Non-AST callers · main export parity · e2e docker not run · rebase churn

{
  "schema_version": "aco-triage-run/v1",
  "run_id": "gap03-tab-reopen-2026-10-14",
  "timestamp": "2026-10-14T08:55:21Z",
  "prompt_version": "1.2-quick",
  "head_sha": "e236c8f2380b58290613195ac55a31c1742f92fa",
  "pr": "https://github.com/LesleyMurfin/orca/pull/74",
  "graph_mode": "rg-heuristic-non-ast",
  "p0_open": 0,
  "p1_open": 3,
  "verification": "57 unit tests passed",
  "changes_made": []
}

Reproduces the additive-merge bug in adoptStrandedHostPartitionSession
(src/shared/workspace-session-stranded-partition-adoption.ts): when a
worktree's tabs are all closed server-side while a client is offline, the
client's base tabsByWorktree row for that worktree becomes empty, so
workspacesTheBaseOwns() no longer protects it and the stale cached ssh:*
partition's closed tabs are unconditionally re-adopted into the merged
session on reconnect.

- RED: closed tabs must not reappear after reconnect (fails today)
- GREEN: a genuinely offline-created tab (never synced) must survive
  reconnect -- the stablyai#12721 non-regression constraint the downstream fix
  must not break

Fixes stablyai#22038. No fix code included; implementation of
reconcileLocalTabsWithServerManifest()/hasPendingReconnectionHandshake is
a separate downstream task.
@svc-orca

svc-orca Bot commented Oct 14, 2026

Copy link
Copy Markdown
Author

ACO follow-up — P1 main-export decline closed

Head: 898f4aab4d2 (fix(GAP-03): stop publish fallback from re-exporting declined offline tabs)

What landed

  1. Main persistedSessionForTarget now applies the same confirmed + wasTargetConnectedAtLastShutdown → reconciledWorktreeIds decline as renderer boot.
  2. Shared src/shared/workspace-session-shutdown-connection.ts (hydration re-exports).
  3. Renderer upload passes session: buildWorkspaceSessionPayload(...) on setForConnectedTargets so primary path does not rely on disk fallback alone.
  4. Tests in ssh-host-partition-session-export.test.ts: offline omit-session must not re-publish stale confirmed tabs; connected-at-shutdown still adopts stranded (SSH host session partitions are written but never read — tab lists round-trip as empty and remote sync permanently deletes them stablyai/orca#12721 + modern stamp).

Verification (this workspace)

ORCA_BACKGROUND_LAUNCH=1 pnpm test \
  ssh-host-partition-session-export.test.ts \
  workspace-session-host-offline-reconnect.test.ts \
  workspace-session-host-write-queue.test.ts \
  workspace-session-host-write-race.test.ts \
  workspace-session-host-contention.test.ts
→ 5 files, 50 tests passed

Checklist update

P0

  • (unchanged) race / ordinary-restart / parking / shadow / cycle — closed

P1

  • Main export path lacks GAP-03 decline — closed in 898f4aab4d2
  • carriesParkedShadowRows default true still a future-caller contract note (no prod bug)
  • Branch still lags origin/main substantially — rebase before merge

P2

  • Graph still missing (rg-heuristic)
  • Docker e2e not run here

No further code change from this follow-up (parallel commit already matched the intended fix; duplicate local WIP discarded).

adoptStrandedHostPartitionSession's workspacesTheBaseOwns() only ever
protected a worktree from stranded-partition adoption when the base
held a non-empty tabsByWorktree row. Once every tab for a worktree
closed server-side while a client was offline, the base's row went
empty/absent, so the repair no longer protected it and unconditionally
folded in whatever tabs were still sitting in the client's stale
on-disk ssh:<targetId>/runtime cache from before it disconnected --
resurrecting closed tabs on reconnect and round-tripping them back out
via persistWorkspaceSessionByHost to the host partition and every
other connected client (stablyai#22038, revive_labs#962).

Fix: sshPartitionCatalogAttribution (extracted into a new
workspace-session-host-reconciliation.ts module alongside
unownedSessionKeys, to keep workspace-session-host-hydration.ts under
this repo's 300-line file budget) now also tracks, per SSH host, the
worktree ids the repo catalog positively and *currently* confirms
belong to exactly that host -- via the row's own executionHostId
field, not a bare connectionId fallback. That fallback already treats
a legacy catalog row as resolved, which is exactly the pre-stablyai#12723
shape the one-shot gap-fill repair exists for, so it stays on the
unconditional path. adoptStrandedHostPartitionSession takes the
confirmed set as reconciledWorktreeIds: for one of these ids, an
absent tabsByWorktree row on the base is no longer read as an
unverifiable stablyai#12721 gap -- it's the server's current truth of zero
tabs -- so a host row that still names real (non-empty) tabs is
declined rather than adopted. An id the catalog also marks foreign for
another host (residue proving a genuine migration) is exempted, so a
freshly-moved connection's real tabs still adopt normally. Declining
never destroys anything: the id simply drops out of
adoptedWorkspaceIds, so partitionRowsTheWriteWontReturn parks the
stale row in the write-side shadow the same way a contested id already
does.

One regression fixture (workspace-session-ssh-partition-ownership.test
.ts's "keeps a declined row..." case) gave its sibling worktree a bare
`local: session({})` with no tabsByWorktree entry at all for the
sibling -- structurally identical to the GAP-03 bug shape itself. Gave
it the same known-worktree explicit-empty-row shape already used
throughout the round-trip suite (`strandedPartitions()`), which is
what that test's own "sibling is adopted and routes back" premise
actually depends on.

Tests:
- workspace-session-host-offline-reconnect.test.ts: RED -> GREEN
  (closed tab no longer resurrected); stablyai#12721-safety scenario (offline-
  created, never-synced tab) stays GREEN.
- workspace-session-ssh-partition-ownership.test.ts,
  workspace-session-ssh-partition-round-trip.test.ts,
  workspace-session-host-split.test.ts,
  workspace-session-partition-owner.test.ts,
  ssh-host-partition-session-export.test.ts: all still GREEN, zero
  regressions.
- Also re-ran workspace-session-host-contention.test.ts,
  workspace-session-host-persistence.test.ts and
  main/ipc/remote-workspace.test.ts (other direct callers/dependents
  of the touched functions): all green.
- 115/115 across all 9 files above.

No PR opened; stops at a locally-ready, committed branch per this
slice's constraints.
…rness

Adds a two-launch (quit+relaunch) Playwright harness that reproduces the
GAP-03 repro exactly: pair a client to a Dockerized SSH host, open N tabs,
quit the client, close M tabs' local base row while leaving the SSH
host-partition mirror stale (simulating 'closed server-side while offline'),
relaunch, and measure resurrection plus the stablyai#12721 non-regression.

Modeled on run-ssh-docker-e2e.mjs (wrapper shape), golden-quit-relaunch-session.spec.ts
and persisted-session-production-upgrade.spec.ts (two-launch + on-disk fixture
injection), and the docker-ssh-relay-* helpers already used by
ssh-lost-kill-tab-resurrection.spec.ts.

Also hardens docker-ssh-relay-target.ts: some hosts' tmp directories carry a
default ACL that overrides ssh-keygen's normal 0600 permissions, which sshd
then refuses under StrictModes. One chmodSync call fixes it unconditionally
for every SSH-relay Docker e2e spec, not just this one.

BEFORE-state (pre-fix, commit 36beb65f6b) RED evidence captured separately;
see revive_labs specs/22038-gap-03-docker-cross-site-test-harness.md.

Slice A of the GAP-03 before/after test-battery ticket: harness + baseline
only. AFTER-state measurement, Calgary-site run, and aggregate report are
follow-up slices.
…ises the fix

Slice A's harness (commit 8b26787bdc) measured 2/2 tabs resurrected on
both pre-fix and (unverified) post-fix code, and offlineDraftSurvived:
false even on pre-fix code -- an anomaly flagged for the next agent.
Root-caused and fixed three independent harness defects, none in
product code:

1. "Closed tabs" fault injection wrote workspaceSession.tabsByWorktree
   [worktreeId] = [] (a filter of an already-empty array) instead of
   deleting the key. adoptStrandedHostPartitionSession's GAP-03 decline
   path keys its verdict on Object.hasOwn(base.tabsByWorktree,
   workspaceId), and hasOwn is true for a key holding [] exactly as
   much as a populated array -- silently defeating the fix regardless
   of which code was under test. Also switched CLOSE_COUNT/TAB_COUNT
   defaults to close every opened tab (2/2): the fix's own decline
   logic is all-or-nothing (see its doc comment), so a scenario that
   leaves any survivor tab in the worktree never reaches the
   reconciledWorktreeIds path at all, pre- or post-fix, and proves
   nothing about the regression.

2. The stablyai#12721 offline-draft tab was injected only into legacy
   tabsByWorktree, never into unifiedTabs/tabGroups/tabGroupLayouts/
   activeGroupIdByWorktree/activeTabIdByWorktree. The first launch's
   own quit-time write already populates unifiedTabs+tabGroups, so
   buildHydratedTabState takes the unified branch on the second launch
   and enumerates worktrees from unifiedTabs alone -- a legacy-only row
   is invisible there, and even where tabsByWorktree is swept for
   reconciliation, a legacy row with no live PTY and no unified twin is
   exactly what getOrphanTerminalIds deletes as an orphan on the same
   boot. This was the flagged anomaly: offlineDraftSurvived: false
   regardless of which product commit was under test, because the tab
   never reached a store the modern hydration path reads from.

3. Both SSH targets' activeConnectionIdsAtShutdown entries survived
   into the fault-injected profile, so the second launch auto-
   reconnected live SSH and pulled a fresh PTY snapshot straight off
   the still-alive Docker container (applyDirectSshRemoteWorkspaceSnapshot
   / mergeDirectSshRemoteWorkspaceSession) -- a code path GAP-03 never
   touches, since this fixture never actually kills the "closed" PTYs
   on the container. That live resync silently overwrote whatever the
   boot-time fetchWorkspaceSessionWithRuntimeHostOwners merge had
   correctly decided. Clearing the field keeps the second launch on
   the one path the fix targets: persisted-state hydration at boot,
   confirmed by temporary instrumentation showing
   adoptStrandedHostPartitionSession's own "adoptable" set already
   correctly emptied out before this fix (root-caused, then reverted).

Measured clean before/after against the corrected harness by
temporarily reverting to 36beb65f6b (fix's own parent) and back:

  BEFORE (36beb65f6b): resurrectedCount 2/2, offlineDraftSurvived true
  AFTER  (129d11ad4d): resurrectedCount 0/2, offlineDraftSurvived true

offlineDraftSurvived is true on BOTH runs, confirming the stablyai#12721
anomaly was a harness defect throughout, not a real regression on
either commit. All 64 tests across the fix's own directly-affected
unit-test files still pass unchanged.

Also updated run-offline-reconnect-tab-resurrection-docker.mjs to stop
overriding the spec's own TAB_COUNT/CLOSE_COUNT defaults.
…ing PR stablyai#968 scenarios

reconciledWorktreeIdsForHost applied unconditionally to every catalog-confirmed
worktree, regardless of whether this client's SSH connection to that host was
ever actually interrupted. Since an SSH-hosted worktree's tabsByWorktree row
never routes to the 'local' base partition in the first place
(buildHostSessionRouting always sends it to ssh:<targetId>), base.tabsByWorktree
is absent for EVERY confirmed worktree on EVERY boot -- closed or not. Combined
with remote-repo-registration.ts stamping the modern executionHostId field on
every SSH repo it registers (not a rare legacy shape), this meant an ordinary
quit + relaunch with nothing closed by anyone dropped every still-open tab on
every SSH worktree whose repo carries the modern stamp -- the opposite of
GAP-03's own goal, and a severe regression the existing 115-test suite, the
Docker e2e harness (which always closes every opened tab, CLOSE_COUNT=TAB_COUNT
by design) and the 4-client live verification never exercised, because none of
them ever tested a worktree with a genuine SURVIVOR tab through the confirmed
attribution path.

Fix: gate reconciledWorktreeIdsForHost on whether this host's SSH target was
actually connected at the client's last shutdown (activeConnectionIdsAtShutdown
-- the same field use-app-startup-hydration.ts already reads to decide which
targets to auto-reconnect at boot). A target still listed there was never
disconnected; its host-partition mirror is this client's own current state
written moments before quitting, not a stale pre-offline cache, so an absent
base row for it was never evidence of anything and must not be read as "the
server's confirmed zero." A target NOT listed there is the actual GAP-03
precondition, and still declines exactly as before.

Verification of PR stablyai#968's 4 deferred pr-agent scenarios, RED-then-GREEN against
this fix's own parent (36beb65f6b) and current HEAD, per-scenario:

- Concurrent Active Edits: the achievable case (host not disconnected -- see
  the restart regression above) now adopts correctly. The harder case (a live
  sibling worktree while genuinely offline) is a DOCUMENTED, PROVEN
  architectural limitation: reconciledWorktreeIdsForHost gates per host, not
  per worktree, so a worktree the base has never cached is indistinguishable
  from one whose tabs were genuinely closed while offline. Both are safely
  parked (never deleted -- attachHostSessionShadow/partitionRowsTheWriteWontReturn),
  just not adopted this boot. Resolving this needs a live server signal (the
  upstream draft's own manifestEpoch/handshake proposal); this fix has none.
- Split Layout Pruning: proven safe by construction -- GAP-03's decline is
  per-worktree across every field, not per-tab, so a declined worktree carries
  no rows in tabsByWorktree, unifiedTabs, tabGroups OR tabGroupLayouts. No
  orphaned split leaf can survive because nothing about that worktree is
  adopted at all.
- Write Race Protection: CONFIRMED, REPRODUCIBLE, UNFIXED bug. A wake-triggered
  persistWorkspaceSessionByHost never touches a host partition with nothing
  else routed to it (no race). The instant anything else legitimately writes
  to that same host partition in the same call (a sibling worktree gaining a
  tab), attachHostSessionShadow unconditionally re-attaches EVERY parked row
  for that host -- including one GAP-03 just correctly declined -- and it
  round-trips back to the server. A naive "drop instead of park" mitigation
  was prototyped and reverted: it closes this race for a genuinely-closed
  worktree but is blind to the Concurrent-Active-Edits false positive, turning
  a recoverable non-adoption into an unrecoverable delete of another client's
  live tabs. Answers PR stablyai#968's deferred question directly: hasPendingReconnectionHandshake
  is NOT unnecessary -- it (or an equivalent live signal) is the only thing
  that would close this race without risking data loss.
- Rapid Sleep/Wake Cycles: verified convergent and non-corrupting across 5
  consecutive boot cycles against an unchanged on-disk profile, with no
  cumulative resurrection and no duplicate adoption.

Also adds a real Docker cross-site e2e scenario (ordinary quit + relaunch,
nothing closed, connection still active at shutdown) proving the restart
regression fix end-to-end, not just at the unit level.

New/changed tests: workspace-session-host-offline-reconnect.test.ts (+7),
workspace-session-host-write-race.test.ts (new, 2), workspace-session-host-
rapid-reconnect-cycles.test.ts (new, 2). All 115 pre-existing GAP-03 tests
still pass unchanged; 124 total across the 11 touched files.

No PR opened; stays on this pushed branch pending owner sign-off, per this
slice's constraints (nothing goes upstream until fully verified).
…e 1)

- Add remote-workspace-host-testimony.ts with RemoteWorkspaceTestimonyState and hostHasAnsweredForTarget
- Add remote-workspace-host-testimony.test.ts covering unhydrated, conflict, and non-conflict synced states
- Update workspace-terminal-host-authority.ts to compose RemoteWorkspaceTestimonyState and delegate to hostHasAnsweredForTarget (behavior-neutral)
- Add workspace-session-host-shadow-testimony.ts implementing shadowRowsTheHostHasNotAnswered
- Export WORKTREE_KEYED_FIELDS from workspace-session-host-contention.ts
- Extend HostPersistenceState with RemoteWorkspaceTestimonyState and wire shadow filtering into splitWorkspaceSessionForWrite in workspace-session-host-persistence.ts
- Add write-race protection test, retitle existing test, and update docstring verdict in workspace-session-host-write-race.test.ts
… decline loop

CodeRabbit finding: the reconciled-decline loop in adoptStrandedHostPartitionSession
read host.tabsByWorktree[workspaceId] with the bare normalized workspaceId, but
tabsByWorktree keys may be stored in canonical worktree:${workspaceId} form (the
type's own doc comment says so). The adoption loop a few lines down already
normalizes every key before comparing; the decline loop didn't, so a canonical-keyed
host row silently defeated GAP-03's whole decline and adopted stale tabs it should
have declined.

Red test added first (workspace-session-host-offline-reconnect.test.ts): a host
partition with tabsByWorktree keyed as `worktree:${WORKTREE_ID}` under the
confirmed-disconnected-at-shutdown GAP-03 precondition. Failed before the fix
(both tabs survived, just under the canonical key instead of the bare one, so the
naive same-key assertion would have missed it -- the test walks every surviving
tab across every key). Fix: walk host.tabsByWorktree entries and normalize each
key via normalizeWorkspaceSessionKeyToWorkspaceId before matching against
workspaceId, matching the adoption loop's own pattern.

8/8 in the touched suite green; 75/75 across the 7 directly-dependent partition/
reconciliation suites green.
… worktree rows

CodeRabbit finding: partitionRowsTheWriteWontReturn only walked WORKTREE_KEYED_FIELDS
for a declined worktree, leaving tab-keyed (terminalLayoutsByTabId,
remoteSessionIdsByTabId, localOnlyScrollbackByTabId) and pane-keyed
(terminalPtyIncarnationsByPaneKey) rows unparked. If another workspace caused a
write to the same SSH partition before host testimony arrived, the write retained
the declined tabs but dropped their layout/session rows, leaving inconsistent state.

Fix:
1. partitionRowsTheWriteWontReturn now maps each tab- and pane-keyed entry to its
   worktree via buildWorktreeIdByTabId / worktreeIdForPaneKey and parks rows whose
   owning worktree was declined.
2. attachHostSessionShadow and shadowRowsTheHostHasNotAnswered now iterate
   PARKABLE_HOST_SESSION_FIELDS (worktreeKeyed, tabKeyed, paneKeyed) so dependent
   rows are restored alongside tabsByWorktree before testimony and withheld after.
3. Added red-first regression test in workspace-session-host-write-race.test.ts
   proving terminalLayoutsByTabId and remoteSessionIdsByTabId survive the write.
…n activeConnectionIdsAtShutdown is undefined

CodeRabbit finding: when session.activeConnectionIdsAtShutdown is undefined
(older persisted sessions, interrupted shutdowns), hydration defaulted to an
empty set and passed false ("was disconnected") into reconciledWorktreeIdsForHost.
This caused every confirmed worktree on that host to be reconciled and declined,
dropping still-open tabs for sessions where shutdown status was merely unknown
rather than explicitly disconnected.

Fix:
1. wasTargetConnectedAtLastShutdown helper in workspace-session-host-hydration.ts:
   when activeConnectionIdsAtShutdown is defined, check membership as before.
   When undefined, check session.remoteSessionIdsByTabId for any surviving remote
   PTY session belonging to targetId (matching the established pattern in
   workspace-session-reconnect-targets.ts and ssh-pane-connect-gate.ts).
2. Added red-first regression test in workspace-session-host-offline-reconnect.test.ts
   proving tabs survive when activeConnectionIdsAtShutdown is undefined but
   remoteSessionIdsByTabId contains a session for the target.

9/9 in touched suite green, 55/55 across 5 related hydration/reconciliation suites green.
… live snapshot arrival

CodeRabbit finding 3: an api.set/patch call generated while hostHasAnsweredForTarget
was false (so declined tabs were attached by the shadow) could be delivered AFTER
the live snapshot landed and testimony flipped true. Nothing ordered in-flight
writes against the live snapshot's arrival, so a delayed pre-testimony write could
overwrite the authoritative state and resurrect closed tabs.

Fix:
1. enqueueHostPartitionWrite in workspace-session-host-persistence.ts serializes
   writes per host partition and tracks monotonic write generations.
2. When a write was prepared without host testimony, it checks right before dispatch
   whether testimony has since arrived or a newer write was dispatched for that host;
   if so, the stale pre-testimony write is discarded instead of overwriting the
   authoritative snapshot.
3. Added red-first regression test in workspace-session-host-write-race.test.ts
   proving a deferred pre-testimony write queued behind an in-flight write does not
   overwrite the live snapshot when testimony arrives before delivery.

55/55 tests across 5 related persistence/contention/write-race suites green.
…imony arrives

Pullfrog finding 6: shadowRowsTheHostHasNotAnswered dropped ALL non-folder parked
rows once hostHasAnsweredForTarget became true. Because direct-SSH snapshots only
project terminal state (remote-workspace-snapshot-apply.ts:201), non-terminal state
such as openFilesByWorktree editor drafts is never reported by or superseded by the
SSH host snapshot. Withholding those non-terminal rows caused subsequent partition
writes to erase offline-created editor drafts from disk.

Fix:
1. Define SSH_TERMINAL_TESTIMONY_FIELDS in workspace-session-host-shadow-testimony.ts:
   only terminal-scoped fields that SSH snapshots actually manage are withheld by host
   testimony. Non-terminal fields like openFilesByWorktree survive testimony arrival.
2. Added unit test in workspace-session-host-shadow-testimony.test.ts verifying
   openFilesByWorktree survives even after testimony lands for that target.
3. Updated test comment in workspace-session-host-offline-reconnect.test.ts to
   accurately state the testimony boundary.
- Extract `enqueueHostPartitionWrite` and write chain state to `workspace-session-host-write-queue.ts`
  to bring `workspace-session-host-persistence.ts` within the 300-line budget (252 lines).
- Deduplicate imports and add `SAFETY:` comments to all type assertions across touched files
  to satisfy `check-changed-code-quality` rules.
- Add RED-first unit tests for `workspace-session-host-write-queue` verifying FIFO serialization,
  independent host concurrency, generation supersession, and test state reset.
- Verified: 138/138 tests passing across all 13 session suites, typecheck clean, and 0 findings
  in `check-changed-code-quality`.
…hydration, and adoption

- Guard host write generation advancement by replaceable/explicit-generation flag,
  preventing non-replaceable partial patches from superseding durable persists.
- Support live testimony state resolution via getLiveState thunk and global provider
  in enqueueHostPartitionWrite to discard stale pre-testimony writes upon testimony arrival.
- Normalize tab- and pane-keyed worktree ids in partitionRowsTheWriteWontReturn and
  resolve worktree claims in attachHostSessionShadow.
- Preserve stablyai#12723 empty placeholder rows in base tabsByWorktree during stranded adoption.
- Extract partitionRowsTheWriteWontReturn to workspace-session-stranded-partition-parking.ts
  to honor max-lines budget without disabling rules.
- Guarantee Docker cleanup in e2e spec via nested finally blocks.
- Approved by riley-pr-agent, riley-pr-check, and riley-the-maintainer.
…adow rows and clamp CLOSE_COUNT

- Track whether host partition slices actually receive parked shadow rows in attachHostSessionShadow.
- Propagate carriesParkedShadowRows through splitWorkspaceSessionForWrite to enqueueHostPartitionWrite.
- Gate pre-testimony write discarding on carriesParkedShadowRows, preventing data loss for live local edits and delta patches.
- Add regression test ensuring queued pre-testimony writes without parked shadow rows run when live testimony arrives.
- Safely parse TAB_COUNT and clamp CLOSE_COUNT to TAB_COUNT in Docker e2e harness.
…ale docker runner env

- Narrow SSH_TERMINAL_TESTIMONY_FIELDS in workspace-session-host-shadow-testimony.ts to terminal fields managed by direct-SSH snapshots, preserving client-only state such as localOnlyScrollbackByTabId.
- Export isShadowRowSubjectToHostTestimony as a single shared authority for testimony invalidation.
- Track hostsWithTestimonyInvalidatableShadow in attachHostSessionShadow, ensuring writes carrying only preserved shadow entries (folders, editor drafts) are not discarded upon testimony arrival.
- Add required typescript(consistent-type-assertions) safety comment on splitWorkspaceSessionForWrite call in patchWorkspaceSessionByHost.
- Remove dead ORCA_GAP03_CLOSE_COUNT forwarding in config/scripts/run-offline-reconnect-tab-resurrection-docker.mjs.
…imony modules

- Move PARKABLE_HOST_SESSION_FIELDS to src/shared/workspace-session-host-field-ownership.ts alongside other ownership field sets.
- Re-export PARKABLE_HOST_SESSION_FIELDS from workspace-session-host-contention.ts for backwards compatibility.
- Import PARKABLE_HOST_SESSION_FIELDS directly from shared in workspace-session-host-shadow-testimony.ts and merge duplicate imports.
- Satisfies oxlint audit:code-quality:native with deny-warnings.
… tabs

Primary setForConnectedTargets omitted session, so main persistedSessionForTarget
re-adopted stale ssh-partition tabs without reconciledWorktreeIds. Pass the
renderer session on upload and apply the same confirmed+shutdown decline on the
main fallback; regression covers omit-session offline vs connected restart.
@svc-orca

svc-orca Bot commented Oct 14, 2026

Copy link
Copy Markdown
Author

Maintainer follow-up — main merge + clean workspace

Head: 3f9678234fb
Action: merged origin/main into fix/gap-03-offline-client-reconciliation to clear GitHub CONFLICTING / dirty state (~1542 commits behind).

Merge resolution (GAP-03 × main)

Area Resolution
patchWorkspaceSessionByHost Keep GAP-03 write-queue + carriesParkedShadowRows / live testimony; adopt main { localWrite, written } return
use-app-session-persistence Destructure { localWrite, written } + getLiveTestimonyState + pass session on upload
Reconciliation attribution ownedSessionKeysByHostId (main supersede) and confirmedSessionKeysByHostId (GAP-03 decline)
Adoption Both ownedSessionKeys supersede + reconciledWorktreeIds decline
Main export fallback confirmed + shutdown decline; also passes ownedSessionKeys
Export GAP-03 tests createSqliteTestStore (main Store requires profile-state authority)

Verification

8 files / 61 tests passed
(write-queue, shadow, offline-reconnect, write-race, rapid-reconnect, export, testimony, contention)

Checklist

  • Working tree clean; branch pushed
  • Conflicts with base resolved via merge commit
  • No open review line comments to address
  • P0 structural items closed (prior ACO)
  • P1 main-export decline closed
  • Full CI on fork may still be thin (CodeRabbit only previously)
  • Optional residual: carriesParkedShadowRows default true contract; empty-[] vs absent base key test pin (G1 from test audit)

Merge remains Lesley-only.

Pre-testimony write discard is opt-in: only callers that know the write
carries parked shadow rows pass true (persistence uses
hostsWithTestimonyInvalidatableShadow). Default false keeps ordinary
patches from being dropped when live testimony lands mid-flight.

Also: GAP-03 decline reads originalBase for hasBaseKey so main's
ownedSessionKeys superseding of empty stablyai#12723 placeholders cannot
false-trigger offline decline; export GAP-03 tests use createSqliteTestStore.
@LesleyMurfin
LesleyMurfin force-pushed the fix/gap-03-offline-client-reconciliation branch from 3f96782 to 5a89d75 Compare October 14, 2026 09:16
Parked unifiedTabs/tabGroups/tabGroupLayouts/activeGroupIdByWorktree must drop
from the write shadow once the host has answered, same as declined tabs — else
post-testimony persist restores ghost split chrome with no tabs.
@svc-orca

svc-orca Bot commented Oct 14, 2026

Copy link
Copy Markdown
Author

Maintainer pass (post-rebase)

State: rebased onto origin/main · head 5a89d7580e4 · mergeable=MERGEABLE / mergeStateStatus=CLEAN · working tree clean.

ACO P1 checklist

  • Main export path GAP-03 decline — persistedSessionForTarget applies shutdown-gated reconciledWorktreeIds from catalog confirmedSessionKeys (same signal as hydration). Renderer publish passes session so the fallback is not the only path.
  • carriesParkedShadowRows default — now false (opt-in). Persistence still sets the flag from hostsWithTestimonyInvalidatableShadow. Unit coverage: default-omitted / explicit-true / explicit-false.
  • Branch lag — rebased; 21 commits ahead of main, 0 behind.

Verification

75 targeted unit tests passed across write-queue, shadow-testimony, offline-reconnect, write-race, rapid-reconnect, testimony, contention, ssh export, partition ownership.

No open review threads. Ready for Lesley merge review (agents do not merge).

@svc-orca

svc-orca Bot commented Oct 14, 2026

Copy link
Copy Markdown
Author

Correctness follow-up (reviewer P1s)

Head: b578e36fa92

Addressed

  1. fix(session): at startup, a local copy never overrides an SSH-owned workspace's own copy stablyai/orca#26098 ownedSessionKeys + GAP-03 reconciledWorktreeIds — both kept on tip (hydration + adoption + export). Complementary, not substitutes.
  2. carriesParkedShadowRows default false — already on branch (5a89d7580e4).
  3. Split-layout chrome under testimony — unifiedTabs / tabGroups / tabGroupLayouts / activeGroupIdByWorktree added to SSH_TERMINAL_TESTIMONY_FIELDS + regression test so post-answer writes do not restore ghost groups after declined tabs.

Verification

8 files / 63 tests passed (GAP-03 unit surface).

Still residual (P2, non-blocking)

  • Main single-partition export lacks multi-host migratedSessionKeys strip (renderer has it).
  • In-flight sole write not cancelled mid-perform if testimony lands during I/O.

Workspace clean. Merge Lesley-only when GitHub mergeable settles.

Precompute base tabsByWorktree workspace ids and normalize reconciled ids
before the offline decline loop. Drops the worktree: dual-key scan and the
unused hydration re-export of wasTargetConnectedAtLastShutdown.
@svc-orca

svc-orca Bot commented Oct 14, 2026

Copy link
Copy Markdown
Author

Follow-up elegance pass

  • Head 28a96c5daea (on top of split-layout chrome withhold b578e36).
  • Decline loop: one normalized base-key set + normalized reconciled ids (no worktree: dual scan).
  • Dropped unused hydration re-export of wasTargetConnectedAtLastShutdown.
  • 76 targeted unit tests passed.
  • Still MERGEABLE / CLEAN. Agents do not merge.

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.

[Bug]: Offline paired desktop clients retain stale tabs in orca-data.json and re-inject them upon reconnect

1 participant