fix(server): stop reaping sessions with live background work - #5690

Closed
liusqu wants to merge 1 commit into
pingdotgg:mainfrom
liusqu:fix/session-reaper-background-tasks
Closed

fix(server): stop reaping sessions with live background work#5690
liusqu wants to merge 1 commit into
pingdotgg:mainfrom
liusqu:fix/session-reaper-background-tasks

Conversation

@liusqu

@liusquliusqu commented Aug 8, 2026

Copy link
Copy Markdown

Problem

ProviderSessionReaper measures idleness from ProviderSessionDirectory.lastSeenAt, which only advances on turns (ProviderService.sendTurn). But a Claude session keeps working between turns — subagent fleets, workflow runs, background shells all outlive the turn that spawned them. A session with no user turn for 30 minutes gets stopSession'd while its agents are still running, silently killing the in-flight work.

Hit this in production use tonight: a 30+ minute background implementation agent was killed mid-run twice; the only surviving evidence was the worktree state.

Fix

The projection already answers "is native background work still alive": ThreadBackgroundLiveness is fed by the canonical task lifecycle stream for every provider, drops terminal/idle/inert tasks, and is cleared on session.exited. Its backgroundLiveness verdict already rides on the OrchestrationThreadShell the reaper fetches for its existing activeTurnId check — so this is a read of an existing field, not new plumbing. Two files touched.

  • Idle-by-lastSeenAt sessions are spared while backgroundLiveness === "working".
  • "monitoring" deliberately does not earn a reprieve: watch loops tick forever by design and would make sessions immortal, turning the sweep into a no-op as a leak backstop. Losing a watch loop is recoverable; losing a running agent is not.
  • The reprieve is bounded by backgroundExtensionLimitMultiplier (48× threshold = 24 h by default, configurable like the other two options), so a wedged-but-chatty agent cannot pin its session open forever.
  • The activeTurnId skip is unchanged.

Tests

Four new cases in ProviderSessionReaper.test.ts (idle+working → spared; monitoring → reaped; null → reaped; working past ceiling → reaped). Red-green verified against two deliberately unpatched variants — each new guard is independently load-bearing. Full src/provider suite: 490 passed, 6 skipped. Lint/format clean.

🤖 Generated with Claude Code


Note

Medium Risk
Changes when long-idle provider sessions are stopped, which can affect in-flight subagents vs. session leaks; behavior is bounded by a configurable ceiling and covered by new tests.

Overview
Provider session reaper no longer stops sessions that are idle on lastSeenAt but still have real background agent work, using the thread shell’s existing backgroundLiveness field.

After the usual inactivity and activeTurnId checks, the sweep skipsstopSession when backgroundLiveness === "working" and idle time is below a new cap (inactivityThresholdMs × backgroundExtensionLimitMultiplier, default 48× → 24h). "monitoring" does not get that reprieve (watch loops would otherwise keep sessions open indefinitely). null or idle past the cap is still reaped, including wedged agents that keep reporting "working".

Tests cover working (spared), monitoring/null (reaped), and working past a lowered multiplier ceiling (reaped); the harness can pass backgroundExtensionLimitMultiplier.

Reviewed by Cursor Bugbot for commit 23f9b8b. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Stop reaping provider sessions with live background work

  • The session reaper now skips stopping sessions when backgroundLiveness is 'working' and the idle duration is below a configurable ceiling (inactivityThresholdMs × backgroundExtensionLimitMultiplier, defaulting to 48×).
  • Sessions with backgroundLiveness of 'monitoring' or null continue to be reaped at the normal inactivity threshold.
  • Sessions where background work outlives the ceiling are reaped regardless.
  • A new debug log provider.session.reaper.skipped-background-work is emitted when a session is deferred.

Macroscope summarized 23f9b8b.

