@iowarp/clio-coder 0.5.0 → 0.5.2

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 (312) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/CHANGELOG.md +35 -0
  3. package/CONTRIBUTING.md +9 -0
  4. package/README.md +12 -9
  5. package/ROADMAP.md +30 -5
  6. package/dist/{acp-KS7ARGV6.js → acp-F6PGOFZ4.js} +2 -2
  7. package/dist/{agents-YOGJ2YEJ.js → agents-ZHP4R6EX.js} +22 -21
  8. package/dist/assets/codewiki.json +1 -1
  9. package/dist/{auth-CEDCO6O6.js → auth-N75U2H3Z.js} +4 -4
  10. package/dist/{chunk-D3KWFOUT.js → chunk-23UIXH6Z.js} +3 -3
  11. package/dist/{chunk-ZR5YA6ZF.js → chunk-36XVK5P5.js} +5 -5
  12. package/dist/{chunk-AVSR2HKI.js → chunk-3DZ4LPLS.js} +6 -6
  13. package/dist/{chunk-LYJJ6546.js → chunk-3GSPCWFX.js} +12 -12
  14. package/dist/{chunk-CRHPCPZP.js → chunk-4KC4CHKJ.js} +2 -2
  15. package/dist/{chunk-4VDHLCF5.js → chunk-4WJBWKMI.js} +2 -2
  16. package/dist/{chunk-KBUTSJSR.js → chunk-4XBYHXGK.js} +2 -2
  17. package/dist/{chunk-6W75D37F.js → chunk-53P775GE.js} +6 -3
  18. package/dist/{chunk-XZWSP67B.js → chunk-5APYAXCB.js} +2 -2
  19. package/dist/{chunk-D65E25JG.js → chunk-67HPYNTJ.js} +2 -2
  20. package/dist/{chunk-5JQR5B4D.js → chunk-6KV4KCQZ.js} +4 -4
  21. package/dist/{chunk-V4XJWDHR.js → chunk-6UKFRTXI.js} +2 -1
  22. package/dist/{chunk-NDPF2N2L.js → chunk-7DW7CJW2.js} +2 -2
  23. package/dist/{chunk-5KQEUIWM.js → chunk-7LUEQODV.js} +4 -4
  24. package/dist/{chunk-LNOXCGN2.js → chunk-7UFDIOBT.js} +3 -3
  25. package/dist/{chunk-DXSETN72.js → chunk-7WUGNN43.js} +2 -2
  26. package/dist/{chunk-V35KZDUZ.js → chunk-AA3GRZWS.js} +2 -2
  27. package/dist/{chunk-X4CEFOSG.js → chunk-ADO3OQIC.js} +4 -4
  28. package/dist/{chunk-UX2L5CUD.js → chunk-AI36V3ON.js} +3 -3
  29. package/dist/{chunk-B7GY6QER.js → chunk-B26OUMJW.js} +2 -2
  30. package/dist/{chunk-LAR7DX5E.js → chunk-B5TASTCK.js} +153 -62
  31. package/dist/{chunk-TGWZXJ7L.js → chunk-BEMQK5Q7.js} +92 -84
  32. package/dist/{chunk-RNDCROSG.js → chunk-CNXU7TR2.js} +3 -3
  33. package/dist/{chunk-3JLTRPN7.js → chunk-CVMUXT64.js} +35 -66
  34. package/dist/{chunk-733ZBT5P.js → chunk-E74MJF6G.js} +4 -4
  35. package/dist/{chunk-T2MDZES3.js → chunk-EBMTBE76.js} +3 -3
  36. package/dist/{chunk-OZ76TLGU.js → chunk-EEITU7S6.js} +2 -2
  37. package/dist/{chunk-GRHOXDKB.js → chunk-ELDJWJKM.js} +97 -14
  38. package/dist/{chunk-GCQFN7YI.js → chunk-EMYEQLWO.js} +2 -2
  39. package/dist/{chunk-LTWH2ACR.js → chunk-EQPFEDFV.js} +222 -127
  40. package/dist/{chunk-TDGYTWM6.js → chunk-F4I3H56H.js} +2 -2
  41. package/dist/chunk-F6KCJO3U.js +523 -0
  42. package/dist/{chunk-FGSQHUCK.js → chunk-F6VKWTTY.js} +5 -5
  43. package/dist/{chunk-UJD5MPP6.js → chunk-FLNXQQ5B.js} +2 -2
  44. package/dist/{chunk-EX57VVWW.js → chunk-FOSRJ2VZ.js} +4 -4
  45. package/dist/{chunk-ZARZ4POR.js → chunk-G5ZDQQE3.js} +3 -3
  46. package/dist/{chunk-HQVN7G4F.js → chunk-G62JBPXN.js} +7 -7
  47. package/dist/{chunk-ZJPORZTC.js → chunk-GIAI7A2K.js} +2 -2
  48. package/dist/chunk-GPTR6OTU.js +2551 -0
  49. package/dist/{chunk-EZD2BXGP.js → chunk-GUFQPTQK.js} +2 -2
  50. package/dist/{chunk-OPD7GL6F.js → chunk-H6FQVNZC.js} +8 -7
  51. package/dist/chunk-H7REJ7L5.js +80 -0
  52. package/dist/{chunk-EYTLV3W3.js → chunk-HTWTORF7.js} +2 -2
  53. package/dist/{chunk-DF6PI6GN.js → chunk-I6UKQPGS.js} +3 -3
  54. package/dist/{chunk-ISHUS7HC.js → chunk-JRW236JT.js} +3 -3
  55. package/dist/{chunk-DE2LU267.js → chunk-KKAJGT3B.js} +3 -2
  56. package/dist/{chunk-OV62D6KO.js → chunk-KQMYL5GR.js} +3 -3
  57. package/dist/{chunk-QDUUOZ3S.js → chunk-KWMFC7FN.js} +3 -3
  58. package/dist/{chunk-R7FGRZNF.js → chunk-LXYHCCIZ.js} +9 -9
  59. package/dist/{chunk-NFSK2VNU.js → chunk-LZMTVVN4.js} +2 -2
  60. package/dist/{chunk-62UK7AGW.js → chunk-M5PMRZSR.js} +397 -44
  61. package/dist/chunk-MMZMM6SW.js +131 -0
  62. package/dist/{chunk-FFYTC2KQ.js → chunk-MNABNEBX.js} +2 -2
  63. package/dist/{chunk-XMDBTYCH.js → chunk-N3CWOGVF.js} +3 -3
  64. package/dist/{chunk-VYRQHORQ.js → chunk-NMQSO6Z6.js} +4 -3
  65. package/dist/{chunk-R2UUNSNQ.js → chunk-O3ZAF7UY.js} +18 -5
  66. package/dist/{chunk-4SYHGVDE.js → chunk-OQNCLZI4.js} +4 -4
  67. package/dist/{chunk-EG4ARXPC.js → chunk-OX6QTA4N.js} +3 -3
  68. package/dist/{chunk-P4GNZPXO.js → chunk-OYUS3UZP.js} +2 -2
  69. package/dist/{chunk-OQ2EOAHA.js → chunk-PF36KOGI.js} +291 -112
  70. package/dist/{chunk-55RMDDFM.js → chunk-PGQG7ZMW.js} +2 -2
  71. package/dist/{chunk-CBAQPTDU.js → chunk-PIU6BXXW.js} +2 -2
  72. package/dist/{chunk-EKV4UBCM.js → chunk-PZBQUJ2F.js} +2 -2
  73. package/dist/{chunk-7RET77VB.js → chunk-Q2HKY32Y.js} +2 -2
  74. package/dist/{chunk-U6YTUVOX.js → chunk-Q4RMNWMZ.js} +4 -4
  75. package/dist/chunk-Q6FRR3AQ.js +48 -0
  76. package/dist/{chunk-6RJMYYIE.js → chunk-QJSIAJBY.js} +3 -3
  77. package/dist/{chunk-DOUISWEE.js → chunk-QOJ4ERMU.js} +5 -5
  78. package/dist/{chunk-IFPDAD7R.js → chunk-RWSRKXLT.js} +3 -3
  79. package/dist/{chunk-T33IYTZM.js → chunk-SAASVMIY.js} +3 -3
  80. package/dist/{chunk-2DTIWSAW.js → chunk-SEQDH6JC.js} +3 -3
  81. package/dist/{chunk-BRCOSE7O.js → chunk-SZUO5BSE.js} +2 -2
  82. package/dist/{chunk-HOEDA42N.js → chunk-T7HZ7KJF.js} +2 -2
  83. package/dist/{chunk-OJXOA6YU.js → chunk-TL6LNERH.js} +2 -2
  84. package/dist/{chunk-UCINIRHW.js → chunk-TPCZXWLT.js} +3 -3
  85. package/dist/{chunk-ZUKUCZYZ.js → chunk-TQ2KTH4A.js} +2 -2
  86. package/dist/{chunk-4WOOTKFD.js → chunk-U3FETNQB.js} +341 -20
  87. package/dist/{chunk-5B3RTYAK.js → chunk-UGDSB4AN.js} +2 -2
  88. package/dist/{chunk-VKHLUZNO.js → chunk-UZ7YBL43.js} +3 -3
  89. package/dist/{chunk-QNUQ7K7D.js → chunk-VKMSEO7Y.js} +4 -4
  90. package/dist/{chunk-IN7DGBVS.js → chunk-VLX5VZ35.js} +5 -5
  91. package/dist/{chunk-SJGS3GDI.js → chunk-WAGBMMNX.js} +5 -5
  92. package/dist/{chunk-BFOSV5EZ.js → chunk-WX2YCH7F.js} +2 -2
  93. package/dist/{chunk-3LT34CAM.js → chunk-XBIGUILU.js} +32 -17
  94. package/dist/{chunk-XDHUDE5K.js → chunk-XKYBFRWR.js} +5 -5
  95. package/dist/{chunk-U5QU5ZOD.js → chunk-XYPWFSU5.js} +3 -3
  96. package/dist/{chunk-YBUECLAF.js → chunk-YEJQDODA.js} +2 -2
  97. package/dist/{chunk-YQ6XEFVK.js → chunk-Z74OGONW.js} +39 -7
  98. package/dist/{chunk-I4ELN5BX.js → chunk-ZTNOEJRI.js} +2 -2
  99. package/dist/cli/index.js +26 -26
  100. package/dist/{clio-K2PBVCEM.js → clio-4IKFE25F.js} +2 -2
  101. package/dist/{clio-context-tools-GR4OHFSO.js → clio-context-tools-OHJXYX4R.js} +25 -23
  102. package/dist/{code-nav-4X3OI6DK.js → code-nav-XPM3MY5I.js} +7 -7
  103. package/dist/{config-Q5J7UBAB.js → config-343YS25T.js} +43 -41
  104. package/dist/{config-graph-P3555574.js → config-graph-WI263KDX.js} +43 -41
  105. package/dist/{configure-KIJ32ZO6.js → configure-GZDG4HER.js} +21 -21
  106. package/dist/{context-LFBQOSXT.js → context-4OFO7N7Y.js} +12 -12
  107. package/dist/{context-WBH3KSVR.js → context-JLK2RLOG.js} +42 -40
  108. package/dist/{context-NNLBES2Y.js → context-O6DIWCF6.js} +23 -21
  109. package/dist/{context-clear-YOHMQ6SQ.js → context-clear-MR7KOGQI.js} +42 -40
  110. package/dist/{context-working-set-MRAEVDNV.js → context-working-set-YZAD3KWO.js} +14 -13
  111. package/dist/{data-tool-JJPBGMQJ.js → data-tool-EKD7DFHG.js} +7 -7
  112. package/dist/{detail-RHJOTGN4.js → detail-YZPHFEVJ.js} +43 -41
  113. package/dist/{dispatch-runner-NC7PYNIO.js → dispatch-runner-VKVXVTWJ.js} +44 -42
  114. package/dist/{doctor-S5KZ2GTN.js → doctor-53AVQXAO.js} +17 -16
  115. package/dist/{doctor-deep-2EJQSHIZ.js → doctor-deep-BLRB2NCG.js} +5 -5
  116. package/dist/{eval-3D6M36N7.js → eval-P3BQSLQE.js} +21 -20
  117. package/dist/{evidence-XNVPPLVK.js → evidence-6LLI6SZH.js} +44 -42
  118. package/dist/{evidence-6IYN52ET.js → evidence-HTN3JLUO.js} +42 -40
  119. package/dist/{evidence-JVECPI3F.js → evidence-NJRE3R5H.js} +42 -40
  120. package/dist/{evolve-TLPUDJDM.js → evolve-WWZHJVJP.js} +42 -40
  121. package/dist/{fleet-WKVBF5SZ.js → fleet-GGZ4BVJE.js} +64 -62
  122. package/dist/{fleet-6RJWIOEO.js → fleet-QG3IIWWM.js} +44 -42
  123. package/dist/{fleet-commands-L6WFT7KZ.js → fleet-commands-J2ZIKISQ.js} +7 -7
  124. package/dist/{fleet-decisions-BTQMWU5A.js → fleet-decisions-Y2Q7375Q.js} +8 -8
  125. package/dist/{fleet-graph-553DZ67Q.js → fleet-graph-TCBCCCBP.js} +11 -11
  126. package/dist/{fleet-inspect-236FZPRB.js → fleet-inspect-KOLKBC56.js} +44 -42
  127. package/dist/{fleet-preflight-NZQHABWO.js → fleet-preflight-44UNCC4Y.js} +27 -25
  128. package/dist/{fleet-validate-7NLAE26R.js → fleet-validate-K2FXRZVA.js} +13 -13
  129. package/dist/{fleet-verify-E3RO6A5W.js → fleet-verify-AS444WDW.js} +42 -40
  130. package/dist/{fleet-view-KMZQY7YJ.js → fleet-view-X4JU4J6O.js} +43 -41
  131. package/dist/gui/ops-worker.js +9 -9
  132. package/dist/gui/reads-worker.js +9 -8
  133. package/dist/{init-DV5B6HQG.js → init-VSOWFHVL.js} +54 -52
  134. package/dist/{interactive-OXGYZYAS.js → interactive-JRCBENLZ.js} +1733 -577
  135. package/dist/{interop-J7UOBOSF.js → interop-JR6BCY7R.js} +10 -10
  136. package/dist/{inventory-OEGIL6PY.js → inventory-3JUGLVJI.js} +43 -41
  137. package/dist/{library-ROZAMUIL.js → library-7BOH3Y7N.js} +15 -15
  138. package/dist/{library-B3UPGY4V.js → library-7PFONXUI.js} +9 -9
  139. package/dist/{library-MXQA5DLT.js → library-TUYZMFIA.js} +7 -7
  140. package/dist/{library-import-2LTWOVBD.js → library-import-TJVXKLDK.js} +10 -10
  141. package/dist/{library-inventory-LJ3XHLWO.js → library-inventory-MD3AYTP4.js} +9 -9
  142. package/dist/{library-validation-DARHHUNW.js → library-validation-ZDLITEA3.js} +7 -7
  143. package/dist/{mcp-2GSPYQ54.js → mcp-EFCWUPUR.js} +3 -3
  144. package/dist/{memory-J3STUUIK.js → memory-Q2YVOKK5.js} +42 -40
  145. package/dist/{models-Y3GMFJ2V.js → models-M7BOCW7F.js} +17 -16
  146. package/dist/{monitor-LHNH577J.js → monitor-2HD3MC67.js} +47 -45
  147. package/dist/{orchestrator-DTAPXULA.js → orchestrator-OUW3ZN6Y.js} +1894 -226
  148. package/dist/{panes-A743ONZ5.js → panes-TTPPSYCL.js} +3 -3
  149. package/dist/{preload-OJBMAIXF.js → preload-A47C2NUF.js} +42 -40
  150. package/dist/{providers-CMETPZNS.js → providers-T43W7EL4.js} +4 -4
  151. package/dist/{resources-FY6XYFN2.js → resources-TP6X2V6W.js} +20 -11
  152. package/dist/{run-H64MEVTA.js → run-LDSBPMCS.js} +61 -59
  153. package/dist/{share-CAFMMQLM.js → share-URHKKMHR.js} +9 -9
  154. package/dist/{skills-ERKQUC7L.js → skills-E477MOOO.js} +13 -11
  155. package/dist/{skills-eval-CQ23MYQK.js → skills-eval-AIBR2L7H.js} +583 -93
  156. package/dist/{skills-inventory-TVUYJDVN.js → skills-inventory-HLMKY3R5.js} +13 -11
  157. package/dist/{slash-commands-ROG3KIFJ.js → slash-commands-LIIZKA5Y.js} +28 -26
  158. package/dist/{startup-background-KDWCQMWT.js → startup-background-JR2KDK6R.js} +43 -41
  159. package/dist/{steer-KKYXJSH4.js → steer-D7RTAPCM.js} +3 -3
  160. package/dist/{system-ZDW44ZGX.js → system-X2P5N4IY.js} +14 -13
  161. package/dist/{targets-AJWA6E7Z.js → targets-VFFAKUFM.js} +25 -24
  162. package/dist/{tasks-6L2JJRC5.js → tasks-ERAWZA22.js} +6 -6
  163. package/dist/{terminal-lease-YFUSDPAD.js → terminal-lease-V2N2BIKQ.js} +2 -2
  164. package/dist/{trace-LA5MVR4Y.js → trace-MA5BT7XY.js} +3 -3
  165. package/dist/{usage-XSILDHKF.js → usage-WYFY2L6M.js} +50 -48
  166. package/dist/{verifiers-ECA5GIHL.js → verifiers-TYIW2XKF.js} +7 -7
  167. package/dist/{verify-R6QQCCEB.js → verify-242UFYTP.js} +6 -6
  168. package/dist/{web-fetch-PVZNMBOB.js → web-fetch-LRAGYU6Y.js} +3 -3
  169. package/dist/{wiki-generate-RP22WP5K.js → wiki-generate-OFKOX2T6.js} +53 -51
  170. package/dist/worker/entry.js +31 -29
  171. package/docs/architecture/architecture.md +1 -0
  172. package/docs/architecture/context-engine.md +4 -2
  173. package/docs/architecture/observability.md +6 -4
  174. package/docs/architecture/prompt-envelope-and-tools.md +1 -1
  175. package/docs/architecture/session-lifecycle.md +1 -1
  176. package/docs/architecture/tui-design.md +12 -9
  177. package/docs/gui/parity/02-slash-and-surfaces.md +1 -1
  178. package/docs/guide/commands-and-modes.md +91 -5
  179. package/docs/guide/configuration-and-targets.md +2 -2
  180. package/docs/guide/context-continuity.md +49 -0
  181. package/docs/guide/proactive-memory.md +6 -3
  182. package/library/registry.yaml +292 -32
  183. package/library/skills/README.md +27 -2
  184. package/library/skills/context/context-handoff/SKILL.md +10 -4
  185. package/library/skills/meta/clio-coder-dev/SKILL.md +92 -88
  186. package/library/skills/meta/clio-coder-dev/evals.md +51 -46
  187. package/library/skills/meta/clio-coder-dev/plugin.json +2 -2
  188. package/library/skills/meta/clio-coder-dev/references/change-map.md +49 -0
  189. package/library/skills/meta/clio-coder-test/SKILL.md +89 -142
  190. package/library/skills/meta/clio-coder-test/evals.md +58 -62
  191. package/library/skills/meta/clio-coder-test/plugin.json +2 -2
  192. package/library/skills/meta/clio-coder-test/references/harness.md +3 -3
  193. package/library/skills/meta/clio-coder-test/references/lifecycle-validation.md +30 -0
  194. package/library/skills/meta/clio-coder-test/references/test-map.md +60 -89
  195. package/library/skills/registry.yaml +5 -5
  196. package/library/skills/skill-marketplace.json +12 -14
  197. package/package.json +1 -1
  198. package/src/cli/skills-eval.ts +1064 -54
  199. package/src/cli/usage.ts +2 -2
  200. package/src/core/clio-repo.ts +3 -0
  201. package/src/core/tool-names.ts +1 -0
  202. package/src/domains/context/budget/inspection.ts +9 -0
  203. package/src/domains/context/budget/live-view.ts +418 -0
  204. package/src/domains/context/budget/pressure.ts +393 -0
  205. package/src/domains/context/budget/request-fit.ts +15 -0
  206. package/src/domains/evidence/build.ts +24 -0
  207. package/src/domains/gateway/mcp/client.ts +1 -1
  208. package/src/domains/memory/commit-state.ts +160 -0
  209. package/src/domains/memory/operations.ts +18 -10
  210. package/src/domains/memory/prompt-cache.ts +95 -0
  211. package/src/domains/memory/prompt-section.ts +50 -7
  212. package/src/domains/memory/relevance.ts +93 -0
  213. package/src/domains/memory/restoration.ts +78 -0
  214. package/src/domains/memory/store.ts +39 -1
  215. package/src/domains/middleware/memory-intervention.ts +116 -24
  216. package/src/domains/observability/background-memory-usage.ts +1 -1
  217. package/src/domains/observability/cost.ts +3 -3
  218. package/src/domains/observability/extension.ts +1 -1
  219. package/src/domains/observability/metrics.ts +1 -1
  220. package/src/domains/prompts/compiler.ts +1 -1
  221. package/src/domains/prompts/extension.ts +38 -1
  222. package/src/domains/quota/anthropic-max-provider.ts +134 -0
  223. package/src/domains/quota/anthropic-usage.ts +220 -0
  224. package/src/domains/quota/antigravity-provider.ts +339 -0
  225. package/src/domains/quota/cache.ts +87 -0
  226. package/src/domains/quota/claude-code-provider.ts +169 -0
  227. package/src/domains/quota/codex-provider.ts +237 -0
  228. package/src/domains/quota/presentation.ts +240 -0
  229. package/src/domains/quota/registry.ts +23 -0
  230. package/src/domains/quota/service.ts +93 -0
  231. package/src/domains/quota/summary-feed.ts +86 -0
  232. package/src/domains/quota/types.ts +84 -0
  233. package/src/domains/resources/index.ts +11 -0
  234. package/src/domains/resources/skills/catalog-view.ts +571 -0
  235. package/src/domains/resources/skills/lexical-match.ts +136 -0
  236. package/src/domains/resources/skills/loader.ts +38 -0
  237. package/src/domains/resources/skills/promotion.ts +1 -55
  238. package/src/domains/resources/skills/provenance-pin.ts +50 -20
  239. package/src/domains/safety/action-classifier.ts +1 -0
  240. package/src/domains/session/compaction/branch-summary.ts +4 -1
  241. package/src/domains/session/compaction/compact.ts +18 -2
  242. package/src/domains/session/compaction/cut-point.ts +20 -1
  243. package/src/domains/session/compaction/tokens.ts +48 -3
  244. package/src/domains/session/context-accounting.ts +8 -1
  245. package/src/domains/session/continuity/carry.ts +59 -0
  246. package/src/domains/session/continuity/contract.ts +592 -0
  247. package/src/domains/session/continuity/evidence.ts +290 -0
  248. package/src/domains/session/continuity/fold.ts +1075 -0
  249. package/src/domains/session/continuity/note.ts +79 -0
  250. package/src/domains/session/continuity/operator-request.ts +104 -0
  251. package/src/domains/session/continuity/persistence.ts +408 -0
  252. package/src/domains/session/continuity/ports.ts +231 -0
  253. package/src/domains/session/continuity/projection.ts +538 -0
  254. package/src/domains/session/continuity/validate.ts +352 -0
  255. package/src/domains/session/entries.ts +50 -3
  256. package/src/domains/session/index.ts +48 -0
  257. package/src/domains/session/migrations/index.ts +7 -3
  258. package/src/domains/session/tree/fork.ts +15 -1
  259. package/src/domains/session/usage.ts +2 -2
  260. package/src/engine/acp/commands.ts +1 -1
  261. package/src/engine/agent.ts +133 -9
  262. package/src/engine/session.ts +13 -6
  263. package/src/entry/orchestrator.ts +116 -33
  264. package/src/interactive/chat-loop-messages.ts +2 -2
  265. package/src/interactive/chat-loop.ts +219 -63
  266. package/src/interactive/chat-renderer.ts +99 -8
  267. package/src/interactive/context-overlay.ts +24 -3
  268. package/src/interactive/continuity-controller.ts +526 -0
  269. package/src/interactive/dispatch-board.ts +21 -3
  270. package/src/interactive/footer/dashboard.ts +25 -1
  271. package/src/interactive/footer/key-hints.ts +2 -2
  272. package/src/interactive/footer/pages.ts +70 -27
  273. package/src/interactive/footer/widgets.ts +32 -7
  274. package/src/interactive/footer-panel.ts +1 -1
  275. package/src/interactive/interactive-application.ts +7 -2
  276. package/src/interactive/interactive-input-runtime.ts +2 -2
  277. package/src/interactive/interactive-presentation.ts +15 -0
  278. package/src/interactive/interactive-slash-runtime.ts +45 -8
  279. package/src/interactive/interactive-tickers.ts +7 -1
  280. package/src/interactive/model-session-replay.ts +130 -3
  281. package/src/interactive/output-reserve.ts +35 -0
  282. package/src/interactive/overlay-general-openers.ts +12 -8
  283. package/src/interactive/overlay-key-routing.ts +6 -9
  284. package/src/interactive/overlay-lifecycle.ts +10 -6
  285. package/src/interactive/overlay-session-lifecycle.ts +21 -14
  286. package/src/interactive/quota-view.ts +229 -0
  287. package/src/interactive/session-last-turn.ts +1 -1
  288. package/src/interactive/session-transcript.ts +2 -1
  289. package/src/interactive/session-usage-reseed.ts +2 -2
  290. package/src/interactive/side-question.ts +2 -2
  291. package/src/interactive/slash-commands.ts +28 -9
  292. package/src/interactive/turn-context.ts +686 -63
  293. package/src/interactive/turn-middleware.ts +39 -6
  294. package/src/interactive/turn-persistence.ts +15 -2
  295. package/src/interactive/turn-prewarm.ts +1 -1
  296. package/src/interactive/turn-runtime.ts +48 -5
  297. package/src/interactive/{cost-overlay.ts → usage-overlay.ts} +121 -33
  298. package/src/interactive/welcome-dashboard.ts +26 -3
  299. package/src/tools/agent-tools.ts +3 -1
  300. package/src/tools/bootstrap.ts +6 -0
  301. package/src/tools/builtin-tool-catalog.ts +9 -0
  302. package/src/tools/context/index.ts +148 -113
  303. package/src/tools/context/surface.ts +11 -7
  304. package/src/tools/core-bootstrap.ts +4 -0
  305. package/src/tools/observation.ts +8 -2
  306. package/src/tools/policy.ts +3 -0
  307. package/src/tools/self-compact.ts +31 -0
  308. package/src/tools/surface.ts +1 -0
  309. package/src/tools/tasks.ts +1 -1
  310. package/dist/chunk-CTFPFW3H.js +0 -44
  311. package/dist/chunk-EOJPDNUP.js +0 -1325
  312. package/dist/chunk-I4Y4WKZR.js +0 -386
