@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
@@ -22,6 +22,7 @@ import { extractText, hasStructuredToolCall, toolNamesFromAgentState } from "./c
22
22
  import type { AgentRuntime, ChatTurnState } from "./turn-state.js";
23
23
 
24
24
  export interface TurnMiddlewareDeps {
25
+ memoryContentGuard?: (() => () => boolean) | undefined;
25
26
  state: ChatTurnState;
26
27
  middleware?: MiddlewareContract | undefined;
27
28
  session?: SessionContract | undefined;
@@ -50,6 +51,7 @@ export interface TurnMiddleware {
50
51
  tokensBefore?: number,
51
52
  ): void;
52
53
  flushPendingReminders(): string;
54
+ takePendingReminderProjection(): () => string;
53
55
  clearPendingReminders(): void;
54
56
  /**
55
57
  * Deliver a reminder produced after its own turn boundary already closed.
@@ -57,7 +59,7 @@ export interface TurnMiddleware {
57
59
  * value; the reminder joins the same buffer, ledger, and transcript path a
58
60
  * turn_end reminder takes.
59
61
  */
60
- injectDeferredReminder(message: string, severity?: MiddlewareReminderSeverity): void;
62
+ injectDeferredReminder(message: string, severity?: MiddlewareReminderSeverity, isCurrent?: () => boolean): void;
61
63
  /**
62
64
  * Deliver an operator-facing finding produced after its turn boundary closed.
63
65
  * The watchdog seam: its run settles well after the turn it reviewed, and
@@ -76,11 +78,29 @@ export function createTurnMiddleware(deps: TurnMiddlewareDeps): TurnMiddleware {
76
78
  // injections). The next accepted prompt flushes them into the model
77
79
  // request as one system-reminder block. Completed background reminders
78
80
  // can also drain at a native tool-batch boundary; session switches clear all.
79
- const pendingReminders: Array<{ message: string; severity: MiddlewareReminderSeverity; deferred: boolean }> = [];
81
+ const pendingReminders: Array<{
82
+ message: string;
83
+ severity: MiddlewareReminderSeverity;
84
+ deferred: boolean;
85
+ isCurrent?: (() => boolean) | undefined;
86
+ }> = [];
87
+ const dropStale = () => {
88
+ for (let index = pendingReminders.length - 1; index >= 0; index--) {
89
+ if (pendingReminders[index]?.isCurrent?.() === false) pendingReminders.splice(index, 1);
90
+ }
91
+ };
80
92
 
81
- const bufferReminder = (message: string, severity: MiddlewareReminderSeverity, deferred = false): void => {
93
+ const bufferReminder = (
94
+ message: string,
95
+ severity: MiddlewareReminderSeverity,
96
+ deferred = false,
97
+ isCurrent?: () => boolean,
98
+ ): void => {
99
+ dropStale();
100
+ isCurrent ??= message.startsWith("Memory:") ? deps.memoryContentGuard?.() : undefined;
101
+ if (isCurrent?.() === false) return;
82
102
  if (pendingReminders.some((entry) => entry.message === message && entry.severity === severity)) return;
83
- pendingReminders.push({ message, severity, deferred });
103
+ pendingReminders.push({ message, severity, deferred, isCurrent });
84
104
  };
85
105
 
86
106
  const runMiddlewareTurnHook = (input: MiddlewareHookInput): ReadonlyArray<MiddlewareEffect> => {
@@ -194,6 +214,7 @@ export function createTurnMiddleware(deps: TurnMiddlewareDeps): TurnMiddleware {
194
214
  [],
195
215
  );
196
216
  if (!current()) return false;
217
+ dropStale();
197
218
  const ready = pendingReminders.filter((entry) => entry.deferred);
198
219
  if (ready.length === 0) return false;
199
220
  const content = `<system-reminder>\n${[...new Set(ready.map((entry) => entry.message))].join("\n\n")}\n</system-reminder>`;
@@ -314,7 +335,17 @@ export function createTurnMiddleware(deps: TurnMiddlewareDeps): TurnMiddleware {
314
335
  });
315
336
  },
316
337
 
338
+ takePendingReminderProjection(): () => string {
339
+ const entries = pendingReminders.splice(0);
340
+ return () => {
341
+ const messages = [
342
+ ...new Set(entries.filter((entry) => entry.isCurrent?.() !== false).map((entry) => entry.message)),
343
+ ];
344
+ return messages.length ? `<system-reminder>\n${messages.join("\n\n")}\n</system-reminder>` : "";
345
+ };
346
+ },
317
347
  flushPendingReminders(): string {
348
+ dropStale();
318
349
  if (pendingReminders.length === 0) return "";
319
350
  // Severity affects host actions, not the text delivered to the model.
320
351
  // A paired continuation/info and reminder/warn still needs one copy.
@@ -327,14 +358,16 @@ export function createTurnMiddleware(deps: TurnMiddlewareDeps): TurnMiddleware {
327
358
  pendingReminders.length = 0;
328
359
  },
329
360
 
330
- injectDeferredReminder(message, severity = "advisory"): void {
361
+ injectDeferredReminder(message, severity = "advisory", isCurrent): void {
362
+ dropStale();
363
+ if (isCurrent?.() === false) return;
331
364
  const text = message.trim();
332
365
  if (text.length === 0) return;
333
366
  // A deferred reminder never carries hard-block authority: the turn it
334
367
  // could have interrupted is already over.
335
368
  const level: MiddlewareReminderSeverity = severity === "hard-block" ? "advisory" : severity;
336
369
  if (pendingReminders.some((entry) => entry.message === text)) return;
337
- bufferReminder(text, level, true);
370
+ bufferReminder(text, level, true, isCurrent);
338
371
  appendMiddlewareReminderEntry(text, level);
339
372
  deps.emitNotice(text);
340
373
  },
@@ -60,7 +60,11 @@ export interface TurnPersistenceDeps {
60
60
  export interface TurnPersistence {
61
61
  /** True when this exact assistant message object was already persisted. */
62
62
  wasPersisted(message: unknown): boolean;
63
- appendAssistantTurn(message: AgentMessage, timing?: AssistantCallTiming | null): void;
63
+ appendAssistantTurn(
64
+ message: AgentMessage,
65
+ timing?: AssistantCallTiming | null,
66
+ deliveryId?: string,
67
+ ): string | undefined;
64
68
  appendQueuedUserTurn(message: AgentMessage): void;
65
69
  appendToolCallTurn(event: Extract<AgentEvent, { type: "tool_execution_start" }>): void;
66
70
  appendToolResultTurn(
@@ -226,7 +230,11 @@ export function createTurnPersistence(deps: TurnPersistenceDeps): TurnPersistenc
226
230
  };
227
231
  };
