@iowarp/clio-coder 0.3.3 → 0.3.6

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 (370) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/CONTRIBUTING.md +7 -7
  3. package/README.md +3 -3
  4. package/dist/{acp-P2AQILE2.js → acp-2BEHC4DL.js} +9 -8
  5. package/dist/{agents-72W3BI7I.js → agents-LNNFTM53.js} +29 -24
  6. package/dist/assets/codewiki.json +1 -1
  7. package/dist/{auth-5TWEIYDN.js → auth-KXXFI2VS.js} +14 -10
  8. package/dist/{chunk-GGXXDWE4.js → chunk-22NAGB7X.js} +2 -2
  9. package/dist/{chunk-YCWGATWI.js → chunk-24I7BN55.js} +2 -2
  10. package/dist/{chunk-EKMEHE4H.js → chunk-33YXPOE3.js} +2 -3
  11. package/dist/chunk-3BPUFZDL.js +37 -0
  12. package/dist/chunk-43AOLP7E.js +375 -0
  13. package/dist/{chunk-ZDOOVTXZ.js → chunk-4OC57DA6.js} +27 -4
  14. package/dist/{chunk-6SGHMWE3.js → chunk-5JGRAMKL.js} +5 -5
  15. package/dist/{chunk-V6RTAOC2.js → chunk-6US73PDB.js} +572 -51
  16. package/dist/{chunk-5UFT4SUX.js → chunk-6XXKFVSN.js} +3 -3
  17. package/dist/{chunk-A3CYT5EX.js → chunk-AD2SYQYC.js} +55 -2
  18. package/dist/chunk-AOCYTWAV.js +449 -0
  19. package/dist/chunk-CFGTUFWB.js +67 -0
  20. package/dist/chunk-CJUB2JJ2.js +1478 -0
  21. package/dist/{chunk-FNTMWMX5.js → chunk-CKXWIANG.js} +14 -12
  22. package/dist/{chunk-PIWWS5BL.js → chunk-CYQKWTG3.js} +63 -78
  23. package/dist/{chunk-LZSJBIVT.js → chunk-DJVECN66.js} +271 -762
  24. package/dist/{chunk-CBCAPZAA.js → chunk-E25LMLRW.js} +2 -2
  25. package/dist/{chunk-ZWMF7253.js → chunk-E2ER4LJF.js} +304 -9
  26. package/dist/{chunk-STBPMHSX.js → chunk-EKY57CSP.js} +51 -84
  27. package/dist/{chunk-DUYJ5IO6.js → chunk-EYPA3EGJ.js} +12 -4
  28. package/dist/{chunk-M6SHUN7Q.js → chunk-FO5ZOVUY.js} +2 -2
  29. package/dist/chunk-FYYLNIL5.js +313 -0
  30. package/dist/{chunk-OQ33BKR3.js → chunk-G7MUEIGA.js} +3 -60
  31. package/dist/chunk-GEYXPTRF.js +613 -0
  32. package/dist/chunk-GOXNB3AO.js +261 -0
  33. package/dist/{chunk-G4BMMOKF.js → chunk-HVDIIIQW.js} +2 -2
  34. package/dist/chunk-HWUFFB6L.js +83 -0
  35. package/dist/{chunk-4XUGQOHA.js → chunk-K7T3E2SR.js} +15 -8
  36. package/dist/chunk-K7VKOLQQ.js +15 -0
  37. package/dist/{chunk-UFIIWP2H.js → chunk-KHSFENX2.js} +8 -8
  38. package/dist/{chunk-BMEMKKIT.js → chunk-KOHPCX4K.js} +2 -2
  39. package/dist/chunk-LCGCVYZ4.js +57 -0
  40. package/dist/chunk-LL4KHSZI.js +22 -0
  41. package/dist/{chunk-PAJK6MAQ.js → chunk-LYF7OHWH.js} +42 -15
  42. package/dist/{chunk-POHLU5DW.js → chunk-M6L6IDJG.js} +3 -3
  43. package/dist/{chunk-5UUP6MWO.js → chunk-MV3K5QF2.js} +5 -436
  44. package/dist/{chunk-X4RCMKVQ.js → chunk-NDINPTJ4.js} +2 -2
  45. package/dist/{chunk-TZK7PACC.js → chunk-NILBFAPG.js} +14 -8
  46. package/dist/chunk-ODFEOB4F.js +1082 -0
  47. package/dist/{chunk-AGYYIBLL.js → chunk-OH3TOQTB.js} +6 -2
  48. package/dist/chunk-OZNBF4L3.js +23 -0
  49. package/dist/{chunk-DSELYM6W.js → chunk-PBTHKCPN.js} +30 -10
  50. package/dist/{verify-375KUB3Y.js → chunk-PCZJO5TI.js} +127 -42
  51. package/dist/{chunk-ED4KHGC3.js → chunk-PPAMZ32Z.js} +9 -2
  52. package/dist/{chunk-SRDMMSEP.js → chunk-QM3F2GKX.js} +1063 -1645
  53. package/dist/{chunk-X6IAEBZR.js → chunk-QNQHSOLF.js} +7 -7
  54. package/dist/{chunk-OC7FIQPC.js → chunk-R46L2BIR.js} +10 -7
  55. package/dist/{chunk-2TLUCQVG.js → chunk-RD5U66HV.js} +3 -3
  56. package/dist/{chunk-6N5PTWMY.js → chunk-RY3LY4J5.js} +50 -13
  57. package/dist/{chunk-OKGUZO2U.js → chunk-SPULKLCF.js} +4 -3
  58. package/dist/{chunk-OOJYHWRB.js → chunk-TSHXZTOQ.js} +6 -5
  59. package/dist/chunk-TZSKNMZG.js +434 -0
  60. package/dist/{chunk-7MNJORFF.js → chunk-UL3WSD3F.js} +6 -1
  61. package/dist/{chunk-VJWL6YS5.js → chunk-UUVG37B4.js} +2 -2
  62. package/dist/{chunk-COU2UHX6.js → chunk-VEZEGCGW.js} +170 -2
  63. package/dist/chunk-W6GROXXM.js +69 -0
  64. package/dist/{chunk-OAO4GE4M.js → chunk-WHGPSPT5.js} +2 -2
  65. package/dist/chunk-WHJYKASB.js +677 -0
  66. package/dist/{chunk-ORBHGJC5.js → chunk-WR67VIZY.js} +3 -3
  67. package/dist/{chunk-YHZX5GEU.js → chunk-XAKHZX5N.js} +2 -2
  68. package/dist/{chunk-TZTZS7QK.js → chunk-XE2VEJHX.js} +5 -3
  69. package/dist/{chunk-LM5TQCJZ.js → chunk-XF5N4U5A.js} +8 -7
  70. package/dist/{chunk-LWLEKMDQ.js → chunk-XXQNGV4M.js} +1073 -552
  71. package/dist/{chunk-KZWTDYJF.js → chunk-XYDYPRZI.js} +7 -7
  72. package/dist/chunk-ZGVHUX3M.js +66 -0
  73. package/dist/{chunk-LW6DSM3M.js → chunk-ZRGEBJ4T.js} +1192 -1119
  74. package/dist/{chunk-2DJ2KNFG.js → chunk-ZXF4XRKW.js} +202 -40
  75. package/dist/chunk-ZZMN5OM4.js +122 -0
  76. package/dist/cli/index.js +34 -30
  77. package/dist/{clio-JOU4FXVA.js → clio-M2KGYUFZ.js} +7 -6
  78. package/dist/{code-nav-7AX6FYE6.js → code-nav-GQNL7XA6.js} +8 -6
  79. package/dist/codewiki/build-worker.js +4 -4
  80. package/dist/{components-KELWS457.js → components-5TTYYX6G.js} +3 -3
  81. package/dist/{config-XCDVKR23.js → config-XUUYQIWO.js} +47 -35
  82. package/dist/{configure-4GAP54ZW.js → configure-IHJ7YOMV.js} +18 -15
  83. package/dist/{context-77FM5DV5.js → context-74JLXAWD.js} +18 -10
  84. package/dist/{context-4UOGGLQ5.js → context-75MIWW3U.js} +41 -29
  85. package/dist/{context-5VKGUVJJ.js → context-ZQ7SIFJV.js} +85 -9
  86. package/dist/{context-clear-XXJRLCJJ.js → context-clear-GYKWNUML.js} +41 -29
  87. package/dist/{context-index-BZ4UYMTC.js → context-index-SSR5ECNE.js} +3 -3
  88. package/dist/context-working-set-UX5KEP4J.js +1553 -0
  89. package/dist/{dispatch-runner-QPRDDBDX.js → dispatch-runner-GIJBHNFL.js} +47 -32
  90. package/dist/{docs-2C2LTVT2.js → docs-6FZSCG5B.js} +3 -3
  91. package/dist/{doctor-HR46URBJ.js → doctor-SVJ5BZCW.js} +12 -12
  92. package/dist/{eval-XSSNATB4.js → eval-CG6LLBLD.js} +54 -238
  93. package/dist/{evidence-6HG2PY2B.js → evidence-ZYFIEN42.js} +57 -28
  94. package/dist/{evolve-K7YU3NCY.js → evolve-QGEXEMDW.js} +36 -25
  95. package/dist/{extensions-QVDOHDGJ.js → extensions-ADGNCJJD.js} +3 -3
  96. package/dist/{fleet-VY3HHKN6.js → fleet-S5R4ZOQY.js} +73 -44
  97. package/dist/{fleet-preflight-DDN536IT.js → fleet-preflight-BHSNPBMH.js} +3 -3
  98. package/dist/{init-JYGXI3FK.js → init-5DRU55YR.js} +49 -37
  99. package/dist/memory-7YKKR6UC.js +467 -0
  100. package/dist/{models-I5QWSEOM.js → models-ZPOLRU2C.js} +24 -21
  101. package/dist/{monitor-GE4ID3IA.js → monitor-US5F5YGZ.js} +73 -46
  102. package/dist/{orchestrator-EM5MC3HM.js → orchestrator-E2AL4T5N.js} +1624 -1007
  103. package/dist/{paths-UXLN5YYZ.js → paths-E7KYAQWE.js} +3 -3
  104. package/dist/{reset-L2FQEE3E.js → reset-KZ652EK6.js} +6 -5
  105. package/dist/{run-ZU3QMZPZ.js → run-SRNBKDWD.js} +76 -54
  106. package/dist/{share-S5BZQC5I.js → share-CGZE33UP.js} +7 -6
  107. package/dist/{skills-X5VXCRNQ.js → skills-S2X4DLY5.js} +4 -4
  108. package/dist/{skills-eval-WKIHWTHR.js → skills-eval-W2GGIC4R.js} +40 -29
  109. package/dist/{targets-SNCPI2NR.js → targets-54SWINWB.js} +28 -23
  110. package/dist/{terminal-lease-BNAHVHBS.js → terminal-lease-SAIF2OGY.js} +6 -4
  111. package/dist/{uninstall-FZCQCDKC.js → uninstall-BVLWXKBT.js} +3 -3
  112. package/dist/{upgrade-JQHHPQ4K.js → upgrade-JKAR27XC.js} +20 -19
  113. package/dist/{usage-OR4O5SMZ.js → usage-MSAWCLX4.js} +79 -36
  114. package/dist/verifiers-NCBTHHN2.js +1220 -0
  115. package/dist/verify-X5HDROLA.js +25 -0
  116. package/dist/{wiki-generate-UEXP2ARI.js → wiki-generate-GUSOQ6ZP.js} +50 -37
  117. package/dist/worker/entry.js +90 -70
  118. package/dist/{workspace-G4ZWUIPR.js → workspace-ZJ6BFM3Q.js} +4 -4
  119. package/docs/README.md +8 -7
  120. package/docs/acp.md +1 -1
  121. package/docs/alcf-provider.md +1 -1
  122. package/docs/architecture.md +2 -2
  123. package/docs/artifact-placement.md +1 -2
  124. package/docs/artifact-versions.md +1 -1
  125. package/docs/built-in-agents.md +1 -1
  126. package/docs/capacity-and-scheduling.md +1 -1
  127. package/docs/commands-and-modes.md +60 -26
  128. package/docs/config-knobs-audit.md +1 -2
  129. package/docs/configuration-and-targets.md +26 -1
  130. package/docs/context-engine.md +67 -13
  131. package/docs/context-working-set.md +194 -0
  132. package/docs/development-pipeline.md +1 -1
  133. package/docs/documentation-coverage.md +6 -6
  134. package/docs/documentation-guide.md +7 -6
  135. package/docs/environment-variables.md +2 -1
  136. package/docs/eval-runner.md +1 -1
  137. package/docs/evals-internal.md +4 -32
  138. package/docs/evidence-and-memory.md +139 -7
  139. package/docs/evolution.md +1 -1
  140. package/docs/exit-codes-and-output.md +1 -1
  141. package/docs/extensions-and-sharing.md +2 -2
  142. package/docs/fleet-dispatch.md +49 -8
  143. package/docs/glossary.md +21 -1
  144. package/docs/installation-and-lifecycle.md +2 -2
  145. package/docs/middleware-and-components.md +19 -2
  146. package/docs/model-catalog.md +7 -9
  147. package/docs/observability.md +4 -4
  148. package/docs/proactive-memory.md +26 -16
  149. package/docs/prompt-envelope-and-tools.md +7 -5
  150. package/docs/provider-adapter-cookbook.md +1 -1
  151. package/docs/release-cut-checklist.md +43 -40
  152. package/docs/safety-model.md +49 -8
  153. package/docs/scientific-validation.md +21 -3
  154. package/docs/session-lifecycle.md +3 -3
  155. package/docs/skills-marketplace.md +1 -1
  156. package/docs/tool-usage.md +79 -12
  157. package/docs/trace-store.md +1 -1
  158. package/docs/troubleshooting.md +1 -1
  159. package/docs/tui-design.md +38 -4
  160. package/docs/worker-dispatch-mechanics.md +11 -1
  161. package/package.json +13 -13
  162. package/skills/meta/clio-test/SKILL.md +20 -17
  163. package/skills/meta/clio-test/evals.md +3 -3
  164. package/skills/meta/clio-test/references/harness.md +35 -6
  165. package/skills/meta/clio-test/references/test-map.md +20 -10
  166. package/skills/registry.yaml +2 -2
  167. package/skills/skill-marketplace.json +1 -1
  168. package/src/cli/agents.ts +2 -3
  169. package/src/cli/argv.ts +14 -1
  170. package/src/cli/context-working-set.ts +513 -0
  171. package/src/cli/context.ts +8 -0
  172. package/src/cli/evidence.ts +20 -2
  173. package/src/cli/fleet.ts +15 -0
  174. package/src/cli/index.ts +5 -1
  175. package/src/cli/memory.ts +272 -10
  176. package/src/cli/modes/json-stream.ts +2 -2
  177. package/src/cli/modes/print.ts +12 -1
  178. package/src/cli/run.ts +22 -2
  179. package/src/cli/targets.ts +12 -3
  180. package/src/cli/usage.ts +55 -7
  181. package/src/cli/verifiers.ts +325 -0
  182. package/src/core/bash-exec.ts +39 -14
  183. package/src/core/bus-events.ts +22 -4
  184. package/src/core/config.ts +54 -0
  185. package/src/core/defaults.ts +50 -3
  186. package/src/core/response-model-id.ts +134 -0
  187. package/src/core/toml.ts +62 -0
  188. package/src/core/verification-scripts.ts +6 -0
  189. package/src/core/workspace-files.ts +0 -1
  190. package/src/domains/agents/builtins/architect.md +1 -1
  191. package/src/domains/agents/builtins/verifier.md +3 -0
  192. package/src/domains/agents/catalog.ts +5 -4
  193. package/src/domains/agents/recipe.ts +54 -14
  194. package/src/domains/agents/result-contract.ts +7 -4
  195. package/src/domains/config/classify.ts +1 -0
  196. package/src/domains/context/bootstrap.ts +36 -27
  197. package/src/domains/context/project-metadata.ts +19 -63
  198. package/src/domains/context/prompt-context.ts +8 -0
  199. package/src/domains/context/working-set/contract.ts +161 -0
  200. package/src/domains/context/working-set/defaults.ts +28 -0
  201. package/src/domains/context/working-set/engine.ts +203 -0
  202. package/src/domains/context/working-set/fold.ts +62 -0
  203. package/src/domains/context/working-set/horizon.ts +38 -0
  204. package/src/domains/context/working-set/marker.ts +103 -0
  205. package/src/domains/context/working-set/path-index.ts +436 -0
  206. package/src/domains/context/working-set/payload.ts +152 -0
  207. package/src/domains/context/working-set/policies/age-horizon.ts +55 -0
  208. package/src/domains/context/working-set/policies/index.ts +20 -0
  209. package/src/domains/context/working-set/policies/structural.ts +160 -0
  210. package/src/domains/context/working-set/project.ts +132 -0
  211. package/src/domains/context/working-set/protect.ts +109 -0
  212. package/src/domains/context/working-set/recall.ts +177 -0
  213. package/src/domains/context/working-set/replay/controls.ts +112 -0
  214. package/src/domains/context/working-set/replay/load-clio.ts +199 -0
  215. package/src/domains/context/working-set/replay/metrics.ts +185 -0
  216. package/src/domains/context/working-set/replay/reference-graph.ts +79 -0
  217. package/src/domains/context/working-set/replay/report.ts +139 -0
  218. package/src/domains/context/working-set/replay/runner.ts +325 -0
  219. package/src/domains/context/working-set/replay/synthetic.ts +422 -0
  220. package/src/domains/context/working-set/replay/trace.ts +21 -0
  221. package/src/domains/context/working-set/visible.ts +54 -0
  222. package/src/domains/dispatch/budget-envelope.ts +396 -0
  223. package/src/domains/dispatch/contract.ts +2 -0
  224. package/src/domains/dispatch/extension.ts +81 -27
  225. package/src/domains/dispatch/orphan-recovery.ts +1 -0
  226. package/src/domains/dispatch/receipt-integrity.ts +4 -0
  227. package/src/domains/dispatch/state.ts +1 -0
  228. package/src/domains/dispatch/types.ts +10 -3
  229. package/src/domains/dispatch/validation.ts +14 -0
  230. package/src/domains/dispatch/worker-spawn.ts +14 -3
  231. package/src/domains/eval/metrics/evidence.ts +0 -116
  232. package/src/domains/eval/metrics/invariants.ts +1 -1
  233. package/src/domains/eval/runners/clio-run.ts +1 -10
  234. package/src/domains/eval/runners/external-command.ts +2 -29
  235. package/src/domains/eval/schema/suite.ts +0 -7
  236. package/src/domains/eval/suites/run.ts +1 -7
  237. package/src/domains/evidence/build.ts +112 -45
  238. package/src/domains/evidence/eval.ts +24 -7
  239. package/src/domains/evidence/index.ts +53 -0
  240. package/src/domains/evidence/ordering.ts +12 -0
  241. package/src/domains/evidence/run-trust.ts +221 -0
  242. package/src/domains/evidence/store.ts +46 -6
  243. package/src/domains/evidence/trust-status.ts +854 -0
  244. package/src/domains/evidence/types.ts +26 -0
  245. package/src/domains/memory/index.ts +22 -0
  246. package/src/domains/memory/operations.ts +58 -1
  247. package/src/domains/memory/promotion.ts +281 -0
  248. package/src/domains/memory/prompt-section.ts +25 -5
  249. package/src/domains/memory/proposal.ts +51 -7
  250. package/src/domains/memory/task-bank.ts +3 -2
  251. package/src/domains/memory/task-memory-handoff.ts +181 -24
  252. package/src/domains/memory/task-memory-policy.ts +3 -1
  253. package/src/domains/memory/types.ts +37 -0
  254. package/src/domains/memory/validate.ts +178 -0
  255. package/src/domains/middleware/memory-intervention.ts +38 -25
  256. package/src/domains/middleware/runtime.ts +6 -0
  257. package/src/domains/middleware/skills-reminder.ts +19 -4
  258. package/src/domains/middleware/stalled-turn.ts +208 -5
  259. package/src/domains/middleware/types.ts +10 -0
  260. package/src/domains/observability/contract.ts +6 -1
  261. package/src/domains/observability/cost.ts +20 -4
  262. package/src/domains/observability/extension.ts +2 -2
  263. package/src/domains/providers/index.ts +3 -0
  264. package/src/domains/providers/model-discovery.ts +9 -0
  265. package/src/domains/providers/runtime-resolution.ts +38 -1
  266. package/src/domains/providers/runtimes/common/probe-helpers.ts +97 -16
  267. package/src/domains/providers/types/context-window-slots.ts +18 -0
  268. package/src/domains/providers/types/runtime-descriptor.ts +3 -1
  269. package/src/domains/safety/autonomy.ts +1 -1
  270. package/src/domains/safety/call-target.ts +211 -14
  271. package/src/domains/safety/decision-presentation.ts +268 -0
  272. package/src/domains/safety/default-path-policy.ts +8 -0
  273. package/src/domains/safety/finish-contract.ts +4 -3
  274. package/src/domains/safety/policy-engine.ts +48 -6
  275. package/src/domains/safety/redaction.ts +73 -0
  276. package/src/domains/session/compaction/compact.ts +23 -1
  277. package/src/domains/session/compaction/cut-point.ts +2 -0
  278. package/src/domains/session/compaction/tokens.ts +16 -1
  279. package/src/domains/session/context-ledger.ts +12 -1
  280. package/src/domains/session/decision-board.ts +4 -0
  281. package/src/domains/session/entries.ts +110 -1
  282. package/src/domains/session/history.ts +68 -19
  283. package/src/domains/session/manager.ts +9 -2
  284. package/src/domains/session/migrations/index.ts +22 -3
  285. package/src/domains/session/usage.ts +24 -7
  286. package/src/engine/acp/event-mapper.ts +7 -0
  287. package/src/engine/acp/server.ts +32 -2
  288. package/src/engine/agent.ts +18 -1
  289. package/src/engine/apis/lmstudio.ts +25 -4
  290. package/src/engine/apis/openai-completions.ts +147 -22
  291. package/src/engine/claude/sdk-runtime.ts +8 -2
  292. package/src/engine/claude/tool-safety.ts +13 -0
  293. package/src/engine/loop-guard.ts +27 -3
  294. package/src/engine/session.ts +9 -3
  295. package/src/engine/worker-events.ts +4 -3
  296. package/src/engine/worker-runtime.ts +59 -54
  297. package/src/entry/orchestrator.ts +34 -5
  298. package/src/interactive/chat-loop-messages.ts +40 -6
  299. package/src/interactive/chat-loop.ts +13 -0
  300. package/src/interactive/chat-panel.ts +17 -1
  301. package/src/interactive/chat-renderer.ts +49 -24
  302. package/src/interactive/clio-editor.ts +44 -7
  303. package/src/interactive/context-meter.ts +10 -0
  304. package/src/interactive/context-overlay.ts +120 -7
  305. package/src/interactive/context-recall-command.ts +110 -0
  306. package/src/interactive/cost-overlay.ts +39 -8
  307. package/src/interactive/dispatch-board.ts +212 -35
  308. package/src/interactive/footer/widgets.ts +13 -0
  309. package/src/interactive/interactive-application.ts +6 -1
  310. package/src/interactive/interactive-input-runtime.ts +11 -1
  311. package/src/interactive/interactive-presentation.ts +11 -1
  312. package/src/interactive/interactive-slash-runtime.ts +37 -1
  313. package/src/interactive/memory-overlay.ts +89 -4
  314. package/src/interactive/model-session-replay.ts +21 -0
  315. package/src/interactive/overlay-ask-user-lifecycle.ts +1 -1
  316. package/src/interactive/overlay-frame.ts +5 -2
  317. package/src/interactive/overlay-general-openers.ts +46 -1
  318. package/src/interactive/overlay-key-routing.ts +41 -1
  319. package/src/interactive/overlay-lifecycle.ts +11 -4
  320. package/src/interactive/overlay-permission-lifecycle.ts +23 -8
  321. package/src/interactive/overlay-session-lifecycle.ts +8 -4
  322. package/src/interactive/overlay-transitions.ts +11 -0
  323. package/src/interactive/overlays/ask-user.ts +74 -30
  324. package/src/interactive/overlays/decisions.ts +3 -1
  325. package/src/interactive/permission-hint.ts +35 -0
  326. package/src/interactive/permission-overlay.ts +95 -45
  327. package/src/interactive/renderers/tool-execution.ts +37 -51
  328. package/src/interactive/session-last-turn.ts +8 -1
  329. package/src/interactive/session-transcript.ts +2 -2
  330. package/src/interactive/session-usage-reseed.ts +36 -10
  331. package/src/interactive/slash-commands.ts +31 -4
  332. package/src/interactive/status/summary.ts +5 -0
  333. package/src/interactive/status/types.ts +5 -0
  334. package/src/interactive/terminal-lease.ts +1 -0
  335. package/src/interactive/turn-context.ts +333 -110
  336. package/src/interactive/turn-middleware.ts +7 -6
  337. package/src/interactive/turn-runtime.ts +37 -8
  338. package/src/interactive/turn-state.ts +3 -0
  339. package/src/interactive/worker-progress.ts +440 -0
  340. package/src/interactive/worker-stream.ts +51 -110
  341. package/src/tools/agent-tools.ts +39 -7
  342. package/src/tools/ask-user.ts +21 -1
  343. package/src/tools/bash.ts +144 -82
  344. package/src/tools/builtin-tool-catalog.ts +11 -5
  345. package/src/tools/context/index.ts +107 -5
  346. package/src/tools/context/surface.ts +3 -2
  347. package/src/tools/core-bootstrap.ts +21 -0
  348. package/src/tools/dispatch-arguments.ts +8 -0
  349. package/src/tools/dispatch-event-text.ts +19 -0
  350. package/src/tools/dispatch-runner.ts +9 -7
  351. package/src/tools/dispatch.ts +24 -1
  352. package/src/tools/monitor.ts +43 -20
  353. package/src/tools/registry.ts +72 -10
  354. package/src/tools/result-disposition.ts +706 -0
  355. package/src/tools/result-shaping.ts +321 -20
  356. package/src/tools/safe-exec.ts +2 -0
  357. package/src/tools/verify/authoring.ts +1120 -0
  358. package/src/tools/verify/catalog.ts +346 -0
  359. package/src/tools/verify/index.ts +13 -3
  360. package/src/tools/verify/scripts.ts +135 -37
  361. package/src/tools/verify/surface.ts +9 -5
  362. package/src/tools/worker-evidence.ts +54 -12
  363. package/src/worker/spec-contract.ts +43 -3
  364. package/dist/chunk-J7CWMCQD.js +0 -255
  365. package/dist/chunk-T6YILFSB.js +0 -80
  366. package/dist/chunk-VAKQQHWR.js +0 -434
  367. package/dist/chunk-VPAYEGVX.js +0 -184
  368. package/dist/chunk-XBXAASKX.js +0 -18
  369. package/dist/memory-WFZMGYHX.js +0 -236
  370. package/src/domains/eval/metrics/chaos-stream.ts +0 -93
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { isSkillActivation, type SkillActivation } from "../../core/skill-activation.js";
15
15
  import type { ClioTurnRecord } from "../../engine/session.js";
