Repository navigation
release(stable): v4.260520.1 — agents survive VM reboot - #2461
Conversation
…ack + TMUX-presence shim
The oldest genie problem: after a VM reboot (or pm2 restart of Genie),
no agents auto-resumed. Reference instance: 33 agents, 20 classified
`eager_invoke` by the boot pass, `recovery_completed: resumed_agents: 0`
on every restart. The recovery infrastructure existed (`recoverOnStartup`
→ `runBootPass` → `runAgentRecoveryPass` → `attemptAgentResume` →
`defaultResumeAgent`), but two coupled defects silently killed it:
1. **runAgentRecoveryPass over-filtered.** The filter at line 1126 was
`state != 'suspended/done' && currentSessionId`. But `currentSessionId`
comes from the `executors.claude_session_id` join, which is null for
most agents on a fresh boot — yet `genie status`'s classifier
(`shouldResume`) resolves the session via a BROADER `getResumeSessionId`
path: DB happy path → JSONL on-disk fallback (`~/.claude/projects/...`).
So bootPass eager-invoked 20 agents whose JSONLs sat right there on
disk, but the recovery pass filtered them out before the dead-pane
check could see them. Fix: drop the `currentSessionId` filter clause,
resolve inside the loop via `getResumeSessionId` for dead-pane
survivors only (cheap), and enrich `worker.currentSessionId` in-place
so attemptAgentResume can issue `claude --resume <uuid>`. SDK / non-tmux
agents naturally fall out at attemptAgentResume's `no_session_id`
early-exit when neither source yields a UUID.
2. **defaultResumeAgent shelled without TMUX.** `genie agent resume <id>`
errors with `Cannot resume agent "<id>": no team context (template,
agent record, env, or session). Pass --team or set GENIE_TEAM, or run
inside a registered tmux session.` for `dir:`-prefixed agents whose
`team` is NULL (29/33 on the reference instance). The scheduler is a
pm2 child (never inside tmux) and shelled the CLI bare → exit 1 →
execSync threw → bare `catch {}` swallowed everything → silent
`agent_resume_failed`. The CLI's check is just env-presence (value
isn't validated). Fix: pass a synthetic `TMUX` sentinel when the
parent process has none, and surface previously-invisible CLI errors
behind `GENIE_DEBUG_RESUME=1` instead of dropping them on the floor.
Validated on the reference instance (33 agents, post-fix Genie restart):
recovery_completed → **resumed_agents: 17, failed_agents: 0** (was 0/0)
26 live tmux panes on the canonical /tmp/tmux-1000/genie socket (was 2)
20 agents in `spawning`/`idle` state in DB
`genie ls` shows real registered agents with running panes
biome + check:fast green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI failure on PR #2460 caught by state-machine.invariants.test.ts (invariant 3): only src/lib/should-resume.ts and the definition site may import getResumeSessionId() — every consumer must read the ShouldResumeResult instead. My fix imported the raw helper directly in scheduler-daemon.ts, breaking the chokepoint contract. Switch the dynamic import to `shouldResume` and read `decision.sessionId`. Same semantics for the enrichment path (DB happy path → JSONL fallback), respects the architectural contract, and incidentally gives the recovery pass access to `decision.resume` if a future change wants to short-circuit auto_resume_disabled / assignment_closed cases here too. Validated: - bun test src/__tests__/state-machine.invariants.test.ts → 1 pass / 0 fail - bun run typecheck → clean - biome check src/lib/scheduler-daemon.ts → clean Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(scheduler): agents survive VM reboot — bridge JSONL session fallback + TMUX-presence shim
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (5)
📒 Files selected for processing (8)
📝 WalkthroughWalkthroughBump manifests to 4.260520.2, append metrics-updater run and daily stat entries, and modify scheduler daemon resume/recovery to inject a synthetic TMUX, optionally surface resume CLI stderr, relax the resumable pre-filter, and consult shouldResume per worker before handling dead panes. ChangesRelease 4.260520.2
Sequence Diagram(s)sequenceDiagram
participant SchedulerDaemon
participant ShouldResume
participant GenieCLI
participant HandleDeadPane
SchedulerDaemon->>ShouldResume: shouldResume(worker.id)
ShouldResume-->>SchedulerDaemon: decision { resume, sessionId, reason }
alt decision.resume == false (boot mode) and decision.reason != "unknown_agent"
SchedulerDaemon->>SchedulerDaemon: skip worker processing
else decision.resume == true
alt decision.sessionId present && worker.currentSessionId missing
SchedulerDaemon->>SchedulerDaemon: set worker.currentSessionId = decision.sessionId
end
SchedulerDaemon->>GenieCLI: spawn "genie agent resume" (env includes synthetic TMUX)
GenieCLI-->>SchedulerDaemon: exit / error (emit to stderr if GENIE_DEBUG_RESUME=1)
SchedulerDaemon->>HandleDeadPane: handleDeadPane(worker)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@package.json`:
- Line 3: The release version bump in the "version" field (currently
"4.260520.1") must be blocked until the pinned integrity hashes are updated;
locate any binarySha256 pins referenced in package.json or related install/asset
metadata (search for "binarySha256" or asset entries) and update those SHA256
values to match the actual artifact(s) used by CI, recomputing the correct
SHA256 from the released asset(s) and replacing the stale pin(s); only then
re-apply the version bump and re-run CI to confirm the SHA drift error is
resolved.
In `@src/lib/scheduler-daemon.ts`:
- Around line 1179-1181: The code currently swallows errors from shouldResume
(using .catch(() => null)) which makes transient resolver/DB failures look like
“no session” and causes attemptAgentResume to treat them as permanent no-session
state; change the call to shouldResume(worker.id) so errors are not converted to
null—either remove the .catch entirely or rethrow the caught error (so the
exception propagates) so that calling logic treats resolver failures as worker
failures rather than as missing session IDs; touch the block around
shouldResume, decision, enriched and ensure attemptAgentResume only runs when
decision is explicitly present (decision?.sessionId) and not when shouldResume
errored.
🪄 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: 19dab3ef-6ee9-4592-aa90-b89743f6e661
📒 Files selected for processing (5)
.claude-plugin/marketplace.jsonpackage.jsonplugins/genie/.claude-plugin/plugin.jsonplugins/genie/package.jsonsrc/lib/scheduler-daemon.ts
| { | ||
| "name": "@automagik/genie", | ||
| "version": "4.260519.3", | ||
| "version": "4.260520.1", |
There was a problem hiding this comment.
Block release bump until SHA drift is resolved.
Line 3 promotes a stable release, but CI reports SHA drift for $asset; that means pinned binarySha256 integrity data is currently stale and release verification is not trustworthy. Update the affected SHA pin(s) before merging this version bump.
🤖 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 `@package.json` at line 3, The release version bump in the "version" field
(currently "4.260520.1") must be blocked until the pinned integrity hashes are
updated; locate any binarySha256 pins referenced in package.json or related
install/asset metadata (search for "binarySha256" or asset entries) and update
those SHA256 values to match the actual artifact(s) used by CI, recomputing the
correct SHA256 from the released asset(s) and replacing the stale pin(s); only
then re-apply the version bump and re-run CI to confirm the SHA drift error is
resolved.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bc4053f99c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const decision = await shouldResume(worker.id).catch(() => null); | ||
| if (decision?.sessionId) enriched = { ...worker, currentSessionId: decision.sessionId }; |
There was a problem hiding this comment.
Respect shouldResume decision before injecting session IDs
runAgentRecoveryPass now calls shouldResume(worker.id) but only uses decision.sessionId, ignoring decision.resume. In should-resume.ts, sessionId can be present even when resume is explicitly disallowed (e.g., assignment_closed or auto_resume_disabled), so this code can re-enable resume attempts for agents that should not be resumed whenever currentSessionId is null but JSONL fallback finds a UUID. That can relaunch completed/closed task agents after reboot and duplicate side effects.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Code Review
This pull request increments the version to 4.260520.1 and enhances the agent recovery logic in scheduler-daemon.ts. Key improvements include adding a TMUX environment variable shim to allow agent resumption from the scheduler and updating the recovery pass to resolve session IDs for agents missing them after a reboot. A review comment suggests optimizing the recovery loop by moving a dynamic import outside the loop and ensuring that rehydration hints and the shouldResume chokepoint are correctly respected for all workers.
|
|
||
| for (const worker of resumable) { | ||
| try { | ||
| const alive = await deps.isPaneAlive(worker.paneId); | ||
| const hasTmuxPane = typeof worker.paneId === 'string' && /^%\d+$/.test(worker.paneId); | ||
| const alive = hasTmuxPane ? await deps.isPaneAlive(worker.paneId) : false; | ||
| if (alive) continue; | ||
| const outcome = await handleDeadPane(deps, resolvedConfig, daemonId, worker, turnAware, mode); | ||
|
|
||
| // Dead pane (or never had one — e.g. fresh-from-DB after a VM | ||
| // reboot). Bridge `currentSessionId` via the `shouldResume` chokepoint | ||
| // so attemptAgentResume can spawn `claude --resume <uuid>` against an | ||
| // on-disk JSONL the DB-join didn't surface. Going through the | ||
| // chokepoint (vs. calling `getResumeSessionId` directly) preserves | ||
| // invariant 3 in `state-machine.invariants.test.ts` — only | ||
| // `should-resume.ts` and the definition site may read the raw helper. | ||
| let enriched = worker; | ||
| if (!enriched.currentSessionId) { | ||
| const { shouldResume } = await import('./should-resume.js'); | ||
| const decision = await shouldResume(worker.id).catch(() => null); | ||
| if (decision?.sessionId) enriched = { ...worker, currentSessionId: decision.sessionId }; | ||
| } | ||
|
|
There was a problem hiding this comment.
The dynamic import of shouldResume is currently performed inside the worker loop. Since runAgentRecoveryPass can iterate over many workers, performing an await import() in each iteration adds unnecessary overhead, even if the module is cached by Node.js. It is more efficient to import the function once before the loop starts.
Furthermore, the current implementation only calls shouldResume for workers missing a session ID, and it ignores the resume flag and rehydrate hint. To properly enforce the "chokepoint" design and align with the PR's stated goal of eager vs. lazy rehydration:
shouldResumeshould be called for all dead-pane survivors to ensure that assignment status and other resume preconditions are checked.- The
enrichedworker should only carry acurrentSessionIdifdecision.resumeis true and, in boot mode, if the agent is marked foreagerrehydration. This prevents auto-spawning task-bound agents that should only be surfaced in status.
const { shouldResume } = await import('./should-resume.js');
for (const worker of resumable) {
try {
const hasTmuxPane = typeof worker.paneId === 'string' && /^%\d+$/.test(worker.paneId);
const alive = hasTmuxPane ? await deps.isPaneAlive(worker.paneId) : false;
if (alive) continue;
// Dead pane (or never had one — e.g. fresh-from-DB after a VM
// reboot). Bridge currentSessionId via the shouldResume chokepoint
// so attemptAgentResume can spawn claude --resume <uuid> against an
// on-disk JSONL the DB-join didn't surface. Going through the
// chokepoint (vs. calling getResumeSessionId directly) preserves
// invariant 3 in state-machine.invariants.test.ts — only
// should-resume.ts and the definition site may read the raw helper.
const decision = await shouldResume(worker.id).catch(() => null);
// Respect the chokepoint decision and rehydration hint. We only provide
// a sessionId if resume is permitted and, in boot mode, if the agent is
// marked for eager rehydration. Task-bound agents (lazy) are surfaced
// in status but not auto-resumed here.
const canResume = decision?.resume && (mode !== 'boot' || decision.rehydrate === 'eager');
const enriched = { ...worker, currentSessionId: canResume ? decision.sessionId : null };References
- Use dynamic imports (
require()orimport()) to break circular dependency cycles that would otherwise occur at module load time.
Three bot-reviewed correctness fixes on the v4.260520.1 recovery work. HIGH-1: codex P1 + gemini M on PR #2461. shouldResume returns sessionId for assignment_closed and auto_resume_disabled rows (for genie status display), not only for the resumable ok case. Previous code gated enrichment on decision?.sessionId alone, so a task agent whose latest assignment was done/failed would be re-invoked on every reboot — JSONL on disk, sessionId resurfaced, currentSessionId enriched, attemptAgentResume re-spawned. Fix: gate on decision.resume && decision.sessionId. HIGH-2: codex P1 on PR #2460. Dropping the currentSessionId clause widened the prefilter to state-only, so SDK/inline workers (paneId = synthetic id like 'sdk', not %N) entered the loop. With hasTmuxPane=false, alive was false and the worker fell into handleDeadPane. In sweep mode with turn-aware enabled, an idle SDK worker hits terminalizeCleanExitUnverified — a state-mutating write that corrupts live executor state for healthy non-tmux workers. Fix: prefilter on paneId shape (^%\d+$ only). Mirrors the canonical transport gate in countActiveWorkers. Also adds archived to excluded states (gemini M on #2460). MEDIUM: CodeRabbit major on PR #2461. .catch(() => null) on shouldResume converted transient resolver/DB failures to decision=null, the worker fell through unenriched, attemptAgentResume counted it as no_session_id and incremented the retry budget. After 3 transient blips a healthy worker's auto_resume flipped to false. Fix: let shouldResume throw — the outer try/catch routes through handleRecoveryProbeError or increments failed. LOW polish: hoisted shouldResume import outside loop (gemini M on #2461). Added archived to excluded states. Validated: - invariant 3 in state-machine.invariants.test.ts: pass - bun test --test-name-pattern recovery: 10 pass / 0 fail - bun run typecheck: clean - biome check src/lib/scheduler-daemon.ts: clean Ignored CodeRabbit major "SHA drift on package.json" on #2461 — false positive, Binary SHA Drift workflow is not running on this PR. 17/17 CI green on #2461. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…and-transport-gate fix(scheduler): respect shouldResume.resume + gate recovery on transport
There was a problem hiding this comment.
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 `@src/lib/scheduler-daemon.ts`:
- Around line 1207-1211: The current logic only calls shouldResume(worker.id)
when enriched.currentSessionId is falsy, which allows dead panes that already
have a session id to skip the resume decision; change the flow so that
shouldResume(worker.id) is consulted for every dead pane (regardless of
enriched.currentSessionId) and short-circuit handling when decision.resume !==
true (i.e., only proceed to boot-mode recovery/handleDeadPane if decision.resume
is true and decision.sessionId is present), updating the use of
enriched/currentSessionId assignment accordingly to use decision.sessionId when
resuming; references: enriched.currentSessionId, shouldResume(worker.id),
decision.resume, decision.sessionId, handleDeadPane.
🪄 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: 441c4b57-fcaa-4e8f-8836-34197ad1b83c
📒 Files selected for processing (1)
src/lib/scheduler-daemon.ts
CodeRabbit follow-up on PR #2461 (line 1211, posted 07:03Z). The v4.260520.2 fix only called shouldResume when currentSessionId was missing. Task agents with a closed assignment (decision.resume=false, reason=assignment_closed) usually KEEP their currentSessionId from the executor join — so they bypassed the chokepoint entirely and fell into handleDeadPane(mode='boot'). isLegitimatelyClosed there checks the executor's outcome, not the assignment's outcome, and misses the case when the agent finished its task but the executor didn't terminalize cleanly. Result: a task agent the boot pass had classified as lazy_surface gets re-invoked on reboot. Exact regression class the chokepoint exists to prevent. Fix: - Always consult shouldResume for dead-pane survivors. - Boot mode: short-circuit when decision.resume is false (matches the boot pass's lazy_surface/skip classification upstream). - Sweep mode: unaffected. Idle dead-pane → terminalizeCleanExitUnverified is the correct cleanup for done agents; the existing handleDeadPane path keeps doing its thing. Adds one shouldResume call per dead-pane survivor in the worst case (was: only null-session ones), but the set is narrow — most workers stay alive across a tick. Validated: - invariant 3 in state-machine.invariants.test.ts: pass - bun test --test-name-pattern recovery: 10 pass / 0 fail - bun run typecheck: clean - biome check src/lib/scheduler-daemon.ts: clean Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…epoint-for-sessioned-panes fix(scheduler): consult shouldResume for every dead pane in boot mode
CI red on PR #2461 caught by scheduler-daemon.test.ts and auto-resume-zombie-cap.test.ts after PR #2463 landed. Root cause: PR #2463 added a chokepoint call for every dead-pane survivor and short-circuited boot mode when decision.resume is false. Tests mock listWorkers but don't seed the agents table, so readAgentResumeRow returns null and shouldResume returns { resume: false, reason: 'unknown_agent' } — the short-circuit then drops fixtures the test expects to be resumed. In production this is impossible: listWorkers and readAgentResumeRow both read the agents table, so a row visible to the first read is visible to the second. The unknown_agent branch only fires on test fixtures that intentionally bypass the chokepoint's data source. Fix: exclude decision.reason === 'unknown_agent' from the short-circuit. Production semantics unchanged (case impossible); legacy test fixtures restored. A real production race (row visible to one read, invisible to the other) still falls through to attemptAgentResume which has its own guards. Validated: - bun test scheduler-daemon.test.ts: 118 pass / 0 fail (was 2 fail) - bun test auto-resume-zombie-cap.test.ts: pass (was undefined fail) - bun test state-machine.invariants.test.ts: 1 pass / 0 fail - bun run typecheck: clean - biome check: clean Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…-agent-in-boot-short-circuit fix(scheduler): exempt unknown_agent from boot-mode short-circuit
Summary
Promote dev → main for stable release v4.260520.1.
Headline fix: agents now survive Genie supervisor restarts (and by extension VM reboots).
Commits landing on main
71b7d4bc— fix(scheduler): agents survive VM reboot — bridge JSONL session fallback + TMUX-presence shim12bd5f45— fix(scheduler): route JSONL fallback through shouldResume chokepoint (CI fix; preserves invariant 3)bc4053f9— chore(version): bump to 4.260520.1 [auto-version]What changed (one paragraph)
src/lib/scheduler-daemon.ts—runAgentRecoveryPassanddefaultResumeAgent. The recovery pass previously filtered out every worker whoseexecutors.claude_session_idwas null, even thoughgenie statusresolves the session via a broader path (DB happy path → JSONL on-disk fallback viashouldResume). AnddefaultResumeAgentshelledgenie agent resumewithoutTMUXin env, so team-lessdir:agents (the majority after a reboot) erroredout with "no team context" and the barecatch {}swallowed it. Both fixed; session UUID lookup now routes through theshouldResumechokepoint per invariant 3.Reality check (validated on a reference host, pre-merge)
pm2 restart Genie→recovery_completed: resumed_agents: 0(across 33 registered agents, 132 teams, 2729 sessions).recovery_completed: resumed_agents: 17, failed_agents: 0+ 26 live tmux panes (was 2).no_session_idagents are genuine "JSONL not findable" cases handled by the runbook inUPGRADING-pgserve-v3.md.CI / quality
4054d11c.state-machine.invariants.test.ts) passes.Test plan
install.shfrom dev channel on a real host — confirmgenie --versionreports v4.260520.1 and apm2 restart Genieauto-resumes registered agentsrecovery_completedevent count after restart (genie events list --since 2m | grep recovery_completed) shows non-zeroresumed_agentsinstall.shon stable channel to confirm public users get the same fix🤖 Generated with Claude Code
Summary by CodeRabbit
Chores
Bug Fixes