@@ -1,4 +1,7 @@
1
+ import type { SuccessfulMemoryContextCommit } from "../domains/memory/commit-state.js";
2
+ import type { MemoryInterventionRegistration } from "../domains/middleware/memory-intervention.js";
1
3
  import { replaceEngineMessages, setEngineSystemPrompt } from "../engine/agent.js";
4
+ import type { ContinuityReductionHooks } from "./continuity-controller.js";
2
5
  /**
3
6
  * Turn context ownership: the session-prompt compile cache, context-snapshot
4
7
  * accounting, prompt-cache honesty, and compaction. `runAutoCompact` is the
@@ -27,11 +30,23 @@ import type { SafeEventBus } from "../core/event-bus.js";
27
30
  import { residencyTargetKey } from "../core/residency-target-key.js";
28
31
  import type { PendingSkillToolPolicy } from "../core/skill-activation.js";
29
32
  import type { ToolName } from "../core/tool-names.js";
33
+ import type { BudgetInspection } from "../domains/context/budget/inspection.js";
34
+ import {
35
+ createLiveBudgetProducer,
36
+ type LiveBudgetBreakdown,
37
+ type LiveBudgetHandoffProjection,
38
+ type LiveBudgetInput,
39
+ type LiveBudgetOutcomeProjection,
40
+ type LiveBudgetView,
41
+ resolveLiveBudgetPolicy,
42
+ } from "../domains/context/budget/live-view.js";
43
+ import { requestFits } from "../domains/context/budget/request-fit.js";
30
44
  import { buildEvictionFields, planEviction } from "../domains/context/working-set/engine.js";
31
45
  import { foldWorkingSet } from "../domains/context/working-set/fold.js";
32
46
  import { isTurnStart } from "../domains/context/working-set/horizon.js";
33
47
  import { resolveWorkingSetPolicy } from "../domains/context/working-set/policies/index.js";
34
48
  import { selectVisibleEntries } from "../domains/context/working-set/visible.js";
49
+ import type { MemoryPromptRequest } from "../domains/memory/prompt-cache.js";
35
50
  import type { ObservabilityContract } from "../domains/observability/contract.js";
36
51
  import type { CompiledSessionPrompt, SessionPromptInputs } from "../domains/prompts/compiler.js";
37
52
  import type { PromptsContract } from "../domains/prompts/contract.js";
@@ -39,6 +54,7 @@ import {
39
54
  type ContextWindowDetails,
40
55
  type ContextWindowSource,
41
56
  canonicalEndpointKey,
57
+ isOrchestratorEligibleRuntime,
42
58
  type ProvidersContract,
43
59
  resolveRuntimeTarget,
44
60
  } from "../domains/providers/index.js";
@@ -56,7 +72,6 @@ import {
56
72
  appendContextSnapshot,
57
73
  type CaptureContextSnapshotInput,
58
74
  type ContextSnapshot,
59
- type ContextUsageBreakdown,
60
75
  type ContextUsageSnapshot,
61
76
  captureContextSnapshot,
62
77
  ceilChars,
@@ -85,7 +100,6 @@ import {
85
100
  type SessionPromptCompileRecord,
86
101
  } from "../domains/session/prompt-manifest.js";
87
102
  import { filterEntriesToActivePath } from "../domains/session/tree/active-path.js";
88
- import { resolveReservedOutputTokens } from "../engine/apis/output-budget.js";
89
103
  import type { AgentMessage, Usage } from "../engine/types.js";
90
104
  import { resolveToolPromptHint, type ToolRegistry } from "../tools/registry.js";
91
105
  import {
@@ -94,14 +108,17 @@ import {
94
108
  runtimeSupportsTools,
95
109
  sumRunUsage,
96
110
  toolNamesFromAgentState,
111
+ toolSignatureFromState,
97
112
  } from "./chat-loop-messages.js";
98
- import { buildModelReplayAgentMessagesFromTurns } from "./model-session-replay.js";
113
+ import { buildModelReplayAgentMessagesFromTurns, continuityContextFromSession } from "./model-session-replay.js";
114
+ import { resolveTurnOutputReserve } from "./output-reserve.js";
99
115
  import { attachedToolSchemasFromState, mainPromptCacheIdentity } from "./prompt-cache-identity.js";
100
116
  import { renderCompactionSummaryLine, renderEvictionSkipLine } from "./renderers/compaction-summary.js";
101
117
  import type { TurnMiddleware } from "./turn-middleware.js";
102
118
  import type { AgentRuntime, ChatTurnState } from "./turn-state.js";
103
119
 
104
120
  export interface TurnContextDeps {
121
+ memoryCommitBridge?: MemoryInterventionRegistration | undefined;
105
122
  interactiveGuidance?: boolean;
106
123
  state: ChatTurnState;
107
124
  getSettings: () => Readonly<ClioSettings>;
@@ -116,13 +133,29 @@ export interface TurnContextDeps {
116
133
  | ((
117
134
  instructions?: string,
118
135
  trigger?: CompactionTrigger,
119
- budget?: Pick<CompactInput, "keepRecentTokens" | "preserveUserTurnId" | "skillContextState" | "signal">,
136
+ budget?: Pick<
137
+ CompactInput,
138
+ | "keepRecentTokens"
139
+ | "preserveUserTurnId"
140
+ | "skillContextState"
141
+ | "signal"
142
+ | "beforeSummaryCall"
143
+ | "checkpointForSummary"
144
+ >,
120
145
  ) => Promise<CompactResult | null>)
121
146
  | undefined;
122
147
  /** Test seam for the eviction planner; production uses `planEviction` from the working-set engine. */
