agent-nuvira 3.3.7 → 3.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/README.md +57 -50
  2. package/dist/agents/agents/planner.d.ts.map +1 -1
  3. package/dist/agents/agents/planner.js +41 -0
  4. package/dist/agents/agents/planner.js.map +1 -1
  5. package/dist/agents/agents/reasoner.d.ts.map +1 -1
  6. package/dist/agents/agents/reasoner.js +6 -0
  7. package/dist/agents/agents/reasoner.js.map +1 -1
  8. package/dist/agents/agents/runner.d.ts +6 -0
  9. package/dist/agents/agents/runner.d.ts.map +1 -1
  10. package/dist/agents/agents/runner.js +82 -5
  11. package/dist/agents/agents/runner.js.map +1 -1
  12. package/dist/agents/agents/writer-tool-calling.d.ts.map +1 -1
  13. package/dist/agents/agents/writer-tool-calling.js +5 -2
  14. package/dist/agents/agents/writer-tool-calling.js.map +1 -1
  15. package/dist/agents/agents/writer.d.ts.map +1 -1
  16. package/dist/agents/agents/writer.js +8 -4
  17. package/dist/agents/agents/writer.js.map +1 -1
  18. package/dist/agents/fanout-scheduler.d.ts +72 -0
  19. package/dist/agents/fanout-scheduler.d.ts.map +1 -0
  20. package/dist/agents/fanout-scheduler.js +100 -0
  21. package/dist/agents/fanout-scheduler.js.map +1 -0
  22. package/dist/agents/orchestrator.d.ts.map +1 -1
  23. package/dist/agents/orchestrator.js +30 -1
  24. package/dist/agents/orchestrator.js.map +1 -1
  25. package/dist/agents/plan-quality.d.ts +58 -0
  26. package/dist/agents/plan-quality.d.ts.map +1 -0
  27. package/dist/agents/plan-quality.js +152 -0
  28. package/dist/agents/plan-quality.js.map +1 -0
  29. package/dist/agents/prompt-assembly.d.ts +8 -0
  30. package/dist/agents/prompt-assembly.d.ts.map +1 -1
  31. package/dist/agents/prompt-assembly.js +18 -0
  32. package/dist/agents/prompt-assembly.js.map +1 -1
  33. package/dist/agents/step-handoff.d.ts +12 -2
  34. package/dist/agents/step-handoff.d.ts.map +1 -1
  35. package/dist/agents/step-handoff.js +65 -8
  36. package/dist/agents/step-handoff.js.map +1 -1
  37. package/dist/agents/tool-calling-agent.d.ts.map +1 -1
  38. package/dist/agents/tool-calling-agent.js +18 -0
  39. package/dist/agents/tool-calling-agent.js.map +1 -1
  40. package/dist/cli/cache.d.ts.map +1 -1
  41. package/dist/cli/cache.js +34 -4
  42. package/dist/cli/cache.js.map +1 -1
  43. package/dist/cli/chat.d.ts +6 -0
  44. package/dist/cli/chat.d.ts.map +1 -1
  45. package/dist/cli/chat.js +79 -18
  46. package/dist/cli/chat.js.map +1 -1
  47. package/dist/cli/cli-program.d.ts.map +1 -1
  48. package/dist/cli/cli-program.js +6 -0
  49. package/dist/cli/cli-program.js.map +1 -1
  50. package/dist/cli/config.d.ts +10 -0
  51. package/dist/cli/config.d.ts.map +1 -1
  52. package/dist/cli/config.js +179 -1
  53. package/dist/cli/config.js.map +1 -1
  54. package/dist/cli/continuity.d.ts +61 -0
  55. package/dist/cli/continuity.d.ts.map +1 -0
  56. package/dist/cli/continuity.js +112 -0
  57. package/dist/cli/continuity.js.map +1 -0
  58. package/dist/cli/dashboard.d.ts +3 -1
  59. package/dist/cli/dashboard.d.ts.map +1 -1
  60. package/dist/cli/dashboard.js +5 -16
  61. package/dist/cli/dashboard.js.map +1 -1
  62. package/dist/cli/doctor.d.ts.map +1 -1
  63. package/dist/cli/doctor.js +48 -1
  64. package/dist/cli/doctor.js.map +1 -1
  65. package/dist/cli/execute.d.ts +8 -0
  66. package/dist/cli/execute.d.ts.map +1 -1
  67. package/dist/cli/execute.js +34 -1
  68. package/dist/cli/execute.js.map +1 -1
  69. package/dist/cli/loop-executor.d.ts +24 -0
  70. package/dist/cli/loop-executor.d.ts.map +1 -1
  71. package/dist/cli/loop-executor.js +134 -13
  72. package/dist/cli/loop-executor.js.map +1 -1
  73. package/dist/cli/website.d.ts +25 -0
  74. package/dist/cli/website.d.ts.map +1 -0
  75. package/dist/cli/website.js +58 -0
  76. package/dist/cli/website.js.map +1 -0
  77. package/dist/config/manager.d.ts.map +1 -1
  78. package/dist/config/manager.js +35 -0
  79. package/dist/config/manager.js.map +1 -1
  80. package/dist/config/process-env.d.ts.map +1 -1
  81. package/dist/config/process-env.js +20 -0
  82. package/dist/config/process-env.js.map +1 -1
  83. package/dist/config/provider-env.d.ts +2 -1
  84. package/dist/config/provider-env.d.ts.map +1 -1
  85. package/dist/config/provider-env.js +19 -2
  86. package/dist/config/provider-env.js.map +1 -1
  87. package/dist/config/service-catalog.d.ts +81 -0
  88. package/dist/config/service-catalog.d.ts.map +1 -0
  89. package/dist/config/service-catalog.js +286 -0
  90. package/dist/config/service-catalog.js.map +1 -0
  91. package/dist/config/service-probe.d.ts +27 -0
  92. package/dist/config/service-probe.d.ts.map +1 -0
  93. package/dist/config/service-probe.js +104 -0
  94. package/dist/config/service-probe.js.map +1 -0
  95. package/dist/config/types.d.ts +63 -0
  96. package/dist/config/types.d.ts.map +1 -1
  97. package/dist/context/cache.d.ts +46 -2
  98. package/dist/context/cache.d.ts.map +1 -1
  99. package/dist/context/cache.js +85 -7
  100. package/dist/context/cache.js.map +1 -1
  101. package/dist/findings/verdicts.d.ts +59 -0
  102. package/dist/findings/verdicts.d.ts.map +1 -1
  103. package/dist/findings/verdicts.js +117 -0
  104. package/dist/findings/verdicts.js.map +1 -1
  105. package/dist/forwarded.d.ts +2 -0
  106. package/dist/forwarded.d.ts.map +1 -0
  107. package/dist/forwarded.js +2 -0
  108. package/dist/forwarded.js.map +1 -0
  109. package/dist/gateway/registry.d.ts.map +1 -1
  110. package/dist/gateway/registry.js +9 -0
  111. package/dist/gateway/registry.js.map +1 -1
  112. package/dist/learning/auto-router.d.ts +30 -0
  113. package/dist/learning/auto-router.d.ts.map +1 -1
  114. package/dist/learning/auto-router.js +69 -12
  115. package/dist/learning/auto-router.js.map +1 -1
  116. package/dist/learning/context-assembly.d.ts +37 -0
  117. package/dist/learning/context-assembly.d.ts.map +1 -0
  118. package/dist/learning/context-assembly.js +63 -0
  119. package/dist/learning/context-assembly.js.map +1 -0
  120. package/dist/learning/reasoning-trace.d.ts +26 -5
  121. package/dist/learning/reasoning-trace.d.ts.map +1 -1
  122. package/dist/learning/reasoning-trace.js +21 -1
  123. package/dist/learning/reasoning-trace.js.map +1 -1
  124. package/dist/learning/session-digest.d.ts +78 -0
  125. package/dist/learning/session-digest.d.ts.map +1 -0
  126. package/dist/learning/session-digest.js +185 -0
  127. package/dist/learning/session-digest.js.map +1 -0
  128. package/dist/learning/session-recall.d.ts +135 -0
  129. package/dist/learning/session-recall.d.ts.map +1 -0
  130. package/dist/learning/session-recall.js +305 -0
  131. package/dist/learning/session-recall.js.map +1 -0
  132. package/dist/learning/session-store.d.ts +172 -0
  133. package/dist/learning/session-store.d.ts.map +1 -0
  134. package/dist/learning/session-store.js +424 -0
  135. package/dist/learning/session-store.js.map +1 -0
  136. package/dist/learning/step-checkpoint.d.ts +28 -0
  137. package/dist/learning/step-checkpoint.d.ts.map +1 -1
  138. package/dist/learning/step-checkpoint.js +90 -6
  139. package/dist/learning/step-checkpoint.js.map +1 -1
  140. package/dist/learning/working-state.d.ts +19 -0
  141. package/dist/learning/working-state.d.ts.map +1 -1
  142. package/dist/learning/working-state.js +34 -1
  143. package/dist/learning/working-state.js.map +1 -1
  144. package/dist/parity/drivers.d.ts.map +1 -1
  145. package/dist/parity/drivers.js +29 -0
  146. package/dist/parity/drivers.js.map +1 -1
  147. package/dist/parity/wire-fixtures.d.ts +88 -0
  148. package/dist/parity/wire-fixtures.d.ts.map +1 -0
  149. package/dist/parity/wire-fixtures.js +405 -0
  150. package/dist/parity/wire-fixtures.js.map +1 -0
  151. package/dist/tools/artifact-append.d.ts +23 -0
  152. package/dist/tools/artifact-append.d.ts.map +1 -1
  153. package/dist/tools/artifact-append.js +28 -0
  154. package/dist/tools/artifact-append.js.map +1 -1
  155. package/dist/tools/ask-user.d.ts +5 -0
  156. package/dist/tools/ask-user.d.ts.map +1 -1
  157. package/dist/tools/ask-user.js +51 -5
  158. package/dist/tools/ask-user.js.map +1 -1
  159. package/dist/tools/child-agent-entry.js +1 -0
  160. package/dist/tools/child-agent-entry.js.map +1 -1
  161. package/dist/tools/child-agent-runtime.d.ts +7 -0
  162. package/dist/tools/child-agent-runtime.d.ts.map +1 -1
  163. package/dist/tools/child-agent-runtime.js +116 -6
  164. package/dist/tools/child-agent-runtime.js.map +1 -1
  165. package/dist/tools/child-agent-worker.d.ts +15 -0
  166. package/dist/tools/child-agent-worker.d.ts.map +1 -0
  167. package/dist/tools/child-agent-worker.js.map +1 -0
  168. package/dist/tools/coding-tools.d.ts.map +1 -1
  169. package/dist/tools/coding-tools.js +11 -0
  170. package/dist/tools/coding-tools.js.map +1 -1
  171. package/dist/tools/finding-tool.d.ts +6 -1
  172. package/dist/tools/finding-tool.d.ts.map +1 -1
  173. package/dist/tools/finding-tool.js +15 -2
  174. package/dist/tools/finding-tool.js.map +1 -1
  175. package/dist/tools/followup-utils.d.ts +35 -0
  176. package/dist/tools/followup-utils.d.ts.map +1 -1
  177. package/dist/tools/followup-utils.js +36 -0
  178. package/dist/tools/followup-utils.js.map +1 -1
  179. package/dist/tools/loop-project-context.d.ts +4 -1
  180. package/dist/tools/loop-project-context.d.ts.map +1 -1
  181. package/dist/tools/loop-project-context.js +48 -1
  182. package/dist/tools/loop-project-context.js.map +1 -1
  183. package/dist/tools/loop-skill-hint.d.ts.map +1 -1
  184. package/dist/tools/loop-skill-hint.js +7 -0
  185. package/dist/tools/loop-skill-hint.js.map +1 -1
  186. package/dist/tools/modality/image-gen.d.ts +25 -6
  187. package/dist/tools/modality/image-gen.d.ts.map +1 -1
  188. package/dist/tools/modality/image-gen.js +46 -27
  189. package/dist/tools/modality/image-gen.js.map +1 -1
  190. package/dist/tools/modality/image-providers.d.ts +71 -0
  191. package/dist/tools/modality/image-providers.d.ts.map +1 -0
  192. package/dist/tools/modality/image-providers.js +277 -0
  193. package/dist/tools/modality/image-providers.js.map +1 -0
  194. package/dist/tools/registry.d.ts +10 -1
  195. package/dist/tools/registry.d.ts.map +1 -1
  196. package/dist/tools/registry.js +35 -8
  197. package/dist/tools/registry.js.map +1 -1
  198. package/dist/tools/run-cli.d.ts.map +1 -1
  199. package/dist/tools/run-cli.js +39 -1
  200. package/dist/tools/run-cli.js.map +1 -1
  201. package/dist/tools/run-terminal.d.ts.map +1 -1
  202. package/dist/tools/run-terminal.js +50 -6
  203. package/dist/tools/run-terminal.js.map +1 -1
  204. package/dist/tools/subagent-spawner.d.ts +8 -0
  205. package/dist/tools/subagent-spawner.d.ts.map +1 -1
  206. package/dist/tools/subagent-spawner.js +3 -0
  207. package/dist/tools/subagent-spawner.js.map +1 -1
  208. package/dist/tools/tool-loop.d.ts +55 -0
  209. package/dist/tools/tool-loop.d.ts.map +1 -1
  210. package/dist/tools/tool-loop.js +136 -0
  211. package/dist/tools/tool-loop.js.map +1 -1
  212. package/dist/tools/untrusted-content.d.ts +29 -0
  213. package/dist/tools/untrusted-content.d.ts.map +1 -0
  214. package/dist/tools/untrusted-content.js +53 -0
  215. package/dist/tools/untrusted-content.js.map +1 -0
  216. package/dist/tools/web-research.d.ts +76 -14
  217. package/dist/tools/web-research.d.ts.map +1 -1
  218. package/dist/tools/web-research.js +280 -35
  219. package/dist/tools/web-research.js.map +1 -1
  220. package/dist/utils/effect-verification.d.ts +115 -0
  221. package/dist/utils/effect-verification.d.ts.map +1 -0
  222. package/dist/utils/effect-verification.js +337 -0
  223. package/dist/utils/effect-verification.js.map +1 -0
  224. package/dist/utils/open-url.d.ts +16 -0
  225. package/dist/utils/open-url.d.ts.map +1 -0
  226. package/dist/utils/open-url.js +32 -0
  227. package/dist/utils/open-url.js.map +1 -0
  228. package/dist/utils/project-env.d.ts +146 -0
  229. package/dist/utils/project-env.d.ts.map +1 -0
  230. package/dist/utils/project-env.js +584 -0
  231. package/dist/utils/project-env.js.map +1 -0
  232. package/dist/utils/shell.d.ts +7 -0
  233. package/dist/utils/shell.d.ts.map +1 -1
  234. package/dist/utils/shell.js +2 -0
  235. package/dist/utils/shell.js.map +1 -1
  236. package/dist/web-dashboard/chat-console.d.ts +2 -0
  237. package/dist/web-dashboard/chat-console.d.ts.map +1 -1
  238. package/dist/web-dashboard/chat-console.js +7 -0
  239. package/dist/web-dashboard/chat-console.js.map +1 -1
  240. package/dist/web-dashboard/server.d.ts +3 -1
  241. package/dist/web-dashboard/server.d.ts.map +1 -1
  242. package/dist/web-dashboard/server.js +534 -8
  243. package/dist/web-dashboard/server.js.map +1 -1
  244. package/dist/web-dashboard/src/types.d.ts +150 -1
  245. package/dist/web-dashboard/src/types.d.ts.map +1 -1
  246. package/dist/web-dashboard/workspace-guard.d.ts +40 -0
  247. package/dist/web-dashboard/workspace-guard.d.ts.map +1 -0
  248. package/dist/web-dashboard/workspace-guard.js +92 -0
  249. package/dist/web-dashboard/workspace-guard.js.map +1 -0
  250. package/package.json +4 -1
  251. package/src/web-dashboard/public/assets/index-Beportyl.js +207 -0
  252. package/src/web-dashboard/public/assets/index-Beportyl.js.map +1 -0
  253. package/src/web-dashboard/public/assets/index-gtQyg9nm.css +1 -0
  254. package/src/web-dashboard/public/index.html +2 -2
  255. package/src/web-dashboard/public/assets/index-D1YFCcIZ.js +0 -207
  256. package/src/web-dashboard/public/assets/index-D1YFCcIZ.js.map +0 -1
  257. package/src/web-dashboard/public/assets/index-jkW5qcf-.css +0 -1