Root cause: the reaper treats a session as idle from
`ProviderSessionDirectory.lastSeenAt`, which only advances on turns
(`ProviderService.sendTurn` — "a turn is the clearest sign a session is
still alive"). But a session keeps working between turns: subagent
fleets, workflow runs and background shells outlive the turn that
spawned them. A session with no user turn for 30 minutes was
`stopSession`'d while its agents were still running, silently killing
30+ minutes of in-flight work.
The projection already answers this question. `ThreadBackgroundLiveness`
is fed by the same task lifecycle stream for every provider, drops
terminal/idle/inert tasks, and is cleared on `session.exited`; its
`backgroundLiveness` verdict already rides on the `OrchestrationThreadShell`
that the reaper fetches for its `activeTurnId` check. So this is a
read of an existing field, not new plumbing.
Only "working" earns a reprieve. "monitoring" is watch loops (monitor
tasks, background shells) that tick forever by design — honouring it
would make those sessions immortal and turn the sweep into a no-op as a
leak backstop. Losing a watch loop to the reaper is recoverable; losing
a running agent is not.
The reprieve is bounded by `backgroundExtensionLimitMultiplier`
(48 x the inactivity threshold = 24h by default), so an agent that is
wedged yet still emitting progress cannot pin its session open forever.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1693a9f0-0000-4db9-add2-1e487704a51c

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

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.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 8, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session reaper behavior to preserve sessions with active background work for up to 24 hours. While the fix intent is clear and tests are comprehensive, this fundamentally alters when sessions get terminated, affecting resource management and production runtime behavior. Human review recommended for this behavioral change.

You can customize Macroscope's approvability policy. Learn more.

@shivamhwp

Copy link
Copy Markdown
Collaborator

gpt-5.6-sol on behalf of shivamhwp.

Thank you for working on the session reaper. We are closing this PR because #5677 has already landed the guard that prevents the reaper from killing sessions with live background agents. It updates the same server path and covers the behavior proposed here.

The fix is present on main, so keeping this branch open would duplicate completed work.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@liusqu@shivamhwp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(server): stop reaping sessions with live background work - #5690

Closed
liusqu wants to merge 1 commit into
pingdotgg:mainfrom
liusqu:fix/session-reaper-background-tasks
Closed

fix(server): stop reaping sessions with live background work#5690
liusqu wants to merge 1 commit into
pingdotgg:mainfrom
liusqu:fix/session-reaper-background-tasks

Conversation

@liusqu

@liusquliusqu commented Aug 8, 2026

Copy link
Copy Markdown

Problem

ProviderSessionReaper measures idleness from ProviderSessionDirectory.lastSeenAt, which only advances on turns (ProviderService.sendTurn). But a Claude session keeps working between turns — subagent fleets, workflow runs, background shells all outlive the turn that spawned them. A session with no user turn for 30 minutes gets stopSession'd while its agents are still running, silently killing the in-flight work.

Hit this in production use tonight: a 30+ minute background implementation agent was killed mid-run twice; the only surviving evidence was the worktree state.

Fix

The projection already answers "is native background work still alive": ThreadBackgroundLiveness is fed by the canonical task lifecycle stream for every provider, drops terminal/idle/inert tasks, and is cleared on session.exited. Its backgroundLiveness verdict already rides on the OrchestrationThreadShell the reaper fetches for its existing activeTurnId check — so this is a read of an existing field, not new plumbing. Two files touched.

  • Idle-by-lastSeenAt sessions are spared while backgroundLiveness === "working".
  • "monitoring" deliberately does not earn a reprieve: watch loops tick forever by design and would make sessions immortal, turning the sweep into a no-op as a leak backstop. Losing a watch loop is recoverable; losing a running agent is not.
  • The reprieve is bounded by backgroundExtensionLimitMultiplier (48× threshold = 24 h by default, configurable like the other two options), so a wedged-but-chatty agent cannot pin its session open forever.
  • The activeTurnId skip is unchanged.

Tests

Four new cases in ProviderSessionReaper.test.ts (idle+working → spared; monitoring → reaped; null → reaped; working past ceiling → reaped). Red-green verified against two deliberately unpatched variants — each new guard is independently load-bearing. Full src/provider suite: 490 passed, 6 skipped. Lint/format clean.

🤖 Generated with Claude Code


Note

Medium Risk
Changes when long-idle provider sessions are stopped, which can affect in-flight subagents vs. session leaks; behavior is bounded by a configurable ceiling and covered by new tests.

Overview
Provider session reaper no longer stops sessions that are idle on lastSeenAt but still have real background agent work, using the thread shell’s existing backgroundLiveness field.

After the usual inactivity and activeTurnId checks, the sweep skipsstopSession when backgroundLiveness === "working" and idle time is below a new cap (inactivityThresholdMs × backgroundExtensionLimitMultiplier, default 48× → 24h). "monitoring" does not get that reprieve (watch loops would otherwise keep sessions open indefinitely). null or idle past the cap is still reaped, including wedged agents that keep reporting "working".

Tests cover working (spared), monitoring/null (reaped), and working past a lowered multiplier ceiling (reaped); the harness can pass backgroundExtensionLimitMultiplier.

Reviewed by Cursor Bugbot for commit 23f9b8b. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Stop reaping provider sessions with live background work

  • The session reaper now skips stopping sessions when backgroundLiveness is 'working' and the idle duration is below a configurable ceiling (inactivityThresholdMs × backgroundExtensionLimitMultiplier, defaulting to 48×).
  • Sessions with backgroundLiveness of 'monitoring' or null continue to be reaped at the normal inactivity threshold.
  • Sessions where background work outlives the ceiling are reaped regardless.
  • A new debug log provider.session.reaper.skipped-background-work is emitted when a session is deferred.

Macroscope summarized 23f9b8b.

Root cause: the reaper treats a session as idle from
`ProviderSessionDirectory.lastSeenAt`, which only advances on turns
(`ProviderService.sendTurn` — "a turn is the clearest sign a session is
still alive"). But a session keeps working between turns: subagent
fleets, workflow runs and background shells outlive the turn that
spawned them. A session with no user turn for 30 minutes was
`stopSession`'d while its agents were still running, silently killing
30+ minutes of in-flight work.
The projection already answers this question. `ThreadBackgroundLiveness`
is fed by the same task lifecycle stream for every provider, drops
terminal/idle/inert tasks, and is cleared on `session.exited`; its
`backgroundLiveness` verdict already rides on the `OrchestrationThreadShell`
that the reaper fetches for its `activeTurnId` check. So this is a
read of an existing field, not new plumbing.
Only "working" earns a reprieve. "monitoring" is watch loops (monitor
tasks, background shells) that tick forever by design — honouring it
would make those sessions immortal and turn the sweep into a no-op as a
leak backstop. Losing a watch loop to the reaper is recoverable; losing
a running agent is not.
The reprieve is bounded by `backgroundExtensionLimitMultiplier`
(48 x the inactivity threshold = 24h by default), so an agent that is
wedged yet still emitting progress cannot pin its session open forever.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1693a9f0-0000-4db9-add2-1e487704a51c

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

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.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 8, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session reaper behavior to preserve sessions with active background work for up to 24 hours. While the fix intent is clear and tests are comprehensive, this fundamentally alters when sessions get terminated, affecting resource management and production runtime behavior. Human review recommended for this behavioral change.

You can customize Macroscope's approvability policy. Learn more.

@shivamhwp

Copy link
Copy Markdown
Collaborator

gpt-5.6-sol on behalf of shivamhwp.

Thank you for working on the session reaper. We are closing this PR because #5677 has already landed the guard that prevents the reaper from killing sessions with live background agents. It updates the same server path and covers the behavior proposed here.

The fix is present on main, so keeping this branch open would duplicate completed work.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@liusqu@shivamhwp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): stop reaping sessions with live background work - #5690

Closed
liusqu wants to merge 1 commit into
pingdotgg:mainfrom
liusqu:fix/session-reaper-background-tasks
Closed

fix(server): stop reaping sessions with live background work#5690
liusqu wants to merge 1 commit into
pingdotgg:mainfrom
liusqu:fix/session-reaper-background-tasks

Conversation

@liusqu

@liusquliusqu commented Aug 8, 2026

Copy link
Copy Markdown

Problem

ProviderSessionReaper measures idleness from ProviderSessionDirectory.lastSeenAt, which only advances on turns (ProviderService.sendTurn). But a Claude session keeps working between turns — subagent fleets, workflow runs, background shells all outlive the turn that spawned them. A session with no user turn for 30 minutes gets stopSession'd while its agents are still running, silently killing the in-flight work.

Hit this in production use tonight: a 30+ minute background implementation agent was killed mid-run twice; the only surviving evidence was the worktree state.

Fix

The projection already answers "is native background work still alive": ThreadBackgroundLiveness is fed by the canonical task lifecycle stream for every provider, drops terminal/idle/inert tasks, and is cleared on session.exited. Its backgroundLiveness verdict already rides on the OrchestrationThreadShell the reaper fetches for its existing activeTurnId check — so this is a read of an existing field, not new plumbing. Two files touched.

  • Idle-by-lastSeenAt sessions are spared while backgroundLiveness === "working".
  • "monitoring" deliberately does not earn a reprieve: watch loops tick forever by design and would make sessions immortal, turning the sweep into a no-op as a leak backstop. Losing a watch loop is recoverable; losing a running agent is not.
  • The reprieve is bounded by backgroundExtensionLimitMultiplier (48× threshold = 24 h by default, configurable like the other two options), so a wedged-but-chatty agent cannot pin its session open forever.
  • The activeTurnId skip is unchanged.

Tests

Four new cases in ProviderSessionReaper.test.ts (idle+working → spared; monitoring → reaped; null → reaped; working past ceiling → reaped). Red-green verified against two deliberately unpatched variants — each new guard is independently load-bearing. Full src/provider suite: 490 passed, 6 skipped. Lint/format clean.

🤖 Generated with Claude Code


Note

Medium Risk
Changes when long-idle provider sessions are stopped, which can affect in-flight subagents vs. session leaks; behavior is bounded by a configurable ceiling and covered by new tests.

Overview
Provider session reaper no longer stops sessions that are idle on lastSeenAt but still have real background agent work, using the thread shell’s existing backgroundLiveness field.

After the usual inactivity and activeTurnId checks, the sweep skipsstopSession when backgroundLiveness === "working" and idle time is below a new cap (inactivityThresholdMs × backgroundExtensionLimitMultiplier, default 48× → 24h). "monitoring" does not get that reprieve (watch loops would otherwise keep sessions open indefinitely). null or idle past the cap is still reaped, including wedged agents that keep reporting "working".

Tests cover working (spared), monitoring/null (reaped), and working past a lowered multiplier ceiling (reaped); the harness can pass backgroundExtensionLimitMultiplier.

Reviewed by Cursor Bugbot for commit 23f9b8b. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Stop reaping provider sessions with live background work

  • The session reaper now skips stopping sessions when backgroundLiveness is 'working' and the idle duration is below a configurable ceiling (inactivityThresholdMs × backgroundExtensionLimitMultiplier, defaulting to 48×).
  • Sessions with backgroundLiveness of 'monitoring' or null continue to be reaped at the normal inactivity threshold.
  • Sessions where background work outlives the ceiling are reaped regardless.
  • A new debug log provider.session.reaper.skipped-background-work is emitted when a session is deferred.

Macroscope summarized 23f9b8b.

Root cause: the reaper treats a session as idle from
`ProviderSessionDirectory.lastSeenAt`, which only advances on turns
(`ProviderService.sendTurn` — "a turn is the clearest sign a session is
still alive"). But a session keeps working between turns: subagent
fleets, workflow runs and background shells outlive the turn that
spawned them. A session with no user turn for 30 minutes was
`stopSession`'d while its agents were still running, silently killing
30+ minutes of in-flight work.
The projection already answers this question. `ThreadBackgroundLiveness`
is fed by the same task lifecycle stream for every provider, drops
terminal/idle/inert tasks, and is cleared on `session.exited`; its
`backgroundLiveness` verdict already rides on the `OrchestrationThreadShell`
that the reaper fetches for its `activeTurnId` check. So this is a
read of an existing field, not new plumbing.
Only "working" earns a reprieve. "monitoring" is watch loops (monitor
tasks, background shells) that tick forever by design — honouring it
would make those sessions immortal and turn the sweep into a no-op as a
leak backstop. Losing a watch loop to the reaper is recoverable; losing
a running agent is not.
The reprieve is bounded by `backgroundExtensionLimitMultiplier`
(48 x the inactivity threshold = 24h by default), so an agent that is
wedged yet still emitting progress cannot pin its session open forever.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1693a9f0-0000-4db9-add2-1e487704a51c

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

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.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 8, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session reaper behavior to preserve sessions with active background work for up to 24 hours. While the fix intent is clear and tests are comprehensive, this fundamentally alters when sessions get terminated, affecting resource management and production runtime behavior. Human review recommended for this behavioral change.

You can customize Macroscope's approvability policy. Learn more.

@shivamhwp

Copy link
Copy Markdown
Collaborator

gpt-5.6-sol on behalf of shivamhwp.

Thank you for working on the session reaper. We are closing this PR because #5677 has already landed the guard that prevents the reaper from killing sessions with live background agents. It updates the same server path and covers the behavior proposed here.

The fix is present on main, so keeping this branch open would duplicate completed work.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@liusqu@shivamhwp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): stop reaping sessions with live background work - #5690

Closed
liusqu wants to merge 1 commit into
pingdotgg:mainfrom
liusqu:fix/session-reaper-background-tasks
Closed

fix(server): stop reaping sessions with live background work#5690
liusqu wants to merge 1 commit into
pingdotgg:mainfrom
liusqu:fix/session-reaper-background-tasks

Conversation

@liusqu

@liusquliusqu commented Aug 8, 2026

Copy link
Copy Markdown

Problem

ProviderSessionReaper measures idleness from ProviderSessionDirectory.lastSeenAt, which only advances on turns (ProviderService.sendTurn). But a Claude session keeps working between turns — subagent fleets, workflow runs, background shells all outlive the turn that spawned them. A session with no user turn for 30 minutes gets stopSession'd while its agents are still running, silently killing the in-flight work.

Hit this in production use tonight: a 30+ minute background implementation agent was killed mid-run twice; the only surviving evidence was the worktree state.

Fix

The projection already answers "is native background work still alive": ThreadBackgroundLiveness is fed by the canonical task lifecycle stream for every provider, drops terminal/idle/inert tasks, and is cleared on session.exited. Its backgroundLiveness verdict already rides on the OrchestrationThreadShell the reaper fetches for its existing activeTurnId check — so this is a read of an existing field, not new plumbing. Two files touched.

  • Idle-by-lastSeenAt sessions are spared while backgroundLiveness === "working".
  • "monitoring" deliberately does not earn a reprieve: watch loops tick forever by design and would make sessions immortal, turning the sweep into a no-op as a leak backstop. Losing a watch loop is recoverable; losing a running agent is not.
  • The reprieve is bounded by backgroundExtensionLimitMultiplier (48× threshold = 24 h by default, configurable like the other two options), so a wedged-but-chatty agent cannot pin its session open forever.
  • The activeTurnId skip is unchanged.

Tests

Four new cases in ProviderSessionReaper.test.ts (idle+working → spared; monitoring → reaped; null → reaped; working past ceiling → reaped). Red-green verified against two deliberately unpatched variants — each new guard is independently load-bearing. Full src/provider suite: 490 passed, 6 skipped. Lint/format clean.

🤖 Generated with Claude Code


Note

Medium Risk
Changes when long-idle provider sessions are stopped, which can affect in-flight subagents vs. session leaks; behavior is bounded by a configurable ceiling and covered by new tests.

Overview
Provider session reaper no longer stops sessions that are idle on lastSeenAt but still have real background agent work, using the thread shell’s existing backgroundLiveness field.

After the usual inactivity and activeTurnId checks, the sweep skipsstopSession when backgroundLiveness === "working" and idle time is below a new cap (inactivityThresholdMs × backgroundExtensionLimitMultiplier, default 48× → 24h). "monitoring" does not get that reprieve (watch loops would otherwise keep sessions open indefinitely). null or idle past the cap is still reaped, including wedged agents that keep reporting "working".

Tests cover working (spared), monitoring/null (reaped), and working past a lowered multiplier ceiling (reaped); the harness can pass backgroundExtensionLimitMultiplier.

Reviewed by Cursor Bugbot for commit 23f9b8b. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Stop reaping provider sessions with live background work

  • The session reaper now skips stopping sessions when backgroundLiveness is 'working' and the idle duration is below a configurable ceiling (inactivityThresholdMs × backgroundExtensionLimitMultiplier, defaulting to 48×).
  • Sessions with backgroundLiveness of 'monitoring' or null continue to be reaped at the normal inactivity threshold.
  • Sessions where background work outlives the ceiling are reaped regardless.
  • A new debug log provider.session.reaper.skipped-background-work is emitted when a session is deferred.

Macroscope summarized 23f9b8b.

Root cause: the reaper treats a session as idle from
`ProviderSessionDirectory.lastSeenAt`, which only advances on turns
(`ProviderService.sendTurn` — "a turn is the clearest sign a session is
still alive"). But a session keeps working between turns: subagent
fleets, workflow runs and background shells outlive the turn that
spawned them. A session with no user turn for 30 minutes was
`stopSession`'d while its agents were still running, silently killing
30+ minutes of in-flight work.
The projection already answers this question. `ThreadBackgroundLiveness`
is fed by the same task lifecycle stream for every provider, drops
terminal/idle/inert tasks, and is cleared on `session.exited`; its
`backgroundLiveness` verdict already rides on the `OrchestrationThreadShell`
that the reaper fetches for its `activeTurnId` check. So this is a
read of an existing field, not new plumbing.
Only "working" earns a reprieve. "monitoring" is watch loops (monitor
tasks, background shells) that tick forever by design — honouring it
would make those sessions immortal and turn the sweep into a no-op as a
leak backstop. Losing a watch loop to the reaper is recoverable; losing
a running agent is not.
The reprieve is bounded by `backgroundExtensionLimitMultiplier`
(48 x the inactivity threshold = 24h by default), so an agent that is
wedged yet still emitting progress cannot pin its session open forever.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1693a9f0-0000-4db9-add2-1e487704a51c

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

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.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 8, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session reaper behavior to preserve sessions with active background work for up to 24 hours. While the fix intent is clear and tests are comprehensive, this fundamentally alters when sessions get terminated, affecting resource management and production runtime behavior. Human review recommended for this behavioral change.

You can customize Macroscope's approvability policy. Learn more.

@shivamhwp

Copy link
Copy Markdown
Collaborator

gpt-5.6-sol on behalf of shivamhwp.

Thank you for working on the session reaper. We are closing this PR because #5677 has already landed the guard that prevents the reaper from killing sessions with live background agents. It updates the same server path and covers the behavior proposed here.

The fix is present on main, so keeping this branch open would duplicate completed work.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@liusqu@shivamhwp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(server): stop reaping sessions with live background work - #5690

Closed
liusqu wants to merge 1 commit into
pingdotgg:mainfrom
liusqu:fix/session-reaper-background-tasks
Closed

fix(server): stop reaping sessions with live background work#5690
liusqu wants to merge 1 commit into
pingdotgg:mainfrom
liusqu:fix/session-reaper-background-tasks

Conversation

@liusqu

@liusquliusqu commented Aug 8, 2026

Copy link
Copy Markdown

Problem

ProviderSessionReaper measures idleness from ProviderSessionDirectory.lastSeenAt, which only advances on turns (ProviderService.sendTurn). But a Claude session keeps working between turns — subagent fleets, workflow runs, background shells all outlive the turn that spawned them. A session with no user turn for 30 minutes gets stopSession'd while its agents are still running, silently killing the in-flight work.

Hit this in production use tonight: a 30+ minute background implementation agent was killed mid-run twice; the only surviving evidence was the worktree state.

Fix

The projection already answers "is native background work still alive": ThreadBackgroundLiveness is fed by the canonical task lifecycle stream for every provider, drops terminal/idle/inert tasks, and is cleared on session.exited. Its backgroundLiveness verdict already rides on the OrchestrationThreadShell the reaper fetches for its existing activeTurnId check — so this is a read of an existing field, not new plumbing. Two files touched.

  • Idle-by-lastSeenAt sessions are spared while backgroundLiveness === "working".
  • "monitoring" deliberately does not earn a reprieve: watch loops tick forever by design and would make sessions immortal, turning the sweep into a no-op as a leak backstop. Losing a watch loop is recoverable; losing a running agent is not.
  • The reprieve is bounded by backgroundExtensionLimitMultiplier (48× threshold = 24 h by default, configurable like the other two options), so a wedged-but-chatty agent cannot pin its session open forever.
  • The activeTurnId skip is unchanged.

Tests

Four new cases in ProviderSessionReaper.test.ts (idle+working → spared; monitoring → reaped; null → reaped; working past ceiling → reaped). Red-green verified against two deliberately unpatched variants — each new guard is independently load-bearing. Full src/provider suite: 490 passed, 6 skipped. Lint/format clean.

🤖 Generated with Claude Code


Note

Medium Risk
Changes when long-idle provider sessions are stopped, which can affect in-flight subagents vs. session leaks; behavior is bounded by a configurable ceiling and covered by new tests.

Overview
Provider session reaper no longer stops sessions that are idle on lastSeenAt but still have real background agent work, using the thread shell’s existing backgroundLiveness field.

After the usual inactivity and activeTurnId checks, the sweep skipsstopSession when backgroundLiveness === "working" and idle time is below a new cap (inactivityThresholdMs × backgroundExtensionLimitMultiplier, default 48× → 24h). "monitoring" does not get that reprieve (watch loops would otherwise keep sessions open indefinitely). null or idle past the cap is still reaped, including wedged agents that keep reporting "working".

Tests cover working (spared), monitoring/null (reaped), and working past a lowered multiplier ceiling (reaped); the harness can pass backgroundExtensionLimitMultiplier.

Reviewed by Cursor Bugbot for commit 23f9b8b. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Stop reaping provider sessions with live background work

  • The session reaper now skips stopping sessions when backgroundLiveness is 'working' and the idle duration is below a configurable ceiling (inactivityThresholdMs × backgroundExtensionLimitMultiplier, defaulting to 48×).
  • Sessions with backgroundLiveness of 'monitoring' or null continue to be reaped at the normal inactivity threshold.
  • Sessions where background work outlives the ceiling are reaped regardless.
  • A new debug log provider.session.reaper.skipped-background-work is emitted when a session is deferred.

Macroscope summarized 23f9b8b.

Root cause: the reaper treats a session as idle from
`ProviderSessionDirectory.lastSeenAt`, which only advances on turns
(`ProviderService.sendTurn` — "a turn is the clearest sign a session is
still alive"). But a session keeps working between turns: subagent
fleets, workflow runs and background shells outlive the turn that
spawned them. A session with no user turn for 30 minutes was
`stopSession`'d while its agents were still running, silently killing
30+ minutes of in-flight work.
The projection already answers this question. `ThreadBackgroundLiveness`
is fed by the same task lifecycle stream for every provider, drops
terminal/idle/inert tasks, and is cleared on `session.exited`; its
`backgroundLiveness` verdict already rides on the `OrchestrationThreadShell`
that the reaper fetches for its `activeTurnId` check. So this is a
read of an existing field, not new plumbing.
Only "working" earns a reprieve. "monitoring" is watch loops (monitor
tasks, background shells) that tick forever by design — honouring it
would make those sessions immortal and turn the sweep into a no-op as a
leak backstop. Losing a watch loop to the reaper is recoverable; losing
a running agent is not.
The reprieve is bounded by `backgroundExtensionLimitMultiplier`
(48 x the inactivity threshold = 24h by default), so an agent that is
wedged yet still emitting progress cannot pin its session open forever.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1693a9f0-0000-4db9-add2-1e487704a51c

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

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.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 8, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session reaper behavior to preserve sessions with active background work for up to 24 hours. While the fix intent is clear and tests are comprehensive, this fundamentally alters when sessions get terminated, affecting resource management and production runtime behavior. Human review recommended for this behavioral change.

You can customize Macroscope's approvability policy. Learn more.

@shivamhwp

Copy link
Copy Markdown
Collaborator

gpt-5.6-sol on behalf of shivamhwp.

Thank you for working on the session reaper. We are closing this PR because #5677 has already landed the guard that prevents the reaper from killing sessions with live background agents. It updates the same server path and covers the behavior proposed here.

The fix is present on main, so keeping this branch open would duplicate completed work.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@liusqu@shivamhwp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): stop reaping sessions with live background work - #5690

Closed
liusqu wants to merge 1 commit into
pingdotgg:mainfrom
liusqu:fix/session-reaper-background-tasks
Closed

fix(server): stop reaping sessions with live background work#5690
liusqu wants to merge 1 commit into
pingdotgg:mainfrom
liusqu:fix/session-reaper-background-tasks

Conversation

@liusqu

@liusquliusqu commented Aug 8, 2026

Copy link
Copy Markdown

Problem

ProviderSessionReaper measures idleness from ProviderSessionDirectory.lastSeenAt, which only advances on turns (ProviderService.sendTurn). But a Claude session keeps working between turns — subagent fleets, workflow runs, background shells all outlive the turn that spawned them. A session with no user turn for 30 minutes gets stopSession'd while its agents are still running, silently killing the in-flight work.

Hit this in production use tonight: a 30+ minute background implementation agent was killed mid-run twice; the only surviving evidence was the worktree state.

Fix

The projection already answers "is native background work still alive": ThreadBackgroundLiveness is fed by the canonical task lifecycle stream for every provider, drops terminal/idle/inert tasks, and is cleared on session.exited. Its backgroundLiveness verdict already rides on the OrchestrationThreadShell the reaper fetches for its existing activeTurnId check — so this is a read of an existing field, not new plumbing. Two files touched.

  • Idle-by-lastSeenAt sessions are spared while backgroundLiveness === "working".
  • "monitoring" deliberately does not earn a reprieve: watch loops tick forever by design and would make sessions immortal, turning the sweep into a no-op as a leak backstop. Losing a watch loop is recoverable; losing a running agent is not.
  • The reprieve is bounded by backgroundExtensionLimitMultiplier (48× threshold = 24 h by default, configurable like the other two options), so a wedged-but-chatty agent cannot pin its session open forever.
  • The activeTurnId skip is unchanged.

Tests

Four new cases in ProviderSessionReaper.test.ts (idle+working → spared; monitoring → reaped; null → reaped; working past ceiling → reaped). Red-green verified against two deliberately unpatched variants — each new guard is independently load-bearing. Full src/provider suite: 490 passed, 6 skipped. Lint/format clean.

🤖 Generated with Claude Code


Note

Medium Risk
Changes when long-idle provider sessions are stopped, which can affect in-flight subagents vs. session leaks; behavior is bounded by a configurable ceiling and covered by new tests.

Overview
Provider session reaper no longer stops sessions that are idle on lastSeenAt but still have real background agent work, using the thread shell’s existing backgroundLiveness field.

After the usual inactivity and activeTurnId checks, the sweep skipsstopSession when backgroundLiveness === "working" and idle time is below a new cap (inactivityThresholdMs × backgroundExtensionLimitMultiplier, default 48× → 24h). "monitoring" does not get that reprieve (watch loops would otherwise keep sessions open indefinitely). null or idle past the cap is still reaped, including wedged agents that keep reporting "working".

Tests cover working (spared), monitoring/null (reaped), and working past a lowered multiplier ceiling (reaped); the harness can pass backgroundExtensionLimitMultiplier.

Reviewed by Cursor Bugbot for commit 23f9b8b. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Stop reaping provider sessions with live background work

  • The session reaper now skips stopping sessions when backgroundLiveness is 'working' and the idle duration is below a configurable ceiling (inactivityThresholdMs × backgroundExtensionLimitMultiplier, defaulting to 48×).
  • Sessions with backgroundLiveness of 'monitoring' or null continue to be reaped at the normal inactivity threshold.
  • Sessions where background work outlives the ceiling are reaped regardless.
  • A new debug log provider.session.reaper.skipped-background-work is emitted when a session is deferred.

Macroscope summarized 23f9b8b.

Root cause: the reaper treats a session as idle from
`ProviderSessionDirectory.lastSeenAt`, which only advances on turns
(`ProviderService.sendTurn` — "a turn is the clearest sign a session is
still alive"). But a session keeps working between turns: subagent
fleets, workflow runs and background shells outlive the turn that
spawned them. A session with no user turn for 30 minutes was
`stopSession`'d while its agents were still running, silently killing
30+ minutes of in-flight work.
The projection already answers this question. `ThreadBackgroundLiveness`
is fed by the same task lifecycle stream for every provider, drops
terminal/idle/inert tasks, and is cleared on `session.exited`; its
`backgroundLiveness` verdict already rides on the `OrchestrationThreadShell`
that the reaper fetches for its `activeTurnId` check. So this is a
read of an existing field, not new plumbing.
Only "working" earns a reprieve. "monitoring" is watch loops (monitor
tasks, background shells) that tick forever by design — honouring it
would make those sessions immortal and turn the sweep into a no-op as a
leak backstop. Losing a watch loop to the reaper is recoverable; losing
a running agent is not.
The reprieve is bounded by `backgroundExtensionLimitMultiplier`
(48 x the inactivity threshold = 24h by default), so an agent that is
wedged yet still emitting progress cannot pin its session open forever.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1693a9f0-0000-4db9-add2-1e487704a51c

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

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.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 8, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session reaper behavior to preserve sessions with active background work for up to 24 hours. While the fix intent is clear and tests are comprehensive, this fundamentally alters when sessions get terminated, affecting resource management and production runtime behavior. Human review recommended for this behavioral change.

You can customize Macroscope's approvability policy. Learn more.

@shivamhwp

Copy link
Copy Markdown
Collaborator

gpt-5.6-sol on behalf of shivamhwp.

Thank you for working on the session reaper. We are closing this PR because #5677 has already landed the guard that prevents the reaper from killing sessions with live background agents. It updates the same server path and covers the behavior proposed here.

The fix is present on main, so keeping this branch open would duplicate completed work.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@liusqu@shivamhwp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): stop reaping sessions with live background work - #5690

Closed
liusqu wants to merge 1 commit into
pingdotgg:mainfrom
liusqu:fix/session-reaper-background-tasks
Closed

fix(server): stop reaping sessions with live background work#5690
liusqu wants to merge 1 commit into
pingdotgg:mainfrom
liusqu:fix/session-reaper-background-tasks

Conversation

@liusqu

@liusquliusqu commented Aug 8, 2026

Copy link
Copy Markdown

Problem

ProviderSessionReaper measures idleness from ProviderSessionDirectory.lastSeenAt, which only advances on turns (ProviderService.sendTurn). But a Claude session keeps working between turns — subagent fleets, workflow runs, background shells all outlive the turn that spawned them. A session with no user turn for 30 minutes gets stopSession'd while its agents are still running, silently killing the in-flight work.

Hit this in production use tonight: a 30+ minute background implementation agent was killed mid-run twice; the only surviving evidence was the worktree state.

Fix

The projection already answers "is native background work still alive": ThreadBackgroundLiveness is fed by the canonical task lifecycle stream for every provider, drops terminal/idle/inert tasks, and is cleared on session.exited. Its backgroundLiveness verdict already rides on the OrchestrationThreadShell the reaper fetches for its existing activeTurnId check — so this is a read of an existing field, not new plumbing. Two files touched.

  • Idle-by-lastSeenAt sessions are spared while backgroundLiveness === "working".
  • "monitoring" deliberately does not earn a reprieve: watch loops tick forever by design and would make sessions immortal, turning the sweep into a no-op as a leak backstop. Losing a watch loop is recoverable; losing a running agent is not.
  • The reprieve is bounded by backgroundExtensionLimitMultiplier (48× threshold = 24 h by default, configurable like the other two options), so a wedged-but-chatty agent cannot pin its session open forever.
  • The activeTurnId skip is unchanged.

Tests

Four new cases in ProviderSessionReaper.test.ts (idle+working → spared; monitoring → reaped; null → reaped; working past ceiling → reaped). Red-green verified against two deliberately unpatched variants — each new guard is independently load-bearing. Full src/provider suite: 490 passed, 6 skipped. Lint/format clean.

🤖 Generated with Claude Code


Note

Medium Risk
Changes when long-idle provider sessions are stopped, which can affect in-flight subagents vs. session leaks; behavior is bounded by a configurable ceiling and covered by new tests.

Overview
Provider session reaper no longer stops sessions that are idle on lastSeenAt but still have real background agent work, using the thread shell’s existing backgroundLiveness field.

After the usual inactivity and activeTurnId checks, the sweep skipsstopSession when backgroundLiveness === "working" and idle time is below a new cap (inactivityThresholdMs × backgroundExtensionLimitMultiplier, default 48× → 24h). "monitoring" does not get that reprieve (watch loops would otherwise keep sessions open indefinitely). null or idle past the cap is still reaped, including wedged agents that keep reporting "working".

Tests cover working (spared), monitoring/null (reaped), and working past a lowered multiplier ceiling (reaped); the harness can pass backgroundExtensionLimitMultiplier.

Reviewed by Cursor Bugbot for commit 23f9b8b. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Stop reaping provider sessions with live background work

  • The session reaper now skips stopping sessions when backgroundLiveness is 'working' and the idle duration is below a configurable ceiling (inactivityThresholdMs × backgroundExtensionLimitMultiplier, defaulting to 48×).
  • Sessions with backgroundLiveness of 'monitoring' or null continue to be reaped at the normal inactivity threshold.
  • Sessions where background work outlives the ceiling are reaped regardless.
  • A new debug log provider.session.reaper.skipped-background-work is emitted when a session is deferred.

Macroscope summarized 23f9b8b.

Root cause: the reaper treats a session as idle from
`ProviderSessionDirectory.lastSeenAt`, which only advances on turns
(`ProviderService.sendTurn` — "a turn is the clearest sign a session is
still alive"). But a session keeps working between turns: subagent
fleets, workflow runs and background shells outlive the turn that
spawned them. A session with no user turn for 30 minutes was
`stopSession`'d while its agents were still running, silently killing
30+ minutes of in-flight work.
The projection already answers this question. `ThreadBackgroundLiveness`
is fed by the same task lifecycle stream for every provider, drops
terminal/idle/inert tasks, and is cleared on `session.exited`; its
`backgroundLiveness` verdict already rides on the `OrchestrationThreadShell`
that the reaper fetches for its `activeTurnId` check. So this is a
read of an existing field, not new plumbing.
Only "working" earns a reprieve. "monitoring" is watch loops (monitor
tasks, background shells) that tick forever by design — honouring it
would make those sessions immortal and turn the sweep into a no-op as a
leak backstop. Losing a watch loop to the reaper is recoverable; losing
a running agent is not.
The reprieve is bounded by `backgroundExtensionLimitMultiplier`
(48 x the inactivity threshold = 24h by default), so an agent that is
wedged yet still emitting progress cannot pin its session open forever.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1693a9f0-0000-4db9-add2-1e487704a51c

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

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.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 8, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session reaper behavior to preserve sessions with active background work for up to 24 hours. While the fix intent is clear and tests are comprehensive, this fundamentally alters when sessions get terminated, affecting resource management and production runtime behavior. Human review recommended for this behavioral change.

You can customize Macroscope's approvability policy. Learn more.

@shivamhwp

Copy link
Copy Markdown
Collaborator

gpt-5.6-sol on behalf of shivamhwp.

Thank you for working on the session reaper. We are closing this PR because #5677 has already landed the guard that prevents the reaper from killing sessions with live background agents. It updates the same server path and covers the behavior proposed here.

The fix is present on main, so keeping this branch open would duplicate completed work.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@liusqu@shivamhwp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(server): stop reaping sessions with live background work - #5690

Closed
liusqu wants to merge 1 commit into
pingdotgg:mainfrom
liusqu:fix/session-reaper-background-tasks
Closed

fix(server): stop reaping sessions with live background work#5690
liusqu wants to merge 1 commit into
pingdotgg:mainfrom
liusqu:fix/session-reaper-background-tasks

Conversation

@liusqu

@liusquliusqu commented Aug 8, 2026

Copy link
Copy Markdown

Problem

ProviderSessionReaper measures idleness from ProviderSessionDirectory.lastSeenAt, which only advances on turns (ProviderService.sendTurn). But a Claude session keeps working between turns — subagent fleets, workflow runs, background shells all outlive the turn that spawned them. A session with no user turn for 30 minutes gets stopSession'd while its agents are still running, silently killing the in-flight work.

Hit this in production use tonight: a 30+ minute background implementation agent was killed mid-run twice; the only surviving evidence was the worktree state.

Fix

The projection already answers "is native background work still alive": ThreadBackgroundLiveness is fed by the canonical task lifecycle stream for every provider, drops terminal/idle/inert tasks, and is cleared on session.exited. Its backgroundLiveness verdict already rides on the OrchestrationThreadShell the reaper fetches for its existing activeTurnId check — so this is a read of an existing field, not new plumbing. Two files touched.

  • Idle-by-lastSeenAt sessions are spared while backgroundLiveness === "working".
  • "monitoring" deliberately does not earn a reprieve: watch loops tick forever by design and would make sessions immortal, turning the sweep into a no-op as a leak backstop. Losing a watch loop is recoverable; losing a running agent is not.
  • The reprieve is bounded by backgroundExtensionLimitMultiplier (48× threshold = 24 h by default, configurable like the other two options), so a wedged-but-chatty agent cannot pin its session open forever.
  • The activeTurnId skip is unchanged.

Tests

Four new cases in ProviderSessionReaper.test.ts (idle+working → spared; monitoring → reaped; null → reaped; working past ceiling → reaped). Red-green verified against two deliberately unpatched variants — each new guard is independently load-bearing. Full src/provider suite: 490 passed, 6 skipped. Lint/format clean.

🤖 Generated with Claude Code


Note

Medium Risk
Changes when long-idle provider sessions are stopped, which can affect in-flight subagents vs. session leaks; behavior is bounded by a configurable ceiling and covered by new tests.

Overview
Provider session reaper no longer stops sessions that are idle on lastSeenAt but still have real background agent work, using the thread shell’s existing backgroundLiveness field.

After the usual inactivity and activeTurnId checks, the sweep skipsstopSession when backgroundLiveness === "working" and idle time is below a new cap (inactivityThresholdMs × backgroundExtensionLimitMultiplier, default 48× → 24h). "monitoring" does not get that reprieve (watch loops would otherwise keep sessions open indefinitely). null or idle past the cap is still reaped, including wedged agents that keep reporting "working".

Tests cover working (spared), monitoring/null (reaped), and working past a lowered multiplier ceiling (reaped); the harness can pass backgroundExtensionLimitMultiplier.

Reviewed by Cursor Bugbot for commit 23f9b8b. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Stop reaping provider sessions with live background work

  • The session reaper now skips stopping sessions when backgroundLiveness is 'working' and the idle duration is below a configurable ceiling (inactivityThresholdMs × backgroundExtensionLimitMultiplier, defaulting to 48×).
  • Sessions with backgroundLiveness of 'monitoring' or null continue to be reaped at the normal inactivity threshold.
  • Sessions where background work outlives the ceiling are reaped regardless.
  • A new debug log provider.session.reaper.skipped-background-work is emitted when a session is deferred.

Macroscope summarized 23f9b8b.

Root cause: the reaper treats a session as idle from
`ProviderSessionDirectory.lastSeenAt`, which only advances on turns
(`ProviderService.sendTurn` — "a turn is the clearest sign a session is
still alive"). But a session keeps working between turns: subagent
fleets, workflow runs and background shells outlive the turn that
spawned them. A session with no user turn for 30 minutes was
`stopSession`'d while its agents were still running, silently killing
30+ minutes of in-flight work.
The projection already answers this question. `ThreadBackgroundLiveness`
is fed by the same task lifecycle stream for every provider, drops
terminal/idle/inert tasks, and is cleared on `session.exited`; its
`backgroundLiveness` verdict already rides on the `OrchestrationThreadShell`
that the reaper fetches for its `activeTurnId` check. So this is a
read of an existing field, not new plumbing.
Only "working" earns a reprieve. "monitoring" is watch loops (monitor
tasks, background shells) that tick forever by design — honouring it
would make those sessions immortal and turn the sweep into a no-op as a
leak backstop. Losing a watch loop to the reaper is recoverable; losing
a running agent is not.
The reprieve is bounded by `backgroundExtensionLimitMultiplier`
(48 x the inactivity threshold = 24h by default), so an agent that is
wedged yet still emitting progress cannot pin its session open forever.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1693a9f0-0000-4db9-add2-1e487704a51c

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

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.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 8, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes session reaper behavior to preserve sessions with active background work for up to 24 hours. While the fix intent is clear and tests are comprehensive, this fundamentally alters when sessions get terminated, affecting resource management and production runtime behavior. Human review recommended for this behavioral change.

You can customize Macroscope's approvability policy. Learn more.

@shivamhwp

Copy link
Copy Markdown
Collaborator

gpt-5.6-sol on behalf of shivamhwp.

Thank you for working on the session reaper. We are closing this PR because #5677 has already landed the guard that prevents the reaper from killing sessions with live background agents. It updates the same server path and covers the behavior proposed here.

The fix is present on main, so keeping this branch open would duplicate completed work.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@liusqu@shivamhwp