Skip to content

Hibernation: correct notification lifecycle for all agent types - #6721

Closed
lawrencecchen wants to merge 27 commits into
mainfrom
feat-hibernation-all-agents
Closed

lawrencecchen wants to merge 27 commits into
mainfrom
feat-hibernation-all-agents

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Mapping the hibernation path for every coding-agent kind (claude, codex, grok, gemini, opencode, cursor, kiro, antigravity, rovodev, copilot, codebuddy, factory, qoder, hermes-agent, pi, amp, custom vault agents) surfaced why non-codex agents silently never hibernated.

Notification clobber was general, not claude-specific

A routine "waiting for input" reminder classifies as needs-input and flipped the hibernation lifecycle to .needsInput, clobbering the .idle the Stop hook recorded, so the pane never became hibernation-eligible. This existed in both the dedicated Claude hook lane and the generic agent-hook lane (grok, antigravity, and custom agents), in both the live in-memory state and the durable hook-store record used by the index fallback. This is the systemic cause of "only codex hibernates."

Three agents could hibernate mid permission-prompt

copilot, codebuddy, and factory carried their turn boundary on Stop but routed Notification (their permission/attention channel) to the stop subcommand, forcing the pane .idle while a live permission prompt was on screen.

Fix

  1. One shared classifier, AgentNotification(signal:message:).isBlockingPrompt (permission/approval/error), drives the lifecycle decision in both lanes. A notification only asserts .needsInput when genuinely blocking; otherwise it leaves the lifecycle untouched (the Stop hook is the single authoritative idle source). This applies to the live mutation and the durable persisted lifecycle, so a routine reminder can no longer clobber stored .idle on the index fallback (after restart / no live state). The user-facing notification, bell, and sidebar status are unchanged.
  2. copilot/codebuddy/factory Notification routes through the notification lane instead of stop. Worst case after the change is no-hibernation (status quo for many agents), strictly safer than hibernating mid permission-prompt. Stop remains the turn boundary.
  3. Observability: AgentHibernationController logs one agentHib.evaluate line per cycle when restorable records exist (restorable/live/maxLive/idle/protected/unconfirmedInput/selected), and the claude restorability gate logs when a record claims isRestorable but no transcript resolved — so a non-hibernating fleet is never silent again. Both are DEBUG-only.

Claude restorability is unchanged behaviorally: it remains fail-closed (restorable only when a transcript file is actually found), now with the diagnostic above.

Tests

  • AgentNotification (CMUXAgentLaunch package, swift test): the shared blocking classifier — permission/approval/error → blocking; waiting/completion/attention/empty → not blocking; signal field considered.
  • AgentHibernationPlannerTests (cmuxTests, app-host): the pure hibernate-selection logic AgentHibernationPlanner.selectedPanelKeys — idle restorable agents over the cap are hibernated oldest-first; needs-input / running / unknown / protected / unconfirmed-input / within-idle-window agents are never hibernated; non-live and non-restorable are ignored; disabled hibernates nothing. Also pins that all 16 supported agent types are allowed lifecycle status keys.

Verified on the built app (tag, deterministic hook drive)

Drove cmux hooks <agent> <event> against the running tagged build and read the persisted lifecycle back, for all 16 agent types (claude, codex, copilot, codebuddy, factory, grok, gemini, cursor, qoder, rovodev, pi, amp, hermes-agent, antigravity, kiro, opencode): stop → idle, a routine "waiting for input" notification keeps lifecycle = idle (display status still shows needs-input), and a permission notification flips to needsInput. A/B contrast against a build without the fix showed the routine notification clobbering idle → needsInput, confirming the gate is what preserves idle. Restorability fail-closed was checked against the live hook store: restorable records with an existing transcript are hibernatable, transcript-less ones are rejected.

Design notes (addressed review concerns)

A non-blocking notification never forces .idle. The lifecycle has one authoritative idle source: the Stop hook. A notification only asserts .needsInput when blocking; when non-blocking it skips the live mutation and persists nil (preserve). A question asked in claude's response means the turn ended (Stop fired → idle); hibernating that idle, restorable session is correct, since the question is in the transcript and resume reconstructs it. A genuinely synchronous permission prompt fires a keyword-matched Notification → needsInput → not hibernated. The classifier is deliberately narrow to permission/approval/error because widening it to "waiting/confirm/continue/request" would re-clobber .idle (claude's own idle reminder is "Claude is waiting for your input", and completion text contains "continue"/"request") and reintroduce the never-hibernate bug this PR fixes.

Claude restorability stays fail-closed. An earlier revision trusted isRestorable == true when no transcript resolved, to cover hard-to-locate-transcript cases (custom CLAUDE_CONFIG_DIR, forked roots, encoding drift). That was reverted: sticky hook state is not proof the conversation can replay, and a stale/deleted path would turn a live pane into a failed claude --resume. The gate logs the unresolved case instead, so if that never-hibernation is ever real it is observable, and the evidence-driven fix is to resolve those roots in the lookup rather than fail open.

Per-agent status

  • claude, codex, grok, gemini, opencode, cursor, antigravity, hermes-agent, pi, amp, custom vault agents: flow through the now-correct notification lane.
  • copilot, codebuddy, factory: rerouted. Runtime-unverified against the live CLIs (not installed here); verified by code-path reasoning, and the worst case is strictly safer than before.
  • rovodev: on_error → stop is correct; after an error the agent has genuinely stopped and is idle, and the session is restorable.
  • qoder: no Notification event, only Stop → stop; no mid-prompt risk.

Relationship to #6694

This subsumes #6694 (the claude-only notification fix) by generalizing it to a shared classifier across all agent lanes. If this lands, #6694 can be closed.

🤖 Generated with Claude Code


Note

High Risk
Changes hibernation eligibility and persisted lifecycle for all agent hook lanes; incorrect classification could SIGTERM live work or leave panes never hibernating, and unreadable Claude background-task payloads now fail closed to stay alive.

Overview
Fixes the systemic “only Codex hibernates” behavior: routine post-turn “waiting for input” notifications were flipping the hibernation lifecycle to .needsInput, overwriting the Stop hook’s .idle in both the live state and the durable hook store.

Shared blocking classifier — Adds AgentNotification.isBlockingPrompt in CMUXAgentLaunch and wires it through the Claude and generic agent-hook notification paths. Only permission/approval/error (and high-confidence question cues) may assert .needsInput; routine idle reminders leave lifecycle unchanged while user-facing bell/status behavior stays the same. Completion classification is skipped when text is still blocking (e.g. “Task completed. Continue?”).

