@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,95 @@
1
+ import { createHash } from "node:crypto";
2
+ import { canonicalMemoryRepositoryIdentity } from "./operations.js";
3
+ import { buildMemoryPromptSection, type MemoryPromptOptions } from "./prompt-section.js";
4
+ import { MEMORY_RELEVANCE_VERSION } from "./relevance.js";
5
+ import { readMemoryStoreSnapshot } from "./store.js";
6
+
7
+ /** Host-owned attempt identity, distinct from the ledger ID allocated after preflight. */
8
+ export interface MemoryPromptRequest {
9
+ readonly turnId: string | null;
10
+ readonly sessionAuthority: string;
11
+ readonly cwd: string;
12
+ readonly targetId: string;
13
+ readonly runtimeId: string;
14
+ readonly modelId: string;
15
+ readonly taskText: string;
16
+ readonly activePaths: readonly string[];
17
+ readonly activeSymbols?: readonly string[];
18
+ }
19
+
20
+ export interface MemoryPromptReaderOptions {
21
+ getDataDir: () => string;
22
+ /** Explicit experiment only; production leaves relevance off. */
23
+ experimentalRelevance?: boolean;
24
+ selection?: Pick<MemoryPromptOptions, "scopes" | "tokenBudget" | "maxItems">;
25
+ /** Source seam for deterministic read-count tests; production reads the real bounded store. */
26
+ readStore?: typeof readMemoryStoreSnapshot;
27
+ }
28
+
29
+ /**
30
+ * Single-entry immutable text cache. A new prepared operator attempt rereads the
31
+ * store; ordinary external edits become visible at the next attempt. Within an
32
+ * attempt/its admitted continuations, selection is frozen, except authority
33
+ * changes (session/branch, data root, canonical repo, actual target/runtime/model).
34
+ * Calls without a prepared turn (boot/reset prewarm) read each time. Later
35
+ * prewarm can reuse the preceding snapshot; a fresh attempt always rereads.
36
+ */
37
+ export function createMemoryPromptReader(options: MemoryPromptReaderOptions): (request: MemoryPromptRequest) => string {
38
+ // Copy options now: mutable caller arrays must not silently change eligibility.
39
+ const selection = { ...options.selection, scopes: [...(options.selection?.scopes ?? ["global", "repo", "runtime"])] };
40
+ const experimentalRelevance = options.experimentalRelevance === true;
41
+ const readStore = options.readStore ?? readMemoryStoreSnapshot;
42
+ let frame: { key: string; section: string } | null = null;
43
+ let selected: { key: string; section: string } | null = null;
44
+ return (request) => {
45
+ const dataDir = options.getDataDir();
46
+ const activeRepository = canonicalMemoryRepositoryIdentity(request.cwd);
47
+ const authority = JSON.stringify([
48
+ dataDir,
49
+ request.sessionAuthority,
50
+ request.cwd,
51
+ activeRepository,
52
+ request.targetId,
53
+ request.runtimeId,
54
+ request.modelId,
55
+ ]);
56
+ const frameKey = JSON.stringify([request.turnId, authority]);
57
+ if (request.turnId !== null && frame?.key === frameKey) return frame.section;
58
+ let section = "";
59
+ try {
60
+ const snapshot = readStore(dataDir);
61
+ const relevance = {
62
+ taskText: request.taskText,
63
+ activePaths: [...request.activePaths],
64
+ activeSymbols: [...(request.activeSymbols ?? [])],
65
+ };
66
+ const key = createHash("sha256")
67
+ .update(
68
+ JSON.stringify([
69
+ snapshot.revision,
70
+ authority,
71
+ selection,
72
+ relevance,
73
+ experimentalRelevance,
74
+ MEMORY_RELEVANCE_VERSION,
75
+ ]),
76
+ )
77
+ .digest("hex");
78
+ if (selected?.key === key) section = selected.section;
79
+ else {
80
+ section = buildMemoryPromptSection(snapshot.records, {
81
+ ...selection,
82
+ activeRepository,
83
+ activeRuntime: { kind: "runtime", key: request.runtimeId },
84
+ ...(experimentalRelevance ? { relevance } : {}),
85
+ }).section;
86
+ selected = Object.freeze({ key, section });
87
+ }
88
+ } catch {
89
+ // A failed fresh read revokes old approved content, including its cache.
90
+ selected = null;
91
+ }
92
+ frame = request.turnId === null ? null : Object.freeze({ key: frameKey, section });
93
+ return section;
94
+ };
95
+ }
@@ -1,4 +1,6 @@
1
- import { estimateMemoryTokens, selectApprovedMemory } from "./operations.js";
1
+ import { ceilChars } from "../session/context-accounting.js";
2
+ import { eligibleMemoryRecords } from "./operations.js";
3
+ import { type MemoryRelevanceInput, type RankedMemoryCandidate, rankMemoryByRelevance } from "./relevance.js";
2
4
  import type {
3
5
  MemoryAgentIdentity,
4
6
  MemoryRecord,
@@ -21,6 +23,8 @@ export const MEMORY_PROMPT_DEFAULT_MAX_ITEMS = 5;
21
23
  export const MEMORY_PROMPT_DEFAULT_SCOPES: ReadonlyArray<MemoryScope> = ["global", "repo"];
22
24
 
23
25
  export interface MemoryPromptOptions {
26
+ /** Explicit experiment only; omitted options preserve legacy priority. */
27
+ relevance?: MemoryRelevanceInput;
24
28
  scopes?: ReadonlyArray<MemoryScope>;
25
29
  tokenBudget?: number;
26
30
  maxItems?: number;
@@ -42,18 +46,51 @@ export function selectMemoryForPrompt(
42
46
  records: ReadonlyArray<MemoryRecord>,
43
47
  options: MemoryPromptOptions = {},
44
48
  ): MemoryRecord[] {
49
+ return inspectMemoryPromptSelection(records, options).records;
50
+ }
51
+
52
+ export interface MemoryPromptDecision {
53
+ id: string;
54
+ reason: "selected" | "token-budget" | "item-limit";
55
+ relevance?: Omit<RankedMemoryCandidate, "record">;
56
+ }
57
+
58
+ /** Diagnostics cover eligible candidates only: excluded records are never scored. */
59
+ export function inspectMemoryPromptSelection(
60
+ records: ReadonlyArray<MemoryRecord>,
61
+ options: MemoryPromptOptions = {},
62
+ ): { records: MemoryRecord[]; decisions: MemoryPromptDecision[] } {
45
63
  const scopes = options.scopes ?? MEMORY_PROMPT_DEFAULT_SCOPES;
46
64
  const tokenBudget = options.tokenBudget ?? MEMORY_PROMPT_DEFAULT_TOKEN_BUDGET;
47
65
  const maxItems = options.maxItems ?? MEMORY_PROMPT_DEFAULT_MAX_ITEMS;
48
- if (tokenBudget <= 0 || maxItems <= 0) return [];
49
- const selected = selectApprovedMemory(records, {
66
+ if (!Number.isFinite(tokenBudget) || !Number.isFinite(maxItems) || tokenBudget <= 0 || maxItems < 1)
67
+ return { records: [], decisions: [] };
68
+ const eligibility = {
50
69
  scopes,
51
- tokenBudget,
52
70
  activeRepository: options.activeRepository ?? null,
53
71
  activeRuntime: options.activeRuntime ?? null,
54
72
  activeAgent: options.activeAgent ?? null,
55
- });
56
- return selected.slice(0, maxItems);
73
+ };
74
+ const candidates =
75
+ options.relevance === undefined
76
+ ? eligibleMemoryRecords(records, eligibility).map((record) => ({ record, relevance: undefined }))
77
+ : rankMemoryByRelevance(records, eligibility, options.relevance).map(({ record, ...relevance }) => ({
78
+ record,
79
+ relevance,
80
+ }));
81
+ const selected: MemoryRecord[] = [];
82
+ const decisions: MemoryPromptDecision[] = [];
83
+ for (const { record, relevance } of candidates) {
84
+ const reason =
85
+ selected.length >= Math.floor(maxItems)
86
+ ? "item-limit"
87
+ : ceilChars(renderMemoryPromptSection([...selected, record]).length) > tokenBudget
88
+ ? "token-budget"
89
+ : "selected";
90
+ decisions.push({ id: record.id, reason, ...(relevance === undefined ? {} : { relevance }) });
91
+ if (reason === "selected") selected.push(record);
92
+ }
93
+ return { records: selected, decisions };
57
94
  }
58
95
 
59
96
  /**
@@ -74,10 +111,16 @@ export function renderMemoryPromptSection(records: ReadonlyArray<MemoryRecord>):
74
111
  const lesson = record.lesson.replace(/\s+/g, " ").trim();
75
112
  const evidence = record.evidenceRefs.join(", ");
76
113
  lines.push(`- [${record.id}] (${renderApplicability(record)}) ${lesson} Evidence: ${evidence}.`);
114
+ if (record.appliesWhen.length > 0) lines.push(` Applies when: ${record.appliesWhen.map(compactText).join("; ")}.`);
115
+ if (record.avoidWhen.length > 0) lines.push(` Avoid when: ${record.avoidWhen.map(compactText).join("; ")}.`);
77
116
  }
78
117
  return lines.join("\n");
79
118
  }
80
119
 
120
+ function compactText(value: string): string {
121
+ return value.replace(/\s+/g, " ").trim();
122
+ }
123
+
81
124
  function renderApplicability(record: MemoryRecord): string {
82
125
  if (record.scope === "repo") {
83
126
  if (record.repository !== undefined) {
@@ -101,6 +144,6 @@ export function buildMemoryPromptSection(
101
144
  ): { section: string; records: MemoryRecord[]; tokens: number } {
102
145
  const selected = selectMemoryForPrompt(records, options);
103
146
  const section = renderMemoryPromptSection(selected);
104
- const tokens = selected.reduce((acc, record) => acc + estimateMemoryTokens(record), 0);
147
+ const tokens = ceilChars(section.length);
105
148
  return { section, records: selected, tokens };
106
149
  }
@@ -0,0 +1,93 @@
1
+ import { eligibleMemoryRecords } from "./operations.js";
2
+ import type { MemoryRecord, MemoryRetrievalOptions } from "./types.js";
3
+
4
+ /** Versioned, deterministic heuristic; not a semantic applicability or truth check. */
5
+ export const MEMORY_RELEVANCE_VERSION = "lexical-v1";
6
+ export const MEMORY_RELEVANCE_WEIGHTS = { taskTerm: 1, path: 8, symbol: 4 } as const;
7
+
8
+ export interface MemoryRelevanceInput {
9
+ readonly taskText: string;
10
+ readonly activePaths?: readonly string[];
11
+ readonly activeSymbols?: readonly string[];
12
+ }
13
+
14
+ export interface RankedMemoryCandidate {
15
+ readonly record: MemoryRecord;
16
+ readonly score: number;
17
+ readonly matches: {
18
+ readonly taskTerms: readonly string[];
19
+ readonly paths: readonly string[];
20
+ readonly symbols: readonly string[];
21
+ };
22
+ readonly fallback: boolean;
23
+ }
24
+
25
+ const STOP_WORDS = new Set(["and", "are", "for", "from", "into", "the", "this", "that", "with", "use", "when"]);
26
+
27
+ function normalized(value: string): string {
28
+ return value.normalize("NFKC").toLowerCase();
29
+ }
30
+
31
+ function terms(value: string): string[] {
32
+ return [...new Set(normalized(value.replace(/([a-z])([A-Z])/g, "$1 $2")).match(/[\p{L}\p{N}]+/gu) ?? [])].filter(
33
+ (term) => term.length >= 2 && !STOP_WORDS.has(term),
34
+ );
35
+ }
36
+
37
+ function pathKey(value: string): string {
38
+ return normalized(value).replaceAll("\\", "/").replace(/^\.\//, "");
39
+ }
40
+
41
+ /**
42
+ * Gates are identical to legacy retrieval and run before scoring. Text features
43
+ * use NFKC/lowercase exact Unicode terms; paths/symbols match whole lexical
44
+ * features, never filesystem ancestry or fuzzy substrings. Task features are
45
+ * bounded to 64 terms, 32 explicit paths, and 32 symbols in supplied order.
46
+ * Evidence IDs and avoidWhen do not create positive relevance.
47
+ *
48
+ * One legacy-priority zero-overlap lesson gets an early opportunity after the
49
+ * first positive hit. This can displace a relevant item and cannot guarantee
50
+ * that general constraints fit a finite budget. All other ties retain legacy
51
+ * verification/creation-time then ID order. No input record is mutated.
52
+ */
53
+ export function rankMemoryByRelevance(
54
+ records: ReadonlyArray<MemoryRecord>,
55
+ options: Omit<MemoryRetrievalOptions, "tokenBudget">,
56
+ input: MemoryRelevanceInput,
57
+ ): RankedMemoryCandidate[] {
58
+ const taskTerms = terms(input.taskText).slice(0, 64);
59
+ const paths = [...new Set((input.activePaths ?? []).map(pathKey).filter(Boolean))].slice(0, 32);
60
+ const symbols = [...new Set((input.activeSymbols ?? []).map(normalized).filter(Boolean))].slice(0, 32);
61
+ const candidates = eligibleMemoryRecords(records, options).map((record): RankedMemoryCandidate => {
62
+ const text = [record.key, record.lesson, ...record.appliesWhen].join("\n");
63
+ const textTerms = new Set(terms(text));
64
+ const textPaths = new Set(
65
+ (text.match(/[\p{L}\p{N}_./\\:-]+/gu) ?? []).map((path) => pathKey(path.replace(/[.:]+$/, ""))),
66
+ );
67
+ const textSymbols = new Set(normalized(text).match(/[\p{L}\p{N}_$]+/gu) ?? []);
68
+ const matches = {
69
+ taskTerms: taskTerms.filter((term) => textTerms.has(term)),
70
+ paths: paths.filter((path) => textPaths.has(path)),
71
+ symbols: symbols.filter((symbol) => textSymbols.has(symbol)),
72
+ };
73
+ return {
74
+ record,
75
+ matches,
76
+ fallback: false,
77
+ score:
78
+ matches.taskTerms.length * MEMORY_RELEVANCE_WEIGHTS.taskTerm +
79
+ matches.paths.length * MEMORY_RELEVANCE_WEIGHTS.path +
80
+ matches.symbols.length * MEMORY_RELEVANCE_WEIGHTS.symbol,
81
+ };
82
+ });
83
+ // Stable sorting preserves the already established legacy tie ordering.
84
+ candidates.sort((a, b) => b.score - a.score);
85
+ if ((candidates[0]?.score ?? 0) > 0) {
86
+ const fallbackIndex = candidates.findIndex((candidate) => candidate.score === 0);
87
+ if (fallbackIndex >= 0) {
88
+ const [fallback] = candidates.splice(fallbackIndex, 1);
89
+ if (fallback !== undefined) candidates.splice(1, 0, { ...fallback, fallback: true });
90
+ }
91
+ }
92
+ return candidates;
93
+ }
@@ -0,0 +1,78 @@
1
+ import { ceilChars } from "../session/context-accounting.js";
2
+ import type { TaskMemoryEntry, TaskMemorySnapshot } from "./task-bank.js";
3
+
4
+ export interface MemoryRestorationInput {
5
+ readonly bank: {
6
+ readonly version: TaskMemorySnapshot["version"];
7
+ readonly status: Readonly<TaskMemoryEntry> | null;
8
+ readonly knowledge: readonly Readonly<TaskMemoryEntry>[];
9
+ readonly procedural: readonly Readonly<TaskMemoryEntry>[];
10
+ };
11
+ /** Already admitted current state. Only nonblank text suppresses historical status; it is never recopied. */
12
+ readonly currentState: { readonly kind: "handoff" | "summary"; readonly text: string } | null;
13
+ readonly maxTokens: number;
14
+ }
15
+
16
+ export interface MemoryRestorationRender {
17
+ readonly message: string;
18
+ readonly tokens: number;
19
+ readonly citedEntryIds: readonly string[];
20
+ readonly omittedEntryIds: readonly string[];
21
+ }
22
+
23
+ const PREFIX = "Memory: remembered execution context (unverified; current instructions take precedence).\n";
24
+
25
+ /**
26
+ * Pure conservative renderer. lastTouchedAt is only a recency/usage heuristic,
27
+ * never verification. Equal whitespace-normalized content shares one rendered
28
+ * row with all source IDs; near duplicates remain distinct. Knowledge and
29
+ * procedure have equal access in recency order, with ID as the stable tie.
30
+ * Nothing is deleted, marked injected, promoted, or inferred to be current.
31
+ */
32
+ export function renderMemoryRestoration(input: MemoryRestorationInput): MemoryRestorationRender {
33
+ const hasCurrentState = input.currentState !== null && input.currentState.text.trim().length > 0;
34
+ const source = [
35
+ ...(!hasCurrentState && input.bank.status !== null ? [input.bank.status] : []),
36
+ ...input.bank.knowledge,
37
+ ...input.bank.procedural,
38
+ ].sort((a, b) => {
39
+ const status = Number(b.kind === "status") - Number(a.kind === "status");
40
+ return status || b.lastTouchedAt.localeCompare(a.lastTouchedAt) || a.id.localeCompare(b.id);
41
+ });
42
+ const groups = new Map<string, { ids: string[]; kinds: Set<TaskMemoryEntry["kind"]>; content: string }>();
43
+ for (const entry of source) {
44
+ const content = entry.content.replace(/\s+/gu, " ").trim();
45
+ const group = groups.get(content);
46
+ if (group) {
47
+ if (!group.ids.includes(entry.id)) group.ids.push(entry.id);
48
+ group.kinds.add(entry.kind);
49
+ } else groups.set(content, { ids: [entry.id], kinds: new Set([entry.kind]), content: entry.content });
50
+ }
51
+ const lines: string[] = [];
52
+ const cited: string[] = [];
53
+ const omitted = hasCurrentState && input.bank.status !== null ? [input.bank.status.id] : [];
54
+ const validBudget = Number.isFinite(input.maxTokens) && input.maxTokens > 0;
55
+ for (const [content, group] of groups) {
56
+ const kinds = [...group.kinds]
57
+ .sort()
58
+ .map((kind) => (kind === "status" ? "historical status" : `remembered ${kind}`))
59
+ .join("/");
60
+ // Quote the chosen source losslessly, including its original whitespace.
61
+ // Normalization above is only the dedup key, never a rewrite of evidence.
62
+ const line = `- ${group.ids.map((id) => `[${id}]`).join(" ")} ${kinds}: ${JSON.stringify(group.content)}`;
63
+ const candidate = PREFIX + [...lines, line].join("\n");
64
+ if (!validBudget || content.length === 0 || ceilChars(candidate.length) > input.maxTokens) {
65
+ omitted.push(...group.ids);
66
+ continue;
67
+ }
68
+ lines.push(line);
69
+ cited.push(...group.ids);
70
+ }
71
+ const message = lines.length > 0 ? PREFIX + lines.join("\n") : "";
72
+ return Object.freeze({
73
+ message,
74
+ tokens: ceilChars(message.length),
75
+ citedEntryIds: Object.freeze(cited),
76
+ omittedEntryIds: Object.freeze(omitted),
77
+ });
78
+ }
@@ -1,4 +1,5 @@
1
- import { readFileSync } from "node:fs";
1
+ import { createHash } from "node:crypto";
2
+ import { closeSync, openSync, readFileSync, readSync } from "node:fs";
2
3
  import { readFile } from "node:fs/promises";
3
4
  import { join } from "node:path";
4
5
  import { safeResourceWrite } from "../../core/safe-resource-write.js";
@@ -8,6 +9,43 @@ import { validateMemoryStore } from "./validate.js";
8
9
  export const MEMORY_STORE_MAX_RECORDS = 500;
9
10
  export const MEMORY_STALE_APPROVED_DAYS = 180;
10
11
  export const MEMORY_STALE_UNAPPROVED_DAYS = 30;
12
+ /** Prompt compilation fails closed above this read ceiling; store writers are unchanged. */
13
+ export const MEMORY_PROMPT_STORE_MAX_BYTES = 16 * 1024 * 1024;
14
+
15
+ export interface MemoryStoreSnapshot {
16
+ revision: string;
17
+ records: MemoryRecord[];
18
+ }
19
+
20
+ /** Hash the exact bounded bytes read, so same-size external replacements invalidate selection. */
21
+ export function readMemoryStoreSnapshot(dataDir: string): MemoryStoreSnapshot {
22
+ const path = memoryStorePath(dataDir);
23
+ let fd: number;
24
+ try {
25
+ fd = openSync(path, "r");
26
+ } catch (error) {
27
+ if (isErrorWithCode(error) && error.code === "ENOENT") return { revision: "missing", records: [] };
28
+ throw error;
29
+ }
30
+ const chunks: Buffer[] = [];
31
+ let total = 0;
32
+ try {
33
+ while (true) {
34
+ const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, MEMORY_PROMPT_STORE_MAX_BYTES + 1 - total));
35
+ const count = readSync(fd, chunk);
36
+ if (count === 0) break;
37
+ total += count;
38
+ if (total > MEMORY_PROMPT_STORE_MAX_BYTES) throw new Error("memory prompt store exceeds read ceiling");
39
+ chunks.push(chunk.subarray(0, count));
40
+ }
41
+ } finally {
42
+ closeSync(fd);
43
+ }
44
+ const raw = Buffer.concat(chunks);
45
+ const records = parseAndValidate(raw.toString("utf8"), path);
46
+ if (records.length > MEMORY_STORE_MAX_RECORDS) throw new Error("memory prompt store exceeds record limit");
47
+ return { revision: createHash("sha256").update(raw).digest("hex"), records };
48
+ }
11
49
 
12
50
  export function memoryRoot(dataDir: string): string {
13
51
  return join(dataDir, "memory");
@@ -8,6 +8,13 @@ import {
8
8
  sanitizeToolResultDigest,
9
9
  type ToolResultDigest,
10
10
  } from "../../tools/result-disposition.js";
11
+ import {
12
+ type MemoryCommitScope,
13
+ MemoryCommitState,
14
+ type MemoryRestorationOffer,
15
+ type SuccessfulMemoryContextCommit,
16
+ } from "../memory/commit-state.js";
17
+ import type { MemoryRestorationInput } from "../memory/restoration.js";
11
18
  import { TASK_MEMORY_DEFAULT_PROCEDURAL_CAP, type TaskMemoryBank, type TaskMemoryEntry } from "../memory/task-bank.js";
12
19
  import {
13
20
  runTaskMemoryPolicy,
@@ -119,7 +126,7 @@ export interface MemoryInterventionDeps {
119
126
  * closed. The composition root buffers it into the next submitted turn, which
120
127
  * is where a synchronous turn_end reminder would have landed anyway.
121
128
  */
122
- onDeferredReminder?: (message: string) => void;
129
+ onDeferredReminder?: (message: string, isCurrent?: () => boolean) => void;
123
130
  /**
124
131
  * False when no further turn will be submitted in this process. A background
125
132
  * step is detached from the boundary that triggered it and a local route
@@ -145,6 +152,14 @@ export interface MemoryPromptedStepResult extends TaskMemoryPolicyResult {
145
152
  }
146
153
 
147
154
  export interface MemoryInterventionRegistration extends MiddlewareHookRegistration {
155
+ bindCommitScope(scope: MemoryCommitScope): void;
156
+ notifyContextCommitted(input: SuccessfulMemoryContextCommit): void;
157
+ prepareRestoration(
158
+ currentState: MemoryRestorationInput["currentState"],
159
+ maxTokens: number,
160
+ ): MemoryRestorationOffer | null;
161
+ acknowledgeRestoration(offer: MemoryRestorationOffer): boolean;
162
+ isContentCurrent(): () => boolean;
148
163
  /** Invalidate pending content and discard all transient session/branch state. */
149
164
  reset(): void;
150
165
  /** Invalidate immediately without waiting for the model; refuse further work. */
@@ -177,6 +192,20 @@ export interface MemoryInterventionRegistration extends MiddlewareHookRegistrati
177
192
  */
178
193
  export function createMemoryInterventionRegistration(deps: MemoryInterventionDeps): MemoryInterventionRegistration {
179
194
  let generation = 0;
195
+ let commitState: MemoryCommitState | null = null;
196
+ let commitScope: MemoryCommitScope | null = null;
197
+ let commitBridgeEnabled = false;
198
+ const captureContentGuard = (): (() => boolean) => {
199
+ const capturedGeneration = generation;
200
+ const owner = commitState;
201
+ const scope = commitScope;
202
+ const stamp = owner?.capture();
203
+ return () =>
204
+ !disposed &&
205
+ generation === capturedGeneration &&
206
+ commitState === owner &&
207
+ (!owner || (!!stamp && !!scope && owner.isCurrent(stamp, scope)));
208
+ };
180
209
  let generationController = new AbortController();
181
210
  let observedSessionId: string | undefined;
182
211
  let disposed = false;
@@ -207,6 +236,40 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
207
236
  let consecutiveLlmTimeouts = 0;
208
237
 
209
238
  return {
239
+ bindCommitScope(scope) {
240
+ commitBridgeEnabled = true;
241
+ if (
242
+ commitScope?.sessionId === scope.sessionId &&
243
+ commitScope.branchAnchorTurnId === scope.branchAnchorTurnId &&
244
+ commitState
245
+ )
246
+ return;
247
+ if (commitState) reset();
248
+ commitScope = { ...scope };
249
+ commitState = new MemoryCommitState(scope, generation);
250
+ observedSessionId = scope.sessionId;
251
+ },
252
+ notifyContextCommitted(input) {
253
+ commitState?.notifyContextCommitted(input, generation);
254
+ },
255
+ prepareRestoration(currentState, maxTokens) {
256
+ if (!commitScope || !settings().enabled) return null;
257
+ return (
258
+ commitState?.prepareRestoration({
259
+ scope: commitScope,
260
+ generation,
261
+ bank: deps.bank.snapshot(),
262
+ currentState,
263
+ maxTokens: Math.min(maxTokens, settings().maxTokens),
264
+ }) ?? null
265
+ );
266
+ },
267
+ acknowledgeRestoration(offer) {
268
+ if (!commitScope || !commitState?.acknowledgeRestoration(offer, commitScope, generation)) return false;
269
+ deps.bank.recordInjection(offer.citedEntryIds);
270
+ return true;
271
+ },
272
+ isContentCurrent: captureContentGuard,
210
273
  reset,
211
274
  dispose(): void {
212
275
  disposed = true;
@@ -257,7 +320,7 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
257
320
  // Recall grows the working set; it is an observability point, not
258
321
  // context loss that should reactivate compacted task memory.
259
322
  if (input.metadata?.stage === "working_set_recall") return NO_EFFECTS;
260
- reactivateAfterCompaction = true;
323
+ if (!commitBridgeEnabled) reactivateAfterCompaction = true;
261
324
  return NO_EFFECTS;
262
325
  case "turn_start": {
263
326
  if (input.text?.trim()) currentTask = shortText(input.text, 2_000);
@@ -346,6 +409,7 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
346
409
  false);
347
410
  rulesInjectedSincePromptedStep = false;
348
411
  const stepGeneration = generation;
412
+ const contentCurrent = captureContentGuard();
349
413
  promptedStepInFlight = true;
350
414
  outstandingStep = runPromptedStep({
351
415
  deterministicTrigger: triggers.some((trigger) => trigger !== "interval"),
@@ -353,14 +417,14 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
353
417
  triggerReasons: triggers,
354
418
  })
355
419
  .then((result) => {
356
- if (stepGeneration !== generation || disposed) return;
420
+ if (!contentCurrent()) return;
357
421
  // A rules-only reminder can win the visible boundary while the optional
358
422
  // background route resolves to null. Preserve the operator-visible
359
423
  // injected outcome instead of overwriting it with that no-client silence.
360
424
  if (rulesAlreadySpoke && result.decision === "silent") lastDecision = "injected";
361
425
  if (result.reminder === null) return;
362
426
  lastInjectedMessage = result.reminder;
363
- deps.onDeferredReminder?.(result.reminder);
427
+ deps.onDeferredReminder?.(result.reminder, contentCurrent);
364
428
  })
365
429
  .catch(() => {
366
430
  // runPromptedStep already resolves failures to silence; this only
@@ -393,6 +457,7 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
393
457
  // Retain completed bank entries, but revoke pending content and delivery
394
458
  // authority. Late provider usage still belongs to its captured origin.
395
459
  generation += 1;
460
+ commitState?.cancel(generation);
396
461
  generationController.abort();
397
462
  generationController = new AbortController();
398
463
  promptedStepInFlight = false;
@@ -408,6 +473,9 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
408
473
  }
409
474
 
410
475
  function reset(): void {
476
+ commitState?.dispose();
477
+ commitState = null;
478
+ commitScope = null;
411
479
  generation += 1;
412
480
  generationController.abort();
413
481
  generationController = new AbortController();
@@ -466,7 +534,8 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
466
534
  });
467
535
  const live = settings();
468
536
  const stepGeneration = generation;
469
- const isCurrent = () => !disposed && stepGeneration === generation;
537
+ const isCurrent = captureContentGuard();
538
+ const isUsageCurrent = () => !disposed && stepGeneration === generation;
470
539
  const usageSink = deps.captureStepUsage?.();
471
540
  if (disposed || !live.enabled) {
472
541
  const result = silent("no_client");
@@ -504,7 +573,7 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
504
573
  isCurrent,
505
574
  signal: generationController.signal,
506
575
  onStepUsage: (usage) => {
507
- if (usageSink) usageSink(usage, isCurrent());
576
+ if (usageSink) usageSink(usage, isUsageCurrent());
508
577
  else deps.onStepUsage?.(usage);
509
578
  },
510
579
  task: input.task?.trim() || currentTask,
@@ -515,7 +584,13 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
515
584
  ...(input.suppressIntervention === undefined ? {} : { suppressIntervention: input.suppressIntervention }),
516
585
  previousReminder: lastInjectedMessage,
517
586
  timeoutMs,
518
- ...(deps.onEnvelope === undefined ? {} : { onEnvelope: deps.onEnvelope }),
587
+ ...(deps.onEnvelope === undefined
588
+ ? {}
589
+ : {
590
+ onEnvelope: (envelope: TaskMemoryEnvelope) => {
591
+ if (isCurrent()) deps.onEnvelope?.(envelope);
592
+ },
593
+ }),
519
594
  });
520
595
  const remainingMs = () => Math.floor(live.timeoutMs - Number(process.hrtime.bigint() - started) / 1_000_000);
521
596
  const initialTimeoutMs = remainingMs();
@@ -557,17 +632,18 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
557
632
  // runTaskMemoryPolicy resolves its own failures, so reaching here means
558
633
  // resolving the client itself threw. That is still a broken route.
559
634
  promptedResult = silent("client_error");
560
- deps.onEnvelope?.({
561
- systemPrompt: "",
562
- userPrompt: "",
563
- response: "",
564
- decision: "silent",
565
- reason: "client_error",
566
- bankOperations: 0,
567
- droppedOperations: 0,
568
- reminder: null,
569
- error: error instanceof Error ? error.message : "resolving the background model client failed",
570
- });
635
+ if (isCurrent())
636
+ deps.onEnvelope?.({
637
+ systemPrompt: "",
638
+ userPrompt: "",
639
+ response: "",
640
+ decision: "silent",
641
+ reason: "client_error",
642
+ bankOperations: 0,
643
+ droppedOperations: 0,
644
+ reminder: null,
645
+ error: error instanceof Error ? error.message : "resolving the background model client failed",
646
+ });
571
647
  }
572
648
  if (!isCurrent()) return { ...promptedResult, reminder: null, effects: NO_EFFECTS };
573
649
  if (promptedResult.reason === "endpoint_busy") telemetryDecision = "dropped";
@@ -705,6 +781,14 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
705
781
  };
706
782
  trajectory.push(step);
707
783
  if (trajectory.length > live.windowSteps) trajectory.splice(0, trajectory.length - live.windowSteps);
784
+ if (input.metadata?.resultKind === "ok") {
785
+ // Only an explicit receipt for this exact operation closes its failure
786
+ // episode. Keep the bank entry and trajectory as history; this does not
787
+ // verify the surrounding task or reinterpret tool exit classifications.
788
+ failures.delete(step.operationFingerprint);
789
+ annotatedThisTurn.delete(step.operationFingerprint);
790
+ if (lastInjectedOperationFingerprint === step.operationFingerprint) lastInjectedOperationFingerprint = null;
791
+ }
708
792
  if (outcome !== "error") return NO_EFFECTS;
709
793
  rememberFailure(step);
710
794
  return annotateRepeatedFailure(step);
@@ -718,11 +802,15 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
718
802
  */
719
803
  function annotateRepeatedFailure(step: TrajectoryStep): ReadonlyArray<MiddlewareEffect> {
720
804
  if (annotatedThisTurn.has(step.operationFingerprint)) return NO_EFFECTS;
805
+ const failure = failures.get(step.operationFingerprint);
806
+ if (failure === undefined) return NO_EFFECTS;
721
807
  const occurrences = trajectory.filter(
722
- (candidate) => candidate.outcome === "error" && candidate.operationFingerprint === step.operationFingerprint,
808
+ (candidate) =>
809
+ candidate.outcome === "error" &&
810
+ candidate.operationFingerprint === step.operationFingerprint &&
811
+ candidate.step >= failure.firstStep,
723
812
  ).length;
724
- const failure = failures.get(step.operationFingerprint);
725
- if (occurrences < 2 || failure === undefined) return NO_EFFECTS;
813
+ if (occurrences < 2) return NO_EFFECTS;
726
814
  const message = boundedReminder(
727
815
  `Memory: [${failure.entryId}] you already tried ${failure.callDescription} at step ${failure.firstStep} and it failed with ${failure.errorDigest}. Change the approach rather than repeating it.`,
728
816
  settings().maxTokens,
@@ -780,11 +868,15 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
780
868
  ) {
781
869
  continue;
782
870
  }
871
+ const failure = failures.get(step.operationFingerprint);
872
+ if (failure === undefined || step.step < failure.firstStep) continue;
783
873
  const occurrences = trajectory.filter(
784
- (candidate) => candidate.outcome === "error" && candidate.operationFingerprint === step.operationFingerprint,
874
+ (candidate) =>
875
+ candidate.outcome === "error" &&
876
+ candidate.operationFingerprint === step.operationFingerprint &&
877
+ candidate.step >= failure.firstStep,
785
878
  ).length;
786
- const failure = failures.get(step.operationFingerprint);
787
- if (occurrences < 2 || failure === undefined) continue;
879
+ if (occurrences < 2) continue;
788
880
  const message = boundedReminder(
789
881
  `Memory: [${failure.entryId}] you already tried ${failure.callDescription} at step ${failure.firstStep} and it failed with ${failure.errorDigest}.`,
790
882
  settings().maxTokens,