Skip to content

fix(native-chat): keep an idle chat alive while its subagents or background commands run - #22794

Merged
brennanb2025 merged 1 commit into
mainfrom
brennanb2025/chat-idle-background-work
Sep 25, 2026
Merged

brennanb2025 merged 1 commit into
mainfrom
brennanb2025/chat-idle-background-work

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​61 $\color{#cf222e}{\Huge{\mathbf{−}}}$​4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​57
Prod 2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​9 $\color{#cf222e}{\Huge{\mathbf{−}}}$​3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​6

ELI5

When you leave a native chat, Orca waits 30 idle minutes and then stops the agent process behind it to free memory. It only checked whether the main agent was mid-answer. If the main agent had finished but its subagents, background commands or monitors were still running, Orca stopped the process anyway and that work died without a word. Now that work counts as busy, and the chat is only stopped once it has finished.

What Changed

  • Before: a chat nobody was viewing, whose main agent had finished its turn while a subagent, background shell or monitor kept running, was stopped 30 minutes after the last viewer let go. The sidebar showed it as working right up to the moment it was stopped.
  • After: the idle clock treats that running work as owed. It keeps re-checking, and stops the chat only after a full idle window in which nothing is running.
  • Mechanism: the release clock's "is work still owed?" check (hasOwedWork in structured-agent-session-host-lifetime.ts) now also reads the provider's live background roster through agentChildWorkLiveness, the same rule that makes the sidebar show the chat as working. The two existing conditions (a running lead turn, or a message held while the provider is still starting) are unchanged.

Why

The host already knew about this work: the sidebar status and the restart-resume snapshot both fold the live background roster into "working". The idle check was the one place that asked a narrower question, so it could stop a chat that everything else was showing as busy. Reusing the same liveness rule keeps those answers the same.

The roster is in-memory state rebuilt from the provider's own task events and cleared when the process goes away, so nothing durable can latch a chat alive. When the tasks settle, the next check releases the chat normally.

Alternatives considered:

  • Reusing the full sidebar status check. It also counts an unanswered send as working, and the clock deliberately retires those, because an echo that never comes would otherwise keep the process alive forever.
  • Checking on a fixed interval instead of re-arming per session. That is a larger change to the clock with no user-visible benefit here.

Known difference: when the clock finds work still running, it re-arms a full 30-minute window. So a chat can be stopped up to 30 minutes after its last background task finishes, not right away.

Linked Issue

No separate issue. This was deferred out of #22560 as its own decision: "A chat whose main agent finished and that nobody is viewing is still stopped after 30 idle minutes ... which ends any subagent or background command it left running."

Visual Proof

Live Electron QA on an isolated dev instance (hidden window, CDP), macOS, real Claude Code sessions. The idle window was shortened to 45 s in the QA build only. "Before" builds swap in one file set from main at a time and restart the app. Process liveness comes from ps on the provider pid the app spawned.

Before (this fix removed): leave while a background command runs, come back After: turn done, command still running After: come back once it finished
before after-running after-return

Prompt: run sleep 150; echo finished in the background and end the turn, then switch to another worktree.

  • Before: provider alive at +30 s, then DEAD at +60 s with the sleep killed. On return the task reads "Background task stopped · Background shell command didn't finish before the previous session ended".
  • After: provider ALIVE at +30, +60, +90, +120 and +150 s. The sleep finished by +150 s, Claude woke and replied, and the provider was stopped by +180 s. On return the task reads done · 2m 30s, "completed (exit code 0)", and a new send is answered.

Testing

  • structured-agent-session-owed-work-release.test.ts (renamed from structured-agent-session-starting-release.test.ts, which already covered the other owed-work case). New cases use the real Claude adapter over a fake connection: the lead's turn settles while a subagent, a background command or a monitor keeps running. The chat is released, the test waits out several idle windows and checks the chat is still alive, then the task settles and the chat is stopped. A finished background command wakes the lead for a new turn, and the test settles that turn too.

  • Ablation: with the production change removed, all three new cases fail; with it, all 7 in the file pass.

  • Neighbouring suites: surface lifetime, send preparation, and the hold RPC (66 tests) pass. pnpm tc:node, oxlint, and check:code-quality:changed are clean.

  • I manually tested these changes locally

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

AI Disclosure

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

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)

@brennanb2025
brennanb2025 force-pushed the brennanb2025/chat-idle-background-work branch from 12c5548 to b815e8c Compare September 25, 2026 08:27
@brennanb2025
brennanb2025 changed the base branch from main to brennanb2025/claude-reopen-after-root-exit September 25, 2026 08:27
@brennanb2025
brennanb2025 force-pushed the brennanb2025/chat-idle-background-work branch from b815e8c to 8bbedb6 Compare September 25, 2026 08:34
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status

Ready to merge after #22802. No code changes were needed. Head is still 8bbedb6f97.

Cause and fix. The 30-minute idle check only counted a running lead turn, or a message waiting for the agent to start. Subagents, background commands and monitors keep running inside the same agent process after the lead's turn ends, so the check stopped the process and that work died silently. The PR makes the check read the same live background-task list the sidebar uses to show the chat as working. This fixes the cause directly instead of adding a second guard. Nothing new is stored.

Can a stuck entry keep a chat alive forever? Checked for both Claude and Codex. Their lists only hold tasks the provider still reports as running. Failed, stopped and completed tasks move to the settled list, which this check does not read. So a failed or out-of-contact task cannot keep the process alive. Each new agent process starts with an empty list, and the list is cleared when the process exits. If the provider never reports a task as finished, the sidebar already shows the chat as working. The user can stop the task or close the tab, and quitting still stops every agent.

Expected behaviour to be aware of. A background command that never ends, such as a dev server started in the background, now keeps an unviewed chat's agent process running until the command is stopped, the tab is closed, or the app quits. That is the intended outcome: stopping it silently was the bug. As the PR body notes, a chat is released up to 30 minutes after its last background task finishes.

Validation

  • Six test files next to the change pass (64–66 tests), plus the tests for the liveness rule and the Claude task tracker. pnpm tc:node, oxlint and check:code-quality:changed are clean.
  • With the new condition removed, all three new tests fail. With it back, all seven in the file pass.
  • An adversarial review and a readiness checklist at the start and end of the review all came back clean. They covered performance (one read of the in-memory list each time the 30-minute timer fires, no polling), SSH (the check runs on the machine that runs the agent, and nothing sent between machines changes) and mobile.

Minor, not blocking

  • The rewrapped comment in structured-agent-session-release-clock.ts (lines 11–14) leaves one short line.
  • The new tests cover Claude only. Codex goes through the same check but has no test of its own.

Still open: live Electron QA screenshots, which are coming from a separate QA lane.

@brennanb2025
brennanb2025 force-pushed the brennanb2025/chat-idle-background-work branch from 8bbedb6 to 55a9106 Compare September 25, 2026 11:11
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Live Electron QA (whole stack)

Isolated dev instance (hidden window, driven over CDP), macOS, real Claude Code sessions. The idle window was shortened to 45 s in the QA build only. A "before" build swaps one fix's files back to main and restarts the app. Liveness is ps on the provider pid the app spawned. Screenshots are in each PR's Visual Proof.

Scenario Build Result
Leave a finished chat (switch worktree) stack head 2c7ab9991b provider ALIVE +30 s, DEAD +60 s. On return: history intact, new provider starts, the next send is answered. Pass
Same without #22801 provider still ALIVE at +30/+60/+90/+150 s (never released). Old behavior reproduced
Background sleep 150 left running, then leave stack head 2c7ab9991b provider ALIVE +30…+150 s while the command runs. Command finishes, provider DEAD by +180 s. On return: task done, new send answered. Pass
Same without #22794 provider and sleep both DEAD at +60 s. Chat shows "Background task stopped · didn't finish before the previous session ended". Old bug reproduced
Leave, idle stop, come back without #22802 "Claude couldn't restart: provider close unproven" and no new provider. An earlier run showed "Could not load conversation". Old bug reproduced

The scenario that crashes Claude mid-chat was not driven live; the #22802 integration tests cover it. Windows, Linux and SSH were not run live: the changes are host-side and not platform-specific, and no wire format changed.

@brennanb2025
brennanb2025 marked this pull request as ready for review September 25, 2026 11:20

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes

  • Idle release gate now honors live child work — hasOwedWork in structured-agent-session-host-lifetime.ts:204 gained a third arm reading context.deps.adapter.backgroundTaskState?.(sessionId)?.tasks through agentChildWorkLiveness, so a settled lead with running subagents, background commands or monitors re-arms the release clock instead of being evicted.
  • Release-clock comment — structured-agent-session-release-clock.ts:10-14 now names the third owed-work arm beside the running turn and the held send.
  • Coverage — the test file moved to structured-agent-session-owed-work-release.test.ts and adds an it.each over local_agent / local_bash / monitor driving the real Claude adapter over a fake connection: the session survives several idle windows while a task is live, then releases once the task settles and its wake-up turn completes.

Validation I ran: the file's 7 tests pass; removing only the new hasOwedWork arm fails exactly the 3 new cases at structured-agent-session-owed-work-release.test.ts:262, so the coverage is load-bearing. The agentChildWorkLiveness input is the same raw backgroundTaskState().tasks list the sidebar's structuredAgentSessionAgentStatus folds (the status feed only strips totalTokens, which is liveness-neutral), so there is no divergence between the two answers.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…ground commands run

The idle release clock stopped a chat's provider process 30 minutes after the last viewer left unless the lead agent's own turn was still running. A lead whose turn had finished but whose subagents, background commands or monitors were still running counted as idle, so stopping the process ended that work silently.

Owed work now also counts the provider's live background roster, read through the same liveness rule the sidebar uses to show a chat as working.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/chat-idle-background-work branch from 55a9106 to fecd6f5 Compare September 25, 2026 17:59
@brennanb2025
brennanb2025 changed the base branch from brennanb2025/claude-reopen-after-root-exit to main September 25, 2026 17:59
@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: eb5217b9-8870-4cd7-9242-efe3f28d07f4

📥 Commits

Reviewing files that changed from the base of the PR and between 8009939 and fecd6f5.

📒 Files selected for processing (3)
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-lifetime.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-owed-work-release.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-release-clock.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

The host lifetime check now treats live adapter background tasks as owed work. Tests cover released sessions with running subagents, background commands, and monitors. They verify that the session remains open while the task runs and while its resulting lead turn is unsettled, then closes after that turn settles. Comments describe the same release-clock behavior.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to fecd6

Released chats can remain available while background work runs and close after that work and its resulting turn settle. No actionable merge-blocking risk is established.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to fecd6

Released chats will no longer discard running background work. A task that never finishes, however, can also keep an unused agent process running without a time limit. No externally reachable exploit was established.

Retained concerns

  • Medium · security · inferred: A live or stale provider task entry can repeatedly veto eviction of a released session, extending child-process lifetime without an independent deadline. A later aggregate roster can also restore an entry after its terminal notification. Attacker control of those events was not established.
Security review details

Security Blast Radius

  • inferred — The demonstrated effect is retention of an individual released session’s provider child and its work. The evidence does not establish a new tenant, service, credential, or network boundary crossing; multiple independently affected sessions remain possible.

Security Findings and Attack Paths

  • inferred — A continuously live or restored task entry can keep a released provider child running past successive idle windows. This is a process-lifetime containment concern, not a verified attacker-originated event-injection path.

Trust Boundaries and Controls

  • observed — The host looks up its session before querying the adapter with that session ID. Terminal task notifications retire individual tasks, and provider-ended events clear the roster. Independent authenticity guarantees for provider messages were not established by the scoped evidence.

Resilience and Maintainability Implications

  • observed — The added tests show retention during background work followed by eviction after task notification and the resulting lead turn settle. They do not exercise stale repeated rosters, missing terminal events, or cancellation and failure transitions.

Hardening Proposals

  • proposed — Consider a session-scoped stale-task reconciliation or maximum unheld retention policy that preserves genuinely running work while allowing eventual teardown when terminal evidence never arrives.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 3 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: keeping idle native chats alive while subagents or background commands run.
Description check ✅ Passed The description is detailed and covers the user impact, mechanism, rationale, testing, visual proof, compatibility considerations, and checklist. The required linked issue is not provided, and the che…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

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.

@brennanb2025
brennanb2025 merged commit 9643e16 into main Sep 25, 2026
32 checks passed
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