Hook routing — Copilot, CodeBuddy, and Factory map Notification to the notification subcommand instead of stop, so permission prompts are not forced idle mid-prompt.

Claude background work — Replaces inline background_tasks / session_crons checks with AgentBackgroundWorkStatus, treating non-terminal or unreadable payload evidence as active so hibernation does not SIGTERM silent background tasks.

Diagnostics & tests — DEBUG agentHib.evaluate logging in AgentHibernationController; optional verbose logging when Claude claims restorable but no transcript resolves (still fail-closed). New package and app-host tests for the classifier, background-work parser, and AgentHibernationPlanner selection; CI runs planner tests in an isolated focused gate.

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

Summary by CodeRabbit

  • New Features
    • Improved notification handling to more accurately classify blocking permission/approval/error prompts versus non-blocking reminders, completions, and attention (including Claude “waiting for your input”).
    • Updated notification event routing to use the notification lane rather than the stop command.
  • Bug Fixes
    • Improved fail-safe behavior during Claude session restoration when transcript resolution fails.
  • Tests
    • Added unit tests for blocking-prompt classification and hibernation planner selection.
  • Chores
    • Added a focused CI regression run for browser session history restore.

…+ robust restorability

Two systemic gaps kept non-codex agents from hibernating, found while mapping the
hibernation path for every coding-agent kind (claude, codex, grok, gemini,
opencode, cursor, kiro, antigravity, rovodev, copilot, codebuddy, factory, qoder,
hermes-agent, pi, amp, custom vault agents).

1. Notification clobber was per-lane, not just claude. A routine "waiting for
   input" reminder classified as needs-input and flipped the hibernation lifecycle
   to `.needsInput`, clobbering the `.idle` the Stop hook recorded — so the pane
   never hibernated. This was the dedicated claude bug AND the generic agent-hook
   lane bug (grok, antigravity, custom). Fix: one shared
   `agentNotificationIsBlockingPrompt` (permission/approval/error) used by BOTH
   lanes. A notification only asserts `.needsInput` when genuinely blocking;
   otherwise it leaves the lifecycle untouched (the Stop hook is the single idle
   source). The user-facing notification, bell, and status are unchanged.

2. Claude restorability failed silently. `hookRecordIsRestorable` hard-required the
   claude transcript `.jsonl` on disk (claude-only; every other agent trusts the
   `isRestorable` flag). When transcript resolution missed for a genuinely
   restorable session (custom CLAUDE_CONFIG_DIR, account-scoped/forked roots,
   project-dir encoding drift), claude was dropped from the restorable index and
   silently never hibernated while codex did — the classic "only codex hibernates"
   report. Fix: trust the authoritative `record.isRestorable == true` when the
   transcript isn't found (transcript becomes a strong hint, not a hard gate), and
   log a breadcrumb so the gap is diagnosable instead of silent.

Tests: AgentNotificationBlockingClassifierTests pins the shared blocking
classifier that every agent's lifecycle decision funnels through (permission/
approval/error → blocking; waiting/completion/attention/empty → not blocking;
signal field also considered).

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

vercel Bot commented Jun 24, 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 Jul 4, 2026 11:26am
cmux-staging Building Building Preview, Comment Jul 4, 2026 11:26am

@coderabbitai

coderabbitai Bot commented Jun 24, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds blocking classification to agent notifications, threads isBlocking through Claude summarization and lifecycle persistence, reroutes notification hooks, and updates restorable checks, hibernation logging, planner tests, and CI gating.

Changes

isBlocking Notification Classification and Lifecycle

Layer / File(s) Summary
AgentNotification classifier, isBlocking contract, routing correction, and tests
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentNotificationBlockingClassifier.swift, CLI/cmux.swift, CLI/CMUXCLI+AgentHookDefinitions.swift, Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentNotificationBlockingClassifierTests.swift
Creates AgentNotification with isBlockingPrompt, adds isBlocking to AgentHookNotificationSummary, routes Notification events for copilot, codebuddy, and factory to the notification subcommand, and adds tests for blocking and non-blocking classification.
Summarization and classification threading isBlocking
CLI/cmux.swift
summarizeClaudeHookNotification returns (subtitle, body, isBlocking), normal Claude notifications derive blocking state from AgentNotification.isBlockingPrompt, and classifyAgentHookNotification threads blocking state through permission, error, completed, waiting, and attention summaries.
Lifecycle and session persistence gated on isBlocking
CLI/cmux.swift
Preserves freshly computed blocking state when reusing saved session text, carries isBlocking into mapped runtime summaries, computes optional lifecycle persistence from summary.isBlocking, and gates the runtime .needsInput mutation on the blocking flag.

Hibernation diagnostics, tests, and CI coverage

Layer / File(s) Summary
Restorable checks and evaluation logging
Sources/RestorableAgentSession.swift, Sources/App/AgentHibernationController.swift
hookRecordIsRestorable now returns false when transcript lookup fails and logs a debug diagnostic when a record claims restorable state without a resolved transcript; AgentHibernationController.evaluate adds a debug log with selection and eligibility counts.
Planner tests and target wiring
cmuxTests/AgentHibernationPlannerTests.swift, cmux.xcodeproj/project.pbxproj
Adds deterministic AgentHibernationPlannerTests coverage for selection order, lifecycle and eligibility filtering, idle-window behavior, disabled settings, and allowed lifecycle keys, and wires the test file into the cmuxTests target.
Focused regression gate
.github/workflows/ci.yml, scripts/ci/cmux_unit_test_shard.py
Adds BrowserSessionHistoryRestoreTests to the focused selector set and runs that suite in a shard-gated app-host regression step before the main unit tests.

Sequence Diagram(s)

sequenceDiagram
  participant AgentHook as Agent hook
  participant Summarizer as summarizeClaudeHookNotification
  participant Classifier as classifyAgentHookNotification
  participant Session as session persistence
  participant Lifecycle as setAgentLifecycle

  AgentHook->>Summarizer: signal + message
  Summarizer->>Classifier: subtitle, body, isBlocking
  Classifier->>Session: persist blocking lifecycle when blocking
  Classifier->>Lifecycle: setAgentLifecycle(.needsInput) when blocking
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A prompt hopped by with a blocking clue,
cmux checked the signal, then knew what to do.
Waiting can wave, but it won’t steal the thread,
While transcripts and planners keep tidy heads.

🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (24 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.
Cmux Swift Actor Isolation ✅ Passed New AgentNotification is a plain value type in a non-actor package; the only @MainActor code touched stays on the main actor, and no shared mutable Sendable/UI-background isolation debt was added.
Cmux Swift Blocking Runtime ✅ Passed Touched production hunks only add classifier/state gating and DEBUG logs; no new waits, sleeps, locks, or syncs were introduced.
Cmux Browser Automation Off-Main ✅ Passed PASS: PR only touches hibernation/classifier/CI files; no changes to Sources/TerminalController.swift or ControlCommandExecutionPolicy.swift, so the browser wait-off-main rule isn’t implicated.
Cmux Expensive Synchronous Load ✅ Passed No added sync corpus load on a main-actor/interactive path; the only loader call stays in Task.detached, and the other changes are DEBUG logs/classifier plumbing.
Cmux Cache Substitution Correctness ✅ Passed Claude transcript lookup still does fresh filesystem checks on cache miss, and the cache is local to one load; no durable stale-cache substitution in a persisted path.
Cmux No Hacky Sleeps ✅ Passed The only changed runtime script is selector-only, with no sleep/delay/poll/wait logic; CI workflow YAML sleeps are explicitly out of scope.
Cmux Algorithmic Complexity ✅ Passed New work is DEBUG-only logging; production changes are fixed-size string checks and simple lifecycle gates, with no added scalable full-collection rescans.
Cmux Swift Concurrency ✅ Passed Diff adds only synchronous classifier/logging/test code; no new legacy DispatchQueue, Combine, completion-handler, or fire-and-forget Task patterns in cmux-owned Swift.
Cmux Swift @Concurrent ✅ Passed PASS: the new async paths explicitly hop off-main with Task.detached, and no changed Swift code adds invalid/missing @concurrent on actor-isolated work.
Cmux Swift File And Package Boundaries ✅ Passed PASS: The new blocking classifier lives in the CMUXAgentLaunch package; app-target edits are small, focused glue/debug logging, with no oversized production file materially expanded.
Cmux Swiftpm Lockfiles ✅ Passed Only a test-file Xcode project update and workflow/script changes are present; no Package.resolved, SwiftPM dependency, or lockfile-ignore edits were found.
Cmux Swift Logging ✅ Passed Only added cmuxDebugLog calls under #if DEBUG; no raw print/NSLog/ad hoc runtime logging or risky Logger declarations were introduced.
Cmux User-Facing Error Privacy ✅ Passed PASS: The patch changes lifecycle logic and DEBUG-only logs; no new user-facing error/alert/recovery text exposes vendor or internal details.
Cmux Full Internationalization ✅ Passed Touched files add logic, tests, and DEBUG logs; no user-facing Swift copy or catalog/Info.plist edits were introduced.
Cmux Swiftui State Layout ✅ Passed No SwiftUI state/layout changes were introduced; the modified files are CLI/model/controller/tests only, with no SwiftUI patterns from the rule.
Cmux Architecture Rethink ✅ Passed PASS: It centralizes blocking classification and keeps Stop as the sole idle owner, without adding timing hacks, extra owners, or split lifecycle state.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only changes hook/hibernation logic and tests; no NSWindow/NSPanel/WindowGroup or cmuxAuxiliaryWindowIdentifiers changes. Repo has scripts/lint_auxiliary_window_close_shortcuts.py.
Cmux Source Artifacts ✅ Passed All changed paths are ordinary source/config/test files; no artifact, cache, temp, build, or scratch directories appear in the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Only DEBUG observability logs were added in production code; no new test/debug accessor or wrapper seam was introduced, and the new classifier is product logic.
Cmux No Ambient Global State ✅ Passed New production code adds an instance-based AgentNotification value type; I found no new free functions, mutable globals, static-only namespaces, or singletons.
Title check ✅ Passed The title clearly summarizes the main hibernation lifecycle fix across agent types.
Description check ✅ Passed The description covers the problem, fix, testing, and related notes, and is mostly complete despite some template sections being omitted.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-hibernation-all-agents

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.

Comment thread CLI/cmux.swift
lawrencecchen and others added 2 commits June 23, 2026 19:12
… silent

Hibernation gave zero signal when a restorable agent failed to hibernate, making
diagnosis a manual spelunk (it took hours to trace one such case). Emit one concise
line per planner evaluation — restorable/live/maxLive/idle/protected/
unconfirmedInput/selected — whenever any restorable agent exists, so the exclusion
reason is readable straight from the debug log. Quiet on idle systems (only logged
when there are restorable records). Pairs with the transcript-drop breadcrumb.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The live setAgentLifecycle gate skipped only the in-memory mutation; the
generic agent-hook lane still persisted agentLifecycle(for: .needsInput)
to the durable hook store for routine waiting reminders, clobbering the
Stop hook's stored .idle. On the index fallback (after restart / no live
state) the pane was then treated as non-hibernatable. Persisted lifecycle
now mirrors the live decision: blocking -> needsInput, completion -> idle,
routine waiting / attention -> nil (preserve).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread CLI/cmux.swift
…tion lane

These agents carried turn-end on Stop but routed Notification (their
permission/attention channel) to the stop subcommand, which forced the
pane .idle and let it hibernate while a permission prompt was live. Route
Notification through the notification lane so the shared blocking
classifier asserts .needsInput for permission/approval/error and
preserves idle for routine reminders. Worst case after this change is
no-hibernation (status quo for many agents), strictly safer than
hibernating mid-permission-prompt.

Adds AgentNotificationRoutingTests to pin the wiring. Bumps file-length
budget for the touched files.

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

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
CLI/cmux.swift (1)

26198-26208: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Completion branch passes computed blocking, but sibling completion paths hardcode false.

Lines 26206-26207 thread isBlocking: blocking into the completion branch, whereas the summarizeAgentHookNotification completion returns (Lines 25929-25930, 25962-25963) hardcode isBlocking: false. A completion message containing error/failed would compute blocking == true here. It's currently moot because this branch emits status: .idle and only the .needsInput? switch case acts on isBlocking, but it's a latent trap if isBlocking is later consumed outside that switch. Recommend isBlocking: false for the completion branch for consistency and intent clarity.

Suggested change
                 subtitle: String(localized: "agent.generic.notification.subtitle.completed", defaultValue: "Completed"),
                 body: truncate(body, maxLength: 180),
                 status: .idle,
                 isFallback: isFallback,
