@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,120 @@
1
+ /**
2
+ * Transcript detail policy: the one place `/output minimal|default|verbose`
3
+ * becomes a decision about how much of each foldable transcript block is open
4
+ * before the operator touches it.
5
+ *
6
+ * The chat panel used to consult the verbosity string in four places, each
7
+ * spelling the rule a little differently, and nothing else (running rows, live
8
+ * tool output, worker cards in replay) consulted it at all. The panel now reads
9
+ * this policy once per frame and threads it down; the verbosity string never
10
+ * reaches a renderer.
11
+ *
12
+ * Operator toggles (`Alt+O`, `Alt+R`, expand-all, collapse-all) are overrides
13
+ * layered on top of this policy, not fights with it: the effective state of a
14
+ * block is `override ?? policy`, and `resolveFold` is that one rule.
15
+ *
16
+ * Pure module: no I/O, no UI imports.
17
+ */
18
+
19
+ import type { OutputVerbosity } from "../core/defaults.js";
20
+ import type { ToolFoldDefault, ToolPresentationPolicy } from "../tools/presentation.js";
21
+
22
+ /** A block's fold state. The same vocabulary serves tools, workers, thinking, and local bash. */
23
+ export type Fold = ToolFoldDefault;
24
+
25
+ /** An operator's explicit choice for one block, or none. */
26
+ export type FoldOverride = Fold | undefined;
27
+
28
+ export interface TranscriptDetailPolicy {
29
+ /** Finished tool call body: folded subline, the tool's own default, or open. */
30
+ toolBody: "folded" | "per-tool" | "expanded";
31
+ /** In-flight tool call: one-line row with elapsed, or the header plus streaming body. */
32
+ runningTool: "row" | "body";
33
+ /** Reasoning stretch: bare marker, folded marker with live progress, or the open rail. */
34
+ thinking: "marker" | "folded" | "rail";
35
+ /** Worker block: folded card, the origin default, or open. */
36
+ worker: "folded" | "origin" | "expanded";
37
+ /** Settled turn receipt: none, `turn · in N · out M`, or the full receipt. */
38
+ receipt: "none" | "compact" | "full";
39
+ /** Failed tool under its folded row: the bounded excerpt, or the full body. */
40
+ errors: "excerpt" | "body";
41
+ }
42
+
43
+ const MINIMAL: TranscriptDetailPolicy = {
44
+ toolBody: "folded",
45
+ runningTool: "row",
46
+ thinking: "marker",
47
+ worker: "folded",
48
+ receipt: "none",
49
+ errors: "excerpt",
50
+ };
51
+
52
+ const DEFAULT: TranscriptDetailPolicy = {
53
+ toolBody: "per-tool",
54
+ runningTool: "row",
55
+ thinking: "folded",
56
+ worker: "origin",
57
+ receipt: "compact",
58
+ errors: "excerpt",
59
+ };
60
+
61
+ const VERBOSE: TranscriptDetailPolicy = {
62
+ toolBody: "expanded",
63
+ runningTool: "body",
64
+ thinking: "rail",
65
+ worker: "expanded",
66
+ receipt: "full",
67
+ errors: "body",
68
+ };
69
+
70
+ /**
71
+ * Map a verbosity to the full policy table. An absent verbosity (a panel built
72
+ * without settings) is the balanced default, so the literal never has to be
73
+ * spelled by the caller.
74
+ */
75
+ export function transcriptDetail(verbosity: OutputVerbosity | undefined): TranscriptDetailPolicy {
76
+ switch (verbosity) {
77
+ case "minimal":
78
+ return MINIMAL;
79
+ case "verbose":
80
+ return VERBOSE;
81
+ default:
82
+ return DEFAULT;
83
+ }
84
+ }
85
+
86
+ /** Effective state of one block: the operator's override when set, else the policy's answer. */
87
+ export function resolveFold(override: FoldOverride, policyFold: Fold): Fold {
88
+ return override ?? policyFold;
89
+ }
90
+
91
+ /** The fold the policy gives a finished tool call, through the tool's own presentation when asked to. */
92
+ export function policyToolFold(detail: TranscriptDetailPolicy, presentation: ToolPresentationPolicy): Fold {
93
+ if (detail.toolBody === "per-tool") return presentation.foldDefault;
94
+ return detail.toolBody;
95
+ }
96
+
97
+ /** The fold the policy gives an in-flight tool call. */
98
+ export function policyRunningToolFold(detail: TranscriptDetailPolicy): Fold {
99
+ return detail.runningTool === "body" ? "expanded" : "folded";
100
+ }
101
+
102
+ /** The fold the policy gives a reasoning stretch. */
103
+ export function policyThinkingFold(detail: TranscriptDetailPolicy): Fold {
104
+ return detail.thinking === "rail" ? "expanded" : "folded";
105
+ }
106
+
107
+ /**
108
+ * The fold the policy gives a worker block. `askedByModel` is the origin rule
109
+ * the caller already knows: a run the model asked for folds, the operator's own
110
+ * run opens.
111
+ */
112
+ export function policyWorkerFold(detail: TranscriptDetailPolicy, askedByModel: boolean): Fold {
113
+ if (detail.worker === "origin") return askedByModel ? "folded" : "expanded";
114
+ return detail.worker;
115
+ }
116
+
117
+ /** The override that flips a block away from its current effective state. */
118
+ export function toggledFold(effective: Fold): Fold {
119
+ return effective === "expanded" ? "folded" : "expanded";
120
+ }
@@ -10,11 +10,16 @@ import {
10
10
  BusChannels,
11
11
  type ContextActivityStatus,
12
12
  type ContextPrunedPayload,
13
+ type ContextRecalledPayload,
13
14
  type ContextWarningPayload,
14
15
  } from "../core/bus-events.js";
