@iowarp/clio-coder 0.5.1 → 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 (263) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/CHANGELOG.md +24 -0
  3. package/CONTRIBUTING.md +9 -0
  4. package/README.md +3 -3
  5. package/ROADMAP.md +30 -5
  6. package/dist/{acp-X3N7JDGS.js → acp-F6PGOFZ4.js} +2 -2
  7. package/dist/{agents-UYGSMAWR.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-DGMY2DWE.js → chunk-23UIXH6Z.js} +3 -3
  11. package/dist/{chunk-QTYRMBW6.js → chunk-36XVK5P5.js} +5 -5
  12. package/dist/{chunk-BIQLF3NZ.js → chunk-3DZ4LPLS.js} +6 -6
  13. package/dist/{chunk-7VNLPEUG.js → chunk-3GSPCWFX.js} +12 -12
  14. package/dist/{chunk-CRHPCPZP.js → chunk-4KC4CHKJ.js} +2 -2
  15. package/dist/{chunk-M5FPIZQW.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-A7G7OUND.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-GW2KVAVI.js → chunk-AA3GRZWS.js} +2 -2
  27. package/dist/{chunk-6B76U65H.js → chunk-ADO3OQIC.js} +3 -3
  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-AKQ3N3L4.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-7KI4ZSEB.js → chunk-ELDJWJKM.js} +97 -14
  38. package/dist/{chunk-GCQFN7YI.js → chunk-EMYEQLWO.js} +2 -2
  39. package/dist/{chunk-7FHSA7VB.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-GGEEG7CD.js → chunk-FOSRJ2VZ.js} +4 -4
  45. package/dist/{chunk-ZARZ4POR.js → chunk-G5ZDQQE3.js} +3 -3
  46. package/dist/{chunk-QM7HFPQQ.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-WAISUJNQ.js → chunk-KWMFC7FN.js} +3 -3
  58. package/dist/{chunk-4D7QM7S5.js → chunk-LXYHCCIZ.js} +9 -9
  59. package/dist/{chunk-NFSK2VNU.js → chunk-LZMTVVN4.js} +2 -2
  60. package/dist/{chunk-ZNTGRDFA.js → chunk-M5PMRZSR.js} +123 -37
  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-4SYHGVDE.js → chunk-OQNCLZI4.js} +4 -4
  66. package/dist/{chunk-EG4ARXPC.js → chunk-OX6QTA4N.js} +3 -3
  67. package/dist/{chunk-P4GNZPXO.js → chunk-OYUS3UZP.js} +2 -2
  68. package/dist/{chunk-VWKR2NN5.js → chunk-PF36KOGI.js} +290 -111
  69. package/dist/{chunk-LFYA3HYH.js → chunk-PIU6BXXW.js} +2 -2
  70. package/dist/{chunk-EKV4UBCM.js → chunk-PZBQUJ2F.js} +2 -2
  71. package/dist/{chunk-7RET77VB.js → chunk-Q2HKY32Y.js} +2 -2
  72. package/dist/{chunk-VMTCZ2GR.js → chunk-Q4RMNWMZ.js} +4 -4
  73. package/dist/chunk-Q6FRR3AQ.js +48 -0
  74. package/dist/{chunk-6RJMYYIE.js → chunk-QJSIAJBY.js} +3 -3
  75. package/dist/{chunk-DOUISWEE.js → chunk-QOJ4ERMU.js} +5 -5
  76. package/dist/{chunk-IFPDAD7R.js → chunk-RWSRKXLT.js} +3 -3
  77. package/dist/{chunk-T33IYTZM.js → chunk-SAASVMIY.js} +3 -3
  78. package/dist/{chunk-2DTIWSAW.js → chunk-SEQDH6JC.js} +3 -3
  79. package/dist/{chunk-BRCOSE7O.js → chunk-SZUO5BSE.js} +2 -2
  80. package/dist/{chunk-HOEDA42N.js → chunk-T7HZ7KJF.js} +2 -2
  81. package/dist/{chunk-UCINIRHW.js → chunk-TPCZXWLT.js} +3 -3
  82. package/dist/{chunk-ZUKUCZYZ.js → chunk-TQ2KTH4A.js} +2 -2
  83. package/dist/{chunk-4WOOTKFD.js → chunk-U3FETNQB.js} +341 -20
  84. package/dist/{chunk-5B3RTYAK.js → chunk-UGDSB4AN.js} +2 -2
  85. package/dist/{chunk-VKHLUZNO.js → chunk-UZ7YBL43.js} +3 -3
  86. package/dist/{chunk-OXWRE2QJ.js → chunk-VKMSEO7Y.js} +4 -4
  87. package/dist/{chunk-FBXTHPNZ.js → chunk-VLX5VZ35.js} +5 -5
  88. package/dist/{chunk-KIAUZGKF.js → chunk-WAGBMMNX.js} +5 -5
  89. package/dist/{chunk-BFOSV5EZ.js → chunk-WX2YCH7F.js} +2 -2
  90. package/dist/{chunk-SAL2SBP4.js → chunk-XBIGUILU.js} +25 -10
  91. package/dist/{chunk-XOH3SD7H.js → chunk-XKYBFRWR.js} +5 -5
  92. package/dist/{chunk-U5QU5ZOD.js → chunk-XYPWFSU5.js} +3 -3
  93. package/dist/{chunk-YBUECLAF.js → chunk-YEJQDODA.js} +2 -2
  94. package/dist/{chunk-YQ6XEFVK.js → chunk-Z74OGONW.js} +39 -7
  95. package/dist/{chunk-I4ELN5BX.js → chunk-ZTNOEJRI.js} +2 -2
  96. package/dist/cli/index.js +25 -25
  97. package/dist/{clio-Z3JTEL2I.js → clio-4IKFE25F.js} +2 -2
  98. package/dist/{clio-context-tools-GR4OHFSO.js → clio-context-tools-OHJXYX4R.js} +25 -23
  99. package/dist/{code-nav-4X3OI6DK.js → code-nav-XPM3MY5I.js} +7 -7
  100. package/dist/{config-QYIRZHI7.js → config-343YS25T.js} +42 -40
  101. package/dist/{config-graph-TS3SGGQW.js → config-graph-WI263KDX.js} +42 -40
  102. package/dist/{configure-OX7KVJTQ.js → configure-GZDG4HER.js} +21 -21
  103. package/dist/{context-2BNF6HTP.js → context-4OFO7N7Y.js} +12 -12
  104. package/dist/{context-GTMUGAR4.js → context-JLK2RLOG.js} +41 -39
  105. package/dist/{context-NNLBES2Y.js → context-O6DIWCF6.js} +23 -21
  106. package/dist/{context-clear-FVJQBBL7.js → context-clear-MR7KOGQI.js} +41 -39
  107. package/dist/{context-working-set-MOE72SD6.js → context-working-set-YZAD3KWO.js} +14 -13
  108. package/dist/{data-tool-JJPBGMQJ.js → data-tool-EKD7DFHG.js} +7 -7
  109. package/dist/{detail-QZM3QAAU.js → detail-YZPHFEVJ.js} +42 -40
  110. package/dist/{dispatch-runner-2KSQO5FU.js → dispatch-runner-VKVXVTWJ.js} +43 -41
  111. package/dist/{doctor-2U6YR5NH.js → doctor-53AVQXAO.js} +16 -15
  112. package/dist/{doctor-deep-2EJQSHIZ.js → doctor-deep-BLRB2NCG.js} +5 -5
  113. package/dist/{eval-ZKK77JWZ.js → eval-P3BQSLQE.js} +21 -20
  114. package/dist/{evidence-POSEQW4Q.js → evidence-6LLI6SZH.js} +43 -41
  115. package/dist/{evidence-FMP7KZZ2.js → evidence-HTN3JLUO.js} +41 -39
  116. package/dist/{evidence-PJTOE7JA.js → evidence-NJRE3R5H.js} +41 -39
  117. package/dist/{evolve-JT6PAMZE.js → evolve-WWZHJVJP.js} +41 -39
  118. package/dist/{fleet-4XR2ZEOF.js → fleet-GGZ4BVJE.js} +63 -61
  119. package/dist/{fleet-6JJKNIMV.js → fleet-QG3IIWWM.js} +43 -41
  120. package/dist/{fleet-commands-L6WFT7KZ.js → fleet-commands-J2ZIKISQ.js} +7 -7
  121. package/dist/{fleet-decisions-BTQMWU5A.js → fleet-decisions-Y2Q7375Q.js} +8 -8
  122. package/dist/{fleet-graph-553DZ67Q.js → fleet-graph-TCBCCCBP.js} +11 -11
  123. package/dist/{fleet-inspect-SWJMFVDP.js → fleet-inspect-KOLKBC56.js} +43 -41
  124. package/dist/{fleet-preflight-KSIHWQAU.js → fleet-preflight-44UNCC4Y.js} +26 -24
  125. package/dist/{fleet-validate-7NLAE26R.js → fleet-validate-K2FXRZVA.js} +13 -13
  126. package/dist/{fleet-verify-QM6XU2KL.js → fleet-verify-AS444WDW.js} +41 -39
  127. package/dist/{fleet-view-RLKFHFCM.js → fleet-view-X4JU4J6O.js} +42 -40
  128. package/dist/gui/ops-worker.js +9 -9
  129. package/dist/gui/reads-worker.js +9 -8
  130. package/dist/{init-VBUPHABR.js → init-VSOWFHVL.js} +53 -51
  131. package/dist/{interactive-JCVBPVYT.js → interactive-JRCBENLZ.js} +154 -92
  132. package/dist/{interop-J7UOBOSF.js → interop-JR6BCY7R.js} +10 -10
  133. package/dist/{inventory-54G7FSVY.js → inventory-3JUGLVJI.js} +42 -40
  134. package/dist/{library-ROZAMUIL.js → library-7BOH3Y7N.js} +15 -15
  135. package/dist/{library-B3UPGY4V.js → library-7PFONXUI.js} +9 -9
  136. package/dist/{library-MXQA5DLT.js → library-TUYZMFIA.js} +7 -7
  137. package/dist/{library-import-2LTWOVBD.js → library-import-TJVXKLDK.js} +10 -10
  138. package/dist/{library-inventory-LJ3XHLWO.js → library-inventory-MD3AYTP4.js} +9 -9
  139. package/dist/{library-validation-DARHHUNW.js → library-validation-ZDLITEA3.js} +7 -7
  140. package/dist/{memory-5BPQSSOP.js → memory-Q2YVOKK5.js} +41 -39
  141. package/dist/{models-R6HILX6X.js → models-M7BOCW7F.js} +17 -16
  142. package/dist/{monitor-HVRKSFEQ.js → monitor-2HD3MC67.js} +46 -44
  143. package/dist/{orchestrator-OR7V4YWK.js → orchestrator-OUW3ZN6Y.js} +1890 -222
  144. package/dist/{panes-A743ONZ5.js → panes-TTPPSYCL.js} +3 -3
  145. package/dist/{preload-KLXFOX2F.js → preload-A47C2NUF.js} +41 -39
  146. package/dist/{providers-CMETPZNS.js → providers-T43W7EL4.js} +4 -4
  147. package/dist/{resources-FY6XYFN2.js → resources-TP6X2V6W.js} +20 -11
  148. package/dist/{run-UIDRUYAY.js → run-LDSBPMCS.js} +60 -58
  149. package/dist/{share-CAFMMQLM.js → share-URHKKMHR.js} +9 -9
  150. package/dist/{skills-ERKQUC7L.js → skills-E477MOOO.js} +13 -11
  151. package/dist/{skills-eval-JDAXOPWC.js → skills-eval-AIBR2L7H.js} +582 -92
  152. package/dist/{skills-inventory-TVUYJDVN.js → skills-inventory-HLMKY3R5.js} +13 -11
  153. package/dist/{slash-commands-MNKQ5QP3.js → slash-commands-LIIZKA5Y.js} +26 -24
  154. package/dist/{startup-background-PJMWTKVO.js → startup-background-JR2KDK6R.js} +42 -40
  155. package/dist/{steer-KKYXJSH4.js → steer-D7RTAPCM.js} +3 -3
  156. package/dist/{system-IVVZRKGI.js → system-X2P5N4IY.js} +14 -13
  157. package/dist/{targets-RFRUMCQ5.js → targets-VFFAKUFM.js} +25 -24
  158. package/dist/{tasks-6L2JJRC5.js → tasks-ERAWZA22.js} +6 -6
  159. package/dist/{trace-LA5MVR4Y.js → trace-MA5BT7XY.js} +3 -3
  160. package/dist/{usage-LA33SG3R.js → usage-WYFY2L6M.js} +49 -47
  161. package/dist/{verifiers-ECA5GIHL.js → verifiers-TYIW2XKF.js} +7 -7
  162. package/dist/{verify-R6QQCCEB.js → verify-242UFYTP.js} +6 -6
  163. package/dist/{web-fetch-PVZNMBOB.js → web-fetch-LRAGYU6Y.js} +3 -3
  164. package/dist/{wiki-generate-HL73TICA.js → wiki-generate-OFKOX2T6.js} +52 -50
  165. package/dist/worker/entry.js +30 -28
  166. package/docs/architecture/context-engine.md +2 -0
  167. package/docs/guide/context-continuity.md +49 -0
  168. package/docs/guide/proactive-memory.md +3 -0
  169. package/library/registry.yaml +292 -32
  170. package/library/skills/README.md +27 -2
  171. package/library/skills/context/context-handoff/SKILL.md +10 -4
  172. package/library/skills/meta/clio-coder-dev/SKILL.md +92 -88
  173. package/library/skills/meta/clio-coder-dev/evals.md +51 -46
  174. package/library/skills/meta/clio-coder-dev/plugin.json +2 -2
  175. package/library/skills/meta/clio-coder-dev/references/change-map.md +49 -0
  176. package/library/skills/meta/clio-coder-test/SKILL.md +89 -142
  177. package/library/skills/meta/clio-coder-test/evals.md +58 -62
  178. package/library/skills/meta/clio-coder-test/plugin.json +2 -2
  179. package/library/skills/meta/clio-coder-test/references/harness.md +3 -3
  180. package/library/skills/meta/clio-coder-test/references/lifecycle-validation.md +30 -0
  181. package/library/skills/meta/clio-coder-test/references/test-map.md +60 -89
  182. package/library/skills/registry.yaml +5 -5
  183. package/library/skills/skill-marketplace.json +12 -14
  184. package/package.json +1 -1
  185. package/src/cli/skills-eval.ts +1064 -54
  186. package/src/core/clio-repo.ts +3 -0
  187. package/src/core/tool-names.ts +1 -0
  188. package/src/domains/context/budget/inspection.ts +9 -0
  189. package/src/domains/context/budget/live-view.ts +418 -0
  190. package/src/domains/context/budget/pressure.ts +393 -0
  191. package/src/domains/context/budget/request-fit.ts +15 -0
  192. package/src/domains/evidence/build.ts +24 -0
  193. package/src/domains/memory/commit-state.ts +160 -0
  194. package/src/domains/memory/operations.ts +18 -10
  195. package/src/domains/memory/prompt-cache.ts +95 -0
  196. package/src/domains/memory/prompt-section.ts +50 -7
  197. package/src/domains/memory/relevance.ts +93 -0
  198. package/src/domains/memory/restoration.ts +78 -0
  199. package/src/domains/memory/store.ts +39 -1
  200. package/src/domains/middleware/memory-intervention.ts +116 -24
  201. package/src/domains/prompts/compiler.ts +1 -1
  202. package/src/domains/prompts/extension.ts +38 -1
  203. package/src/domains/resources/index.ts +11 -0
  204. package/src/domains/resources/skills/catalog-view.ts +571 -0
  205. package/src/domains/resources/skills/lexical-match.ts +136 -0
  206. package/src/domains/resources/skills/loader.ts +38 -0
  207. package/src/domains/resources/skills/promotion.ts +1 -55
  208. package/src/domains/resources/skills/provenance-pin.ts +50 -20
  209. package/src/domains/safety/action-classifier.ts +1 -0
  210. package/src/domains/session/compaction/branch-summary.ts +4 -1
  211. package/src/domains/session/compaction/compact.ts +18 -2
  212. package/src/domains/session/compaction/cut-point.ts +20 -1
  213. package/src/domains/session/compaction/tokens.ts +48 -3
  214. package/src/domains/session/context-accounting.ts +8 -1
  215. package/src/domains/session/continuity/carry.ts +59 -0
  216. package/src/domains/session/continuity/contract.ts +592 -0
  217. package/src/domains/session/continuity/evidence.ts +290 -0
  218. package/src/domains/session/continuity/fold.ts +1075 -0
  219. package/src/domains/session/continuity/note.ts +79 -0
  220. package/src/domains/session/continuity/operator-request.ts +104 -0
  221. package/src/domains/session/continuity/persistence.ts +408 -0
  222. package/src/domains/session/continuity/ports.ts +231 -0
  223. package/src/domains/session/continuity/projection.ts +538 -0
  224. package/src/domains/session/continuity/validate.ts +352 -0
  225. package/src/domains/session/entries.ts +50 -3
  226. package/src/domains/session/index.ts +48 -0
  227. package/src/domains/session/migrations/index.ts +7 -3
  228. package/src/domains/session/tree/fork.ts +15 -1
  229. package/src/engine/agent.ts +133 -9
  230. package/src/engine/session.ts +13 -6
  231. package/src/entry/orchestrator.ts +114 -31
  232. package/src/interactive/chat-loop.ts +216 -60
  233. package/src/interactive/chat-renderer.ts +99 -8
  234. package/src/interactive/context-overlay.ts +24 -3
  235. package/src/interactive/continuity-controller.ts +526 -0
  236. package/src/interactive/footer/dashboard.ts +12 -1
  237. package/src/interactive/footer/pages.ts +15 -7
  238. package/src/interactive/footer/widgets.ts +32 -7
  239. package/src/interactive/interactive-application.ts +2 -0
  240. package/src/interactive/interactive-slash-runtime.ts +43 -6
  241. package/src/interactive/model-session-replay.ts +130 -3
  242. package/src/interactive/output-reserve.ts +35 -0
  243. package/src/interactive/overlay-session-lifecycle.ts +18 -11
  244. package/src/interactive/session-transcript.ts +2 -1
  245. package/src/interactive/slash-commands.ts +20 -1
  246. package/src/interactive/turn-context.ts +685 -62
  247. package/src/interactive/turn-middleware.ts +39 -6
  248. package/src/interactive/turn-persistence.ts +15 -2
  249. package/src/interactive/turn-runtime.ts +48 -5
  250. package/src/tools/agent-tools.ts +3 -1
  251. package/src/tools/bootstrap.ts +6 -0
  252. package/src/tools/builtin-tool-catalog.ts +9 -0
  253. package/src/tools/context/index.ts +148 -113
  254. package/src/tools/context/surface.ts +11 -7
  255. package/src/tools/core-bootstrap.ts +4 -0
  256. package/src/tools/observation.ts +8 -2
  257. package/src/tools/policy.ts +3 -0
  258. package/src/tools/self-compact.ts +31 -0
  259. package/src/tools/surface.ts +1 -0
  260. package/src/tools/tasks.ts +1 -1
  261. package/dist/chunk-3OODBJ45.js +0 -1325
  262. package/dist/chunk-CTFPFW3H.js +0 -44
  263. package/dist/chunk-I4Y4WKZR.js +0 -386