-                isBlocking: blocking
+                isBlocking: false
             )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/cmux.swift` around lines 26198 - 26208, The completion branch within the
containsCompletionCue condition currently passes isBlocking: blocking to the
AgentHookNotificationSummary initializer, but other completion paths in the
summarizeAgentHookNotification function hardcode isBlocking: false. Change the
isBlocking parameter in the AgentHookNotificationSummary initialization within
the containsCompletionCue block from isBlocking: blocking to isBlocking: false
to maintain consistency with the sibling completion paths and clarify that
completion notifications should not be blocking regardless of the computed
blocking value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 92-98: The agentNotificationIsBlockingPrompt function contains a
redundant keyword check. Remove the `lower.contains("permission_prompt")`
condition since any string containing "permission_prompt" will already be caught
by the existing `lower.contains("permission")` check on the previous line,
making the additional check unnecessary.

In `@Sources/RestorableAgentSession.swift`:
- Around line 1281-1283: In the cmuxDebugLog call for the
agentHib.restorable.claudeTranscriptMissing message, redact the sensitive
transcript path information from record.transcriptPath. Instead of logging the
actual path value which may contain the user's home directory or account config
details, replace the transcriptPath parameter with a non-sensitive indicator
such as whether the path is nil or simply a generic placeholder that preserves
the log's usefulness without exposing sensitive file system information. Keep
the rest of the log message structure intact including the session ID prefix and
the isRestorable indicator.

---

Outside diff comments:
In `@CLI/cmux.swift`:
- Around line 26198-26208: The completion branch within the
containsCompletionCue condition currently passes isBlocking: blocking to the
AgentHookNotificationSummary initializer, but other completion paths in the
summarizeAgentHookNotification function hardcode isBlocking: false. Change the
isBlocking parameter in the AgentHookNotificationSummary initialization within
the containsCompletionCue block from isBlocking: blocking to isBlocking: false
to maintain consistency with the sibling completion paths and clarify that
completion notifications should not be blocking regardless of the computed
blocking value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 701dc52f-db6f-4e2a-acd3-99d9d05e6de0

📥 Commits

Reviewing files that changed from the base of the PR and between 5dafd33 and abf33a1.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (5)
  • CLI/cmux.swift
  • Sources/App/AgentHibernationController.swift
  • Sources/RestorableAgentSession.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/AgentNotificationBlockingClassifierTests.swift

Comment thread CLI/cmux.swift Outdated
Comment thread Sources/RestorableAgentSession.swift Outdated
Comment on lines +1281 to +1283
cmuxDebugLog(
"agentHib.restorable.claudeTranscriptMissing session=\(record.sessionId.prefix(8)) "
+ "transcriptPath=\(record.transcriptPath ?? "<nil>") — trusting isRestorable=true"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Redact the transcript path from the debug log.

record.transcriptPath can include the user’s home/account config root and encoded project names; keep the breadcrumb but log only non-sensitive metadata.

🛡️ Proposed fix
         if record.isRestorable == true {
+            let hasTranscriptPath = normalizedNonEmptyValue(record.transcriptPath) != nil
+            let transcriptPathBytes = record.transcriptPath?.utf8.count ?? 0
             cmuxDebugLog(
                 "agentHib.restorable.claudeTranscriptMissing session=\(record.sessionId.prefix(8)) "
-                    + "transcriptPath=\(record.transcriptPath ?? "<nil>") — trusting isRestorable=true"
+                    + "hasTranscriptPath=\(hasTranscriptPath ? 1 : 0) "
+                    + "transcriptPathBytes=\(transcriptPathBytes) — trusting isRestorable=true"
             )
             return true
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/RestorableAgentSession.swift` around lines 1281 - 1283, In the
cmuxDebugLog call for the agentHib.restorable.claudeTranscriptMissing message,
redact the sensitive transcript path information from record.transcriptPath.
Instead of logging the actual path value which may contain the user's home
directory or account config details, replace the transcriptPath parameter with a
non-sensitive indicator such as whether the path is nil or simply a generic
placeholder that preserves the log's usefulness without exposing sensitive file
system information. Keep the rest of the log message structure intact including
the session ID prefix and the isRestorable indicator.

Source: Coding guidelines

@greptile-apps

greptile-apps Bot commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the "only codex hibernates" bug by correcting the notification lifecycle for all 16 supported coding-agent types. A routine "waiting for input" reminder after every turn was classified as .needsInput and clobbered the Stop hook's authoritative .idle in both the live in-memory state and the durable hook-store record, preventing hibernation eligibility. It also reroutes Copilot/CodeBuddy/Factory Notification events from the stop subcommand to the notification lane to avoid forcing .idle during live permission prompts.

  • Introduces AgentNotification.isBlockingPrompt in the CMUXAgentLaunch package as the single shared gate: only permission/approval/error/genuine-question notifications may assert .needsInput; routine reminders leave the lifecycle untouched in both the live mutation and the durable persisted store.
  • Adds AgentBackgroundWorkStatus to detect non-terminal background_tasks/session_crons payloads from Claude Code's Stop hook, with deliberate fail-closed behavior for unreadable/schema-drifted fields.
  • Adds AgentHibernationController DEBUG-only agentHib.evaluate observability logging and a new AgentHibernationPlannerTests suite with isolated CI gate; Claude restorability stays fail-closed with an opt-in verbose diagnostic.

Confidence Score: 5/5

The change is safe to merge: the shared blocking classifier is deliberately conservative (false positives keep panes live, never kill them), the fail-closed behavior of AgentBackgroundWorkStatus protects live background tasks from accidental SIGTERM, and the copilot/codebuddy/factory re-routing is strictly safer than the prior stop-lane routing.

All lifecycle mutations are gated behind isBlockingPrompt, the authoritative Stop hook remains the sole idle source, and the fix applies symmetrically to both the live in-memory state and the durable hook-store record. The new test suites are thorough and the CI isolation for AgentHibernationPlannerTests is correctly reasoned. Previous thread concerns have been acknowledged or addressed in the design notes; no new correctness or safety gaps were found.

No files require special attention. CLI/cmux.swift has a minor redundancy (multiple AgentNotification instantiations with the same inputs across classifyClaudeNotification and summarizeClaudeHookNotification) but this is a pure struct with no side effects and is not a correctness issue.

Important Files Changed