16
+ import { AUTONOMY_EXPOSURES, type AutonomyExposure } from "../safety/autonomy.js";
16
17
 
17
18
  export interface SessionHeader {
18
19
  type: "session";
@@ -229,6 +230,7 @@ export interface DecisionLedgerEntry extends BaseSessionEntry {
229
230
  roundCount: number;
230
231
  summary?: string;
231
232
  transcriptPath?: string;
233
+ exposure?: AutonomyExposure;
232
234
  decisions: DecisionRecord[];
233
235
  }
234
236
 
@@ -276,6 +278,75 @@ export interface WorkerRunEntry extends BaseSessionEntry {
276
278
  parentToolCallId?: string;
277
279
  }
278
280
 
281
+ /**
282
+ * Working-set layer (context domain) ledger records. Eviction is a projection:
283
+ * these entries say what left the model's working set and what came back; the
284
+ * original bodies stay in the ledger untouched. The context domain owns the
285
+ * semantics (`src/domains/context/working-set/contract.ts`); the session
286
+ * domain owns the wire shape because it owns the ledger format.
287
+ */
288
+ export const EVICTION_REASONS = [
289
+ "superseded_read",
290
+ "stale_after_mutation",
291
+ "listing_consumed",
292
+ "failure_resolved",
293
+ "thinking_turn_closed",
294
+ "age_horizon",
295
+ "operator",
296
+ ] as const;
297
+ export type EvictionReason = (typeof EVICTION_REASONS)[number];
298
+
299
+ export const EVICTION_TRIGGERS = ["pressure", "operator"] as const;
300
+ export type EvictionTrigger = (typeof EVICTION_TRIGGERS)[number];
301
+
302
+ export const RECALL_TRIGGERS = ["tool", "operator"] as const;
303
+ export type RecallTrigger = (typeof RECALL_TRIGGERS)[number];
304
+
305
+ /**
306
+ * Identity of an evictable unit: the `turnId` of a ledger entry. For a
307
+ * `tool_result` message the unit is the result body; for an `assistant`
308
+ * message the unit is every thinking block it carries. Partial (per-block)
309
+ * eviction is deliberately not modelled; add a `block` field here when it is.
310
+ */
311
+ export interface WorkingSetRef {
312
+ entry: string;
313
+ }
314
+
315
+ export interface EvictedItem {
316
+ ref: WorkingSetRef;
317
+ reason: EvictionReason;
318
+ /** Estimated tokens the projection removes for this item (marker cost already subtracted). */
319
+ tokensFreed: number;
320
+ /**
321
+ * Byte-stable one-line stub the projection renders in place of the body.
322
+ * Empty for thinking-block eviction, which removes without a marker.
323
+ */
324
+ marker: string;
325
+ /** Ref key of the entry that superseded or resolved this one, when the reason names one. */
326
+ by?: string;
327
+ }
328
+
329
+ export interface ContextEvictionEntry extends BaseSessionEntry {
330
+ kind: "contextEviction";
331
+ policyId: string;
332
+ trigger: EvictionTrigger;
333
+ evicted: ReadonlyArray<EvictedItem>;
334
+ tokensBefore: number;
335
+ tokensAfter: number;
336
+ /** Used/window ratio that fired the event; null for operator-triggered events. */
337
+ pressureBefore: number | null;
338
+ snapshotIdBefore: string | null;
339
+ }
340
+
341
+ export interface ContextRecallEntry extends BaseSessionEntry {
342
+ kind: "contextRecall";
343
+ ref: WorkingSetRef;
344
+ trigger: RecallTrigger;
345
+ tokensReadmitted: number;
346
+ /** The tool call that performed the recall, when `trigger` is `tool`. */
347
+ toolCallId?: string;
348
+ }
349
+
279
350
  export type SessionEntry =
280
351
  | MessageEntry
281
352
  | BashExecutionEntry
@@ -291,7 +362,9 @@ export type SessionEntry =
291
362
  | SkillActivationEntry
292
363
  | TaskLedgerEntry
293
364
  | DecisionLedgerEntry
294
- | WorkerRunEntry;
365
+ | WorkerRunEntry
366
+ | ContextEvictionEntry
367
+ | ContextRecallEntry;
295
368
 
296
369
  export type SessionFileEntry = SessionHeader | SessionEntry;
297
370
 
@@ -317,6 +390,8 @@ export const SESSION_ENTRY_KINDS = [
317
390
  "taskLedger",
318
391
  "decisionLedger",
319
392
  "workerRun",
393
+ "contextEviction",
394
+ "contextRecall",
320
395
  ] as const;
321
396
 
322
397
  export type SessionEntryKind = (typeof SESSION_ENTRY_KINDS)[number];
@@ -477,6 +552,21 @@ export function isSessionHeader(value: unknown): value is SessionHeader {
477
552
  );
478
553
  }