15
16
  import type { ClioSettings } from "../core/config.js";
16
17
  import type { SafeEventBus } from "../core/event-bus.js";
17
18
  import type { ToolName } from "../core/tool-names.js";
19
+ import { buildEvictionFields, planEviction } from "../domains/context/working-set/engine.js";
20
+ import { foldWorkingSet } from "../domains/context/working-set/fold.js";
21
+ import { resolveWorkingSetPolicy } from "../domains/context/working-set/policies/index.js";
22
+ import { selectVisibleEntries } from "../domains/context/working-set/visible.js";
18
23
  import type { ObservabilityContract } from "../domains/observability/contract.js";
19
24
  import type { CompiledSessionPrompt, SessionPromptInputs } from "../domains/prompts/compiler.js";
20
25
  import type { PromptsContract } from "../domains/prompts/contract.js";
@@ -27,6 +32,7 @@ import {
27
32
  } from "../domains/session/compaction/auto.js";
28
33
  import type { CompactResult } from "../domains/session/compaction/compact.js";
29
34
  import { maskStaleObservations } from "../domains/session/compaction/mask-observations.js";
35
+ import { estimateTokens } from "../domains/session/compaction/tokens.js";
30
36
  import {
31
37
  appendContextSnapshot,
32
38
  type CaptureContextSnapshotInput,
@@ -57,7 +63,7 @@ import {
57
63
  sumRunUsage,
58
64
  toolNamesFromAgentState,
59
65
  } from "./chat-loop-messages.js";
60
- import { buildReplayAgentMessagesFromTurns } from "./chat-renderer.js";
66
+ import { buildModelReplayAgentMessagesFromTurns } from "./model-session-replay.js";
61
67
  import { renderCompactionSummaryLine } from "./renderers/compaction-summary.js";
62
68
  import type { TurnMiddleware } from "./turn-middleware.js";
63
69
  import type { AgentRuntime, ChatTurnState } from "./turn-state.js";
@@ -73,6 +79,8 @@ export interface TurnContextDeps {
73
79
  bus?: SafeEventBus | undefined;
74
80
  readSessionEntries?: (() => ReadonlyArray<SessionEntry>) | undefined;
75
81
  autoCompact?: ((instructions?: string, trigger?: CompactionTrigger) => Promise<CompactResult | null>) | undefined;
82
+ /** Test seam for the eviction planner; production uses `planEviction` from the working-set engine. */
83
+ planEviction?: typeof planEviction;
76
84
  getMemorySection?: (() => string) | undefined;
77
85
  middleware: TurnMiddleware;
78
86
  emitNotice: (text: string) => void;
@@ -159,6 +167,11 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
159
167
  let sessionPromptKey: string | null = null;
160
168
  const sessionWorkingContextPaths = new Set<string>();
161
169
  let pendingPromptLogEntry: SessionPromptCompileRecord | null = null;
170
+ // A post-tool guard can run after every tool result in one model turn. Once
171
+ // both automatic stages report that they have nothing to do, remember that
172
+ // stable user-turn id so the remaining results do not repeat the same plan
173
+ // and summary probes. A new submitted user turn gets a new id naturally.
174
+ let emptyAutoCompactTurnId: string | null = null;
162
175
 
163
176
  // Cache-disturbance honesty (T3.3). Dispatch traffic and history
164
177
  // compaction invalidate a single-slot local backend's prefix cache.
@@ -167,20 +180,41 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
167
180
  // its first assistant entry, and shows one dim notice.
168
181
  const pendingColdReasons = new Set<string>();
169
182
  let runExpectedColdReasons: string[] = [];
170
- let stampColdReasonsPending = false;
183
+ let nextAssistantColdReasons: string[] = [];
184
+ // A working-set eviction changes the prefix itself, so it cools every
185
+ // tier's cache, not only a single-slot local one. Dispatch and compaction
186
+ // disturbances keep the local-native gate below.
187
+ const TIER_INDEPENDENT_COLD_REASONS: ReadonlySet<string> = new Set(["working_set_evict"]);
188
+ const stampsOnTier = (reason: string, runtimeId: string | undefined): boolean =>
189
+ TIER_INDEPENDENT_COLD_REASONS.has(reason) ||
190
+ (runtimeId !== undefined && deps.providers.getRuntime(runtimeId)?.tier === "local-native");
191
+ const noteColdReason = (reason: string): void => {
192
+ if (!state.streaming) {
193
+ pendingColdReasons.add(reason);
194
+ return;
195
+ }
196
+ if (!stampsOnTier(reason, state.runtime?.runtimeId)) return;
197
+ if (!runExpectedColdReasons.includes(reason)) runExpectedColdReasons.push(reason);
198
+ if (nextAssistantColdReasons.includes(reason)) return;
199
+ nextAssistantColdReasons.push(reason);
200
+ deps.emitNotice(`[context engine] backend prefix cache likely cold this turn: ${reason}`);
201
+ };
171
202
  const unsubscribeColdReasonSources = [
172
203
  ...[BusChannels.DispatchStarted, BusChannels.DispatchCompleted, BusChannels.DispatchFailed].map(
173
204
  (channel) =>
174
205
  deps.bus?.on(channel, () => {
175
- pendingColdReasons.add("dispatch");
206
+ noteColdReason("dispatch");
176
207
  }) ?? null,
177
208
  ),
178
209
  ...[BusChannels.CompactionBegin, BusChannels.CompactionEnd].map(
179
210
  (channel) =>
180
211
  deps.bus?.on(channel, () => {
181
- pendingColdReasons.add("compaction");
212
+ noteColdReason("compaction");
182
213
  }) ?? null,
183
214
  ),
215
+ deps.bus?.on(BusChannels.ContextRecalled, (payload: ContextRecalledPayload) => {
216
+ middleware.fireCompactionHook("working_set_recall", payload.trigger);
217
+ }) ?? null,
184
218
  ];
185
219
 
186
220
  /**
@@ -292,7 +326,7 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
292
326
 
293
327
  const refreshAgentMessagesFromSession = (agentRuntime: AgentRuntime): ReadonlyArray<SessionEntry> => {
294
328
  const refreshedEntries = deps.readSessionEntries?.() ?? [];
295
- agentRuntime.agent.state.messages = buildReplayAgentMessagesFromTurns(refreshedEntries, {
329
+ agentRuntime.agent.state.messages = buildModelReplayAgentMessagesFromTurns(refreshedEntries, {
296
330
  ...(state.lastTurnId ? { activeLeafTurnId: state.lastTurnId } : {}),
297
331
  });
298
332
  state.replayedContextMessages = [];
@@ -339,12 +373,12 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
339
373
 
340
374
  /**
341
375
  * Two-mechanism context protection. When pressure crosses the single
342
- * threshold, first mask the bodies of tool observations older than
343
- * `excludeLastTurns` (cheap, no LLM call). If pressure stays above the
344
- * threshold, delegate to the pi-style LLM compaction path: append a
345
- * compaction summary entry, then replay from the session view.
376
+ * threshold, first apply a non-destructive working-set eviction. If pressure
377
+ * stays above the threshold, delegate to the pi-style LLM compaction path:
378
+ * append a compaction summary entry, then replay from the session view. The
379
+ * destructive observation mask remains only as a one-release escape hatch.
346
380
  *
347
- * `force = true` skips the pressure check and the mask pre-stage and runs
381
+ * `force = true` skips the pressure check and every pre-stage and runs
348
382
  * the LLM summary directly. Used for `/context compact`, CLIO_CODER_FORCE_COMPACT=1,
349
383
  * and overflow recovery.
350
384
  */
@@ -355,11 +389,17 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
355
389
  triggerOverride?: CompactionTrigger,
356
390
  pendingUserText?: string,
357
391
  ): Promise<boolean> => {
358
- if (!deps.autoCompact || !deps.readSessionEntries) return false;
392
+ if (!deps.readSessionEntries) return false;
393
+ const activeAutoTurnId = force ? null : state.activeUserTurnId;
394
+ if (activeAutoTurnId && emptyAutoCompactTurnId === activeAutoTurnId) return false;
359
395
  const settings = deps.getSettings();
360
396
  const cfg = settings.compaction;
361
397
  const autoEnabled = cfg?.auto !== false;
362
398
  if (!force && !autoEnabled) return false;
399
+ let preSummaryStageActed = false;
400
+ const rememberEmptyAutomaticAttempt = (): void => {
401
+ if (!force && !preSummaryStageActed && activeAutoTurnId) emptyAutoCompactTurnId = activeAutoTurnId;
402
+ };
363
403
 
364
404
  const compactionThreshold = cfg?.threshold ?? DEFAULT_COMPACTION_THRESHOLD;
365
405
  const trigger: CompactionTrigger = triggerOverride ?? (force ? "force" : "auto");
@@ -369,91 +409,198 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
369
409
  const verdict = shouldCompact(estimate.tokens, compactionThreshold, estimate.contextWindow);
370
410
  if (!verdict.shouldCompact) return false;
371
411
 
372
- // Mechanism B pre-stage: mask stale observations before paying for
373
- // an LLM summary. History rewrites here invalidate the backend
374
- // prefix cache; the "compaction" expectedColdReasons stamp from the
375
- // CompactionBegin/End subscription explains the next cold turn.
412
+ // One-release compatibility escape hatch. This is the destructive
413
+ // pre-stage that working-set eviction replaces; keep it byte-for-byte
414
+ // reachable only when explicitly requested.
376
415
  if (deps.session?.current()) {
377
416
  const beforeSnapshotId = currentContextSnapshot?.snapshotId ?? null;
378
- let masked: ReturnType<typeof maskStaleObservations>;
379
- try {
380
- masked = maskStaleObservations(deps.readSessionEntries() ?? [], cfg?.excludeLastTurns ?? 6);
381
- } catch (error) {
382
- middleware.fireCompactionHook("mask_observations", trigger, estimate.tokens);
383
- deps.bus?.emit(BusChannels.CompactionBegin, { trigger, at: Date.now() });
384
- emitCompactionActivity("started", "compacting context (mask stage)");
385
- emitCompactionActivity("failed", compactionFailureMessage(error));
386
- deps.bus?.emit(BusChannels.CompactionEnd, { trigger, at: Date.now() });
387
- throw error;
388
- }
389
- if (masked.changed) {
390
- middleware.fireCompactionHook("mask_observations", trigger, estimate.tokens);
391
- deps.bus?.emit(BusChannels.CompactionBegin, { trigger, at: Date.now() });
392
- emitCompactionActivity("started", "compacting context (mask stage)");
393
- deps.session.replaceEntries(masked.entries);
394
- refreshAgentMessagesFromSession(agentRuntime);
395
- deps.bus?.emit(BusChannels.CompactionEnd, { trigger, at: Date.now() });
396
-
397
- const postMaskSnapshot = captureRuntimeContextSnapshot(
398
- agentRuntime,
399
- state.activeUserTurnId || "compaction",
400
- compactionThreshold,
401
- );
402
- currentContextSnapshot = postMaskSnapshot;
403
- persistContextSnapshot(postMaskSnapshot);
404
-
405
- const tokensAfterMask = snapshotInputTokens(postMaskSnapshot);
406
- lastCompactionEvent = {
407
- stage: "mask_observations",
408
- tokensBefore: estimate.tokens,
409
- tokensAfter: tokensAfterMask,
410
- trigger,
411
- };
412
- deps.bus?.emit(BusChannels.ContextPruned, {
413
- stage: "mask_observations",
414
- pressure: verdict.pressure,
415
- tokensBefore: estimate.tokens,
416
- tokensAfter: tokensAfterMask,
417
- maskedObservations: masked.maskedObservations,
418
- maskedThinkingBlocks: masked.maskedThinkingBlocks,
419
- maskedThinkingChars: masked.maskedThinkingChars,
420
- trigger,
421
- snapshotIdBefore: beforeSnapshotId,
422
- snapshotIdAfter: postMaskSnapshot.snapshotId,
423
- at: Date.now(),
424
- } satisfies ContextPrunedPayload);
425
- emitCompactionActivity("completed", `${masked.maskedObservations} observations masked`);
426
- const thinkingNote =
427
- masked.maskedThinkingBlocks > 0
428
- ? `, ${masked.maskedThinkingBlocks} thinking blocks dropped (~${masked.maskedThinkingChars} chars)`
429
- : "";
430
- deps.emitNotice(
431
- `[context engine] mask_observations: ${masked.maskedObservations} observations masked${thinkingNote}; ~${estimate.tokens} tokens -> ~${tokensAfterMask} tokens`,
432
- );
433
-
434
- const after = liveContextEstimate(agentRuntime, pendingUserText);
435
- if (!shouldCompact(after.tokens, compactionThreshold, after.contextWindow).shouldCompact) return true;
417
+ if (process.env.CLIO_CODER_LEGACY_MASK === "1") {
418
+ let masked: ReturnType<typeof maskStaleObservations>;
419
+ try {
420
+ masked = maskStaleObservations(deps.readSessionEntries() ?? [], cfg?.excludeLastTurns ?? 6);
421
+ } catch (error) {
422
+ middleware.fireCompactionHook("mask_observations", trigger, estimate.tokens);
423
+ deps.bus?.emit(BusChannels.CompactionBegin, { trigger, at: Date.now() });
424
+ emitCompactionActivity("started", "compacting context (mask stage)");
425
+ emitCompactionActivity("failed", compactionFailureMessage(error));
426
+ deps.bus?.emit(BusChannels.CompactionEnd, { trigger, at: Date.now() });
427
+ throw error;
428
+ }
429
+ if (masked.changed) {
430
+ preSummaryStageActed = true;
431
+ middleware.fireCompactionHook("mask_observations", trigger, estimate.tokens);
432
+ deps.bus?.emit(BusChannels.CompactionBegin, { trigger, at: Date.now() });
433
+ emitCompactionActivity("started", "compacting context (mask stage)");
434
+ deps.session.replaceEntries(masked.entries);
435
+ refreshAgentMessagesFromSession(agentRuntime);
436
+ deps.bus?.emit(BusChannels.CompactionEnd, { trigger, at: Date.now() });
437
+
438
+ const postMaskSnapshot = captureRuntimeContextSnapshot(
439
+ agentRuntime,
440
+ state.activeUserTurnId || "compaction",
441
+ compactionThreshold,
442
+ );
443
+ currentContextSnapshot = postMaskSnapshot;
444
+ persistContextSnapshot(postMaskSnapshot);
445
+
446
+ const tokensAfterMask = snapshotInputTokens(postMaskSnapshot);
447
+ lastCompactionEvent = {
448
+ stage: "mask_observations",
449
+ tokensBefore: estimate.tokens,
450
+ tokensAfter: tokensAfterMask,
451
+ trigger,
452
+ };
453
+ deps.bus?.emit(BusChannels.ContextPruned, {
454
+ stage: "mask_observations",
455
+ pressure: verdict.pressure,
456
+ tokensBefore: estimate.tokens,
457
+ tokensAfter: tokensAfterMask,
458
+ maskedObservations: masked.maskedObservations,
459
+ maskedThinkingBlocks: masked.maskedThinkingBlocks,
460
+ maskedThinkingChars: masked.maskedThinkingChars,
461
+ trigger,
462
+ snapshotIdBefore: beforeSnapshotId,
463
+ snapshotIdAfter: postMaskSnapshot.snapshotId,
464
+ at: Date.now(),
465
+ } satisfies ContextPrunedPayload);
466
+ emitCompactionActivity("completed", `${masked.maskedObservations} observations masked`);
467
+ const thinkingNote =
468
+ masked.maskedThinkingBlocks > 0
469
+ ? `, ${masked.maskedThinkingBlocks} thinking blocks dropped (~${masked.maskedThinkingChars} chars)`
470
+ : "";
471
+ deps.emitNotice(
472
+ `[context engine] mask_observations: ${masked.maskedObservations} observations masked${thinkingNote}; ~${estimate.tokens} tokens -> ~${tokensAfterMask} tokens`,
473
+ );
474
+
475
+ const after = liveContextEstimate(agentRuntime, pendingUserText);
476
+ if (!shouldCompact(after.tokens, compactionThreshold, after.contextWindow).shouldCompact) return true;
477
+ }
478
+ } else if (settings.context.workingSet.enabled) {
479
+ let planned: ReturnType<typeof planEviction>;
480
+ try {
481
+ const entries = deps.readSessionEntries() ?? [];
482
+ const view = foldWorkingSet(entries, state.lastTurnId ?? undefined);
483
+ const policy = resolveWorkingSetPolicy(settings.context.workingSet.policy);
484
+ planned = (deps.planEviction ?? planEviction)(policy, {
485
+ entries: selectVisibleEntries(entries, state.lastTurnId ?? undefined),
486
+ view,
487
+ cwd: deps.session.current()?.cwd ?? null,
488
+ settings: settings.context.workingSet,
489
+ pressure: {
490
+ tokens: estimate.tokens,
491
+ contextWindow: estimate.contextWindow,
492
+ threshold: compactionThreshold,
493
+ target: settings.context.workingSet.target,
494
+ },
495
+ estimateTokens,
496
+ });
497
+ } catch (error) {
498
+ middleware.fireCompactionHook("working_set_evict", "pressure", estimate.tokens);
499
+ emitCompactionActivity("started", "compacting context (working-set eviction)");
500
+ emitCompactionActivity("failed", compactionFailureMessage(error));
501
+ throw error;
502
+ }
503
+ if (planned) {
504
+ preSummaryStageActed = true;
505
+ middleware.fireCompactionHook("working_set_evict", "pressure", estimate.tokens);
506
+ emitCompactionActivity("started", "compacting context (working-set eviction)");
507
+ try {
508
+ deps.session.appendEntry({
509
+ ...buildEvictionFields(planned, {
510
+ trigger: "pressure",
511
+ pressureBefore: verdict.pressure,
512
+ snapshotIdBefore: beforeSnapshotId,
513
+ }),
514
+ // appendEntry does not infer this anchor; the interactive
515
+ // cursor is the leaf the next message will extend.
516
+ parentTurnId: state.lastTurnId,
517
+ });
518
+ noteColdReason("working_set_evict");
519
+ refreshAgentMessagesFromSession(agentRuntime);
520
+
521
+ const postEvictionSnapshot = captureRuntimeContextSnapshot(
522
+ agentRuntime,
523
+ state.activeUserTurnId || "compaction",
524
+ compactionThreshold,
525
+ );
526
+ currentContextSnapshot = postEvictionSnapshot;
527
+ persistContextSnapshot(postEvictionSnapshot);
528
+
529
+ // Every surface that describes this event (the notice, the toast
530
+ // ContextPruned feeds, the overlay's last-compaction line, the
531
+ // ledger entry) quotes the plan: the same chars/4 pricing over the
532
+ // same visible slice. The live estimate prices the agent message
533
+ // list and differs by the tool schemas and replay text; it stays
534
+ // what the meter and the re-check below read, not what the event
535
+ // reports about itself.
536
+ lastCompactionEvent = {
537
+ stage: "working_set",
538
+ tokensBefore: planned.tokensBefore,
539
+ tokensAfter: planned.tokensAfter,
540
+ trigger,
541
+ };
542
+ deps.bus?.emit(BusChannels.ContextPruned, {
543
+ stage: "working_set",
544
+ pressure: verdict.pressure,
545
+ tokensBefore: planned.tokensBefore,
546
+ tokensAfter: planned.tokensAfter,
547
+ trigger,
548
+ snapshotIdBefore: beforeSnapshotId,
549
+ snapshotIdAfter: postEvictionSnapshot.snapshotId,
550
+ policyId: planned.policyId,
551
+ evictedItems: planned.items.length,
552
+ at: Date.now(),
553
+ } satisfies ContextPrunedPayload);
554
+ const itemsWord = planned.items.length === 1 ? "item" : "items";
555
+ emitCompactionActivity("completed", `${planned.items.length} working-set ${itemsWord} evicted`);
556
+ deps.emitNotice(
557
+ `[context engine] working set: ${planned.items.length} ${itemsWord} evicted by ${planned.policyId}; ~${planned.tokensBefore} -> ~${planned.tokensAfter} tokens, recall by ref with context(scope="recall")`,
558
+ );
559
+
560
+ const after = liveContextEstimate(agentRuntime, pendingUserText);
561
+ if (!shouldCompact(after.tokens, compactionThreshold, after.contextWindow).shouldCompact) return true;
562
+ } catch (error) {
563
+ emitCompactionActivity("failed", compactionFailureMessage(error));
564
+ throw error;
565
+ }
566
+ }
436
567
  }
437
568
  }
438
569
  }
439
570
 
440
- middleware.fireCompactionHook("llm_summary", trigger);
441
- deps.bus?.emit(BusChannels.CompactionBegin, { trigger, at: Date.now() });
442
- emitCompactionActivity("started", "compacting context (summary)");
571
+ if (!deps.autoCompact) {
572
+ rememberEmptyAutomaticAttempt();
573
+ return false;
574
+ }
575
+ let summaryLifecycleStarted = false;
576
+ const startSummaryLifecycle = (): void => {
577
+ middleware.fireCompactionHook("llm_summary", trigger);
578
+ deps.bus?.emit(BusChannels.CompactionBegin, { trigger, at: Date.now() });
579
+ emitCompactionActivity("started", "compacting context (summary)");
580
+ summaryLifecycleStarted = true;
581
+ };
582
+ if (force) startSummaryLifecycle();
443
583
  let result: CompactResult | null = null;
444
584
  const beforeSnapshotId = currentContextSnapshot?.snapshotId ?? null;
445
585
  try {
446
586
  result = await compactionTrigger.fire(() => (deps.autoCompact ?? (async () => null))(instructions, trigger));
447
587
  } catch (error) {
588
+ if (!summaryLifecycleStarted) startSummaryLifecycle();
448
589
  emitCompactionActivity("failed", compactionFailureMessage(error));
449
- throw error;
450
- } finally {
451
590
  deps.bus?.emit(BusChannels.CompactionEnd, { trigger, at: Date.now() });
591
+ throw error;
452
592
  }
453
593
  if (!result || result.summary.length === 0) {
454
- emitCompactionActivity("completed", "nothing to compact");
594
+ if (summaryLifecycleStarted) {
595
+ deps.bus?.emit(BusChannels.CompactionEnd, { trigger, at: Date.now() });
596
+ emitCompactionActivity("completed", "nothing to compact");
597
+ } else {
598
+ rememberEmptyAutomaticAttempt();
599
+ }
455
600
  return false;
456
601
  }
602
+ if (!summaryLifecycleStarted) startSummaryLifecycle();
603
+ deps.bus?.emit(BusChannels.CompactionEnd, { trigger, at: Date.now() });
457
604
 
458
605
  // The summarization call spends tokens on the same target the turn would
459
606
  // have. The ledger entry carries its usage for a later reseed; this is the
@@ -790,16 +937,17 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
790
937
  consumeExpectedColdReasons(runtimeId: string): void {
791
938
  // Cache-disturbance honesty (T3.3): consume disturbances since
792
939
  // the last settled run. Only single-slot local backends lose their
793
- // prefix cache to interleaved work, so only local-native targets
794
- // stamp reasons and notify; other tiers just clear the set.
940
+ // prefix cache to interleaved work, so dispatch and compaction
941
+ // stamp only on local-native targets; a working-set eviction moved
942
+ // the prefix itself and stamps on every tier (see stampsOnTier).
795
943
  runExpectedColdReasons = [];
796
- stampColdReasonsPending = false;
944
+ nextAssistantColdReasons = [];
797
945
  if (pendingColdReasons.size > 0) {
798
- const reasons = [...pendingColdReasons];
946
+ const reasons = [...pendingColdReasons].filter((reason) => stampsOnTier(reason, runtimeId));
799
947
  pendingColdReasons.clear();
800
- if (deps.providers.getRuntime(runtimeId)?.tier === "local-native") {
948
+ if (reasons.length > 0) {
801
949
  runExpectedColdReasons = reasons;
802
- stampColdReasonsPending = true;
950
+ nextAssistantColdReasons = reasons;
803
951
  deps.emitNotice(`[context engine] backend prefix cache likely cold this turn: ${reasons.join(", ")}`);
804
952
  }
805
953
  }
@@ -818,9 +966,9 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
818
966
  cacheWrite,
819
967
  backendVerdict: backendCacheVerdict(input, cacheRead),
820
968
  };
821
- if (stampColdReasonsPending && runExpectedColdReasons.length > 0) {
822
- promptCache.expectedColdReasons = [...runExpectedColdReasons];
823
- stampColdReasonsPending = false;
969
+ if (nextAssistantColdReasons.length > 0) {
970
+ promptCache.expectedColdReasons = [...nextAssistantColdReasons];
971
+ nextAssistantColdReasons = [];
824
972
  }
825
973
  return promptCache;
826
974
  },
@@ -834,6 +982,7 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
834
982
  cacheWriteTokens: cacheSummary.cacheRead > 0 || cacheSummary.cacheWrite > 0 ? cacheSummary.cacheWrite : null,
835
983
  uncachedInputTokens: cacheSummary.input,
836
984
  backendVerdict: runFirstCallVerdict,
985
+ ...(runExpectedColdReasons.length > 0 ? { expectedColdReasons: [...runExpectedColdReasons] } : {}),
837
986
  };
838
987
  }
839
988
  },