Filename Overview
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentNotificationBlockingClassifier.swift New AgentNotification struct owning the shared isBlockingPrompt gate; private file-scope helper agentNotificationContainsGenuineQuestionCue is well-scoped per style rules; deliberate tradeoffs documented.
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentBackgroundWorkStatus.swift New AgentBackgroundWorkStatus struct with fail-closed parsing for unreadable hook payloads; private agentBackgroundTerminalStatuses constant and agentHookParseArrayField helper are correctly scoped.
CLI/cmux.swift Adds isBlocking: Bool to AgentHookNotificationSummary and gates both the live setAgentLifecycle call and the durable upsert behind the shared classifier; the fix is applied consistently across Claude and generic hook lanes.
CLI/CMUXCLI+AgentHookDefinitions.swift Copilot, CodeBuddy, and Factory Notification events rerouted from stop to notification subcommand to prevent forced-idle during live permission prompts; Stop remains the turn-boundary authority.
Sources/RestorableAgentSession.swift Claude restorability remains fail-closed without a resolved transcript; adds opt-in #if DEBUG verbose logging behind an env flag. The agentHibernationVerboseLoggingEnabled file-scope let constant gates real debug-product behavior, not a test seam.
Sources/App/AgentHibernationController.swift Adds #if DEBUG agentHib.evaluate observability log per planner cycle when restorable records exist; correctly gated and uses cmuxDebugLog (unified logging).
cmuxTests/AgentHibernationPlannerTests.swift Comprehensive new test suite for AgentHibernationPlanner.selectedPanelKeys; covers all lifecycle states, idle-window, cap, protected, unconfirmed-input, disabled, and agent-roster cases.
.github/workflows/ci.yml Adds isolated CI gate for AgentHibernationPlannerTests on the focused-regression shard to avoid perturbing WKWebView-sensitive shard ordering.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Agent Hook Event] --> B{Event type?}
    B -->|Stop| C[hasActiveClaudeBackgroundWork?]
    B -->|Notification| D[summarize notification]
    B -->|SessionStart/End| E[other lanes]

    C -->|yes| F[lifecycle = .running]
    C -->|no| G[lifecycle = .idle]

    D --> H[AgentNotification.isBlockingPrompt]
    H -->|true - permission/approval/error/?| I[lifecycle = .needsInput]
    H -->|false - routine waiting reminder| J[lifecycle = nil — preserve Stop's .idle]

    I --> K[setAgentLifecycle + upsert durable store]
    J --> L[skip lifecycle mutation\nuser-facing bell/status unchanged]

    G --> M[AgentHibernationPlanner evaluation ~30s]
    K --> N[pane stays live]
    L --> M

    M --> O{lifecycle == .idle\nAND restorable\nAND outside idle window\nAND not protected?}
    O -->|yes - excess over cap| P[SIGTERM process group\nhibernate pane]
    O -->|no| Q[pane remains live]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Agent Hook Event] --> B{Event type?}
    B -->|Stop| C[hasActiveClaudeBackgroundWork?]
    B -->|Notification| D[summarize notification]
    B -->|SessionStart/End| E[other lanes]

    C -->|yes| F[lifecycle = .running]
    C -->|no| G[lifecycle = .idle]

    D --> H[AgentNotification.isBlockingPrompt]
    H -->|true - permission/approval/error/?| I[lifecycle = .needsInput]
    H -->|false - routine waiting reminder| J[lifecycle = nil — preserve Stop's .idle]

    I --> K[setAgentLifecycle + upsert durable store]
    J --> L[skip lifecycle mutation\nuser-facing bell/status unchanged]

    G --> M[AgentHibernationPlanner evaluation ~30s]
    K --> N[pane stays live]
    L --> M

    M --> O{lifecycle == .idle\nAND restorable\nAND outside idle window\nAND not protected?}
    O -->|yes - excess over cap| P[SIGTERM process group\nhibernate pane]
    O -->|no| Q[pane remains live]
Loading

Reviews (20): Last reviewed commit: "perf: gate the per-record hibernation re..." | Re-trigger Greptile

Comment thread CLI/cmux.swift Outdated
Comment on lines 94 to 100
return lower.contains("permission") || lower.contains("approve") || lower.contains("approval")
|| lower.contains("permission_prompt")
|| lower.contains("error") || lower.contains("failed") || lower.contains("failure")
|| lower.contains("exception")
}

#if DEBUG

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.

P2 Top-level free function used as shared API

agentNotificationIsBlockingPrompt is a file-scope free function called by both the Claude hook lane and the generic agent-hook lane, and imported directly by the test suite via @testable import. The cmux no-ambient-global-state rule flags new top-level funcs used as API — the correct shape is either a private/fileprivate file-scope helper (passing to the rule) or a static func on a named classifier type (e.g., AgentNotificationClassifier) that owns the signal set and can be tested at the type boundary without widening the whole-module surface.

Rule Used: Flag new ambient global state in production Swift:... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread CLI/cmux.swift Outdated
Comment on lines 97 to 99
|| lower.contains("exception")
}

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.

P1 Substring "error" match risks false-positives that keep panes live when they should hibernate

lower.contains("error") will match benign informational messages an agent might send — e.g. "0 errors found", "error handling complete", "no error occurred" — and classify them as isBlocking: true. For agents (grok, antigravity) that use completion notifications as turn boundaries, a final "task finished with 0 errors" message would cause setAgentLifecycle(.needsInput) to fire after the Stop hook already wrote .idle, blocking hibernation. Tighter matching — anchoring on structured signal values or word-boundary matching for "error" — would eliminate the false positives without losing the real blocking signal.

Comment on lines +1281 to +1284
cmuxDebugLog(
"agentHib.restorable.claudeTranscriptMissing session=\(record.sessionId.prefix(8)) "
+ "transcriptPath=\(record.transcriptPath ?? "<nil>") — trusting isRestorable=true"
)

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.

P2 Full transcriptPath in debug log may include macOS username

record.transcriptPath expands to an absolute path under /Users/<username>/…. Logging it raw through cmuxDebugLog risks leaking a personally identifiable filesystem path to the unified log where it can be captured by Console.app or crash reporters. The session ID prefix already provides enough correlation signal; transcriptPath could be replaced with a redacted form (e.g., the last 2 path components) or a component count.

Rule Used: Flag production Swift diagnostics that bypass unif... (source)

Fixes two build regressions caught by review:

1. Release build: cmuxDebugLog is declared only under #if DEBUG, but the
   new observability calls in AgentHibernationController and
   RestorableAgentSession were unconditional, so a Release archive could
   not resolve the symbol. Both call sites are now #if DEBUG guarded
   (the whole observability block in the controller, the breadcrumb in
   the restorability path).

