Skip to content

Hibernation: don't let an unknown lifecycle clobber a proven idle - #6693

Closed
lawrencecchen wants to merge 2 commits into
mainfrom
feat-hib-preserve-definitive
Closed

lawrencecchen wants to merge 2 commits into
mainfrom
feat-hib-preserve-definitive

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 23, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Agent Hibernation only hibernated agents whose lifecycle resolved to idle, and in practice mostly only codex got there. A restarting/resuming agent (and plugin agents that emit no live lifecycle) reports .unknown on SessionStart, which overwrote the .idle the agent had recorded at its previous turn end. The hibernation reader intentionally ranks unknown above idle (an unclassified agent should block hibernation), so once the stored state decayed to .unknown the panel never became eligible again.

Fix

Fix the write path, not the read priority. Workspace.setAgentLifecycle now applies AgentHibernationLifecycleState.preservingDefinitive: an incoming .unknown is kept only when there is no proven definitive state to preserve. A definitive incoming state (idle/running/needsInput) always wins, so eligibility stays positive-evidence-only and we never invent idleness.

It also stops treating an .unknown write as activity. recordAgentLifecycleChange resets the idle countdown, so a periodic .unknown heartbeat would otherwise keep a genuinely idle agent from ever accumulating enough idle time to hibernate.

This is the smallest, single-consumer slice of the broader hibernation work in #5500, split out so it can land independently.

Tests

cmuxTests/AgentHibernationTests.swift: the pure preservingDefinitive resolution table, plus two Workspace behaviors (an .unknown write preserves a stored .idle; a real .running still overrides it).

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Changes hibernation eligibility and idle-timer semantics; wrong merging could hibernate live agents or block hibernation, but scope is limited to lifecycle write paths with strong test coverage.

Overview
Fixes agent hibernation so .unknown lifecycle writes no longer wipe a stored .idle / .running / .needsInput, which had left many non-codex panels permanently ineligible after SessionStart or silent plugin agents.

AgentHibernationLifecycleState.preservingDefinitive merges incoming vs existing state: definitive incoming values still win; .unknown only applies when there is no prior definitive state. Workspace.setAgentLifecycle uses that merge and skips recordAgentLifecycleChange for .unknown, so idle heartbeats do not reset the hibernation idle timer.

When recordAgentPID sees a new PID for the same agent key, it clears stored lifecycle so a replacement process is not hibernated on the old process’s idle evidence (same-PID re-register keeps lifecycle).

Tests cover the merge table, workspace behavior, and PID-boundary clearing.

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


Summary by cubic

Prevent .unknown lifecycle reports from wiping a proven .idle, and clear stale state when an agent runtime (PID) changes, so non-codex agents can hibernate again. Also stop .unknown heartbeats from resetting idle timers.

  • Bug Fixes
    • Added AgentHibernationLifecycleState.preservingDefinitive and used it in Workspace.setAgentLifecycle to keep a definitive stored state when the incoming value is .unknown; definitive incoming values still override.
    • Stopped treating .unknown as activity in Workspace.setAgentLifecycle; only definitive states trigger recordAgentLifecycleChange, allowing genuine idle time to accrue.
    • Clear a key’s stored lifecycle in recordAgentPID when its PID changes, so a new runtime doesn’t inherit a prior runtime’s .idle and the idle countdown restarts.

Written for commit e943c78. Summary will update on new commits.

Review in cubic

Non-codex agents rarely hibernated when idle off-screen: only codex did. A
restarting/resuming agent (and plugin agents with no live lifecycle) emit an
`.unknown` lifecycle on SessionStart, which overwrote the `.idle` the agent had
reported at its previous turn end. The hibernation reader intentionally ranks
`unknown` above `idle` (an unclassified agent should block hibernation), so once
the stored state decayed to `.unknown` the panel never became eligible again.

Fix the write path instead of the read priority: `setAgentLifecycle` now applies
`AgentHibernationLifecycleState.preservingDefinitive`, so an incoming `.unknown`
is kept only when there is no proven definitive state to preserve. A definitive
incoming state (idle/running/needsInput) always wins, so eligibility stays
positive-evidence-only and we never invent idleness.

Also stop treating an `.unknown` write as activity: `recordAgentLifecycleChange`
resets the idle countdown, so a periodic `.unknown` heartbeat would otherwise
keep a genuinely idle agent from ever accumulating enough idle time to hibernate.

Unit tests cover the pure resolution table plus the two Workspace behaviors
(unknown preserves stored idle; a real running still overrides it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 23, 2026 4:22pm
cmux-staging Building Building Preview, Comment Jun 23, 2026 4:22pm

@coderabbitai

coderabbitai Bot commented Jun 23, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@lawrencecchen, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 3 minutes and 13 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses rolling per-developer review limits. Reviews become available again as older review attempts age out of the rolling limit window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d198b9cb-8e9d-44d9-8d95-afdbc7072a12

📥 Commits

Reviewing files that changed from the base of the PR and between dd7f7d3 and e943c78.

📒 Files selected for processing (4)
  • Sources/AgentHibernation/AgentHibernationLifecycleState.swift
  • Sources/Workspace+PanelLifecycle.swift
  • Sources/Workspace.swift
  • cmuxTests/AgentHibernationTests.swift
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-hib-preserve-definitive

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.

@greptile-apps

greptile-apps Bot commented Jun 23, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes agent hibernation for non-codex agents by preventing an .unknown lifecycle report (emitted at SessionStart after a restart, or by no-emit plugin agents) from overwriting a proven .idle stored from the previous turn. It also stops .unknown reports from resetting the idle countdown in AgentHibernationController.

  • Adds AgentHibernationLifecycleState.preservingDefinitive(existing:incoming:) — a pure static merge function that keeps a definitive existing state (idle/running/needsInput) when the incoming value is .unknown, while definitive incoming values always win.
  • Updates Workspace.setAgentLifecycle to use preservingDefinitive on every write and to skip recordAgentLifecycleChange for .unknown so periodic unknown heartbeats cannot prevent genuine idle time from accumulating.
  • Adds a PID-boundary lifecycle clear in Workspace.recordAgentPID: when a different PID registers for the same key, the stale lifecycle is erased so the new process cannot be hibernated on the dead process's idle evidence.

Confidence Score: 5/5

Safe to merge — changes are tightly scoped to the lifecycle write path and PID registration boundary, both backed by targeted regression tests.

The preservingDefinitive function is a pure, deterministic merge with no side effects and is fully covered by a truth-table test. The setAgentLifecycle change is contained to two decisions (which value to write, whether to record activity) with clear invariants. The PID-boundary clear in recordAgentPID follows the same agentStatusKey key-resolution pattern used throughout the file. No actor isolation issues, no blocking primitives, and no ambient global state were introduced.

No files require special attention.

Important Files Changed

Filename Overview
Sources/AgentHibernation/AgentHibernationLifecycleState.swift Adds the pure static preservingDefinitive(existing:incoming:) merge function with clear semantics: incoming .unknown defers to a definitive existing state; any definitive incoming value wins unconditionally. Logic is straightforward and fully covered by the truth-table tests.
Sources/Workspace.swift Uses preservingDefinitive on every setAgentLifecycle write and gates recordAgentLifecycleChange on definitive states only. Changes are well-scoped to the two-line lifecycle write path with no actor-isolation issues.
Sources/Workspace+PanelLifecycle.swift Adds PID-boundary lifecycle clearing in recordAgentPID: detects a changed PID for the same key before updating agentPIDs and calls clearAgentLifecycle to erase stale state. Uses the same agentStatusKey key-resolution pattern already employed elsewhere in the file.
cmuxTests/AgentHibernationTests.swift Adds five new tests: a full preservingDefinitive truth table, a workspace regression for idle surviving unknown, running overriding idle, and a PID-boundary clear test covering same-PID preservation and cross-PID erasure.

Reviews (2): Last reviewed commit: "Hibernation: clear a key's stale lifecyc..." | Re-trigger Greptile

…replaced

Autoreview caught that applying preservingDefinitive to every `.unknown` write
also blocks an explicit `set_agent_lifecycle <key> unknown` from clearing a prior
`.idle` across a process restart. A new agent runtime (new PID) for the same key
would then inherit the dead process's `.idle` and could be hibernated on stale
evidence.

Scope preservation to a single runtime: when `recordAgentPID` sees a different
PID for a key, clear that key's stored lifecycle so the new process starts from
no evidence (and the idle countdown restarts). Within one runtime an `.unknown`
heartbeat still preserves a proven definitive state; across a runtime boundary it
no longer does.

Adds a test: same-PID re-register preserves idle; a new PID clears it so a
following `.unknown` stays unknown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e943c78. Configure here.

// starts from no evidence and the idle countdown restarts.
if let panelId, let previousPID = agentPIDs[key], previousPID != pid {
_ = clearAgentLifecycle(key: agentStatusKey(forAgentPIDKey: key), panelId: panelId)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PID change skips clear without panel

Medium Severity

When recordAgentPID sees a new PID for an existing key, lifecycle is cleared only if panelId is non-nil. Hooks and sidebar paths often call recordAgentPID with a nil panel while still updating the global PID map. After this change, a following .unknown lifecycle report no longer overwrites a stored .idle, so stale idle from the previous process can remain and make hibernation treat a new runtime as eligible.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e943c78. Configure here.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Closing after deeper review: this fix targets a scenario that does not occur on current main, and the unconditional preservingDefinitive it introduced creates a real regression (autoreview flagged it twice).

Findings from first-principles verification against current main:

  • Every set_agent_lifecycle … unknown emission is a SessionStart / new-runtime path (CLI/cmux.swift: claude session-start ~23032, generic ~29802/29815/29891). At a runtime boundary the previous runtime's .idle is stale and should be cleared, which is exactly what overwriting with .unknown does today. Preserving it (this PR) would hibernate a freshly-restarted agent on the dead process's evidence.
  • resumeAgentHibernation clears lifecycle on resume (clearAgentLifecycleStates), it does not re-seed .idle. So the one case that would justify preserving idle against a SessionStart .unknown (the Fix Agent Hibernation lifecycle for non-codex agents #5500 resume-reseed premise) does not exist here.
  • There is no same-runtime .unknown heartbeat that clobbers a live .idle, so the preservation never triggers usefully.

The actual non-codex hibernation gaps on current main are covered elsewhere: the Claude notification clobber is fixed by #6694, and generic/plugin agents (opencode, grok) already reach .idle through the generic hook's completion→idle mapping. No preservingDefinitive change is needed.

This branch was successfully deployed

1 active deployment
Preview – cmux — e943c785 Deployed Jun 23, 2026 by vercel[bot]
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