Skip to content

fix(resource-manager): never destroy a session Orca cannot prove is idle (#8459) - #10893

Merged
OrcaWin merged 2 commits into
mainfrom
session-liveness-unbound-safety
Jul 27, 2026
Merged

OrcaWin merged 2 commits into
mainfrom
session-liveness-unbound-safety

Conversation

@Jinwoo-H

Copy link
Copy Markdown
Contributor

Fixes #8459.

What was happening

Resource Manager's "Kill orphan terminals" decided a session was an orphan from the absence of a renderer binding, then force-killed it with no prompt.

Absence of a binding is not evidence a session is idle. Two cases where it is routinely wrong:

  • During restore, the renderer's binding map is legitimately empty while workspaceSessionReady is already true.
  • Deferred SSH sessions never appear in it at all — restore records them in deferredSshSessionIdsByTabId and skips reattach while the relay is disconnected.

Both describe sessions that are alive. Live agent sessions were destroyed this way, and an agent's work is not recoverable.

The rule

Only positive evidence authorizes destruction. Three gaps closed:

1. Ownership was discarded at the IPC boundary. PtyProcessInfo carries agentSessionOwners, but pty:listSessions narrowed each row to {id, cwd, title} — so the renderer never saw the one fact that proves work is running. It now reports hasAgentOwner, typed once in src/shared/pty-listed-session.ts so the main handler, both preload surfaces, and the renderer can't drift.

2. Deferred SSH sessions weren't in the binding index. buildResourceSessionBindingIndex consulted live PTYs, tab wake hints, and layouts — but not deferredSshSessionIdsByTabId. Sessions whose tab no longer exists are still ignored, so a stranded id can't read as liveness.

3. The count and the kill set were computed separately. The button rendered countUnboundDaemonSessions(...) while the handler ran its own sessions.filter(...). Two expressions that must agree but nothing made them. Both now call selectUnboundDaemonSessions, and a test asserts they stay identical.

The single-row path had the same defect. handleKillSession skipped confirmation whenever bound was false — the same unproven classification. requiresKillConfirmation now also holds for agent-owned sessions, and snapshot-derived rows carry ownership across from the daemon list rather than defaulting to false.

Verification

Every guard mutation-tested — reverted individually, confirmed the mutation applied, confirmed specific tests fail:

Mutation Result
hasAgentOwner always false at IPC 1 test fails
deferred-SSH binding loop removed 2 tests fail
hasAgentOwner short-circuit removed from selector 2 tests fail
requiresKillConfirmation → session.bound only 1 test fails
snapshot rows report hasAgentOwner: false 1 test fails

E2E through a real quit/relaunch — tests/e2e/resource-manager-unbound-session-safety.spec.ts starts a real terminal, quits the app so the detached daemon keeps owning the PTY (the #8459 precondition), relaunches, and asserts the live session is bound once restore reports ready.

That spec was itself mutation-tested: removing hasAgentOwner from the IPC payload and rebuilding makes it fail with pty:listSessions dropped hasAgentOwner across the real IPC boundary. It fails for the right reason, not vacuously.

Suites: 603 tests across status-bar + pty.test.ts; 5,871 across status-bar, pty IPC, store, and shared. pnpm run typecheck clean across all three projects. oxlint exits 0 with no findings in touched files. max-lines ratchet, reliability gates, and locale parity all pass. resource-usage-warm-reattach.spec.ts still passes.

Scope

No new user-facing strings, so no locale changes. No UI layout or styling changes — the only visible difference is that agent-owned and deferred-SSH sessions stop being counted as orphans, and killing one now asks first. Cross-platform: no platform-specific code paths touched. SSH is the primary case this fixes.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b137bc97-5aeb-473a-ba09-7afe5c834c35

📥 Commits

Reviewing files that changed from the base of the PR and between f8726b2e2e03653415333a4f7634ad38fe43e0a3 and 53a1db4.

📒 Files selected for processing (20)
  • src/main/ipc/pty.test.ts
  • src/main/ipc/pty.ts
  • src/preload/api-types.ts
  • src/preload/index.ts
  • src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx
  • src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx
  • src/renderer/src/components/status-bar/mergeSnapshotAndSessions.test.ts
  • src/renderer/src/components/status-bar/mergeSnapshotAndSessions.ts
  • src/renderer/src/components/status-bar/resource-session-bindings.test.ts
  • src/renderer/src/components/status-bar/resource-session-bindings.ts
  • src/renderer/src/components/status-bar/resource-session-classification-parity.test.ts
  • src/renderer/src/components/status-bar/resource-session-inventory.test.ts
  • src/renderer/src/components/status-bar/resource-session-kill-confirmation.test.ts
  • src/renderer/src/components/status-bar/resource-session-kill-confirmation.ts
  • src/renderer/src/components/status-bar/resource-usage-merge-types.ts
  • src/renderer/src/components/status-bar/resource-usage-open-slices.test.ts
  • src/renderer/src/components/status-bar/resource-usage-open-slices.ts
  • src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx
  • src/shared/pty-listed-session.ts
  • tests/e2e/resource-manager-unbound-session-safety.spec.ts
🚧 Files skipped from review as they are similar to previous changes (19)
  • src/renderer/src/components/status-bar/resource-session-inventory.test.ts
  • src/renderer/src/components/status-bar/resource-session-kill-confirmation.test.ts
  • src/renderer/src/components/status-bar/resource-session-classification-parity.test.ts
  • src/preload/api-types.ts
  • src/preload/index.ts
  • src/renderer/src/components/status-bar/resource-session-kill-confirmation.ts
  • src/main/ipc/pty.ts
  • src/shared/pty-listed-session.ts
  • src/renderer/src/components/status-bar/resource-usage-merge-types.ts
  • src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx
  • src/renderer/src/components/status-bar/resource-usage-open-slices.test.ts
  • src/renderer/src/components/status-bar/mergeSnapshotAndSessions.ts
  • src/renderer/src/components/status-bar/resource-usage-open-slices.ts
  • src/renderer/src/components/status-bar/resource-session-bindings.test.ts
  • src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx
  • src/main/ipc/pty.test.ts
  • src/renderer/src/components/status-bar/resource-session-bindings.ts
  • src/renderer/src/components/status-bar/mergeSnapshotAndSessions.test.ts
  • tests/e2e/resource-manager-unbound-session-safety.spec.ts

📝 Walkthrough

Walkthrough

PTY session listings now use a shared PtyListedSession type containing three-valued agent ownership evidence. The renderer propagates this metadata into unified session rows, includes deferred SSH bindings in classification, excludes protected sessions from cleanup, and requires confirmation unless ownership absence is authoritative. Unit, IPC, and end-to-end tests cover these behaviors.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the change, but it omits required template sections for Screenshots, Testing, AI Review Report, Security Audit, and Notes. Rewrite the PR description to match the repository template and add the missing sections, even if some are short or marked No visual change.
Docstring Coverage ⚠️ Warning Docstring coverage is 27.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: preventing unsafe session destruction when ownership can't be proven.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@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

🧹 Nitpick comments (1)
src/renderer/src/components/status-bar/resource-session-bindings.ts (1)

8-14: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Condense repeated ownership-policy comments.

Keep the invariant, but remove repeated incident history and implementation walkthroughs.

  • src/renderer/src/components/status-bar/resource-session-bindings.ts#L8-L14: reduce to a concise description of known-live deferred SSH bindings.
  • src/renderer/src/components/status-bar/resource-session-bindings.ts#L87-L102: retain the selector’s safety invariant in one short comment.
  • src/renderer/src/components/status-bar/resource-session-kill-confirmation.ts#L3-L10: retain only the confirmation rule and reason.

As per coding guidelines, comments must be concise, limited to non-obvious information, and preferably one line.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dd1bb0ad-a65f-4cae-b534-7ba88cb67e39

📥 Commits

Reviewing files that changed from the base of the PR and between b96c2f0 and ff62d45.

📒 Files selected for processing (19)
  • src/main/ipc/pty.test.ts
  • src/main/ipc/pty.ts
  • src/preload/api-types.ts
  • src/preload/index.ts
  • src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx
  • src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx
  • src/renderer/src/components/status-bar/mergeSnapshotAndSessions.test.ts
  • src/renderer/src/components/status-bar/mergeSnapshotAndSessions.ts
  • src/renderer/src/components/status-bar/resource-session-bindings.test.ts
  • src/renderer/src/components/status-bar/resource-session-bindings.ts
  • src/renderer/src/components/status-bar/resource-session-inventory.test.ts
  • src/renderer/src/components/status-bar/resource-session-kill-confirmation.test.ts
  • src/renderer/src/components/status-bar/resource-session-kill-confirmation.ts
  • src/renderer/src/components/status-bar/resource-usage-merge-types.ts
  • src/renderer/src/components/status-bar/resource-usage-open-slices.test.ts
  • src/renderer/src/components/status-bar/resource-usage-open-slices.ts
  • src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx
  • src/shared/pty-listed-session.ts
  • tests/e2e/resource-manager-unbound-session-safety.spec.ts

OrcaWin added 2 commits July 27, 2026 11:34
…dle (#8459)

Resource Manager decided a session was an "orphan" from the absence of a
renderer binding, then force-killed it with no prompt. Absence of a binding is
not evidence a session is idle — during restore the binding map is legitimately
empty, and deferred SSH sessions never appear in it at all. Live agent sessions
were destroyed this way, losing unrecoverable work.

Three gaps, one rule: only positive evidence authorizes destruction.

- `pty:listSessions` dropped `agentSessionOwners` at the IPC boundary, so the
  renderer could not see the one fact that proves work is running. It now
  reports `hasAgentOwner`, typed once in `shared/pty-listed-session.ts` so the
  main handler, both preload surfaces, and the renderer cannot drift.
- The binding index ignored `deferredSshSessionIdsByTabId` — sessions restore
  knows are live on an SSH host but has not reattached. No other binding source
  can see them.
- The bulk-kill handler filtered sessions separately from the button's count,
  so the set killed could differ from the set advertised. Both now call
  `selectUnboundDaemonSessions`.

The single-row kill path had the same defect: it skipped confirmation whenever
`bound` was false. `requiresKillConfirmation` now also holds for agent-owned
sessions, and snapshot-derived rows carry ownership across from the daemon list
rather than reporting `false`.
…bsence

Adversarial review of the previous commit found it committed the same class of
error it was fixing: it collapsed "no agent owns this" and "this provider cannot
tell me" into one boolean `false`, and both destructive paths read that as proof.

A daemon generation below the claim protocol, an older SSH relay, or the
in-process local fallback all list no owners for a session that may well have
one. `pty.ts` already encodes the rule at :613 — "only providers that serialize
claims may make listing absence authoritative" — and the new IPC row ignored it.
So after upgrading with a legacy daemon still holding a live agent terminal,
bulk cleanup would have destroyed it: exactly #8459, one layer down.

`hasAgentOwner: boolean` is now `agentOwnership: 'present' | 'absent' | 'unknown'`,
derived via `providesAgentSessionOwnerListings`. Only `absent` authorizes
destruction, so `unknown` protects and confirms.

Second defect, found independently by four review lenses: the deferred-SSH
bindings reached the bulk selector but not `mergeSnapshotAndSessions`, because
the merge call site re-listed the binding fields instead of reusing the object.
A deferred SSH session therefore rendered `bound: false`, and its single-row kill
skipped confirmation while bulk cleanup correctly spared it. The call site now
spreads `resourceSessionBindings`, and a parity test fails if any binding field
is re-listed inline — the drift itself is now impossible to reintroduce quietly.

The e2e ownership assertion was also weak: it checked only that a boolean
arrived. It now asserts the exact arm, and that the live local provider reports
`absent` rather than `unknown`, so a degenerate all-unknown implementation fails.
@OrcaWin
OrcaWin force-pushed the session-liveness-unbound-safety branch from f8726b2 to 53a1db4 Compare July 27, 2026 18:34
@OrcaWin
OrcaWin merged commit 05603a2 into main Jul 27, 2026
4 checks passed
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…dle (stablyai#8459) (stablyai#10893)

* fix(resource-manager): never destroy a session Orca cannot prove is idle (stablyai#8459)

Resource Manager decided a session was an "orphan" from the absence of a
renderer binding, then force-killed it with no prompt. Absence of a binding is
not evidence a session is idle — during restore the binding map is legitimately
empty, and deferred SSH sessions never appear in it at all. Live agent sessions
were destroyed this way, losing unrecoverable work.

Three gaps, one rule: only positive evidence authorizes destruction.

- `pty:listSessions` dropped `agentSessionOwners` at the IPC boundary, so the
  renderer could not see the one fact that proves work is running. It now
  reports `hasAgentOwner`, typed once in `shared/pty-listed-session.ts` so the
  main handler, both preload surfaces, and the renderer cannot drift.
- The binding index ignored `deferredSshSessionIdsByTabId` — sessions restore
  knows are live on an SSH host but has not reattached. No other binding source
  can see them.
- The bulk-kill handler filtered sessions separately from the button's count,
  so the set killed could differ from the set advertised. Both now call
  `selectUnboundDaemonSessions`.

The single-row kill path had the same defect: it skipped confirmation whenever
`bound` was false. `requiresKillConfirmation` now also holds for agent-owned
sessions, and snapshot-derived rows carry ownership across from the daemon list
rather than reporting `false`.

* fix(resource-manager): distinguish unprovable ownership from proven absence

Adversarial review of the previous commit found it committed the same class of
error it was fixing: it collapsed "no agent owns this" and "this provider cannot
tell me" into one boolean `false`, and both destructive paths read that as proof.

A daemon generation below the claim protocol, an older SSH relay, or the
in-process local fallback all list no owners for a session that may well have
one. `pty.ts` already encodes the rule at :613 — "only providers that serialize
claims may make listing absence authoritative" — and the new IPC row ignored it.
So after upgrading with a legacy daemon still holding a live agent terminal,
bulk cleanup would have destroyed it: exactly stablyai#8459, one layer down.

`hasAgentOwner: boolean` is now `agentOwnership: 'present' | 'absent' | 'unknown'`,
derived via `providesAgentSessionOwnerListings`. Only `absent` authorizes
destruction, so `unknown` protects and confirms.

Second defect, found independently by four review lenses: the deferred-SSH
bindings reached the bulk selector but not `mergeSnapshotAndSessions`, because
the merge call site re-listed the binding fields instead of reusing the object.
A deferred SSH session therefore rendered `bound: false`, and its single-row kill
skipped confirmation while bulk cleanup correctly spared it. The call site now
spreads `resourceSessionBindings`, and a parity test fails if any binding field
is re-listed inline — the drift itself is now impossible to reintroduce quietly.

The e2e ownership assertion was also weak: it checked only that a boolean
arrived. It now asserts the exact arm, and that the live local provider reports
`absent` rather than `unknown`, so a degenerate all-unknown implementation fails.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.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.

[Bug]: Resource Manager misclassifies live daemon sessions as orphan and force-kills them without confirmation

2 participants