@@ -844,6 +993,7 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
844
993
  sessionPromptKey = null;
845
994
  sessionWorkingContextPaths.clear();
846
995
  pendingPromptLogEntry = null;
996
+ emptyAutoCompactTurnId = null;
847
997
  const session = deps.session?.current();
848
998
  currentContextSnapshot = session ? getLatestContextSnapshot(session) : null;
849
999
  },
@@ -15,7 +15,7 @@ import {
15
15
  type MiddlewareToolChoiceControl,
16
16
  } from "../domains/middleware/index.js";
17
17
  import type { SessionContract } from "../domains/session/contract.js";
18
- import type { CompactionTrigger } from "../domains/session/entries.js";
18
+ import type { CompactionTrigger, EvictionTrigger, RecallTrigger } from "../domains/session/entries.js";
19
19
  import type { AgentMessage } from "../engine/types.js";
20
20
  import { extractText, hasStructuredToolCall, toolNamesFromAgentState } from "./chat-loop-messages.js";
21
21
  import type { AgentRuntime, ChatTurnState } from "./turn-state.js";
@@ -42,8 +42,8 @@ export interface TurnMiddleware {
42
42
  terminalToolResult?: { toolCallId: string; toolName: string },
43
43
  ): Promise<void>;
44
44
  fireCompactionHook(
45
- stage: "mask_observations" | "llm_summary",
46
- trigger: CompactionTrigger,
45
+ stage: "mask_observations" | "working_set_evict" | "working_set_recall" | "llm_summary",
46
+ trigger: CompactionTrigger | EvictionTrigger | RecallTrigger,
47
47
  tokensBefore?: number,
48
48
  ): void;
49
49
  flushPendingReminders(): string;
@@ -240,9 +240,9 @@ export function createTurnMiddleware(deps: TurnMiddlewareDeps): TurnMiddleware {
240
240
  },
241
241
 
242
242
  /**
243
- * Observe-only lifecycle point fired before each compaction stage, at the
244
- * existing CompactionBegin emit sites. Consumers record telemetry or state
245
- * ahead of context loss; returned effects are discarded by design.
243
+ * Observe-only lifecycle point fired before each compaction or working-set
244
+ * stage. Consumers record telemetry or state around projected context
245
+ * changes; returned effects are discarded by design.
246
246
  */
247
247
  fireCompactionHook(stage, trigger, tokensBefore): void {
248
248
  if (!deps.middleware) return;