@@ -0,0 +1,424 @@
1
+ /**
2
+ * WS5 (#27) — Phase 4b/4c: the PERSISTENT SESSION STORE (the L2 layer).
3
+ *
4
+ * WHY THIS EXISTS. The resume ledger (`step-checkpoint.ts`) makes a re-run cheap
5
+ * by replaying unchanged MODEL CALLS, and the task checkpoints
6
+ * (`agents/checkpoint-store.ts`) skip COMPLETED pipeline tasks. Both are keyed by
7
+ * what a run FINISHED. Neither survives the one failure a long-running agent
8
+ * actually hits: the PROCESS dies mid-turn (a closed laptop, a killed terminal,
9
+ * a quota-killed provider). At that instant the conversation lived only in
10
+ * memory, and the next process starts cold — it re-derives the whole context and
11
+ * re-pays for the steps that were already done.
12
+ *
13
+ * WHAT IT STORES, AND WHAT IT REFUSES TO STORE. At every step boundary the loop
14
+ * hands over its live `ToolMessage[]` thread plus a small accumulator (steps,
15
+ * successful tools, mutated paths). The store keeps a bounded, REDACTED copy on
16
+ * disk. It is a transcript — nothing more. It does NOT record a completion
17
+ * state, an outcome, or any verdict: completion is still derived from the
18
+ * filesystem and the run's own facts (see the Part 1/Part 2 lesson). Re-importing
19
+ * the transcript gives the next process the CONVERSATION, not a claim that the
20
+ * work is done.
21
+ *
22
+ * WHY THE HEAD IS REPLACED ON REHYDRATION. The first messages of a loop thread
23
+ * are the system prompt (tool contract) and the `[Project context]` block. Those
24
+ * are reconstructible, and they DRIFT between runs (the file tree, the git
25
+ * digest). Persisting them would both age and bloat the record. So the store
26
+ * counts the leading head messages at save time (`headLength`) and the caller
27
+ * re-injects a FRESH head over the stored conversation tail on resume: the
28
+ * contract is current, the history is intact.
29
+ *
30
+ * WHY A COMPLETED SESSION IS NOT RESUMABLE. If a turn ran to a clean end, its
31
+ * transcript is history — re-feeding a finished conversation would have the model
32
+ * continue a conversation that is over, and risk narrating a stale "done". So
33
+ * `finish()` marks a snapshot closed, and the resumable lookup skips closed
34
+ * snapshots. The transcript is still kept (bounded) for a human to read; it is
35
+ * simply not replayed.
36
+ *
37
+ * WHY NOTHING HAPPENS WITHOUT A REQUEST. Opening the store is the ONLY thing that
38
+ * reads or writes the session directory. An ordinary run never touches it, pays
39
+ * no filesystem cost, and cannot inherit another run's conversation.
40
+ *
41
+ * Everything here is best-effort: a corrupt record is a MISS, never a crash, and
42
+ * a failed write reports (or stays silent) without breaking the turn that asked.
43
+ */
44
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
45
+ import { join } from 'node:path';
46
+ import { checkpointIdFor, goalsLookSame } from '../agents/checkpoint-store.js';
47
+ import { envBuff, resolveNuviraHome } from '../config/paths.js';
48
+ import { redact } from '../enterprise/secrets.js';
49
+ /** The environment key a deployment uses to turn session persistence OFF. */
50
+ export const SESSION_ENABLE_ENV = 'NUVIRA_SESSION_STORE';
51
+ /** Words a reader treats as OFF (same vocabulary the process-env page uses). */
52
+ const OFF_WORDS = new Set(['0', 'false', 'off', 'no']);
53
+ /**
54
+ * Is session persistence on? DEFAULT ON — like checkpointing, a fresh install
55
+ * gets continuity and an operator can turn it off. `0`/`false`/`off`/`no` turn
56
+ * it off; unset (or any other value) leaves it ON.
57
+ */
58
+ export function sessionStoreEnabled(env = process.env) {
59
+ const raw = env[SESSION_ENABLE_ENV];
60
+ if (typeof raw !== 'string')
61
+ return true;
62
+ return !OFF_WORDS.has(raw.trim().toLowerCase());
63
+ }
64
+ /**
65
+ * Resolve whether session persistence is on, with the repo's precedence: an
66
+ * explicit FLAG wins, then the environment, then config, then the default (ON).
67
+ * One place decides, so the CLI, the loop and the dashboard cannot disagree.
68
+ */
69
+ export function resolveSessionStore(input = {}) {
70
+ if (input.flag !== undefined)
71
+ return input.flag;
72
+ const viaEnv = envBuff('SESSION_STORE');
73
+ if (typeof viaEnv === 'string')
74
+ return !OFF_WORDS.has(viaEnv.trim().toLowerCase());
75
+ return input.configManager?.getAll().memory?.sessionStore !== false;
76
+ }
77
+ /** Bound on stored conversation messages — the most recent N are kept. */
78
+ export const MAX_SESSION_MESSAGES = 48;
79
+ /** Bound on a single stored message's content (the rest is clipped). */
80
+ export const MAX_MESSAGE_CHARS = 8000;
81
+ // ─── Storage ────────────────────────────────────────────────────────────────
82
+ function sessionsDir() {
83
+ const base = envBuff('MEMORY_DIR') || join(resolveNuviraHome(), 'memory');
84
+ return join(base, 'sessions');
85
+ }
86
+ function snapshotPath(id) {
87
+ return join(sessionsDir(), `${id}.json`);
88
+ }
89
+ /** Read a snapshot, best-effort. A corrupt record is a miss, never a crash. */
90
+ export function loadSessionSnapshot(id) {
91
+ try {
92
+ const path = snapshotPath(id);
93
+ if (!existsSync(path))
94
+ return null;
95
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
96
+ if (!parsed || typeof parsed !== 'object')
97
+ return null;
98
+ if (typeof parsed.id !== 'string' || !Array.isArray(parsed.messages))
99
+ return null;
100
+ if (typeof parsed.goal !== 'string' || typeof parsed.cwd !== 'string')
101
+ return null;
102
+ return {
103
+ id: parsed.id,
104
+ goal: parsed.goal,
105
+ cwd: parsed.cwd,
106
+ savedAt: typeof parsed.savedAt === 'number' ? parsed.savedAt : 0,
107
+ revision: typeof parsed.revision === 'number' ? parsed.revision : 0,
108
+ open: parsed.open !== false,
109
+ headLength: typeof parsed.headLength === 'number' ? parsed.headLength : 0,
110
+ messages: parsed.messages.filter(isToolMessage),
111
+ accumulators: normalizeAccumulators(parsed.accumulators),
112
+ };
113
+ }
114
+ catch {
115
+ return null;
116
+ }
117
+ }
118
+ function isToolMessage(value) {
119
+ if (!value || typeof value !== 'object')
120
+ return false;
121
+ const m = value;
122
+ return ((m.role === 'system' || m.role === 'user' || m.role === 'assistant' || m.role === 'tool') &&
123
+ typeof m.content === 'string');
124
+ }
125
+ function normalizeAccumulators(value) {
126
+ const a = (value ?? {});
127
+ return {
128
+ steps: typeof a.steps === 'number' ? a.steps : 0,
129
+ successfulTools: Array.isArray(a.successfulTools) ? a.successfulTools.filter((t) => typeof t === 'string') : [],
130
+ mutatedPaths: Array.isArray(a.mutatedPaths) ? a.mutatedPaths.filter((t) => typeof t === 'string') : [],
131
+ ...(Array.isArray(a.erroredTools)
132
+ ? { erroredTools: a.erroredTools.filter((t) => typeof t === 'string') }
133
+ : {}),
134
+ };
135
+ }
136
+ /** Write a snapshot, best-effort. Returns whether it reached the disk. */
137
+ function writeSnapshot(snapshot) {
138
+ try {
139
+ mkdirSync(sessionsDir(), { recursive: true });
140
+ writeFileSync(snapshotPath(snapshot.id), JSON.stringify(snapshot, null, 2), 'utf-8');
141
+ return true;
142
+ }
143
+ catch {
144
+ return false;
145
+ }
146
+ }
147
+ // ─── Bounds ─────────────────────────────────────────────────────────────────
148
+ /**
149
+ * Bound and redact a thread for storage.
150
+ *
151
+ * The TAIL is kept (the last `MAX_SESSION_MESSAGES` messages): the most recent
152
+ * tool results and the current question are what a resuming model needs, while
153
+ * the opening context is reconstructible. Every content string is clipped to
154
+ * `MAX_MESSAGE_CHARS` and run through the shared `redact` — a transcript that
155
+ * leaks a key is not worth the continuity it buys.
156
+ *
157
+ * Tool-call arguments and `providerMeta` are preserved verbatim only when they
158
+ * survive JSON (they are structured data the provider may need) and are never
159
+ * trusted to be secret-free, so they are redacted through a JSON round-trip.
160
+ */
161
+ function boundMessages(messages) {
162
+ const headLength = countHead(messages);
163
+ const sliced = messages.length > MAX_SESSION_MESSAGES ? messages.slice(-MAX_SESSION_MESSAGES) : messages.slice();
164
+ const bounded = sliced.map((message) => boundMessage(message));
165
+ // If the slice dropped the head, the stored head is gone too (the caller's
166
+ // `headLength` then refers to a head that is not in the record).
167
+ return { messages: bounded, headLength: Math.min(headLength, bounded.length) };
168
+ }
169
+ /** Leading system messages, plus a directly-following `[Project context]` block. */
170
+ function countHead(messages) {
171
+ let head = 0;
172
+ while (head < messages.length && messages[head].role === 'system')
173
+ head += 1;
174
+ if (head < messages.length && messages[head].role === 'user' && messages[head].content.startsWith('[Project context]')) {
175
+ head += 1;
176
+ }
177
+ return head;
178
+ }
179
+ function boundMessage(message) {
180
+ const out = {
181
+ role: message.role,
182
+ content: clip(message.content),
183
+ };
184
+ if (message.toolCallId)
185
+ out.toolCallId = message.toolCallId;
186
+ if (Array.isArray(message.toolCalls) && message.toolCalls.length > 0) {
187
+ out.toolCalls = message.toolCalls.map((call) => {
188
+ const copy = { ...call };
189
+ if (typeof copy.arguments === 'string')
190
+ copy.arguments = clip(copy.arguments);
191
+ return copy;
192
+ });
193
+ }
194
+ return out;
195
+ }
196
+ function clip(text) {
197
+ if (typeof text !== 'string')
198
+ return '';
199
+ const redacted = redact(text);
200
+ return redacted.length > MAX_MESSAGE_CHARS ? `${redacted.slice(0, MAX_MESSAGE_CHARS)}…[clipped]` : redacted;
201
+ }
202
+ const STORE_STATES = new WeakMap();
203
+ /**
204
+ * Open (or start) the session for this ask, in this directory.
205
+ *
206
+ * An explicit `id` is honoured exactly. With none, the deterministic
207
+ * `checkpointIdFor(goal, cwd)` is used — the SAME key the resume ledger uses —
208
+ * so a reworded re-ask resolves to the same session through
209
+ * {@link resolveSessionIdFor} / {@link findResumableSessionFor}.
210
+ */
211
+ /** Whether this process has already opportunistically bounded the store. */
212
+ let PRUNED_THIS_PROCESS = false;
213
+ /** Keep the store from growing without bound — run at most once per process. */
214
+ function pruneOnce() {
215
+ if (PRUNED_THIS_PROCESS)
216
+ return;
217
+ PRUNED_THIS_PROCESS = true;
218
+ try {
219
+ const entries = listSessionSnapshots();
220
+ if (entries.length > 100)
221
+ pruneSessions({ maxCount: 100 });
222
+ }
223
+ catch {
224
+ // Best-effort — a prune failure must never break an open.
225
+ }
226
+ }
227
+ export function openSession(input) {
228
+ pruneOnce();
229
+ const id = input.id?.trim() || checkpointIdFor(input.goal, input.cwd);
230
+ const prior = loadSessionSnapshot(id);
231
+ const state = {
232
+ id,
233
+ goal: input.goal,
234
+ cwd: input.cwd,
235
+ revision: prior?.revision ?? 0,
236
+ saved: false,
237
+ lastMessages: prior?.messages ?? [],
238
+ lastAccumulators: prior?.accumulators ?? { steps: 0, successfulTools: [], mutatedPaths: [] },
239
+ };
240
+ const persist = (open) => {
241
+ const { messages, headLength } = boundMessages(state.lastMessages);
242
+ const snapshot = {
243
+ id: state.id,
244
+ goal: state.goal,
245
+ cwd: state.cwd,
246
+ savedAt: Date.now(),
247
+ revision: state.revision + 1,
248
+ open,
249
+ headLength,
250
+ messages,
251
+ accumulators: state.lastAccumulators,
252
+ };
253
+ state.revision += 1;
254
+ state.saved = writeSnapshot(snapshot);
255
+ };
256
+ const store = {
257
+ id,
258
+ save(thread, accumulators) {
259
+ state.lastMessages = thread.slice();
260
+ state.lastAccumulators = normalizeAccumulators(accumulators);
261
+ persist(true);
262
+ },
263
+ finish(thread, accumulators) {
264
+ if (thread)
265
+ state.lastMessages = thread.slice();
266
+ if (accumulators)
267
+ state.lastAccumulators = normalizeAccumulators(accumulators);
268
+ persist(false);
269
+ },
270
+ lastSaved() {
271
+ return state.saved;
272
+ },
273
+ };
274
+ STORE_STATES.set(store, state);
275
+ return store;
276
+ }
277
+ /**
278
+ * The session id for this ask, in this directory, possibly WORDED DIFFERENTLY.
279
+ *
280
+ * Same contract as `resolveRecordIdFor` in the resume ledger: the exact id if a
281
+ * snapshot exists, else the newest OPEN snapshot in this directory whose goal
282
+ * `goalsLookSame`. A snapshot for a different ask is never returned — the caller
283
+ * would otherwise rehydrate a conversation it never had.
284
+ */
285
+ export function resolveSessionIdFor(goal, cwd, options = {}) {
286
+ const exact = checkpointIdFor(goal, cwd);
287
+ const onlyOpen = options.onlyOpen ?? true;
288
+ const exactSnap = loadSessionSnapshot(exact);
289
+ if (exactSnap && (!onlyOpen || exactSnap.open))
290
+ return exact;
291
+ const wanted = normalizeDir(cwd);
292
+ let best = null;
293
+ try {
294
+ for (const file of readdirSync(sessionsDir())) {
295
+ if (!file.endsWith('.json'))
296
+ continue;
297
+ const id = file.slice(0, -'.json'.length);
298
+ if (id === exact)
299
+ continue;
300
+ const snap = loadSessionSnapshot(id);
301
+ if (!snap)
302
+ continue;
303
+ if (onlyOpen && !snap.open)
304
+ continue;
305
+ if (normalizeDir(snap.cwd) !== wanted)
306
+ continue;
307
+ if (!goalsLookSame(snap.goal, goal))
308
+ continue;
309
+ if (!best || snap.savedAt > best.savedAt)
310
+ best = { id, savedAt: snap.savedAt };
311
+ }
312
+ }
313
+ catch {
314
+ return exact;
315
+ }
316
+ return best?.id ?? exact;
317
+ }
318
+ /**
319
+ * The resumable snapshot for this ask, or null when there is nothing to resume.
320
+ *
321
+ * Only OPEN snapshots qualify (see the module docstring): a transcript from a
322
+ * turn that ended cleanly is history, and re-feeding it would have the model
323
+ * continue a finished conversation.
324
+ */
325
+ export function findResumableSessionFor(goal, cwd) {
326
+ const id = resolveSessionIdFor(goal, cwd, { onlyOpen: true });
327
+ const snap = loadSessionSnapshot(id);
328
+ if (!snap || !snap.open || snap.messages.length === 0)
329
+ return null;
330
+ return snap;
331
+ }
332
+ /**
333
+ * Rebuild a loop thread from a stored snapshot and a FRESH head.
334
+ *
335
+ * The fresh head (system prompt, current project context) replaces the stored
336
+ * head; the stored conversation tail — the goal and everything done after it —
337
+ * is preserved. The result is what the next process loops over: it already knows
338
+ * what the dead process did, so those steps are neither re-run nor re-paid.
339
+ */
340
+ export function rehydrateThread(head, snapshot) {
341
+ const start = Math.min(snapshot.headLength, snapshot.messages.length);
342
+ return [...head, ...snapshot.messages.slice(start)];
343
+ }
344
+ /** A bounded, human-facing line describing a resumable snapshot. */
345
+ export function formatSessionResume(snapshot) {
346
+ const steps = snapshot.accumulators.steps;
347
+ const tools = snapshot.accumulators.successfulTools.length;
348
+ const paths = snapshot.accumulators.mutatedPaths.length;
349
+ return (`↩️ session for this ask is still OPEN — ${steps} step(s), ${tools} successful tool call(s)` +
350
+ (paths > 0 ? `, ${paths} path(s) changed` : '') +
351
+ ' — the conversation is restored (this is HISTORY, not a status: verify artifacts on disk)');
352
+ }
353
+ /**
354
+ * List every stored session snapshot, newest first. Best-effort: a corrupt or
355
+ * unreadable record is skipped, never thrown.
356
+ */
357
+ export function listSessionSnapshots() {
358
+ try {
359
+ const out = [];
360
+ for (const file of readdirSync(sessionsDir())) {
361
+ if (!file.endsWith('.json'))
362
+ continue;
363
+ const snap = loadSessionSnapshot(file.slice(0, -'.json'.length));
364
+ if (snap)
365
+ out.push(snap);
366
+ }
367
+ return out.sort((a, b) => b.savedAt - a.savedAt);
368
+ }
369
+ catch {
370
+ return [];
371
+ }
372
+ }
373
+ /** Remove one snapshot. Returns whether a file was removed. */
374
+ export function clearSession(id) {
375
+ try {
376
+ const path = snapshotPath(id);
377
+ if (!existsSync(path))
378
+ return false;
379
+ rmSync(path, { force: true });
380
+ return true;
381
+ }
382
+ catch {
383
+ return false;
384
+ }
385
+ }
386
+ /**
387
+ * Bound the session directory: drop snapshots older than `maxAgeMs` and keep at
388
+ * most `maxCount` (newest first). Returns how many files were removed.
389
+ *
390
+ * Best-effort and never throws — a prune that fails leaves the records (a disk
391
+ * full of old transcripts is a smaller problem than a broken turn).
392
+ */
393
+ export function pruneSessions(options = {}) {
394
+ const now = options.now ?? Date.now();
395
+ const maxAgeMs = options.maxAgeMs ?? 30 * 24 * 60 * 60 * 1000;
396
+ const maxCount = options.maxCount ?? 50;
397
+ let removed = 0;
398
+ try {
399
+ const entries = [];
400
+ for (const file of readdirSync(sessionsDir())) {
401
+ if (!file.endsWith('.json'))
402
+ continue;
403
+ const id = file.slice(0, -'.json'.length);
404
+ const snap = loadSessionSnapshot(id);
405
+ entries.push({ id, savedAt: snap?.savedAt ?? 0 });
406
+ }
407
+ entries.sort((a, b) => b.savedAt - a.savedAt);
408
+ for (let i = 0; i < entries.length; i += 1) {
409
+ const { id, savedAt } = entries[i];
410
+ const tooOld = maxAgeMs > 0 && now - savedAt > maxAgeMs;
411
+ const overCount = i >= maxCount;
412
+ if ((tooOld || overCount) && clearSession(id))
413
+ removed += 1;
414
+ }
415
+ }
416
+ catch {
417
+ return removed;
418
+ }
419
+ return removed;
420
+ }
421
+ function normalizeDir(dir) {
422
+ return (dir || '').replace(/[\\/]+$/, '');
423
+ }
424
+ //# sourceMappingURL=session-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-store.js","sourceRoot":"","sources":["../../src/learning/session-store.ts"],"names":[],"mappings":"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"}
@@ -68,6 +68,21 @@ export declare function resolveResumeRequest(input: {
68
68
  * not a change of input — while a different VALUE anywhere hashes differently.
69
69
  */
70
70
  export declare function stepDigest(thread: readonly unknown[], schemas: readonly unknown[]): string;
71
+ /**
72
+ * B3 — the record id for this ask, in this directory, possibly WORDED DIFFERENTLY.
73
+ *
74
+ * `checkpointIdFor` hashes the literal goal, so "fix the hotkey" and "hotkey
75
+ * still not working" are two unrelated ids and the second ask cannot find the
76
+ * first's record — the reworded-ask miss the pipeline arm already solves with
77
+ * `findRelatedCheckpointFor` / `goalsLookSame`. The loop's replay ledger had no
78
+ * equivalent, so a reworded `--resume` replayed nothing.
79
+ *
80
+ * Order of preference: the EXACT id (a record we know is this ask), then the
81
+ * newest record in this directory whose goal `goalsLookSame` (the same ask,
82
+ * worded differently). A record for a DIFFERENT ask is never returned — the
83
+ * caller would otherwise replay answers to questions it never asked.
84
+ */
85
+ export declare function resolveRecordIdFor(goal: string, cwd: string): string;
71
86
  /** ─── The ledger ───────────────────────────────────────────────────────────── */
72
87
  /**
73
88
  * The ledger a loop threads its steps through.
@@ -99,6 +114,12 @@ export declare function openResume(input: {
99
114
  goal: string;
100
115
  cwd: string;
101
116
  resume: ResumeRequest;
117
+ /**
118
+ * B4 — reuse recorded steps (default true). `false` = checkpoint-only: write a
119
+ * resume point without replaying anything, which is what makes default-on
120
+ * checkpointing safe. A caller that only wants to LEAVE a record passes false.
121
+ */
122
+ replay?: boolean;
102
123
  }): OpenResume;
