feat: PreCompact persists agent state (Bet 2 slice 3)#126
feat: PreCompact persists agent state (Bet 2 slice 3)#126kelsonpw wants to merge 2 commits intokelsonpw/agent-loop-statusfrom
Conversation
Bet 2, Slice 3 (real hooks — PreCompact substance). Why: compaction drops earlier turns from the LLM's context window. The agent can forget which files it has edited and where it was in the workflow, leading to re-writes of the same file or skipped steps. Persisting a small snapshot to disk just before compaction gives a future UserPromptSubmit hook (or a human debugging a stuck run) a grounded recovery point. What changed: - New `src/lib/agent-state.ts` — per-attempt AgentState bag tracking modified files (deduped + sorted), last status code/detail, and compaction count. Serializes to /tmp/amplitude-wizard-state-<attemptId>.json via atomic sync write with 0o600 perms. Schema versioned as amplitude-wizard-agent-state/1. - `createPreCompactHook(counters, state?)` — extended to record the compaction on AgentState and persist the snapshot. State arg is optional so existing callers without state still compile. - `createPostToolUseHook(counters, state?)` — extended to record file_path from Write and Edit tool inputs into AgentState. - StatusReporter in `runAgent` now also calls `agentState.recordStatus` so the persisted snapshot always carries the most recent status message. Tests: +11 in `agent-state.test.ts` (dedup, sort, status tracking, compaction counter, JSON schema validity, tmpdir path, hook wiring for Write/Edit/Read and counter-only fallback). 1068 total passing (was 1057). Post-compaction hydration (UserPromptSubmit reads the snapshot and prepends a recovery block to the user message) is a separate follow-up slice — not in scope here. This PR lands the persistence half of the round-trip; restoration lands next. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🧙 Wizard CIRun the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands: Test all apps:
Test all apps in a directory:
Test an individual app:
Show more apps
Results will be posted here when complete. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: AgentState not reset between retry attempts
- Added a reset() method to AgentState that clears modifiedFiles, lastStatus, and compactionCount, and called it alongside the other per-attempt resets in the retry loop.
Or push these changes by commenting:
@cursor push aeb8f831ea
Preview (aeb8f831ea)
diff --git a/src/lib/agent-interface.ts b/src/lib/agent-interface.ts
--- a/src/lib/agent-interface.ts
+++ b/src/lib/agent-interface.ts
@@ -1386,6 +1386,7 @@
collectedText.length = 0;
recentStatuses.length = 0;
authErrorDetected = false;
+ agentState.reset();
}
// Fresh prompt stream per attempt — stdin stays open until result received
diff --git a/src/lib/agent-state.ts b/src/lib/agent-state.ts
--- a/src/lib/agent-state.ts
+++ b/src/lib/agent-state.ts
@@ -45,6 +45,13 @@
this.compactionCount += 1;
}
+ /** Reset all mutable state for a fresh retry attempt. */
+ reset(): void {
+ this.modifiedFiles.clear();
+ this.lastStatus = null;
+ this.compactionCount = 0;
+ }
+
snapshot(): SerializedAgentState {
return {
schema: 'amplitude-wizard-agent-state/1',You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 9a4870e. Configure here.
…ation Applied via @cursor push command
Trivial fix
for (let attempt = 0; attempt < maxAttempts; attempt++) {
+ agentState.reset();
// ...existing reset block(or drop the pre-loop construction and re-instantiate inside the loop.) |
|
Superseded by #173 after the Bet 2 chain was re-rooted onto #171 (recovered slice 1). The original #126 sat on the orphan |
Adds per-attempt AgentState bag that tracks modified files, last structured status, and compaction count. Serializes to a deterministic tmpdir path so a future post-compaction UserPromptSubmit hook can hydrate the agent back with the context that compaction dropped. - New src/lib/agent-state.ts — AgentState class with setAttemptId, recordModifiedFile, recordStatus, recordCompaction, snapshot, persist, snapshotPath, reset. Schema versioned as amplitude-wizard-agent-state/1. - Instantiated per runAgent call; reset between retry attempts. - Wired into StatusReporter.onStatus so the persisted snapshot always carries the most recent status message. - +11 tests (agent-state.test.ts) covering dedup/sort, status tracking, compaction count, JSON schema, tmpdir path, reset semantics. Bet 2 slice 3. Recreated from #126 onto kelsonpw/agent-loop-status-recovered after the 2026-04-20 history reset. Hook-factory wiring for PreCompact and PostToolUse depends on Bet 1 ToolCallCounters landing first (PR #149); this slice ships the persistence primitive so later slices can wire it up with zero merge friction.
Adds per-attempt AgentState bag that tracks modified files, last structured status, and compaction count. Serializes to a deterministic tmpdir path so a future post-compaction UserPromptSubmit hook can hydrate the agent back with the context that compaction dropped. - New src/lib/agent-state.ts — AgentState class with setAttemptId, recordModifiedFile, recordStatus, recordCompaction, snapshot, persist, snapshotPath, reset. Schema versioned as amplitude-wizard-agent-state/1. - Instantiated per runAgent call; reset between retry attempts. - Wired into StatusReporter.onStatus so the persisted snapshot always carries the most recent status message. - +11 tests (agent-state.test.ts) covering dedup/sort, status tracking, compaction count, JSON schema, tmpdir path, reset semantics. Bet 2 slice 3. Recreated from #126 onto kelsonpw/agent-loop-status-recovered after the 2026-04-20 history reset. Hook-factory wiring for PreCompact and PostToolUse depends on Bet 1 ToolCallCounters landing first (PR #149); this slice ships the persistence primitive so later slices can wire it up with zero merge friction. Co-authored-by: Cursor Agent <cursoragent@cursor.com>


What changes for users
Nothing user-visible yet. When the wizard is mid-run and Claude has to compact its conversation to fit more context, the wizard now saves a small snapshot — which files it has edited, its most recent status, how many compactions have happened — so the agent can pick back up without forgetting. The follow-up slice reads that snapshot back; together they stop the wizard from re-writing files or skipping steps after a compaction.
Scope of this slice
src/lib/agent-state.ts: per-attemptAgentStatebag tracking modified files (deduped + sorted), last status code/detail, and compaction count.persist()writes a schema-versioned JSON snapshot totmpdir()/amplitude-wizard-state-<attemptId>.jsonwith0o600perms. Schema tagamplitude-wizard-agent-state/1so future readers can detect format drift.src/lib/agent-interface.ts:createPreCompactHook(counters, state?)records the compaction on AgentState and callsstate.persist();createPostToolUseHook(counters, state?)pullsfile_pathfromWrite/Edittool inputs and records it on AgentState. Bothstateargs optional so callers without state still compile.StatusReporterinrunAgentnow also callsagentState.recordStatusso the persisted snapshot always carries the most recent status message.agent-state.test.ts(dedup + sort, last-status-wins, compactionCount, JSON schema + contents, tmpdir path carries attempt id, hooks no-op when state omitted). 1068 total passing.How this advances Bet 2
Bet 2's "real hooks" deliverable turns the previously-declared-but-unwired hooks into actual behavior. This slice lands the persistence half of the PreCompact → UserPromptSubmit round-trip; the on-disk format is stable and versioned so slice 4 can consume it without coordination. Once hydration lands, the three-phase pipeline inherits a grounded recovery point across compactions.
Deferred to next slice
Post-compaction restoration — a
UserPromptSubmithook that reads the snapshot and prepends a recovery block to the first user message after a compaction fires. This PR lands the persistence half of the round-trip; hydration lands next. The on-disk format is stable and versioned so the restoration PR can consume it without coordination.Also out of scope: typecheck/lint gate on
PostToolUse(briefs says Stop-hook blocks on errors). Lower priority than hydration.Tests
+11 new in
agent-state.test.ts:recordStatuscreatePreCompactHookwrites to diskcreatePreCompactHookis a no-op on state when state arg is omittedcreatePostToolUseHookrecords Write + Edit but ignores ReadcreatePostToolUseHookcounter still increments when state is omitted1068 total passing (was 1057).
Test plan
pnpm testgreenpnpm buildsmoke passespnpm trywith a large-context framework (e.g. Next.js monorepo) until a compaction fires → verify/tmp/amplitude-wizard-state-*.jsonexists with modifiedFiles + lastStatus populatedwizard cli: tool summarystill reportscompactions: Ncc @amplitude/growth
Generated with Claude Code