@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
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Local lexical matching over skill text. No model call, no network, and no
3
+ * request text ever leaves the process.
4
+ *
5
+ * Two callers with two different error costs share this vocabulary.
6
+ * `promotion.ts` scores an *unsolicited* install offer, where a false positive
7
+ * costs operator trust, so it keeps its own conservative thresholds on top of
8
+ * these helpers. The catalog view filters a listing the model explicitly asked
9
+ * to narrow, where the expensive error is the opposite one: a skill the
10
+ * operator named that the filter silently hides. {@link lexicalMatches} is
11
+ * tuned for that second case and is not a promotion matcher.
12
+ */
13
+
14
+ /**
15
+ * Words too common in a coding request to distinguish one skill from another.
16
+ * Shared so both matchers agree about what carries no signal.
17
+ */
18
+ export const TOKEN_STOPWORDS = new Set([
19
+ "the",
20
+ "a",
21
+ "an",
22
+ "and",
23
+ "or",
24
+ "for",
25
+ "with",
26
+ "this",
27
+ "that",
28
+ "when",
29
+ "what",
30
+ "how",
31
+ "into",
32
+ "from",
33
+ "use",
34
+ "used",
35
+ "using",
36
+ "one",
37
+ "not",
38
+ "skill",
39
+ "skills",
40
+ "clio",
41
+ "please",
42
+ "can",
43
+ "you",
44
+ "should",
45
+ "would",
46
+ "about",
47
+ "need",
48
+ "want",
49
+ "help",
50
+ "make",
51
+ "file",
52
+ "files",
53
+ "code",
54
+ ]);
55
+
56
+ /** Lowercase, punctuation to spaces, whitespace collapsed. */
57
+ export function normalize(text: string): string {
58
+ return text
59
+ .toLowerCase()
60
+ .replace(/[^\p{L}\p{N}\s]/gu, " ")
61
+ .replace(/\s+/g, " ")
62
+ .trim();
63
+ }
64
+
65
+ /**
66
+ * Distinctive tokens: 4+ chars and outside the stopword set.
67
+ *
68
+ * The 4-character floor belongs to the promotion matcher, which fires an offer
69
+ * nobody asked for and must not do so on `tdd` appearing in a sentence. The
70
+ * catalog filter deliberately does not use this.
71
+ */
72
+ export function distinctiveTokens(text: string): Set<string> {
73
+ return new Set(
74
+ normalize(text)
75
+ .split(" ")
76
+ .filter((token) => token.length >= 4 && !TOKEN_STOPWORDS.has(token)),
77
+ );
78
+ }
79
+
80
+ /** How many of a multi-word query's tokens a row has to carry. */
81
+ export type LexicalMatchMode = "all" | "any";
82
+
83
+ /** Query tokens worth matching on: 2+ chars, outside the stopword set. */
84
+ function queryTokens(normalizedQuery: string): string[] {
85
+ return normalizedQuery.split(" ").filter((token) => token.length >= 2 && !TOKEN_STOPWORDS.has(token));
86
+ }
87
+
88
+ /**
89
+ * A token matches a word outright, or as its prefix once it is 3+ characters.
90
+ *
91
+ * Prefix matching is what makes `conflict` find `resolve-merge-conflicts` and
92
+ * `worktree` find `worktree-create`. It is floored at 3 characters so a
93
+ * two-letter token cannot match most of the catalog.
94
+ */
95
+ function wordMatches(words: ReadonlySet<string>, token: string): boolean {
96
+ if (words.has(token)) return true;
97
+ if (token.length < 3) return false;
98
+ for (const word of words) {
99
+ if (word.startsWith(token)) return true;
100
+ }
101
+ return false;
102
+ }
103
+
104
+ /**
105
+ * Does `haystack` match `query`?
106
+ *
107
+ * An empty query matches everything, so a caller can pass the operator's
108
+ * argument through without branching.
109
+ *
110
+ * The whole query matching as a word sequence always wins, which is how a name
111
+ * shorter than the token floor (`tdd`, `prd`) stays findable by typing it. The
112
+ * sequence is matched with word boundaries on both ends, so `git` does not hit
113
+ * `digital`.
114
+ *
115
+ * Failing that, the query is tokenized and matched per token. `mode` decides
116
+ * whether every token must land or any one of them may: the catalog view tries
117
+ * `all` first and only falls back to `any` when that returned nothing, so a
118
+ * precise query narrows and a conversational one still finds something instead
119
+ * of an empty page.
120
+ */
121
+ export function lexicalMatches(query: string, haystack: string, mode: LexicalMatchMode = "all"): boolean {
122
+ // Canonical composition on both sides, so a decomposed `café` typed by
123
+ // one editor matches a composed `café` written by another. Applied here
124
+ // rather than inside `normalize`, because `normalize` is also the promotion
125
+ // matcher's and changing what that fires on is a separate decision.
126
+ const normalizedQuery = normalize(query.normalize("NFC"));
127
+ if (normalizedQuery.length === 0) return true;
128
+ const normalizedHaystack = normalize(haystack.normalize("NFC"));
129
+ if (` ${normalizedHaystack} `.includes(` ${normalizedQuery} `)) return true;
130
+ const tokens = queryTokens(normalizedQuery);
131
+ if (tokens.length === 0) return false;
132
+ const words = new Set(normalizedHaystack.split(" ").filter((word) => word.length > 0));
133
+ return mode === "all"
134
+ ? tokens.every((token) => wordMatches(words, token))
135
+ : tokens.some((token) => wordMatches(words, token));
136
+ }
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { type Dirent, existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
+ import { CLIO_SELF_DEVELOPMENT_SKILLS, detectClioCoderRepo } from "../../../core/clio-repo.js";
5
6
  import { warnLegacyNaming } from "../../../core/naming-compat.js";
6
7
  import type { PendingSkillRequest } from "../../../core/skill-activation.js";
7
8
  import { type ToolName, ToolNames } from "../../../core/tool-names.js";
@@ -145,6 +146,8 @@ export interface SkillRoot {
145
146
  * package roots use the package directory as their containment boundary.
146
147
  */
147
148
  containment?: string;
149
+ /** Repository-provided roots cannot relocate their containment through a symlink. */
150
+ strictContainment?: boolean;
148
151
  }
149
152
 
150
153
  export interface SkillList {
@@ -242,6 +245,40 @@ export function defaultSkillRoots(input: LoadSkillsInput = {}): SkillRoot[] {
242
245
  const trustProject = projectCompatTrusted(input.trustProjectCompatRoots);
243
246
  const roots: SkillRoot[] = [];
244
247
 
248
+ // Source-checkout guidance is a narrow native discovery tier, not a package
249
+ // installation. An installed package owns its name even when disabled,
250
+ // damaged, or awaiting reload; never resurrect it through a source fallback.
251
+ const selfRepo = detectClioCoderRepo(cwd).repoRoot;
252
+ if (selfRepo) {
253
+ const ownedNames = installedSkillNames([], cwd);
254
+ if (path.resolve(cwd) !== selfRepo) {
255
+ for (const name of installedSkillNames([], selfRepo)) ownedNames.add(name);
256
+ // Native skills can be flat Markdown files or directories whose
257
+ // frontmatter name differs from the path. Use the loader's names so
258
+ // a descendant launch cannot resurrect a hidden root override.
259
+ const rootOverrides = loadSkillRoot(
260
+ { path: path.join(selfRepo, ".clio-coder", "skills"), scope: "project", containment: selfRepo },
261
+ [],
262
+ );
263
+ for (const candidate of rootOverrides) ownedNames.add(candidate.skill.name);
264
+ }
265
+ for (const name of CLIO_SELF_DEVELOPMENT_SKILLS) {
266
+ if (ownedNames.has(name)) continue;
267
+ const skillPath = path.join(selfRepo, "library", "skills", "meta", name);
268
+ if (!existsSync(path.join(skillPath, "SKILL.md"))) continue;
269
+ roots.push({
270
+ path: skillPath,
271
+ scope: "project",
272
+ source: "clio-coder",
273
+ origin: "self-development",
274
+ precedence: SKILL_PRECEDENCE.package - 1,
275
+ trusted: true,
276
+ containment: selfRepo,
277
+ strictContainment: true,
278
+ });
279
+ }
280
+ }
281
+
245
282
  for (const root of enabledPluginResourceRoots("skills", cwd)) {
246
283
  roots.push({
247
284
  path: root.path,
@@ -560,6 +597,7 @@ function pathIsInside(candidate: string, anchor: string): boolean {
560
597
  function containmentAnchor(root: SkillRoot): string | null {
561
598
  if (root.containment === undefined) return null;
562
599
  const anchor = canonicalizePath(root.containment);
600
+ if (root.strictContainment) return anchor;
563
601
  const rootReal = canonicalizePath(root.path);
564
602
  return pathIsInside(rootReal, anchor) ? anchor : rootReal;
565
603
  }
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { safeResourceWrite } from "../../../core/safe-resource-write.js";
4
4
  import { clioConfigDir } from "../../../core/xdg.js";
5
+ import { distinctiveTokens, normalize } from "./lexical-match.js";
5
6
  import type { MarketplaceSkill } from "./marketplace.js";
6
7
 
7
8
  /**
@@ -26,61 +27,6 @@ export interface PromotionMatch {
26
27
  matchedTrigger?: string;
27
28
  }
28
29
 
29
- const TOKEN_STOPWORDS = new Set([
30
- "the",
31
- "a",
32
- "an",
33
- "and",
34
- "or",
35
- "for",
36
- "with",
37
- "this",
38
- "that",
39
- "when",
40
- "what",
41
- "how",
42
- "into",
43
- "from",
44
- "use",
45
- "used",
46
- "using",
47
- "one",
48
- "not",
49
- "skill",
50
- "skills",
51
- "clio",
52
- "please",
53
- "can",
54
- "you",
55
- "should",
56
- "would",
57
- "about",
58
- "need",
59
- "want",
60
- "help",
61
- "make",
62
- "file",
63
- "files",
64
- "code",
65
- ]);
66
-
67
- function normalize(text: string): string {
68
- return text
69
- .toLowerCase()
70
- .replace(/[^\p{L}\p{N}\s]/gu, " ")
71
- .replace(/\s+/g, " ")
72
- .trim();
73
- }
74
-
75
- /** Distinctive tokens: 4+ chars and outside the stopword set. */
76
- function distinctiveTokens(text: string): Set<string> {
77
- return new Set(
78
- normalize(text)
79
- .split(" ")
80
- .filter((token) => token.length >= 4 && !TOKEN_STOPWORDS.has(token)),
81
- );
82
- }
83
-
84
30
  const TRIGGER_SCORE = 4;
85
31
  /** Distinct description/name token overlaps required to fire without a trigger. */
86
32
  const TOKEN_FIRE_THRESHOLD = 3;
@@ -101,28 +101,58 @@ export interface SkillDriftReport {
101
101
  * means nothing recorded a hash for this skill; drift evidence is opt-in and
102
102
  * never a gate, and skills still pass the normal tool safety gates either way.
103
103
  */
104
- export function checkSkillDrift(
105
- skill: { name: string; normalizedHash: string; provenance?: { installedHash?: string } },
104
+ export function checkSkillDrift(skill: SkillDriftSubject, cwd: string): SkillDriftReport | null {
105
+ return checkSkillDriftBatch([skill], cwd).get(skill.name) ?? null;
106
+ }
107
+
108
+ /** The fields a drift comparison reads. Structural, so any loaded skill satisfies it. */
109
+ export interface SkillDriftSubject {
110
+ name: string;
111
+ normalizedHash: string;
112
+ provenance?: { installedHash?: string };
113
+ }
114
+
115
+ /**
116
+ * The same comparison for many skills, resolving and parsing the pin manifest
117
+ * once.
118
+ *
119
+ * Activation checks one skill and can afford to re-read the manifest each time.
120
+ * A listing checks every row, and doing that through the single-skill entry
121
+ * point would re-stat up to four candidate paths and re-parse the YAML once per
122
+ * skill. {@link checkSkillDrift} is the single-skill case of this function
123
+ * rather than a parallel implementation, so the listing and the activation can
124
+ * never disagree about a verdict.
125
+ *
126
+ * Skills with nothing recorded for them are absent from the map rather than
127
+ * present with a null: an entry means a comparison happened.
128
+ */
129
+ export function checkSkillDriftBatch(
130
+ skills: ReadonlyArray<SkillDriftSubject>,
106
131
  cwd: string,
107
- ): SkillDriftReport | null {
108
- const actual = skill.normalizedHash.toLowerCase();
132
+ ): Map<string, SkillDriftReport> {
133
+ const reports = new Map<string, SkillDriftReport>();
134
+ if (skills.length === 0) return reports;
109
135
  const manifestPath = resolveSkillPinManifestPath(cwd);
110
136
  const manifest = manifestPath === null ? null : loadSkillPinManifest(manifestPath);
111
- const pinned = manifest?.get(skill.name);
112
- if (pinned !== undefined) {
113
- return {
114
- verdict: pinned.sha256 === actual ? "match" : "mismatch",
115
- authority: "pinned-manifest",
116
- expected: pinned.sha256,
117
- };
137
+ for (const skill of skills) {
138
+ const actual = skill.normalizedHash.toLowerCase();
139
+ const pinned = manifest?.get(skill.name);
140
+ if (pinned !== undefined) {
141
+ reports.set(skill.name, {
142
+ verdict: pinned.sha256 === actual ? "match" : "mismatch",
143
+ authority: "pinned-manifest",
144
+ expected: pinned.sha256,
145
+ });
146
+ continue;
147
+ }
148
+ const recorded = skill.provenance?.installedHash?.trim().toLowerCase();
149
+ if (recorded !== undefined && /^[0-9a-f]{64}$/.test(recorded)) {
150
+ reports.set(skill.name, {
151
+ verdict: recorded === actual ? "match" : "mismatch",
152
+ authority: "install-record",
153
+ expected: recorded,
154
+ });
155
+ }
118
156
  }
119
- const recorded = skill.provenance?.installedHash?.trim().toLowerCase();
120
- if (recorded !== undefined && /^[0-9a-f]{64}$/.test(recorded)) {
121
- return {
122
- verdict: recorded === actual ? "match" : "mismatch",
123
- authority: "install-record",
124
- expected: recorded,
125
- };
126
- }
127
- return null;
157
+ return reports;
128
158
  }
@@ -98,6 +98,7 @@ function baseClassify(tool: string): ActionClass | null {
98
98
  case ToolNames.WebFetch:
99
99
  case ToolNames.Git:
100
100
  case ToolNames.CodeNav:
101
+ case ToolNames.SelfCompact:
101
102
  case ToolNames.Context:
102
103
  // clio_docs and clio_library read Clio's bundled documentation and the
103
104
  // recipe catalog; data streams structured files. None writes.
@@ -185,7 +185,10 @@ export function serializeConversation(entries: ReadonlyArray<SessionEntry>): str
185
185
  }
186
186
  // custom, modelChange, thinkingLevelChange, fileEntry, sessionInfo,
187
187
  // protectedArtifact, taskLedger, decisionLedger, and workerRun do not
188
- // project into the serialized conversation.
188
+ // project into the serialized conversation. Neither do handoffTransaction
189
+ // and continuityCommit: this is an allowlist, so transaction JSON can
190
+ // never reach the summarization prompt as if it were conversation, and
191
+ // the accepted note is never regenerated from summarizer prose.
189
192
  }
190
193
  return parts.join("\n\n");
191
194
  }
@@ -1,3 +1,4 @@
1
+ import type { ContinuityCheckpointPayload } from "../continuity/contract.js";
1
2
  /**
2
3
  * Compaction orchestration.
3
4
  *
@@ -30,7 +31,7 @@ import {
30
31
  import { serializeConversation } from "./branch-summary.js";
31
32
  import { findCutPoint } from "./cut-point.js";
32
33
  import { DEFAULT_KEEP_RECENT_TOKENS, DEFAULT_RESERVE_TOKENS } from "./defaults.js";
33
- import { calculateContextTokens, getLastAssistantUsage } from "./tokens.js";
34
+ import { type ContinuityNoteCost, calculateContextTokens, getLastAssistantUsage } from "./tokens.js";
34
35
 
35
36
  interface FileOperations {
36
37
  read: Set<string>;
@@ -144,6 +145,11 @@ export interface CompactionCallObservation {
144
145
  }
145
146
 
146
147
  export interface CompactInput {
148
+ /** Synchronous admission after exact request-fit, before each provider call. */
149
+ beforeSummaryCall?: (() => void) | undefined;
150
+ checkpointForSummary?:
151
+ | ((summaryRef: string, tokensBefore: number, tokensAfter: number) => ContinuityCheckpointPayload)
152
+ | undefined;
147
153
  /** Ordered active-path session entries to compact; returned cut indexes address this same array. */
148
154
  entries: ReadonlyArray<SessionEntry>;
149
155
  /** Authoritative main-agent selection; omitted for unknown legacy state. */
@@ -175,6 +181,15 @@ export interface CompactInput {
175
181
  preserveUserTurnId?: string;
176
182
  /** Accounting observer; called once per invoked stream, including failures. */
177
183
  onCall?: (call: CompactionCallObservation) => void;
184
+ /**
185
+ * The continuity note replay projects, from `continuityProjectionTokens` and
186
+ * the projection's `noteAnchorTurnId`. The note survives the reduction, so it
187
+ * is on both sides of the before/after comparison; counting it needs the
188
+ * session and fork facts the caller has and this module does not, and the
189
+ * anchor id is what keeps it from being counted twice once a provider usage
190
+ * anchor already includes it.
191
+ */
192
+ continuityNote?: ContinuityNoteCost;
178
193
  }
179
194
 
180
195
  export interface CompactResult {
@@ -675,6 +690,7 @@ async function runSummaryStream(
675
690
  }
676
691
 
677
692
  input.signal?.throwIfAborted();
693
+ input.beforeSummaryCall?.();
678
694
  const timestamp = new Date().toISOString();
679
695
  const started = performance.now();
680
696
  let usage: unknown;
@@ -804,7 +820,7 @@ export async function compact(input: CompactInput): Promise<CompactResult> {
804
820
  const usageStart = prevCompactionIndex >= 0 ? prevCompactionIndex : 0;
805
821
  const usageEntries = entries.slice(usageStart);
806
822
  const lastUsage = getLastAssistantUsage(usageEntries);
807
- const tokensBefore = calculateContextTokens(usageEntries, lastUsage);
823
+ const tokensBefore = calculateContextTokens(usageEntries, lastUsage, input.continuityNote);
808
824
  const rawCut = findCutPoint(entries, keepRecentTokens, { startIndex: boundaryStart });
809
825
  const cut =
810
826
  protectedStart !== null && rawCut.firstKeptEntryIndex > protectedStart
@@ -74,6 +74,12 @@ function isValidCutPoint(entry: SessionEntry): boolean {
74
74
  case "workerRun":
75
75
  case "contextEviction":
76
76
  case "contextRecall":
77
+ // Transaction bookkeeping is not a place the conversation can be sliced.
78
+ // A cut landing on a handoff record would put the summary boundary in the
79
+ // middle of a durable transaction, and the records carry no conversation
80
+ // text for the retained suffix to be about.
81
+ case "handoffTransaction":
82
+ case "continuityCommit":
77
83
  return false;
78
84
  }
79
85
  }
@@ -193,7 +199,20 @@ export function findCutPoint(
193
199
  prev.kind === "bashExecution" ||
194
200
  prev.kind === "custom" ||
195
201
  prev.kind === "skillActivation" ||
196
- prev.kind === "branchSummary"
202
+ prev.kind === "branchSummary" ||
203
+ // Stop before a continuity record rather than widening onto it. The
204
+ // widening exists so bookkeeping rides with the retained suffix, but
205
+ // the cut index also names `firstKeptTurnId`, which becomes the new
206
+ // summary's structural parent. Parking that on a transaction record
207
+ // would anchor the summary to a non-message, and `entryBelongsToPath`
208
+ // resolves a summary through its anchor: on a branched session the
209
+ // summary would then be filtered off its own active path. Continuity
210
+ // records ahead of the cut lose nothing by being on the summarized
211
+ // side, because `serializeConversation` never projects them into the
212
+ // prompt and the fold reads the full pre-cut ledger rather than the
213
+ // replay slice.
214
+ prev.kind === "handoffTransaction" ||
215
+ prev.kind === "continuityCommit"
197
216
  ) {
198
217
  break;
199
218
  }
@@ -13,6 +13,7 @@
13
13
 
14
14
  import type { Usage } from "../../../engine/types.js";
15
15
  import { ceilChars, contentChars, estimateAgentMessageTokens } from "../context-accounting.js";
16
+ import { HANDOFF_RECOVERY_REQUEST_CUSTOM_TYPE } from "../continuity/operator-request.js";
16
17
  import type {
17
18
  BashExecutionEntry,
18
19
  BranchSummaryEntry,
@@ -53,6 +54,11 @@ function estimateBashExecution(entry: BashExecutionEntry): number {
53
54
 
54
55
  function estimateCustom(entry: CustomEntry): number {
55
56
  if (entry.data === undefined) return 0;
57
+ // The reserved operator recovery request is a control record, not model text.
58
+ // Model replay skips it by design (it is never turned into a user turn), so
59
+ // charging its data to the context budget would claim the model can see a
60
+ // record it is never shown.
61
+ if (entry.customType === HANDOFF_RECOVERY_REQUEST_CUSTOM_TYPE) return 0;
56
62
  return ceilChars(contentChars(entry.data));
57
63
  }
58
64
 
@@ -100,6 +106,14 @@ export function estimateTokens(entry: SessionEntry): number {
100
106
  // projection accounts for marker cost on the projected messages.
101
107
  case "contextEviction":
102
108
  case "contextRecall":
109
+ // Continuity bookkeeping. The transaction chain and the commit payload
110
+ // are never model text, and a carried payload is a copy of facts the
111
+ // projection already holds: charging every copy would price one accepted
112
+ // note three times over three summary cycles. The single rendered note
113
+ // the projection does emit is priced once, by the caller that resolved
114
+ // it, through `continuityNoteTokens` on `calculateContextTokens`.
115
+ case "handoffTransaction":
116
+ case "continuityCommit":
103
117
  return 0;
104
118
  }
105
119
  }
@@ -152,8 +166,27 @@ function findLastAssistantUsageIndex(entries: ReadonlyArray<SessionEntry>): numb
152
166
  * accurate for sessions whose most recent assistant turn carried real
153
167
  * provider usage data.
154
168
  */
155
- export function calculateContextTokens(entries: ReadonlyArray<SessionEntry>, lastUsage?: Usage): number {
169
+ /**
170
+ * The continuity note a replay projects, for callers pricing a whole prompt.
171
+ *
172
+ * `tokens` comes from `continuityProjectionTokens`, and `anchorTurnId` is the
173
+ * ledger record that introduced the transaction. Selecting which note gets
174
+ * projected needs session and fork facts a token estimator does not have, so
175
+ * the caller resolves it; a second selection rule here would be free to drift
176
+ * from the fold's.
177
+ */
178
+ export interface ContinuityNoteCost {
179
+ tokens: number;
180
+ anchorTurnId: string | null;
181
+ }
182
+
183
+ export function calculateContextTokens(
184
+ entries: ReadonlyArray<SessionEntry>,
185
+ lastUsage?: Usage,
186
+ continuityNote?: ContinuityNoteCost,
187
+ ): number {
156
188
  const usage = lastUsage ?? getLastAssistantUsage(entries);
189
+ const noteTokens = continuityNote?.tokens ?? 0;
157
190
  if (usage) {
158
191
  const anchorTokens = usageTotalTokens(usage);
159
192
  const anchorIndex = findLastAssistantUsageIndex(entries);
@@ -162,9 +195,21 @@ export function calculateContextTokens(entries: ReadonlyArray<SessionEntry>, las
162
195
  const entry = entries[i];
163
196
  if (entry) trailing += estimateTokens(entry);
164
197
  }
165
- return anchorTokens + trailing;
198
+ // The anchor is a real provider prompt count. Once a continuation has run
199
+ // with the note in front of it, that measurement already contains the note,
200
+ // and adding it again would inflate `tokensBefore` a little more on every
201
+ // compaction cycle. Only a note introduced *after* the anchored call is new
202
+ // material. An unlocatable anchor record is treated as new, which
203
+ // over-counts by one note rather than hiding it.
204
+ const introducedAt =
205
+ continuityNote?.anchorTurnId == null
206
+ ? -1
207
+ : entries.findIndex((entry) => entry.turnId === continuityNote.anchorTurnId);
208
+ const alreadyMeasured = introducedAt >= 0 && introducedAt <= anchorIndex;
209
+ return anchorTokens + trailing + (alreadyMeasured ? 0 : noteTokens);
166
210
  }
167
- let total = 0;
211
+ // With no anchor everything is estimated, so the note is always new material.
212
+ let total = noteTokens;
168
213
  for (const entry of entries) total += estimateTokens(entry);
169
214
  return total;
170
215
  }
@@ -14,6 +14,7 @@ export interface AgentContextEstimateInput {
14
14
  messages: ReadonlyArray<AgentMessage>;
15
15
  systemPrompt?: string;
16
16
  pendingUserText?: string;
17
+ pendingUserImages?: ReadonlyArray<unknown>;
17
18
  tools?: ReadonlyArray<unknown>;
18
19
  }
19
20
 
@@ -253,7 +254,13 @@ export function estimateAgentContextBreakdown(input: AgentContextEstimateInput):
253
254
  (sum, message) => sum + (message.role === "system" ? 0 : estimateAgentMessageTokens(message)),
254
255
  0,
255
256
  ),
256
- pendingUserTokens: input.pendingUserText ? ceilChars(input.pendingUserText.length) : 0,
257
+ pendingUserTokens:
258
+ input.pendingUserText !== undefined
259
+ ? estimateAgentMessageTokens({
260
+ role: "user",
261
+ content: [{ type: "text", text: input.pendingUserText }, ...(input.pendingUserImages ?? [])],
262
+ })
263
+ : 0,
257
264
  toolSchemaTokens: (input.tools ?? []).reduce<number>((sum, tool) => sum + ceilChars(toolSchemaChars(tool)), 0),
258
265
  };
259
266
  }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Summary carry and missing-commit reconstruction (CONTRACTS.md §3.5, §6).
3
+ *
4
+ * A later summary embeds the current validated folded payload: the same
5
+ * identity, the same agent-authored note, the original policy, the original
6
+ * commit envelope/outcome/refs and spent attempts, with `state` advanced to the
7
+ * latest delivery, acknowledgement, pause or explicit resume. It never
8
+ * manufactures a commit and never turns an eviction-only outcome into a
9
+ * summarized one.
10
+ *
11
+ * Both functions are projections of a validated fold. Neither reads a clock,
12
+ * mints an id, or writes anything.
13
+ */
14
+
15
+ import type { ContinuityCheckpointPayload, ContinuityFoldResult, MissingCommitReconstruction } from "./contract.js";
16
+ import { HANDOFF_SCHEMA_VERSION } from "./contract.js";
17
+ import { isContinuityCheckpointPayload } from "./validate.js";
18
+
19
+ /**
20
+ * The payload a summary written now should carry, or null when this fold has
21
+ * nothing durable to carry.
22
+ *
23
+ * A carry requires a commit: before one exists there is no checkpoint to
24
+ * project, only an in-flight transaction whose own records are the evidence.
25
+ * The result is validated before being returned, so a fold that produced a
26
+ * shape this core would refuse to read back cannot be written out.
27
+ */
28
+ export function continuityPayloadFromFold(fold: ContinuityFoldResult): ContinuityCheckpointPayload | null {
29
+ // Only a validated fold may be published. This is not the same as execution
30
+ // authority: a paused or inherited historical transaction is recall-only and
31
+ // still carries perfectly well, while a chain with conflicting copies or a
32
+ // malformed record has nothing trustworthy to write forward.
33
+ if (!fold.validated) return null;
34
+ if (!fold.identity || !fold.accepted || !fold.policy || !fold.commit || !fold.state) return null;
35
+ const payload: ContinuityCheckpointPayload = {
36
+ schemaVersion: HANDOFF_SCHEMA_VERSION,
37
+ identity: fold.identity,
38
+ accepted: fold.accepted,
39
+ policy: fold.policy,
40
+ commit: fold.commit,
41
+ state: fold.state,
42
+ };
43
+ return isContinuityCheckpointPayload(payload) ? payload : null;
44
+ }
45
+
46
+ /**
47
+ * The commit a validated carry proves existed, when this projection does not
48
+ * contain it.
49
+ *
50
+ * The fold has already refused to conclude absence while unreadable records
51
+ * could hide the commit, so this is a projection of that decision rather than a
52
+ * second search. The reconstruction reuses the reserved entry id, parent and
53
+ * timestamp, which is what keeps recovery from minting a second commit
54
+ * identity, and returns the later carried head separately so restoring it after
55
+ * the append cannot regress an acknowledgement or a pause to `ready`.
56
+ */
57
+ export function missingCommitFromCarry(fold: ContinuityFoldResult): MissingCommitReconstruction | null {
58
+ return fold.validated ? fold.missingCommit : null;
59
+ }