@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
@@ -0,0 +1,79 @@
1
+ import type { SessionEntry } from "../../../session/entries.js";
2
+ import type { PathIndex, PathObservation } from "../path-index.js";
3
+ import type { Trace } from "./trace.js";
4
+
5
+ export interface ReferenceEdge {
6
+ /** Ref key of the earlier tool_result. */
7
+ from: string;
8
+ toTurnIndex: number;
9
+ kind: "file_reread" | "file_discovery" | "file_rewrite";
10
+ }
11
+
12
+ export interface ReferenceGraph {
13
+ edges: ReadonlyArray<ReferenceEdge>;
14
+ /** Critical future references only: file_reread and file_discovery. */
15
+ futureTurnsOf: ReadonlyMap<string, ReadonlyArray<number>>;
16
+ }
17
+
18
+ const READ_CLASS_OPS = new Set<PathObservation["op"]>(["read", "grep", "find", "ls", "code_nav"]);
19
+ const MUTATION_OPS = new Set<PathObservation["op"]>(["write", "edit"]);
20
+
21
+ function isToolResult(entry: SessionEntry | undefined): boolean {
22
+ return entry?.kind === "message" && entry.role === "tool_result";
23
+ }
24
+
25
+ function isReadableObservation(observation: PathObservation): boolean {
26
+ return !observation.isError && observation.path.length > 0 && READ_CLASS_OPS.has(observation.op);
27
+ }
28
+
29
+ function edgeKey(edge: ReferenceEdge): string {
30
+ return `${edge.from}\u0000${edge.toTurnIndex}\u0000${edge.kind}`;
31
+ }
32
+
33
+ /**
34
+ * Label future path use without inspecting result prose. Rewrites are emitted
35
+ * for diagnosis but deliberately stay out of `futureTurnsOf`: a mutation makes
36
+ * the earlier read stale rather than critical to retain.
37
+ */
38
+ export function buildReferenceGraph(trace: Trace, index: PathIndex): ReferenceGraph {
39
+ const entryById = new Map(trace.entries.map((entry) => [entry.turnId, entry]));
40
+ const edges: ReferenceEdge[] = [];
41
+ const seenEdges = new Set<string>();
42
+ const future = new Map<string, Set<number>>();
43
+
44
+ const add = (edge: ReferenceEdge): void => {
45
+ const key = edgeKey(edge);
46
+ if (seenEdges.has(key)) return;
47
+ seenEdges.add(key);
48
+ edges.push(edge);
49
+ if (edge.kind === "file_rewrite") return;
50
+ const turns = future.get(edge.from);
51
+ if (turns === undefined) future.set(edge.from, new Set([edge.toTurnIndex]));
52
+ else turns.add(edge.toTurnIndex);
53
+ };
54
+
55
+ for (const earlier of index.observations) {
56
+ if (!isToolResult(entryById.get(earlier.ref.entry)) || !isReadableObservation(earlier)) continue;
57
+ const surfaced = new Set(earlier.surfaced);
58
+ for (const later of index.observations) {
59
+ if (later.entryIndex <= earlier.entryIndex) continue;
60
+ if (isReadableObservation(later) && later.path === earlier.path) {
61
+ add({ from: earlier.ref.entry, toTurnIndex: later.turnIndex, kind: "file_reread" });
62
+ }
63
+ if (isReadableObservation(later) && surfaced.has(later.path)) {
64
+ add({ from: earlier.ref.entry, toTurnIndex: later.turnIndex, kind: "file_discovery" });
65
+ }
66
+ if (later.path === earlier.path && MUTATION_OPS.has(later.op)) {
67
+ add({ from: earlier.ref.entry, toTurnIndex: later.turnIndex, kind: "file_rewrite" });
68
+ }
69
+ }
70
+ }
71
+
72
+ const futureTurnsOf = new Map<string, ReadonlyArray<number>>();
73
+ for (const [ref, turns] of future)
74
+ futureTurnsOf.set(
75
+ ref,
76
+ [...turns].sort((a, b) => a - b),
77
+ );
78
+ return { edges, futureTurnsOf };
79
+ }
@@ -0,0 +1,139 @@
1
+ import type { WorkingSetSettings } from "../../../../core/defaults.js";
2
+ import type { ReplayLoadCascade } from "./load-clio.js";
3
+ import type { ReplayMetricAggregate, ReplayMetrics } from "./metrics.js";
4
+
5
+ export interface ReplayReportConfig {
6
+ policies: ReadonlyArray<string>;
7
+ budgets: ReadonlyArray<number>;
8
+ threshold: number;
9
+ target: number;
10
+ seed: number;
11
+ /** Synthetic corpus ids replayed, if any; ledgers from --sessions are "ledgers". */
12
+ corpus: ReadonlyArray<string>;
13
+ filter: "default" | "none";
14
+ settings: WorkingSetSettings;
15
+ }
16
+
17
+ export interface ReplayPolicyResult {
18
+ budgetTokens: number;
19
+ policyId: string;
20
+ metrics: ReplayMetricAggregate;
21
+ }
22
+
23
+ export interface ReplayReportInput {
24
+ config: ReplayReportConfig;
25
+ cascade: ReplayLoadCascade;
26
+ results: ReadonlyArray<ReplayPolicyResult>;
27
+ gitSha: string | null;
28
+ commandLine: ReadonlyArray<string>;
29
+ }
30
+
31
+ function metricObject(metrics: ReplayMetrics, turnsToFirstSummaryCount: number): Record<string, number | null> {
32
+ return {
33
+ traces: metrics.traces,
34
+ retention: metrics.retention,
35
+ retentionCovered: metrics.retentionCovered,
36
+ retentionAt10: metrics.retentionAt10,
37
+ evictionPrecision: metrics.evictionPrecision,
38
+ tokensEvicted: metrics.tokensEvicted,
39
+ recallTokens: metrics.recallTokens,
40
+ coldPrefixTokens: metrics.coldPrefixTokens,
41
+ evictionEvents: metrics.evictionEvents,
42
+ saturatedEvents: metrics.saturatedEvents,
43
+ turnsToFirstSummary: metrics.turnsToFirstSummary,
44
+ turnsToFirstSummaryCount,
45
+ summaries: metrics.summaries,
46
+ };
47
+ }
48
+
49
+ export function renderReplayJson(input: ReplayReportInput): string {
50
+ const filtered = Object.fromEntries(Object.entries(input.cascade.filtered).sort(([a], [b]) => a.localeCompare(b)));
51
+ const artifact = {
52
+ schema: "clio-context-replay-v2",
53
+ config: {
54
+ policies: [...input.config.policies],
55
+ budgets: [...input.config.budgets],
56
+ threshold: input.config.threshold,
57
+ target: input.config.target,
58
+ seed: input.config.seed,
59
+ corpus: [...input.config.corpus],
60
+ filter: input.config.filter,
61
+ settings: {
62
+ enabled: input.config.settings.enabled,
63
+ policy: input.config.settings.policy,
64
+ target: input.config.settings.target,
65
+ protectLastTurns: input.config.settings.protectLastTurns,
66
+ minEvictableTokens: input.config.settings.minEvictableTokens,
67
+ },
68
+ },
69
+ provenance: {
70
+ gitSha: input.gitSha,
71
+ commandLine: [...input.commandLine],
72
+ },
73
+ cascade: {
74
+ found: input.cascade.found,
75
+ unreadable: input.cascade.unreadable,
76
+ filtered,
77
+ kept: input.cascade.kept,
78
+ },
79
+ results: input.results.map((result) => ({
80
+ budgetTokens: result.budgetTokens,
81
+ policyId: result.policyId,
82
+ metrics: {
83
+ mean: metricObject(result.metrics.mean, result.metrics.turnsToFirstSummaryCount),
84
+ pooledRetention: result.metrics.pooledRetention,
85
+ pooledRetentionCovered: result.metrics.pooledRetentionCovered,
86
+ pooledRetentionAt10: result.metrics.pooledRetentionAt10,
87
+ },
88
+ })),
89
+ };
90
+ return `${JSON.stringify(artifact, null, "\t")}\n`;
91
+ }
92
+
93
+ function ratio(value: number): string {
94
+ return value.toFixed(3);
95
+ }
96
+
97
+ function quantity(value: number): string {
98
+ return Number.isInteger(value) ? String(value) : value.toFixed(1);
99
+ }
100
+
101
+ function cascadeRows(cascade: ReplayLoadCascade): string[] {
102
+ return [
103
+ `| found | ${cascade.found} |`,
104
+ `| unreadable | ${cascade.unreadable} |`,
105
+ ...Object.entries(cascade.filtered).map(([stage, count]) => `| ${stage} | ${count} |`),
106
+ `| kept | ${cascade.kept} |`,
107
+ ];
108
+ }
109
+
110
+ export function renderReplayMarkdown(input: ReplayReportInput): string {
111
+ const lines = [
112
+ "# Clio working-set replay",
113
+ "",
114
+ "## Inclusion cascade",
115
+ "",
116
+ "| stage | traces |",
117
+ "| --- | ---: |",
118
+ ...cascadeRows(input.cascade),
119
+ ];
120
+ for (const budget of input.config.budgets) {
121
+ lines.push(
122
+ "",
123
+ `## Budget ${budget}`,
124
+ "",
125
+ "| policy | n | retention (mean) | retention (pooled) | retention covered (mean) | retention covered (pooled) | retention@10 (mean) | eviction precision (mean) | tokens evicted (mean) | recall tokens (mean) | cold prefix tokens (mean) | eviction events (mean) | saturated events | turns to first summary (mean) | summaries (mean) |",
126
+ "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
127
+ );
128
+ for (const policy of input.config.policies) {
129
+ const result = input.results.find((entry) => entry.budgetTokens === budget && entry.policyId === policy);
130
+ if (result === undefined) continue;
131
+ const metrics = result.metrics.mean;
132
+ lines.push(
133
+ `| ${policy} | ${metrics.traces} | ${ratio(metrics.retention)} | ${ratio(result.metrics.pooledRetention)} | ${ratio(metrics.retentionCovered)} | ${ratio(result.metrics.pooledRetentionCovered)} | ${ratio(metrics.retentionAt10)} | ${ratio(metrics.evictionPrecision)} | ${quantity(metrics.tokensEvicted)} | ${quantity(metrics.recallTokens)} | ${quantity(metrics.coldPrefixTokens)} | ${quantity(metrics.evictionEvents)} | ${ratio(metrics.saturatedEvents)} | ${metrics.turnsToFirstSummary === null ? "—" : quantity(metrics.turnsToFirstSummary)} (n=${result.metrics.turnsToFirstSummaryCount}) | ${quantity(metrics.summaries)} |`,
134
+ );
135
+ }
136
+ }
137
+ lines.push("");
138
+ return lines.join("\n");
139
+ }
@@ -0,0 +1,325 @@
1
+ import type { WorkingSetSettings } from "../../../../core/defaults.js";
2
+ import { findCutPoint } from "../../../session/compaction/cut-point.js";
3
+ import { estimateTokens } from "../../../session/compaction/tokens.js";
4
+ import type {
5
+ CompactionSummaryEntry,
6
+ ContextEvictionEntry,
7
+ EvictedItem,
8
+ SessionEntry,
9
+ } from "../../../session/entries.js";
10
+ import { EMPTY_WORKING_SET_VIEW, type PolicyInput, type WorkingSetPolicy, type WorkingSetView } from "../contract.js";
11
+ import { buildEvictionFields, planEviction } from "../engine.js";
12
+ import { foldWorkingSet } from "../fold.js";
13
+ import { isTurnStart } from "../horizon.js";
14
+ import { projectWorkingSet } from "../project.js";
15
+ import { selectVisibleEntries } from "../visible.js";
16
+ import type { ReplayCandidatePoolPolicy } from "./controls.js";
17
+ import type { Trace } from "./trace.js";
18
+
19
+ /**
20
+ * The summary stage, modeled. Live, when the projection is still over the
21
+ * threshold after an eviction, Clio summarizes: it cuts the history at
22
+ * `findCutPoint(entries, keepRecentTokens)` and replaces everything before
23
+ * the cut with one paraphrase. On iterative compaction, the cut search starts
24
+ * strictly after the previous `compactionSummary`; the retained suffix that
25
+ * replay still shows is canonical input to the new paraphrase, not new raw
26
+ * history to price a second time. A long trace replayed without that stage
27
+ * spends most of its length in a regime the product never enters, because
28
+ * operator text, call arguments, and markers accumulate past any budget and
29
+ * no policy can evict them. The model appends the same `compactionSummary`
30
+ * record the live path would, with a constant-size summary standing in for the
31
+ * paraphrase, and counts it, so a policy is judged by how rarely it forces the
32
+ * one lossy, token-spending stage.
33
+ */
34
+ export interface ReplaySummaryModel {
35
+ /** Mirrors `compaction.keepRecentTokens`: the cut keeps at least this many recent tokens. */
36
+ keepRecentTokens: number;
37
+ /** Tokens the stand-in summary occupies. */
38
+ summaryTokens: number;
39
+ }
40
+
41
+ export interface ReplayConfig {
42
+ policyId: string;
43
+ budgetTokens: number;
44
+ threshold: number;
45
+ target: number;
46
+ settings: WorkingSetSettings;
47
+ seed: number;
48
+ /** Absent: summaries are counted as `turnsToFirstSummary` but never applied. */
49
+ summaries?: ReplaySummaryModel;
50
+ }
51
+
52
+ export interface ReplayEvictionEvent {
53
+ turnIndex: number;
54
+ items: ReadonlyArray<EvictedItem>;
55
+ tokensBefore: number;
56
+ tokensAfter: number;
57
+ /** True when this event exhausted the policy's usable candidate pool. */
58
+ saturated: boolean;
59
+ /**
60
+ * Tokens of the projected working set from the earliest evicted position to
61
+ * the end, after the event: what an exact-prefix cache (Anthropic, OpenAI,
62
+ * vLLM) re-prefills on the next request because the bytes before it moved.
63
+ */
64
+ coldPrefixTokens: number;
65
+ }
66
+
67
+ export interface ReplayTraceResult {
68
+ traceId: string;
69
+ policyId: string;
70
+ budgetTokens: number;
71
+ turnCount: number;
72
+ events: ReadonlyArray<ReplayEvictionEvent>;
73
+ /** Tool-result refs only; thinking-unit evictions are intentionally absent. */
74
+ evictedAtTurn: ReadonlyMap<string, number>;
75
+ turnsToFirstSummary: number | null;
76
+ /** Summary compactions applied under `config.summaries`. */
77
+ summaries: number;
78
+ /** Original entries plus synthetic append-only contextEviction and compactionSummary records. */
79
+ entries: ReadonlyArray<SessionEntry>;
80
+ }
81
+
82
+ function sumTokens(entries: ReadonlyArray<SessionEntry>): number {
83
+ let tokens = 0;
84
+ for (const entry of entries) tokens += estimateTokens(entry);
85
+ return tokens;
86
+ }
87
+
88
+ function hasCandidatePool(policy: WorkingSetPolicy): policy is ReplayCandidatePoolPolicy {
89
+ return "replayCandidateCount" in policy && typeof policy.replayCandidateCount === "function";
90
+ }
91
+
92
+ function latestCompactionIndex(entries: ReadonlyArray<SessionEntry>): number {
93
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
94
+ if (entries[index]?.kind === "compactionSummary") return index;
95
+ }
96
+ return -1;
97
+ }
98
+
99
+ function eventSaturated(
100
+ policy: WorkingSetPolicy,
101
+ input: PolicyInput,
102
+ plan: { items: ReadonlyArray<EvictedItem>; tokensAfter: number },
103
+ ): boolean {
104
+ if (policy.id === "age-horizon") return true;
105
+ const targetTokens = input.pressure.target * input.pressure.contextWindow;
106
+ if (policy.id === "structural-v1") {
107
+ const thresholdTokens = input.pressure.threshold * input.pressure.contextWindow;
108
+ const usedAgeRung = plan.items.some((item) => item.reason === "age_horizon");
109
+ // Rungs 1-5 can legitimately stop between target and threshold. Saturation
110
+ // means rung 6 actually ran and exhausted its pool before reaching target.
111
+ return plan.tokensAfter > targetTokens && (plan.tokensAfter > thresholdTokens || usedAgeRung);
112
+ }
113
+ if (hasCandidatePool(policy)) return plan.items.length === policy.replayCandidateCount(input);
114
+ return plan.tokensAfter > targetTokens;
115
+ }
116
+
117
+ interface IncrementalProjection {
118
+ raw: SessionEntry[];
119
+ projected: SessionEntry[];
120
+ indexByTurnId: Map<string, number>;
121
+ tokens: number;
122
+ }
123
+
124
+ function rebuildProjection(
125
+ soFar: ReadonlyArray<SessionEntry>,
126
+ leaf: string | null,
127
+ view: WorkingSetView,
128
+ ): IncrementalProjection {
129
+ const raw = selectVisibleEntries(soFar, leaf ?? undefined);
130
+ const projected = projectWorkingSet(raw, view);
131
+ return {
132
+ raw,
133
+ projected,
134
+ indexByTurnId: new Map(raw.map((entry, index) => [entry.turnId, index])),
135
+ tokens: sumTokens(projected),
136
+ };
137
+ }
138
+
139
+ function projectAppendedEntry(entry: SessionEntry, state: IncrementalProjection, view: WorkingSetView): SessionEntry {
140
+ if (view.evictionEvents === 0) return entry;
141
+ const lastEventId = view.lastEvictionTurnId;
142
+ if (lastEventId !== null && state.indexByTurnId.has(lastEventId)) {
143
+ // The new entry follows the visible cutoff event, so it is unchanged.
144
+ return entry;
145
+ }
146
+ // The latest event is behind a compaction cut. projectWorkingSet deliberately
147
+ // treats an absent event as later than this slice, so new assistants inherit
148
+ // the same usage-invalidation stamp as the rest of the visible slice.
149
+ return projectWorkingSet([entry], view)[0] ?? entry;
150
+ }
151
+
152
+ function appendVisibleEntry(entry: SessionEntry, state: IncrementalProjection, view: WorkingSetView): void {
153
+ const projected = projectAppendedEntry(entry, state, view);
154
+ state.indexByTurnId.set(entry.turnId, state.raw.length);
155
+ state.raw.push(entry);
156
+ state.projected.push(projected);
157
+ state.tokens += estimateTokens(projected);
158
+ }
159
+
160
+ function applyEvictionProjection(
161
+ state: IncrementalProjection,
162
+ synthetic: ContextEvictionEntry,
163
+ view: WorkingSetView,
164
+ items: ReadonlyArray<EvictedItem>,
165
+ ): void {
166
+ const affected = new Set<number>();
167
+ for (const item of items) {
168
+ const index = state.indexByTurnId.get(item.ref.entry);
169
+ if (index !== undefined) affected.add(index);
170
+ }
171
+ // The new cutoff invalidates usage on every assistant before it. Scanning
172
+ // assistants once per applied event is linear in events, never in turns.
173
+ for (let index = 0; index < state.raw.length - 1; index += 1) {
174
+ const entry = state.raw[index];
175
+ if (entry?.kind === "message" && entry.role === "assistant") affected.add(index);
176
+ }
177
+ for (const index of affected) {
178
+ const source = state.raw[index];
179
+ const before = state.projected[index];
180
+ if (source === undefined || before === undefined) continue;
181
+ const after = projectWorkingSet([source, synthetic], view)[0] ?? source;
182
+ state.projected[index] = after;
183
+ state.tokens += estimateTokens(after) - estimateTokens(before);
184
+ }
185
+ }
186
+
187
+ function coldPrefixTokens(state: IncrementalProjection, items: ReadonlyArray<EvictedItem>): number {
188
+ let cut = state.projected.length;
189
+ for (const item of items) {
190
+ const index = state.indexByTurnId.get(item.ref.entry);
191
+ if (index !== undefined && index < cut) cut = index;
192
+ }
193
+ let tokens = 0;
194
+ for (let index = cut; index < state.projected.length; index += 1) {
195
+ const entry = state.projected[index];
196
+ if (entry !== undefined) tokens += estimateTokens(entry);
197
+ }
198
+ return tokens;
199
+ }
200
+
201
+ /** Live plan/fold/project code driven at deterministic ledger turn boundaries. */
202
+ export function replayTrace(trace: Trace, policy: WorkingSetPolicy, config: ReplayConfig): ReplayTraceResult {
203
+ const soFar: SessionEntry[] = [];
204
+ const events: ReplayEvictionEvent[] = [];
205
+ const evictedAtTurn = new Map<string, number>();
206
+ const toolResults = new Set(
207
+ trace.entries
208
+ .filter((entry) => entry.kind === "message" && entry.role === "tool_result")
209
+ .map((entry) => entry.turnId),
210
+ );
211
+ let evictionSequence = 0;
212
+ let summaries = 0;
213
+ let turnIndex = 0;
214
+ let turnsToFirstSummary: number | null = null;
215
+ let lastMessageTurnId: string | null = null;
216
+ let view: WorkingSetView = EMPTY_WORKING_SET_VIEW;
217
+ let visible: IncrementalProjection = { raw: [], projected: [], indexByTurnId: new Map(), tokens: 0 };
218
+ const pressureLimit = config.threshold * config.budgetTokens;
219
+
220
+ for (const entry of trace.entries) {
221
+ if (isTurnStart(entry)) {
222
+ turnIndex += 1;
223
+ const leaf = lastMessageTurnId;
224
+ const tokens = visible.tokens;
225
+ if (tokens > pressureLimit) {
226
+ const input: PolicyInput = {
227
+ entries: visible.raw,
228
+ view,
229
+ cwd: trace.cwd,
230
+ settings: config.settings,
231
+ pressure: {
232
+ tokens,
233
+ contextWindow: config.budgetTokens,
234
+ threshold: config.threshold,
235
+ target: config.target,
236
+ },
237
+ estimateTokens,
238
+ };
239
+ const plan = planEviction(policy, input);
240
+ if (plan !== null) {
241
+ const saturated = eventSaturated(policy, input, plan);
242
+ evictionSequence += 1;
243
+ const previous = soFar[soFar.length - 1];
244
+ const synthetic: ContextEvictionEntry = {
245
+ ...buildEvictionFields(plan, {
246
+ trigger: "pressure",
247
+ pressureBefore: tokens / config.budgetTokens,
248
+ snapshotIdBefore: null,
249
+ }),
250
+ turnId: `replay-evict-${evictionSequence}`,
251
+ parentTurnId: leaf,
252
+ timestamp: previous?.timestamp ?? entry.timestamp,
253
+ };
254
+ soFar.push(synthetic);
255
+ appendVisibleEntry(synthetic, visible, view);
256
+ view = foldWorkingSet(soFar, leaf ?? undefined);
257
+ applyEvictionProjection(visible, synthetic, view, plan.items);
258
+ events.push({
259
+ turnIndex,
260
+ items: plan.items,
261
+ tokensBefore: plan.tokensBefore,
262
+ tokensAfter: plan.tokensAfter,
263
+ saturated,
264
+ coldPrefixTokens: coldPrefixTokens(visible, plan.items),
265
+ });
266
+ for (const item of plan.items) {
267
+ if (toolResults.has(item.ref.entry) && !evictedAtTurn.has(item.ref.entry)) {
268
+ evictedAtTurn.set(item.ref.entry, turnIndex);
269
+ }
270
+ }
271
+ }
272
+ if (visible.tokens > pressureLimit) {
273
+ if (turnsToFirstSummary === null) turnsToFirstSummary = turnIndex;
274
+ const previousCompactionIndex = latestCompactionIndex(soFar);
275
+ const boundaryStart = previousCompactionIndex + 1;
276
+ const cut =
277
+ config.summaries === undefined
278
+ ? boundaryStart
279
+ : findCutPoint(soFar, config.summaries.keepRecentTokens, { startIndex: boundaryStart }).firstKeptEntryIndex;
280
+ // A cut at the iterative boundary has no new history or turn prefix,
281
+ // which is the live "nothing to compact" case.
282
+ if (config.summaries !== undefined && cut > boundaryStart) {
283
+ summaries += 1;
284
+ const visibleBefore = new Set(visible.raw.map((candidate) => candidate.turnId));
285
+ const previous = soFar[soFar.length - 1];
286
+ const firstKeptTurnId = soFar[cut]?.turnId ?? "";
287
+ const synthetic: CompactionSummaryEntry = {
288
+ kind: "compactionSummary",
289
+ turnId: `replay-summary-${summaries}`,
290
+ parentTurnId: firstKeptTurnId || null,
291
+ timestamp: previous?.timestamp ?? entry.timestamp,
292
+ summary: "#".repeat(config.summaries.summaryTokens * 4),
293
+ tokensBefore: visible.tokens,
294
+ firstKeptTurnId,
295
+ };
296
+ soFar.push(synthetic);
297
+ visible = rebuildProjection(soFar, lastMessageTurnId, view);
298
+ const visibleAfter = new Set(visible.raw.map((candidate) => candidate.turnId));
299
+ for (const removed of visibleBefore) {
300
+ if (toolResults.has(removed) && !visibleAfter.has(removed) && !evictedAtTurn.has(removed)) {
301
+ evictedAtTurn.set(removed, turnIndex);
302
+ }
303
+ }
304
+ }
305
+ }
306
+ }
307
+ }
308
+ soFar.push(entry);
309
+ if (entry.kind === "compactionSummary") visible = rebuildProjection(soFar, lastMessageTurnId, view);
310
+ else appendVisibleEntry(entry, visible, view);
311
+ if (entry.kind === "message") lastMessageTurnId = entry.turnId;
312
+ }
313
+
314
+ return {
315
+ traceId: trace.id,
316
+ policyId: config.policyId,
317
+ budgetTokens: config.budgetTokens,
318
+ turnCount: trace.turnCount,
319
+ events,
320
+ evictedAtTurn,
321
+ turnsToFirstSummary,
322
+ summaries,
323
+ entries: soFar,
324
+ };
325
+ }