479
554
 
555
+ function isWorkingSetRef(value: unknown): value is WorkingSetRef {
556
+ return isRecord(value) && isString(value.entry);
557
+ }
558
+
559
+ function isEvictedItem(value: unknown): value is EvictedItem {
560
+ return (
561
+ isRecord(value) &&
562
+ isWorkingSetRef(value.ref) &&
563
+ isOneOf(value.reason, EVICTION_REASONS) &&
564
+ isNumber(value.tokensFreed) &&
565
+ isString(value.marker) &&
566
+ isOptionalString(value.by)
567
+ );
568
+ }
569
+
480
570
  export function isSessionEntry(value: unknown): value is SessionEntry {
481
571
  if (!value || typeof value !== "object") return false;
482
572
  const v = value as Record<string, unknown>;
@@ -555,6 +645,7 @@ export function isSessionEntry(value: unknown): value is SessionEntry {
555
645
  v.roundCount >= 0 &&
556
646
  isOptionalString(v.summary) &&
557
647
  isOptionalString(v.transcriptPath) &&
648
+ (v.exposure === undefined || isOneOf(v.exposure, AUTONOMY_EXPOSURES)) &&
558
649
  Array.isArray(v.decisions) &&
559
650
  v.decisions.every(isDecisionRecord)
560
651
  );
@@ -567,6 +658,24 @@ export function isSessionEntry(value: unknown): value is SessionEntry {
567
658
  isWorkerRunRuntime(v.runtime) &&
568
659
  isOptionalString(v.parentToolCallId)
569
660
  );
661
+ case "contextEviction":
662
+ return (
663
+ isString(v.policyId) &&
664
+ isOneOf(v.trigger, EVICTION_TRIGGERS) &&
665
+ Array.isArray(v.evicted) &&
666
+ v.evicted.every(isEvictedItem) &&
667
+ isNumber(v.tokensBefore) &&
668
+ isNumber(v.tokensAfter) &&
669
+ (v.pressureBefore === null || isNumber(v.pressureBefore)) &&
670
+ (v.snapshotIdBefore === null || isString(v.snapshotIdBefore))
671
+ );
672
+ case "contextRecall":
673
+ return (
674
+ isWorkingSetRef(v.ref) &&
675
+ isOneOf(v.trigger, RECALL_TRIGGERS) &&
676
+ isNumber(v.tokensReadmitted) &&
677
+ isOptionalString(v.toolCallId)
678
+ );
570
679
  }
571
680
  return false;
572
681
  }
@@ -48,21 +48,15 @@ function collapseWhitespace(text: string): string {
48
48
  }
49
49
 
50
50
  /**
51
- * Pull the first non-empty piece of user-authored text out of a payload.
52
- * Structured message payloads may contain a bare string, a text property,
53
- * or pi-ai content blocks.
51
+ * Pull the first non-empty piece of text out of a message payload. Structured
52
+ * message payloads may contain a bare string, a text property, or pi-ai
53
+ * content blocks.
54
54
  */
55
- function extractUserText(payload: unknown): string | null {
56
- if (typeof payload === "string") {
57
- const collapsed = collapseWhitespace(payload);
58
- return collapsed.length > 0 ? collapsed : null;
59
- }
55
+ function rawMessageText(payload: unknown): string | null {
56
+ if (typeof payload === "string") return payload.trim().length > 0 ? payload : null;
60
57
  if (!payload || typeof payload !== "object") return null;
61
58
  const obj = payload as Record<string, unknown>;
62
- if (typeof obj.text === "string") {
63
- const collapsed = collapseWhitespace(obj.text);
64
- if (collapsed.length > 0) return collapsed;
65
- }
59
+ if (typeof obj.text === "string" && obj.text.trim().length > 0) return obj.text;
66
60
  if (Array.isArray(obj.content)) {
67
61
  const parts: string[] = [];
68
62
  for (const part of obj.content) {
@@ -75,14 +69,65 @@ function extractUserText(payload: unknown): string | null {
75
69
  parts.push((part as { text: string }).text);
76
70
  }
77
71
  }
78
- const joined = collapseWhitespace(parts.join(" "));
79
- if (joined.length > 0) return joined;
72
+ const joined = parts.join("\n");
73
+ if (joined.trim().length > 0) return joined;
80
74
  }
81
75
  return null;
82
76
  }
83
77
 
78
+ function extractMessageText(payload: unknown): string | null {
79
+ const raw = rawMessageText(payload);
80
+ if (raw === null) return null;
81
+ const collapsed = collapseWhitespace(raw);
82
+ return collapsed.length > 0 ? collapsed : null;
83
+ }
84
+
85
+ /**
86
+ * What the operator typed for a user turn, for the picker preview.
87
+ *
88
+ * The persisted text is the composed prompt: a `<system-reminder>` block and
89
+ * any `[Skill request]` preamble ride ahead of the operator's words as visible
90
+ * text the model receives. Reading that verbatim made every row of the
91
+ * `/resume` picker read `<system-reminder> [Skills] 9 installed…`, so sessions
92
+ * were indistinguishable and the type-to-filter matched them all (issue #188).
93
+ * Entries written since the operator text was persisted carry it as
94
+ * `operatorText`; older ones drop the leading scaffolding. Null when nothing
95
+ * operator-authored remains, so the caller moves on to the next turn.
96
+ */
97
+ function operatorTextOfUserPayload(payload: unknown): string | null {
98
+ if (
99
+ payload &&
100
+ typeof payload === "object" &&
101
+ typeof (payload as { operatorText?: unknown }).operatorText === "string"
102
+ ) {
103
+ const collapsed = collapseWhitespace((payload as { operatorText: string }).operatorText);
104
+ return collapsed.length > 0 ? collapsed : null;
105
+ }
106
+ // Strip before collapsing: the preamble's line structure is what the
107
+ // patterns anchor on.
108
+ const text = rawMessageText(payload);
109
+ if (text === null) return null;
110
+ const stripped = collapseWhitespace(stripInjectedPreamble(text));
111
+ return stripped.length > 0 ? stripped : null;
112
+ }
113
+
114
+ const LEADING_SYSTEM_REMINDER = /^\s*<system-reminder>[\s\S]*?<\/system-reminder>\s*/u;
115
+ const LEADING_SKILL_REQUEST =
116
+ /^\s*\[Skill request\][\s\S]*?Only these pending skill names are allowed this turn\.[^\n]*\s*/u;
117
+
118
+ function stripInjectedPreamble(text: string): string {
119
+ let current = text;
120
+ for (;;) {
121
+ const next = current.replace(LEADING_SYSTEM_REMINDER, "").replace(LEADING_SKILL_REQUEST, "");
122
+ if (next === current) return current;
123
+ current = next;
124
+ }
125
+ }
126
+
84
127
  interface ScanResult {
85
128
  firstUserMessage: string | null;
129
+ /** The first assistant text, the preview when no turn carries operator words. */
130
+ firstAssistantMessage: string | null;
86
131
  messageCount: number;
87
132
  lastTimestamp: string | null;
88
133
  name: string | null;
@@ -92,6 +137,7 @@ interface ScanResult {
92
137
  function scanCurrentJsonl(currentPath: string): ScanResult {
93
138
  const result: ScanResult = {
94
139
  firstUserMessage: null,
140
+ firstAssistantMessage: null,
95
141
  messageCount: 0,
96
142
  lastTimestamp: null,
97
143
  name: null,
@@ -106,10 +152,14 @@ function scanCurrentJsonl(currentPath: string): ScanResult {
106
152
  if (parsed.kind === "message" && parsed.role === "user") {
107
153
  result.messageCount += 1;
108
154
  if (result.firstUserMessage === null) {
109
- const text = extractUserText(parsed.payload);
155
+ const text = operatorTextOfUserPayload(parsed.payload);
110
156
  if (text !== null) result.firstUserMessage = text;
111
157
  }
112
158
  }
159
+ if (parsed.kind === "message" && parsed.role === "assistant" && result.firstAssistantMessage === null) {
160
+ const text = extractMessageText(parsed.payload);
161
+ if (text !== null) result.firstAssistantMessage = text;
162
+ }
113
163
  if (parsed.kind === "sessionInfo") {
114
164
  const info = parsed as SessionInfoEntry;
115
165
  if (info.name !== undefined) {
@@ -139,11 +189,10 @@ function enrichMetaForListing(meta: SessionMeta, currentPath: string): void {
139
189
  if (scan.name) meta.name = scan.name;
140
190
  const labelValues = [...scan.labels.values()].map((entry) => entry.label.trim()).filter((label) => label.length > 0);
141
191
  if (labelValues.length > 0) meta.labels = labelValues;
142
- if (scan.firstUserMessage) {
192
+ const preview = scan.firstUserMessage ?? scan.firstAssistantMessage;
193
+ if (preview) {
143
194
  meta.firstMessagePreview =
144
- scan.firstUserMessage.length > PREVIEW_MAX_CHARS
145
- ? `${scan.firstUserMessage.slice(0, PREVIEW_MAX_CHARS - 1)}…`
146
- : scan.firstUserMessage;
195
+ preview.length > PREVIEW_MAX_CHARS ? `${preview.slice(0, PREVIEW_MAX_CHARS - 1)}…` : preview;
147
196
  }
148
197
  if (scan.messageCount > 0) meta.messageCount = scan.messageCount;
149
198
  const fallbackMtime = readMtimeIso(currentPath);
@@ -70,9 +70,16 @@ export function resumeSessionState(sessionId: string): {
70
70
  } {
71
71
  const persistedMeta = readSessionMeta(sessionId) as SessionMeta;
72
72
  const paths = sessionPaths(persistedMeta);
73
- runMigrations(persistedMeta, paths.meta);
73
+ const migration = runMigrations(persistedMeta, paths.meta);
74
74
  const { meta, writer, tree } = engineResumeSession(sessionId);
75
- return { state: { meta: meta as SessionMeta, writer }, nodes: tree };
75
+ const state: SessionManagerState = { meta: meta as SessionMeta, writer };
76
+ if (migration.migrated) {
77
+ // The ledger needed no transformation; only the stamp moves, so the next
78
+ // reader does not re-run the same no-op.
79
+ state.meta.sessionFormatVersion = migration.to;
80
+ persistSessionMeta(state);
81
+ }
82
+ return { state, nodes: tree };
76
83
  }
77
84
 
78
85
  export function appendTurn(state: SessionManagerState, input: TurnInput): ClioTurnRecord {
@@ -2,7 +2,15 @@
2
2
  * Strict session-format version reader.
3
3
  *
4
4
  * Runs before every session resume. Earlier pre-1.0 formats are disposable
5
- * local state and must not be transformed into the current format.
5
+ * local state and must not be transformed into the current format. A version
6
+ * from the future is refused for the opposite reason: the file is not
7
+ * disposable, it belongs to a newer Clio, and this build would silently drop
8
+ * whatever that build understood and this one does not. Downgrading and
9
+ * resuming would then write the truncated reading back over the original.
10
+ *
11
+ * Version 3 to 4 is the one additive step: 4 only adds the working-set kinds
12
+ * (`contextEviction`, `contextRecall`), so a version-3 ledger is read as-is
13
+ * and the metadata is restamped. Nothing in the file changes.
6
14
  */
7
15
 
8
16
  import { CURRENT_SESSION_FORMAT_VERSION } from "../../../engine/session.js";
@@ -10,6 +18,9 @@ import type { SessionMeta } from "../contract.js";
10
18
 
11
19
  export { CURRENT_SESSION_FORMAT_VERSION };
12
20
 
21
+ /** Oldest version this build reads without transforming the ledger. */
22
+ export const OLDEST_READABLE_SESSION_FORMAT_VERSION = 3;
23
+
13
24
  export interface MigrationResult {
14
25
  migrated: boolean;
15
26
  from: number;
@@ -18,10 +29,18 @@ export interface MigrationResult {
18
29
 
19
30
  export function runMigrations(meta: SessionMeta, sessionPath: string): MigrationResult {
20
31
  const from = meta.sessionFormatVersion ?? 1;
21
- if (from < CURRENT_SESSION_FORMAT_VERSION) {
32
+ if (from < OLDEST_READABLE_SESSION_FORMAT_VERSION) {
33
+ throw new Error(
34
+ `session metadata has an unsupported format version (expected version ${CURRENT_SESSION_FORMAT_VERSION}, got ${from}): ${sessionPath}. Remove the session directory to start a new session.`,
35
+ );
36
+ }
37
+ if (from > CURRENT_SESSION_FORMAT_VERSION) {
22
38
  throw new Error(
23
- `session metadata has an unsupported format version (expected version 3, got ${from}): ${sessionPath}. Remove the session directory to start a new session.`,
39
+ `session was written by a newer Clio (format version ${from}, this build reads version ${CURRENT_SESSION_FORMAT_VERSION}): ${sessionPath}. Upgrade clio-coder to resume this session.`,
24
40
  );
25
41
  }
42
+ if (from < CURRENT_SESSION_FORMAT_VERSION) {
43
+ return { migrated: true, from, to: CURRENT_SESSION_FORMAT_VERSION };
44
+ }
26
45
  return { migrated: false, from, to: from };
27
46
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Fold a session ledger's assistant turns into the per-call usage the rest of
3
3
  * Clio reports: tokens in and out, cache reads and writes, reasoning tokens,
4
- * and provider-reported cost, attributed to the target and wire model that
5
- * served each call.
4
+ * and provider-reported cost, attributed to the target and observed model id
5
+ * for each call.
6
6
  *
7
7
  * This lives in the session domain because the ledger is the only durable
8
8
  * record of what a session spent, and three surfaces read it: the `/cost`
@@ -19,12 +19,22 @@
19
19
  * counting it would add an API call worth nothing.
20
20
  */
21
21
 
22
+ import {
23
+ attributedModelId,
24
+ type ResponseModelIdObservation,
25
+ responseModelIdObservationFromRecord,
26
+ } from "../../core/response-model-id.js";
22
27
  import type { SessionEntry } from "./entries.js";
23
28
 
24
29
  /** One completed assistant API call, as the ledger recorded it. */
25
30
  export interface LedgerUsageCall {
26
31
  providerId: string;
27
- modelId: string;
32
+ /** Model id the accounting row uses, or `unknown` when an observed response omitted it. */
33
+ attributedModelId: string;
34
+ /** The id the session asked for (the configured model, or the message's own `model`). */
35
+ requestedModelId: string;
36
+ /** Direct presence observation, with the pre-#193 difference-only shape labeled separately. */
37
+ responseModelIdObservation: ResponseModelIdObservation;
28
38
  input: number;
29
39
  output: number;
30
40
  cacheRead: number;
@@ -71,8 +81,8 @@ function stringAt(source: Record<string, unknown>, ...keys: string[]): string |
71
81
  * payload instead split one endpoint into two blocks in `/cost`, so a single
72
82
  * `node-a` target on `llamacpp` rendered as two providers whose turn counts
73
83
  * diverged with every resume. `modelChange` rows are replayed in order so a
74
- * session that switched targets mid-way attributes each call to the one that
75
- * served it.
84
+ * session that switched targets mid-way attributes each call to the active
85
+ * target.
76
86
  */
77
87
  export function ledgerUsageCalls(
78
88
  entries: ReadonlyArray<SessionEntry>,
@@ -92,7 +102,9 @@ export function ledgerUsageCalls(
92
102
  if (!usage || usage.totalTokens <= 0) continue;
93
103
  calls.push({
94
104
  providerId: currentTarget ?? "unknown",
95
- modelId: currentModel ?? "unknown",
105
+ attributedModelId: currentModel ?? "unknown",
106
+ requestedModelId: currentModel ?? "unknown",
107
+ responseModelIdObservation: { state: "not-observed" },
96
108
  input: usage.input,
97
109
  output: usage.output,
98
110
  cacheRead: usage.cacheRead,
@@ -132,9 +144,14 @@ export function ledgerUsageCalls(
132
144
  if (totalTokens === 0) continue;
133
145
  const cost = usage.cost;
134
146
  const costUsd = cost && typeof cost === "object" ? numberAt(cost as Record<string, unknown>, "total") : 0;
147
+ const requestedModelId = currentModel ?? stringAt(record, "model") ?? "unknown";
148
+ const differingResponseModelId = stringAt(record, "responseModel");
149
+ const responseModelIdObservation = responseModelIdObservationFromRecord(record, "legacy-difference-only");
135
150
  calls.push({
136
151
  providerId: currentTarget ?? stringAt(record, "provider", "api") ?? "unknown",
137
- modelId: currentModel ?? stringAt(record, "responseModel", "model") ?? "unknown",
152
+ attributedModelId: attributedModelId(responseModelIdObservation, requestedModelId, differingResponseModelId),
153
+ requestedModelId,
154
+ responseModelIdObservation,
138
155
  input,
139
156
  output,
140
157
  cacheRead,
@@ -1,4 +1,5 @@
1
1
  import { performance } from "node:perf_hooks";
2
+ import { describeCallAction } from "../../domains/safety/call-target.js";
2
3
  import type { AgentEvent, AgentMessage } from "../types.js";
3
4
  import type { ClioWorkerEvent } from "../worker-events.js";
4
5
  import type { AcpPromptResponse, AcpSessionUpdateParams, AcpToolCallUpdate } from "./types.js";
@@ -111,12 +112,17 @@ export class AcpEventMapper {
111
112
  // keeps the monotonic twin, which is what the duration below spans.
112
113
  const startedAt = Date.now();
113
114
  this.toolStarts.set(toolCallId, performance.now());
115
+ // A peer's `rawInput` is its own argument object and never reaches an
116
+ // operator surface. The descriptor composed from it here does.
117
+ const action = describeCallAction(update.kind ?? title, update.rawInput);
114
118
  out.push({
115
119
  type: "clio_tool_start",
116
120
  payload: {
117
121
  tool: update.kind ?? title,
122
+ toolCallId,
118
123
  posture: "operating",
119
124
  startedAt,
125
+ ...(action !== null ? { action } : {}),
120
126
  },
121
127
  });
122
128
  out.push({
@@ -142,6 +148,7 @@ export class AcpEventMapper {
142
148
  type: "clio_tool_finish",
143
149
  payload: {
144
150
  tool: update.kind ?? title,
151
+ toolCallId,
145
152
  posture: "operating",
146
153
  durationMs: Math.round(performance.now() - startedAtClock),
147
154
  outcome: isError ? "error" : "ok",
@@ -5,13 +5,20 @@ import { BusChannels, type LoopBlockedPayload } from "../../core/bus-events.js";
5
5
  import { DEFAULT_DELEGATION_PERMISSION_TIMEOUT_MS } from "../../core/defaults.js";
6
6
  import type { SafeEventBus } from "../../core/event-bus.js";
7
7
  import { MAX_TIMER_DELAY_MS } from "../../core/timers.js";
8
+ import { ToolNames } from "../../core/tool-names.js";
8
9
  import type { ProvidersContract } from "../../domains/providers/contract.js";
9
10
  import { isOrchestratorEligibleRuntime } from "../../domains/providers/eligibility.js";
10
11
  import { type AutonomyLevel, DEFAULT_AUTONOMY_LEVEL } from "../../domains/safety/autonomy.js";
12
+ import {
13
+ classifyDecisionPresentation,
14
+ decisionFactsForPermission,
15
+ } from "../../domains/safety/decision-presentation.js";
11
16
  import type { SessionContract, SessionMeta } from "../../domains/session/contract.js";
12
17
  import type { MessageEntry, SessionEntry } from "../../domains/session/entries.js";
13
18
  import { filterEntriesToActivePath } from "../../domains/session/tree/active-path.js";
19
+ import { askUserExposure } from "../../tools/ask-user.js";
14
20
  import type { ToolRegistry } from "../../tools/registry.js";
21
+ import { toolResultPresentationText } from "../../tools/result-disposition.js";
15
22
  import type { AgentMessage } from "../types.js";
16
23
  import { ACP_TURN_FAILED_MESSAGE, AcpRequestError, AcpTimeoutError, acpErrorMessage } from "./errors.js";
17
24
  import type { AcpJsonRpcPeerTransport } from "./transport.js";
@@ -537,6 +544,8 @@ function applyStopReason(active: ActivePrompt, message: unknown): void {
537
544
  function outputText(value: unknown): string {
538
545
  if (typeof value === "string") return value;
539
546
  if (!isRecord(value)) return "";
547
+ const presentationText = toolResultPresentationText(value);
548
+ if (presentationText !== null) return presentationText;
540
549
  if (Array.isArray(value.content)) return contentText(value.content);
541
550
  if (typeof value.output === "string") return value.output;
542
551
  if (typeof value.text === "string") return value.text;
@@ -1262,6 +1271,19 @@ function installPermissionBridge(input: {
1262
1271
  const queuedRequestDetails = new Map<string, { tool: string; actionClass: string }>();
1263
1272
  const unregister = input.toolRegistry.onPermissionRequired((call, decision, meta) => {
1264
1273
  if (queuedRequestIds.has(meta.requestId)) return;
1274
+ const presentation = classifyDecisionPresentation(
1275
+ decisionFactsForPermission({
1276
+ tool: call.tool,
1277
+ actionClass: decision.classification.actionClass,
1278
+ axis: meta.axis.startsWith("net:")
1279
+ ? { kind: "safety-net", ruleId: meta.axis.slice("net:".length) || "unknown" }
1280
+ : { kind: "autonomy", level: meta.axis.slice("autonomy:".length) || DEFAULT_AUTONOMY_LEVEL },
1281
+ origin: { kind: "main" },
1282
+ ...(call.tool === ToolNames.AskUser ? { exposure: askUserExposure(call.args) } : {}),
1283
+ }),
1284
+ );
1285
+ const approveAction = presentation.requiredActions.find((action) => action.id === "approve-once");
1286
+ const denyAction = presentation.requiredActions.find((action) => action.id === "deny");
1265
1287
  queuedRequestIds.add(meta.requestId);
1266
1288
  queuedRequestDetails.set(meta.requestId, {
1267
1289
  tool: call.tool,
@@ -1378,8 +1400,16 @@ function installPermissionBridge(input: {
1378
1400
  ...(snapshot.locations !== undefined ? { locations: snapshot.locations } : {}),
1379
1401
  },
1380
1402
  options: [
1381
- { optionId: "allow-once", name: "Allow once", kind: "allow_once" },
1382
- { optionId: "reject-once", name: "Reject", kind: "reject_once" },
1403
+ {
1404
+ optionId: "allow-once",
1405
+ name: approveAction?.label ?? "Approve once",
1406
+ kind: "allow_once",
1407
+ },
1408
+ {
1409
+ optionId: "reject-once",
1410
+ name: denyAction?.label ?? "Deny this request",
1411
+ kind: "reject_once",
1412
+ },
1383
1413
  ],
1384
1414
  },
1385
1415
  input.permissionTimeoutMs,
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { Agent, type AgentOptions, type StreamFn } from "@earendil-works/pi-agent-core";
14
+ import { isDispositionedToolResultError } from "../tools/result-disposition.js";
14
15
  import { engineStreamSimple } from "./api-registry.js";
15
16
 
16
17
  export type EngineAgentOptions = Omit<AgentOptions, "streamFn"> & { streamFn?: StreamFn };
@@ -20,8 +21,24 @@ export interface EngineAgentHandle {
20
21
  state(): Agent["state"];
21
22
  }
22
23
 
24
+ function dispositionAwareAfterToolCall(
25
+ delegate: AgentOptions["afterToolCall"],
26
+ ): NonNullable<AgentOptions["afterToolCall"]> {
27
+ return async (context, signal) => {
28
+ const override = await delegate?.(context, signal);
29
+ const effectiveResult =
30
+ override?.details === undefined ? context.result : { ...context.result, details: override.details };
31
+ if (isDispositionedToolResultError(effectiveResult)) return { ...override, isError: true };
32
+ return override;
33
+ };
34
+ }
35
+
23
36
  export function createEngineAgent(options: EngineAgentOptions = {}): EngineAgentHandle {
24
- const agent = new Agent({ streamFn: engineStreamSimple, ...options });
37
+ const agent = new Agent({
38
+ streamFn: engineStreamSimple,
39
+ ...options,
40
+ afterToolCall: dispositionAwareAfterToolCall(options.afterToolCall),
41
+ });
25
42
  return {
26
43
  agent,
27
44
  state: () => agent.state,
@@ -130,6 +130,25 @@ async function loadOwnedInstance(
130
130
  return instanceId;
131
131
  }
132
132
 
133
+ /**
134
+ * Residency runs before every request, and the same co-residency facts held
135
+ * every turn, so the peer warning printed once per turn (issue #185). Each
136
+ * distinct fact is said once per process; a new target, model, or peer set is
137
+ * a new fact.
138
+ */
139
+ const announcedResidencyFacts = new Set<string>();
140
+
141
+ function emitResidencyNoticeOnce(key: string, notice: Parameters<typeof emitResidencyNotice>[0]): void {
142
+ if (announcedResidencyFacts.has(key)) return;
143
+ announcedResidencyFacts.add(key);
144
+ emitResidencyNotice(notice);
145
+ }
146
+
147
+ /** Test seam: forget which residency facts were announced. */
148
+ export function resetAnnouncedResidencyFacts(): void {
149
+ announcedResidencyFacts.clear();
150
+ }
151
+
133
152
  export async function ensureLmStudioResidency(
134
153
  model: Model<"openai-completions">,
135
154
  options: { apiKey?: string; signal?: AbortSignal } = {},
@@ -155,7 +174,7 @@ export async function ensureLmStudioResidency(
155
174
  );
156
175
  }
157
176
  if (resolution.wireModelId !== model.id) {
158
- emitResidencyNotice({
177
+ emitResidencyNoticeOnce(`resolved|${info.targetId}|${model.id}|${resolution.wireModelId}`, {
159
178
  kind: "co-resident",
160
179
  level: "info",
161
180
  targetId: info.targetId,
@@ -166,14 +185,16 @@ export async function ensureLmStudioResidency(
166
185
  });
167
186
  }
168
187
  if (resolution.peerTargets.length > 0) {
169
- emitResidencyNotice({
188
+ const peers = resolution.peerTargets.join(", ");
189
+ const requested = resolution.wireModelId === model.id ? "" : ` (requested '${model.id}')`;
190
+ emitResidencyNoticeOnce(`peer|${info.targetId}|${model.id}|${resolution.wireModelId}|${peers}`, {
170
191
  kind: "co-resident",
171
192
  level: "warning",
172
193
  targetId: info.targetId,
173
194
  runtimeId: "lmstudio",
174
195
  model: resolution.wireModelId,
175
- message: `LM Studio instance '${resolution.wireModelId}' is also loaded on ${resolution.peerTargets.join(", ")}; this request may be served by that LM Link peer.`,
176
- detail: { peerTargets: resolution.peerTargets.join(", ") },
196
+ message: `LM Studio instance '${resolution.wireModelId}'${requested} is also loaded on ${peers}; a request may be served by that LM Link peer, and the footer and usage ledger name the id that answered when it differs.`,
197
+ detail: { requestedModel: model.id, wireModel: resolution.wireModelId, peerTargets: peers },
177
198
  });
178
199
  }
179
200
  if (!load || Object.keys(load).length === 0 || resolution.instance) return resolution.wireModelId;