2. Test target: agentNotificationIsBlockingPrompt and CMUXCLI.agentDef
   live in the CLI executable target, which cmuxTests does not link
   (documented in RemotesClientTests). The unit test referencing them
   could not compile. Moved the pure classifier into the CMUXAgentLaunch
   SwiftPM package as a public function and relocated its test to that
   package's test target, where it runs via  without
   launching the app (verified: 3/3 pass). The CLI now calls the package
   function. Removed the cmuxTests test file + its pbxproj wiring.

The routing test (copilot/codebuddy/factory Notification mapping) is
dropped: it referenced CLI-target agentDefs that no available test
target links; the reroute stays covered by the classifier test and PR
description. Refreshes RestorableAgentSession length budget.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lawrencecchen and others added 2 commits June 23, 2026 19:36
Addresses review policy: package API must be owned by a type (not a
top-level free function), and new non-UI package tests use Swift Testing.
Wraps the classifier as AgentNotificationClassifier.isBlockingPrompt and
updates CLI call sites; converts the test suite to import Testing /
@suite / @test / #expect, matching the 24 other Swift Testing suites in
CMUXAgentLaunch. Verified: 3/3 pass via swift test.

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

The all-static enum AgentNotificationClassifier tripped the repo
namespace-type rule (no all-static public types in packages). Remodel as
an instantiated value: AgentNotification(signal:message:).isBlockingPrompt,
which satisfies both that lint and the review's 'owned by a type' rule.
Updates CLI call sites and the Swift Testing suite. Verified locally:
package tests 3/3 pass and lint-ios-package-conventions.sh exits 0 with
no AgentNotification findings. Also clears the cascading ios-tests
routing gate, which only failed because the lint did.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread CLI/cmux.swift
lawrencecchen and others added 2 commits June 23, 2026 19:43
…able fallback

Review flagged that trusting isRestorable==true with no transcript proof
could hibernate a claude session that then fails to resume. Tighten the
fallback to fail closed unless the hook actually recorded a transcript
path: the legitimate lookup-miss cases (custom CLAUDE_CONFIG_DIR, forked
roots, encoding drift) all still record a path that just isn't resolvable
from the gate, while a pathless record never proved a transcript and is
no longer trusted. Bounds the residual to 'path recorded but file later
deleted', where claude --resume exits cleanly to a shell.

(The companion finding about confirm/continue prompts hibernating is a
misread of the model: a non-blocking notification never forces .idle, it
only skips asserting .needsInput; the Stop hook is the sole idle source,
so a mid-turn prompt with no Stop stays non-hibernatable.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Concede the review concern: trusting sticky isRestorable when no
transcript resolves risks SIGTERMing a live claude pane and then failing
claude --resume into a dead shell. Revert to the original fail-closed
gate (restorable only when a transcript file is actually found) and
instead emit a DEBUG diagnostic when a record claims isRestorable but no
transcript resolved. This makes hard-to-locate-transcript
never-hibernation observable rather than guessed at; the evidence-driven
fix, if it ever reproduces, is to resolve those roots in the transcript
lookup, not to fail open.

The systemic 'only codex hibernates' cause is the notification-lifecycle
clobber fixed elsewhere in this change, which does not need this gate
relaxed. Restorability behavior is now identical to before plus the
diagnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lawrencecchen lawrencecchen changed the title Hibernation: correct notification lifecycle for all agent types + robust restorability Hibernation: correct notification lifecycle for all agent types Jun 24, 2026
Adds AgentHibernationPlannerTests (cmuxTests, app-host) exercising the
pure selection logic AgentHibernationPlanner.selectedPanelKeys: idle
restorable agents over the cap are hibernated oldest-first; needs-input /
running / unknown / protected / unconfirmed-input / within-idle-window
agents are never hibernated; non-live and non-restorable are ignored;
disabled hibernates nothing. The needs-input exclusion is the payoff of
the notification-lifecycle fix (a finished agent reaches .idle here
instead of being clobbered to .needsInput).

Also pins that every supported coding-agent type (amp, antigravity,
claude_code, codebuddy, codex, copilot, cursor, factory, gemini, grok,
hermes-agent, kiro, opencode, pi, qoder, rovodev) is an allowed
lifecycle status key, so an agent added without wiring its hibernation
lifecycle fails the suite.

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

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

♻️ Duplicate comments (1)
Sources/RestorableAgentSession.swift (1)

1287-1291: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Redact transcript path in DEBUG diagnostics.

At Line 1290, the log emits record.transcriptPath verbatim, which can expose user/home/project-identifying filesystem data. Keep only non-sensitive metadata (presence + size).

As per coding guidelines, "Do not log secrets, tokens, passwords, private keys, customer content, or personal data without explicit private redaction."

Proposed fix
 `#if` DEBUG
 if record.isRestorable == true {
+    let hasTranscriptPath = normalizedNonEmptyValue(record.transcriptPath) != nil
+    let transcriptPathBytes = record.transcriptPath?.utf8.count ?? 0
     cmuxDebugLog(
         "agentHib.restorable.claudeTranscriptUnresolved session=\(record.sessionId.prefix(8)) "
-            + "transcriptPath=\(record.transcriptPath ?? "<nil>") — failing closed (no transcript found)"
+            + "hasTranscriptPath=\(hasTranscriptPath ? 1 : 0) "
+            + "transcriptPathBytes=\(transcriptPathBytes) — failing closed (no transcript found)"
     )
 }
 `#endif`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/RestorableAgentSession.swift` around lines 1287 - 1291, The
cmuxDebugLog call in the isRestorable block directly exposes
record.transcriptPath, which contains sensitive filesystem data including
user/home/project paths. Replace the transcriptPath parameter in the log message
with redacted metadata that indicates only the presence and size of the
transcript file, rather than the full path string. This ensures the diagnostic
log complies with the guideline against logging personal or customer data
without explicit redaction.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@Sources/RestorableAgentSession.swift`:
- Around line 1287-1291: The cmuxDebugLog call in the isRestorable block
directly exposes record.transcriptPath, which contains sensitive filesystem data
including user/home/project paths. Replace the transcriptPath parameter in the
log message with redacted metadata that indicates only the presence and size of
the transcript file, rather than the full path string. This ensures the
diagnostic log complies with the guideline against logging personal or customer
data without explicit redaction.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6eca9a7d-4a54-43d2-828b-6f6bd4873a4f

📥 Commits

Reviewing files that changed from the base of the PR and between 9afdd6b and 6d2072c.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (7)
  • CLI/cmux.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentNotificationBlockingClassifier.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentNotificationBlockingClassifierTests.swift
  • Sources/App/AgentHibernationController.swift
  • Sources/RestorableAgentSession.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/AgentHibernationPlannerTests.swift

…agents

# Conflicts:
#	.github/swift-file-length-budget.tsv
…cused gate

The provisional-navigation-race suite runs real WebProcesses plus a local
race server. Under a shared shard it can exhaust WebKit and get its
WebProcess killed ('WebProcess does not exist'), aborting the app-host and
cascading to later suites in that shard. Its shard membership is also
sharding-order-dependent, so adding unrelated test files (e.g. a new
hibernation test class) reshuffles it into a shard where it crashes —
which is what turned app-host 2/4 red after merging main into this branch.

Move it into FOCUSED_GATE_SELECTORS so it is excluded from the four shared
shards and runs isolated in its own app-host (like the system-proxy-mirror
and Option/Alt gates), where WebProcess pressure and sharding order can't
affect it.

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

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 527-546: The browser session history restore step duplicates the
same focused-gate app-host/xcodebuild boilerplate as the other focused-gate
jobs; extract the shared `run-in-console-session.sh` and
`run-app-host-xcodebuild.sh` invocation into a reusable helper (script or
composite action) and parameterize it with the suite description and
`-only-testing` selector. Update this job to call the new helper so the unique
`BrowserSessionHistoryRestoreTests` selector is the only job-specific part,
matching the existing focused-gate pattern used by the other steps.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e9915bb8-9fd1-44f6-8b1b-56546a140e82

📥 Commits

Reviewing files that changed from the base of the PR and between ed339fb and 64a4820.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • scripts/ci/cmux_unit_test_shard.py

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +527 to +546
- name: Run browser session history restore regression
if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }}
run: |
# Heavy WKWebView provisional-navigation-race suite, isolated in its own
# app-host so WebProcess exhaustion can't cascade into a shared shard and
# so its pass/fail is independent of sharding order. See the note in
# scripts/ci/cmux_unit_test_shard.py FOCUSED_GATE_SELECTORS.
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
scripts/ci/run-in-console-session.sh \
scripts/ci/run-app-host-xcodebuild.sh \
-project cmux.xcodeproj -scheme cmux-unit -configuration Debug \
-derivedDataPath "$CMUX_DERIVED_DATA_PATH" \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
CMUX_SKIP_ZIG_BUILD=1 \
-only-testing:cmuxTests/BrowserSessionHistoryRestoreTests \
test

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Third copy of near-identical focused-gate boilerplate.