@@ -1,5 +1,6 @@
1
1
  import type { OutputStyle } from "../../core/defaults.js";
2
2
  import { ToolNames } from "../../core/tool-names.js";
3
+ import type { LiveBudgetView } from "../../domains/context/budget/live-view.js";
3
4
  import {
4
5
  type CostAggregate,
5
6
  formatCostAggregate,
@@ -99,6 +100,8 @@ export interface SessionFacts {
99
100
 
100
101
  /** Context engine telemetry. */
101
102
  export interface ContextEngineFacts {
103
+ /** Identifies the published accounting used by the meter; ledger stays diagnostic. */
104
+ budget?: Pick<LiveBudgetView, "revision" | "historical" | "inputSource">;
102
105
  label: string | null;
103
106
  used: number | null;
104
107
  contextWindow: number | null;
@@ -393,14 +396,36 @@ function contextComposition(context: ContextEngineFacts): {
393
396
  system: finiteNonNegative(breakdown?.systemPromptTokens),
394
397
  tools: finiteNonNegative(breakdown?.toolSchemaTokens),
395
398
  chat: finiteNonNegative(breakdown?.messageTokens) + finiteNonNegative(breakdown?.pendingUserTokens),
396
- free: window > 0 ? Math.max(0, window - used) : null,
399
+ free:
400
+ window > 0 && typeof context.used === "number" && Number.isFinite(context.used) ? Math.max(0, window - used) : null,
397
401
  };
398
402
  }
399
403
 
404
+ /** Short truthful occupancy label shared by compact and expanded surfaces. */
405
+ export function contextUsageText(context: ContextEngineFacts): string {
406
+ const used = context.budget ? context.used : (context.ledger?.usedTokens ?? context.used);
407
+ const window = context.budget ? context.contextWindow : (context.ledger?.contextWindow ?? context.contextWindow);
408
+ const source = context.budget?.inputSource === "historical" ? "saved " : context.budget && used !== null ? "~" : "";
409
+ return `${source}${used === null ? "?" : formatFooterTokens(used)} / ${window ? formatFooterTokens(window) : "?"}`;
410
+ }
411
+
412
+ /** Reads published numbers only; never refreshes accounting from a renderer. */
413
+ export function contextOccupancyBar(context: ContextEngineFacts, cells: number, theme: ClioTheme): string {
414
+ if (!context.budget && context.ledger) return renderContextMeterBar(context.ledger, cells, theme);
415
+ return buildSegmentedContextBar(
416
+ theme,
417
+ cells,
418
+ context.contextWindow ?? 0,
419
+ context.used === null ? undefined : contextBreakdownForBar(context),
420
+ );
421
+ }
422
+
400
423
  /** The live window after the percent; null when unknown or the row is too narrow to carry it. */
401
424
  export function compactContextWindowLabel(context: ContextEngineFacts, width: number): string | null {
402
425
  if (width < 72) return null;
403
- const window = finiteNonNegative(context.ledger?.contextWindow ?? context.contextWindow);
426
+ const window = finiteNonNegative(
427
+ context.budget ? context.contextWindow : (context.ledger?.contextWindow ?? context.contextWindow),
428
+ );
404
429
  return window > 0 ? formatFooterTokens(window) : null;
405
430
  }
406
431
 
@@ -427,7 +452,7 @@ export function compactSecondaryLine(
427
452
  const safeWidth = Math.max(1, Math.floor(width));
428
453
  const barCells = compactContextBarWidth(safeWidth);
429
454
  let left = "";
430
- if (context.ledger) {
455
+ if (context.ledger && !context.budget) {
431
456
  // The row follows the kv grammar: dim key, muted value. An unmeasured
432
457
  // percent renders the ?% placeholder dim because it is scaffolding for a
433
458
  // number that has not arrived, not a measurement.
@@ -437,7 +462,7 @@ export function compactSecondaryLine(
437
462
  );
438
463
  left = `${theme.fg("dim", "ctx")} ${renderContextMeterBar(context.ledger, barCells, theme)} ${percent}`;
439
464
  } else {
440
- left = buildSegmentedContextBar(theme, barCells, context.contextWindow ?? 0, contextBreakdownForBar(context));
465
+ left = `${context.budget?.inputSource === "historical" ? theme.fg("dim", "saved ") : ""}${contextOccupancyBar(context, barCells, theme)}`;
441
466
  }
442
467
  // A percentage alone does not say whether the window is 128k or 1M, so at
443
468
  // widths that can afford it the live window follows the percent. It comes
@@ -713,7 +738,7 @@ function ledgerBar(theme: ClioTheme, ledger: ContextLedger, cells: number): stri
713
738
  export function contextQuadrant(facts: ContextEngineFacts, options: ExpandedQuadrantOptions = {}): string[] {
714
739
  const theme = clioTheme();
715
740
  const ledger = facts.ledger ?? null;
716
- const hasLedger = ledger !== null && ledger.contextWindow > 0;
741
+ const hasLedger = !facts.budget && ledger !== null && ledger.contextWindow > 0;
717
742
  const barCells = expandedContextBarCells(options.width);
718
743
 
719
744
  let bar: string;
@@ -737,7 +762,7 @@ export function contextQuadrant(facts: ContextEngineFacts, options: ExpandedQuad
737
762
  ? theme.style("frame", `free ${formatFooterTokens(composition.free)}`, { dim: true })
738
763
  : null,
739
764
  ]);
740
- bar = buildSegmentedContextBar(theme, barCells, facts.contextWindow ?? 0, contextBreakdownForBar(facts));
765
+ bar = contextOccupancyBar(facts, barCells, theme);
741
766
  const filledChar = visibleWidth(GLYPH.contextFull) === 1 ? GLYPH.contextFull : GLYPH.barFull;
742
767
  const freeChar = visibleWidth(GLYPH.contextFree) === 1 ? GLYPH.contextFree : GLYPH.barEmpty;
743
768
  legendRows = [
@@ -752,7 +777,7 @@ export function contextQuadrant(facts: ContextEngineFacts, options: ExpandedQuad
752
777
  "Context",
753
778
  [
754
779
  statusRow(bar),
755
- kv("used", formatUsedWindow(usedTokens, windowTokens)),
780
+ kv("used", facts.budget ? contextUsageText(facts) : formatUsedWindow(usedTokens, windowTokens)),
756
781
  fill ? styledKv("fill", fill) : statusRow(null),
757
782
  chatFree ? styledKv("budget", chatFree) : statusRow(null),
758
783
  kv("compact", formatCompaction(facts)),
@@ -252,6 +252,7 @@ export interface InteractiveDeps {
252
252
  * engine, and persists a compactionSummary entry.
253
253
  */
254
254
  onCompact?: (instructions: string | undefined) => Promise<void>;
255
+ onRecoverHandoff?: (handoffId: string, action: "reduce" | "deliver") => Promise<void>;
255
256
  /** Run /context init for the current working directory. */
256
257
  onInit?: (options: InitCommandOptions, io?: RunIo) => Promise<void>;
257
258
  /** Run /context reset for the current working directory. */
@@ -827,6 +828,7 @@ export async function createInteractiveApplication(deps: InteractiveDeps): Promi
827
828
  ...(deps.onSelectModel ? { onSelectModel: deps.onSelectModel } : {}),
828
829
  ...(deps.onSetThinkingLevel ? { onSetThinkingLevel: deps.onSetThinkingLevel } : {}),
829
830
  ...(deps.onCompact ? { onCompact: deps.onCompact } : {}),
831
+ ...(deps.onRecoverHandoff ? { onRecoverHandoff: deps.onRecoverHandoff } : {}),
830
832
  ...(deps.onInit ? { onInit: deps.onInit } : {}),
831
833
  ...(deps.onContextClear ? { onContextClear: deps.onContextClear } : {}),
832
834
  ...(deps.onContextRefresh ? { onContextRefresh: deps.onContextRefresh } : {}),
@@ -36,6 +36,7 @@ import { appendNotice, appendOperatorAside, appendOperatorCommand, appendReferen
36
36
  import { runOperatorRecall } from "./context-recall-command.js";
37
37
  import { renderSessionHtml } from "./export-html/index.js";
38
38
  import { dateLocal } from "./format-time.js";
39
+ import { withContinuityReplay } from "./model-session-replay.js";
39
40
  import type { OracleDigestSources } from "./oracle.js";
40
41
  import type { PendingModelScope } from "./overlays/model-scope.js";
41
42
  import { renderDoctorReport } from "./renderers/doctor-report.js";
@@ -57,6 +58,15 @@ import type { WorkerEntryState } from "./worker-stream.js";
57
58
 
58
59
  const EXPORT_RENDER_WIDTH = 100;
59
60
 
61
+ /** Longest run of consecutive backticks in the rendered transcript, for fence sizing. */
62
+ function longestBacktickRun(lines: ReadonlyArray<string>): number {
63
+ let longest = 0;
64
+ for (const line of lines) {
65
+ for (const run of line.match(/`+/gu) ?? []) longest = Math.max(longest, run.length);
66
+ }
67
+ return longest;
68
+ }
69
+
60
70
  export interface InteractiveSlashSubmitExpansion {
61
71
  text: string;
62
72
  images: ImageContent[];
@@ -104,6 +114,7 @@ export interface InteractiveSlashRuntimeDeps {
104
114
  onSelectModel?: (ref: { target: string; model: string }, scope: "session" | "global") => void;
105
115
  onSetThinkingLevel?: (level: ThinkingLevel, scope?: "session" | "global") => void;
106
116
  onCompact?: (instructions: string | undefined) => Promise<void>;
117
+ onRecoverHandoff?: (handoffId: string, action: "reduce" | "deliver") => Promise<void>;
107
118
  onInit?: (options: InitCommandOptions, io?: RunIo) => Promise<void>;
108
119
  onContextClear?: (options: ContextClearCommandOptions, io?: RunIo) => Promise<void>;
109
120
  onContextRefresh?: (io?: RunIo) => Promise<void>;
@@ -584,6 +595,17 @@ export function createInteractiveSlashRuntime(deps: InteractiveSlashRuntimeDeps)
584
595
  deps.setEditorText(text);
585
596
  deps.requestRender();
586
597
  },
598
+ runHandoffRecovery: (handoffId, action) => {
599
+ if (!deps.onRecoverHandoff) {
600
+ appendCommandNotice("error", "[/context recover] native handoff recovery is unavailable");
601
+ return;
602
+ }
603
+ void deps
604
+ .onRecoverHandoff(handoffId, action)
605
+ .catch((error) =>
606
+ appendCommandNotice("error", `[/context recover] ${error instanceof Error ? error.message : String(error)}`),
607
+ );
608
+ },
587
609
  runCompact: (instructions) => {
588
610
  runCompactWithNotice(deps.onCompact, appendCommandNotice, instructions);
589
611
  },
@@ -608,10 +630,19 @@ export function createInteractiveSlashRuntime(deps: InteractiveSlashRuntimeDeps)
608
630
  // rehydrate exported it as ordinary history (issue #109). No session
609
631
  // contract means no pin can exist for this reader, so the file replays whole.
610
632
  const leafTurnId = deps.session?.tree(sessionId).leafId ?? null;
611
- rehydrateChatPanelFromTurns(exportPanel, turns, {
612
- unboundedToolBodies: true,
613
- ...(leafTurnId ? { activeLeafTurnId: leafTurnId } : {}),
614
- });
633
+ // The export shows the same continuity the model sees, resolved from
634
+ // the same ledger through the same helper, so the two cannot drift.
635
+ // It is labelled as assistant handoff text and is never rendered as an
636
+ // operator turn.
637
+ rehydrateChatPanelFromTurns(
638
+ exportPanel,
639
+ turns,
640
+ withContinuityReplay(
641
+ turns,
642
+ { unboundedToolBodies: true, ...(leafTurnId ? { activeLeafTurnId: leafTurnId } : {}) },
643
+ deps.session,
644
+ ),
645
+ );
615
646
  const ansiLines = exportPanel.render(EXPORT_RENDER_WIDTH);
616
647
  // The operator names this file by the day they ran the export, so the
617
648
  // date in it is their calendar date. The header below keeps the ISO
@@ -626,8 +657,14 @@ export function createInteractiveSlashRuntime(deps: InteractiveSlashRuntimeDeps)
626
657
  mkdirSync(resolve(target, ".."), { recursive: true });
627
658
  if (markdown) {
628
659
  const lines = ansiLines.map(stripTerminalSequences);
629
- const header = [`# Clio session ${sessionId}`, "", `Exported ${exportedAt.toISOString()}`, "", "```text"];
630
- writeFileSync(target, `${[...header, ...lines, "```", ""].join("\n")}`, "utf8");
660
+ // The fence has to be longer than the longest backtick run the
661
+ // transcript contains, or a body that includes its own fence closes
662
+ // the block early and the rest of the session renders as Markdown.
663
+ // A handoff note is assistant-authored prose that routinely carries
664
+ // fenced code, so this is reachable content, not a hypothetical.
665
+ const fence = "`".repeat(Math.max(3, longestBacktickRun(lines) + 1));
666
+ const header = [`# Clio session ${sessionId}`, "", `Exported ${exportedAt.toISOString()}`, "", `${fence}text`];
667
+ writeFileSync(target, `${[...header, ...lines, fence, ""].join("\n")}`, "utf8");
631
668
  } else {
632
669
  writeFileSync(target, renderSessionHtml({ sessionId, exportedAt: exportedAt.toISOString(), ansiLines }), "utf8");
633
670
  }
@@ -1,8 +1,123 @@
1
1
  import { foldWorkingSet } from "../domains/context/working-set/fold.js";
2
2
  import { projectWorkingSet } from "../domains/context/working-set/project.js";
3
+ import {
4
+ type ContinuityForkBinding,
5
+ type ContinuityProjection,
6
+ continuityReplayBlocks,
7
+ resolveContinuityProjection,
8
+ } from "../domains/session/continuity/projection.js";
9
+ import type { SessionContract } from "../domains/session/contract.js";
3
10
  import type { SessionEntry } from "../domains/session/entries.js";
4
11
  import type { AgentMessage } from "../engine/types.js";
5
- import { buildReplayAgentMessagesFromTurns, type RehydrateChatPanelOptions } from "./chat-renderer.js";
12
+ import {
13
+ activeEntriesBeforeCompactionCut,
14
+ buildReplayAgentMessagesFromTurns,
15
+ type RehydrateChatPanelOptions,
16
+ } from "./chat-renderer.js";
17
+
18
+ /**
19
+ * The session-ownership facts continuity needs, which a renderer does not have.
20
+ *
21
+ * Supplying this is what separates a fork's own transactions from the ones it
22
+ * inherited. Without it a reader owns nothing: inherited recall still projects,
23
+ * because losing the note is the one outcome that is never safe, but no
24
+ * transaction can reach execution authority.
25
+ */
26
+ export interface ContinuityReplayContext {
27
+ /** The opened session's actual id, from `session.current()`. */
28
+ sessionId?: string;
29
+ /** `meta.parentSessionId` / `meta.parentTurnId`, when this session is a fork. */
30
+ fork?: ContinuityForkBinding;
31
+ /**
32
+ * True only for a genuine historical cut of this session's own transactions.
33
+ * A live `/tree` selection is not one, even though the renderer passes
34
+ * `uptoTurnId` for both.
35
+ */
36
+ historical?: boolean;
37
+ /** Parse failures the reader counted, so a torn record cannot become permission. */
38
+ unreadableRecords?: number;
39
+ nowMs?: number;
40
+ }
41
+
42
+ export interface ModelReplayOptions extends RehydrateChatPanelOptions {
43
+ continuity?: ContinuityReplayContext;
44
+ }
45
+
46
+ /**
47
+ * Derive replay ownership from the open session.
48
+ *
49
+ * `meta.parentSessionId` and `meta.parentTurnId` are what `enrichForkMeta`
50
+ * stamps on a child, and together they are the fork binding. The header's
51
+ * `parentSession` is a path to the parent's ledger, a different fact, and is
52
+ * deliberately not used as a session id. A session with no fork pointers
53
+ * publishes no binding, so a foreign-origin record in a session that never
54
+ * forked stays unprojected instead of being read as inheritance.
55
+ *
56
+ * `historical` is never set here. A replay is historical only when a caller
57
+ * standing at a past turn says so; deriving it from the renderer's `uptoTurnId`
58
+ * would make an ordinary `/tree` selection permanently surrender this session's
59
+ * ownership of its own transactions.
60
+ */
61
+ export function continuityContextFromSession(
62
+ /** Narrowed to the one method this needs, so partial session owners qualify. */
63
+ session: Pick<SessionContract, "current"> | undefined,
64
+ ): ContinuityReplayContext {
65
+ const meta = session?.current();
66
+ if (!meta) return {};
67
+ const parentSessionId = meta.parentSessionId ?? null;
68
+ const parentTurnId = meta.parentTurnId ?? null;
69
+ return {
70
+ sessionId: meta.id,
71
+ ...(parentSessionId !== null && parentTurnId !== null ? { fork: { parentSessionId, parentTurnId } } : {}),
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Resolve the continuity projection for a replay.
77
+ *
78
+ * Folded over the active path **before** the compaction cut and before the
79
+ * working-set projection, because both of those remove records the fold needs:
80
+ * a commit, a pause or an operator control request older than the cut is still
81
+ * the evidence for the state the note describes. `withContinuityReplay` is the
82
+ * exported entry point, so the transcript and the model always render blocks
83
+ * from one resolution rather than two.
84
+ */
85
+ function resolveReplayContinuity(
86
+ entries: ReadonlyArray<SessionEntry>,
87
+ options: ModelReplayOptions = {},
88
+ ): ContinuityProjection {
89
+ const context = options.continuity ?? {};
90
+ return resolveContinuityProjection({
91
+ // The positional cut applies only to a real historical selection. A live
92
+ // /tree switch passes `uptoTurnId` for display and must still see the
93
+ // sidecars anchored after the selected message, because they are part of
94
+ // its own branch's current state.
95
+ entries: activeEntriesBeforeCompactionCut(entries, options, context.historical === true),
96
+ ...(context.sessionId === undefined ? {} : { sessionId: context.sessionId }),
97
+ ...(context.fork === undefined ? {} : { fork: context.fork }),
98
+ ...(context.historical === undefined ? {} : { historical: context.historical }),
99
+ ...(context.unreadableRecords === undefined ? {} : { unreadableRecords: context.unreadableRecords }),
100
+ ...(context.nowMs === undefined ? {} : { nowMs: context.nowMs }),
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Resolve continuity once and hand the same finished blocks to both surfaces.
106
+ *
107
+ * The transcript and the model must show byte-identical note text, so the
108
+ * projection is resolved here and the rendered blocks travel on the options
109
+ * object that `rehydrateChatPanelFromTurns` and
110
+ * `buildModelReplayAgentMessagesFromTurns` both receive. Resolving it twice
111
+ * would be two chances to disagree.
112
+ */
113
+ export function withContinuityReplay(
114
+ turns: ReadonlyArray<SessionEntry>,
115
+ options: RehydrateChatPanelOptions,
116
+ session: Pick<SessionContract, "current"> | undefined,
117
+ ): ModelReplayOptions {
118
+ const merged: ModelReplayOptions = { ...options, continuity: continuityContextFromSession(session) };
119
+ return { ...merged, continuityBlocks: continuityReplayBlocks(resolveReplayContinuity(turns, merged)) };
120
+ }
6
121
 
7
122
  /**
8
123
  * Build provider-facing replay messages from the durable session ledger.
@@ -10,12 +125,24 @@ import { buildReplayAgentMessagesFromTurns, type RehydrateChatPanelOptions } fro
10
125
  * setting gates creation of new evictions, not replay of durable state.
11
126
  * Visible transcript and export callers intentionally keep using the raw
12
127
  * rehydration helpers so eviction remains a model projection, not data loss.
128
+ *
129
+ * Continuity is resolved first, from the complete pre-cut ledger, so the
130
+ * accepted note survives both the working-set projection and the compaction
131
+ * cut. It is carried as labelled data and never becomes a synthetic operator
132
+ * instruction.
13
133
  */
14
134
  export function buildModelReplayAgentMessagesFromTurns(
15
135
  entries: ReadonlyArray<SessionEntry>,
16
- options: RehydrateChatPanelOptions = {},
136
+ options: ModelReplayOptions = {},
17
137
  ): AgentMessage[] {
18
138
  const activeLeafTurnId = options.activeLeafTurnId ?? options.uptoTurnId;
139
+ // A caller that already resolved the projection passes its blocks through,
140
+ // so the transcript beside this replay renders the identical bytes.
141
+ const blocks = options.continuityBlocks ?? continuityReplayBlocks(resolveReplayContinuity(entries, options));
19
142
  const projected = projectWorkingSet(entries, foldWorkingSet(entries, activeLeafTurnId), activeLeafTurnId);
20
- return buildReplayAgentMessagesFromTurns(projected, { ...options, skillContextEntries: entries });
143
+ return buildReplayAgentMessagesFromTurns(projected, {
144
+ ...options,
145
+ skillContextEntries: entries,
146
+ continuityBlocks: blocks,
147
+ });
21
148
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The one preflight output reservation a turn asks for.
3
+ *
4
+ * Extracted from the submit guard in `chat-loop.ts` so the guard and the live
5
+ * budget view quote the same number instead of growing a second formula. It
6
+ * resolves through `resolveReservedOutputTokens`, which applies the configured
7
+ * output budget, the model's advertised cap, and the remaining-context clamp
8
+ * the `openai-completions` and `ollama-native` transports already perform on
9
+ * the wire. The capability-resolved cap is the input, because that is what the
10
+ * runtime resolution decided this target may actually be asked for.
11
+ *
12
+ * This is a preflight reservation, not a guarantee of the final wire ceiling:
13
+ * `remainingContextMaxTokens` re-derives the ceiling at request time against
14
+ * the context the transport is about to send.
15
+ *
16
+ * It is not the compaction-threshold reserve. `ContextSnapshot.categories.reserve`
17
+ * is window headroom held back so auto-compaction fires before a request is
18
+ * refused, and the two figures are unrelated.
19
+ */
20
+
21
+ import { resolveReservedOutputTokens } from "../engine/apis/output-budget.js";
22
+ import type { AgentRuntime } from "./turn-state.js";
23
+
24
+ export function resolveTurnOutputReserve(agentRuntime: AgentRuntime, inputTokens: number): number {
25
+ // A runtime resolution always carries capability decisions in production. The
26
+ // optional read is for a partially built runtime: an absent cap is the
27
+ // resolver's own "no advertised limit" case, which falls back to the
28
+ // configured budget and then the product floor, rather than a crash on a path
29
+ // that now runs during ordinary accounting.
30
+ return resolveReservedOutputTokens(agentRuntime.runtimeResolution.capabilityDecisions?.maxTokens, {
31
+ api: agentRuntime.agent.state.model?.api ?? "",
32
+ contextWindow: agentRuntime.runtimeResolution.contextWindowDetails.effectiveContextWindow,
33
+ inputTokens,
34
+ });
35
+ }
@@ -20,7 +20,7 @@ import type { ChatPanel } from "./chat-panel.js";
20
20
  import { rehydrateChatPanelFromTurns } from "./chat-renderer.js";
21
21
  import { emitCommandNotice } from "./command-fallbacks.js";
22
22
  import type { InteractiveNoticeLevel } from "./interactive-subscriptions.js";
23
- import { buildModelReplayAgentMessagesFromTurns } from "./model-session-replay.js";
23
+ import { buildModelReplayAgentMessagesFromTurns, withContinuityReplay } from "./model-session-replay.js";
24
24
  import type { OverlayTransitions } from "./overlay-transitions.js";
25
25
  import { openCwdFallbackOverlay } from "./overlays/cwd-fallback.js";
26
26
  import { openHandoffReviewOverlay } from "./overlays/handoff-review.js";
@@ -188,7 +188,7 @@ export function createOverlaySessionLifecycle(deps: OverlaySessionLifecycleDeps)
188
188
  // after it (a compaction summary covering the leaf, above all)
189
189
  // still belong on screen. uptoTurnId is the historical-truncation
190
190
  // variant `/tree` uses.
191
- const replayOptions = leafTurnId ? { activeLeafTurnId: leafTurnId } : {};
191
+ const replayOptions = withContinuityReplay(turns, leafTurnId ? { activeLeafTurnId: leafTurnId } : {}, session);
192
192
  deps.resetTranscript();
193
193
  rehydrateChatPanelFromTurns(deps.chatPanel, turns, replayOptions);
194
194
  const replayMessages = buildModelReplayAgentMessagesFromTurns(turns, replayOptions);
@@ -240,8 +240,14 @@ export function createOverlaySessionLifecycle(deps: OverlaySessionLifecycleDeps)
240
240
  if (!sessionId) throw new Error("no current session after turn switch");
241
241
  const turns = deps.readStructuredEntries(sessionId);
242
242
  deps.resetTranscript();
243
- rehydrateChatPanelFromTurns(deps.chatPanel, turns, { uptoTurnId: turnId });
244
- const replayMessages = buildModelReplayAgentMessagesFromTurns(turns, { uptoTurnId: turnId });
243
+ // `uptoTurnId` truncates the display at the selected turn. It is
244
+ // deliberately not a continuity `historical` flag: a live /tree
245
+ // selection is still this session's own branch, and treating every
246
+ // display truncation as a historical fork would surrender ownership
247
+ // of its own transactions for good.
248
+ const replayOptions = withContinuityReplay(turns, { uptoTurnId: turnId }, session);
249
+ rehydrateChatPanelFromTurns(deps.chatPanel, turns, replayOptions);
250
+ const replayMessages = buildModelReplayAgentMessagesFromTurns(turns, replayOptions);
245
251
  deps.chat.resetForSession(turnId, replayMessages);
246
252
  // The same branch the transcript above was just scoped to. Without the
247
253
  // leaf, /usage, the footer Σ, and the last-turn line kept reporting the
@@ -324,12 +330,12 @@ export function createOverlaySessionLifecycle(deps: OverlaySessionLifecycleDeps)
324
330
  function replayFork(forkedSessionId: string, parentTurnId: string, session: SessionContract): void {
325
331
  try {
326
332
  const turns = deps.readStructuredEntries(forkedSessionId);
327
- rehydrateChatPanelFromTurns(deps.chatPanel, turns);
328
333
  const leafTurnId = session.tree(forkedSessionId).leafId ?? parentTurnId;
329
- const replayMessages = buildModelReplayAgentMessagesFromTurns(
330
- turns,
331
- leafTurnId ? { activeLeafTurnId: leafTurnId } : {},
332
- );
334
+ // The child is already current here, so its own id and fork pointers are
335
+ // what separate the transactions it inherited from any it later mints.
336
+ const replayOptions = withContinuityReplay(turns, leafTurnId ? { activeLeafTurnId: leafTurnId } : {}, session);
337
+ rehydrateChatPanelFromTurns(deps.chatPanel, turns, replayOptions);
338
+ const replayMessages = buildModelReplayAgentMessagesFromTurns(turns, replayOptions);
333
339
  deps.chat.resetForSession(leafTurnId, replayMessages);
334
340
  rescopeToBranch(session, turns, leafTurnId);
335
341
  } catch (error) {
@@ -617,8 +623,9 @@ export function createOverlaySessionLifecycle(deps: OverlaySessionLifecycleDeps)
617
623
  try {
618
624
  const turns = deps.readStructuredEntries(toSessionId);
619
625
  deps.resetTranscript();
620
- rehydrateChatPanelFromTurns(deps.chatPanel, turns);
621
- deps.chat.resetForSession(null, buildModelReplayAgentMessagesFromTurns(turns));
626
+ const replayOptions = withContinuityReplay(turns, {}, session);
627
+ rehydrateChatPanelFromTurns(deps.chatPanel, turns, replayOptions);
628
+ deps.chat.resetForSession(null, buildModelReplayAgentMessagesFromTurns(turns, replayOptions));
622
629
  rescopeToBranch(session, turns, null);
623
630
  } catch (error) {
624
631
  deps.stderr(`[/handoff] seeding replay failed: ${error instanceof Error ? error.message : String(error)}\n`);
@@ -3,7 +3,7 @@ import { collectSessionEntries } from "../domains/session/compaction/session-ent
3
3
  import type { SessionContract, SessionEntry } from "../domains/session/index.js";
4
4
  import { openSession, sessionPaths } from "../engine/session.js";
5
5
  import type { ChatLoop } from "./chat-loop.js";
6
- import { buildModelReplayAgentMessagesFromTurns } from "./model-session-replay.js";
6
+ import { buildModelReplayAgentMessagesFromTurns, continuityContextFromSession } from "./model-session-replay.js";
7
7
 
8
8
  type SessionOwner = Pick<SessionContract, "create" | "current">;
9
9
  type SessionChat = Pick<ChatLoop, "resetForSession">;
@@ -89,6 +89,7 @@ export function createSessionTranscript(deps: SessionTranscriptDeps): SessionTra
89
89
  leafTurnId,
90
90
  buildModelReplayAgentMessagesFromTurns(turns, {
91
91
  ...(leafTurnId ? { activeLeafTurnId: leafTurnId } : {}),
92
+ continuity: continuityContextFromSession(deps.session),
92
93
  }),
93
94
  );
94
95
  deps.refreshStatus();
@@ -144,6 +144,7 @@ type SlashCommandVariant =
144
144
  | { kind: "context-refresh" }
145
145
  /** `ref` is the turnId an `[evicted ...]` marker names. */
146
146
  | { kind: "context-recall"; ref: string }
147
+ | { kind: "context-recover"; handoffId: string; action: "reduce" | "deliver" }
147
148
  | {
148
149
  kind: "resources";
149
150
  family?: "extensions" | "plugins";
@@ -823,6 +824,7 @@ export interface SlashCommandContext {
823
824
  * appends a compactionSummary entry. No-op when no session is open, so the
824
825
  * handler prints an actionable stderr line instead.
825
826
  */
827
+ runHandoffRecovery?: (handoffId: string, action: "reduce" | "deliver") => void;
826
828
  runCompact: (instructions: string | undefined) => void;
827
829
  /**
828
830
  * Escape hatch for the `view verify` entry: verify a receipt file on disk
@@ -1700,9 +1702,10 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
1700
1702
  name: "context",
1701
1703
  description: "Context hub: window overlay plus compact, recall, init, refresh, and reset",
1702
1704
  group: "Inspect",
1703
- kinds: ["context-view", "compact", "context-recall", "init", "context-clear", "context-refresh"],
1705
+ kinds: ["context-view", "compact", "context-recover", "context-recall", "init", "context-clear", "context-refresh"],
1704
1706
  subcommandDescriptions: {
1705
1707
  compact: "Compact session context",
1708
+ recover: "Recover a paused handoff: ID reduce|deliver",
1706
1709
  recall: "Recall an evicted result",
1707
1710
  init: "Initialize project context",
1708
1711
  refresh: "Refresh project context",
@@ -1711,6 +1714,12 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
1711
1714
  args: {
1712
1715
  subcommands: {
1713
1716
  compact: { positionals: [...COMPACT_POSITIONALS] },
1717
+ recover: {
1718
+ positionals: [
1719
+ { name: "handoffId", required: true },
1720
+ { name: "action", required: true },
1721
+ ],
1722
+ },
1714
1723
  recall: { positionals: [...RECALL_POSITIONALS] },
1715
1724
  init: {
1716
1725
  flags: CONTEXT_INIT_FLAG_TABLE.flatMap(({ flag, aliases = [] }) => [flag, ...aliases].map((name) => ({ name }))),
@@ -1724,6 +1733,12 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
1724
1733
  switch (parsed.subcommand) {
1725
1734
  case "compact":
1726
1735
  return { kind: "compact", instructions: parsed.rest };
1736
+ case "recover": {
1737
+ const [handoffId, action] = parsed.positionals;
1738
+ return handoffId && (action === "reduce" || action === "deliver")
1739
+ ? { kind: "context-recover", handoffId, action }
1740
+ : { kind: "usage-error", command: "context", reason: "Use /context recover <handoffId> <reduce|deliver>" };
1741
+ }
1727
1742
  case "recall":
1728
1743
  return { kind: "context-recall", ref: parsed.positionals[0] ?? "" };
1729
1744
  case "init": {
@@ -1752,6 +1767,10 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
1752
1767
  case "compact":
1753
1768
  ctx.runCompact(command.instructions);
1754
1769
  return;
1770
+ case "context-recover":
1771
+ if (ctx.runHandoffRecovery) ctx.runHandoffRecovery(command.handoffId, command.action);
1772
+ else ctx.notice("error", "native handoff recovery is unavailable");
1773
+ return;
1755
1774
  case "context-recall":
1756
1775
  if (ctx.runContextRecall) {
1757
1776
  ctx.runContextRecall(command.ref);