123
148
  planEviction?: typeof planEviction;
124
- getMemorySection?: (() => string) | undefined;
149
+ getMemorySection?: ((request: MemoryPromptRequest) => string) | undefined;
125
150
  getReadySkillCount?: (() => number) | undefined;
151
+ /**
152
+ * Optional continuity projections carried into the live budget view. They
153
+ * are injected interfaces: this module never reads, writes, or validates the
154
+ * durable records behind them, and a throwing reader degrades to null rather
155
+ * than failing a budget publication.
156
+ */
157
+ getPendingHandoff?: (() => LiveBudgetHandoffProjection | null) | undefined;
158
+ getLastOutcome?: (() => LiveBudgetOutcomeProjection | null) | undefined;
126
159
  middleware: TurnMiddleware;
127
160
  emitNotice: (text: string) => void;
128
161
  }
@@ -165,7 +198,23 @@ export type ExpectedColdReason =
165
198
  */
166
199
  | "background_memory";
167
200
 
201
+ export type LiveContextUsage = ContextUsageSnapshot &
202
+ Pick<LiveBudgetView, "revision" | "inputSource" | "historical" | "breakdownSource">;
203
+
168
204
  export interface TurnContext {
205
+ notifyMemoryCommit(
206
+ commitId: string,
207
+ kind: SuccessfulMemoryContextCommit["kind"],
208
+ outcome: SuccessfulMemoryContextCommit["outcome"],
209
+ ): void;
210
+ installMemoryRestoration(runtime: AgentRuntime, pendingUserText?: string): boolean;
211
+ /** Prepare an attempt before preflight; rejected attempts never freeze the next submit. */
212
+ prepareMemoryTurn(
213
+ runtime: AgentRuntime,
214
+ input: { taskText: string; continuation: boolean; images?: ReadonlyArray<unknown> | undefined },
215
+ ): void;
216
+ /** Bind first-session creation after append without rereading the prepared snapshot. */
217
+ commitMemoryTurn(runtime: AgentRuntime): void;
169
218
  ensureSessionPrompt(agentRuntime: AgentRuntime): Promise<CompiledSessionPrompt | null>;
170
219
  logPromptCompileIfPending(): void;
171
220
  invalidateSessionPromptCache(): void;
@@ -188,6 +237,26 @@ export interface TurnContext {
188
237
  */
189
238
  promptSideTokens(): number;
190
239
  liveContextEstimate(agentRuntime: AgentRuntime, pendingUserText?: string): LiveContextEstimate;
240
+ /**
241
+ * Recompute and publish the live budget view.
242
+ *
243
+ * Called at the seams that change the next request (capture, reconcile,
244
+ * reduction, the mid-run tool-batch boundary, run settlement) and by any
245
+ * consumer about to make a decision from it. The only thing it mutates is
246
+ * the accounting cache: no
247
+ * persistence, no model call, and no reduction happens because something
248
+ * asked what the budget is.
249
+ */
250
+ refreshLiveBudget(pendingUserText?: string): LiveBudgetView;
251
+ /**
252
+ * The published live budget view. A pure getter for renderers and tools:
253
+ * it never rescans the conversation or reprices the footer. The first call
254
+ * of a session publishes once, because a view has to exist before it can be
255
+ * read.
256
+ */
257
+ liveBudget(): LiveBudgetView;
258
+ /** Inspect only when the native engine owns the request; never initialize it. */
259
+ inspectLiveBudget(): BudgetInspection;
191
260
  /**
192
261
  * The loaded context window this session already recorded for a target and
193
262
  * model, so a resume budgets against it instead of re-probing. Null when the
@@ -203,6 +272,7 @@ export interface TurnContext {
203
272
  pendingUserText?: string,
204
273
  pendingSkillPolicy?: PendingSkillToolPolicy,
205
274
  signal?: AbortSignal,
275
+ handoff?: ContinuityReductionHooks,
206
276
  ): Promise<boolean>;
207
277
  cancelCompaction(): void;
208
278
  postToolContinuationGuard(
@@ -218,7 +288,7 @@ export interface TurnContext {
218
288
  }
219
289
  | undefined
220
290
  >;
221
- contextUsage(): ContextUsageSnapshot;
291
+ contextUsage(): LiveContextUsage;
222
292
  contextLedger(): ContextLedger;
223
293
  emitContextWindowWarningTransition(warning: string | null): void;
224
294
  /** Record one known cache disturbance for the current or next call. */
@@ -235,7 +305,14 @@ export interface TurnContext {
235
305
  * of a disturbance, it is the prefix the next turn wants already resident.
236
306
  */
237
307
  notePrewarm(prewarm: PrewarmStats): void;
238
- resetForSession(): void;
308
+ /**
309
+ * Drop per-session accounting. `branchAnchorTurnId` is the leaf the incoming
310
+ * session's next turn parents under; it becomes the advisory branch identity,
311
+ * which is why it is supplied only here. Ordinary appends advance the leaf
312
+ * without re-arming an advisory; real navigation re-arms it.
313
+ */
314
+ resetForSession(branchAnchorTurnId?: string | null): void;
315
+ navigationRevision(): number;
239
316
  dispose(): void;
240
317
  }
241
318
 
@@ -260,6 +337,43 @@ function evictionSkipMessage(
260
337
  });
261
338
  }
