Repository navigation
fix(workspace): hibernate detached sessions on rehydrate - #258
Merged
Merged
Conversation
Previously every entry in `ownedIds` got a full PTY + mitmdump spawn on
app startup via rehydrate's Promise.all. `ownedIds` lumps three things
together — visible tile leaves, detached dispatch parking, and buried
panes — and treats them all as live. For users who park dispatch agents
("I'll come back to this later"), this meant every restart resurrected
the entire detached pool as live processes. Field data: 49 sessions
persisted, 9 visible in the UI, 40 in detachedSessions — last launch
fork-bombed the machine with 40 claude + 40 mitmdump processes, loadavg
peaked at 906 on an 8-core M3.
Split the conflated concept into two sets in sessionOwnership.ts:
collectOwnedSessionIds → metadata-preservation set (unchanged).
Tile leaves + detached + buried. Used by
pruneSessionOwnership to decide what
survives a save cycle.
collectLiveProcessIds → rehydrate-spawn set (new).
Tile leaves ONLY. The strict subset the
user is actually going to be exposed to on
launch.
rehydrate.ts now filters the Promise.all by liveProcessIds. Hibernated
sessions still round-trip through state — their metadata is seeded into
freshSessions under the original persisted sessionId before the spawn
loop runs, so the dispatch list and buried-panes UI keep rendering and
a future wake-on-attach action can resume them by providerSessionId.
Side fixes pulled along by the new contract:
- `sanitizeRemappedNode` now checks the new `spawnedIds` set instead
of `freshSessions` membership. freshSessions includes hibernated
metadata seeds now, so the old check would have promoted hibernated
ids into visible tile leaves with no backing process.
- `buildRemappedDetachedSessions` / `buildRemappedBuried` fall back
to the original sessionId when idMap has no entry, instead of
silently dropping the record. Hibernated sessions never go through
the spawn loop, so they never get an idMap entry — without this
fallback the dispatch parking pool would have emptied on every
restart.
- `expectedSessions` for the autosave-gating `complete` flag is now
`liveProcessIds.size` (not `ownedIds.size`). Otherwise `complete`
would be permanently false for anyone with parked agents and
autosave would silently disable for them forever.
Tests in scripts/test-session-ownership.ts pin the contract:
- tile leaves ARE live processes
- detached are NOT
- buried are NOT
- empty tabs ⇒ no live processes even with detached/buried present
Non-goals (separate PRs):
- Wake-on-attach UI affordance for hibernated sessions
- Orchestration MCP parent→child reaping (duplicate providerSessionId
bug, found during diagnosis)
- TTL/LRU eviction of stale detached records
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three sibling instances of the same "hibernated session silently disappears" bug, plus a back-compat-safe shape on remapSessionMetaRelationships. 1. remapSessionMetaRelationships now takes a `knownSessionIds` set in addition to idMap, and falls back to the original id when a relationship endpoint is in the known set but not in idMap. Without this, hibernated→hibernated links (parked orchestration parent and child) silently lost their linkedParentId/orchestrationParentId/ orchestrationRootId on rehydrate even though both endpoints survived in freshSessions. buildRemappedSessions passes the freshSessions key set as knownSessionIds; the argument is optional so any future caller outside rehydrate keeps the strict idMap-only behavior. 2. buildRemappedPinnedSessionIds applies the same fallback shape used in buildRemappedDetachedSessions / buildRemappedBuried. A pin pointing at a hibernated agent now round-trips through rehydrate instead of disappearing. 3. reloadAgentSessions in src/renderer/.../hook/actions/session.ts now filters by collectLiveProcessIds, not collectOwnedSessionIds. This is the sibling code path the reviewer caught: toggling dangerous mode used to call killSession + spawnSession on every owned claude/codex session, which would have re-introduced the original fork-bomb for users with parked dispatch agents — every mode toggle would resurrect N hibernated agents as live processes. The pickOwnedSessions call further down (state-prune use, not spawn-filter use) deliberately keeps collectOwnedSessionIds. Test extensions in scripts/test-session-ownership.ts pin the new relationship-remap contract: spawned->spawned remap via idMap; hibernated->hibernated preserved via knownSessionIds; orphaned endpoint dropped; back-compat call shape (no knownSessionIds) keeps the strict pre-PR behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
detachedSessions— rehydrate spawned all of them, producing 40claude --resume <uuid>+ 40mitmdumpprocesses in ~3 seconds and a load average of 906 on an 8-core M3.collectOwnedSessionIdslumped tile leaves + detached + buried into one set, and rehydrate used it as the spawn filter. Parking a dispatch agent for later use ("detach") permanently added a backend process to every future launch.collectOwnedSessionIdskeeps its current meaning (what metadata to preserve across save/load). NewcollectLiveProcessIdsanswers the strictly smaller question rehydrate actually needs: "what tile leaves is the user currently exposed to?" Rehydrate'sPromise.allis now filtered by the live set. Hibernated session metadata still round-trips through state so the dispatch list keeps rendering and a future wake-on-attach action can resume byproviderSessionId.Commits
fix(workspace): hibernate detached sessions on rehydrate— the core split + the three structural side-fixes (sanitizeRemappedNode now keys onspawnedIds, detached/buried remappers fall back to original id,expectedSessionsswitches toliveProcessIds.sizeso autosave-gating stays correct).fix(workspace): address PR review — hibernated link/pin/reload paths— three sibling instances of the same "hibernated session silently disappears" pattern caught in review:remapSessionMetaRelationshipstakes aknownSessionIdsset; hibernated→hibernated parent/root links survive instead of being silently dropped.buildRemappedPinnedSessionIdsfalls back to the original id when the target is infreshSessions(parked-agent pins survive).reloadAgentSessionsfilters bycollectLiveProcessIdsinstead ofcollectOwnedSessionIds— toggling dangerous mode no longer resurrects parked agents.Telemetry note
The
workspace.rehydrate.complete.expectedSessionsperf-mark semantic narrows from "all owners" to "live-process count only". The previous total is recoverable asexpectedSessions + hibernatedSessions(the new field added in the same perf mark). Dashboards reading the metric should be migrated to the sum if they care about absolute totals.Files changed
src/renderer/src/workspace/sessionOwnership.tscollectLiveProcessIds. Rewrite module header to explain the live-vs-owned split.src/renderer/src/workspace/hook/persistence/rehydrate.tsliveProcessIds. Seed hibernated metadata intofreshSessionsunder original sessionId. AddspawnedIdsset sosanitizeRemappedNodedistinguishes "process started" from "metadata exists". Fall back to original sessionId in detached/buried/pinned remap.expectedSessions=liveProcessIds.size.remapSessionMetaRelationshipsgains optionalknownSessionIdsfor hibernated→hibernated link preservation.src/renderer/src/workspace/hook/actions/session.tsreloadAgentSessionsfilters byliveProcessIdsinstead ofownedIds(sibling bug).scripts/test-session-ownership.tscollectLiveProcessIdsreturns only tile leaves; relationship remap preserves hibernated→hibernated links viaknownSessionIds; orphaned endpoints still drop; back-compat withoutknownSessionIdsstays strict.Pushback on review item #1 (hibernated runtime status)
The review claimed the runtimes seed loop "iterates all
freshSessionskeys" and would mis-state hibernated sessions asstarted/inputReady: true. The actual loop at rehydrate.ts:344 iteratesidMap.entries(), notfreshSessions. Hibernated sessions never get added toidMap(the seed loop only writes tofreshSessions), so they get no runtime entry at all. UI lookups fall back toemptyRuntime()which returnsprocessStatus: 'idle',inputReady: false(workspaceState.ts:594, 596) — the correct defaults for a non-running session. The wake-on-attach PR can rely on this without a separate'hibernated'enum value.What this is NOT (deliberate non-goals — separate PRs)
workspace.jsonwith duplicateproviderSessionIds (twosessionIdkeys pointing at one provider UUID). That's a separate spawn/cleanup bug and warrants its own fix.Test plan
npm run test:session-ownership— passes with new assertions (live-process contract + relationship remap fallback)npm run test:review-fixes— all green excepttest:claude-proxy-api-error-releasewhich fails onmaintoo (unrelated; submodule not initialized in this worktree)npx tsc --noEmit -p tsconfig.web.json— error count unchanged frommain; zero new errors and zero in edited filesdetachedSessionspool; confirm only the visible-tab sessions get a PTY + mitmdump, and that detached entries remain visible in the dispatch list (just sleeping).🤖 Generated with Claude Code