@iowarp/clio-coder 0.3.4 → 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 (265) hide show
  1. package/CHANGELOG.md +37 -2
  2. package/CONTRIBUTING.md +6 -6
  3. package/README.md +2 -2
  4. package/dist/{acp-S5R4RR5B.js → acp-2BEHC4DL.js} +4 -4
  5. package/dist/{agents-P6DMMVZY.js → agents-LNNFTM53.js} +13 -11
  6. package/dist/assets/codewiki.json +1 -1
  7. package/dist/{auth-2XCZLPKS.js → auth-KXXFI2VS.js} +6 -6
  8. package/dist/{chunk-YCWGATWI.js → chunk-24I7BN55.js} +2 -2
  9. package/dist/{chunk-EKMEHE4H.js → chunk-33YXPOE3.js} +2 -3
  10. package/dist/chunk-3BPUFZDL.js +37 -0
  11. package/dist/{chunk-WPQLXFOZ.js → chunk-43AOLP7E.js} +2 -2
  12. package/dist/{chunk-N4CZJQRK.js → chunk-5JGRAMKL.js} +4 -4
  13. package/dist/{chunk-BRXQQJFP.js → chunk-6US73PDB.js} +568 -47
  14. package/dist/{chunk-K6WL7QZT.js → chunk-6XXKFVSN.js} +2 -2
  15. package/dist/{chunk-QQK64KLB.js → chunk-CJUB2JJ2.js} +138 -20
  16. package/dist/{chunk-HV5X7OR2.js → chunk-CKXWIANG.js} +12 -12
  17. package/dist/{chunk-UZHIZC5S.js → chunk-CYQKWTG3.js} +61 -76
  18. package/dist/{chunk-QWU7ZBO7.js → chunk-DJVECN66.js} +204 -45
  19. package/dist/{chunk-ZWMF7253.js → chunk-E2ER4LJF.js} +304 -9
  20. package/dist/{chunk-7RXG6QRZ.js → chunk-EKY57CSP.js} +2 -75
  21. package/dist/{chunk-EDRHSCIE.js → chunk-EYPA3EGJ.js} +10 -2
  22. package/dist/{chunk-TTNYS3EA.js → chunk-G7MUEIGA.js} +1 -1
  23. package/dist/{chunk-BPGS2WCQ.js → chunk-GEYXPTRF.js} +2 -1
  24. package/dist/{chunk-BEY543CS.js → chunk-GOXNB3AO.js} +5 -2
  25. package/dist/{chunk-G4BMMOKF.js → chunk-HVDIIIQW.js} +2 -2
  26. package/dist/chunk-HWUFFB6L.js +83 -0
  27. package/dist/{chunk-35MKKU5R.js → chunk-K7T3E2SR.js} +15 -8
  28. package/dist/{chunk-VAWWTKDP.js → chunk-KHSFENX2.js} +2 -2
  29. package/dist/chunk-LCGCVYZ4.js +57 -0
  30. package/dist/{chunk-X6COSD2O.js → chunk-LYF7OHWH.js} +41 -14
  31. package/dist/{chunk-POHLU5DW.js → chunk-M6L6IDJG.js} +3 -3
  32. package/dist/{chunk-X4RCMKVQ.js → chunk-NDINPTJ4.js} +2 -2
  33. package/dist/{chunk-5M54SPOL.js → chunk-ODFEOB4F.js} +161 -5
  34. package/dist/{chunk-3JLKSKD7.js → chunk-OH3TOQTB.js} +5 -1
  35. package/dist/{chunk-MEQ45TQ4.js → chunk-PBTHKCPN.js} +18 -4
  36. package/dist/{chunk-ED4KHGC3.js → chunk-PPAMZ32Z.js} +9 -2
  37. package/dist/{chunk-QQL5RT5M.js → chunk-QM3F2GKX.js} +94 -36
  38. package/dist/{chunk-A2GZF7DC.js → chunk-QNQHSOLF.js} +4 -4
  39. package/dist/{chunk-KRPY7NTG.js → chunk-R46L2BIR.js} +3 -3
  40. package/dist/{chunk-BP4OYD6A.js → chunk-RY3LY4J5.js} +20 -2
  41. package/dist/{chunk-34475P3I.js → chunk-TSHXZTOQ.js} +5 -4
  42. package/dist/{chunk-VJWL6YS5.js → chunk-UUVG37B4.js} +2 -2
  43. package/dist/{chunk-2TZWSW76.js → chunk-WHGPSPT5.js} +2 -2
  44. package/dist/{chunk-TW3WDMVS.js → chunk-WHJYKASB.js} +2 -2
  45. package/dist/{chunk-YHZX5GEU.js → chunk-XAKHZX5N.js} +2 -2
  46. package/dist/{chunk-HXG4IURW.js → chunk-XE2VEJHX.js} +2 -2
  47. package/dist/{chunk-3HZ5RWN2.js → chunk-XF5N4U5A.js} +7 -6
  48. package/dist/{chunk-ZYKPLLNQ.js → chunk-XXQNGV4M.js} +590 -32
  49. package/dist/{chunk-4JUF2NNX.js → chunk-XYDYPRZI.js} +4 -4
  50. package/dist/{chunk-VMNQ6OZA.js → chunk-ZRGEBJ4T.js} +971 -794
  51. package/dist/{chunk-2LZI5CAG.js → chunk-ZXF4XRKW.js} +75 -33
  52. package/dist/{chunk-VSNATDE6.js → chunk-ZZMN5OM4.js} +2 -2
  53. package/dist/cli/index.js +31 -31
  54. package/dist/{clio-J5JIOIDS.js → clio-M2KGYUFZ.js} +2 -2
  55. package/dist/{code-nav-AXCXSBHX.js → code-nav-GQNL7XA6.js} +5 -5
  56. package/dist/codewiki/build-worker.js +4 -4
  57. package/dist/{components-KELWS457.js → components-5TTYYX6G.js} +3 -3
  58. package/dist/{config-OEBMIN2U.js → config-XUUYQIWO.js} +27 -25
  59. package/dist/{configure-PUQOSIXQ.js → configure-IHJ7YOMV.js} +7 -7
  60. package/dist/{context-URSXPBCK.js → context-74JLXAWD.js} +12 -12
  61. package/dist/{context-MGSE4Z2T.js → context-75MIWW3U.js} +24 -22
  62. package/dist/{context-EKDCKUUZ.js → context-ZQ7SIFJV.js} +8 -7
  63. package/dist/{context-clear-KDAJRNUK.js → context-clear-GYKWNUML.js} +24 -22
  64. package/dist/{context-index-BZ4UYMTC.js → context-index-SSR5ECNE.js} +3 -3
  65. package/dist/{context-working-set-SBKMPPI2.js → context-working-set-UX5KEP4J.js} +11 -10
  66. package/dist/{dispatch-runner-MSWN72NK.js → dispatch-runner-GIJBHNFL.js} +21 -20
  67. package/dist/{docs-2C2LTVT2.js → docs-6FZSCG5B.js} +3 -3
  68. package/dist/{doctor-7BSE27PJ.js → doctor-SVJ5BZCW.js} +4 -4
  69. package/dist/{eval-IZGDOO4H.js → eval-CG6LLBLD.js} +47 -232
  70. package/dist/{evidence-SR7WXB5B.js → evidence-ZYFIEN42.js} +19 -18
  71. package/dist/{evolve-K7VE2CBX.js → evolve-QGEXEMDW.js} +19 -18
  72. package/dist/{extensions-QVDOHDGJ.js → extensions-ADGNCJJD.js} +3 -3
  73. package/dist/{fleet-7XMJNQNF.js → fleet-S5R4ZOQY.js} +49 -30
  74. package/dist/{fleet-preflight-AQNAH644.js → fleet-preflight-BHSNPBMH.js} +2 -2
  75. package/dist/{init-JGNPAYXT.js → init-5DRU55YR.js} +31 -29
  76. package/dist/memory-7YKKR6UC.js +467 -0
  77. package/dist/{models-ZMMLFJNN.js → models-ZPOLRU2C.js} +10 -10
  78. package/dist/{monitor-2F3T5KHP.js → monitor-US5F5YGZ.js} +33 -18
  79. package/dist/{orchestrator-ORHT43JB.js → orchestrator-E2AL4T5N.js} +1092 -659
  80. package/dist/{paths-UXLN5YYZ.js → paths-E7KYAQWE.js} +3 -3
  81. package/dist/{reset-NXGTYNUO.js → reset-KZ652EK6.js} +3 -3
  82. package/dist/{run-RF4WJGMT.js → run-SRNBKDWD.js} +52 -40
  83. package/dist/{share-UT3W6E4M.js → share-CGZE33UP.js} +3 -3
  84. package/dist/{skills-PSACKC5Q.js → skills-S2X4DLY5.js} +4 -4
  85. package/dist/{skills-eval-WJSI55RZ.js → skills-eval-W2GGIC4R.js} +19 -18
  86. package/dist/{targets-PIIRAOYS.js → targets-54SWINWB.js} +14 -12
  87. package/dist/{terminal-lease-ULWXWNVY.js → terminal-lease-SAIF2OGY.js} +5 -4
  88. package/dist/{uninstall-FZCQCDKC.js → uninstall-BVLWXKBT.js} +3 -3
  89. package/dist/{upgrade-346TZ6AV.js → upgrade-JKAR27XC.js} +8 -8
  90. package/dist/{usage-6KKXR32N.js → usage-MSAWCLX4.js} +60 -27
  91. package/dist/{verifiers-4UUM6TEE.js → verifiers-NCBTHHN2.js} +60 -54
  92. package/dist/{wiki-generate-7STOCIFZ.js → wiki-generate-GUSOQ6ZP.js} +30 -28
  93. package/dist/worker/entry.js +69 -58
  94. package/dist/{workspace-G4ZWUIPR.js → workspace-ZJ6BFM3Q.js} +4 -4
  95. package/docs/README.md +3 -3
  96. package/docs/acp.md +1 -1
  97. package/docs/alcf-provider.md +1 -1
  98. package/docs/architecture.md +2 -2
  99. package/docs/artifact-placement.md +1 -2
  100. package/docs/artifact-versions.md +1 -1
  101. package/docs/built-in-agents.md +1 -1
  102. package/docs/capacity-and-scheduling.md +1 -1
  103. package/docs/commands-and-modes.md +9 -7
  104. package/docs/configuration-and-targets.md +12 -1
  105. package/docs/context-engine.md +4 -2
  106. package/docs/context-working-set.md +4 -4
  107. package/docs/development-pipeline.md +1 -1
  108. package/docs/documentation-coverage.md +3 -3
  109. package/docs/documentation-guide.md +2 -2
  110. package/docs/eval-runner.md +1 -1
  111. package/docs/evals-internal.md +4 -45
  112. package/docs/evidence-and-memory.md +67 -7
  113. package/docs/evolution.md +1 -1
  114. package/docs/exit-codes-and-output.md +1 -1
  115. package/docs/extensions-and-sharing.md +2 -2
  116. package/docs/fleet-dispatch.md +28 -2
  117. package/docs/installation-and-lifecycle.md +2 -2
  118. package/docs/middleware-and-components.md +19 -2
  119. package/docs/model-catalog.md +1 -1
  120. package/docs/observability.md +3 -3
  121. package/docs/proactive-memory.md +26 -16
  122. package/docs/prompt-envelope-and-tools.md +4 -2
  123. package/docs/provider-adapter-cookbook.md +1 -1
  124. package/docs/release-cut-checklist.md +38 -35
  125. package/docs/safety-model.md +29 -7
  126. package/docs/scientific-validation.md +3 -3
  127. package/docs/session-lifecycle.md +1 -1
  128. package/docs/skills-marketplace.md +1 -1
  129. package/docs/tool-usage.md +2 -2
  130. package/docs/trace-store.md +1 -1
  131. package/docs/troubleshooting.md +1 -1
  132. package/docs/tui-design.md +38 -4
  133. package/docs/worker-dispatch-mechanics.md +1 -1
  134. package/package.json +7 -4
  135. package/src/cli/agents.ts +2 -3
  136. package/src/cli/argv.ts +14 -1
  137. package/src/cli/fleet.ts +15 -0
  138. package/src/cli/index.ts +1 -1
  139. package/src/cli/memory.ts +272 -10
  140. package/src/cli/modes/json-stream.ts +2 -2
  141. package/src/cli/modes/print.ts +12 -1
  142. package/src/cli/run.ts +22 -2
  143. package/src/cli/targets.ts +12 -3
  144. package/src/cli/usage.ts +55 -7
  145. package/src/core/bus-events.ts +3 -0
  146. package/src/core/response-model-id.ts +134 -0
  147. package/src/core/toml.ts +62 -0
  148. package/src/core/workspace-files.ts +0 -1
  149. package/src/domains/agents/builtins/architect.md +1 -1
  150. package/src/domains/agents/catalog.ts +5 -4
  151. package/src/domains/agents/recipe.ts +54 -14
  152. package/src/domains/agents/result-contract.ts +7 -4
  153. package/src/domains/context/bootstrap.ts +36 -27
  154. package/src/domains/context/project-metadata.ts +19 -63
  155. package/src/domains/context/prompt-context.ts +8 -0
  156. package/src/domains/context/working-set/policies/index.ts +3 -4
  157. package/src/domains/dispatch/budget-envelope.ts +396 -0
  158. package/src/domains/dispatch/contract.ts +2 -0
  159. package/src/domains/dispatch/extension.ts +81 -27
  160. package/src/domains/dispatch/orphan-recovery.ts +1 -0
  161. package/src/domains/dispatch/receipt-integrity.ts +4 -0
  162. package/src/domains/dispatch/state.ts +1 -0
  163. package/src/domains/dispatch/types.ts +10 -3
  164. package/src/domains/dispatch/validation.ts +14 -0
  165. package/src/domains/dispatch/worker-spawn.ts +14 -3
  166. package/src/domains/eval/metrics/evidence.ts +0 -116
  167. package/src/domains/eval/metrics/invariants.ts +1 -1
  168. package/src/domains/eval/runners/clio-run.ts +1 -10
  169. package/src/domains/eval/runners/external-command.ts +2 -29
  170. package/src/domains/eval/schema/suite.ts +0 -7
  171. package/src/domains/eval/suites/run.ts +1 -7
  172. package/src/domains/memory/index.ts +22 -0
  173. package/src/domains/memory/operations.ts +58 -1
  174. package/src/domains/memory/promotion.ts +281 -0
  175. package/src/domains/memory/prompt-section.ts +25 -5
  176. package/src/domains/memory/proposal.ts +51 -7
  177. package/src/domains/memory/task-bank.ts +3 -2
  178. package/src/domains/memory/task-memory-handoff.ts +181 -24
  179. package/src/domains/memory/task-memory-policy.ts +3 -1
  180. package/src/domains/memory/types.ts +37 -0
  181. package/src/domains/memory/validate.ts +178 -0
  182. package/src/domains/middleware/memory-intervention.ts +35 -25
  183. package/src/domains/middleware/runtime.ts +6 -0
  184. package/src/domains/middleware/skills-reminder.ts +19 -4
  185. package/src/domains/middleware/stalled-turn.ts +43 -1
  186. package/src/domains/middleware/types.ts +10 -0
  187. package/src/domains/observability/contract.ts +6 -1
  188. package/src/domains/observability/cost.ts +20 -4
  189. package/src/domains/observability/extension.ts +2 -2
  190. package/src/domains/providers/index.ts +3 -0
  191. package/src/domains/providers/model-discovery.ts +9 -0
  192. package/src/domains/providers/runtime-resolution.ts +38 -1
  193. package/src/domains/providers/runtimes/common/probe-helpers.ts +97 -16
  194. package/src/domains/providers/types/context-window-slots.ts +18 -0
  195. package/src/domains/providers/types/runtime-descriptor.ts +3 -1
  196. package/src/domains/safety/call-target.ts +211 -14
  197. package/src/domains/safety/decision-presentation.ts +268 -0
  198. package/src/domains/safety/redaction.ts +73 -0
  199. package/src/domains/session/context-ledger.ts +10 -1
  200. package/src/domains/session/decision-board.ts +4 -0
  201. package/src/domains/session/entries.ts +3 -0
  202. package/src/domains/session/history.ts +68 -19
  203. package/src/domains/session/usage.ts +24 -7
  204. package/src/engine/acp/event-mapper.ts +7 -0
  205. package/src/engine/acp/server.ts +29 -2
  206. package/src/engine/apis/lmstudio.ts +25 -4
  207. package/src/engine/apis/openai-completions.ts +147 -22
  208. package/src/engine/claude/sdk-runtime.ts +8 -2
  209. package/src/engine/claude/tool-safety.ts +13 -0
  210. package/src/engine/loop-guard.ts +27 -3
  211. package/src/engine/worker-events.ts +4 -3
  212. package/src/engine/worker-runtime.ts +59 -54
  213. package/src/entry/orchestrator.ts +18 -1
  214. package/src/interactive/chat-loop-messages.ts +22 -0
  215. package/src/interactive/chat-loop.ts +13 -0
  216. package/src/interactive/chat-renderer.ts +19 -3
  217. package/src/interactive/clio-editor.ts +44 -7
  218. package/src/interactive/context-overlay.ts +43 -5
  219. package/src/interactive/cost-overlay.ts +39 -8
  220. package/src/interactive/dispatch-board.ts +212 -35
  221. package/src/interactive/footer/widgets.ts +13 -0
  222. package/src/interactive/interactive-application.ts +6 -1
  223. package/src/interactive/interactive-input-runtime.ts +11 -1
  224. package/src/interactive/interactive-presentation.ts +11 -1
  225. package/src/interactive/memory-overlay.ts +89 -4
  226. package/src/interactive/overlay-ask-user-lifecycle.ts +1 -1
  227. package/src/interactive/overlay-frame.ts +5 -2
  228. package/src/interactive/overlay-general-openers.ts +40 -1
  229. package/src/interactive/overlay-key-routing.ts +41 -1
  230. package/src/interactive/overlay-lifecycle.ts +11 -4
  231. package/src/interactive/overlay-permission-lifecycle.ts +23 -8
  232. package/src/interactive/overlay-transitions.ts +11 -0
  233. package/src/interactive/overlays/ask-user.ts +74 -30
  234. package/src/interactive/overlays/decisions.ts +3 -1
  235. package/src/interactive/permission-hint.ts +35 -0
  236. package/src/interactive/permission-overlay.ts +95 -45
  237. package/src/interactive/renderers/tool-execution.ts +19 -49
  238. package/src/interactive/session-last-turn.ts +8 -1
  239. package/src/interactive/session-usage-reseed.ts +36 -10
  240. package/src/interactive/slash-commands.ts +2 -2
  241. package/src/interactive/status/summary.ts +5 -0
  242. package/src/interactive/status/types.ts +5 -0
  243. package/src/interactive/terminal-lease.ts +1 -0
  244. package/src/interactive/turn-context.ts +96 -23
  245. package/src/interactive/turn-middleware.ts +1 -0
  246. package/src/interactive/turn-runtime.ts +37 -8
  247. package/src/interactive/turn-state.ts +3 -0
  248. package/src/interactive/worker-progress.ts +440 -0
  249. package/src/interactive/worker-stream.ts +51 -110
  250. package/src/tools/agent-tools.ts +28 -3
  251. package/src/tools/ask-user.ts +21 -1
  252. package/src/tools/context/index.ts +2 -2
  253. package/src/tools/dispatch-arguments.ts +8 -0
  254. package/src/tools/dispatch-event-text.ts +19 -0
  255. package/src/tools/dispatch.ts +24 -1
  256. package/src/tools/monitor.ts +15 -0
  257. package/src/tools/registry.ts +15 -5
  258. package/src/tools/result-disposition.ts +156 -0
  259. package/src/tools/result-shaping.ts +59 -1
  260. package/src/tools/verify/authoring.ts +55 -54
  261. package/src/tools/worker-evidence.ts +19 -0
  262. package/src/worker/spec-contract.ts +43 -3
  263. package/dist/chunk-EFADSJET.js +0 -18
  264. package/dist/memory-4ALKDJ4Q.js +0 -246
  265. package/src/domains/eval/metrics/chaos-stream.ts +0 -93
@@ -22,6 +22,7 @@ import { type SkillActivation, skillActivationFromToolDetails } from "../core/sk
22
22
  import type { ToolName } from "../core/tool-names.js";
23
23
  import { ToolNames } from "../core/tool-names.js";
24
24
  import type { ResolvedRuntimeTarget } from "../domains/providers/index.js";
25
+ import { type CallActionDescriptor, describeCallAction } from "../domains/safety/call-target.js";
25
26
  import type { SafetyDecision } from "../domains/safety/contract.js";
26
27
  import { formatModelRejection } from "../domains/safety/rejection-feedback.js";
27
28
  import { validateEngineToolArguments } from "../engine/ai.js";
@@ -45,6 +46,12 @@ export type ToolOutcome = "ok" | "error" | "blocked";
45
46
 
46
47
  export interface ToolStartEvent {
47
48
  tool: string;
49
+ /**
50
+ * The engine's id for this call, when the producer has one. New producers
51
+ * put the same id on start and finish so concurrent calls of one tool remain
52
+ * independently attributable. Optional for direct callers and old streams.
53
+ */
54
+ toolCallId?: string;
48
55
  posture: "operating";
49
56
  /**
50
57
  * Wall-clock anchor for the instant the call began, for a human correlating
@@ -53,6 +60,14 @@ export interface ToolStartEvent {
53
60
  * event is measured on that process's monotonic clock instead.
54
61
  */
55
62
  startedAt: number;
63
+ /**
64
+ * What this call is doing, as a bounded redacted descriptor composed here
65
+ * where the arguments are trusted. This is the only account of a worker's
66
+ * arguments that may cross the NDJSON stdout seam; the arguments themselves
67
+ * never do, so an operator surface reads a verb and an object it can show,
68
+ * never an argument object it would have to sanitize itself.
69
+ */
70
+ action?: CallActionDescriptor;
56
71
  }
57
72
 
58
73
  export interface ToolFinishEvent {
@@ -130,11 +145,21 @@ async function runValidatedToolCall(input: RunValidatedToolCallInput): Promise<W
130
145
  // spans the call, so `durationMs` survives a clock correction mid-tool.
131
146
  const startedAt = Date.now();
132
147
  const startedAtClock = performance.now();
133
- telemetry?.onStart?.({ tool: spec.name, posture: "operating", startedAt });
134
- // Stamped onto every finish event so a consumer can join this authoritative
135
- // outcome to the engine's `tool_execution_end` for the same call.
148
+ // Stamped onto both lifecycle events so a concurrent finish resolves only
149
+ // the start for its own engine call. Direct callers may legitimately omit it.
136
150
  const callId = input.invokeOptions?.toolCallId;
137
151
  const withCallId = callId === undefined ? {} : { toolCallId: callId };
152
+ // The one seam that holds validated arguments and publishes telemetry, so it
153
+ // is where the redacted descriptor is composed. Everything downstream of here
154
+ // sees the descriptor and never the arguments.
155
+ const action = describeCallAction(spec.name, args);
156
+ telemetry?.onStart?.({
157
+ tool: spec.name,
158
+ ...withCallId,
159
+ posture: "operating",
160
+ startedAt,
161
+ ...(action !== null ? { action } : {}),
162
+ });
138
163
  const invokeOpts: ToolInvokeOptions = {};
139
164
  if (input.invokeOptions) Object.assign(invokeOpts, input.invokeOptions);
140
165
  if (signal) invokeOpts.signal = signal;
@@ -5,6 +5,7 @@ import { Type } from "typebox";
5
5
  import { ToolNames } from "../core/tool-names.js";
6
6
  import { clioStateDir } from "../core/xdg.js";
7
7
  import { type AutonomyExposure, DEFAULT_AUTONOMY_EXPOSURE } from "../domains/safety/autonomy.js";
8
+ import { classifyDecisionPresentation, decisionFactsForAnswer } from "../domains/safety/decision-presentation.js";
8
9
  import { StringEnum } from "../engine/ai.js";
9
10
  import type {
10
11
  AskUserToolPolicy,
@@ -57,6 +58,7 @@ export interface AskUserResult {
57
58
 
58
59
  export interface AskUserCall {
59
60
  action: AskUserAction;
61
+ exposure?: AutonomyExposure;
60
62
  mode?: AskUserMode;
61
63
  questions?: AskUserQuestion[];
62
64
  decisions?: AskUserDecision[];
@@ -284,6 +286,7 @@ export function normalizeAskUserCall(args: Record<string, unknown>): { call?: As
284
286
  return {
285
287
  call: {
286
288
  action: "ask",
289
+ exposure: exposure.exposure ?? DEFAULT_AUTONOMY_EXPOSURE,
287
290
  ...(mode.mode ? { mode: mode.mode } : {}),
288
291
  questions: questions.questions,
289
292
  ...(summary ? { summary } : {}),
@@ -319,6 +322,7 @@ function createStandalonePolicy(options?: ToolInvokeOptions): AskUserToolPolicy
319
322
  updatedAt: now,
320
323
  ...(options?.sessionId ? { sessionId: options.sessionId } : {}),
321
324
  ...(options?.turnId ? { turnId: options.turnId } : {}),
325
+ exposure: DEFAULT_AUTONOMY_EXPOSURE,
322
326
  rounds: [],
323
327
  decisions: [],
324
328
  inFlight: false,
@@ -335,6 +339,13 @@ function hydratePolicy(policy: AskUserToolPolicy, options?: ToolInvokeOptions):
335
339
  if (options?.turnId && !policy.turnId) policy.turnId = options.turnId;
336
340
  }
337
341
 
342
+ /** Once an interview is outward, a later local round cannot lower its replay tier. */
343
+ function mergePolicyExposure(policy: AskUserToolPolicy, exposure: AutonomyExposure): AutonomyExposure {
344
+ const merged = policy.exposure === "outward" || exposure === "outward" ? "outward" : "local";
345
+ policy.exposure = merged;
346
+ return merged;
347
+ }
348
+
338
349
  function transcriptQuestions(questions: ReadonlyArray<AskUserQuestion>): AskUserTranscriptQuestion[] {
339
350
  return questions.map((question) => ({
340
351
  question: question.question,
@@ -455,6 +466,7 @@ async function persistAskUserTranscript(
455
466
  ...(policy.endedAt ? { endedAt: policy.endedAt } : {}),
456
467
  ...(policy.sessionId ? { sessionId: policy.sessionId } : {}),
457
468
  ...(policy.turnId ? { turnId: policy.turnId } : {}),
469
+ exposure: policy.exposure ?? DEFAULT_AUTONOMY_EXPOSURE,
458
470
  ...(policy.summary ? { summary: policy.summary } : {}),
459
471
  decisions: policy.decisions,
460
472
  rounds: policy.rounds,
@@ -478,6 +490,7 @@ function compactInterview(
478
490
  rounds: policy.rounds.length,
479
491
  max_rounds: policy.maxCalls,
480
492
  transcript_path: policy.transcriptPath ?? null,
493
+ exposure: policy.exposure ?? DEFAULT_AUTONOMY_EXPOSURE,
481
494
  latest_answers: latestAnswers,
482
495
  decisions: policy.decisions,
483
496
  ...(policy.summary ? { summary: policy.summary } : {}),
@@ -591,6 +604,7 @@ export function createAskUserTool(deps: AskUserToolDeps = {}): ToolSpec {
591
604
  return completeInterview(policy, "complete", options, call.summary, call.decisions ?? []);
592
605
  }
593
606
  const questions = call.questions ?? [];
607
+ const exposure = mergePolicyExposure(policy, call.exposure ?? DEFAULT_AUTONOMY_EXPOSURE);
594
608
  if (call.max_rounds !== undefined) {
595
609
  const nextLimit = policy.callCount === 0 ? call.max_rounds : Math.max(policy.maxCalls, call.max_rounds);
596
610
  policy.maxCalls = Math.max(nextLimit, policy.callCount + 1);
@@ -632,7 +646,13 @@ export function createAskUserTool(deps: AskUserToolDeps = {}): ToolSpec {
632
646
  if (call.summary) policy.summary = call.summary;
633
647
  const handler = deps.askUser ?? defaultAskUserHandler;
634
648
  try {
635
- const result = normalizeAskUserResult(questions, await handler(questions, options));
649
+ const result = normalizeAskUserResult(
650
+ questions,
651
+ await handler(questions, {
652
+ ...options,
653
+ decisionPresentation: classifyDecisionPresentation(decisionFactsForAnswer(exposure)),
654
+ }),
655
+ );
636
656
  const answeredAt = new Date().toISOString();
637
657
  policy.callCount += 1;
638
658
  policy.askedQuestionKeys.add(key);
@@ -168,7 +168,7 @@ function policyIsRecipeBound(policy: { requests: ReadonlyArray<{ source: string
168
168
  // load it (only the operator activates), so its move is suggest-and-wait.
169
169
  const NO_PENDING_SKILL_DENIAL =
170
170
  "context: no pending skill request is active this turn; only the operator can activate a skill, so do not retry this load. " +
171
- `If a listed skill matches the task, open your reply with the line \`${SKILL_SUGGESTION_ANCHOR}\` and wait for the operator to run it; otherwise continue without skills.`;
171
+ `If a listed skill matches the task, open your reply with the line \`${SKILL_SUGGESTION_ANCHOR}\` and continue the task without it; only the operator can run it. Otherwise continue without skills.`;
172
172
 
173
173
  function pendingSkillPolicyError(name: string, options: ToolInvokeOptions | undefined): string | null {
174
174
  const policy = options?.pendingSkillPolicy;
@@ -287,7 +287,7 @@ function renderSkillsList(
287
287
  // template where they skip conditional prose in the header.
288
288
  lines.push(
289
289
  "",
290
- `If one skill above matches the current task, begin your reply with the line \`${SKILL_SUGGESTION_ANCHOR}\` (a comma-separated sequence, in order, when several compose) and wait for the operator to run it. If none match, do not mention skills.`,
290
+ `If one skill above matches the current task, begin your reply with the line \`${SKILL_SUGGESTION_ANCHOR}\` (a comma-separated sequence, in order, when several compose), then continue the task in the same turn without it; only the operator can run it. If none match, do not mention skills.`,
291
291
  );
292
292
  return lines.join("\n");
293
293
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { AgentAutomationAuthority } from "../domains/agents/spec.js";
4
4
  import { type AgentTaskType, classifyAgentTask } from "../domains/dispatch/agent-candidates.js";
5
+ import { cloneDispatchBudgetRequest } from "../domains/dispatch/budget-envelope.js";
5
6
  import type { DispatchRequest } from "../domains/dispatch/contract.js";
6
7
  import { type AgentRoleFactsResolver, requestExecutionRole } from "../domains/dispatch/execution-role.js";
7
8
  import { parseRoutingIntent } from "../domains/dispatch/routing-intent.js";
@@ -171,6 +172,13 @@ function dispatchRequestFromArgs(
171
172
  }
172
173
  request.thinkingLevel = thinkingLevel as JobThinkingLevel;
173
174
  }
175
+ if (args.budget !== undefined) {
176
+ try {
177
+ request.budget = cloneDispatchBudgetRequest(args.budget);
178
+ } catch (error) {
179
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
180
+ }
181
+ }
174
182
  return { ok: true, request };
175
183
  }
176
184
 
@@ -1,6 +1,25 @@
1
+ import { responseModelIdObservationFromRecord } from "../core/response-model-id.js";
1
2
  import { durableAssistantTextFromEvent } from "../domains/dispatch/event-pump.js";
3
+ import type { RunReceipt } from "../domains/dispatch/types.js";
2
4
 
3
5
  /** Return the durable final assistant text carried by a worker event. */
4
6
  export function assistantTextFromEvent(event: unknown): string {
5
7
  return durableAssistantTextFromEvent(event).trim();
6
8
  }
9
+
10
+ /** Human projection of the response model-id observations carried by a receipt. */
11
+ export function receiptResponseModelIdObservationLabel(receipt: Pick<RunReceipt, "upstreamResponses">): string | null {
12
+ const responses = receipt.upstreamResponses ?? [];
13
+ if (responses.length === 0) return null;
14
+ const labels = responses.map((response) => {
15
+ const observation = responseModelIdObservationFromRecord(
16
+ response as unknown as Record<string, unknown>,
17
+ "legacy-difference-only",
18
+ );
19
+ if (observation.state === "reported") return `reported:${observation.reportedModelId}`;
20
+ if (observation.state === "not-reported") return "not-reported";
21
+ if (observation.state === "not-observed") return "not-observed";
22
+ return `legacy-difference-only:${observation.differingModelId ?? "none"}`;
23
+ });
24
+ return `response_model_id_observation=${labels.join(",")}`;
25
+ }
@@ -25,6 +25,27 @@ export type { DispatchToolDeps } from "./dispatch-types.js";
25
25
 
26
26
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
27
27
 
28
+ const DispatchBudgetPhaseSchema = Type.Object(
29
+ {
30
+ toolCalls: Type.Integer({ minimum: 1 }),
31
+ readReserve: Type.Integer({ minimum: 0 }),
32
+ },
33
+ { additionalProperties: false },
34
+ );
35
+
36
+ const DispatchBudgetSchema = Type.Object(
37
+ {
38
+ toolCalls: Type.Integer({ minimum: 1, description: "Requested tool-call phase boundary." }),
39
+ readReserve: Type.Integer({ minimum: 0, description: "Requested tail reserve for canonical read calls." }),
40
+ retryRevision: Type.Optional(DispatchBudgetPhaseSchema),
41
+ },
42
+ {
43
+ additionalProperties: false,
44
+ description:
45
+ "Invocation budget inside the recipe policy. retryRevision preauthorizes one ceiling for retry, result-contract revision, or review revision phases.",
46
+ },
47
+ );
48
+
28
49
  /**
29
50
  * Stable, lightweight dispatch surface. Admission remains synchronous so the
30
51
  * policy decision and provisional reservation are bound to the exact argument
@@ -62,7 +83,7 @@ export function createDispatchTool(
62
83
  return {
63
84
  name: ToolNames.Dispatch,
64
85
  description:
65
- 'Dispatch one bounded task with task, or a batch with tasks (never both). Singular example: {agent:"debugger", task:"Verify the receipt boundary", briefing:"Prior receipt evidence...", detach:true}. task is the worker assignment; briefing is separate bounded parent context/data and cannot replace task. Ordinary calls auto-wait; detach:true returns ids for monitoring/steering, and collect is the authoritative terminal batch operation before final synthesis. Batch modes are parallel (default), sequential, pipeline, or compete. Task objects may include persona and tool_profile. Sealed receipts are durable evidence; report receipt integrity, evidence verification, briefing provenance, and project-context provenance separately. Call with list:true to see agents. Do not repeat an identical successful dispatch in the same user turn. Prefer this tool over inline exploration whenever work is read-only fan-out, parallel investigation, or the operator asked for a worker by name; if you cannot dispatch, say so plainly and name the reason, and never narrate or summarize a worker you did not actually dispatch.',
86
+ 'Dispatch one bounded task with task, or a batch with tasks (never both). Singular example: {agent:"debugger", task:"Verify the receipt boundary", briefing:"Prior receipt evidence...", detach:true}. task is the worker assignment; briefing is separate bounded parent context/data and cannot replace task. Ordinary calls auto-wait; detach:true returns ids for monitoring/steering, and collect is the authoritative terminal batch operation before final synthesis. Batch modes are parallel (default), sequential, pipeline, or compete. Task objects may include persona, tool_profile, and a recipe-admitted budget envelope. Sealed receipts are durable evidence; report receipt integrity, evidence verification, briefing provenance, and project-context provenance separately. Call with list:true to see agents. Do not repeat an identical successful dispatch in the same user turn. Prefer this tool over inline exploration whenever work is read-only fan-out, parallel investigation, or the operator asked for a worker by name; if you cannot dispatch, say so plainly and name the reason, and never narrate or summarize a worker you did not actually dispatch.',
66
87
  parameters: Type.Object({
67
88
  list: Type.Optional(Type.Boolean({ description: "List available agents instead of dispatching." })),
68
89
  from_scout: Type.Optional(
@@ -105,6 +126,7 @@ export function createDispatchTool(
105
126
  tool_profile: Type.Optional(
106
127
  StringEnum(TOOL_PROFILE_NAMES, { description: "Narrow this worker's available tools." }),
107
128
  ),
129
+ budget: Type.Optional(DispatchBudgetSchema),
108
130
  target: Type.Optional(Type.String()),
109
131
  model: Type.Optional(Type.String()),
110
132
  node: Type.Optional(Type.String({ description: "Fleet node pin: local or a fleet.nodes id." })),
@@ -190,6 +212,7 @@ export function createDispatchTool(
190
212
  Type.String({ description: "Default ad-hoc specialist persona for dispatched tasks, max 8000 chars." }),
191
213
  ),
192
214
  tool_profile: Type.Optional(StringEnum(TOOL_PROFILE_NAMES, { description: "Default worker tool profile." })),
215
+ budget: Type.Optional(DispatchBudgetSchema),
193
216
  target: Type.Optional(Type.String({ description: "Default configured target id (omit for fleet default)." })),
194
217
  model: Type.Optional(Type.String({ description: "Default model override." })),
195
218
  node: Type.Optional(
@@ -2,6 +2,12 @@ import { readFileSync } from "node:fs";
2
2
  import { sleep } from "../core/timers.js";
3
3
  import { renderAgentLedgerBoard } from "../domains/dispatch/agent-ledger-store.js";
4
4
  import type { DurableAssignmentRecord } from "../domains/dispatch/assignment-store.js";
5
+ import {
6
+ formatBudgetPolicy,
7
+ formatBudgetReasons,
8
+ formatBudgetRequest,
9
+ formatEffectiveBudget,
10
+ } from "../domains/dispatch/budget-envelope.js";
5
11
  import type { DispatchContract } from "../domains/dispatch/contract.js";
6
12
  import { UNVERIFIABLE_RECEIPT_VERIFICATION } from "../domains/dispatch/receipt-findings.js";
7
13
  import type { ReceiptIntegrityResult } from "../domains/dispatch/receipt-integrity.js";
@@ -117,6 +123,14 @@ function runStatus(deps: MonitorToolDeps, runId: string): ToolResult {
117
123
  `started=${run.startedAt} ended=${run.endedAt ?? "n/a"} exit=${run.exitCode ?? "n/a"}`,
118
124
  `tokens=${run.tokenCount} cost=${formatCostAggregate(costAggregateForAmount(run.costUsd, run.costProvenance)) ?? COST_NOT_MEASURED} receipt=${run.receiptPath ?? "n/a"}`,
119
125
  ];
126
+ if (run.budget !== undefined) {
127
+ lines.push(
128
+ `recipe policy: ${formatBudgetPolicy(run.budget)}`,
129
+ `requested envelope: ${formatBudgetRequest(run.budget)}`,
130
+ `effective envelope: ${formatEffectiveBudget(run.budget)}`,
131
+ `clamp or escalation reason: ${formatBudgetReasons(run.budget)}`,
132
+ );
133
+ }
120
134
  if (live) {
121
135
  lines.push(
122
136
  `live: phase=${live.outcomePhase} heartbeat=${live.heartbeat} elapsed=${Math.round(live.elapsedMs / 1000)}s tokens=${live.tokens.total}`,
@@ -143,6 +157,7 @@ function runStatus(deps: MonitorToolDeps, runId: string): ToolResult {
143
157
  tokenCount: run.tokenCount,
144
158
  costUsd: run.costUsd,
145
159
  costProvenance: run.costProvenance ?? "unknown",
160
+ budget: run.budget ?? null,
146
161
  receiptPath: run.receiptPath,
147
162
  running: live !== null,
148
163
  },
@@ -22,13 +22,14 @@ import {
22
22
  mapAutonomy,
23
23
  } from "../domains/safety/autonomy.js";
24
24
  import type { SafetyContract, SafetyDecision } from "../domains/safety/contract.js";
25
+ import type { DecisionPresentation } from "../domains/safety/decision-presentation.js";
25
26
  import { hashToolCall } from "../domains/safety/loop-detector.js";
26
27
  import { detectValidationCommand } from "../domains/safety/protected-artifacts.js";
27
28
  import { askUserExposure } from "./ask-user.js";
28
29
  import { type DispatchPlanView, describeDispatchPlan } from "./dispatch-plan.js";
29
30
  import type { ToolPresentationPolicy } from "./presentation.js";
30
- import type { ToolResultDisposition } from "./result-disposition.js";
31
- import { DEFAULT_TOOL_RESULT_MAX_BYTES, shapeToolResult } from "./result-shaping.js";
31
+ import type { ToolResultDigest, ToolResultDisposition } from "./result-disposition.js";
32
+ import { DEFAULT_TOOL_RESULT_MAX_BYTES, shapeToolResult, toolResultDigestFor } from "./result-shaping.js";
32
33
 
33
34
  /**
34
35
  * Tool registry. Admission point for every tool call. Delegates classification
@@ -200,6 +201,8 @@ export interface ToolInvokeOptions {
200
201
  correlationId?: string;
201
202
  pendingSkillPolicy?: PendingSkillToolPolicy;
202
203
  askUserPolicy?: AskUserToolPolicy;
204
+ /** Host-derived display copy for an ask_user round. It has no admission authority. */
205
+ decisionPresentation?: DecisionPresentation;
203
206
  /** Registry-authenticated one-shot operator approval for this execution. */
204
207
  approval?: { requestId: string; requestedBy: string; actionClass: ActionClass };
205
208
  /**
@@ -259,6 +262,8 @@ export interface AskUserToolPolicy {
259
262
  sessionId?: string;
260
263
  turnId?: string;
261
264
  transcriptPath?: string;
265
+ /** Monotonic exposure fact for live presentation and durable replay. */
266
+ exposure?: AutonomyExposure;
262
267
  summary?: string;
263
268
  rounds: AskUserTranscriptRound[];
264
269
  decisions: AskUserTranscriptDecision[];
@@ -421,13 +426,15 @@ export function createRegistry(deps: RegistryDeps): ToolRegistry {
421
426
  const preparedArgs = prepareToolArgs(spec, call.args ?? {});
422
427
  resultDisposition = resolveToolResultDisposition(spec, preparedArgs);
423
428
  const result = await spec.run(preparedArgs, options);
424
- const afterEffects = runToolHook("after_tool", spec, call, decision, options, result);
429
+ const digest = toolResultDigestFor(spec, result, resultDisposition);
430
+ const afterEffects = runToolHook("after_tool", spec, call, decision, options, result, digest);
425
431
  const finalResult = shapeToolResult(spec, applyToolResultEffects(result, afterEffects), options, resultDisposition);
426
432
  return { kind: "ok", result: finalResult, decision };
427
433
  } catch (err) {
428
434
  const message = err instanceof Error ? err.message : String(err);
429
435
  const result: ToolResult = { kind: "error", message };
430
- const afterEffects = runToolHook("after_tool", spec, call, decision, options, result);
436
+ const digest = toolResultDigestFor(spec, result, resultDisposition);
437
+ const afterEffects = runToolHook("after_tool", spec, call, decision, options, result, digest);
431
438
  return {
432
439
  kind: "ok",
433
440
  result: shapeToolResult(spec, applyToolResultEffects(result, afterEffects), options, resultDisposition),
@@ -446,9 +453,10 @@ export function createRegistry(deps: RegistryDeps): ToolRegistry {
446
453
  decision: SafetyDecision,
447
454
  options: ToolInvokeOptions | undefined,
448
455
  result?: ToolResult,
456
+ resultDigest?: ToolResultDigest,
449
457
  ): ReadonlyArray<MiddlewareEffect> => {
450
458
  if (!deps.middleware) return [];
451
- const input = buildToolHookInput(hook, spec, call, decision, "operating", options, result);
459
+ const input = buildToolHookInput(hook, spec, call, decision, "operating", options, result, resultDigest);
452
460
  const effects = deps.middleware.runHook(input).effects;
453
461
  try {
454
462
  deps.onMiddlewareEffects?.(effects, input);
@@ -1135,6 +1143,7 @@ function buildToolHookInput(
1135
1143
  posture: string,
1136
1144
  options: ToolInvokeOptions | undefined,
1137
1145
  result: ToolResult | undefined,
1146
+ resultDigest: ToolResultDigest | undefined,
1138
1147
  ): MiddlewareHookInput {
1139
1148
  const metadata: Record<string, MiddlewareMetadataValue> = {
1140
1149
  posture,
@@ -1171,6 +1180,7 @@ function buildToolHookInput(
1171
1180
  };
1172
1181
  if (call.args !== undefined) input.toolArgs = call.args;
1173
1182
  if (result?.details !== undefined) input.toolResultDetails = result.details;
1183
+ if (resultDigest !== undefined) input.toolResultDigest = resultDigest;
1174
1184
  if (options?.runId !== undefined) input.runId = options.runId;
1175
1185
  if (options?.sessionId !== undefined) input.sessionId = options.sessionId;
1176
1186
  if (options?.turnId !== undefined) input.turnId = options.turnId;
@@ -52,6 +52,23 @@ export interface ToolResultSummaryProvenance {
52
52
  redactions?: number;
53
53
  }
54
54
 
55
+ export const TOOL_RESULT_DIGEST_MAX_BYTES = 240;
56
+
57
+ export interface ToolResultDigestProvenance {
58
+ producer: "code";
59
+ source: "canonical-result-disposition" | "legacy-fallback";
60
+ algorithm: "redacted-context-digest-v1" | "redacted-legacy-digest-v1";
61
+ contextMode?: AppliedToolResultContextMode;
62
+ summaryAlgorithm?: ToolResultSummaryProvenance["algorithm"];
63
+ redactions?: number;
64
+ }
65
+
66
+ /** A bounded diagnostic that is safe to pass to task memory. */
67
+ export interface ToolResultDigest {
68
+ text: string;
69
+ provenance: ToolResultDigestProvenance;
70
+ }
71
+
55
72
  export interface ToolResultDispositionMetadata {
56
73
  version: 1;
57
74
  applications: 1;
@@ -223,6 +240,14 @@ function capUtf8(text: string, maxBytes: number): string {
223
240
  return byteLength(text) <= maxBytes ? text : utf8Prefix(text, maxBytes);
224
241
  }
225
242
 
243
+ function capDigestUtf8(text: string, maxBytes: number): string {
244
+ if (byteLength(text) <= maxBytes) return text;
245
+ const marker = "…";
246
+ const markerBytes = byteLength(marker);
247
+ if (markerBytes >= maxBytes) return utf8Prefix(text, maxBytes);
248
+ return `${utf8Prefix(text, maxBytes - markerBytes).trimEnd()}${marker}`;
249
+ }
250
+
226
251
  /** A deterministic, UTF-8-safe excerpt whose total bytes never exceed maxBytes. */
227
252
  function boundedToolResultExcerpt(text: string, maxBytes: number, bias: ToolResultExcerptBias = "head-tail"): string {
228
253
  if (maxBytes <= 0) return "";
@@ -504,6 +529,137 @@ export function projectToolResultContext(input: ProjectToolResultContextInput):
504
529
  };
505
530
  }
506
531
 
532
+ const DIGEST_FACT_KEYS = [
533
+ "outcome",
534
+ "exitCode",
535
+ "timedOut",
536
+ "aborted",
537
+ "outputCapped",
538
+ "signal",
539
+ "status",
540
+ "error",
541
+ "decision",
542
+ "next",
543
+ ] as const;
544
+
545
+ function digestFactText(details: ToolResultDetails | undefined): string {
546
+ if (details === undefined) return "";
547
+ const facts: string[] = [];
548
+ for (const key of DIGEST_FACT_KEYS) {
549
+ if (details[key] !== undefined) facts.push(`${key}=${stableJson(details[key])}`);
550
+ }
551
+ return facts.join(" ");
552
+ }
553
+
554
+ function projectionBody(input: ProjectToolResultContextInput, projection: ToolResultContextProjection): string {
555
+ if (projection.appliedMode === "metadata-only") return "";
556
+ if (projection.appliedMode === "full") {
557
+ const trailer = fullTrailer(input);
558
+ return trailer.length > 0 && projection.text.endsWith(`\n\n${trailer}`)
559
+ ? projection.text.slice(0, -(trailer.length + 2))
560
+ : projection.text;
561
+ }
562
+ const header = contextHeader(input, projection.appliedMode, projection.truncated, projection.summaryProvenance);
563
+ const prefix = `${header}\n`;
564
+ return projection.text.startsWith(prefix) ? projection.text.slice(prefix.length) : "";
565
+ }
566
+
567
+ function digestText(
568
+ status: string,
569
+ facts: string,
570
+ body: string,
571
+ redactions: number,
572
+ maxBytes: number,
573
+ ): { text: string; redactions: number } {
574
+ const tally = createRedactionTally();
575
+ const safeStatus = redactSecretsText(status, tally);
576
+ const safeFacts = redactSecretsText(facts, tally);
577
+ const statusAndFacts = [safeStatus, safeFacts].filter((value) => value.length > 0).join(" ");
578
+ const bodyBudget = Math.max(0, maxBytes - byteLength(statusAndFacts) - 2);
579
+ const selected = deterministicDiagnosticSummary(body, bodyBudget, true);
580
+ const candidate = [statusAndFacts, selected.text.replace(/\s+/gu, " ").trim()]
581
+ .filter((value) => value.length > 0)
582
+ .join("; ");
583
+ return {
584
+ text: capDigestUtf8(candidate, maxBytes),
585
+ redactions: redactions + tally.count + selected.redactions,
586
+ };
587
+ }
588
+
589
+ /**
590
+ * Reuse the applied model-context projection as task memory's diagnostic
591
+ * source. Metadata-only never contributes captured content, and bounded or
592
+ * summarized modes contribute only the body admitted by that projection.
593
+ */
594
+ export function deterministicToolResultDigest(
595
+ input: ProjectToolResultContextInput,
596
+ maxBytes = TOOL_RESULT_DIGEST_MAX_BYTES,
597
+ ): ToolResultDigest {
598
+ const projection = projectToolResultContext(input);
599
+ const status = [
600
+ `kind=${input.kind}`,
601
+ `mode=${projection.appliedMode}`,
602
+ `truncated=${String(projection.truncated)}`,
603
+ ...(projection.appliedMode === "metadata-only" || projection.truncated
604
+ ? [`capturedBytes=${input.capturedBytes}`]
605
+ : []),
606
+ ].join(" ");
607
+ const digest = digestText(
608
+ status,
609
+ digestFactText(input.details),
610
+ projectionBody(input, projection),
611
+ projection.summaryProvenance?.redactions ?? 0,
612
+ Math.max(1, Math.floor(maxBytes)),
613
+ );
614
+ return {
615
+ text: digest.text,
616
+ provenance: {
617
+ producer: "code",
618
+ source: "canonical-result-disposition",
619
+ algorithm: "redacted-context-digest-v1",
620
+ contextMode: projection.appliedMode,
621
+ ...(projection.summaryProvenance === undefined ? {} : { summaryAlgorithm: projection.summaryProvenance.algorithm }),
622
+ ...(digest.redactions > 0 ? { redactions: digest.redactions } : {}),
623
+ },
624
+ };
625
+ }
626
+
627
+ /** Deterministic compatibility path for results without a canonical disposition. */
628
+ export function legacyToolResultDigest(
629
+ text: string,
630
+ options: { outcome: "ok" | "error"; maxBytes?: number },
631
+ ): ToolResultDigest {
632
+ const maxBytes = Math.max(1, Math.floor(options.maxBytes ?? TOOL_RESULT_DIGEST_MAX_BYTES));
633
+ const fallback = options.outcome === "error" && text.trim().length === 0 ? "an unknown tool error" : text;
634
+ const digest = digestText("", "", fallback, 0, maxBytes);
635
+ return {
636
+ text: digest.text,
637
+ provenance: {
638
+ producer: "code",
639
+ source: "legacy-fallback",
640
+ algorithm: "redacted-legacy-digest-v1",
641
+ ...(digest.redactions > 0 ? { redactions: digest.redactions } : {}),
642
+ },
643
+ };
644
+ }
645
+
646
+ /** Reapply the memory boundary's redaction and byte cap to a supplied digest. */
647
+ export function sanitizeToolResultDigest(
648
+ digest: ToolResultDigest,
649
+ maxBytes = TOOL_RESULT_DIGEST_MAX_BYTES,
650
+ ): ToolResultDigest {
651
+ const tally = createRedactionTally();
652
+ const safe = redactSecretsText(digest.text, tally).replace(/\s+/gu, " ").trim();
653
+ const redactions = (digest.provenance.redactions ?? 0) + tally.count;
654
+ return {
655
+ text: capDigestUtf8(safe, Math.max(1, Math.floor(maxBytes))),
656
+ provenance: {
657
+ ...digest.provenance,
658
+ ...(redactions > 0 ? { redactions } : {}),
659
+ },
660
+ };
661
+ }
662
+
507
663
  /** The text inserted into the provider conversation for a registry result. */
508
664
  export function toolResultContextText(result: ToolResult): string {
509
665
  if (result.modelContext !== undefined) return result.modelContext;
@@ -1,11 +1,14 @@
1
1
  import { createHash } from "node:crypto";
2
- import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync, readdirSync, rmdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { clioStateDir } from "../core/xdg.js";
5
5
  import type { ToolInvokeOptions, ToolResult, ToolResultDetails, ToolSpec } from "./registry.js";
6
6
  import {
7
+ deterministicToolResultDigest,
8
+ legacyToolResultDigest,
7
9
  normalizeToolResultDisposition,
8
10
  projectToolResultContext,
11
+ type ToolResultDigest,
9
12
  type ToolResultDisposition,
10
13
  type ToolResultDispositionFallback,
11
14
  type ToolResultDispositionMetadata,
@@ -21,6 +24,7 @@ export const DEFAULT_TOOL_RESULT_MAX_BYTES = DEFAULT_MAX_BYTES + 2 * 1024;
21
24
  const RESULT_TRUNCATION_MARKER = "\n[tool result truncated]";
22
25
  const RESULT_OFFLOAD_MAX_BYTES = 10 * 1024 * 1024;
23
26
  const TAIL_NOTICE_RESERVE_BYTES = 512;
27
+ export const TOOL_OFFLOAD_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
24
28
 
25
29
  type ToolResultShapeContext = Pick<ToolInvokeOptions, "sessionId" | "toolCallId">;
26
30
 
@@ -49,6 +53,34 @@ function offloadMaxBytesFor(spec: ToolSpec): number {
49
53
  : RESULT_OFFLOAD_MAX_BYTES;
50
54
  }
51
55
 
56
+ /**
57
+ * Build the task-memory diagnostic from the same normalized disposition that
58
+ * will shape model context. Legacy tools use the explicit compatibility path.
59
+ */
60
+ export function toolResultDigestFor(
61
+ spec: ToolSpec,
62
+ result: ToolResult,
63
+ requestedDisposition?: ToolResultDisposition,
64
+ ): ToolResultDigest {
65
+ const text = resultText(result);
66
+ const disposition = normalizeToolResultDisposition(spec, maxBytesFor(spec), requestedDisposition);
67
+ if (disposition === null) {
68
+ const projected = shapeLegacyToolResult(spec, result);
69
+ return legacyToolResultDigest(resultText(projected), { outcome: result.kind });
70
+ }
71
+ const capturedBytes = capturedBytesFor(result, text);
72
+ return deterministicToolResultDigest({
73
+ text,
74
+ kind: result.kind,
75
+ details: result.details,
76
+ disposition,
77
+ capturedBytes,
78
+ displayedBytes: Math.min(capturedBytes, disposition.presentation.maxBytes),
79
+ offloadPath: existingOffloadPath(result.details),
80
+ followUpHint: followUpHint(spec),
81
+ });
82
+ }
83
+
52
84
  function detailsRecord(details: ToolResultDetails | undefined, key: string): Record<string, unknown> | null {
53
85
  const candidate = details?.[key];
54
86
  return candidate !== null && typeof candidate === "object" && !Array.isArray(candidate)
@@ -91,6 +123,32 @@ function offloadBody(text: string, bytes: number, maxBytes: number): string {
91
123
  return `${truncateUtf8(text, Math.max(0, prefixBudget), "")}${notice}`;
92
124
  }
93
125
 
126
+ /** Remove regular offload files older than the boot retention cap. */
127
+ export function sweepExpiredToolOffloads(now = Date.now()): number {
128
+ const root = join(clioStateDir(), "scratch");
129
+ const cutoff = now - TOOL_OFFLOAD_MAX_AGE_MS;
130
+ let removed = 0;
131
+ try {
132
+ for (const session of readdirSync(root, { withFileTypes: true })) {
133
+ if (!session.isDirectory()) continue;
134
+ const sessionDir = join(root, session.name);
135
+ for (const entry of readdirSync(sessionDir, { withFileTypes: true })) {
136
+ if (!entry.isFile()) continue;
137
+ const path = join(sessionDir, entry.name);
138
+ try {
139
+ if (statSync(path).mtimeMs >= cutoff) continue;
140
+ rmSync(path, { force: true });
141
+ removed += 1;
142
+ } catch {}
143
+ }
144
+ try {
145
+ if (readdirSync(sessionDir).length === 0) rmdirSync(sessionDir);
146
+ } catch {}
147
+ }
148
+ } catch {}
149
+ return removed;
150
+ }
151
+
94
152
  /**
95
153
  * Persist the full text of a tool result to a per-session scratch file and
96
154
  * return its path (or null if the write fails). Self-shaping callers use this