@iowarp/clio-coder 0.3.2 → 0.3.4

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 (259) hide show
  1. package/CHANGELOG.md +269 -458
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +3 -3
  4. package/dist/{acp-BIYHVZIM.js → acp-S5R4RR5B.js} +7 -6
  5. package/dist/{agents-YT6SSRIT.js → agents-P6DMMVZY.js} +24 -21
  6. package/dist/assets/codewiki.json +1 -1
  7. package/dist/{auth-5TWEIYDN.js → auth-2XCZLPKS.js} +12 -8
  8. package/dist/{chunk-GGXXDWE4.js → chunk-22NAGB7X.js} +2 -2
  9. package/dist/{chunk-WMSVI4G2.js → chunk-2LZI5CAG.js} +133 -13
  10. package/dist/{chunk-OAO4GE4M.js → chunk-2TZWSW76.js} +2 -2
  11. package/dist/{chunk-OOJYHWRB.js → chunk-34475P3I.js} +2 -2
  12. package/dist/{chunk-WVO7V2QY.js → chunk-35MKKU5R.js} +4 -4
  13. package/dist/{chunk-LBNRH5WM.js → chunk-3HZ5RWN2.js} +5 -5
  14. package/dist/{chunk-AGYYIBLL.js → chunk-3JLKSKD7.js} +2 -2
  15. package/dist/{chunk-MBS4V7ZP.js → chunk-4JUF2NNX.js} +7 -7
  16. package/dist/{chunk-ZDOOVTXZ.js → chunk-4OC57DA6.js} +27 -4
  17. package/dist/chunk-5M54SPOL.js +926 -0
  18. package/dist/{chunk-STBPMHSX.js → chunk-7RXG6QRZ.js} +51 -11
  19. package/dist/{chunk-77VKQEHF.js → chunk-A2GZF7DC.js} +5 -5
  20. package/dist/{chunk-A3CYT5EX.js → chunk-AD2SYQYC.js} +55 -2
  21. package/dist/chunk-AOCYTWAV.js +449 -0
  22. package/dist/chunk-BEY543CS.js +258 -0
  23. package/dist/{chunk-6N5PTWMY.js → chunk-BP4OYD6A.js} +32 -13
  24. package/dist/chunk-BPGS2WCQ.js +612 -0
  25. package/dist/{chunk-J5HN4RYU.js → chunk-BRXQQJFP.js} +8 -8
  26. package/dist/chunk-CFGTUFWB.js +67 -0
  27. package/dist/{chunk-CBCAPZAA.js → chunk-E25LMLRW.js} +2 -2
  28. package/dist/{chunk-G2DE3C7R.js → chunk-EDRHSCIE.js} +4 -4
  29. package/dist/{chunk-4KLWL3UC.js → chunk-EFADSJET.js} +2 -2
  30. package/dist/{chunk-M6SHUN7Q.js → chunk-FO5ZOVUY.js} +2 -2
  31. package/dist/chunk-FYYLNIL5.js +313 -0
  32. package/dist/{chunk-EPVUXGXG.js → chunk-HV5X7OR2.js} +14 -12
  33. package/dist/{chunk-TZTZS7QK.js → chunk-HXG4IURW.js} +5 -3
  34. package/dist/{chunk-IGLFWIYI.js → chunk-K6WL7QZT.js} +3 -3
  35. package/dist/chunk-K7VKOLQQ.js +15 -0
  36. package/dist/{chunk-BMEMKKIT.js → chunk-KOHPCX4K.js} +2 -2
  37. package/dist/{chunk-V4RXGQ5Q.js → chunk-KRPY7NTG.js} +10 -7
  38. package/dist/chunk-LL4KHSZI.js +22 -0
  39. package/dist/{chunk-KJ5LWLOE.js → chunk-MEQ45TQ4.js} +15 -9
  40. package/dist/{chunk-5UUP6MWO.js → chunk-MV3K5QF2.js} +5 -436
  41. package/dist/{chunk-AO4RKG4M.js → chunk-N4CZJQRK.js} +5 -5
  42. package/dist/{chunk-ARBGF5F7.js → chunk-NILBFAPG.js} +14 -8
  43. package/dist/chunk-OZNBF4L3.js +23 -0
  44. package/dist/{verify-G6V4D2G7.js → chunk-PCZJO5TI.js} +127 -42
  45. package/dist/chunk-QQK64KLB.js +1360 -0
  46. package/dist/{chunk-6EJV5X2W.js → chunk-QQL5RT5M.js} +979 -1619
  47. package/dist/{chunk-LZSJBIVT.js → chunk-QWU7ZBO7.js} +70 -720
  48. package/dist/{chunk-2EHAIA3X.js → chunk-RD5U66HV.js} +3 -3
  49. package/dist/{chunk-OKGUZO2U.js → chunk-SPULKLCF.js} +4 -3
  50. package/dist/{chunk-OQ33BKR3.js → chunk-TTNYS3EA.js} +3 -60
  51. package/dist/chunk-TW3WDMVS.js +677 -0
  52. package/dist/chunk-TZSKNMZG.js +434 -0
  53. package/dist/{chunk-7MNJORFF.js → chunk-UL3WSD3F.js} +6 -1
  54. package/dist/{chunk-7EYHLWU7.js → chunk-UZHIZC5S.js} +7 -7
  55. package/dist/{chunk-QTYWRVRA.js → chunk-VAWWTKDP.js} +8 -8
  56. package/dist/{chunk-X75S7HFS.js → chunk-VEZEGCGW.js} +214 -20
  57. package/dist/{chunk-OHHN2SO4.js → chunk-VMNQ6OZA.js} +98 -202
  58. package/dist/chunk-VSNATDE6.js +122 -0
  59. package/dist/chunk-W6GROXXM.js +69 -0
  60. package/dist/chunk-WPQLXFOZ.js +375 -0
  61. package/dist/{chunk-ORBHGJC5.js → chunk-WR67VIZY.js} +3 -3
  62. package/dist/{chunk-3ZXDFGR5.js → chunk-X6COSD2O.js} +5 -5
  63. package/dist/chunk-ZGVHUX3M.js +66 -0
  64. package/dist/{chunk-MAW544W2.js → chunk-ZWMF7253.js} +4 -4
  65. package/dist/{chunk-MQSRRFWA.js → chunk-ZYKPLLNQ.js} +563 -546
  66. package/dist/cli/index.js +27 -23
  67. package/dist/{clio-4LY5K2AC.js → clio-J5JIOIDS.js} +7 -6
  68. package/dist/{code-nav-7AX6FYE6.js → code-nav-AXCXSBHX.js} +5 -3
  69. package/dist/{config-GTLUW2PR.js → config-OEBMIN2U.js} +37 -27
  70. package/dist/{configure-R6A64DHX.js → configure-PUQOSIXQ.js} +16 -13
  71. package/dist/{context-5VKGUVJJ.js → context-EKDCKUUZ.js} +82 -7
  72. package/dist/{context-RW5HC47S.js → context-MGSE4Z2T.js} +33 -23
  73. package/dist/{context-JFZEJ7W5.js → context-URSXPBCK.js} +17 -9
  74. package/dist/{context-clear-6ZHBAZZT.js → context-clear-KDAJRNUK.js} +33 -23
  75. package/dist/context-working-set-SBKMPPI2.js +1552 -0
  76. package/dist/{dispatch-runner-VKBRCWQC.js → dispatch-runner-MSWN72NK.js} +43 -29
  77. package/dist/{doctor-KI767GSN.js → doctor-7BSE27PJ.js} +10 -10
  78. package/dist/{eval-XSSNATB4.js → eval-IZGDOO4H.js} +9 -8
  79. package/dist/{evidence-UA6AWDQQ.js → evidence-SR7WXB5B.js} +51 -23
  80. package/dist/{evolve-QNTFGV6Z.js → evolve-K7VE2CBX.js} +30 -20
  81. package/dist/{fleet-Q7UOMUSG.js → fleet-7XMJNQNF.js} +48 -38
  82. package/dist/{fleet-preflight-DDN536IT.js → fleet-preflight-AQNAH644.js} +3 -3
  83. package/dist/{init-WBB65ZHQ.js → init-JGNPAYXT.js} +41 -31
  84. package/dist/{memory-MD3O64RI.js → memory-4ALKDJ4Q.js} +32 -22
  85. package/dist/{models-BZU34YWD.js → models-ZMMLFJNN.js} +22 -19
  86. package/dist/{monitor-MEQA5C3I.js → monitor-2F3T5KHP.js} +55 -43
  87. package/dist/{orchestrator-CGFKEP27.js → orchestrator-ORHT43JB.js} +2507 -1896
  88. package/dist/{reset-L2FQEE3E.js → reset-NXGTYNUO.js} +4 -3
  89. package/dist/{run-IV4Q6RLN.js → run-RF4WJGMT.js} +51 -41
  90. package/dist/{share-S5BZQC5I.js → share-UT3W6E4M.js} +5 -4
  91. package/dist/{skills-LQEKRDTN.js → skills-PSACKC5Q.js} +2 -2
  92. package/dist/{skills-eval-3DC4HEWS.js → skills-eval-WJSI55RZ.js} +34 -24
  93. package/dist/{targets-C4SSGQOB.js → targets-PIIRAOYS.js} +23 -20
  94. package/dist/{terminal-lease-IT5JW2NR.js → terminal-lease-ULWXWNVY.js} +5 -4
  95. package/dist/{upgrade-7TT7SQ3G.js → upgrade-346TZ6AV.js} +18 -17
  96. package/dist/{usage-GV4PKT3M.js → usage-6KKXR32N.js} +34 -24
  97. package/dist/verifiers-4UUM6TEE.js +1214 -0
  98. package/dist/verify-X5HDROLA.js +25 -0
  99. package/dist/{wiki-generate-DQF6Z66B.js → wiki-generate-7STOCIFZ.js} +42 -31
  100. package/dist/worker/entry.js +33 -24
  101. package/docs/README.md +8 -7
  102. package/docs/acp.md +1 -1
  103. package/docs/alcf-provider.md +1 -1
  104. package/docs/architecture.md +2 -2
  105. package/docs/artifact-versions.md +1 -1
  106. package/docs/built-in-agents.md +1 -1
  107. package/docs/capacity-and-scheduling.md +1 -1
  108. package/docs/commands-and-modes.md +53 -21
  109. package/docs/config-knobs-audit.md +1 -2
  110. package/docs/configuration-and-targets.md +15 -1
  111. package/docs/context-engine.md +64 -12
  112. package/docs/context-working-set.md +194 -0
  113. package/docs/development-pipeline.md +1 -1
  114. package/docs/documentation-coverage.md +5 -5
  115. package/docs/documentation-guide.md +6 -5
  116. package/docs/environment-variables.md +2 -1
  117. package/docs/eval-runner.md +1 -1
  118. package/docs/evals-internal.md +14 -1
  119. package/docs/evidence-and-memory.md +74 -2
  120. package/docs/evolution.md +1 -1
  121. package/docs/exit-codes-and-output.md +1 -1
  122. package/docs/extensions-and-sharing.md +2 -2
  123. package/docs/fleet-dispatch.md +22 -7
  124. package/docs/glossary.md +21 -1
  125. package/docs/installation-and-lifecycle.md +6 -6
  126. package/docs/middleware-and-components.md +1 -1
  127. package/docs/model-catalog.md +7 -9
  128. package/docs/observability.md +4 -4
  129. package/docs/performance-methodology.md +2 -2
  130. package/docs/proactive-memory.md +1 -1
  131. package/docs/prompt-envelope-and-tools.md +4 -4
  132. package/docs/provider-adapter-cookbook.md +1 -1
  133. package/docs/release-cut-checklist.md +35 -35
  134. package/docs/safety-model.md +23 -4
  135. package/docs/scientific-validation.md +21 -3
  136. package/docs/session-lifecycle.md +3 -3
  137. package/docs/skills-marketplace.md +1 -1
  138. package/docs/tool-usage.md +79 -12
  139. package/docs/trace-store.md +1 -1
  140. package/docs/troubleshooting.md +1 -1
  141. package/docs/tui-design.md +2 -2
  142. package/docs/worker-dispatch-mechanics.md +11 -1
  143. package/package.json +8 -11
  144. package/skills/meta/clio-test/SKILL.md +20 -17
  145. package/skills/meta/clio-test/evals.md +3 -3
  146. package/skills/meta/clio-test/references/harness.md +35 -6
  147. package/skills/meta/clio-test/references/test-map.md +20 -10
  148. package/skills/registry.yaml +2 -2
  149. package/skills/skill-marketplace.json +1 -1
  150. package/src/cli/context-working-set.ts +513 -0
  151. package/src/cli/context.ts +8 -0
  152. package/src/cli/evidence.ts +20 -2
  153. package/src/cli/index.ts +4 -0
  154. package/src/cli/verifiers.ts +325 -0
  155. package/src/core/bash-exec.ts +39 -14
  156. package/src/core/bus-events.ts +19 -4
  157. package/src/core/config.ts +54 -0
  158. package/src/core/defaults.ts +50 -3
  159. package/src/core/git-commit-attribution.ts +46 -21
  160. package/src/core/verification-scripts.ts +6 -0
  161. package/src/domains/agents/builtins/verifier.md +3 -0
  162. package/src/domains/config/classify.ts +1 -0
  163. package/src/domains/config/keybindings.ts +3 -3
  164. package/src/domains/context/working-set/contract.ts +161 -0
  165. package/src/domains/context/working-set/defaults.ts +28 -0
  166. package/src/domains/context/working-set/engine.ts +203 -0
  167. package/src/domains/context/working-set/fold.ts +62 -0
  168. package/src/domains/context/working-set/horizon.ts +38 -0
  169. package/src/domains/context/working-set/marker.ts +103 -0
  170. package/src/domains/context/working-set/path-index.ts +436 -0
  171. package/src/domains/context/working-set/payload.ts +152 -0
  172. package/src/domains/context/working-set/policies/age-horizon.ts +55 -0
  173. package/src/domains/context/working-set/policies/index.ts +21 -0
  174. package/src/domains/context/working-set/policies/structural.ts +160 -0
  175. package/src/domains/context/working-set/project.ts +132 -0
  176. package/src/domains/context/working-set/protect.ts +109 -0
  177. package/src/domains/context/working-set/recall.ts +177 -0
  178. package/src/domains/context/working-set/replay/controls.ts +112 -0
  179. package/src/domains/context/working-set/replay/load-clio.ts +199 -0
  180. package/src/domains/context/working-set/replay/metrics.ts +185 -0
  181. package/src/domains/context/working-set/replay/reference-graph.ts +79 -0
  182. package/src/domains/context/working-set/replay/report.ts +139 -0
  183. package/src/domains/context/working-set/replay/runner.ts +325 -0
  184. package/src/domains/context/working-set/replay/synthetic.ts +422 -0
  185. package/src/domains/context/working-set/replay/trace.ts +21 -0
  186. package/src/domains/context/working-set/visible.ts +54 -0
  187. package/src/domains/evidence/build.ts +112 -45
  188. package/src/domains/evidence/eval.ts +24 -7
  189. package/src/domains/evidence/index.ts +53 -0
  190. package/src/domains/evidence/ordering.ts +12 -0
  191. package/src/domains/evidence/run-trust.ts +221 -0
  192. package/src/domains/evidence/store.ts +46 -6
  193. package/src/domains/evidence/trust-status.ts +854 -0
  194. package/src/domains/evidence/types.ts +26 -0
  195. package/src/domains/middleware/memory-intervention.ts +3 -0
  196. package/src/domains/middleware/stalled-turn.ts +165 -4
  197. package/src/domains/safety/autonomy.ts +1 -1
  198. package/src/domains/safety/default-path-policy.ts +8 -0
  199. package/src/domains/safety/finish-contract.ts +4 -3
  200. package/src/domains/safety/policy-engine.ts +48 -6
  201. package/src/domains/session/compaction/compact.ts +23 -1
  202. package/src/domains/session/compaction/cut-point.ts +2 -0
  203. package/src/domains/session/compaction/tokens.ts +16 -1
  204. package/src/domains/session/context-ledger.ts +2 -0
  205. package/src/domains/session/entries.ts +107 -1
  206. package/src/domains/session/manager.ts +9 -2
  207. package/src/domains/session/migrations/index.ts +22 -3
  208. package/src/engine/acp/server.ts +3 -0
  209. package/src/engine/agent.ts +18 -1
  210. package/src/engine/session.ts +9 -3
  211. package/src/entry/orchestrator.ts +16 -4
  212. package/src/interactive/chat-loop-messages.ts +18 -6
  213. package/src/interactive/chat-panel.ts +571 -244
  214. package/src/interactive/chat-renderer.ts +79 -39
  215. package/src/interactive/context-meter.ts +10 -0
  216. package/src/interactive/context-overlay.ts +81 -6
  217. package/src/interactive/context-recall-command.ts +110 -0
  218. package/src/interactive/editor-submit.ts +26 -1
  219. package/src/interactive/footer/widgets.ts +22 -20
  220. package/src/interactive/footer-panel.ts +6 -1
  221. package/src/interactive/interactive-application.ts +2 -0
  222. package/src/interactive/interactive-event-projection.ts +12 -0
  223. package/src/interactive/interactive-slash-runtime.ts +49 -8
  224. package/src/interactive/model-session-replay.ts +21 -0
  225. package/src/interactive/overlay-general-openers.ts +6 -0
  226. package/src/interactive/overlay-session-lifecycle.ts +8 -4
  227. package/src/interactive/overlays/ask-user.ts +146 -24
  228. package/src/interactive/renderers/tool-execution.ts +167 -56
  229. package/src/interactive/session-transcript.ts +2 -2
  230. package/src/interactive/slash-commands.ts +29 -2
  231. package/src/interactive/status/index.ts +12 -1
  232. package/src/interactive/status/reasoning.ts +87 -0
  233. package/src/interactive/status/summary.ts +13 -2
  234. package/src/interactive/transcript-detail.ts +120 -0
  235. package/src/interactive/turn-context.ts +238 -88
  236. package/src/interactive/turn-middleware.ts +6 -6
  237. package/src/tools/agent-tools.ts +11 -4
  238. package/src/tools/bash.ts +144 -82
  239. package/src/tools/builtin-tool-catalog.ts +18 -6
  240. package/src/tools/context/index.ts +105 -3
  241. package/src/tools/context/surface.ts +3 -2
  242. package/src/tools/core-bootstrap.ts +21 -0
  243. package/src/tools/dispatch-runner.ts +9 -7
  244. package/src/tools/monitor.ts +28 -20
  245. package/src/tools/presentation.ts +107 -0
  246. package/src/tools/registry.ts +65 -7
  247. package/src/tools/result-disposition.ts +550 -0
  248. package/src/tools/result-shaping.ts +262 -19
  249. package/src/tools/safe-exec.ts +2 -0
  250. package/src/tools/verify/authoring.ts +1119 -0
  251. package/src/tools/verify/catalog.ts +346 -0
  252. package/src/tools/verify/index.ts +13 -3
  253. package/src/tools/verify/scripts.ts +135 -37
  254. package/src/tools/verify/surface.ts +9 -5
  255. package/src/tools/worker-evidence.ts +35 -12
  256. package/dist/chunk-MNA4JGU4.js +0 -255
  257. package/dist/chunk-SRF2PJNW.js +0 -184
  258. package/dist/chunk-T6YILFSB.js +0 -80
  259. package/dist/chunk-VAKQQHWR.js +0 -434