103
124
  /** What a resumed run replayed, what it paid for, and whether the record survived. */
104
125
  export interface ResumeOutcome {
@@ -107,6 +128,13 @@ export interface ResumeOutcome {
107
128
  replayed: number;
108
129
  /** Model calls the RESUMED run still made (0 = every step came from the record). */
109
130
  modelCalls: number;
131
+ /**
132
+ * Phase 5 — the CONTINUATION SAVING, stated as a first-class number: each
133
+ * replayed step is one model call the resumed run did NOT pay for. Equal to
134
+ * `replayed` by construction, but named so a surface can report the saving
135
+ * without knowing that identity.
136
+ */
137
+ callsAvoided: number;
110
138
  /** False when the record could not be written (the run itself still happened). */
111
139
  saved: boolean;
112
140
  /** The operator-facing line: the counts, and why nothing replayed when it didn't. */
@@ -1 +1 @@
1
- {"version":3,"file":"step-checkpoint.d.ts","sourceRoot":"","sources":["../../src/learning/step-checkpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAQH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,kEAAkE;AAClE,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAOjD,mFAAmF;AAEnF;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GAAG,aAAa,GAAG,SAAS,CAgBpG;AAED,mFAAmF;AAEnF;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,EAAE,OAAO,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,CAE1F;AAmED,mFAAmF;AAEnF;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,kFAAkF;IAClF,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAAC;IAC7D,uEAAuE;IACvE,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACtE,gEAAgE;IAChE,UAAU,IAAI,MAAM,CAAC;CACtB;AA2BD,iFAAiF;AACjF,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,UAAU,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAE,GAAG,UAAU,CAgDlG;AAMD,sFAAsF;AACtF,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,UAAU,EAAE,MAAM,CAAC;IACnB,kFAAkF;IAClF,KAAK,EAAE,OAAO,CAAC;IACf,qFAAqF;IACrF,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CAuCnG"}
1
+ {"version":3,"file":"step-checkpoint.d.ts","sourceRoot":"","sources":["../../src/learning/step-checkpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAQH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,kEAAkE;AAClE,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AASjD,mFAAmF;AAEnF;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GAAG,aAAa,GAAG,SAAS,CAgBpG;AAED,mFAAmF;AAEnF;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,EAAE,OAAO,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,CAE1F;AAgFD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAoBpE;AAED,mFAAmF;AAEnF;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,kFAAkF;IAClF,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAAC;IAC7D,uEAAuE;IACvE,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACtE,gEAAgE;IAChE,UAAU,IAAI,MAAM,CAAC;CACtB;AAmCD,iFAAiF;AACjF,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,UAAU,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,aAAa,CAAC;IACtB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,GAAG,UAAU,CAqEb;AAMD,sFAAsF;AACtF,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,KAAK,EAAE,OAAO,CAAC;IACf,qFAAqF;IACrF,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CA4CnG"}
@@ -34,9 +34,9 @@
34
34
  * anything — including on runs that then replayed everything.
35
35
  */
36
36
  import { createHash } from 'node:crypto';
37
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
37
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
38
38
  import { join } from 'node:path';
39
- import { checkpointIdFor } from '../agents/checkpoint-store.js';
39
+ import { checkpointIdFor, goalsLookSame } from '../agents/checkpoint-store.js';
40
40
  import { envBuff, resolveNuviraHome } from '../config/paths.js';
41
41
  /** The environment key a deployment asks for a resume through. */
42
42
  export const RESUME_ENABLE_ENV = 'NUVIRA_RESUME';
@@ -44,6 +44,8 @@ export const RESUME_ENABLE_ENV = 'NUVIRA_RESUME';
44
44
  const CHANGED_REASON = 'its input changed';
45
45
  const EMPTY_REASON = 'the recorded step was an empty provider response';
46
46
  const MISSING_REASON = 'not in the record';
47
+ /** B4 — a checkpoint-only run does not replay, so every step reports this. */
48
+ const NOT_REQUESTED_REASON = 'replay was not requested (checkpoint only)';
47
49
  /**
48
50
  * Resolve a resume request from a caller's option, with the environment as the
49
51
  * fallback.
@@ -129,6 +131,64 @@ function writeRecord(file) {
129
131
  return false;
130
132
  }
131
133
  }
134
+ /** Read a record's metadata without trusting its steps (null when missing/corrupt). */
135
+ function readRecordMeta(id) {
136
+ try {
137
+ const path = recordPath(id);
138
+ if (!existsSync(path))
139
+ return null;
140
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
141
+ if (!parsed || typeof parsed.goal !== 'string' || typeof parsed.cwd !== 'string')
142
+ return null;
143
+ return { goal: parsed.goal, cwd: parsed.cwd, savedAt: typeof parsed.savedAt === 'number' ? parsed.savedAt : 0 };
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ /**
150
+ * B3 — the record id for this ask, in this directory, possibly WORDED DIFFERENTLY.
151
+ *
152
+ * `checkpointIdFor` hashes the literal goal, so "fix the hotkey" and "hotkey
153
+ * still not working" are two unrelated ids and the second ask cannot find the
154
+ * first's record — the reworded-ask miss the pipeline arm already solves with
155
+ * `findRelatedCheckpointFor` / `goalsLookSame`. The loop's replay ledger had no
156
+ * equivalent, so a reworded `--resume` replayed nothing.
157
+ *
158
+ * Order of preference: the EXACT id (a record we know is this ask), then the
159
+ * newest record in this directory whose goal `goalsLookSame` (the same ask,
160
+ * worded differently). A record for a DIFFERENT ask is never returned — the
161
+ * caller would otherwise replay answers to questions it never asked.
162
+ */
163
+ export function resolveRecordIdFor(goal, cwd) {
164
+ const exact = checkpointIdFor(goal, cwd);
165
+ if (existsSync(recordPath(exact)))
166
+ return exact;
167
+ const wanted = cwd.replace(/[\\/]+$/, '');
168
+ let best = null;
169
+ try {
170
+ for (const file of readdirSync(stepsDir())) {
171
+ if (!file.endsWith('.json'))
172
+ continue;
173
+ const id = file.slice(0, -'.json'.length);
174
+ if (id === exact)
175
+ continue;
176
+ const meta = readRecordMeta(id);
177
+ if (!meta)
178
+ continue;
179
+ if (meta.cwd.replace(/[\\/]+$/, '') !== wanted)
180
+ continue;
181
+ if (!goalsLookSame(meta.goal, goal))
182
+ continue;
183
+ if (!best || meta.savedAt > best.savedAt)
184
+ best = { id, savedAt: meta.savedAt };
185
+ }
186
+ }
187
+ catch {
188
+ return exact;
189
+ }
190
+ return best?.id ?? exact;
191
+ }
132
192
  /** The ledger instances this module created, so `closeResume` can read its state. */
133
193
  const STATES = new WeakMap();
134
194
  /** A response with no text and no tool call is a provider failure, not an answer. */
@@ -143,7 +203,13 @@ function isEmptyResponse(response) {
143
203
  * Open the record for this ask, reading it once (a record is not re-read per step).
144
204
  */
145
205
  export function openResume(input) {
146
- const id = input.resume.id?.trim() || checkpointIdFor(input.goal, input.cwd);
206
+ const replayEnabled = input.replay !== false;
207
+ // B3 — an explicit id is honoured exactly; otherwise resolve the record for
208
+ // this ask HERE, matching a reworded version of the same ask too. A
209
+ // checkpoint-only run does NOT go looking for a reworded record: it is
210
+ // writing forward, not resuming, so it uses this ask's own id.
211
+ const id = input.resume.id?.trim() ||
212
+ (replayEnabled ? resolveRecordIdFor(input.goal, input.cwd) : checkpointIdFor(input.goal, input.cwd));
147
213
  const prior = readRecord(id);
148
214
  const state = {
149
215
  id,
@@ -153,11 +219,20 @@ export function openResume(input) {
153
219
  replayed: 0,
154
220
  modelCalls: 0,
155
221
  misses: new Map(),
156
- staged: new Map(),
222
+ replayEnabled,
223
+ // Checkpoint-only: seed the staged map with what the record already holds so
224
+ // `closeResume` MERGES this run's steps into it instead of replacing a
225
+ // usable record with only the latest run's work.
226
+ staged: replayEnabled ? new Map() : new Map(prior.map((entry) => [entry.key, entry])),
157
227
  };
158
228
  const byKey = new Map(prior.map((entry) => [entry.key, entry]));
159
229
  const ledger = {
160
230
  replay(key, digest) {
231
+ if (!state.replayEnabled) {
232
+ // Checkpoint-only: nothing is reused this run (see `replayEnabled`).
233
+ countMiss(state, NOT_REQUESTED_REASON);
234
+ return null;
235
+ }
161
236
  const entry = byKey.get(key);
162
237
  if (!entry) {
163
238
  countMiss(state, MISSING_REASON);
@@ -182,6 +257,11 @@ export function openResume(input) {
182
257
  state.staged.set(key, { key, digest, response });
183
258
  },
184
259
  openNotice() {
260
+ if (!state.replayEnabled) {
261
+ return state.loaded > 0
262
+ ? `💾 checkpointing this run — the existing record (${state.loaded} step(s)) will be updated`
263
+ : '💾 checkpointing this run — a resume point will be written for this ask';
264
+ }
185
265
  return state.loaded > 0
186
266
  ? `↩️ resumed: ${state.loaded} recorded step(s) loaded — a step replays only when its whole input is unchanged`
187
267
  : '↩️ no record for this ask in this directory yet — this run will write one';
@@ -205,7 +285,7 @@ export function closeResume(resume, input) {
205
285
  const state = STATES.get(resume.ledger);
206
286
  if (!state) {
207
287
  const notice = '↩️ resume probe: replayed 0, made 0 model call(s)';
208
- return { id: resume.id, replayed: 0, modelCalls: 0, saved: false, notice };
288
+ return { id: resume.id, replayed: 0, modelCalls: 0, callsAvoided: 0, saved: false, notice };
209
289
  }
210
290
  const steps = [...state.staged.values()];
211
291
  let saved;
@@ -223,8 +303,11 @@ export function closeResume(resume, input) {
223
303
  steps,
224
304
  });
225
305
  }
306
+ const avoided = state.replayed > 0
307
+ ? ` — ${state.replayed} model call(s) REUSED from the record (not re-paid)`
308
+ : '';
226
309
  const lines = [
227
- `↩️ resume probe: replayed ${state.replayed}, made ${state.modelCalls} model call(s)`,
310
+ `↩️ resume probe: replayed ${state.replayed}, made ${state.modelCalls} model call(s)${avoided}`,
228
311
  ];
229
312
  if (state.replayed === 0 && state.misses.size > 0) {
230
313
  const reasons = [...state.misses.entries()].map(([reason, count]) => `${reason} (${count})`);
@@ -237,6 +320,7 @@ export function closeResume(resume, input) {
237
320
  id: state.id,
238
321
  replayed: state.replayed,
239
322
  modelCalls: state.modelCalls,
323
+ callsAvoided: state.replayed,
240
324
  saved,
241
325
  notice: lines.join('\n'),
242
326
  };