228
232
 
229
- const appendAssistantTurn = (message: AgentMessage, timing?: AssistantCallTiming | null): void => {
233
+ const appendAssistantTurn = (
234
+ message: AgentMessage,
235
+ timing?: AssistantCallTiming | null,
236
+ deliveryId?: string,
237
+ ): string | undefined => {
230
238
  if (message?.role !== "assistant") return;
231
239
  if (persistedAssistantMessages.has(message)) return;
232
240
  // A loop-guard interrupt already persisted a durable closing turn with the
@@ -237,6 +245,10 @@ export function createTurnPersistence(deps: TurnPersistenceDeps): TurnPersistenc
237
245
  const failure = terminalFailureFromAssistantMessage(message);
238
246
  const payload = assistantSessionPayload(message, failure);
239
247
  if (timing) payload.timing = timing;
248
+ if (deliveryId) {
249
+ payload.continuityDeliveryId = deliveryId;
250
+ payload.stopReason = message.stopReason;
251
+ }
240
252
  const usage = (message as { usage?: Usage }).usage;
241
253
  if (usage && typeof usage === "object") {
242
254
  const backendTimings = (message as { backendTimings?: BackendCompletionTimings }).backendTimings;
@@ -279,6 +291,7 @@ export function createTurnPersistence(deps: TurnPersistenceDeps): TurnPersistenc
279
291
  tokens: traceUsage?.totalTokens ?? null,
280
292
  });
281
293
  if (!turnContinues) finishTracedTurn(failure ? "fail" : "success", failure?.errorMessage ?? null);
294
+ return turn.id;
282
295
  };
283
296
 