@@ -3,6 +3,7 @@ import type { RunPersonaOverride, RunPipelineProvenance, RunReceiptAutonomyEnfor
3
3
  import type { EvalCommandPhase, EvalFailureClass, EvalRunRecord } from "../eval/index.js";
4
4
  import type { ProtectedArtifact } from "../safety/protected-artifacts.js";
5
5
  import type { RunEscalationCounts } from "./provenance.js";
6
+ import type { CanonicalTrustStatus } from "./trust-status.js";
6
7
 
7
8
  export const EVIDENCE_VERSION = 1;
8
9
 
@@ -131,13 +132,38 @@ export interface EvidenceBuildResult {
131
132
  directory: string;
132
133
  overview: EvidenceOverview;
133
134
  findings: EvidenceFinding[];
135
+ trustStatus: EvidenceTrustStatusFile;
134
136
  }
135
137
 
136
138
  export interface EvidenceInspectable {
137
139
  overview: EvidenceOverview;
138
140
  findings: EvidenceFinding[];
141
+ trustStatus: EvidenceTrustStatusView;
139
142
  }
140
143
 
144
+ export interface EvidenceRunTrustStatus {
145
+ runId: string;
146
+ status: CanonicalTrustStatus;
147
+ }
148
+
149
+ /** Canonical per-run projection persisted by current evidence builders. */
150
+ export interface EvidenceTrustStatusFile {
151
+ version: 1;
152
+ evidenceId: string;
153
+ projection: "canonical";
154
+ runs: EvidenceRunTrustStatus[];
155
+ }
156
+
157
+ /** Explicit compatibility result for a bundle built before the projection existed. */
158
+ export interface HistoricalEvidenceTrustStatus {
159
+ version: 1;
160
+ evidenceId: string;
161
+ projection: "historical_format";
162
+ runs: [];
163
+ }
164
+
165
+ export type EvidenceTrustStatusView = EvidenceTrustStatusFile | HistoricalEvidenceTrustStatus;
166
+
141
167
  export interface EvidenceRunSource {
142
168
  envelope: RunEnvelope;
143
169
  receipt: RunReceipt | null;
@@ -202,6 +202,9 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
202
202
  return effects;
203
203
  }
204
204
  case "on_compaction":
205
+ // Recall grows the working set; it is an observability point, not
206
+ // context loss that should reactivate compacted task memory.
207
+ if (input.metadata?.stage === "working_set_recall") return NO_EFFECTS;
205
208
  reactivateAfterCompaction = true;
206
209
  return NO_EFFECTS;
207
210
  case "turn_start": {
@@ -7,7 +7,116 @@ export const STALLED_TURN_REQUEST_CONTINUATION_MESSAGE =
7
7
  "You ended your turn after announcing an action without executing it: no tools were called. Continue now and perform the announced action, or state plainly that you are finished and waiting for the user.";
8
8
 
9
9
  const INTENT_PATTERN = /\b(let me|i['’]ll|i will|i am going to|i['’]m going to|now i|next i|let's|let us)\b/i;
10
+
11
+ // Bare stems only. Inflected forms are derived below so that "running",
12
+ // "checking", and "verifies" count as the same announced action as "run".
13
+ const CONCRETE_ACTION_STEMS = [
14
+ "add",
15
+ "analyse",
16
+ "analyze",
17
+ "apply",
18
+ "audit",
19
+ "build",
20
+ "change",
21
+ "check",
22
+ "compare",
23
+ "compile",
24
+ "create",
25
+ "debug",
26
+ "delete",
27
+ "edit",
28
+ "execute",
29
+ "explore",
30
+ "find",
31
+ "fix",
32
+ "format",
33
+ "grep",
34
+ "implement",
35
+ "inspect",
36
+ "install",
37
+ "investigate",
38
+ "list",
39
+ "load",
40
+ "look",
41
+ "modify",
42
+ "open",
43
+ "patch",
44
+ "query",
45
+ "read",
46
+ "refactor",
47
+ "remove",
48
+ "rename",
49
+ "replace",
50
+ "review",
51
+ "run",
52
+ "scan",
53
+ "search",
54
+ "test",
55
+ "trace",
56
+ "update",
57
+ "validate",
58
+ "verify",
59
+ "write",
60
+ ] as const;
61
+
62
+ // Stems whose final consonant doubles before a vowel suffix.
63
+ const DOUBLED_CONSONANT_STEMS = new Set(["debug", "format", "run", "scan"]);
64
+
65
+ function concreteActionForms(stem: string): string[] {
66
+ const forms = new Set([stem, `${stem}s`, `${stem}ed`, `${stem}ing`]);
67
+ if (/(?:ch|sh|s|x|z)$/.test(stem)) forms.add(`${stem}es`);
68
+ if (stem.endsWith("e")) {
69
+ forms.add(`${stem}d`);
70
+ forms.add(`${stem.slice(0, -1)}ing`);
71
+ }
72
+ if (stem.endsWith("y")) {
73
+ forms.add(`${stem.slice(0, -1)}ies`);
74
+ forms.add(`${stem.slice(0, -1)}ied`);
75
+ }
76
+ if (DOUBLED_CONSONANT_STEMS.has(stem)) {
77
+ const doubled = `${stem}${stem.slice(-1)}`;
78
+ forms.add(`${doubled}ed`);
79
+ forms.add(`${doubled}ing`);
80
+ }
81
+ return [...forms];
82
+ }
83
+
84
+ const CONCRETE_ACTION_PATTERN = new RegExp(
85
+ `\\b(?:${CONCRETE_ACTION_STEMS.flatMap(concreteActionForms).join("|")})\\b`,
86
+ "giu",
87
+ );
88
+
89
+ // Extensions a model actually names when announcing work on a file. Requiring
90
+ // one (or an explicit ./, ../, ~/ prefix) keeps "and/or" and "I/O" from
91
+ // reading as paths.
92
+ const NAMED_PATH_EXTENSIONS =
93
+ "c|cc|cfg|cjs|cmake|conf|cpp|cs|css|csv|env|go|h|hpp|html|ini|java|jl|js|json|jsx|lock|log|lua|md|mjs|nc|php|py|rb|rs|rst|sh|sql|toml|ts|tsx|txt|yaml|yml";
94
+ const NAMED_PATH_PATTERN = new RegExp(
95
+ `(?:^|[\\s\`"'(\\[])(?:[\\w@.\\-/]*[\\w@\\-]\\.(?:${NAMED_PATH_EXTENSIONS})\\b|(?:\\.{1,2}|~)\\/[\\w@.\\-/]+)`,
96
+ "iu",
97
+ );
98
+
99
+ // A named executable followed by at least one argument token. Words that are
100
+ // ordinary English on their own ("go", "make", "test") are deliberately absent.
101
+ const NAMED_COMMAND_PATTERN =
102
+ /\b(?:npm|npx|pnpm|yarn|bun|git|cargo|rustc|cmake|ctest|ninja|meson|bazel|gradle|mvn|pytest|pip|python3?|node|deno|tsc|biome|eslint|prettier|jest|vitest|ruff|mypy|docker|kubectl|mpirun|srun|sbatch|gcc|clang)\s+[-\w./]/iu;
103
+
104
+ const ACTION_OBJECT_CONNECTOR_PATTERN = /^(?:at|for|in|into|on|through|to)\s+/i;
105
+ const ACTION_OBJECT_DETERMINER_PATTERN = /^(?:a|an|my|our|that|the|their|these|this|those|your)\s+/i;
106
+ const ACTION_OBJECT_PATTERN = /^(?!(?:again|everyone|later|next|now|soon|then|you)\b)[`"'./~]*[\p{L}\p{N}_-]/iu;
107
+ const CONDITIONAL_LEAD_IN_PATTERN = /\b(?:if|once|unless|until|when|whenever)\b/i;
108
+ const CONDITIONAL_INVITATION_PATTERN = /\b(?:ask|give|point|say|send|share|tell)\b[\s\S]*\b(?:and|then)\s*$/i;
109
+ // "if needed", and any conditional whose condition is about the user: "if you
110
+ // want", "if that is what you need". Conditions about the work ("if the config
111
+ // is valid") stay announcements.
112
+ const CONDITIONAL_OFFER_SUFFIX_PATTERN =
113
+ /\b(?:if\s+(?:needed|necessary|requested)\b|if\b[^.!?]*\byou\b|when(?:ever)?\s+you\b)/i;
10
114
  const LET_ME_KNOW_PATTERN = /\blet me know\b/i;
115
+ // "I'll wait for your go-ahead before I touch src/cli/index.ts" names a path
116
+ // and promises nothing now. A clause that opens by deferring is a wait
117
+ // statement whatever it goes on to name, so it is read before any verb or
118
+ // path inside it can count as an announcement.
119
+ const DEFERRAL_LEAD_PATTERN = /^\s*(?:just\s+|simply\s+|now\s+)?(?:wait|hold\s+off|hold|stand\s+by|pause|defer)\b/i;
11
120
  const COMPLETION_PATTERN =
12
121
  /^\s*(?:all\s+done|done|complete|completed|all\s+set)\b|^\s*(?:here(?:'s| is)\s+)?(?:a\s+)?summary\b|^\s*summary\s*:?$/i;
13
122
 
@@ -38,7 +147,47 @@ export function shouldRequestStalledTurnContinuation(input: MiddlewareHookInput)
38
147
  if (LET_ME_KNOW_PATTERN.test(finalSentence)) return false;
39
148
  if (COMPLETION_PATTERN.test(lastLine) || COMPLETION_PATTERN.test(finalSentence)) return false;
40
149
 
41
- return finalSentence.endsWith(":") || INTENT_PATTERN.test(finalSentence);
150
+ return announcesConcreteAction(finalSentence);
151
+ }
152
+
153
+ function announcesConcreteAction(sentence: string): boolean {
154
+ const intent = INTENT_PATTERN.exec(sentence);
155
+ if (intent) {
156
+ const leadIn = sentence.slice(0, intent.index);
157
+ if (
158
+ CONDITIONAL_LEAD_IN_PATTERN.test(leadIn) ||
159
+ CONDITIONAL_INVITATION_PATTERN.test(leadIn) ||
160
+ CONDITIONAL_OFFER_SUFFIX_PATTERN.test(sentence)
161
+ ) {
162
+ return false;
163
+ }
164
+ const clause = sentence.slice(intent.index + intent[0].length);
165
+ if (DEFERRAL_LEAD_PATTERN.test(clause)) return false;
166
+ if (hasActionWithObject(clause)) return true;
167
+ // A path or command is evidence of an immediate action only when the
168
+ // announcement itself is unconditional; "once package.json is updated"
169
+ // names a file but promises nothing now.
170
+ if (CONDITIONAL_LEAD_IN_PATTERN.test(clause)) return false;
171
+ return namesToolTarget(clause);
172
+ }
173
+
174
+ // A trailing colon can still signal a cut-off action lead-in, but only when
175
+ // the lead-in names work and its target instead of relying on punctuation.
176
+ return sentence.endsWith(":") && hasActionWithObject(sentence);
177
+ }
178
+
179
+ function namesToolTarget(clause: string): boolean {
180
+ return NAMED_PATH_PATTERN.test(clause) || NAMED_COMMAND_PATTERN.test(clause);
181
+ }
182
+
183
+ function hasActionWithObject(clause: string): boolean {
184
+ for (const action of clause.matchAll(CONCRETE_ACTION_PATTERN)) {
185
+ const remainder = clause.slice((action.index ?? 0) + action[0].length).trimStart();
186
+ const withoutConnector = remainder.replace(ACTION_OBJECT_CONNECTOR_PATTERN, "");
187
+ const object = withoutConnector.replace(ACTION_OBJECT_DETERMINER_PATTERN, "");
188
+ if (ACTION_OBJECT_PATTERN.test(object)) return true;
189
+ }
190
+ return false;
42
191
  }
43
192
 
44
193
  function isNormalStopReason(value: unknown): boolean {
@@ -60,10 +209,22 @@ function lastNonEmptyLine(text: string): string | null {
60
209
  return null;
61
210
  }
62
211
 
212
+ // A period only terminates a sentence when it ends a token: "bash.ts" and
213
+ // "package.json" must survive extraction intact.
214
+ const SENTENCE_TERMINATOR_PATTERN = /[.!?]+["'’)\]]*(?=\s|$)/gu;
215
+
63
216
  function lastSentence(line: string): string {
64
- const matches = line.match(/[^.!?]+[.!?]+(?:["')\]]+)?|[^.!?]+$/g);
65
- const sentence = matches?.at(-1)?.trim() ?? line.trim();
66
- return sentence.replace(/^[\s"'([{-]+/, "").trim();
217
+ const trimmed = line.trimEnd();
218
+ let start = 0;
219
+ for (const terminator of trimmed.matchAll(SENTENCE_TERMINATOR_PATTERN)) {
220
+ const end = (terminator.index ?? 0) + terminator[0].length;
221
+ if (end >= trimmed.length) break;
222
+ start = end;
223
+ }
224
+ return trimmed
225
+ .slice(start)
226
+ .replace(/^[\s"'([{-]+/, "")
227
+ .trim();
67
228
  }
68
229
 
69
230
  function isQuestion(sentence: string): boolean {
@@ -165,7 +165,7 @@ export function autonomyAskRejection(
165
165
  // directory listings.
166
166
  ...(actionClass === "execute"
167
167
  ? [
168
- 'A declared package.json check runs without approval through the verify tool: verify(check="<script>").',
168
+ 'A declared package or project-catalog check runs without approval through the verify tool: verify(check="<id>").',
169
169
  "Read-only inspection runs without approval through the ls, read, grep, and find tools.",
170
170
  ]
171
171
  : []),
@@ -61,6 +61,14 @@ export const DEFAULT_DAMAGE_CONTROL_PATH_POLICY: PathPolicyInput = {
61
61
  "*.dump",
62
62
  ],
63
63
  readOnlyPaths: [
64
+ // Clio's own operator-authority files in the project. `safety.yaml` is the
65
+ // policy that gates the model's tools, and `verifiers.yaml` is an argv
66
+ // catalog that verify() executes; a model that can author either one can
67
+ // widen its own permissions in two tool calls. Both are authored by the
68
+ // operator through the CLI (`clio-coder verifiers author` lands nothing
69
+ // before `--yes`), so model-side writes and deletes are refused.
70
+ ".clio-coder/safety.yaml",
71
+ ".clio-coder/verifiers.yaml",
64
72
  "/etc/",
65
73
  "/usr/",
66
74
  "/bin/",
@@ -1,5 +1,5 @@
1
1
  import { ToolNames } from "../../core/tool-names.js";
2
- import { isVerificationScriptName } from "../../core/verification-scripts.js";
2
+ import { isProjectVerifierCheckId, isVerificationScriptName } from "../../core/verification-scripts.js";
3
3
  import {
4
4
  detectValidationCommand,
5
5
  extractCommandDeleteTargets,
@@ -399,8 +399,9 @@ export function typedValidationSummary(toolName: string, payload: Record<string,
399
399
  const path = typeof args?.path === "string" && args.path.trim().length > 0 ? args.path.trim() : "artifact";
400
400
  return `verify frontend ${path}`;
401
401
  }
402
- if (!isVerificationScriptName(check)) return null;
403
- return `npm run ${check}`;
402
+ if (isVerificationScriptName(check)) return `npm run ${check}`;
403
+ if (isProjectVerifierCheckId(check)) return `verify ${check}`;
404
+ return null;
404
405
  }
405
406
  return null;
406
407
  }
@@ -4,6 +4,7 @@ import { artifactDefaultPath } from "../../core/artifact-paths.js";
4
4
  import { canonicalizeExistingPath } from "../../core/path-canonical.js";
5
5
  import { ToolNames } from "../../core/tool-names.js";
6
6
  import { clioConfigDir } from "../../core/xdg.js";
7
+ import { type DeclaredCheck, loadProjectVerifierCatalog } from "../../tools/verify/catalog.js";
7
8
  import { type ActionClass, type Classification, type ClassifierCall, classify } from "./action-classifier.js";
8
9
  import type { DamageControlMatch, DamageControlRule } from "./damage-control.js";
9
10
  import { DEFAULT_DAMAGE_CONTROL_PATH_POLICY, mergePathPolicyInputs } from "./default-path-policy.js";
@@ -225,7 +226,9 @@ export function createSafetyPolicyEngine(options: SafetyPolicyEngineOptions = {}
225
226
  const rawClassification = classify(call);
226
227
  const command = commandArg(call.args);
227
228
  const callCwd = cwdArg(call.args, cwd);
228
- const scan = damageControlScan(call);
229
+ const catalogCheck = resolveVerifyCatalogCheck(call, cwd);
230
+ const catalogCommand = catalogCheck === null ? null : catalogCheck.command.join(" ");
231
+ const scan = catalogCommand ?? damageControlScan(call);
229
232
  const hit = scan ? matchSourcedRule(scan, sourcedRules) : null;
230
233
  const classification = effectiveClassification(rawClassification, hit?.match);
231
234
 
@@ -335,8 +338,9 @@ export function createSafetyPolicyEngine(options: SafetyPolicyEngineOptions = {}
335
338
  // command; the one carve-out is the exit-code-only presence check
336
339
  // (`grep -q`/`grep -sq` with a ^NAME= pattern), which is the safe
337
340
  // protocol the credentials skill teaches.
338
- if (call.tool === ToolNames.Bash && command !== null) {
339
- const secretRead = evaluateBashZeroAccessRead(zeroAccessPolicy, command, callCwd);
341
+ const scannedCommand = call.tool === ToolNames.Bash ? command : catalogCommand;
342
+ if (scannedCommand !== null) {
343
+ const secretRead = evaluateBashZeroAccessRead(zeroAccessPolicy, scannedCommand, callCwd);
340
344
  if (secretRead !== null) {
341
345
  const blockInput: Omit<
342
346
  SafetyPolicyDecision,
@@ -407,9 +411,22 @@ export function createSafetyPolicyEngine(options: SafetyPolicyEngineOptions = {}
407
411
  };
408
412
  if (hit?.match.ruleId !== undefined) allowInput.ruleId = hit.match.ruleId;
409
413
  if (hit?.match !== undefined) allowInput.match = hit.match;
410
- // The typed execution tool (verify) is bounded by its own check
411
- // allowlist, so it sits in the no-prompt set.
412
- if (classification.actionClass === "execute") allowInput.execRecognition = "recognized";
414
+ // The typed execution tool (verify) sits in the no-prompt set when the
415
+ // check is a package script or the frontend validator: both are bounded
416
+ // by the verification-script family and run a fixed argv shape. A
417
+ // project-catalog check runs whatever argv the workspace file declares,
418
+ // so it is unrecognized: auto-edit parks it for one confirmation that
419
+ // shows the argv, full-auto runs it, and the damage-control match and
420
+ // zero-access scan above have already seen the declared vector.
421
+ if (classification.actionClass === "execute") {
422
+ allowInput.execRecognition = catalogCheck === null ? "recognized" : "unrecognized";
423
+ if (catalogCheck !== null) {
424
+ allowInput.reasons = [
425
+ ...allowInput.reasons,
426
+ `project verifier '${catalogCheck.id}' runs declared argv: ${catalogCommand ?? ""}`,
427
+ ];
428
+ }
429
+ }
413
430
  return allowDecision(base, allowInput);
414
431
  },
415
432
  metadata() {
@@ -958,6 +975,31 @@ function damageControlScan(call: ClassifierCall): string {
958
975
  return typeof pathArg === "string" ? pathArg : "";
959
976
  }
960
977
 
978
+ /**
979
+ * The project-catalog check a verify call resolves to, or null for a listing,
980
+ * the frontend validator, a package script, or an id the catalog does not
981
+ * declare. The catalog is an argv vector the operator authored, but the file
982
+ * sits in the workspace, so the engine reads it fresh on every verify call and
983
+ * treats the declared argv exactly like a bash command string: it is what the
984
+ * damage-control rules and the zero-access read guard scan, and it is not in
985
+ * the no-prompt set. An unreadable or invalid catalog resolves to null here and
986
+ * fails closed in the tool itself.
987
+ */
988
+ function resolveVerifyCatalogCheck(call: ClassifierCall, workspaceRoot: string): DeclaredCheck | null {
989
+ if (call.tool !== ToolNames.Verify) return null;
990
+ const check = call.args?.check;
991
+ if (typeof check !== "string") return null;
992
+ const id = check.trim();
993
+ if (id.length === 0 || id === "frontend") return null;
994
+ try {
995
+ const catalog = loadProjectVerifierCatalog(workspaceRoot);
996
+ if (!catalog.ok || catalog.source === null) return null;
997
+ return catalog.source.checks.find((candidate) => candidate.id === id) ?? null;
998
+ } catch {
999
+ return null;
1000
+ }
1001
+ }
1002
+
961
1003
  function serializeArgs(args?: Record<string, unknown>): string {
962
1004
  if (!args) return "";
963
1005
  const parts: string[] = [];
@@ -12,6 +12,8 @@
12
12
 
13
13
  import { stream } from "../../../engine/ai.js";
14
14
  import type { EngineModel, Usage } from "../../../engine/types.js";
15
+ import { foldWorkingSet } from "../../context/working-set/fold.js";
16
+ import { recallableRefListing } from "../../context/working-set/recall.js";
15
17
  import type { CompactionUsage, SessionEntry } from "../entries.js";
16
18
  import { serializeConversation } from "./branch-summary.js";
17
19
  import { findCutPoint } from "./cut-point.js";
@@ -307,6 +309,23 @@ function formatFileOperations(fileOps: FileOperations): string {
307
309
  return sections.length > 0 ? `\n\n${sections.join("\n\n")}` : "";
308
310
  }
309
311
 
312
+ function formatRecallableRefs(entries: ReadonlyArray<SessionEntry>, firstKeptEntryIndex: number): string {
313
+ const entryIndexes = new Map<string, number>();
314
+ for (let index = 0; index < entries.length; index += 1) {
315
+ const entry = entries[index];
316
+ if (entry) entryIndexes.set(entry.turnId, index);
317
+ }
318
+ const view = foldWorkingSet(entries);
319
+ const evictedBeforeCut = new Map(
320
+ [...view.evicted].filter(([ref]) => (entryIndexes.get(ref) ?? Number.POSITIVE_INFINITY) < firstKeptEntryIndex),
321
+ );
322
+ const listing = recallableRefListing(entries, { ...view, evicted: evictedBeforeCut });
323
+ if (listing.refs.length === 0) return "";
324
+ const rows = [...listing.refs];
325
+ if (listing.remaining > 0) rows.push(`and ${listing.remaining} more`);
326
+ return `\n\n<recallable-refs>\n${rows.join("\n")}\n</recallable-refs>`;
327
+ }
328
+
310
329
  function numberOrZero(value: unknown): number {
311
330
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
312
331
  }
@@ -457,7 +476,10 @@ export async function compact(input: CompactInput): Promise<CompactResult> {
457
476
  }
458
477
  }
459
478
 
460
- const summary = `${summaryParts.join("\n\n---\n\n").trim()}${formatFileOperations(fileOps)}`.trim();
479
+ const summary = `${summaryParts.join("\n\n---\n\n").trim()}${formatFileOperations(fileOps)}${formatRecallableRefs(
480
+ input.entries,
481
+ cut.firstKeptEntryIndex,
482
+ )}`.trim();
461
483
 
462
484
  return {
463
485
  summary,
@@ -72,6 +72,8 @@ function isValidCutPoint(entry: SessionEntry): boolean {
72
72
  case "taskLedger":
73
73
  case "decisionLedger":
74
74
  case "workerRun":
75
+ case "contextEviction":
76
+ case "contextRecall":
75
77
  return false;
76
78
  }
77
79
  }
@@ -34,7 +34,18 @@ export interface TokenEstimator {
34
34
  }
35
35
 
36
36
  function estimateMessage(entry: MessageEntry): number {
37
- return estimateAgentMessageTokens(entry);
37
+ // Usage invalidation is bookkeeping for token-anchor selection. The replay
38
+ // builder carries it as AgentMessage metadata, but providers never see it in
39
+ // message content, so it cannot change a projected entry's prompt cost. Drop
40
+ // it here so plan-time pricing and the post-event projection use one byte
41
+ // definition even though only the latter has applied the stamp.
42
+ if (entry.role !== "assistant" || !entry.payload || typeof entry.payload !== "object") {
43
+ return estimateAgentMessageTokens(entry);
44
+ }
45
+ const payload = entry.payload as Record<string, unknown>;
46
+ if (!("contextUsageInvalidated" in payload)) return estimateAgentMessageTokens(entry);
47
+ const { contextUsageInvalidated: _stamp, ...promptPayload } = payload;
48
+ return estimateAgentMessageTokens({ ...entry, payload: promptPayload });
38
49
  }
39
50
 
40
51
  function estimateBashExecution(entry: BashExecutionEntry): number {
@@ -78,6 +89,10 @@ export function estimateTokens(entry: SessionEntry): number {
78
89
  case "taskLedger":
79
90
  case "decisionLedger":
80
91
  case "workerRun":
92
+ // Working-set bookkeeping: refs and markers, never bodies. The
93
+ // projection accounts for marker cost on the projected messages.
94
+ case "contextEviction":
95
+ case "contextRecall":
81
96
  return 0;
82
97
  }
83
98
  }
@@ -122,6 +122,8 @@ export interface PromptCacheStats {
122
122
  * combination the overlay renders as a warning.
123
123
  */
124
124
  backendVerdict: "hot" | "partial" | "cold" | "small" | null;
125
+ /** Cache disturbances Clio expected before the last settled run. */
126
+ expectedColdReasons?: ReadonlyArray<string>;
125
127
  }
126
128
 
127
129
  export interface ContextLedgerGroup {
@@ -276,6 +276,75 @@ export interface WorkerRunEntry extends BaseSessionEntry {
276
276
  parentToolCallId?: string;
277
277
  }
278
278
 
279
+ /**
280
+ * Working-set layer (context domain) ledger records. Eviction is a projection:
281
+ * these entries say what left the model's working set and what came back; the
282
+ * original bodies stay in the ledger untouched. The context domain owns the
283
+ * semantics (`src/domains/context/working-set/contract.ts`); the session
284
+ * domain owns the wire shape because it owns the ledger format.
285
+ */
286
+ export const EVICTION_REASONS = [
287
+ "superseded_read",
288
+ "stale_after_mutation",
289
+ "listing_consumed",
290
+ "failure_resolved",
291
+ "thinking_turn_closed",
292
+ "age_horizon",
293
+ "operator",
294
+ ] as const;
295
+ export type EvictionReason = (typeof EVICTION_REASONS)[number];
296
+
297
+ export const EVICTION_TRIGGERS = ["pressure", "operator"] as const;
298
+ export type EvictionTrigger = (typeof EVICTION_TRIGGERS)[number];
299
+
300
+ export const RECALL_TRIGGERS = ["tool", "operator"] as const;
301
+ export type RecallTrigger = (typeof RECALL_TRIGGERS)[number];
302
+
303
+ /**
304
+ * Identity of an evictable unit: the `turnId` of a ledger entry. For a
305
+ * `tool_result` message the unit is the result body; for an `assistant`
306
+ * message the unit is every thinking block it carries. Partial (per-block)
307
+ * eviction is deliberately not modelled; add a `block` field here when it is.
308
+ */
309
+ export interface WorkingSetRef {
310
+ entry: string;
311
+ }
312
+
313
+ export interface EvictedItem {
314
+ ref: WorkingSetRef;
315
+ reason: EvictionReason;
316
+ /** Estimated tokens the projection removes for this item (marker cost already subtracted). */
317
+ tokensFreed: number;
318
+ /**
319
+ * Byte-stable one-line stub the projection renders in place of the body.
320
+ * Empty for thinking-block eviction, which removes without a marker.
321
+ */
322
+ marker: string;
323
+ /** Ref key of the entry that superseded or resolved this one, when the reason names one. */
324
+ by?: string;
325
+ }
326
+
327
+ export interface ContextEvictionEntry extends BaseSessionEntry {
328
+ kind: "contextEviction";
329
+ policyId: string;
330
+ trigger: EvictionTrigger;
331
+ evicted: ReadonlyArray<EvictedItem>;
332
+ tokensBefore: number;
333
+ tokensAfter: number;
334
+ /** Used/window ratio that fired the event; null for operator-triggered events. */
335
+ pressureBefore: number | null;
336
+ snapshotIdBefore: string | null;
337
+ }
338
+
339
+ export interface ContextRecallEntry extends BaseSessionEntry {
340
+ kind: "contextRecall";
341
+ ref: WorkingSetRef;
342
+ trigger: RecallTrigger;
343
+ tokensReadmitted: number;
344
+ /** The tool call that performed the recall, when `trigger` is `tool`. */
345
+ toolCallId?: string;
346
+ }
347
+
279
348
  export type SessionEntry =
280
349
  | MessageEntry
281
350
  | BashExecutionEntry
@@ -291,7 +360,9 @@ export type SessionEntry =
291
360
  | SkillActivationEntry
292
361
  | TaskLedgerEntry
293
362
  | DecisionLedgerEntry
294
- | WorkerRunEntry;
363
+ | WorkerRunEntry
364
+ | ContextEvictionEntry
365
+ | ContextRecallEntry;
295
366
 
296
367
  export type SessionFileEntry = SessionHeader | SessionEntry;
297
368
 
@@ -317,6 +388,8 @@ export const SESSION_ENTRY_KINDS = [
317
388
  "taskLedger",
318
389
  "decisionLedger",
319
390
  "workerRun",
391
+ "contextEviction",
392
+ "contextRecall",
320
393
  ] as const;
321
394
 
322
395
  export type SessionEntryKind = (typeof SESSION_ENTRY_KINDS)[number];
@@ -477,6 +550,21 @@ export function isSessionHeader(value: unknown): value is SessionHeader {
477
550
  );
478
551
  }
479
552
 
553
+ function isWorkingSetRef(value: unknown): value is WorkingSetRef {
554
+ return isRecord(value) && isString(value.entry);
555
+ }
556
+
557
+ function isEvictedItem(value: unknown): value is EvictedItem {
558
+ return (
559
+ isRecord(value) &&
560
+ isWorkingSetRef(value.ref) &&
561
+ isOneOf(value.reason, EVICTION_REASONS) &&
562
+ isNumber(value.tokensFreed) &&
563
+ isString(value.marker) &&
564
+ isOptionalString(value.by)
565
+ );
566
+ }
567
+
480
568
  export function isSessionEntry(value: unknown): value is SessionEntry {
481
569
  if (!value || typeof value !== "object") return false;
482
570
  const v = value as Record<string, unknown>;
@@ -567,6 +655,24 @@ export function isSessionEntry(value: unknown): value is SessionEntry {
567
655
  isWorkerRunRuntime(v.runtime) &&
568
656
  isOptionalString(v.parentToolCallId)
569
657
  );
658
+ case "contextEviction":
659
+ return (
660
+ isString(v.policyId) &&
661
+ isOneOf(v.trigger, EVICTION_TRIGGERS) &&
662
+ Array.isArray(v.evicted) &&
663
+ v.evicted.every(isEvictedItem) &&
664
+ isNumber(v.tokensBefore) &&
665
+ isNumber(v.tokensAfter) &&
666
+ (v.pressureBefore === null || isNumber(v.pressureBefore)) &&
667
+ (v.snapshotIdBefore === null || isString(v.snapshotIdBefore))
668
+ );
669
+ case "contextRecall":
670
+ return (
671
+ isWorkingSetRef(v.ref) &&
672
+ isOneOf(v.trigger, RECALL_TRIGGERS) &&
673
+ isNumber(v.tokensReadmitted) &&
674
+ isOptionalString(v.toolCallId)
675
+ );
570
676
  }
571
677
  return false;
572
678
  }
@@ -70,9 +70,16 @@ export function resumeSessionState(sessionId: string): {
70
70
  } {
71
71
  const persistedMeta = readSessionMeta(sessionId) as SessionMeta;
72
72
  const paths = sessionPaths(persistedMeta);
73
- runMigrations(persistedMeta, paths.meta);
73
+ const migration = runMigrations(persistedMeta, paths.meta);
74
74
  const { meta, writer, tree } = engineResumeSession(sessionId);
75
- return { state: { meta: meta as SessionMeta, writer }, nodes: tree };
75
+ const state: SessionManagerState = { meta: meta as SessionMeta, writer };
76
+ if (migration.migrated) {
77
+ // The ledger needed no transformation; only the stamp moves, so the next
78
+ // reader does not re-run the same no-op.
79
+ state.meta.sessionFormatVersion = migration.to;
80
+ persistSessionMeta(state);
81
+ }
82
+ return { state, nodes: tree };
76
83
  }
77
84
 
78
85
  export function appendTurn(state: SessionManagerState, input: TurnInput): ClioTurnRecord {