feat(llm): resume the Claude Code session across a kill-chain phase - #149
feat(llm): resume the Claude Code session across a kill-chain phase#149N3thunt3r69 wants to merge 1 commit into
Conversation
Every ReAct iteration flattens the growing transcript into a brand-new prompt and spawns a fresh `claude -p` process. Within one task that is a quadratic resend; across a mission it also means each spawn re-pays Claude Code's own CLAUDE.md/skills/MCP bootstrap tax from scratch, since T3MP3ST never told the CLI these calls belong to one session. LocalAgentAdapter now tracks the Claude Code session id from the JSON envelope and passes it back in with --resume on every later call, so the CLI carries the accumulated transcript itself and the repeated part is billed at cache-read pricing instead of resent. The session is scoped to one kill-chain phase: TempestCommand drops it for every operator right after advancePhase(), since local-agent operators are spawned once for the whole mission and would otherwise carry one session across all 7 phases. A stale or expired session id fails fast (confirmed against the real CLI: nonzero exit, no JSON on stdout) and localAgentChat retries once without --resume rather than failing the task over it. Verified against the real CLI: a resumed call recalled prior context correctly and dropped from $0.151 to $0.008 (cache_read_input_tokens absorbing the prior turn instead of cache_creation), and an invalid --resume id triggered the fallback and still succeeded. Ran live against an authorized target: the same session id held across all 4 recon tasks in one phase, then a newly spawned operator's first call in the next phase carried no --resume, confirming both the resume and the reset. Full suite: 764/764, no regressions.
jmagly
left a comment
There was a problem hiding this comment.
Reviewed exact head 577f0a6 against current main (afc9dad). One blocking contract defect remains.
LocalAgentAdapter.chat() always builds prompt = formatPrompt(messages, options), which serializes the complete, growing ReAct transcript. On later calls the PR sends that full transcript while also passing the prior Claude session via --resume. The resumed session already contains earlier turns, so prior context is duplicated; the implementation therefore does not achieve the stated “carry the session instead of resending the transcript” behavior and can distort context as well as cost.
Please make resumed calls send only the delta/new turn (while fresh calls retain the full prompt), and add a regression test that asserts the second CLI prompt excludes prior messages/tool results while retaining the new observation and tool contract. Also cover the stale-session fallback: its fresh retry must receive the complete transcript, not the delta.
Verification on this head:
npm run typecheck: pass- focused local-agent tests: 66/66 pass
npm test: 764/764 pass, plus ops/model/refusal gates
The current tests only assert sessionId plumbing; they do not assert prompt contents, which is why the behavioral mismatch remains green.
Follow-up to #139/#140. That fix stopped
AgentLoop's token budget check from being blind, but the deeper cost driver is architectural: every ReAct iteration resends the whole growing transcript as a brand-newclaude -pprocess, and each of those spawns re-pays Claude Code's own CLAUDE.md/skills/MCP bootstrap tax from scratch. This uses Claude Code's--resumeto carry one session across a kill-chain phase instead, so the repeated part is billed at cache-read pricing instead of resent.Contribution Receipt
--resume) across a kill-chain phase, with a fallback to a fresh session if the resumed one is stale.npm run typecheck-> passnpm test-> pass (764/764 vitest, 11/11 ops-preflight, 19/19 model-matrix, refusal-frontier self-test)npm run doctor-> pass (30/33, 3 warnings: semgrep/promptfoo missing, API health offline with the server not running — no blockers)npm run verify-claims-> pass (27/27, no headline numbers changed)--resumerecall, the cost drop, and the stale-session failure shape; (2) a live recon-phase mission via/api/mission/startagainst an authorized external target, confirmed viapsthat the session id held across all 4 recon tasks in one phase and that a newly spawned operator's first call in the next phase carried no--resume. Both deleted/stopped after use, no logs retained.--resumefallback fires on any failure once a session id is in play, not only a confirmed-stale one; a real transient error on the resumed call pays one extra spawn before surfacing. Session scope is tied toadvancePhase()only — Codex/Hermes/OpenCode/Oh My Pi are untouched (Claude-only, same as Claude Code local-agent responses carry nousage:AgentLoop's token budget check never fires #139/fix(llm): populate usage for the Claude Code local-agent path #140). Mission-level state (the running phase/task graph) still does not survive a server restart, so a resumed session id would already be moot in that case; this PR does not change that.