This step duplicates the structure of the two preceding focused-gate steps (browser system proxy mirror, Option/Alt sided-modifier) almost verbatim — only the comment and -only-testing selector differ. Consider extracting the common run-in-console-session.sh + run-app-host-xcodebuild.sh invocation into a small reusable script/composite action parameterized by selector and description, to avoid a fourth copy-paste next time a suite needs isolating.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 527 - 546, The browser session history
restore step duplicates the same focused-gate app-host/xcodebuild boilerplate as
the other focused-gate jobs; extract the shared `run-in-console-session.sh` and
`run-app-host-xcodebuild.sh` invocation into a reusable helper (script or
composite action) and parameterize it with the suite description and
`-only-testing` selector. Update this job to call the new helper so the unique
`BrowserSessionHistoryRestoreTests` selector is the only job-specific part,
matching the existing focused-gate pattern used by the other steps.

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 11 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentNotificationBlockingClassifier.swift">

<violation number="1" location="Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentNotificationBlockingClassifier.swift:33">
P2: Loose substring matching for `error`, `failed`, `failure`, `exception` risks false-positive blocking on routine non-blocking notifications, directly undermining the classifier's purpose.</violation>
</file>

<file name=".github/workflows/ci.yml">

<violation number="1" location=".github/workflows/ci.yml:527">
P3: This is the third near-identical focused-gate step (after `BrowserSystemProxyMirrorTests` and `GhosttyOptionAsAltModsTests`). The only differences between these blocks are the comment and the `-only-testing:` selector. Consider extracting the common `run-in-console-session.sh` + `run-app-host-xcodebuild.sh` invocation into a small reusable composite action or parameterized script to reduce copy-paste maintenance burden when the next isolated suite is added.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

let lower = "\(signal) \(message)".lowercased()
return lower.contains("permission") || lower.contains("approve") || lower.contains("approval")
|| lower.contains("permission_prompt")
|| lower.contains("error") || lower.contains("failed") || lower.contains("failure")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Loose substring matching for error, failed, failure, exception risks false-positive blocking on routine non-blocking notifications, directly undermining the classifier's purpose.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentNotificationBlockingClassifier.swift, line 33:

<comment>Loose substring matching for `error`, `failed`, `failure`, `exception` risks false-positive blocking on routine non-blocking notifications, directly undermining the classifier's purpose.</comment>

<file context>
@@ -0,0 +1,36 @@
+        let lower = "\(signal) \(message)".lowercased()
+        return lower.contains("permission") || lower.contains("approve") || lower.contains("approval")
+            || lower.contains("permission_prompt")
+            || lower.contains("error") || lower.contains("failed") || lower.contains("failure")
+            || lower.contains("exception")
+    }
</file context>

Comment thread .github/workflows/ci.yml Outdated
-only-testing:cmuxTests/GhosttyOptionAsAltModsTests \
test

- name: Run browser session history restore regression

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This is the third near-identical focused-gate step (after BrowserSystemProxyMirrorTests and GhosttyOptionAsAltModsTests). The only differences between these blocks are the comment and the -only-testing: selector. Consider extracting the common run-in-console-session.sh + run-app-host-xcodebuild.sh invocation into a small reusable composite action or parameterized script to reduce copy-paste maintenance burden when the next isolated suite is added.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/ci.yml, line 527:

<comment>This is the third near-identical focused-gate step (after `BrowserSystemProxyMirrorTests` and `GhosttyOptionAsAltModsTests`). The only differences between these blocks are the comment and the `-only-testing:` selector. Consider extracting the common `run-in-console-session.sh` + `run-app-host-xcodebuild.sh` invocation into a small reusable composite action or parameterized script to reduce copy-paste maintenance burden when the next isolated suite is added.</comment>

<file context>
@@ -524,6 +524,26 @@ jobs:
             -only-testing:cmuxTests/GhosttyOptionAsAltModsTests \
             test
 
+      - name: Run browser session history restore regression
+        if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }}
+        run: |
</file context>

…on == main

