Skip to content

feat(runtime): add idle_cached lifecycle + Claude session resume - #2

Merged
guo-yu merged 3 commits into
chekusu:mainfrom
ZaynJarvis:feat/idle-cached-resume
Apr 25, 2026
Merged

guo-yu merged 3 commits into
chekusu:mainfrom
ZaynJarvis:feat/idle-cached-resume

Conversation

@ZaynJarvis

@ZaynJarvis ZaynJarvis commented Apr 25, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A stateful long-running agent has no good lifecycle option today. 24/7 keeps the process always-respawning but every iteration is a fresh Claude session, losing context. on-demand saves CPU but is also stateless — every trigger cold-starts.

Behavior

Add idle_cached (Claude-only): idle like on-demand, but the runtime captures the Claude session_id from system/init events and passes it back as --resume <id> on the next trigger so prior context is restored. The first trigger always cold-starts. If the local Claude CLI no longer has the captured session, the runtime detects the failure (stderr + resumeMissed()), clears the cached id, and re-spawns once without --resume — no agent gets stranded.

AgentProcess rejects idle_cached paired with a non-Claude effective runtime at construction (honoring WANMAN_RUNTIME env override) so the misconfig fails loudly at startup instead of silently degrading.

24/7 and on-demand semantics are unchanged. Codex adapter is untouched.

Tests

Full suite: 1105 passed / 8 skipped / 0 failed • Lines 90.33%.

New coverage: --resume arg propagation, system/init session-id capture (no double-fire / late-handler replay), resumeMissed from stderr, idle_cached cold-starts the first trigger, resumes the captured id on the next, falls back when stale, on-demand never resumes (regression guard), constructor rejects declared-codex / WANMAN_RUNTIME=codex / accepts default + declared claude.

The 24/7 lifecycle keeps an agent process always available but throws
away conversation context on every respawn (each iteration is a fresh
Claude session). on-demand saves CPU but is also stateless — every
trigger cold-starts. There is currently no way to combine "no CPU when
idle" with "preserved context", which is wasteful for stateful
long-running roles where neither extreme fits.

This PR adds:

- A new `idle_cached` lifecycle. Idle like on-demand, but the runtime
  captures the Claude `session_id` from `system/init` events and passes
  it back as `--resume <id>` on the next trigger so prior context is
  restored. The first trigger always cold-starts.
- Claude `--resume` plumbing in claude-code.ts: accept `resumeSessionId`
  in spawn options, emit a session-id event via `onSessionId(handler)`,
  and detect the "session not found" stderr signal so callers can fall
  back to a cold start when the local Claude CLI has dropped the id.
- A stale-session fallback in AgentProcess.trigger(): if the resumed
  session is reported missing, the runtime clears the cached id and
  re-spawns once without `--resume`. No agent gets stranded by a stale
  session reference.

The supervisor's existing on-demand wake paths (new-message callback,
cron tick, unblocked-task wake) now also wake idle_cached agents.

24/7 and on-demand semantics are unchanged — both still get fresh
sessions per spawn. Existing agents need no migration.

Tests:
- claude-code: --resume arg, session-id capture, late-handler replay,
  resumeMissed detection from stderr, and the no-resume baseline.
- agent-process: idle_cached cold-starts the first trigger, resumes the
  captured id on subsequent triggers, falls back when the id is stale,
  and the existing on-demand path still discards captured ids.

Coverage: 90.33% line / 88.69% statement on the full suite (1101 passed,
8 skipped, 0 failed).
`idle_cached` requires `claude --resume`, which Codex has no equivalent
for in this runtime. Without a guard, an agent declared as `runtime:
codex + lifecycle: idle_cached`, or any idle_cached agent run under
`WANMAN_RUNTIME=codex`, would silently degrade to `on-demand` semantics
— losing the very context the lifecycle is supposed to preserve.

Validate at AgentProcess construction using the *effective* runtime
(via `resolveAgentRuntime`, so the env override is included) and throw
with a message that names the declared runtime, the resolved runtime,
and whether `WANMAN_RUNTIME` was the trigger. Trigger-time fallback is
removed — fail-fast at startup is cleaner than runtime degrade.

Docs (README + architecture.md + AgentLifecycle docstring) now mark
idle_cached explicitly as Claude-only.

Tests cover: declared-runtime mismatch, env-override mismatch, declared
claude runtime, default-runtime claude. 4 new tests, all green; full
suite still passes (1105 passed / 8 skipped).
Comment thread packages/runtime/src/claude-code.ts Resolved
…result.errors

The original `RESUME_MISSING_PATTERN` only matched "session"-shaped wordings
("no session found", "could not resume", etc.) but Claude Code 2.1.119
prints "No conversation found with session ID: ..." — both on stderr and
inside the structured `result` event's `result`/`errors[]` fields.

Fix:
- Extract the regex to a named constant and broaden it to match either
  noun (conversation|session) and either verb pattern. Document why the
  list is permissive (CLI wording shifted from "session" → "conversation"
  at least once).
- Detect resume-miss from the JSONL `result` event too (is_error +
  pattern hit on result/errors[]/error). The JSONL schema is versioned
  by Anthropic, so it's a more stable channel than stderr text.
- Stderr stays as a fallback in case the CLI puts the failure there
  instead of (or in addition to) the result event.

Tests:
- Regression for the exact 2.1.119 stderr wording reported on the PR.
- Result event with the failure in `result` (string).
- Result event with the failure in `errors[]` (array).
- Negative: unrelated `is_error: true` (e.g. max_turns) does not flip
  resumeMissed.

Full suite: 1109 passed / 8 skipped / 0 failed.
@ZaynJarvis

Copy link
Copy Markdown
Contributor Author

Quick note on the design rationale, in case it helps the next reader:

The fallback path (clear cached id → cold-start once) is intentional, not a workaround. A captured Claude session_id can go stale out-of-band — Claude CLI upgrade rotates the local session store, the user clears ~/.claude/sessions/, Anthropic GCs old conversations on their side, etc. None of that is something the daemon can prevent at the source. We can't probe ahead either: claude --resume <id> is the probe, and any cheaper check would race.

So the pattern is "trust the cached id, validate on use, fall back on miss" — same shape as HTTP / DNS / CPU caches. The bug fixed here was not the design but the detection — the regex didn't recognize the 2.1.119 wording, so the fallback never fired and stale-resume agents kept retrying with a known-bad id. After this commit the failure is detected on both stderr and the structured result event (is_error: true + match on result / errors[] / error), the cached id is cleared, and the next spawn cold-starts with no --resume. The "lose context this once" is the only user-visible effect; the agent doesn't get stranded.

@guo-yu guo-yu left a comment

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.

Looks good after the stale-resume fallback fix. I verified the latest head locally: full test suite and typecheck pass.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants