Repository navigation
feat(runtime): add idle_cached lifecycle + Claude session resume - #2
Conversation
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).
…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.
|
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 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 |
guo-yu
left a comment
There was a problem hiding this comment.
Looks good after the stale-resume fallback fix. I verified the latest head locally: full test suite and typecheck pass.
Problem
A stateful long-running agent has no good lifecycle option today.
24/7keeps the process always-respawning but every iteration is a fresh Claude session, losing context.on-demandsaves CPU but is also stateless — every trigger cold-starts.Behavior
Add
idle_cached(Claude-only): idle likeon-demand, but the runtime captures the Claudesession_idfromsystem/initevents 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.AgentProcessrejectsidle_cachedpaired with a non-Claude effective runtime at construction (honoringWANMAN_RUNTIMEenv override) so the misconfig fails loudly at startup instead of silently degrading.24/7andon-demandsemantics are unchanged. Codex adapter is untouched.Tests
Full suite: 1105 passed / 8 skipped / 0 failed • Lines 90.33%.
New coverage:
--resumearg propagation,system/initsession-id capture (no double-fire / late-handler replay),resumeMissedfrom 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.