Adding this new suite reshuffled the greedy unit-test shard packing, which
moved a heavy order/load-sensitive WKWebView suite into a shard where it
exhausted WebKit (WebProcess killed) and aborted the app-host — turning an
app-host shard red purely from reshuffling, not from any real regression.
Isolating one WebKit suite just moved the reshuffle to another shard.

Since this PR's only new cmuxTests class is AgentHibernationPlannerTests
(pure, fast planner-selection tests), exclude IT from the four shared
shards instead. That leaves the shard composition byte-identical to main
(where app-host is green) and runs the suite isolated in its own app-host,
like the other focused gates. Reverts the earlier browser-suite gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Autoreview: the isBlocking gate only matched permission/approval/error, so a
genuine user prompt surfaced only through a Notification hook could hit the
notification lane with a non-blocking classification, skip
setAgentLifecycle(.needsInput), and leave the Stop hook's `.idle` — letting the
pane hibernate (and get SIGTERMed) while an agent waited for the user's answer.

Detect a genuine prompt from a high-confidence signal: a direct interrogative
(the message contains `?`), or the literal `question` token paired with an
interaction word. Wire it in two places that were the actual gap:
  - CLI classifier `containsWaitingCue` now classifies an interrogative as
    `.needsInput`, so the generic lane (which requires status == .needsInput AND
    isBlocking) reaches the lifecycle write.
  - `AgentNotification.isBlockingPrompt` treats the same cue as blocking, so both
    the claude lane (lifecycle = isBlocking ? .needsInput : nil) and the generic
    lane flip for real questions.

Deliberately NOT keyed on bare verbs (confirm/choose/continue/proceed): those
collide with routine agent chatter ("continue when ready") and blocking on them
would clobber `.idle` so the pane never hibernates — the bug this lane prevents.
Routine idle/waiting reminders carry no `?`, so they stay non-blocking. A
punctuation-free imperative ("Choose an option") is intentionally left to the
authoritative structured signal (notification_type), not this free-text heuristic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cmux-policy-check follow-up on the hibernation classifier/background-work types:
- Add Swift-DocC `///` docs to the public memberwise initializers.
- Move the pure helpers (arrayOfObjects, terminalStatuses, containsGenuineQuestionCue)
  off the value types to file-scope private funcs/lets, so the types carry only
  instance members (no static-as-namespace surface). Public API unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lawrencecchen and others added 3 commits July 1, 2026 18:17
Review follow-up: the Stop lane recorded lifecycle `.running` when background
tasks were still alive but painted the status pill "Idle" (pause icon), so the
user-visible state contradicted the hibernation state. Show "Running"
(bolt.fill, #4C8DFF) in that case, matching the antigravity background-work
lane's display. Existing localized key, no new strings.

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

Autoreview round-5 findings, both verified real:

P1: classifyAgentHookNotification checks completion cues before waiting cues, so
"Task completed. Continue?" classified status=.idle with isBlocking=true, and the
generic notification lane's persisted path (agentLifecycle(for: .idle)) durably
wrote .idle while the agent waited on a user answer — hibernatable mid-question.
Guard the completion branch with !blocking so blocking text falls through to the
waiting branch (.needsInput); persisted and live paths then both record
.needsInput. Same guard on the claude display classifier for subtitle
consistency. Side effect (correct): grok/antigravity no longer treat
"completed + question" as a turn boundary.

P2: AgentBackgroundWorkStatus treated a present-but-unreadable background_tasks
value (schema drift to a keyed object, an array of non-objects) as "no work",
failing open to hibernation's group SIGTERM. Parse now distinguishes ABSENT
(older clients — inactive, they must keep hibernating) from PRESENT-but-
unreadable (fail closed as one active task; unreadable evidence of work must
keep the pane alive, never authorize the kill).

Verified against the built binary: drifted payload -> running; completed+question
notification -> needsInput; all prior lifecycle scenarios unchanged.

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 157ef0a. Configure here.

Comment thread CLI/cmux.swift
cmux-policy: one major type per file. AgentHookFieldParse was a private
parsing helper, not a second domain type; replace it with a file-scope private
function returning (objects, unreadableCount). Behavior identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lawrencecchen and others added 2 commits July 2, 2026 19:45
…agents

PR #7129 landed the same claude Stop-lane background-work gate this branch
carries, plus notification category gating. Resolution unifies the two:

- One background-work parser: hasActiveClaudeBackgroundWork now delegates to
  AgentBackgroundWorkStatus(hookObject:) (unit-tested, fail-closed on
  present-but-unreadable payloads; non-terminal statuses count as live).
  Main's inline predicate (exact "running" only, fails open on malformed)
  is replaced. Main's tests only pin running/empty/cron/absent, all
  compatible.
- Stop lane: main's shape kept (hasPendingBackgroundWork, localized
  Running/Idle pill, hadPendingBackgroundWorkAtStop cache).
- Notification lane: union of main's category gating (notifyCategory,
  suppressNeedsInputState) and this branch's blocking gate — lifecycle
  flips to .needsInput only for genuinely blocking prompts
  (summary.isBlocking && !suppressNeedsInputState). Main's unconditional
  .needsInput write would have reintroduced the routine-reminder clobber
  this PR fixes.
- Generic lane: persisted lifecycle composes suppressPendingWaitingState
  (.running) > isBlocking (.needsInput) > completion (.idle) > preserve.
- AgentHookNotificationSummary carries both isBlocking and notifyCategory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lawrencecchen and others added 2 commits July 3, 2026 17:56
…agents

# Conflicts:
#	.github/swift-file-length-budget.tsv
#	cmux.xcodeproj/project.pbxproj
The `agentHib.restorable.claudeTranscriptUnresolved` diagnostic I added fired
once per claude record on every ~30s planner reload over the whole session
store. Stale cross-worktree records (deleted worktree, benign) routinely fail
closed there, so it flooded the debug log (~1700 lines/min observed under a
multi-agent dogfood) and added file-write churn on the reload's background
threads. It is `#if DEBUG` only (never ships), but it degrades the dogfood build.

Gate it behind CMUX_DEBUG_HIBERNATION_VERBOSE (off by default, read once via a
thread-safe global initializer) so the diagnostic stays available when
investigating a real never-hibernation report without the default storm.

The ~30s uncached full-index reload itself is pre-existing (Add Agent
Hibernation #4165, not this PR); filing that as a separate perf follow-up.

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

This branch was successfully deployed

1 active deployment
Preview – cmux — 2ed17441 Deployed Jul 4, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants