fix(codex): finalize Codex sessions instead of leaving them to pile up - #1940
Conversation
State.OwnerExited gated on Phase.IsActive(), which is only PhaseActive. An agent that finishes its last turn transitions ACTIVE -> IDLE, so a session whose agent then quit without firing a session-end hook was invisible to the dead-owner sweep in `entire status` and `entire doctor`. That is exactly the shape Codex leaves behind — it had no SessionEnd hook before 0.146 — and it can happen to any agent that is killed before its hook runs. Those sessions lingered as "active" for 7 days until StaleSessionThreshold made StateStore.Load delete the state file outright, discarding pending checkpoint work rather than condensing it. Process death is unambiguous and phase-independent, so OwnerExited now excludes only already-finalized sessions (PhaseEnded or EndedAt set). doctor's classifySession checks it ahead of the phase switch for the same reason; the repeated stuckSession literals collapse into a local constructor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01KZNXZDK2N57QKVKQF5CARJ46
Codex gained a SessionEnd hook in 0.146 (openai/codex#33895, "Add SessionEnd hooks for thread teardown"). Until now Entire only registered SessionStart, UserPromptSubmit, Stop and PostToolUse, so quitting Codex left the session un-finalized and it showed up in `entire status` indefinitely. Register it, parse it into the normalized SessionEnd event, and include it in install/uninstall/detection. The payload is thinner than Codex's other hooks — no model, permission_mode or turn_id — and its `reason` is a constant, so only session_id, cwd and transcript_path are consumed. The hook is subject to a ceiling the others are not: Codex defaults SessionEnd handlers to 1s and clamps them to 3s (SESSION_END_MAX_TIMEOUT_SEC), warning at every startup when a config asks for more, then terminates the hook's process tree on expiry. So hook timeouts become per-event — SessionEnd at exactly the ceiling, everything else unchanged at 30s — and the command match now includes the timeout so a hook installed by an older Entire is rewritten rather than left to nag. Agents in that position declare a budget through the new optional SessionEndBudgeter interface, which bounds only the eager condense. Mark-ended stays unbounded: it is one atomic state-file rename and must always complete. The bound is best-effort — condensation does not poll ctx between stages — and exists to stop short of the process-tree kill, not to make condensation interruptible. Either way of being cut off is safe, since MutateSessionState persists at the end and PostCommit retries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01KZNZ7MJ2R43V2R4SP363AFB0
The one-pager was written against codex-cli 0.116.0 and had gone stale on every claim that mattered: - "Hooks require feature flag --enable codex_hooks" — CodexHooks went Stage::Stable/default_enabled:true in openai/codex#19012 (2026-04-23) and the key was aliased to plain `hooks` in #20522. No flag is needed. - "No SessionEnd hook" — added in #33895 (2026-07-17), shipped in 0.146. - "PreToolUse is shell-only" — now dispatched generically from the tool registry, covering apply_patch, MCP tools and unified_exec. - "No subagent hooks" — SubagentStart/SubagentStop exist. Four hook events became eleven. Document the SessionEnd payload (thinner than the rest, no output schema, constant `reason`) and its timeout ceiling, and stamp the doc with the codex revision it was verified against so the next reader knows how much to trust it. Superseded claims are kept as a struck-out "Resolved" list rather than deleted, so anyone holding the old assessment can see what changed. CLAUDE.md gains the two invariants this work established: OwnerExited covering IDLE, and what a SessionEndBudgeter budget may and may not bound. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01KZNZGYDSAJXQPMQ73CB5R15B
finalizeExitedSessions runs from `entire status` and `entire doctor`, neither of which initializes logging. With the package logger nil, logging.log falls back to slog.Default(), so the sweep's phase-transition and condense lines printed straight onto the user's terminal, mid-output: ● Enabled · branch main 2026/08/10 17:09:15 INFO phase transition component=session ... from=idle to=ended Finalized 1 exited session(s) (agent process gone). The leak predates this branch but was rare while the sweep only covered ACTIVE sessions; now that it covers IDLE it fires on the common Codex path. Route it to .entire/logs/ via a new logging.EnsureInitialized, which no-ops when a logger already exists so a caller reachable from a hook cannot close the hook's log file. Initialization is deferred to the first real candidate: the overwhelmingly common case finalizes nothing, and Init creates .entire/logs/ as a side effect. Same hazard the Init call sites in setup.go and investigate/cmd.go already document. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01KZP3V62DKQBG9ZKS6T2ADR00
Verified against codex-cli 0.147.0 by running `codex exec` in a scratch repo with Entire enabled: - All five Entire hooks fire (session-start, user-prompt-submit, post-tool-use, stop, session-end) — the one-pager's "codex exec fires no lifecycle hooks" is stale. - With ENTIRE_REVIEW_* in the environment the session IS tagged kind: agent_review, with review_skills and review_prompt captured. - But no hooks fire at all under `-s read-only`; `-s workspace-write` fires the full set. The last point is why this is documented rather than acted on: buildCodexReviewCmd passes no -s flag, so whether a review yields a tagged session depends on the user's default sandbox. The run.Buffer fallback stays required. Reworking review to prefer a tagged session when one exists is real work with its own testing burden, so it is flagged as follow-up instead of being smuggled into a session-cleanup fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01KZP43JPTABN9AVJ66W5WWKRM
Bound the sweep's condensing. Widening OwnerExited to IDLE multiplied the candidate set — IDLE is the resting state, so the sweep now catches every agent quit at its prompt, not just ones killed mid-turn — and each candidate triggered a deadline-free condense inside `entire status`. Measured, one condense is ~120ms floor and seconds for a real transcript, against a 40ms command, serially and uncapped; with a 7-day StaleSessionThreshold the first sweep after upgrade drains a week's backlog in one stall, and `--json` (the MCP entire_status tool) prints nothing until it finishes. sweepCondenseBudget now caps condensing across the whole sweep while every candidate is still marked ENDED, which is what actually un-sticks it from `entire status`. A new test pins that split. Reuse and simplification, no behaviour change: - Hoist the "already finalized" rule to State.IsEnded(); it was spelled out at three sites whose comments claimed they were kept in sync by hand. - Collapse codex's parseSessionStart/parseSessionEnd into parseSessionInfoEvent over one sessionInfoRaw, matching geminicli and factoryaidroid, which solved the same "these differ only in event type" problem. - Drive codex's hooks.json handling from a managedHooks table. Install, uninstall and detection each repeated the same five-event block, so adding SessionEnd was a 12-line edit across three functions with three chances to miss one; the per-event timeout now lives with the event. - finalizeExitedSessions: pre-check with slices.ContainsFunc instead of the lazy-init dance, pair SetLogLevelGetter with EnsureInitialized (without it the sweep ignored log_level in settings), drop an unreachable error branch, and fix a doc comment still claiming ACTIVE-only. - Fold duplicated tests into tables; derive expected hook counts from the table. - State honestly why SessionEndBudgeter is built-in only: an external agent could not enforce a budget itself — the work runs in the entire process. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01KZP8XD3JBCSHTEPFFAXGT3E7
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 4cfc711. Configure here.
There was a problem hiding this comment.
Pull request overview
Fixes un-finalized Codex sessions accumulating by wiring Codex’s SessionEnd hook into the lifecycle finalization path, and by extending the “owner process exited” safety net to cover IDLE sessions (not just ACTIVE), preventing lingering sessions and preserving pending checkpoint work.
Changes:
- Add Codex
SessionEndhook support (install, parse, budget) and finalize sessions on session end under a tight deadline. - Broaden exited-owner reclamation to include IDLE sessions; introduce a sweep-wide condensation time budget to avoid stalling
status/doctor. - Add
State.IsEnded()as the canonical “finalized session” predicate and align session-stop filtering with it.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| cmd/entire/cli/sessions.go | Uses IsEnded() for “active session” filtering and stop behavior. |
| cmd/entire/cli/session/state.go | Expands OwnerExited() to include IDLE; adds canonical IsEnded(). |
| cmd/entire/cli/session/owner_test.go | Updates unit coverage for ended/EndedAt guard cases. |
| cmd/entire/cli/session/owner_live_test.go | Extends live-owner tests to cover IDLE + ACTIVE non-ended phases. |
| cmd/entire/cli/session_finalize.go | Adds sweep condense budget + ensures logging initialized during sweeps. |
| cmd/entire/cli/session_finalize_test.go | Covers IDLE exited-owner finalize + spent-budget still marks ENDED. |
| cmd/entire/cli/logging/logger.go | Adds EnsureInitialized() helper for non-hook commands. |
| cmd/entire/cli/logging/logger_test.go | Tests EnsureInitialized() init/no-op behavior. |
| cmd/entire/cli/lifecycle.go | Finalizes on SessionEnd with a condense-only deadline; adds deadline computation. |
| cmd/entire/cli/doctor.go | Improves stuck-session classification by prioritizing dead-owner detection. |
| cmd/entire/cli/doctor_classify_live_test.go | Adds platform-gated tests for IDLE + dead/alive owner classification. |
| cmd/entire/cli/agent/codex/types.go | Adds SessionEnd hook type support; generalizes session hook payload struct. |
| cmd/entire/cli/agent/codex/lifecycle.go | Registers SessionEnd hook + implements SessionEndBudgeter. |
| cmd/entire/cli/agent/codex/lifecycle_test.go | Tests shared SessionStart/SessionEnd parsing and budget constraints. |
| cmd/entire/cli/agent/codex/hooks.go | Refactors Codex hook management via a managed-hooks table; adds SessionEnd timeout matching. |
| cmd/entire/cli/agent/codex/hooks_test.go | Updates hook tests for SessionEnd + timeout rewrite behavior. |
| cmd/entire/cli/agent/codex/codex_test.go | Ensures session-end is advertised in Codex hook names. |
| cmd/entire/cli/agent/codex/AGENT.md | Updates Codex hook surface + documents SessionEnd constraints and review caveats. |
| cmd/entire/cli/agent/capabilities.go | Adds AsSessionEndBudgeter capability accessor. |
| cmd/entire/cli/agent/agent.go | Defines SessionEndBudgeter interface and documentation. |
| CLAUDE.md | Documents the updated exited-owner reclaim behavior and session-end budgeting. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Verifying the review integration end-to-end turned up a gap in this branch: Codex silently skips hooks with no trusted_hash entry in the user's config.toml, and `declaredCodexEvents`/`MissingEntireHooks` did not know about session_end. So the new hook would have shipped inert for every existing user — they have trusted the four older events, not this one — with nothing telling them why, since neither the SessionStart trust banner nor `entire doctor` enumerated it. Both now cover session_end. Verified against a real repo: doctor reports it as needing approval. Fixture updates follow from that: the two "canonical install set" fixtures gain SessionEnd, the intentionally-stale ones keep it absent (an older release shipped neither SessionEnd nor PostToolUse) and now assert both are reported as out of date. Also records what an end-to-end `entire review` actually does. It produced zero hooks and no session. Holding review's argv fixed and varying one flag: --dangerously-bypass-hook-trust yields 5 hooks and a tagged session, -s workspace-write alone yields nothing. Hook trust is the gate, not the sandbox — and `codex exec` is non-interactive, so it can never prompt for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01KZPG93M2ZARW8NMR4WH8JZSX
Four fixes from the bot review on #1940, plus the e2e gap the first one led to. AreHooksInstalled required every event in managedHooks, so adding SessionEnd un-installed Codex for everyone who enabled it before this release. Worse than it sounds: DetectPresence delegates to it, so such a repo was neither installed nor detected — Codex vanished from `entire status`, the review and investigate pickers, and a non-interactive `entire enable` re-run (which keeps the currently installed agents) would have dropped it rather than repaired it. Only the four pre-existing events gate the check now; the missing hook is drift, which MissingEntireHooks already reports with `entire enable` as the fix, and setupAgentHooks installs unconditionally so that fix still works. That in turn exposed a hole in e2e. The suite pre-trusts hooks by recomputing Codex's trusted_hash values itself, from a hand-maintained event table that did not know SessionEnd — and Codex silently skips hooks with no trust entry. The hook would have been installed but inert for every e2e run, with nothing failing to say so. The table learns SessionEnd, and a new test installs hooks for real and requires a trust entry per Entire handler, so the two can't drift again. logging.Close left the logger in place while closing the buffer underneath it. The handler holds that *bufio.Writer by value, so anything logged after the sweep's cleanup went into an orphaned buffer over a closed file and was never flushed — `entire doctor` keeps condensing and discarding sessions after the sweep returns. Close now drops the logger too. On its own that would push those lines back onto the terminal, which fe46a73 set out to prevent, so finalizeExitedSessions returns its cleanup instead of deferring it internally and doctor scopes it to the whole command. status filtered active sessions on EndedAt while `session stop` filtered on IsEnded. Not just legacy state: `entire session attach` sets Phase to ended without stamping EndedAt, so an attached session showed as active in status and `session stop` then refused to list it. All three status sites use IsEnded, which makes the comment on filterActiveSessions true rather than merely reworded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01KZXMDENWDD1EBDAZEEJHD811
# Conflicts: # cmd/entire/cli/agent/codex/hooks.go # cmd/entire/cli/agent/codex/hooks_test.go
Code reviewReviewed the full branch (8 commits, 29 files, +1194/−298). Five findings, ordered by severity; the top three were confirmed against the code. 1. Condense deadline silently drops mirror checkpoint writes — medium
The Scenario: a Codex session with a large transcript quits; the primary v1 write finishes at ~2.4s, the 2.5s deadline expires, and every mirror write fails with 2.
|
Five items from Victor's review and the trail's agent review. Two of the six were left alone: the mirror-write window is unreachable in a shipped binary (the registry holds only the two git-backed backends and Register is test-only), and the durable fix for the last one is filed as a follow-up. Doctor's logging setup only covered the exited-session sweep, which returns before touching logging whenever nothing needs finalizing — the common case. The condense and discard handlers that run afterwards then logged to slog.Default(), i.e. onto the user's terminal interleaved with doctor's report, which is what the previous commit claimed to prevent. finalizeExitedSessions goes back to owning its own teardown and doctor installs command-scoped logging itself, via a helper that keeps the level-getter pairing in one place. The exited-owner sweep stamped EndedAt with now, dating a session abandoned days ago to today: it sorts above real recent work in the resume picker, which prefers EndedAt, and `session info` reports it as just-ended. Finalization now carries an explicit policy — the hook and `session stop` stamp now, the sweep stamps last-seen — rather than inferring it from a nil event. Note the value has to be read before the transition: the SessionStop edge carries ActionUpdateLastInteraction and overwrites the very field it needs. The e2e trust helper hashes the configured timeout while Codex caps SessionEnd at SESSION_END_MAX_TIMEOUT_SEC, so if that clamp lands before command_hook_hash, anything above the ceiling would hash differently and leave the hook untrusted — silently, since an untrusted hook simply never fires. Mirroring a clamp we haven't verified would break trust rather than preserve it, so instead the guard test pins the premise that makes it moot: SessionEnd is installed at exactly the ceiling. Lastly the session-end budget, which the trail's finding showed was thinner than it reads. Codex's 3s starts when it spawns the `sh -c` wrapper; Entire's clock starts at Go package init, so sh startup, the `command -v` PATH walk and loading a 66MB binary all happen before we can measure anything — ~40ms warm, but a cold page cache or a network filesystem is exactly where it isn't. 2500ms → 2s, with the test pinning at least 1s of headroom. That finding also corrected three doc comments claiming the mark-ended write "always runs to completion". It is deliberately unbounded, which is not the same as guaranteed: it takes the session flock, so a concurrent condense can push it past the cap and get the tree killed — the exited-owner sweep is the backstop. Being cut off costs duplication rather than data, except in one window: the v1 checkpoint is committed inside the MutateSessionState callback while state is saved only after it returns, so a kill in between leaves a committed checkpoint whose bookkeeping never advanced and PostCommit writes a second one over the same transcript range. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01M0AC39GC8DVJM7WM61CGXVKQ
|
Thanks — this was a genuinely useful pass. Verified all five against the code; three are fixed, one is latent, and one I resolved differently than suggested. 3. Doctor's deferred
|

https://entire.io/gh/entireio/cli/trails/1001
Closes the user report that Codex sessions pile up and have to be cleaned by hand with
entire status+entire session stop <id>.The request was for
entire session stop --older-than=3d. That treats the symptom — the root cause is fixable at the source, and process death is a better signal than any time threshold. No new flag is included; see "On--older-than" below.What was wrong
1. Codex never fired a session-end hook. We registered
SessionStart,UserPromptSubmit,StopandPostToolUse. Codex gained aSessionEndhook in 0.146 (openai/codex#33895); we weren't listening, so quitting Codex left the session un-finalized forever.2. The safety net had an off-by-one-phase bug.
State.OwnerExited()gated onPhase.IsActive(), which is onlyPhaseActive. An agent that finishes its last turn transitions ACTIVE → IDLE, so a session whose agent then quit was invisible to the dead-owner sweep inentire statusandentire doctor. That is precisely the shape Codex leaves behind — and it can happen to any agent killed before its hook runs.Those sessions lingered as "active" for 7 days until
StaleSessionThresholdmadeStateStore.Loaddelete the state file outright, discarding pending checkpoint work rather than condensing it.Verified against real Codex
Same flow run twice, with a binary built at
mainfor the baseline:phase: idle,ended_at: nullphase: endedentire statusThe session-end hook completes in 19ms against Codex's 3s cap. The safety net was exercised independently by pointing the new binary at the stale pre-fix repo:
Finalized 1 exited session(s) (agent process gone), idle → ended. That path covers Codex < 0.146, SIGKILL, and reboot.Notable constraint
SessionEndruns inside Codex's shutdown, so it is budgeted far more tightly than any other hook: 1s default, hard 3s cap (SESSION_END_MAX_TIMEOUT_SEC), and on expiry Codex terminates the hook's entire process tree. Asking for more prints a clamping warning at every startup.So hook timeouts became per-event, and the command match now includes the timeout so a hook installed by an older Entire gets rewritten rather than left to nag. Agents in this position declare
agent.SessionEndBudgeter, which bounds only the eager condense — mark-ended is a single atomic rename and always runs to completion, so a session can never be left un-finalized.Also fixed here
entire status— ~120ms floor, seconds for a real transcript, serial and uncapped, in a 40ms command.--jsonis what the MCPentire_statustool calls, and prints nothing until it returns.sweepCondenseBudgetnow caps condensing across the whole sweep while still marking every candidate ENDED.entire status. Neitherstatusnordoctorinitializes logging, so the sweep'sphase transitionline printed raw onto the terminal viaslog.Default(). Pre-existing, but this PR makes it common. Now routed to.entire/logs/— and the level getter is wired, without which the sweep ignoredlog_levelin settings.On
--older-thanDeliberately not included. Once process death handles this automatically, a time threshold only serves cases where liveness is Unknown: Windows (
procliveis unsupported there) and cross-host state. Happy to add it if you still want the escape hatch.Needs a follow-up decision
codex execnow fires hooks — all five, verified against 0.147.0 — which invalidates a premiseentire reviewis built on. WithENTIRE_REVIEW_*set, the session is taggedkind: agent_reviewwith skills and prompt captured.But an end-to-end
entire reviewin a fresh repo still produces zero hooks and no session, and the gate is hook trust, not the sandbox. Holding review's argv fixed and varying one flag:--dangerously-bypass-hook-trust→ 5 hooks and a tagged session;-s workspace-writealone → nothing. Codex silently skips hooks with notrusted_hash, andcodex execis non-interactive so it can never prompt for it. (-s read-onlysuppresses hooks even when trust is bypassed, so the sandbox is a second, independent gate.) Therun.Bufferfallback stays required. Documented and flagged rather than acted on — reworking review needs a trust-bootstrapping decision first and doesn't belong in a session-cleanup fix.Review follow-ups
Four bot comments, all fixed in
1d36ecb67, plus a hole the first one led to.AreHooksInstalledwas all-or-nothing (Bugbot). Adding SessionEnd to the required set un-installed Codex for everyone who enabled it before this release. Worse than reported:DetectPresencedelegates to it, so such a repo was neither installed nor detected — Codex disappeared fromentire status, the review and investigate pickers, and a non-interactiveentire enablere-run (which keeps the currently installed agents) would have dropped Codex rather than repaired it. Only the four pre-existing events gate the check now; the missing hook is drift, whichMissingEntireHooksalready reports withentire enableas the fix, andsetupAgentHooksinstalls unconditionally so that fix still works.The new hook was inert in E2E — found while fixing the above, not reported by either bot. The suite pre-trusts hooks by recomputing Codex's
trusted_hashvalues itself (e2e/agents/codex_trust.go), from a hand-maintained event table that didn't know SessionEnd. Since Codex silently skips untrusted hooks, it would have been installed but never fired for every E2E run, with nothing failing to say so. The table learns SessionEnd, andTestCodexHookTrustState_CoversEveryInstalledEventnow installs hooks for real and requires a trust entry per Entire handler, so the two can't drift again.Stale logger after the sweep (Bugbot + Copilot, same bug).
logging.Closeleft the logger in place while closing the buffer underneath it, and the handler holds that*bufio.Writerby value — so lines logged after the sweep's cleanup went into an orphaned buffer over a closed file and were never flushed.entire doctorhits it directly: it keeps condensing and discarding sessions after the sweep returns.Closenow drops the logger too. On its own that would push those lines back onto the terminal — the thing fe46a73 removed — sofinalizeExitedSessionsreturns its cleanup instead of deferring it internally, and doctor scopes it to the whole command.filterActiveSessionsoverclaimed parity withentire status(Copilot). Correct, and reachable rather than merely legacy:entire session attachsetsPhase = PhaseEndedwithout stampingEndedAt, so an attached session showed as active instatuswhilesession stop— filtering onIsEnded— refused to list it. All threestatussites now useIsEnded, which makes the comment true rather than merely reworded. (The divergence predates this PR; the diff only drew attention to it.)Three regression tests plus the E2E guard:
TestAreHooksInstalled_PreSessionEndInstall,TestEnsureInitialized_CleanupRestoresFallback,TestWriteActiveSessions_PhaseEndedWithoutEndedAtExcluded.Testing
mise run checkgreen: unit, integration, 60 canary E2E, 4 external-agent E2E. Plus the manual before/after against realcodex execdescribed above. Re-run green on1d36ecb67.One flake observed on a first CI run —
TestAttach_DiscoversExternalAgentsandTestDiscoverAndRegister_Deduplication, both at exactly the 10s discovery timeout under load. Both pass in isolation, standalone, and onmain; the re-run was fully green. Not caused by these changes, but noting it.🤖 Generated with Claude Code
Note
Medium Risk
Touches session finalization, owner liveness, and interactive
status/doctorsweeps; incorrect IDLE handling could finalize live sessions or leave checkpoint work uncondensed until PostCommit.Overview
Fixes Codex (and similar) sessions staying active after the agent quits by wiring Codex
SessionEndinto the normal finalize path and tightening the exited-owner safety net.Codex: Installs and parses
session-end(3s hook timeout, self-imposed ~2.5sSessionEndBudgeteron eager condense only). Hook install is refactored to amanagedHookstable; hook sync now matches command + timeout so stale 30sSessionEndentries get rewritten. Docs updated for Codex 0.146+ hooks and review caveats.Lifecycle / reclaim:
State.OwnerExited()now applies to IDLE as well as ACTIVE (onlyIsEnded()sessions are skipped).finalizeExitedSessionsinentire status/entire doctoralways marks candidates ENDED, butsweepCondenseBudget(1s total) caps eager condense so large backlogs do not stallstatus --json.endSessionNowtakes an optional condense deadline; mark-ended is never budgeted.logging.EnsureInitializedroutes sweep logs off the terminal;doctorclassifies dead owners before phase heuristics.Consistency: Active-session filtering uses
IsEnded()insessions/ related paths.Reviewed by Cursor Bugbot for commit 4cfc711. Configure here.