262
339
 
340
+ /**
341
+ * The token and route facts of one budget publication, separate from the
342
+ * identity fields that name it. Split out so the live and pre-runtime paths
343
+ * produce the same shape and the reduction basis can be keyed over it after it
344
+ * is resolved rather than before.
345
+ */
346
+ type LiveBudgetAccounting = Pick<
347
+ LiveBudgetInput,
348
+ | "targetId"
349
+ | "runtimeId"
350
+ | "modelId"
351
+ | "modelApi"
352
+ | "effectiveWindow"
353
+ | "windowSource"
354
+ | "estimatedInputTokens"
355
+ | "anchoredInputTokens"
356
+ | "inputTokens"
357
+ | "inputSource"
358
+ | "historical"
359
+ | "breakdown"
360
+ | "breakdownSource"
361
+ | "outputReserveTokens"
362
+ >;
363
+
364
+ /**
365
+ * Content identity of a message list.
366
+ *
367
+ * The one serialization used for both jobs that need to know whether the same
368
+ * message objects still hold the same bytes: validating a provider anchor's
369
+ * attested prefix, and fingerprinting the live request. Token counts cannot do
370
+ * either, because `estimateAgentMessageTokens` prices by character count and a
371
+ * same-length replacement leaves every figure identical.
372
+ */
373
+ function messageListDigest(messages: ReadonlyArray<AgentMessage>): string {
374
+ return createHash("sha256").update(JSON.stringify(messages)).digest("hex");
375
+ }
376
+
263
377
  export function createTurnContext(deps: TurnContextDeps): TurnContext {
264
378
  const { state, middleware } = deps;
265
379
  const compactionTrigger = new AutoCompactionTrigger<CompactResult | null>();
@@ -282,6 +396,15 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
282
396
  let reconciledAnchor: {
283
397
  tokens: number;
284
398
  anchoredMessages: ReadonlyArray<AgentMessage>;
399
+ /**
400
+ * Content identity of `anchoredMessages` when the attestation was taken.
401
+ * Object identity alone cannot detect an in-place edit: rewriting a
402
+ * message's text with a different string of the same length leaves the
403
+ * reference, the length, and therefore every chars/4 figure unchanged,
404
+ * while the provider counted different tokens. Without this the anchor
405
+ * would keep lending its authority to a prefix the provider never saw.
406
+ */
407
+ contentDigest: string;
285
408
  runtime: AgentRuntime;
286
409
  model: AgentRuntime["agent"]["state"]["model"];
287
410
  modelKey: string;
@@ -322,12 +445,38 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
322
445
  let resumedPromptHashRead = false;
323
446
  let resumedPromptHash: string | null = null;
324
447
  const sessionWorkingContextPaths = new Set<string>();
448
+ let memoryTurnSequence = 0;
449
+ let memoryAuthorityEpoch = 0;
450
+ let memoryTurn: {
451
+ id: string;
452
+ sessionId: string | null;
453
+ sessionAuthority: string;
454
+ origin: string;
455
+ taskText: string;
456
+ activePaths: readonly string[];
457
+ } | null = null;
458
+ const memoryOrigin = (runtime: AgentRuntime): string =>
459
+ JSON.stringify([process.cwd(), runtime.targetId, runtime.runtimeId, runtime.wireModelId]);
325
460
  let pendingPromptLogEntry: SessionPromptCompileRecord | null = null;
326
461
  // Reuse an empty automatic attempt only for identical ledger and provider
327
462
  // context. Same-turn tool growth or same-length content replacement must
328
463
  // get a fresh cut; ordinary below-threshold checks never fingerprint it.
329
464
  let emptyAutoCompactContextKey: string | null = null;
330
465
 
466
+ // The live budget view and the identity that decides when an advisory
467
+ // re-arms. `branchAnchorTurnId` moves only on real branch navigation, so two
468
+ // tool batches advancing `state.lastTurnId` under the same branch keep one
469
+ // advisory epoch; `navigationEpoch` separates two visits to the same leaf id.
470
+ const budgetProducer = createLiveBudgetProducer();
471
+ let branchAnchorTurnId: string | null = null;
472
+ let navigationEpoch = 0;
473
+ // Reduction-basis key of the last automatic attempt that produced nothing.
474
+ // It is the post-accounting identity, so a recalibrated anchor or a
475
+ // different resolved output cap clears the refusal even when the
476
+ // conversation, the route, and the configuration are byte-identical. The
477
+ // pressure policy reuses a refusal only for this exact key.
478
+ let noUsefulCutBasisKey: string | null = null;
479
+
331
480
  // Cache-disturbance honesty (T3.3). Accumulate every known local-runtime
332
481
  // disturbance and prefix-byte change since the last settled run. The next
333
482
  // submit consumes the set, stamps `promptCache.expectedColdReasons` on its
@@ -517,7 +666,7 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
517
666
  return resumedPromptHash;
518
667
  };
519
668
 
520
- const resolveWindowWithoutRuntime = (): ContextWindowDetails | null => {
669
+ const resolveWindowWithoutRuntime = (allowLedgerRead = true): ContextWindowDetails | null => {
521
670
  const settings = deps.getSettings();
522
671
  const targetId = settings.chat?.target?.trim();
523
672
  const wireModelId = settings.chat?.model?.trim();
@@ -529,7 +678,13 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
529
678
  use: "orchestrator",
530
679
  requireTools: false,
531
680
  requireOutputBudget: true,
532
- knownLoadedContextWindow: rememberedLoadedContextWindow(targetId, wireModelId),
681
+ knownLoadedContextWindow: allowLedgerRead
682
+ ? rememberedLoadedContextWindow(targetId, wireModelId)
683
+ : currentContextSnapshot?.contextWindowSource === "loaded" &&
684
+ currentContextSnapshot.providerId === targetId &&
685
+ currentContextSnapshot.modelId === wireModelId
686
+ ? currentContextSnapshot.effectiveContextWindow
687
+ : null,
533
688
  });
534
689
  return resolved.ok ? resolved.target.contextWindowDetails : null;
535
690
  };
@@ -555,12 +710,14 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
555
710
  * resumed snapshot's recorded window, which is what the previous process
556
711
  * measured the same messages against.
557
712
  */
558
- const windowWithoutRuntime = (): {
713
+ const windowWithoutRuntime = (
714
+ allowLedgerRead = true,
715
+ ): {
559
716
  contextWindow: number;
560
717
  contextWindowSource: ContextWindowSource | null;
561
718
  contextWindowSlots: ContextWindowDetails["contextWindowSlots"];
562
719
  } => {
563
- const details = resolveWindowWithoutRuntime();
720
+ const details = resolveWindowWithoutRuntime(allowLedgerRead);
564
721
  if (details) {
565
722
  return {
566
723
  contextWindow: details.effectiveContextWindow,
@@ -589,6 +746,12 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
589
746
  const anchor = reconciledAnchor;
590
747
  if (!anchor) return null;
591
748
  const messages = agentRuntime.agent.state.messages.filter((message) => message.role !== "system");
749
+ // The identity, route, and length checks are cheap and run first; `||`
750
+ // short-circuits, so the content digest is only computed for a prefix that
751
+ // still looks like the attested one. The digest covers the attested prefix
752
+ // and never the tail, so an append does not invalidate the anchor; the
753
+ // prefix itself is re-serialized and rehashed on every validation, which
754
+ // is the cost of detecting an in-place edit at all.
592
755
  if (
593
756
  anchor.runtime !== agentRuntime ||
594
757
  anchor.model !== agentRuntime.agent.state.model ||
@@ -597,7 +760,8 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
597
760
  anchor.runtimeId !== agentRuntime.runtimeId ||
598
761
  anchor.wireModelId !== agentRuntime.wireModelId ||
599
762
  anchor.anchoredMessages.length > messages.length ||
600
- anchor.anchoredMessages.some((message, index) => message !== messages[index])
763
+ anchor.anchoredMessages.some((message, index) => message !== messages[index]) ||
764
+ messageListDigest(messages.slice(0, anchor.anchoredMessages.length)) !== anchor.contentDigest
601
765
  ) {
602
766
  reconciledAnchor = null;
603
767
  return null;
@@ -611,13 +775,14 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
611
775
  return anchor.tokens + tail;
612
776
  };
613
777
 
778
+ let pendingUserImages: ReadonlyArray<unknown> = [];
614
779
  const liveContextEstimate = (agentRuntime: AgentRuntime, pendingUserText?: string): LiveContextEstimate => {
615
780
  const contextWindow = agentRuntime.runtimeResolution.contextWindowDetails.effectiveContextWindow;
616
781
  const estimateInput = {
617
782
  systemPrompt: agentRuntime.agent.state.systemPrompt,
618
783
  messages: agentRuntime.agent.state.messages.filter((message) => message.role !== "system"),
619
784
  tools: agentRuntime.agent.state.tools,
620
- ...(pendingUserText !== undefined ? { pendingUserText } : {}),
785
+ ...(pendingUserText !== undefined ? { pendingUserText, pendingUserImages } : {}),
621
786
  };
622
787
  const breakdown = estimateAgentContextBreakdown(estimateInput);
623
788
  const historyTokens = reconciledHistoryTokens(agentRuntime);
@@ -648,6 +813,240 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
648
813
  };
649
814
  };
650
815
 
816
+ const sha256Json = (value: unknown): string => createHash("sha256").update(JSON.stringify(value)).digest("hex");
817
+
818
+ /**
819
+ * Identity of the exact material the next request would carry, plus the
820
+ * settings that decide its reserve and whether a reduction is even eligible.
821
+ * This is the pre-accounting half: the reduction basis adds the resolved
822
+ * estimate, cap, and reservation on top of it.
823
+ *
824
+ * Token counts alone cannot do this job: `estimateAgentMessageTokens` prices
825
+ * by character count, so replacing a message with another of the same length
826
+ * leaves every figure identical while the request is a different one. The
827
+ * conversation is therefore hashed by content, not by length or count.
828
+ */
829
+ const liveBudgetContentFingerprint = (
830
+ agentRuntime: AgentRuntime | null,
831
+ pendingUserText: string | undefined,
832
+ promptFingerprint: string | null,
833
+ toolSignature: string | null,
834
+ ): string => {
835
+ const settings = deps.getSettings();
836
+ const eligibility = {
837
+ compaction: settings.context.compaction ?? null,
838
+ workingSet: settings.context.workingSet ?? null,
839
+ maxOutputTokens: settings.chat?.maxOutputTokens ?? null,
840
+ };
841
+ if (!agentRuntime) {
842
+ const snapshot = currentContextSnapshot;
843
+ return sha256Json([
844
+ "historical/1",
845
+ settings.chat?.target ?? null,
846
+ settings.chat?.model ?? null,
847
+ snapshot?.snapshotId ?? null,
848
+ snapshot ? snapshotInputTokens(snapshot) : null,
849
+ snapshot?.sources.total ?? null,
850
+ windowWithoutRuntime(false).contextWindow,
851
+ promptFingerprint,
852
+ toolSignature,
853
+ eligibility,
854
+ ]);
855
+ }
856
+ const details = agentRuntime.runtimeResolution.contextWindowDetails;
857
+ return sha256Json([
858
+ "live/1",
859
+ agentRuntime.targetId,
860
+ agentRuntime.runtimeId,
861
+ agentRuntime.wireModelId,
862
+ agentRuntime.agent.state.model?.api ?? null,
863
+ details.effectiveContextWindow,
864
+ details.contextWindowSource,
865
+ promptFingerprint,
866
+ toolSignature,
867
+ // Same serialization the anchor validates its attested prefix with, so
868
+ // the two cannot disagree about whether the conversation changed.
869
+ messageListDigest(agentRuntime.agent.state.messages.filter((message) => message.role !== "system")),
870
+ pendingUserText ?? null,
871
+ pendingUserText === undefined ? null : pendingUserImages,
872
+ eligibility,
873
+ ]);
874
+ };
875
+
876
+ /**
877
+ * A captured decomposition, labelled as one. The mapping is the same the
878
+ * `/context` overlay already uses, so the two cannot disagree; it is not a
879
+ * fresh measurement of the live agent state and is never presented as one.
880
+ */
881
+ const capturedBreakdown = (snapshot: ContextSnapshot, pendingTokens: number): LiveBudgetBreakdown => ({
882
+ systemPromptTokens: snapshot.categories.system,
883
+ messageTokens: snapshot.categories.messages + (snapshot.categories.toolResults ?? 0),
884
+ pendingUserTokens: pendingTokens,
885
+ toolSchemaTokens: snapshot.categories.tools,
886
+ });
887
+
888
+ const optionalProjection = <T>(read: (() => T | null) | undefined): T | null => {
889
+ if (!read) return null;
890
+ try {
891
+ return read();
892
+ } catch {
893
+ // A continuity projection is supplemental. Losing it must not stop the
894
+ // budget from being published.
895
+ return null;
896
+ }
897
+ };
898
+
899
+ /**
900
+ * Token facts of the live next request, all from one `liveContextEstimate`
901
+ * call: the same one admission budgets against. Old snapshot categories are
902
+ * never summed to reconstruct the total, and streaming output is never added
903
+ * on top, because the live next-request message list already carries the
904
+ * completed assistant turn and the estimate already carries the pending
905
+ * text. Each is charged exactly once.
906
+ */
907
+ const liveRuntimeAccounting = (
908
+ agentRuntime: AgentRuntime,
909
+ pendingUserText: string | undefined,
910
+ ): LiveBudgetAccounting => {
911
+ const estimate = liveContextEstimate(agentRuntime, pendingUserText);
912
+ const details = agentRuntime.runtimeResolution.contextWindowDetails;
913
+ return {
914
+ targetId: agentRuntime.targetId,
915
+ runtimeId: agentRuntime.runtimeId,
916
+ modelId: agentRuntime.wireModelId,
917
+ modelApi: agentRuntime.agent.state.model?.api ?? null,
918
+ effectiveWindow: details.effectiveContextWindow > 0 ? details.effectiveContextWindow : null,
919
+ windowSource: details.contextWindowSource,
920
+ estimatedInputTokens: estimate.estimatedTokens,
921
+ anchoredInputTokens: estimate.reconciledTokens,
922
+ inputTokens: estimate.tokens,
923
+ inputSource: estimate.reconciledTokens === null ? "estimated" : "anchored-plus-estimated-tail",
924
+ historical: false,
925
+ breakdown: { ...estimate.breakdown },
926
+ breakdownSource: "live",
927
+ outputReserveTokens: resolveTurnOutputReserve(agentRuntime, estimate.tokens),
928
+ };
929
+ };
930
+
931
+ /** Token facts before this process built a runtime: a persisted measurement, labelled as one. */
932
+ const historicalAccounting = (
933
+ snapshot: ContextSnapshot | null,
934
+ settings: Readonly<ClioSettings>,
935
+ ): LiveBudgetAccounting => {
936
+ // Inspection uses already-loaded capture facts, never a diagnostic-ledger scan.
937
+ const sameRoute =
938
+ !snapshot || (snapshot.providerId === settings.chat?.target && snapshot.modelId === settings.chat?.model);
939
+ // A saved request from model A cannot acquire model B's window while
940
+ // retaining A's identity and input measurement. Keep that capture coherent.
941
+ const window =
942
+ snapshot && !sameRoute
943
+ ? {
944
+ contextWindow: snapshot.effectiveContextWindow,
945
+ contextWindowSource: snapshotWindowSource(snapshot),
946
+ }
947
+ : windowWithoutRuntime(false);
948
+ const pendingTokens = pendingUserInputTokens();
949
+ return {
950
+ targetId: snapshot?.providerId ?? settings.chat?.target ?? null,
951
+ runtimeId: snapshot?.runtimeId ?? null,
952
+ modelId: snapshot?.modelId ?? settings.chat?.model ?? null,
953
+ modelApi: null,
954
+ effectiveWindow: window.contextWindow > 0 ? window.contextWindow : null,
955
+ windowSource: window.contextWindowSource,
956
+ estimatedInputTokens: snapshot?.estimatedTokens ?? null,
957
+ anchoredInputTokens: snapshot?.reconciledTokens ?? null,
958
+ inputTokens: snapshot ? snapshotInputTokens(snapshot) + pendingTokens : null,
959
+ inputSource: snapshot ? "historical" : "unknown",
960
+ historical: true,
961
+ breakdown: snapshot ? capturedBreakdown(snapshot, pendingTokens) : null,
962
+ breakdownSource: snapshot ? "captured" : null,
963
+ // No resolved route means no honest output reservation, and an unpriced
964
+ // reserve is unknown rather than zero.
965
+ outputReserveTokens: null,
966
+ };
967
+ };
968
+
969
+ /**
970
+ * Assemble one publication from the live runtime, or from the persisted
971
+ * capture when this process has not built a runtime yet.
972
+ */
973
+ const liveBudgetInput = (pendingUserText?: string): LiveBudgetInput => {
974
+ const settings = deps.getSettings();
975
+ const agentRuntime = state.runtime;
976
+ const snapshot = currentContextSnapshot;
977
+ const promptFingerprint = agentRuntime
978
+ ? sha256Json(agentRuntime.agent.state.systemPrompt ?? null)
979
+ : (snapshot?.promptHash ?? null);
980
+ const toolSignature = agentRuntime
981
+ ? toolSignatureFromState(agentRuntime.agent.state.tools)
982
+ : (snapshot?.toolSignature ?? null);
983
+ const contentFingerprint = liveBudgetContentFingerprint(
984
+ agentRuntime,
985
+ pendingUserText,
986
+ promptFingerprint,
987
+ toolSignature,
988
+ );
989
+ // Two independent existing settings, not one derived from the other: the
990
+ // reduce point is `context.compaction.threshold` and the working-set
991
+ // target is `context.workingSet.target`. A pair the policy refuses is
992
+ // reported as a refusal rather than repaired into a fabricated setting.
993
+ const policyResolution = resolveLiveBudgetPolicy(
994
+ settings.context.compaction?.threshold,
995
+ settings.context.workingSet?.target,
996
+ );
997
+
998
+ // Accounting first, then the identity that describes it. A reduction
999
+ // refusal is only reusable for the request the reducer actually saw, and
1000
+ // the resolved anchor, cap, and reservation are part of that request even
1001
+ // when not a byte of the conversation moved.
1002
+ const accounting = agentRuntime
1003
+ ? liveRuntimeAccounting(agentRuntime, pendingUserText)
1004
+ : historicalAccounting(snapshot, settings);
1005
+ const reductionBasisKey = sha256Json([
1006
+ "reduction-basis/1",
1007
+ contentFingerprint,
1008
+ accounting.effectiveWindow,
1009
+ accounting.estimatedInputTokens,
1010
+ accounting.anchoredInputTokens,
1011
+ accounting.inputTokens,
1012
+ accounting.inputSource,
1013
+ accounting.outputReserveTokens,
1014
+ // The resolved cap can move without moving the reservation, because a
1015
+ // route clamp may be the binding constraint. Both belong in the key.
1016
+ agentRuntime?.runtimeResolution.capabilityDecisions?.maxTokens ?? null,
1017
+ snapshot?.categories.reserve ?? null,
1018
+ policyResolution.policy.reduce,
1019
+ policyResolution.policy.target,
1020
+ ]);
1021
+
1022
+ return {
1023
+ ...accounting,
1024
+ sessionId: deps.session?.current()?.id ?? snapshot?.sessionId ?? null,
1025
+ branchAnchorTurnId,
1026
+ activeLeafTurnId: state.lastTurnId,
1027
+ activeUserTurnId: state.activeUserTurnId,
1028
+ capturedSnapshotId: snapshot?.snapshotId ?? null,
1029
+ // Compaction headroom recorded by the capture. Carried under its own
1030
+ // name so nothing can mistake it for the output reserve.
1031
+ thresholdReserveTokens: snapshot?.categories.reserve ?? null,
1032
+ policy: policyResolution.policy,
1033
+ policyRejection: policyResolution.rejection,
1034
+ // Reduction has not been asked about unless an automatic attempt for
1035
+ // this exact basis already came back empty.
1036
+ reduction: noUsefulCutBasisKey !== null && noUsefulCutBasisKey === reductionBasisKey ? "no-useful-cut" : "unknown",
1037
+ reductionBasisKey,
1038
+ promptFingerprint,
1039
+ toolSignature,
1040
+ navigationEpoch,
1041
+ lastReduction: lastCompactionEvent ? { ...lastCompactionEvent } : null,
1042
+ pendingHandoff: optionalProjection(deps.getPendingHandoff),
1043
+ lastOutcome: optionalProjection(deps.getLastOutcome),
1044
+ };
1045
+ };
1046
+
1047
+ const refreshLiveBudget = (pendingUserText?: string): LiveBudgetView =>
1048
+ budgetProducer.publish(liveBudgetInput(pendingUserText));
1049
+
651
1050
  /**
652
1051
  * A working-set projection removes tokens from messages the attestation
653
1052
  * covered; it does not make the attestation wrong about the rest. Subtract
@@ -664,10 +1063,15 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
664
1063
  tokensRemoved: number,
665
1064
  ): void => {
666
1065
  if (!reconciledAnchor || historyTokens === null) return;
1066
+ const anchoredMessages = [...agentRuntime.agent.state.messages.filter((message) => message.role !== "system")];
667
1067
  reconciledAnchor = {
668
1068
  ...reconciledAnchor,
669
1069
  tokens: Math.max(0, historyTokens - Math.max(0, tokensRemoved)),
670
- anchoredMessages: [...agentRuntime.agent.state.messages.filter((message) => message.role !== "system")],
1070
+ anchoredMessages,
1071
+ // Re-anchoring on the projected list is the point of this carry, so its
1072
+ // content identity is retaken here. Keeping the old digest would fail
1073
+ // the next validation and discard the attestation this preserves.
1074
+ contentDigest: messageListDigest(anchoredMessages),
671
1075
  };
672
1076
  };
673
1077
 
@@ -677,6 +1081,10 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
677
1081
  agentRuntime.agent,
678
1082
  buildModelReplayAgentMessagesFromTurns(refreshedEntries, {
679
1083
  ...(state.lastTurnId ? { activeLeafTurnId: state.lastTurnId } : {}),
1084
+ // The post-compaction rebuild is where an accepted note has to
1085
+ // survive: wiring continuity only into /fork would lose it on the
1086
+ // very refresh the reduction triggers.
1087
+ continuity: continuityContextFromSession(deps.session),
680
1088
  }),
681
1089
  );
682
1090
  state.replayedContextMessages = [];
@@ -737,7 +1145,7 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
737
1145
  * the LLM summary directly. Used for `/context compact`, CLIO_CODER_FORCE_COMPACT=1,
738
1146
  * and overflow recovery.
739
1147
  */
740
- const runAutoCompact = async (
1148
+ const performAutoCompact = async (
741
1149
  agentRuntime: AgentRuntime,
742
1150
  force: boolean,
743
1151
  instructions?: string,
@@ -745,11 +1153,14 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
745
1153
  pendingUserText?: string,
746
1154
  pendingSkillPolicy?: PendingSkillToolPolicy,
747
1155
  signal?: AbortSignal,
1156
+ handoff?: ContinuityReductionHooks,
748
1157
  ): Promise<boolean> => {
749
1158
  if (!deps.readSessionEntries) return false;
1159
+ const originSession = deps.session?.current()?.id;
1160
+ const originNavigation = navigationEpoch;
750
1161
  // Overflow forces a fit attempt even below the automatic threshold, but
751
1162
  // still needs the request budget and active task. Manual force keeps its defaults.
752
- const useRequestBudget = !force || triggerOverride === "overflow";
1163
+ const useRequestBudget = !force || triggerOverride === "overflow" || handoff !== undefined;
753
1164
  const activeAutoTurnId = useRequestBudget ? state.activeUserTurnId : null;
754
1165
  const skillContextState = mainSkillContextState(
755
1166
  filterEntriesToActivePath(deps.readSessionEntries(), state.lastTurnId ?? undefined),
@@ -760,9 +1171,11 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
760
1171
  const autoEnabled = cfg?.auto !== false;
761
1172
  if (!force && !autoEnabled) return false;
762
1173
  const compactionThreshold = cfg?.threshold ?? DEFAULT_COMPACTION_THRESHOLD;
763
- const pressureEstimate = force ? null : liveContextEstimate(agentRuntime, pendingUserText);
1174
+ const requiredFit = triggerOverride === "overflow";
1175
+ const pressureEstimate = force && !requiredFit ? null : liveContextEstimate(agentRuntime, pendingUserText);
764
1176
  if (
765
1177
  pressureEstimate &&
1178
+ !requiredFit &&
766
1179
  !shouldCompact(pressureEstimate.tokens, compactionThreshold, pressureEstimate.contextWindow).shouldCompact
767
1180
  )
768
1181
  return false;
@@ -802,7 +1215,12 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
802
1215
  // actually follows.
803
1216
  let evictionSkipNotice: string | null = null;
804
1217
  const rememberEmptyAutomaticAttempt = (): void => {
805
- if (!force && !preSummaryStageActed && attemptKey) emptyAutoCompactContextKey = attemptKey;
1218
+ if (force || preSummaryStageActed || !attemptKey) return;
1219
+ emptyAutoCompactContextKey = attemptKey;
1220
+ // The reducer was actually asked and found nothing. Record the request
1221
+ // it was asked about, so the pressure policy can suppress a repeat
1222
+ // advisory for that exact request and no other.
1223
+ noUsefulCutBasisKey = liveBudgetInput(pendingUserText).reductionBasisKey;
806
1224
  };
807
1225
 
808
1226
  const trigger: CompactionTrigger = triggerOverride ?? (force ? "force" : "auto");
@@ -816,7 +1234,7 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
816
1234
  // reachable only when explicitly requested.
817
1235
  if (deps.session?.current()) {
818
1236
  const beforeSnapshotId = currentContextSnapshot?.snapshotId ?? null;
819
- if (process.env.CLIO_CODER_LEGACY_MASK === "1") {
1237
+ if (process.env.CLIO_CODER_LEGACY_MASK === "1" && !handoff) {
820
1238
  let masked: ReturnType<typeof maskStaleObservations>;
821
1239
  try {
822
1240
  masked = maskStaleObservations(deps.readSessionEntries() ?? [], 6);
@@ -876,8 +1294,14 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
876
1294
  `[context engine] mask_observations: ${masked.maskedObservations} observations masked${thinkingNote}; ~${estimate.tokens} tokens -> ~${tokensAfterMask} tokens`,
877
1295
  );
878
1296
 
1297
+ refreshLiveBudget(pendingUserText);
879
1298
  const after = liveContextEstimate(agentRuntime, pendingUserText);
880
- if (!shouldCompact(after.tokens, compactionThreshold, after.contextWindow).shouldCompact) return true;
1299
+ if (
1300
+ requiredFit
1301
+ ? requestFits(after.tokens, resolveTurnOutputReserve(agentRuntime, after.tokens), after.contextWindow)
1302
+ : !shouldCompact(after.tokens, compactionThreshold, after.contextWindow).shouldCompact
1303
+ )
1304
+ return true;
881
1305
  }
882
1306
  } else if (settings.context.workingSet.enabled) {
883
1307
  let planned: ReturnType<typeof planEviction>;
@@ -897,8 +1321,18 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
897
1321
  pressure: {
898
1322
  tokens: estimate.tokens,
899
1323
  contextWindow: estimate.contextWindow,
900
- threshold: compactionThreshold,
901
- target: settings.context.workingSet.target,
1324
+ threshold: requiredFit
1325
+ ? Math.min(
1326
+ compactionThreshold,
1327
+ Math.max(0, 1 - resolveTurnOutputReserve(agentRuntime, estimate.tokens) / estimate.contextWindow),
1328
+ )
1329
+ : compactionThreshold,
1330
+ target: requiredFit
1331
+ ? Math.min(
1332
+ settings.context.workingSet.target,
1333
+ Math.max(0, 1 - resolveTurnOutputReserve(agentRuntime, estimate.tokens) / estimate.contextWindow),
1334
+ )
1335
+ : settings.context.workingSet.target,
902
1336
  },
903
1337
  estimateTokens,
904
1338
  });
@@ -914,7 +1348,7 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
914
1348
  emitCompactionActivity("started", "compacting context (working-set eviction)");
915
1349
  try {
916
1350
  const historyTokens = reconciledHistoryTokens(agentRuntime);
917
- deps.session.appendEntry({
1351
+ const evictionEntry = deps.session.appendEntry({
918
1352
  ...buildEvictionFields(planned, {
919
1353
  trigger: "pressure",
920
1354
  pressureBefore: verdict.pressure,
@@ -924,6 +1358,10 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
924
1358
  // cursor is the leaf the next message will extend.
925
1359
  parentTurnId: state.lastTurnId,
926
1360
  });
1361
+ if (deps.session.flushAppends) {
1362
+ deps.session.flushAppends();
1363
+ if (!handoff) notifyMemoryCommit(evictionEntry.turnId, "summary", "evicted");
1364
+ }
927
1365
  noteColdReason("working_set_evict");
928
1366
  refreshAgentMessagesFromSession(agentRuntime);
929
1367
  carryReconciledAnchorThroughProjection(agentRuntime, historyTokens, planned.tokensBefore - planned.tokensAfter);
@@ -967,8 +1405,19 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
967
1405
  `[context engine] working set: ${planned.items.length} ${itemsWord} evicted by ${planned.policyId}; ~${planned.tokensBefore} -> ~${planned.tokensAfter} tokens, recall by ref with context(scope="recall")`,
968
1406
  );
969
1407
 
1408
+ // Published only after the carried anchor and the post-eviction
1409
+ // capture are both in place: `liveContextEstimate` clears an
1410
+ // anchor whose message prefix no longer matches, so pricing the
1411
+ // rebuilt list any earlier would discard the attestation the
1412
+ // projection carry exists to preserve.
1413
+ refreshLiveBudget(pendingUserText);
970
1414
  const after = liveContextEstimate(agentRuntime, pendingUserText);
971
- if (!shouldCompact(after.tokens, compactionThreshold, after.contextWindow).shouldCompact) return true;
1415
+ if (
1416
+ requiredFit
1417
+ ? requestFits(after.tokens, resolveTurnOutputReserve(agentRuntime, after.tokens), after.contextWindow)
1418
+ : !shouldCompact(after.tokens, compactionThreshold, after.contextWindow).shouldCompact
1419
+ )
1420
+ return true;
972
1421
  } catch (error) {
973
1422
  emitCompactionActivity("failed", compactionFailureMessage(error));
974
1423
  throw error;
@@ -1017,18 +1466,19 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
1017
1466
  snapshotMetadata,
1018
1467
  );
1019
1468
  let budget:
1020
- | Pick<CompactInput, "keepRecentTokens" | "preserveUserTurnId" | "skillContextState" | "signal">
1469
+ | Pick<
1470
+ CompactInput,
1471
+ | "keepRecentTokens"
1472
+ | "preserveUserTurnId"
1473
+ | "skillContextState"
1474
+ | "signal"
1475
+ | "beforeSummaryCall"
1476
+ | "checkpointForSummary"
1477
+ >
1021
1478
  | undefined = skillContextState !== undefined ? { skillContextState } : undefined;
1022
1479
  if (useRequestBudget) {
1023
1480
  const estimate = liveContextEstimate(agentRuntime, pendingUserText);
1024
- const output = Math.min(
1025
- resolveReservedOutputTokens(agentRuntime.agent.state.model?.maxTokens, {
1026
- api: agentRuntime.agent.state.model?.api ?? "",
1027
- contextWindow: estimate.contextWindow,
1028
- inputTokens: estimate.tokens,
1029
- }),
1030
- settings.chat.maxOutputTokens > 0 ? settings.chat.maxOutputTokens : Number.POSITIVE_INFINITY,
1031
- );
1481
+ const output = resolveTurnOutputReserve(agentRuntime, estimate.tokens);
1032
1482
  const inputTarget = Math.min(estimate.contextWindow * compactionThreshold, estimate.contextWindow - output);
1033
1483
  const staticTokens = estimate.breakdown.systemPromptTokens + estimate.breakdown.toolSchemaTokens;
1034
1484
  const calibration = Math.max(
@@ -1055,6 +1505,7 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
1055
1505
  return (
1056
1506
  (await deps.autoCompact?.(instructions, trigger, {
1057
1507
  ...budget,
1508
+ ...handoff,
1058
1509
  signal: summarySignal,
1059
1510
  })) ?? null
1060
1511
  );
@@ -1083,9 +1534,17 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
1083
1534
 
1084
1535
  // The checkpoint identifies the selected summary route; older results
1085
1536
  // fall back to the active chat route. The ledger carries usage for reseed; this is the
1086
- // live sink, so `/cost` and the footer move the moment /context compact
1537
+ // live sink, so `/usage` and the footer move the moment /context compact
1087
1538
  // returns instead of staying byte-identical to before it ran.
1088
1539
  recordCompactionUsage(agentRuntime, result);
1540
+ if (deps.memoryCommitBridge && !handoff && deps.session?.flushAppends) {
1541
+ deps.session.flushAppends();
1542
+ await deps.session.checkpoint("context-summary");
1543
+ if (originSession !== deps.session.current()?.id || originNavigation !== navigationEpoch || signal?.aborted)
1544
+ throw new Error("Context ownership changed during checkpoint.");
1545
+ const summary = [...deps.readSessionEntries()].reverse().find((entry) => entry.kind === "compactionSummary");
1546
+ if (summary) notifyMemoryCommit(summary.turnId, "summary", "summarized");
1547
+ }
1089
1548
 
1090
1549
  refreshAgentMessagesFromSession(agentRuntime);
1091
1550
  // A summary replaces the conversation the attestation described, so no
@@ -1129,6 +1588,75 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
1129
1588
  isSplitTurn: result.isSplitTurn,
1130
1589
  }),
1131
1590
  );
1591
+ refreshLiveBudget(pendingUserText);
1592
+ return true;
1593
+ };
1594
+
1595
+ let reductionInFlight: Promise<boolean> | null = null;
1596
+ const runAutoCompact: TurnContext["runAutoCompact"] = (...args) => {
1597
+ if (reductionInFlight) {
1598
+ if (args[7]) return Promise.reject(new Error("Another context reduction is already in progress."));
1599
+ return reductionInFlight;
1600
+ }
1601
+ const operation = performAutoCompact(...args);
1602
+ reductionInFlight = operation;
1603
+ void operation
1604
+ .finally(() => {
1605
+ if (reductionInFlight === operation) reductionInFlight = null;
1606
+ })
1607
+ .catch(() => {});
1608
+ return operation;
1609
+ };
1610
+
1611
+ const bindMemoryScope = () => {
1612
+ const sessionId = deps.session?.current()?.id;
1613
+ if (!sessionId) return null;
1614
+ const scope = { sessionId, branchAnchorTurnId };
1615
+ deps.memoryCommitBridge?.bindCommitScope(scope);
1616
+ return scope;
1617
+ };
1618
+ const notifyMemoryCommit: TurnContext["notifyMemoryCommit"] = (commitId, kind, outcome) => {
1619
+ const scope = bindMemoryScope();
1620
+ if (scope) deps.memoryCommitBridge?.notifyContextCommitted({ ...scope, commitId, kind, outcome });
1621
+ };
1622
+ const installMemoryRestoration: TurnContext["installMemoryRestoration"] = (runtime, pendingText) => {
1623
+ const bridge = deps.memoryCommitBridge;
1624
+ if (!bridge || !bindMemoryScope()) return false;
1625
+ const entries = filterEntriesToActivePath(deps.readSessionEntries?.() ?? [], state.lastTurnId ?? undefined);
1626
+ const latest = [...entries]
1627
+ .reverse()
1628
+ .find((entry) => entry.kind === "continuityCommit" || entry.kind === "compactionSummary");
1629
+ const currentState =
1630
+ latest?.kind === "continuityCommit"
1631
+ ? { kind: "handoff" as const, text: latest.continuity.accepted.note }
1632
+ : latest?.kind === "compactionSummary"
1633
+ ? { kind: "summary" as const, text: latest.summary }
1634
+ : null;
1635
+ const view = refreshLiveBudget(pendingText);
1636
+ const available = Math.max(
1637
+ 0,
1638
+ (view.effectiveWindow ?? 0) - (view.outputReserveTokens ?? 0) - (view.inputTokens ?? 0) - 64,
1639
+ );
1640
+ const offer = bridge.prepareRestoration(currentState, available);
1641
+ if (!offer) return false;
1642
+ if (!offer.message) {
1643
+ bridge.acknowledgeRestoration(offer);
1644
+ return false;
1645
+ }
1646
+ const message: AgentMessage = {
1647
+ role: "user",
1648
+ content: `<system-reminder>\n${offer.message}\n</system-reminder>`,
1649
+ timestamp: Date.now(),
1650
+ };
1651
+ const cost = estimateAgentMessageTokens(message);
1652
+ if (!requestFits((view.inputTokens ?? Infinity) + cost, view.outputReserveTokens, view.effectiveWindow)) return false;
1653
+ const prior = [...runtime.agent.state.messages];
1654
+ replaceEngineMessages(runtime.agent, [...prior, message]);
1655
+ if (!bridge.acknowledgeRestoration(offer)) {
1656
+ replaceEngineMessages(runtime.agent, prior);
1657
+ return false;
1658
+ }
1659
+ refreshLiveBudget(pendingText);
1132
1660
  return true;
1133
1661
  };
1134
1662
 
@@ -1148,21 +1676,74 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
1148
1676
  });
1149
1677
 
1150
1678
  return {
1679
+ notifyMemoryCommit,
1680
+ installMemoryRestoration,
1681
+ prepareMemoryTurn(runtime, input): void {
1682
+ bindMemoryScope();
1683
+ pendingUserImages = [...(input.images ?? [])];
1684
+ if (input.continuation && memoryTurn !== null) return;
1685
+ const sessionId = deps.session?.current()?.id ?? null;
1686
+ memoryTurnSequence += 1;
1687
+ memoryTurn = {
1688
+ id: String(memoryTurnSequence),
1689
+ sessionId,
1690
+ sessionAuthority: JSON.stringify([memoryAuthorityEpoch, sessionId ?? `pending:${memoryTurnSequence}`]),
1691
+ origin: memoryOrigin(runtime),
1692
+ taskText: input.taskText,
1693
+ activePaths: Object.freeze([...sessionWorkingContextPaths]),
1694
+ };
1695
+ },
1696
+
1697
+ commitMemoryTurn(runtime): void {
1698
+ pendingUserImages = [];
1699
+ // appendSubmittedUserTurn creates the first session synchronously.
1700
+ // Only that null -> bound transition with unchanged origin can alias
1701
+ // its prepared authority; navigation never uses this exception.
1702
+ if (memoryTurn?.sessionId === null && memoryTurn.origin === memoryOrigin(runtime)) {
1703
+ memoryTurn.sessionId = deps.session?.current()?.id ?? null;
1704
+ }
1705
+ },
1706
+
1151
1707
  captureRuntimeContextSnapshot,
1152
1708
  persistContextSnapshot,
1153
1709
  liveContextEstimate,
1710
+ refreshLiveBudget,
1154
1711
  rememberedLoadedContextWindow,
1155
1712
  refreshAgentMessagesFromSession,
1156
1713
  runAutoCompact,
1714
+ navigationRevision: () => navigationEpoch,
1157
1715
  cancelCompaction: () => compactionController?.abort(),
1158
1716
 
1717
+ liveBudget: (): LiveBudgetView => budgetProducer.current() ?? refreshLiveBudget(),
1718
+ inspectLiveBudget(): BudgetInspection {
1719
+ const targetId = deps.getSettings().chat?.target;
1720
+ const target = !state.runtime && targetId ? deps.providers.getTarget(targetId) : null;
1721
+ const runtime =
1722
+ state.runtime?.runtimeResolution.runtime ?? (target ? deps.providers.getRuntime(target.runtime) : null);
1723
+ if (runtime && (!isOrchestratorEligibleRuntime(runtime) || runtime.externalAgentLoop))
1724
+ return {
1725
+ status: "unsupported",
1726
+ reason: "The external agent owns its context; native request accounting is unavailable.",
1727
+ };
1728
+ if (!runtime)
1729
+ return {
1730
+ status: "unavailable",
1731
+ reason: "No native runtime descriptor is resolved. Inspection does not initialize a runtime.",
1732
+ };
1733
+ return { status: "available", capability: "native", view: refreshLiveBudget() };
1734
+ },
1735
+
1159
1736
  setCurrentSnapshot(snapshot: ContextSnapshot): void {
1160
1737
  currentContextSnapshot = snapshot;
1738
+ refreshLiveBudget(snapshot.pendingUserInput);
1161
1739
  },
1162
1740
 
1163
1741
  flushReconciledSnapshot(): void {
1164
- if (!snapshotPersistPending || !currentContextSnapshot) return;
1165
- persistContextSnapshot(currentContextSnapshot);
1742
+ if (snapshotPersistPending && currentContextSnapshot) persistContextSnapshot(currentContextSnapshot);
1743
+ // Run settlement. Its callers are `agent_end` and submit's finally, so
1744
+ // this is where the finished run's accounting is published, not the
1745
+ // mid-run tool-batch boundary; that one is `postToolContinuationGuard`.
1746
+ refreshLiveBudget();
1166
1747
  },
1167
1748
 
1168
1749
  reconcileUsage(usage: Usage): void {
@@ -1172,11 +1753,13 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
1172
1753
  const runtime = state.runtime;
1173
1754
  if (promptTokens > 0 && runtime) {
1174
1755
  const breakdown = estimateAgentContextBreakdown({ ...runtime.agent.state, messages: [] });
1756
+ const anchoredMessages = [...runtime.agent.state.messages.filter((message) => message.role !== "system")];
1175
1757
  // The output of this call is part of the next call's prompt for the
1176
1758
  // same messages, which is why it is folded in here.
1177
1759
  reconciledAnchor = {
1178
1760
  tokens: promptTokens + (usage.output || 0),
1179
- anchoredMessages: [...runtime.agent.state.messages.filter((message) => message.role !== "system")],
1761
+ anchoredMessages,
1762
+ contentDigest: messageListDigest(anchoredMessages),
1180
1763
  runtime,
1181
1764
  model: runtime.agent.state.model,
1182
1765
  modelKey: anchorModelKey(runtime),
@@ -1187,7 +1770,12 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
1187
1770
  toolSchemaTokens: breakdown.toolSchemaTokens,
1188
1771
  };
1189
1772
  }
1190
- if (!currentContextSnapshot) return;
1773
+ if (!currentContextSnapshot) {
1774
+ // The anchor still moved, and it is what the next request budgets
1775
+ // against, so the view is republished with or without a snapshot.
1776
+ refreshLiveBudget();
1777
+ return;
1778
+ }
1191
1779
  // Reconcile in memory on every API call so the live meters
1192
1780
  // track usage; persistence waits for the run to settle.
1193
1781
  if (runtime) {
@@ -1208,6 +1796,9 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
1208
1796
  }
1209
1797
  currentContextSnapshot = reconcileSnapshot(currentContextSnapshot, usage);
1210
1798
  snapshotPersistPending = true;
1799
+ // A reconcile moves the anchor and therefore the figure the next
1800
+ // request budgets against, so it is a publication point of its own.
1801
+ refreshLiveBudget();
1211
1802
  },
1212
1803
 
1213
1804
  promptSideTokens(): number {
@@ -1271,7 +1862,20 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
1271
1862
  };
1272
1863
  if (deps.getMemorySection) {
1273
1864
  try {
1274
- const memorySection = deps.getMemorySection();
1865
+ if (memoryTurn !== null && memoryTurn.sessionId !== (sessionId || null)) {
1866
+ memoryAuthorityEpoch += 1;
1867
+ memoryTurn = null;
1868
+ }
1869
+ const memorySection = deps.getMemorySection({
1870
+ turnId: memoryTurn?.id ?? null,
1871
+ sessionAuthority: memoryTurn?.sessionAuthority ?? JSON.stringify([memoryAuthorityEpoch, sessionId]),
1872
+ cwd,
1873
+ targetId: agentRuntime.targetId,
1874
+ runtimeId: agentRuntime.runtimeId,
1875
+ modelId: agentRuntime.wireModelId,
1876
+ taskText: memoryTurn?.taskText ?? "",
1877
+ activePaths: memoryTurn?.activePaths ?? [],
1878
+ });
1275
1879
  if (memorySection.length > 0) sessionInputs.memorySection = memorySection;
1276
1880
  } catch (err) {
1277
1881
  deps.emitNotice(
@@ -1387,16 +1991,29 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
1387
1991
  // ledger turn. Preserve its newly appended context tail if compaction
1388
1992
  // rebuilds messages from the ledger, and count it in both estimates.
1389
1993
  const reminder = contextChanged ? agentRuntime.agent.state.messages.at(-1) : undefined;
1994
+ // The tool batch appended results the last publication predates, and the
1995
+ // guard is about to decide on them. Publish before that inspection, not
1996
+ // after it.
1997
+ const beforeView = refreshLiveBudget();
1390
1998
  const before = liveContextEstimate(agentRuntime);
1391
- if (before.contextWindow <= 0 || before.tokens <= 0) return undefined;
1999
+ if (before.contextWindow <= 0) throw new Error("Context window is unavailable; continuation refused.");
1392
2000
 
1393
2001
  const settings = deps.getSettings();
1394
2002
  const threshold = settings.context.compaction?.threshold ?? DEFAULT_COMPACTION_THRESHOLD;
1395
2003
  const verdict = shouldCompact(before.tokens, threshold, before.contextWindow);
1396
2004
  let compacted = false;
1397
- if (verdict.shouldCompact) {
2005
+ const mustFit = !requestFits(beforeView.inputTokens, beforeView.outputReserveTokens, beforeView.effectiveWindow);
2006
+ if (mustFit || verdict.shouldCompact) {
1398
2007
  try {
1399
- compacted = await runAutoCompact(agentRuntime, false, undefined, "auto", undefined, undefined, signal);
2008
+ compacted = await runAutoCompact(
2009
+ agentRuntime,
2010
+ mustFit,
2011
+ undefined,
2012
+ mustFit ? "overflow" : "auto",
2013
+ undefined,
2014
+ undefined,
2015
+ signal,
2016
+ );
1400
2017
  } catch (err) {
1401
2018
  throw new Error(
1402
2019
  `[Clio Coder] post-tool context guard could not compact before continuation: ${err instanceof Error ? err.message : String(err)}`,
@@ -1407,32 +2024,28 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
1407
2024
  if (reminder && !agentRuntime.agent.state.messages.includes(reminder)) {
1408
2025
  agentRuntime.agent.state.messages.push(reminder);
1409
2026
  }
1410
- const after = liveContextEstimate(agentRuntime);
1411
- if (after.tokens >= after.contextWindow) {
2027
+ // The mid-run settled tool-batch boundary: whatever the guard did or
2028
+ // declined to do, this is the context the continuation will send.
2029
+ const afterView = refreshLiveBudget();
2030
+ if (!requestFits(afterView.inputTokens, afterView.outputReserveTokens, afterView.effectiveWindow)) {
1412
2031
  throw new Error(
1413
- `[Clio Coder] post-tool context guard stopped continuation before provider call: estimated ${after.tokens} tokens exceeds reported context window ${after.contextWindow}. Use /context compact, narrower reads, or a follow-up turn with smaller observations.`,
2032
+ `[Clio Coder] post-tool context guard stopped continuation before provider call: estimated input ${afterView.inputTokens} plus reserved output ${afterView.outputReserveTokens} tokens does not fit context window ${afterView.effectiveWindow}. Use /context compact, narrower reads, or a follow-up turn with smaller observations.`,
1414
2033
  );
1415
2034
  }
1416
2035
  return compacted ? continuationContextUpdate(agentRuntime) : undefined;
1417
2036
  },
1418
2037
 
1419
- contextUsage(): ContextUsageSnapshot {
1420
- const effectiveWindow = state.runtime
1421
- ? state.runtime.runtimeResolution.contextWindowDetails.effectiveContextWindow
1422
- : windowWithoutRuntime().contextWindow;
1423
- if (!currentContextSnapshot) {
1424
- return contextUsageSnapshot(null, effectiveWindow);
1425
- }
1426
-
1427
- const pendingTokens = pendingUserInputTokens();
1428
- const totalUsed = snapshotInputTokens(currentContextSnapshot) + pendingTokens + liveStreamingOutputTokens();
1429
- const breakdown: ContextUsageBreakdown = {
1430
- systemPromptTokens: currentContextSnapshot.categories.system,
1431
- messageTokens: currentContextSnapshot.categories.messages + (currentContextSnapshot.categories.toolResults ?? 0),
1432
- pendingUserTokens: pendingTokens,
1433
- toolSchemaTokens: currentContextSnapshot.categories.tools,
2038
+ contextUsage(): LiveContextUsage {
2039
+ // Footer reads project the last publication. They never rescan the
2040
+ // ledger or add streaming output to the next-request message estimate.
2041
+ const view = budgetProducer.current() ?? refreshLiveBudget();
2042
+ return {
2043
+ ...contextUsageSnapshot(view.inputTokens, view.effectiveWindow, view.breakdown ?? undefined),
2044
+ revision: view.revision,
2045
+ inputSource: view.inputSource,
2046
+ historical: view.historical,
2047
+ breakdownSource: view.breakdownSource,
1434
2048
  };
1435
- return contextUsageSnapshot(totalUsed > 0 ? totalUsed : null, effectiveWindow, breakdown);
1436
2049
  },
1437
2050
 
1438
2051
  contextLedger(): ContextLedger {
@@ -1449,7 +2062,7 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
1449
2062
  contextWindowSource: state.runtime.runtimeResolution.contextWindowDetails.contextWindowSource,
1450
2063
  contextWindowSlots: state.runtime.runtimeResolution.contextWindowDetails.contextWindowSlots,
1451
2064
  }
1452
- : windowWithoutRuntime();
2065
+ : windowWithoutRuntime(false);
1453
2066
  const provider = state.runtime?.targetId ?? settings.chat?.target ?? null;
1454
2067
  const model = state.runtime?.wireModelId ?? settings.chat?.model ?? null;
1455
2068
  const liveToolCount = state.runtime?.agent.state.tools.length ?? 0;
@@ -1581,7 +2194,9 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
1581
2194
  }
1582
2195
  },
1583
2196
 
1584
- resetForSession(): void {
2197
+ resetForSession(incomingBranchAnchorTurnId: string | null = null): void {
2198
+ memoryTurn = null;
2199
+ memoryAuthorityEpoch += 1;
1585
2200
  compactionController?.abort();
1586
2201
  // An in-process switch replaces the whole prefix: the backend's slot
1587
2202
  // still holds the outgoing session's prompt and history, so the first
@@ -1602,7 +2217,15 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
1602
2217
  sessionWorkingContextPaths.clear();
1603
2218
  pendingPromptLogEntry = null;
1604
2219
  emptyAutoCompactContextKey = null;
2220
+ noUsefulCutBasisKey = null;
1605
2221
  reconciledAnchor = null;
2222
+ // Real branch navigation. The epoch separates two visits to the same
2223
+ // leaf id, so an advisory announced on the branch this session left is
2224
+ // re-armed rather than treated as already said.
2225
+ branchAnchorTurnId = incomingBranchAnchorTurnId;
2226
+ navigationEpoch += 1;
2227
+ bindMemoryScope();
2228
+ budgetProducer.reset();
1606
2229
  const session = deps.session?.current();
1607
2230
  currentContextSnapshot = session ? getLatestContextSnapshot(session) : null;
1608
2231
  },