284
297
  return {
@@ -72,7 +72,7 @@ export interface TurnPrewarmDeps {
72
72
  ) => Promise<{ ok: true; runtime: AgentRuntime; apiKey: string | undefined } | { ok: false; reason: string }>;
73
73
  /** Freeze the session tool surface onto the agent, exactly as a submit does. */
74
74
  applySessionTools: (runtime: AgentRuntime) => void;
75
- /** Report the round's provider usage under its own `/cost` label. */
75
+ /** Report the round's provider usage under its own `/usage` label. */
76
76
  recordUsage: (runtime: AgentRuntime, usage: Usage | null) => void;
77
77
  /**
78
78
  * Claim one in-flight request on the runtime's endpoint for the duration of
@@ -1,4 +1,8 @@
1
+ import { requestFits } from "../domains/context/budget/request-fit.js";
2
+ import { estimateInputTokensFromContext } from "../engine/apis/output-budget.js";
1
3
  import { resolvedRequestContext } from "../engine/context.js";
4
+ import type { ContinuityController } from "./continuity-controller.js";
5
+ import { resolveTurnOutputReserve } from "./output-reserve.js";
2
6
  /**
3
7
  * Turn runtime ownership: orchestrator target resolution, model synthesis,
4
8
  * live-agent construction and model hot-swap, thinking-level reconciliation,
@@ -133,6 +137,8 @@ export interface TurnRuntimeDeps {
133
137
  middlewareToolChoice: MiddlewareToolChoiceControl;
134
138
  persistence: TurnPersistence;
135
139
  context: TurnContext;
140
+ continuity?: ContinuityController;
141
+ hasQueuedSteering?: () => boolean;
136
142
  middleware: TurnMiddleware;
137
143
  retrySettings: () => RetrySettings;
138
144
  /** Stable Clio session id forwarded only to gateway runtimes that understand it. */
@@ -512,6 +518,37 @@ export function createTurnRuntime(deps: TurnRuntimeDeps): TurnRuntime {
512
518
  },
513
519
  maxRetryDelayMs: deps.retrySettings().maxDelayMs,
514
520
  ...(gatewaySessionId ? { sessionId: gatewaySessionId } : {}),
521
+ beforeToolBatch: ({ assistantMessage }) => {
522
+ const calls = assistantMessage.content.filter((block) => block.type === "toolCall");
523
+ if (calls.some((call) => call.name === "self_compact") && calls.length !== 1) {
524
+ return { reason: "self_compact must be the only call in its batch; no sibling was executed." };
525
+ }
526
+ return undefined;
527
+ },
528
+ beforeStreamRequest: ({ context: request }) => {
529
+ setGlobalDefaultMaxOutputTokens(deps.getSettings().chat.maxOutputTokens);
530
+ if (state.activeInterruptReason !== null || state.runtime !== localRuntime) {
531
+ return { block: true, reason: "Request ownership changed before invocation." };
532
+ }
533
+ if (deps.hasQueuedSteering?.() && deps.continuity?.admission().block === false) {
534
+ const handoff = deps.continuity.admission();
535
+ if (!handoff.block && handoff.correlationId)
536
+ return { block: true, reason: "Operator steering arrived during handoff preparation; handoff paused." };
537
+ }
538
+ const view = context.refreshLiveBudget();
539
+ const resolved = resolvedRequestContext(request);
540
+ const actual = state.synthesisToolLock
541
+ ? { ...resolved, systemPrompt: lockedSynthesisSystemPrompt(resolved.systemPrompt ?? "") }
542
+ : request;
543
+ const input = Math.max(estimateInputTokensFromContext(actual), view.inputTokens ?? 0);
544
+ const output = resolveTurnOutputReserve(localRuntime, input);
545
+ return requestFits(input, output, view.effectiveWindow)
546
+ ? (deps.continuity?.admission() ?? { block: false })
547
+ : {
548
+ block: true,
549
+ reason: `Context window exceeded: input ${input} + output ${output}, window ${view.effectiveWindow ?? "unknown"}.`,
550
+ };
551
+ },
515
552
  onStreamInvocation: () => {
516
553
  setGlobalDefaultMaxOutputTokens(deps.getSettings().chat.maxOutputTokens);
517
554
  apiCallStartedAt = performance.now();
@@ -634,19 +671,22 @@ export function createTurnRuntime(deps: TurnRuntimeDeps): TurnRuntime {
634
671
  stallTimer.unref?.();
635
672
  };
636
673
 
637
- handle.agent.prepareNextTurn = async (signal?: AbortSignal) => {
674
+ handle.agent.prepareNextTurnWithContext = async (_completed, signal?: AbortSignal) => {
638
675
  stallSuspendDepth += 1;
639
676
  try {
640
677
  if (signal?.aborted || state.activeInterruptReason !== null) return undefined;
641
- const contextChanged = await middleware.prepareToolContinuation(localRuntime, signal);
678
+ if (deps.hasQueuedSteering?.()) await deps.continuity?.pause();
679
+ const handoffChanged = (await deps.continuity?.settle(signal)) ?? false;
680
+ const contextChanged = (await middleware.prepareToolContinuation(localRuntime, signal)) || handoffChanged;
642
681
  const update = await context.postToolContinuationGuard(localRuntime, signal, contextChanged);
682
+ const restored = context.installMemoryRestoration(localRuntime);
643
683
  if (signal?.aborted || state.activeInterruptReason !== null) return undefined;
644
684
  // Compaction owns any replacement snapshot. Without one, publish the
645
685
  // already-accounted reminder through context, not message events that
646
686
  // would manufacture a new operator turn and reset task authority.
647
687
  return (
648
- update ??
649
- (contextChanged
688
+ (!restored ? update : undefined) ??
689
+ (contextChanged || restored
650
690
  ? {
651
691
  context: {
652
692
  messages: [...localRuntime.agent.state.messages],
@@ -950,7 +990,9 @@ export function createTurnRuntime(deps: TurnRuntimeDeps): TurnRuntime {
950
990
  apiMs: Math.round(Math.max(0, eventClock - apiCallStartedAt)),
951
991
  }
952
992
  : null;
953
- persistence.appendAssistantTurn(enrichedEvent.message, timing);
993
+ const correlationId = event.type === "message_end" ? handle.requestCorrelationId(event.message) : undefined;
994
+ const persistedId = persistence.appendAssistantTurn(enrichedEvent.message, timing, correlationId);
995
+ await deps.continuity?.response(persistedId, correlationId);
954
996
  if (isAssistant) apiCallStartedAt = null;
955
997
  const usage = (enrichedEvent.message as { usage?: Usage }).usage;
956
998
  if (isAssistant && usage && typeof usage === "object" && runFirstCallVerdict === null) {
@@ -974,6 +1016,7 @@ export function createTurnRuntime(deps: TurnRuntimeDeps): TurnRuntime {
974
1016
  : null;
975
1017
  }
976
1018
  if (enrichedEvent.type === "agent_end") {
1019
+ await deps.continuity?.pause();
977
1020
  clearStallTimer();
978
1021
  const terminal = pendingTerminalToolResult;
979
1022
  pendingTerminalToolResult = null;
@@ -11,19 +11,22 @@ import {
11
11
  type ObservabilityContract,
12
12
  type ObservabilitySnapshot,
13
13
  } from "../domains/observability/index.js";
14
+ import type { UsageSnapshot } from "../domains/quota/types.js";
14
15
  import {
15
16
  foldPromptCacheTelemetry,
16
17
  hasPromptCacheTelemetry,
17
18
  type PromptCacheTelemetry,
18
19
  type SessionEntry,
19
20
  } from "../domains/session/index.js";
20
- import type { Component, OverlayHandle, TUI } from "../engine/tui.js";
21
- import { buildHint, showClioOverlayFrame } from "./overlay-frame.js";
21
+ import { type Component, matchesKey, type OverlayHandle, type TUI, wrapTextWithAnsi } from "../engine/tui.js";
22
+ import type { DispatchBoardRow } from "./dispatch-board.js";
23
+ import { buildResponsiveHint, showClioOverlayFrame } from "./overlay-frame.js";
24
+ import { quotaMeter, renderQuotaAccounts, renderWorkerUsage } from "./quota-view.js";
22
25
  import { clioTheme, rule } from "./theme/index.js";
23
26
 
24
- const DEFAULT_CONTENT_WIDTH = 80;
27
+ const DEFAULT_CONTENT_WIDTH = 104;
25
28
 
26
- export const COST_OVERLAY_WIDTH = DEFAULT_CONTENT_WIDTH + 4;
29
+ export const USAGE_OVERLAY_WIDTH = DEFAULT_CONTENT_WIDTH + 4;
27
30
 
28
31
  export interface CostRow {
29
32
  providerId: string;
@@ -295,19 +298,9 @@ function modelBlock(row: CostRow): string[] {
295
298
  ]);
296
299
  }
297
300
 
298
- function formatCostOverlayBodyLines(
299
- totalCost: CostAggregate,
300
- totalTokens: number,
301
- rows: ReadonlyArray<CostRow>,
302
- contentWidth: number,
303
- promptCache: PromptCacheTelemetry | null = null,
304
- ): string[] {
301
+ function modelDetailLines(rows: ReadonlyArray<CostRow>, contentWidth: number): string[] {
305
302
  const theme = clioTheme();
306
- const lines: string[] = [];
307
- for (const line of summaryBlock(totalCost, totalTokens, rows, promptCache)) {
308
- lines.push(line);
309
- }
310
- lines.push(rule(theme, contentWidth));
303
+ const lines: string[] = [rule(theme, contentWidth)];
311
304
  if (rows.length === 0) {
312
305
  lines.push(theme.fg("muted", "no token usage recorded for this session"));
313
306
  } else {
@@ -352,32 +345,116 @@ function buildCostSnapshot(
352
345
  };
353
346
  }
354
347
 
355
- export interface OpenCostOverlayOptions {
348
+ export interface OpenUsageOverlayOptions {
349
+ getDispatchRows?: () => ReadonlyArray<DispatchBoardRow>;
350
+ getQuotaSnapshots?: () => ReadonlyArray<UsageSnapshot>;
356
351
  sessionId?: string | null;
357
352
  /** Session ledger, read on every render so newly settled and branched calls appear. */
358
353
  getSessionEntries?: () => ReadonlyArray<SessionEntry>;
359
354
  }
360
355
 
361
- class CostOverlayBody implements Component {
362
- constructor(private readonly getSnapshot: () => CostSnapshot) {}
356
+ const USAGE_TABS = ["Accounts", "Session", "Models", "Workers"] as const;
357
+
358
+ class UsageOverlayBody implements Component {
359
+ private tab = 0;
360
+ private offsets = [0, 0, 0, 0];
361
+ private height = 20;
362
+ private pageHeight = 16;
363
+ private lineCount = 0;
364
+
365
+ constructor(
366
+ private readonly getSnapshot: () => CostSnapshot,
367
+ private readonly getQuotaSnapshots: () => ReadonlyArray<UsageSnapshot>,
368
+ private readonly getDispatchRows: () => ReadonlyArray<DispatchBoardRow>,
369
+ private readonly requestRender: () => void,
370
+ ) {}
371
+
372
+ setHeight(rows: number): void {
373
+ this.height = Math.max(1, rows - 3);
374
+ }
375
+
376
+ handleInput(data: string): void {
377
+ const selected = (["1", "2", "3", "4"] as const).findIndex((key) => matchesKey(data, key));
378
+ if (selected >= 0) this.tab = selected;
379
+ else if (matchesKey(data, "tab") || matchesKey(data, "right")) this.tab = (this.tab + 1) % USAGE_TABS.length;
380
+ else if (matchesKey(data, "shift+tab") || matchesKey(data, "left"))
381
+ this.tab = (this.tab + USAGE_TABS.length - 1) % USAGE_TABS.length;
382
+ else {
383
+ let offset = this.offsets[this.tab] ?? 0;
384
+ if (matchesKey(data, "up")) offset--;
385
+ else if (matchesKey(data, "down")) offset++;
386
+ else if (matchesKey(data, "pageUp")) offset -= this.pageHeight;
387
+ else if (matchesKey(data, "pageDown")) offset += this.pageHeight;
388
+ else if (matchesKey(data, "home") || matchesKey(data, "ctrl+home")) offset = 0;
389
+ else if (matchesKey(data, "end") || matchesKey(data, "ctrl+end")) offset = this.lineCount;
390
+ else return;
391
+ this.offsets[this.tab] = Math.max(0, Math.min(offset, this.lineCount - this.pageHeight));
392
+ }
393
+ this.requestRender();
394
+ }
363
395
 
364
396
  render(width: number): string[] {
365
397
  const contentWidth = Math.max(1, Math.floor(width));
366
398
  const snapshot = this.getSnapshot();
367
- return formatCostOverlayBodyLines(
368
- snapshot.totalCost,
369
- snapshot.totalTokens,
370
- snapshot.rows,
399
+ const quota = this.getQuotaSnapshots();
400
+ const theme = clioTheme();
401
+ const tabs = wrapTextWithAnsi(
402
+ USAGE_TABS.map((name, index) =>
403
+ index === this.tab
404
+ ? theme.style("accent", `${index + 1} ${name}`, { bold: true, underline: true })
405
+ : theme.fg("dim", `${index + 1} ${name}`),
406
+ ).join(" "),
371
407
  contentWidth,
372
- snapshot.promptCache,
373
408
  );
409
+ let body: string[];
410
+ if (this.tab === 0)
411
+ body = [
412
+ theme.fg("dim", "Account-wide limits · filled = used · shared across sessions and devices"),
413
+ "",
414
+ ...renderQuotaAccounts(quota, contentWidth),
415
+ ];
416
+ else if (this.tab === 1)
417
+ body = [
418
+ theme.style("accent", "Session tokens & cost", { bold: true }),
419
+ theme.fg("dim", "Recorded calls in this session · estimates are marked in the cost totals"),
420
+ "",
421
+ ...summaryBlock(snapshot.totalCost, snapshot.totalTokens, snapshot.rows, snapshot.promptCache),
422
+ "",
423
+ rule(theme, contentWidth),
424
+ theme.fg("dim", "Session tokens cannot be converted to subscription percentages."),
425
+ ...(snapshot.rows.length ? [] : ["no token usage recorded for this session"]),
426
+ ];
427
+ else if (this.tab === 2) {
428
+ body = [
429
+ theme.style("accent", "Model activity · this session", { bold: true }),
430
+ theme.fg("dim", "Share of recorded processed tokens, including cache traffic; not account quota."),
431
+ "",
432
+ ];
433
+ for (const row of [...snapshot.rows].sort((a, b) => b.tokens - a.tokens)) {
434
+ const share = snapshot.totalTokens > 0 ? (row.tokens / snapshot.totalTokens) * 100 : 0;
435
+ body.push(
436
+ `${row.providerId} · ${row.attributedModelId}`,
437
+ `${quotaMeter(share, Math.min(24, contentWidth), "normal")} ${share.toFixed(1)}% · ${formatTokens(row.tokens)} tokens`,
438
+ );
439
+ }
440
+ body.push("", ...modelDetailLines(snapshot.rows, contentWidth));
441
+ } else body = renderWorkerUsage(this.getDispatchRows(), quota, contentWidth);
442
+ const lines = body.flatMap((line) => wrapTextWithAnsi(line, contentWidth));
443
+ this.lineCount = lines.length;
444
+ this.pageHeight = Math.max(1, this.height - tabs.length - 1);
445
+ const offset = Math.max(0, Math.min(this.offsets[this.tab] ?? 0, lines.length - this.pageHeight));
446
+ this.offsets[this.tab] = offset;
447
+ const visible = [...tabs, "", ...lines.slice(offset, offset + this.pageHeight)];
448
+ if (lines.length > this.pageHeight)
449
+ visible.push(theme.fg("dim", `${offset + 1}–${Math.min(lines.length, offset + this.pageHeight)} / ${lines.length}`));
450
+ return visible;
374
451
  }
375
452
 
376
453
  invalidate(): void {}
377
454
  }
378
455
 
379
456
  /**
380
- * Mount a read-only session-cost overlay. The running USD total comes from the
457
+ * Mount the read-only usage overlay. Quota comes from the shared presentation feed. The running USD total comes from the
381
458
  * observability projection's `snapshot().session.costUsd`, while the
382
459
  * per-provider/model rows fold `observability.costEntries()`. The overlay is
383
460
  * kept live by `observability.subscribe()`: the projection already folds the
@@ -385,23 +462,34 @@ class CostOverlayBody implements Component {
385
462
  * update, so a single subscription replaces the former DispatchCompleted /
386
463
  * DispatchFailed / chat-turn refresh wiring. `hide()` unsubscribes.
387
464
  */
388
- export function openCostOverlay(
465
+ export function openUsageOverlay(
389
466
  tui: TUI,
390
467
  observability: ObservabilityContract,
391
- options?: OpenCostOverlayOptions,
468
+ options?: OpenUsageOverlayOptions,
392
469
  ): OverlayHandle {
393
470
  const sessionId = options?.sessionId ?? null;
394
471
  let latest: ObservabilitySnapshot = observability.snapshot();
395
- const body = new CostOverlayBody(() =>
396
- buildCostSnapshot(observability, sessionId, latest, options?.getSessionEntries),
472
+ const body = new UsageOverlayBody(
473
+ () => buildCostSnapshot(observability, sessionId, latest, options?.getSessionEntries),
474
+ options?.getQuotaSnapshots ?? (() => []),
475
+ options?.getDispatchRows ?? (() => []),
476
+ () => tui.requestRender(),
397
477
  );
398
478
  const handle = showClioOverlayFrame(tui, body, {
399
479
  anchor: "center",
400
- width: COST_OVERLAY_WIDTH,
480
+ width: USAGE_OVERLAY_WIDTH,
401
481
  // Not derived from the title: that one carries the session id.
402
- markerId: "cost",
403
- title: sessionId && sessionId.length > 0 ? `Session usage (${sessionId})` : "Session usage",
404
- footerHint: buildHint([]),
482
+ markerId: "usage",
483
+ title: sessionId && sessionId.length > 0 ? `Usage (${sessionId})` : "Usage",
484
+ footerHint: buildResponsiveHint([
485
+ { key: "1–4 / Tab", verb: "view", critical: true },
486
+ { key: "↑↓", verb: "scroll" },
487
+ { key: "PgUp/PgDn", verb: "page" },
488
+ ]),
489
+ visible: (_width, height) => {
490
+ body.setHeight(height);
491
+ return true;
492
+ },
405
493
  });
406
494
 
407
495
  // subscribe() fires immediately with the current snapshot, then on each
@@ -4,7 +4,7 @@ import type { ContextState } from "../domains/context/index.js";
4
4
  import type { ProvidersContract, TargetStatus } from "../domains/providers/index.js";
5
5
  import { sanitizeCallTargetText } from "../domains/safety/call-target.js";
6
6
  import type { WorkspaceSnapshot } from "../domains/session/workspace/index.js";
7
- import { type Component, type Keybinding, truncateToWidth, visibleWidth } from "../engine/tui.js";
7
+ import { type Component, type Keybinding, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../engine/tui.js";
8
8
  import {
9
9
  brandMark,
10
10
  type ClioTheme,
@@ -55,6 +55,12 @@ export interface WelcomeDashboardDeps {
55
55
  getSubmitKeyLabel?: () => string | null;
56
56
  /** Effective shortcut labels; null hides an unbound action. */
57
57
  getKeyLabel?: (action: Keybinding) => string | null;
58
+ /**
59
+ * Connected subscription quota, already summarized, or null when nothing is
60
+ * connected yet. Read synchronously on every frame, so the supplier must
61
+ * serve a cached value and do its own refreshing off the render path.
62
+ */
63
+ getQuotaSummary?: () => string | null;
58
64
  /** Called when an off-render refresh lands, so the frame owner can ask for one. */
59
65
  onFactsRefreshed?: () => void;
60
66
  /**
@@ -82,6 +88,8 @@ export interface WelcomeDashboardStats {
82
88
  autonomy: string;
83
89
  targets: string;
84
90
  fleet: string;
91
+ /** One line of connected subscription usage; null hides the row entirely. */
92
+ quota: string | null;
85
93
  }
86
94
 
87
95
  export type WelcomeDashboardMode = "launchpad" | "session";
@@ -536,13 +544,24 @@ function buildWelcomeDashboardLines(
536
544
  theme.fg("dim", "Ask Clio how to use or extend her."),
537
545
  theme.fg("dim", "Targets"),
538
546
  theme.fg("muted", stats.targets),
547
+ // Keep the subscription field beside the wordmark, between Targets and
548
+ // Fleet. Wrap at the actual detail-column width instead of clipping the
549
+ // last accounts (especially Local) off a single concatenated line.
550
+ ...(stats.quota === null
551
+ ? []
552
+ : wrapTextWithAnsi(
553
+ field("Subscriptions", theme.fg("muted", sanitizeCallTargetText(stats.quota))),
554
+ Math.max(1, detailWidth),
555
+ )),
539
556
  theme.fg("dim", "Fleet"),
540
557
  theme.fg("muted", stats.fleet),
541
558
  ];
559
+
542
560
  return [
543
561
  heading,
544
- ...details.map((detail, index) => {
545
- const art = sideBySide ? `${theme.fg("accent", wordmark[index] ?? "")} ` : "";
562
+ ...Array.from({ length: Math.max(details.length, hints.length, sideBySide ? wordmark.length : 0) }, (_, index) => {
563
+ const detail = details[index] ?? "";
564
+ const art = sideBySide ? `${padAnsi(theme.fg("accent", wordmark[index] ?? ""), artWidth)} ` : "";
546
565
  const content = `${art}${truncateToWidth(detail, detailWidth, GLYPH.ellipsis, false)}`;
547
566
  return row(
548
567
  showHints
@@ -577,6 +596,7 @@ function statsSignature(stats: WelcomeDashboardStats): string {
577
596
  stats.autonomy,
578
597
  stats.targets,
579
598
  stats.fleet,
599
+ stats.quota,
580
600
  ].join("\0");
581
601
  }
582
602
 
@@ -707,6 +727,7 @@ export class WelcomeDashboard implements WelcomeDashboardComponent {
707
727
  projectContext: launchpad ? this.projectContext(cwd) : "checking",
708
728
  submitKeyLabel: launchpad ? (this.deps.getSubmitKeyLabel?.() ?? null) : null,
709
729
  autonomy: settings?.safety?.autonomy ?? "auto-edit",
730
+ quota: launchpad ? (this.deps.getQuotaSummary?.() ?? null) : null,
710
731
  ...inventory(settings, statuses, this.deps.getAgentCount?.()),
711
732
  };
712
733
  }
@@ -804,6 +825,8 @@ export function createBootWelcome(
804
825
  routeReason: null,
805
826
  projectContext: "checking",
806
827
  submitKeyLabel,
828
+ // Boot paints before any provider has been read, so the row stays hidden.
829
+ quota: null,
807
830
  autonomy: settings.safety.autonomy,
808
831
  ...inventory(settings),
809
832
  };
@@ -497,5 +497,7 @@ export function resolveSessionTools(
497
497
  supportsImages: runtime.runtimeResolution.capabilityDecisions.vision === true,
498
498
  });
499
499
  if (telemetry) input.telemetry = telemetry;
500
- return resolveAgentTools(input);
500
+ return resolveAgentTools(input).filter(
501
+ (tool) => tool.name !== ToolNames.SelfCompact || !runtime.runtimeResolution.runtime.externalAgentLoop,
502
+ );
501
503
  }
@@ -22,11 +22,13 @@ import { lazyTool } from "./lazy-tool.js";
22
22
  import { monitorToolSurface } from "./monitor-surface.js";
23
23
  import { panesToolSurface } from "./panes-surface.js";
24
24
  import type { ToolRegistry } from "./registry.js";
25
+ import { createSelfCompactTool, type RequestSelfCompact } from "./self-compact.js";
25
26
  import { steerToolSurface } from "./steer-surface.js";
26
27
 
27
28
  export { toolPromptHintsForNames };
28
29
 
29
30
  export interface ToolBootstrapDeps extends Omit<CoreToolBootstrapDeps, "mcpCapabilities"> {
31
+ requestSelfCompact?: RequestSelfCompact;
30
32
  captureWorkerContext?: () => WorkerContextSnapshot | null;
31
33
  dispatch?: DispatchContract;
32
34
  bus?: SafeEventBus;
@@ -77,6 +79,10 @@ export function registerAllTools(registry: ToolRegistry, deps: ToolBootstrapDeps
77
79
  ...coreDeps,
78
80
  ...(mcpCapabilities ? { mcpCapabilities } : {}),
79
81
  });
82
+ if (deps.requestSelfCompact)
83
+ registry.register(
84
+ builtin(createSelfCompactTool(deps.requestSelfCompact), { path: "src/tools/self-compact.ts", scope: "core" }),
85
+ );
80
86
  if (deps.dispatch) {
81
87
  const dispatch = deps.dispatch;
82
88
  // Display tail only. In a composed process the dispatch domain writes the
@@ -100,6 +100,15 @@ const TOOL_METADATA: Readonly<Record<string, ToolMetadata>> = {
100
100
  promptHint:
101
101
  "Use code_nav with source=workspace (default) for project code and source=clio for Clio's shipped code map; modes: symbol, path, entries, outline, deps, dependents, wiki (workspace only).",
102
102
  },
103
+ [ToolNames.SelfCompact]: {
104
+ objective: "Save an exact assistant handoff and reduce native session context.",
105
+ uiLabel: "Compact",
106
+ retrySafety: "not_retry_safe",
107
+ resultSizePolicy: { kind: "exact", maxBytes: 2048 },
108
+ costLatency: "local_fast",
109
+ promptHint:
110
+ 'Use self_compact alone at a safe boundary. Preserve decisions, evidence, paths, and next actions in note_to_self. Inspect context(scope="budget") for current pressure; recovery belongs to the operator.',
111
+ },
103
112
  [ToolNames.Context]: {
104
113
  objective: "Return workspace, effective configuration, skill, or recall context.",
105
114
  uiLabel: "Context",