@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,79 @@
1
+ /**
2
+ * Exact UTF-8 note admission (CONTRACTS.md §4).
3
+ *
4
+ * The operator's note is stored as the bytes they wrote. Whitespace is tested
5
+ * for emptiness only: nothing here trims, normalizes Unicode, truncates, or
6
+ * regenerates the accepted value, because replay must reproduce the decoded
7
+ * string exactly and JSONL escaping already changes the wire representation.
8
+ *
9
+ * Validation runs before any append or lock, takes no lock itself, and writes
10
+ * nothing. A rejection is typed so the caller can explain it without guessing.
11
+ */
12
+
13
+ import { createHash } from "node:crypto";
14
+ import {
15
+ type AcceptedNote,
16
+ type AcceptedNoteVerification,
17
+ HANDOFF_NOTE_MAX_BYTES,
18
+ type NoteValidation,
19
+ } from "./contract.js";
20
+
21
+ function sha256Utf8(value: string): string {
22
+ return createHash("sha256").update(Buffer.from(value, "utf8")).digest("hex");
23
+ }
24
+
25
+ /**
26
+ * Admit a note, or say precisely why not.
27
+ *
28
+ * The UTF-8 round trip is the check that rejects a lone surrogate: encoding
29
+ * `"\uD800"` and decoding it back yields U+FFFD, so the decoded string differs
30
+ * from the input and the note is refused rather than silently stored as a
31
+ * replacement character the operator never typed. The byte bound is measured on
32
+ * the UTF-8 encoding, not on `String.length`, so an emoji-heavy note is bounded
33
+ * by what the provider will actually carry.
34
+ */
35
+ export function validateContinuityNote(note: unknown): NoteValidation {
36
+ if (typeof note !== "string") return { ok: false, reason: "not_a_string", noteBytes: null };
37
+ if (note.trim().length === 0) return { ok: false, reason: "blank", noteBytes: null };
38
+ if (note.includes("\u0000")) return { ok: false, reason: "contains_nul", noteBytes: null };
39
+
40
+ const encoded = Buffer.from(note, "utf8");
41
+ if (encoded.toString("utf8") !== note) {
42
+ return { ok: false, reason: "not_utf8_round_trip", noteBytes: encoded.byteLength };
43
+ }
44
+ if (encoded.byteLength > HANDOFF_NOTE_MAX_BYTES) {
45
+ return { ok: false, reason: "exceeds_max_bytes", noteBytes: encoded.byteLength };
46
+ }
47
+ return {
48
+ ok: true,
49
+ accepted: { note, noteBytes: encoded.byteLength, noteSha256: sha256Utf8(note) },
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Verify a note read back from a record.
55
+ *
56
+ * Self-consistency first: the text is re-admitted, because a record whose note
57
+ * no longer admits is not usable evidence whatever its digest says, and the
58
+ * stored byte count and hash must match that re-encoding. Byte count is
59
+ * compared before the hash so a truncated readback reports the truncation
60
+ * rather than an opaque digest mismatch.
61
+ *
62
+ * `expected` compares two copies of the same transaction, which is how a
63
+ * commit's note and a later summary carry's note are held to exact decoded
64
+ * equality across records.
65
+ */
66
+ export function verifyAcceptedNote(accepted: AcceptedNote, expected?: AcceptedNote): AcceptedNoteVerification {
67
+ const revalidated = validateContinuityNote(accepted.note);
68
+ if (!revalidated.ok) return { ok: false, reason: revalidated.reason };
69
+ if (revalidated.accepted.noteBytes !== accepted.noteBytes) return { ok: false, reason: "byte_count_mismatch" };
70
+ if (revalidated.accepted.noteSha256 !== accepted.noteSha256) return { ok: false, reason: "hash_mismatch" };
71
+ if (!expected) return { ok: true };
72
+ // Decoded text first: when two copies of one transaction disagree, the
73
+ // useful report is that the text differs, not a digest that differs because
74
+ // the text does.
75
+ if (expected.note !== accepted.note) return { ok: false, reason: "decoded_text_mismatch" };
76
+ if (expected.noteBytes !== accepted.noteBytes) return { ok: false, reason: "byte_count_mismatch" };
77
+ if (expected.noteSha256 !== accepted.noteSha256) return { ok: false, reason: "hash_mismatch" };
78
+ return { ok: true };
79
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The durable operator control request behind `operatorRequestEntryId`
3
+ * (CONTRACTS.md §3.1).
4
+ *
5
+ * Clio's existing control commands are display-only echoes: `/context compact
6
+ * [instructions]` runs a compaction and `/resume` opens session navigation, and
7
+ * neither leaves a durable record that means "resume this handoff". So the
8
+ * authority a `resumed` event cites cannot be a conversation turn. It is a
9
+ * reserved `CustomEntry` subtype, written by an operator surface, carrying the
10
+ * exact binding the fold checks: which handoff, which action, which paused or
11
+ * failed head, which session and which branch.
12
+ *
13
+ * This module owns the wire shape and its strict validation. It deliberately
14
+ * owns nothing else: the `/context recover <handoffId> <reduce|deliver>` command
15
+ * that writes one is packet 03/05, and nothing here or in 02B executes a
16
+ * recovery. An opaque `custom` entry is not authority, which is why
17
+ * `isSessionEntry` routes this `customType` through the strict check below
18
+ * rather than accepting any data under the reserved name.
19
+ */
20
+
21
+ import type { CustomEntry } from "../entries.js";
22
+
23
+ /** Reserved `customType`. A record under this name must satisfy the strict shape. */
24
+ export const HANDOFF_RECOVERY_REQUEST_CUSTOM_TYPE = "contextHandoffRecoveryRequest";
25
+
26
+ export interface HandoffRecoveryRequestData {
27
+ version: 1;
28
+ requestKind: "handoff_recovery";
29
+ handoffId: string;
30
+ action: "reduce" | "deliver";
31
+ /** The session the operator made the request in. */
32
+ sessionId: string;
33
+ /** The handoff's immutable branch anchor, as the request understood it. */
34
+ branchAnchorTurnId: string | null;
35
+ /** The live selected leaf when the request was made. */
36
+ selectedLeafTurnId: string | null;
37
+ /** The exact head this request answers. */
38
+ pausedOrFailedEntryId: string;
39
+ }
40
+
41
+ export type HandoffRecoveryRequestEntry = CustomEntry<HandoffRecoveryRequestData> & {
42
+ customType: typeof HANDOFF_RECOVERY_REQUEST_CUSTOM_TYPE;
43
+ };
44
+
45
+ function isRecord(value: unknown): value is Record<string, unknown> {
46
+ return typeof value === "object" && value !== null && !Array.isArray(value);
47
+ }
48
+
49
+ function isNonEmptyString(value: unknown): value is string {
50
+ return typeof value === "string" && value.length > 0;
51
+ }
52
+
53
+ function isNullableString(value: unknown): value is string | null {
54
+ return value === null || isNonEmptyString(value);
55
+ }
56
+
57
+ /**
58
+ * Strict validation of the reserved subtype's data.
59
+ *
60
+ * Every field is required, including the two nullable ones: an absent
61
+ * `branchAnchorTurnId` and an explicit `null` are different claims, and a
62
+ * request that simply omits the binding must not read as a request that
63
+ * asserted "no anchor". A request missing any part of its binding is not a
64
+ * weaker request, it is not a request at all.
65
+ */
66
+ export function isHandoffRecoveryRequestData(value: unknown): value is HandoffRecoveryRequestData {
67
+ if (!isRecord(value)) return false;
68
+ return (
69
+ value.version === 1 &&
70
+ value.requestKind === "handoff_recovery" &&
71
+ isNonEmptyString(value.handoffId) &&
72
+ (value.action === "reduce" || value.action === "deliver") &&
73
+ isNonEmptyString(value.sessionId) &&
74
+ Object.hasOwn(value, "branchAnchorTurnId") &&
75
+ isNullableString(value.branchAnchorTurnId) &&
76
+ Object.hasOwn(value, "selectedLeafTurnId") &&
77
+ isNullableString(value.selectedLeafTurnId) &&
78
+ isNonEmptyString(value.pausedOrFailedEntryId)
79
+ );
80
+ }
81
+
82
+ /**
83
+ * Whether a ledger entry is a valid operator recovery request.
84
+ *
85
+ * Takes the entry rather than the data so a caller cannot reach authority by
86
+ * validating loose data it assembled itself: the record must really be a
87
+ * `custom` entry under the reserved `customType`.
88
+ */
89
+ export function isHandoffRecoveryRequestEntry(value: unknown): value is HandoffRecoveryRequestEntry {
90
+ if (!isRecord(value)) return false;
91
+ return (
92
+ value.kind === "custom" &&
93
+ value.customType === HANDOFF_RECOVERY_REQUEST_CUSTOM_TYPE &&
94
+ isNonEmptyString(value.turnId) &&
95
+ isNullableString(value.parentTurnId) &&
96
+ isNonEmptyString(value.timestamp) &&
97
+ // §3.1 specifies `display: false`. A control record that renders is a
98
+ // different thing from the one the contract adopted, and the flag is part
99
+ // of the adopted envelope rather than a presentation preference: an
100
+ // authority carrier must not also be transcript text.
101
+ value.display === false &&
102
+ isHandoffRecoveryRequestData(value.data)
103
+ );
104
+ }
@@ -0,0 +1,408 @@
1
+ /**
2
+ * Bounded persistence of an ordered continuity group (CONTRACTS.md §5, §6).
3
+ *
4
+ * Everything is injected: append, an exact payload-comparing readback, the
5
+ * barrier, removed-state and origin checks, and a bounded retry schedule. This
6
+ * module performs no execution of its own and knows nothing about models,
7
+ * runtimes or replay. No failure is logged by recursively persisting more
8
+ * records; a blocked group is reported to its caller and stops there.
9
+ *
10
+ * Rules that drive the shape:
11
+ *
12
+ * - An accepted append is never repeated because a later barrier failed. A
13
+ * failed barrier is retried with the same ids, within a finite bound.
14
+ * - A throwing append proves neither success nor absence. Only an
15
+ * authoritative `absent` readback licenses a re-append, under the original
16
+ * id; `conflicting` and `unresolved`, including a readback that itself
17
+ * throws, stop the group, because malformed or contradictory data cannot
18
+ * prove absence.
19
+ * - Removed state and origin are rechecked at every boundary that can be
20
+ * crossed by a wait or an await, not only at the ends of the group.
21
+ * - The supplied clock is treated as monotonic within one call. A reading
22
+ * that stops being a number, or that jumps backwards between two
23
+ * boundaries, makes the window unjudgeable rather than optimistic.
24
+ * - `durable` means the barrier reported success. Under §1's filesystem
25
+ * ceiling that is not a power-loss guarantee, and only a usable clock that
26
+ * stayed inside the window confirms it: persistence is not permission for
27
+ * dependent execution.
28
+ */
29
+
30
+ import {
31
+ type ContinuityAcceptedProgress,
32
+ type ContinuityAnomaly,
33
+ type ContinuityAppendable,
34
+ type ContinuityPersistBlockedReason,
35
+ type ContinuityPersistencePorts,
36
+ type ContinuityPersistRequest,
37
+ type ContinuityPersistResult,
38
+ type ContinuityReconcileResult,
39
+ type ContinuityRetrySchedule,
40
+ HANDOFF_MAX_FLUSH_RETRIES,
41
+ } from "./contract.js";
42
+ import { canonicalJson } from "./validate.js";
43
+
44
+ /** The digest a progress token carries, so an id can never stand in for a payload. */
45
+ export function continuityRecordDigest(entry: ContinuityAppendable): string {
46
+ return canonicalJson(entry);
47
+ }
48
+
49
+ function blocked(
50
+ reason: ContinuityPersistBlockedReason,
51
+ accepted: ReadonlyArray<ContinuityAcceptedProgress>,
52
+ barrierAttempts: number,
53
+ anomalies: ReadonlyArray<ContinuityAnomaly>,
54
+ ): ContinuityPersistResult {
55
+ return { status: "uncertain", reason, accepted: [...accepted], barrierAttempts, anomalies };
56
+ }
57
+
58
+ function readback(entry: ContinuityAppendable, ports: ContinuityPersistencePorts) {
59
+ try {
60
+ return ports.readExact(entry);
61
+ } catch (error) {
62
+ // A readback that throws read nothing authoritative. It is unresolved,
63
+ // never absence.
64
+ return { status: "unresolved" as const, detail: error instanceof Error ? error.message : String(error) };
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Establish what actually happened to one record whose append was ambiguous.
70
+ *
71
+ * This is a **read-only** classification. The expected record goes to the port
72
+ * so the comparison is on payload, not on the id: a stored record with the same
73
+ * id and different bytes is `conflicting`, and re-appending over it would
74
+ * create a second contradictory copy. Writing is deliberately left to
75
+ * `persistContinuityGroup`, which re-checks removed state, origin and the
76
+ * deadline before it re-appends; a reconciler that wrote on its own would
77
+ * bypass exactly the guards a readback can invalidate.
78
+ */
79
+ export function reconcileContinuityEntry(
80
+ entry: ContinuityAppendable,
81
+ ports: ContinuityPersistencePorts,
82
+ ): ContinuityReconcileResult {
83
+ const result = readback(entry, ports);
84
+ if (result.status === "matching") return { status: "accepted" };
85
+ if (result.status === "conflicting") {
86
+ return { status: "conflicting", ...(result.detail === undefined ? {} : { detail: result.detail }) };
87
+ }
88
+ if (result.status === "unresolved") {
89
+ return { status: "unresolved", ...(result.detail === undefined ? {} : { detail: result.detail }) };
90
+ }
91
+ return { status: "absent" };
92
+ }
93
+
94
+ /** A schedule that cannot bound the work is refused rather than run. */
95
+ function invalidSchedule(retry: ContinuityRetrySchedule): string | null {
96
+ if (!Number.isSafeInteger(retry.limit) || retry.limit < 0 || retry.limit > HANDOFF_MAX_FLUSH_RETRIES) {
97
+ return `retry limit ${retry.limit} is not an integer in 0..${HANDOFF_MAX_FLUSH_RETRIES}`;
98
+ }
99
+ if (!Number.isFinite(retry.deadlineAtMs)) return "retry deadline is not finite";
100
+ const now = retry.now();
101
+ if (!Number.isFinite(now)) return "retry clock is not finite";
102
+ return null;
103
+ }
104
+
105
+ /**
106
+ * A clock reading that is usable for this operation.
107
+ *
108
+ * The supplied clock is read many times across appends, awaits and a barrier.
109
+ * Finiteness alone is not enough: a clock that jumps backwards mid-operation
110
+ * cannot be used to judge a deadline either, and neither can one that stops
111
+ * being a number. The guard is monotonic within one call and answers `null` the
112
+ * moment it stops being usable; every caller then treats the window as
113
+ * unconfirmed rather than assuming the optimistic reading.
114
+ */
115
+ interface ClockGuard {
116
+ read(): number | null;
117
+ }
118
+
119
+ function createClockGuard(retry: ContinuityRetrySchedule): ClockGuard {
120
+ let observed = Number.NEGATIVE_INFINITY;
121
+ let broken = false;
122
+ return {
123
+ read(): number | null {
124
+ if (broken) return null;
125
+ const now = retry.now();
126
+ if (!Number.isFinite(now) || now < observed) {
127
+ broken = true;
128
+ return null;
129
+ }
130
+ observed = now;
131
+ return now;
132
+ },
133
+ };
134
+ }
135
+
136
+ type BarrierOutcome =
137
+ | { ok: true; attempts: number; confirmedWithinDeadline: boolean }
138
+ | { ok: false; attempts: number; reason: ContinuityPersistBlockedReason; detail: string };
139
+
140
+ async function runBarrier(
141
+ request: ContinuityPersistRequest,
142
+ ports: ContinuityPersistencePorts,
143
+ retry: ContinuityRetrySchedule,
144
+ clock: ClockGuard,
145
+ ): Promise<BarrierOutcome> {
146
+ const flushing = request.barrier.kind === "flush";
147
+ const port = flushing ? ports.flushAppends : ports.checkpoint;
148
+ if (!port) {
149
+ // A SessionContract without the barrier is an unsupported persistence
150
+ // port. Treating a missing barrier as success would report durability
151
+ // that was never attempted.
152
+ return {
153
+ ok: false,
154
+ attempts: 0,
155
+ reason: "unsupported_barrier_port",
156
+ detail: `${request.barrier.kind} port is not available`,
157
+ };
158
+ }
159
+
160
+ let attempts = 0;
161
+ let detail = "";
162
+ for (let round = 0; round <= retry.limit; round += 1) {
163
+ // Read before every attempt, round 0 included: the appends that just ran
164
+ // can have taken the clock past the deadline or broken it outright. A late
165
+ // barrier still runs, because it can establish persistence for records
166
+ // already accepted; what it cannot do is confirm the window.
167
+ let before = clock.read();
168
+ if (round > 0) {
169
+ if (before === null || before >= retry.deadlineAtMs) break;
170
+ try {
171
+ await retry.wait(round);
172
+ } catch (error) {
173
+ return {
174
+ ok: false,
175
+ attempts,
176
+ reason: "barrier_failed",
177
+ detail: `retry wait rejected: ${error instanceof Error ? error.message : String(error)}`,
178
+ };
179
+ }
180
+ // The wait is an await, so every fact can have changed across it,
181
+ // including the clock going backwards or stopping being a number.
182
+ before = clock.read();
183
+ if (before === null) {
184
+ return { ok: false, attempts, reason: "invalid_retry_schedule", detail: "retry clock became unusable" };
185
+ }
186
+ if (ports.isStateRemoved()) return { ok: false, attempts, reason: "state_removed", detail: "state removed" };
187
+ if (!ports.isOriginCurrent()) return { ok: false, attempts, reason: "origin_changed", detail: "origin changed" };
188
+ if (before >= retry.deadlineAtMs) break;
189
+ }
190
+ attempts += 1;
191
+ try {
192
+ if (request.barrier.kind === "flush") {
193
+ (port as () => void)();
194
+ } else {
195
+ await (port as (reason: string) => Promise<void>)(request.barrier.reason);
196
+ }
197
+ // The barrier itself can be an await. Confirmation needs a clock that
198
+ // is still usable afterwards and still inside the window.
199
+ const after = clock.read();
200
+ return {
201
+ ok: true,
202
+ attempts,
203
+ confirmedWithinDeadline: before !== null && after !== null && after < retry.deadlineAtMs,
204
+ };
205
+ } catch (error) {
206
+ detail = error instanceof Error ? error.message : String(error);
207
+ }
208
+ }
209
+ return { ok: false, attempts, reason: "barrier_failed", detail: detail || "deadline reached before the barrier ran" };
210
+ }
211
+
212
+ /**
213
+ * Append an ordered group and run one barrier after the complete group.
214
+ *
215
+ * `alreadyAccepted` resumes a partial attempt at this exact group, matched by
216
+ * payload digest so an id cannot carry a different record than the one that was
217
+ * accepted. A previously accepted entry is skipped, never re-appended: only a
218
+ * failing append inside this call reconciles.
219
+ */
220
+ export async function persistContinuityGroup(
221
+ request: ContinuityPersistRequest,
222
+ ports: ContinuityPersistencePorts,
223
+ retry: ContinuityRetrySchedule,
224
+ ): Promise<ContinuityPersistResult> {
225
+ const anomalies: ContinuityAnomaly[] = [];
226
+ const accepted: ContinuityAcceptedProgress[] = [];
227
+
228
+ // One record per id, compared by payload before anything is written.
229
+ const wanted = new Map<string, string>();
230
+ for (const entry of request.entries) {
231
+ const digest = continuityRecordDigest(entry);
232
+ const previous = wanted.get(entry.turnId);
233
+ if (previous !== undefined && previous !== digest) {
234
+ anomalies.push({
235
+ kind: "duplicate_conflict",
236
+ entryId: entry.turnId,
237
+ detail: "the group contains two different records under one entry id",
238
+ });
239
+ return blocked("duplicate_group_entry", accepted, 0, anomalies);
240
+ }
241
+ wanted.set(entry.turnId, digest);
242
+ }
243
+
244
+ // Progress must belong to this request: an id whose recorded digest is not
245
+ // the record this group carries is a different group. Repeats of one id are
246
+ // folded, so duplicated progress cannot stand in for a record that is still
247
+ // missing.
248
+ const acceptedIds = new Set<string>();
249
+ for (const progress of request.alreadyAccepted ?? []) {
250
+ const expected = wanted.get(progress.entryId);
251
+ if (expected === undefined || expected !== progress.payloadDigest) {
252
+ anomalies.push({
253
+ kind: "duplicate_conflict",
254
+ entryId: progress.entryId,
255
+ detail: "reported progress does not match this group's record for that id",
256
+ });
257
+ return blocked("progress_mismatch", [], 0, anomalies);
258
+ }
259
+ if (acceptedIds.has(progress.entryId)) continue;
260
+ acceptedIds.add(progress.entryId);
261
+ accepted.push({ entryId: progress.entryId, payloadDigest: progress.payloadDigest });
262
+ }
263
+
264
+ // Validated progress is established before the schedule is judged, so a
265
+ // refusal that writes nothing still hands back what an earlier attempt
266
+ // really got accepted. Losing it would invite a caller to append it twice.
267
+ const scheduleProblem = invalidSchedule(retry);
268
+ if (scheduleProblem) {
269
+ anomalies.push({ kind: "malformed_record", entryId: null, detail: scheduleProblem });
270
+ return blocked("invalid_retry_schedule", accepted, 0, anomalies);
271
+ }
272
+
273
+ // One monotonic clock reading for the whole call. Every boundary goes
274
+ // through it, so a clock that jumps backwards between two of them is caught
275
+ // rather than believed.
276
+ const clock = createClockGuard(retry);
277
+
278
+ const remaining = () => request.entries.filter((entry) => !acceptedIds.has(entry.turnId));
279
+ // A group whose window has already closed is not started. A late barrier can
280
+ // still establish persistence for work already accepted, but new records are
281
+ // not written into an expired transaction.
282
+ const startedAt = clock.read();
283
+ if (startedAt === null) {
284
+ anomalies.push({ kind: "malformed_record", entryId: null, detail: "the retry clock is not usable" });
285
+ return blocked("invalid_retry_schedule", accepted, 0, anomalies);
286
+ }
287
+ if (startedAt >= retry.deadlineAtMs && remaining().length > 0) {
288
+ anomalies.push({
289
+ kind: "malformed_record",
290
+ entryId: null,
291
+ detail: "the deadline passed before the group was written",
292
+ });
293
+ return blocked("deadline_expired", accepted, 0, anomalies);
294
+ }
295
+ if (ports.isStateRemoved()) return blocked("state_removed", accepted, 0, anomalies);
296
+ if (!ports.isOriginCurrent()) return blocked("origin_changed", accepted, 0, anomalies);
297
+
298
+ /** Every guard a write must clear, rechecked immediately before it. */
299
+ const writeBlocked = (): ContinuityPersistBlockedReason | null => {
300
+ const now = clock.read();
301
+ if (now === null) return "invalid_retry_schedule";
302
+ if (now >= retry.deadlineAtMs) return "deadline_expired";
303
+ if (ports.isStateRemoved()) return "state_removed";
304
+ if (!ports.isOriginCurrent()) return "origin_changed";
305
+ return null;
306
+ };
307
+
308
+ for (const entry of request.entries) {
309
+ if (acceptedIds.has(entry.turnId)) continue;
310
+ // Rechecked per record: a slow append can cross the deadline, and a long
311
+ // group can straddle a removal or a branch switch.
312
+ const guard = writeBlocked();
313
+ if (guard) {
314
+ anomalies.push({ kind: "malformed_record", entryId: entry.turnId, detail: `write refused: ${guard}` });
315
+ return blocked(guard, accepted, 0, anomalies);
316
+ }
317
+
318
+ const digest = continuityRecordDigest(entry);
319
+ const accept = (): void => {
320
+ accepted.push({ entryId: entry.turnId, payloadDigest: digest });
321
+ acceptedIds.add(entry.turnId);
322
+ };
323
+ try {
324
+ ports.append(entry);
325
+ accept();
326
+ continue;
327
+ } catch (error) {
328
+ anomalies.push({
329
+ kind: "malformed_record",
330
+ entryId: entry.turnId,
331
+ detail: `append threw: ${error instanceof Error ? error.message : String(error)}`,
332
+ });
333
+ }
334
+
335
+ const reconciled = reconcileContinuityEntry(entry, ports);
336
+ if (reconciled.status === "accepted") {
337
+ accept();
338
+ continue;
339
+ }
340
+ if (reconciled.status === "conflicting") {
341
+ anomalies.push({
342
+ kind: "duplicate_conflict",
343
+ entryId: entry.turnId,
344
+ detail: reconciled.detail ?? "stored record has this id and different payload",
345
+ });
346
+ return blocked("append_conflicting", accepted, 0, anomalies);
347
+ }
348
+ if (reconciled.status === "unresolved") {
349
+ return blocked("append_unresolved", accepted, 0, anomalies);
350
+ }
351
+
352
+ // Proven absent, so the original id may be written once more. The readback
353
+ // itself can have observed or caused a removal, so the guards run again
354
+ // before the re-append.
355
+ const reguard = writeBlocked();
356
+ if (reguard) {
357
+ anomalies.push({ kind: "malformed_record", entryId: entry.turnId, detail: `reappend refused: ${reguard}` });
358
+ return blocked(reguard, accepted, 0, anomalies);
359
+ }
360
+ try {
361
+ ports.append(entry);
362
+ accept();
363
+ } catch (error) {
364
+ const second = reconcileContinuityEntry(entry, ports);
365
+ if (second.status === "accepted") {
366
+ accept();
367
+ continue;
368
+ }
369
+ anomalies.push({
370
+ kind: "malformed_record",
371
+ entryId: entry.turnId,
372
+ detail: `reappend threw: ${error instanceof Error ? error.message : String(error)}`,
373
+ });
374
+ return blocked(
375
+ second.status === "absent" ? "append_absent_after_retry" : "append_unresolved",
376
+ accepted,
377
+ 0,
378
+ anomalies,
379
+ );
380
+ }
381
+ }
382
+
383
+ if (ports.isStateRemoved()) return blocked("state_removed", accepted, 0, anomalies);
384
+ if (!ports.isOriginCurrent()) return blocked("origin_changed", accepted, 0, anomalies);
385
+
386
+ const barrier = await runBarrier(request, ports, retry, clock);
387
+ if (!barrier.ok) {
388
+ anomalies.push({
389
+ kind: "malformed_record",
390
+ entryId: null,
391
+ detail: `${request.barrier.kind} barrier did not complete: ${barrier.detail}`,
392
+ });
393
+ return blocked(barrier.reason, accepted, barrier.attempts, anomalies);
394
+ }
395
+
396
+ // Both checks run again: an asynchronous checkpoint can resolve after the
397
+ // state root was removed or the origin moved, and resolution alone never
398
+ // licenses execution or recreation of deleted state.
399
+ if (ports.isStateRemoved()) return blocked("state_removed", accepted, barrier.attempts, anomalies);
400
+ if (!ports.isOriginCurrent()) return blocked("origin_changed", accepted, barrier.attempts, anomalies);
401
+
402
+ return {
403
+ status: "durable",
404
+ accepted,
405
+ barrierAttempts: barrier.attempts,
406
+ confirmedWithinDeadline: barrier.confirmedWithinDeadline,
407
+ };
408
+ }