@iowarp/clio-coder 0.3.3 → 0.3.4

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 (244) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +3 -3
  4. package/dist/{acp-P2AQILE2.js → acp-S5R4RR5B.js} +7 -6
  5. package/dist/{agents-72W3BI7I.js → agents-P6DMMVZY.js} +24 -21
  6. package/dist/assets/codewiki.json +1 -1
  7. package/dist/{auth-5TWEIYDN.js → auth-2XCZLPKS.js} +12 -8
  8. package/dist/{chunk-GGXXDWE4.js → chunk-22NAGB7X.js} +2 -2
  9. package/dist/{chunk-2DJ2KNFG.js → chunk-2LZI5CAG.js} +133 -13
  10. package/dist/{chunk-OAO4GE4M.js → chunk-2TZWSW76.js} +2 -2
  11. package/dist/{chunk-OOJYHWRB.js → chunk-34475P3I.js} +2 -2
  12. package/dist/{chunk-4XUGQOHA.js → chunk-35MKKU5R.js} +4 -4
  13. package/dist/{chunk-LM5TQCJZ.js → chunk-3HZ5RWN2.js} +5 -5
  14. package/dist/{chunk-AGYYIBLL.js → chunk-3JLKSKD7.js} +2 -2
  15. package/dist/{chunk-KZWTDYJF.js → chunk-4JUF2NNX.js} +7 -7
  16. package/dist/{chunk-ZDOOVTXZ.js → chunk-4OC57DA6.js} +27 -4
  17. package/dist/chunk-5M54SPOL.js +926 -0
  18. package/dist/{chunk-STBPMHSX.js → chunk-7RXG6QRZ.js} +51 -11
  19. package/dist/{chunk-X6IAEBZR.js → chunk-A2GZF7DC.js} +5 -5
  20. package/dist/{chunk-A3CYT5EX.js → chunk-AD2SYQYC.js} +55 -2
  21. package/dist/chunk-AOCYTWAV.js +449 -0
  22. package/dist/chunk-BEY543CS.js +258 -0
  23. package/dist/{chunk-6N5PTWMY.js → chunk-BP4OYD6A.js} +32 -13
  24. package/dist/chunk-BPGS2WCQ.js +612 -0
  25. package/dist/{chunk-V6RTAOC2.js → chunk-BRXQQJFP.js} +8 -8
  26. package/dist/chunk-CFGTUFWB.js +67 -0
  27. package/dist/{chunk-CBCAPZAA.js → chunk-E25LMLRW.js} +2 -2
  28. package/dist/{chunk-DUYJ5IO6.js → chunk-EDRHSCIE.js} +4 -4
  29. package/dist/{chunk-XBXAASKX.js → chunk-EFADSJET.js} +2 -2
  30. package/dist/{chunk-M6SHUN7Q.js → chunk-FO5ZOVUY.js} +2 -2
  31. package/dist/chunk-FYYLNIL5.js +313 -0
  32. package/dist/{chunk-FNTMWMX5.js → chunk-HV5X7OR2.js} +14 -12
  33. package/dist/{chunk-TZTZS7QK.js → chunk-HXG4IURW.js} +5 -3
  34. package/dist/{chunk-5UFT4SUX.js → chunk-K6WL7QZT.js} +3 -3
  35. package/dist/chunk-K7VKOLQQ.js +15 -0
  36. package/dist/{chunk-BMEMKKIT.js → chunk-KOHPCX4K.js} +2 -2
  37. package/dist/{chunk-OC7FIQPC.js → chunk-KRPY7NTG.js} +10 -7
  38. package/dist/chunk-LL4KHSZI.js +22 -0
  39. package/dist/{chunk-DSELYM6W.js → chunk-MEQ45TQ4.js} +15 -9
  40. package/dist/{chunk-5UUP6MWO.js → chunk-MV3K5QF2.js} +5 -436
  41. package/dist/{chunk-6SGHMWE3.js → chunk-N4CZJQRK.js} +5 -5
  42. package/dist/{chunk-TZK7PACC.js → chunk-NILBFAPG.js} +14 -8
  43. package/dist/chunk-OZNBF4L3.js +23 -0
  44. package/dist/{verify-375KUB3Y.js → chunk-PCZJO5TI.js} +127 -42
  45. package/dist/chunk-QQK64KLB.js +1360 -0
  46. package/dist/{chunk-SRDMMSEP.js → chunk-QQL5RT5M.js} +979 -1619
  47. package/dist/{chunk-LZSJBIVT.js → chunk-QWU7ZBO7.js} +70 -720
  48. package/dist/{chunk-2TLUCQVG.js → chunk-RD5U66HV.js} +3 -3
  49. package/dist/{chunk-OKGUZO2U.js → chunk-SPULKLCF.js} +4 -3
  50. package/dist/{chunk-OQ33BKR3.js → chunk-TTNYS3EA.js} +3 -60
  51. package/dist/chunk-TW3WDMVS.js +677 -0
  52. package/dist/chunk-TZSKNMZG.js +434 -0
  53. package/dist/{chunk-7MNJORFF.js → chunk-UL3WSD3F.js} +6 -1
  54. package/dist/{chunk-PIWWS5BL.js → chunk-UZHIZC5S.js} +7 -7
  55. package/dist/{chunk-UFIIWP2H.js → chunk-VAWWTKDP.js} +8 -8
  56. package/dist/{chunk-COU2UHX6.js → chunk-VEZEGCGW.js} +170 -2
  57. package/dist/{chunk-LW6DSM3M.js → chunk-VMNQ6OZA.js} +98 -202
  58. package/dist/chunk-VSNATDE6.js +122 -0
  59. package/dist/chunk-W6GROXXM.js +69 -0
  60. package/dist/chunk-WPQLXFOZ.js +375 -0
  61. package/dist/{chunk-ORBHGJC5.js → chunk-WR67VIZY.js} +3 -3
  62. package/dist/{chunk-PAJK6MAQ.js → chunk-X6COSD2O.js} +5 -5
  63. package/dist/chunk-ZGVHUX3M.js +66 -0
  64. package/dist/{chunk-LWLEKMDQ.js → chunk-ZYKPLLNQ.js} +510 -547
  65. package/dist/cli/index.js +27 -23
  66. package/dist/{clio-JOU4FXVA.js → clio-J5JIOIDS.js} +7 -6
  67. package/dist/{code-nav-7AX6FYE6.js → code-nav-AXCXSBHX.js} +5 -3
  68. package/dist/{config-XCDVKR23.js → config-OEBMIN2U.js} +37 -27
  69. package/dist/{configure-4GAP54ZW.js → configure-PUQOSIXQ.js} +16 -13
  70. package/dist/{context-5VKGUVJJ.js → context-EKDCKUUZ.js} +82 -7
  71. package/dist/{context-4UOGGLQ5.js → context-MGSE4Z2T.js} +33 -23
  72. package/dist/{context-77FM5DV5.js → context-URSXPBCK.js} +17 -9
  73. package/dist/{context-clear-XXJRLCJJ.js → context-clear-KDAJRNUK.js} +33 -23
  74. package/dist/context-working-set-SBKMPPI2.js +1552 -0
  75. package/dist/{dispatch-runner-QPRDDBDX.js → dispatch-runner-MSWN72NK.js} +43 -29
  76. package/dist/{doctor-HR46URBJ.js → doctor-7BSE27PJ.js} +10 -10
  77. package/dist/{eval-XSSNATB4.js → eval-IZGDOO4H.js} +9 -8
  78. package/dist/{evidence-6HG2PY2B.js → evidence-SR7WXB5B.js} +51 -23
  79. package/dist/{evolve-K7YU3NCY.js → evolve-K7VE2CBX.js} +30 -20
  80. package/dist/{fleet-VY3HHKN6.js → fleet-7XMJNQNF.js} +48 -38
  81. package/dist/{fleet-preflight-DDN536IT.js → fleet-preflight-AQNAH644.js} +3 -3
  82. package/dist/{init-JYGXI3FK.js → init-JGNPAYXT.js} +41 -31
  83. package/dist/{memory-WFZMGYHX.js → memory-4ALKDJ4Q.js} +32 -22
  84. package/dist/{models-I5QWSEOM.js → models-ZMMLFJNN.js} +22 -19
  85. package/dist/{monitor-GE4ID3IA.js → monitor-2F3T5KHP.js} +55 -43
  86. package/dist/{orchestrator-EM5MC3HM.js → orchestrator-ORHT43JB.js} +572 -388
  87. package/dist/{reset-L2FQEE3E.js → reset-NXGTYNUO.js} +4 -3
  88. package/dist/{run-ZU3QMZPZ.js → run-RF4WJGMT.js} +51 -41
  89. package/dist/{share-S5BZQC5I.js → share-UT3W6E4M.js} +5 -4
  90. package/dist/{skills-X5VXCRNQ.js → skills-PSACKC5Q.js} +2 -2
  91. package/dist/{skills-eval-WKIHWTHR.js → skills-eval-WJSI55RZ.js} +34 -24
  92. package/dist/{targets-SNCPI2NR.js → targets-PIIRAOYS.js} +23 -20
  93. package/dist/{terminal-lease-BNAHVHBS.js → terminal-lease-ULWXWNVY.js} +4 -3
  94. package/dist/{upgrade-JQHHPQ4K.js → upgrade-346TZ6AV.js} +18 -17
  95. package/dist/{usage-OR4O5SMZ.js → usage-6KKXR32N.js} +34 -24
  96. package/dist/verifiers-4UUM6TEE.js +1214 -0
  97. package/dist/verify-X5HDROLA.js +25 -0
  98. package/dist/{wiki-generate-UEXP2ARI.js → wiki-generate-7STOCIFZ.js} +42 -31
  99. package/dist/worker/entry.js +33 -24
  100. package/docs/README.md +7 -6
  101. package/docs/acp.md +1 -1
  102. package/docs/alcf-provider.md +1 -1
  103. package/docs/architecture.md +2 -2
  104. package/docs/artifact-versions.md +1 -1
  105. package/docs/built-in-agents.md +1 -1
  106. package/docs/capacity-and-scheduling.md +1 -1
  107. package/docs/commands-and-modes.md +53 -21
  108. package/docs/config-knobs-audit.md +1 -2
  109. package/docs/configuration-and-targets.md +15 -1
  110. package/docs/context-engine.md +64 -12
  111. package/docs/context-working-set.md +194 -0
  112. package/docs/development-pipeline.md +1 -1
  113. package/docs/documentation-coverage.md +5 -5
  114. package/docs/documentation-guide.md +6 -5
  115. package/docs/environment-variables.md +2 -1
  116. package/docs/eval-runner.md +1 -1
  117. package/docs/evals-internal.md +14 -1
  118. package/docs/evidence-and-memory.md +74 -2
  119. package/docs/evolution.md +1 -1
  120. package/docs/exit-codes-and-output.md +1 -1
  121. package/docs/extensions-and-sharing.md +2 -2
  122. package/docs/fleet-dispatch.md +22 -7
  123. package/docs/glossary.md +21 -1
  124. package/docs/installation-and-lifecycle.md +2 -2
  125. package/docs/middleware-and-components.md +1 -1
  126. package/docs/model-catalog.md +7 -9
  127. package/docs/observability.md +4 -4
  128. package/docs/proactive-memory.md +1 -1
  129. package/docs/prompt-envelope-and-tools.md +4 -4
  130. package/docs/provider-adapter-cookbook.md +1 -1
  131. package/docs/release-cut-checklist.md +31 -31
  132. package/docs/safety-model.md +23 -4
  133. package/docs/scientific-validation.md +21 -3
  134. package/docs/session-lifecycle.md +3 -3
  135. package/docs/skills-marketplace.md +1 -1
  136. package/docs/tool-usage.md +79 -12
  137. package/docs/trace-store.md +1 -1
  138. package/docs/troubleshooting.md +1 -1
  139. package/docs/tui-design.md +1 -1
  140. package/docs/worker-dispatch-mechanics.md +11 -1
  141. package/package.json +8 -11
  142. package/skills/meta/clio-test/SKILL.md +20 -17
  143. package/skills/meta/clio-test/evals.md +3 -3
  144. package/skills/meta/clio-test/references/harness.md +35 -6
  145. package/skills/meta/clio-test/references/test-map.md +20 -10
  146. package/skills/registry.yaml +2 -2
  147. package/skills/skill-marketplace.json +1 -1
  148. package/src/cli/context-working-set.ts +513 -0
  149. package/src/cli/context.ts +8 -0
  150. package/src/cli/evidence.ts +20 -2
  151. package/src/cli/index.ts +4 -0
  152. package/src/cli/verifiers.ts +325 -0
  153. package/src/core/bash-exec.ts +39 -14
  154. package/src/core/bus-events.ts +19 -4
  155. package/src/core/config.ts +54 -0
  156. package/src/core/defaults.ts +50 -3
  157. package/src/core/verification-scripts.ts +6 -0
  158. package/src/domains/agents/builtins/verifier.md +3 -0
  159. package/src/domains/config/classify.ts +1 -0
  160. package/src/domains/context/working-set/contract.ts +161 -0
  161. package/src/domains/context/working-set/defaults.ts +28 -0
  162. package/src/domains/context/working-set/engine.ts +203 -0
  163. package/src/domains/context/working-set/fold.ts +62 -0
  164. package/src/domains/context/working-set/horizon.ts +38 -0
  165. package/src/domains/context/working-set/marker.ts +103 -0
  166. package/src/domains/context/working-set/path-index.ts +436 -0
  167. package/src/domains/context/working-set/payload.ts +152 -0
  168. package/src/domains/context/working-set/policies/age-horizon.ts +55 -0
  169. package/src/domains/context/working-set/policies/index.ts +21 -0
  170. package/src/domains/context/working-set/policies/structural.ts +160 -0
  171. package/src/domains/context/working-set/project.ts +132 -0
  172. package/src/domains/context/working-set/protect.ts +109 -0
  173. package/src/domains/context/working-set/recall.ts +177 -0
  174. package/src/domains/context/working-set/replay/controls.ts +112 -0
  175. package/src/domains/context/working-set/replay/load-clio.ts +199 -0
  176. package/src/domains/context/working-set/replay/metrics.ts +185 -0
  177. package/src/domains/context/working-set/replay/reference-graph.ts +79 -0
  178. package/src/domains/context/working-set/replay/report.ts +139 -0
  179. package/src/domains/context/working-set/replay/runner.ts +325 -0
  180. package/src/domains/context/working-set/replay/synthetic.ts +422 -0
  181. package/src/domains/context/working-set/replay/trace.ts +21 -0
  182. package/src/domains/context/working-set/visible.ts +54 -0
  183. package/src/domains/evidence/build.ts +112 -45
  184. package/src/domains/evidence/eval.ts +24 -7
  185. package/src/domains/evidence/index.ts +53 -0
  186. package/src/domains/evidence/ordering.ts +12 -0
  187. package/src/domains/evidence/run-trust.ts +221 -0
  188. package/src/domains/evidence/store.ts +46 -6
  189. package/src/domains/evidence/trust-status.ts +854 -0
  190. package/src/domains/evidence/types.ts +26 -0
  191. package/src/domains/middleware/memory-intervention.ts +3 -0
  192. package/src/domains/middleware/stalled-turn.ts +165 -4
  193. package/src/domains/safety/autonomy.ts +1 -1
  194. package/src/domains/safety/default-path-policy.ts +8 -0
  195. package/src/domains/safety/finish-contract.ts +4 -3
  196. package/src/domains/safety/policy-engine.ts +48 -6
  197. package/src/domains/session/compaction/compact.ts +23 -1
  198. package/src/domains/session/compaction/cut-point.ts +2 -0
  199. package/src/domains/session/compaction/tokens.ts +16 -1
  200. package/src/domains/session/context-ledger.ts +2 -0
  201. package/src/domains/session/entries.ts +107 -1
  202. package/src/domains/session/manager.ts +9 -2
  203. package/src/domains/session/migrations/index.ts +22 -3
  204. package/src/engine/acp/server.ts +3 -0
  205. package/src/engine/agent.ts +18 -1
  206. package/src/engine/session.ts +9 -3
  207. package/src/entry/orchestrator.ts +16 -4
  208. package/src/interactive/chat-loop-messages.ts +18 -6
  209. package/src/interactive/chat-panel.ts +17 -1
  210. package/src/interactive/chat-renderer.ts +30 -21
  211. package/src/interactive/context-meter.ts +10 -0
  212. package/src/interactive/context-overlay.ts +81 -6
  213. package/src/interactive/context-recall-command.ts +110 -0
  214. package/src/interactive/interactive-slash-runtime.ts +37 -1
  215. package/src/interactive/model-session-replay.ts +21 -0
  216. package/src/interactive/overlay-general-openers.ts +6 -0
  217. package/src/interactive/overlay-session-lifecycle.ts +8 -4
  218. package/src/interactive/renderers/tool-execution.ts +18 -2
  219. package/src/interactive/session-transcript.ts +2 -2
  220. package/src/interactive/slash-commands.ts +29 -2
  221. package/src/interactive/turn-context.ts +238 -88
  222. package/src/interactive/turn-middleware.ts +6 -6
  223. package/src/tools/agent-tools.ts +11 -4
  224. package/src/tools/bash.ts +144 -82
  225. package/src/tools/builtin-tool-catalog.ts +11 -5
  226. package/src/tools/context/index.ts +105 -3
  227. package/src/tools/context/surface.ts +3 -2
  228. package/src/tools/core-bootstrap.ts +21 -0
  229. package/src/tools/dispatch-runner.ts +9 -7
  230. package/src/tools/monitor.ts +28 -20
  231. package/src/tools/registry.ts +59 -7
  232. package/src/tools/result-disposition.ts +550 -0
  233. package/src/tools/result-shaping.ts +262 -19
  234. package/src/tools/safe-exec.ts +2 -0
  235. package/src/tools/verify/authoring.ts +1119 -0
  236. package/src/tools/verify/catalog.ts +346 -0
  237. package/src/tools/verify/index.ts +13 -3
  238. package/src/tools/verify/scripts.ts +135 -37
  239. package/src/tools/verify/surface.ts +9 -5
  240. package/src/tools/worker-evidence.ts +35 -12
  241. package/dist/chunk-J7CWMCQD.js +0 -255
  242. package/dist/chunk-T6YILFSB.js +0 -80
  243. package/dist/chunk-VAKQQHWR.js +0 -434
  244. package/dist/chunk-VPAYEGVX.js +0 -184
@@ -1,8 +1,16 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { mkdirSync, writeFileSync } from "node:fs";
2
3
  import { isAbsolute, join, relative, resolve } from "node:path";
3
4
  import { clioStateDir } from "../core/xdg.js";
4
5
  import type { ToolInvokeOptions, ToolResult, ToolResultDetails, ToolSpec } from "./registry.js";
5
- import { DEFAULT_MAX_BYTES } from "./truncate.js";
6
+ import {
7
+ normalizeToolResultDisposition,
8
+ projectToolResultContext,
9
+ type ToolResultDisposition,
10
+ type ToolResultDispositionFallback,
11
+ type ToolResultDispositionMetadata,
12
+ } from "./result-disposition.js";
13
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateTail } from "./truncate.js";
6
14
  import { byteLength, truncateUtf8 } from "./truncate-utf8.js";
7
15
 
8
16
  // Backstop for tools without an explicit resultSizePolicy. Sits above the
@@ -12,6 +20,7 @@ import { byteLength, truncateUtf8 } from "./truncate-utf8.js";
12
20
  export const DEFAULT_TOOL_RESULT_MAX_BYTES = DEFAULT_MAX_BYTES + 2 * 1024;
13
21
  const RESULT_TRUNCATION_MARKER = "\n[tool result truncated]";
14
22
  const RESULT_OFFLOAD_MAX_BYTES = 10 * 1024 * 1024;
23
+ const TAIL_NOTICE_RESERVE_BYTES = 512;
15
24
 
16
25
  type ToolResultShapeContext = Pick<ToolInvokeOptions, "sessionId" | "toolCallId">;
17
26
 
@@ -33,6 +42,13 @@ function followUpHint(spec: ToolSpec): string {
33
42
  );
34
43
  }
35
44
 
45
+ function offloadMaxBytesFor(spec: ToolSpec): number {
46
+ const configured = spec.metadata?.resultSizePolicy?.offloadMaxBytes;
47
+ return typeof configured === "number" && Number.isFinite(configured) && configured > 0
48
+ ? Math.floor(configured)
49
+ : RESULT_OFFLOAD_MAX_BYTES;
50
+ }
51
+
36
52
  function detailsRecord(details: ToolResultDetails | undefined, key: string): Record<string, unknown> | null {
37
53
  const candidate = details?.[key];
38
54
  return candidate !== null && typeof candidate === "object" && !Array.isArray(candidate)
@@ -41,10 +57,10 @@ function detailsRecord(details: ToolResultDetails | undefined, key: string): Rec
41
57
  }
42
58
 
43
59
  /**
44
- * A result that already spilled its full text to scratch (bash's tail-biased
45
- * shaping via `resultSize.offloadPath`, or an OBSERVE tool's envelope via
46
- * `observation.offloadPath`) is left alone; the tool shaped it deliberately
47
- * and the backstop would only cut the tool's own continuation notice.
60
+ * A result that already spilled its full text to scratch (the canonical
61
+ * tail-biased presentation path via `resultSize.offloadPath`, or an OBSERVE
62
+ * tool's envelope via `observation.offloadPath`) is left alone; a second pass
63
+ * would only cut the deliberate continuation notice.
48
64
  */
49
65
  function existingOffloadPath(details: ToolResultDetails | undefined): string | null {
50
66
  const fromResultSize = detailsRecord(details, "resultSize")?.offloadPath;
@@ -53,6 +69,13 @@ function existingOffloadPath(details: ToolResultDetails | undefined): string | n
53
69
  return typeof fromObservation === "string" && fromObservation.length > 0 ? fromObservation : null;
54
70
  }
55
71
 
72
+ function existingDisposition(details: ToolResultDetails | undefined): ToolResultDispositionMetadata | null {
73
+ const disposition = detailsRecord(details, "resultDisposition");
74
+ return disposition?.version === 1 && disposition.applications === 1
75
+ ? (disposition as unknown as ToolResultDispositionMetadata)
76
+ : null;
77
+ }
78
+
56
79
  function safePathSegment(value: string): string {
57
80
  const safe = value
58
81
  .trim()
@@ -61,10 +84,6 @@ function safePathSegment(value: string): string {
61
84
  return safe.length > 0 ? safe : "unnamed";
62
85
  }
63
86
 
64
- function timestampSegment(): string {
65
- return new Date().toISOString().replace(/[^A-Za-z0-9._-]/g, "_");
66
- }
67
-
68
87
  function offloadBody(text: string, bytes: number, maxBytes: number): string {
69
88
  if (bytes <= maxBytes) return text;
70
89
  const notice = `\n[clio-coder scratch output truncated at ${maxBytes} bytes; original size ${bytes} bytes]`;
@@ -74,22 +93,28 @@ function offloadBody(text: string, bytes: number, maxBytes: number): string {
74
93
 
75
94
  /**
76
95
  * Persist the full text of a tool result to a per-session scratch file and
77
- * return its path (or null if the write fails). Callers that truncate their own
78
- * display output (for example bash's tail-biased shaping) use this to spill the
79
- * complete output before truncating, then set `details.resultSize.offloadPath`
80
- * so the registry re-truncation pass leaves the already-shaped result alone.
96
+ * return its path (or null if the write fails). Self-shaping callers use this
97
+ * to spill complete output before truncating and set
98
+ * `details.resultSize.offloadPath`; canonical disposition shaping also uses it
99
+ * once when presentation or model context omits captured content.
100
+ *
101
+ * The file is named by the sha256 of the captured text, so the path is a pure
102
+ * function of the bytes: identical captures share one file and the
103
+ * `retrieve=` header, the eviction marker, and the resume transcript carry the
104
+ * same bytes every time, which is what keeps the prompt-cache prefix stable
105
+ * across runs. Pass `digest` when the caller already hashed the text.
81
106
  */
82
107
  export function writeToolOffload(
83
108
  text: string,
84
109
  context: ToolResultShapeContext | undefined,
85
110
  maxBytes: number = RESULT_OFFLOAD_MAX_BYTES,
111
+ digest: string = createHash("sha256").update(text).digest("hex"),
86
112
  ): string | null {
87
113
  try {
88
114
  const bytes = byteLength(text);
89
115
  const sessionId = safePathSegment(context?.sessionId ?? "no-session");
90
- const callId = safePathSegment(context?.toolCallId ?? timestampSegment());
91
116
  const dir = join(clioStateDir(), "scratch", sessionId);
92
- const path = join(dir, `${callId}.txt`);
117
+ const path = join(dir, `${digest}.txt`);
93
118
  mkdirSync(dir, { recursive: true });
94
119
  writeFileSync(path, offloadBody(text, bytes, maxBytes), "utf8");
95
120
  return path;
@@ -145,9 +170,10 @@ function shapeJsonOverflow(
145
170
  bytes: number,
146
171
  maxBytes: number,
147
172
  context: ToolResultShapeContext | undefined,
173
+ offloadMaxBytes: number,
148
174
  ): ToolResult {
149
175
  const observation = detailsRecord(result.details, "observation");
150
- const offloadPath = writeToolOffload(text, context);
176
+ const offloadPath = writeToolOffload(text, context, offloadMaxBytes);
151
177
  const next =
152
178
  typeof observation?.next === "string" && observation.next.length > 0 ? observation.next : followUpHint(spec);
153
179
  const stub = JSON.stringify({
@@ -170,16 +196,64 @@ function shapeJsonOverflow(
170
196
  return { ...result, message: stub, details: mergeDetails(result.details, resultSize) };
171
197
  }
172
198
 
173
- export function shapeToolResult(spec: ToolSpec, result: ToolResult, context?: ToolResultShapeContext): ToolResult {
199
+ function shapeTailOverflow(
200
+ spec: ToolSpec,
201
+ result: ToolResult,
202
+ text: string,
203
+ bytes: number,
204
+ maxBytes: number,
205
+ context: ToolResultShapeContext | undefined,
206
+ offloadMaxBytes: number,
207
+ ): ToolResult {
208
+ const truncation = truncateTail(text, {
209
+ maxLines: DEFAULT_MAX_LINES,
210
+ maxBytes: Math.max(1, maxBytes - TAIL_NOTICE_RESERVE_BYTES),
211
+ });
212
+ const offloadPath = writeToolOffload(text, context, offloadMaxBytes);
213
+ const startLine = truncation.totalLines - truncation.outputLines + 1;
214
+ const scope = truncation.lastLinePartial
215
+ ? `last ${formatSize(truncation.outputBytes)} of line ${truncation.totalLines} (line is large)`
216
+ : `lines ${startLine}-${truncation.totalLines} of ${truncation.totalLines}`;
217
+ const location =
218
+ offloadPath === null
219
+ ? ""
220
+ : ` Full output saved to ${offloadPath}; ${OFFLOAD_POINTER_NOTE}; read it with offset/limit.`;
221
+ const note = `[Output tail-truncated: showing ${scope} (${formatSize(maxBytes)} display limit).${location}]`;
222
+ const resultSize = {
223
+ bytes,
224
+ shownBytes: truncation.outputBytes,
225
+ maxBytes,
226
+ truncated: true,
227
+ policy: "tail",
228
+ followUpHint: followUpHint(spec),
229
+ ...(offloadPath === null ? {} : { offloadPath }),
230
+ };
231
+ const displayed = `${truncation.content}\n\n${note}`;
232
+ if (result.kind === "ok") return { ...result, output: displayed, details: mergeDetails(result.details, resultSize) };
233
+ return { ...result, message: displayed, details: mergeDetails(result.details, resultSize) };
234
+ }
235
+
236
+ function shapeLegacyToolResult(
237
+ spec: ToolSpec,
238
+ result: ToolResult,
239
+ context?: ToolResultShapeContext,
240
+ overflow: "head" | "tail" = "head",
241
+ ): ToolResult {
174
242
  if (existingOffloadPath(result.details) !== null) return result;
175
243
  const maxBytes = maxBytesFor(spec);
244
+ const offloadMaxBytes = offloadMaxBytesFor(spec);
176
245
  const text = result.kind === "ok" ? result.output : result.message;
177
246
  const bytes = byteLength(text);
178
247
  if (bytes <= maxBytes) return result;
179
248
  const observation = detailsRecord(result.details, "observation");
180
- if (observation?.format === "json") return shapeJsonOverflow(spec, result, text, bytes, maxBytes, context);
249
+ if (observation?.format === "json") {
250
+ return shapeJsonOverflow(spec, result, text, bytes, maxBytes, context, offloadMaxBytes);
251
+ }
252
+ if (overflow === "tail") {
253
+ return shapeTailOverflow(spec, result, text, bytes, maxBytes, context, offloadMaxBytes);
254
+ }
181
255
  const truncated = truncateUtf8(text, maxBytes, RESULT_TRUNCATION_MARKER);
182
- const offloadPath = writeToolOffload(text, context);
256
+ const offloadPath = writeToolOffload(text, context, offloadMaxBytes);
183
257
  const resultSize = {
184
258
  bytes,
185
259
  shownBytes: byteLength(truncated),
@@ -202,3 +276,172 @@ export function shapeToolResult(spec: ToolSpec, result: ToolResult, context?: To
202
276
  details: mergeDetails(result.details, resultSize),
203
277
  };
204
278
  }
279
+
280
+ function specWithDisplayCap(spec: ToolSpec, maxBytes: number): ToolSpec {
281
+ const metadata = spec.metadata;
282
+ if (metadata === undefined) return spec;
283
+ return {
284
+ ...spec,
285
+ metadata: {
286
+ ...metadata,
287
+ resultSizePolicy: { ...metadata.resultSizePolicy, maxBytes },
288
+ },
289
+ };
290
+ }
291
+
292
+ function resultText(result: ToolResult): string {
293
+ return result.kind === "ok" ? result.output : result.message;
294
+ }
295
+
296
+ /**
297
+ * The operator-facing trace of a disposition that was narrowed rather than
298
+ * requested. Without it a throwing resolver degrades every call of that tool
299
+ * to metadata-only with nothing on the transcript row to say so.
300
+ */
301
+ function withFallbackNotice(result: ToolResult, fallback: ToolResultDispositionFallback): ToolResult {
302
+ const note = `\n\n[result disposition fell back to metadata-only: ${fallback.reason} (${fallback.message}); the model received facts and retrieval only]`;
303
+ return result.kind === "ok"
304
+ ? { ...result, output: `${result.output}${note}` }
305
+ : { ...result, message: `${result.message}${note}` };
306
+ }
307
+
308
+ function numberField(record: Record<string, unknown> | null, key: string): number | null {
309
+ const value = record?.[key];
310
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
311
+ }
312
+
313
+ function capturedBytesFor(result: ToolResult, text: string): number {
314
+ const declared = numberField(detailsRecord(result.details, "resultSize"), "bytes");
315
+ return Math.max(byteLength(text), declared ?? 0);
316
+ }
317
+
318
+ function resultWasTruncated(result: ToolResult): boolean {
319
+ return detailsRecord(result.details, "resultSize")?.truncated === true;
320
+ }
321
+
322
+ function withOffloadMetadata(
323
+ result: ToolResult,
324
+ offloadPath: string,
325
+ capturedBytes: number,
326
+ displayedBytes: number,
327
+ maxBytes: number,
328
+ hint: string,
329
+ ): ToolResult {
330
+ const current = detailsRecord(result.details, "resultSize") ?? {};
331
+ const resultSize = {
332
+ bytes: numberField(current, "bytes") ?? capturedBytes,
333
+ shownBytes: numberField(current, "shownBytes") ?? displayedBytes,
334
+ maxBytes: numberField(current, "maxBytes") ?? maxBytes,
335
+ truncated: current.truncated === true,
336
+ policy: typeof current.policy === "string" ? current.policy : "disposition",
337
+ followUpHint: typeof current.followUpHint === "string" ? current.followUpHint : hint,
338
+ ...current,
339
+ offloadPath,
340
+ };
341
+ return { ...result, details: mergeDetails(result.details, resultSize) };
342
+ }
343
+
344
+ /**
345
+ * Apply legacy shaping or one declared canonical disposition. Registry calls
346
+ * this once, after middleware annotations have produced the terminal result.
347
+ */
348
+ export function shapeToolResult(
349
+ spec: ToolSpec,
350
+ result: ToolResult,
351
+ context?: ToolResultShapeContext,
352
+ requestedDisposition?: ToolResultDisposition,
353
+ ): ToolResult {
354
+ if (existingDisposition(result.details) !== null) return result;
355
+ const hardMaxBytes = maxBytesFor(spec);
356
+ const disposition = normalizeToolResultDisposition(spec, hardMaxBytes, requestedDisposition);
357
+ if (disposition === null) return shapeLegacyToolResult(spec, result, context);
358
+
359
+ const capturedText = resultText(result);
360
+ const capturedBytes = capturedBytesFor(result, capturedText);
361
+ let displayed = shapeLegacyToolResult(
362
+ specWithDisplayCap(spec, disposition.presentation.maxBytes),
363
+ result,
364
+ context,
365
+ disposition.presentation.overflow,
366
+ );
367
+ if (disposition.fallback !== undefined) displayed = withFallbackNotice(displayed, disposition.fallback);
368
+ let displayedText = resultText(displayed);
369
+ let displayedBytes = byteLength(displayedText);
370
+ let offloadPath = existingOffloadPath(displayed.details);
371
+ const projectionInput = {
372
+ text: capturedText,
373
+ kind: result.kind,
374
+ details: displayed.details,
375
+ disposition,
376
+ capturedBytes,
377
+ displayedBytes,
378
+ offloadPath,
379
+ followUpHint: followUpHint(spec),
380
+ };
381
+ // Project first, then decide on retrieval. A projection that already carries
382
+ // every captured byte (a short `summary`, a `full` inside budget) needs no
383
+ // scratch artifact and must not claim truncation; anything that genuinely
384
+ // omits content earns one offload and the honest omission facts with it.
385
+ let projection = projectToolResultContext(projectionInput);
386
+ if (offloadPath === null && projection.truncated) {
387
+ offloadPath = writeToolOffload(
388
+ capturedText,
389
+ context,
390
+ offloadMaxBytesFor(spec),
391
+ projection.summaryProvenance?.sourceSha256,
392
+ );
393
+ if (offloadPath !== null) {
394
+ displayed = withOffloadMetadata(
395
+ displayed,
396
+ offloadPath,
397
+ capturedBytes,
398
+ displayedBytes,
399
+ disposition.presentation.maxBytes,
400
+ followUpHint(spec),
401
+ );
402
+ displayedText = resultText(displayed);
403
+ displayedBytes = byteLength(displayedText);
404
+ projection = projectToolResultContext({
405
+ ...projectionInput,
406
+ details: displayed.details,
407
+ displayedBytes,
408
+ offloadPath,
409
+ });
410
+ }
411
+ }
412
+ const metadata: ToolResultDispositionMetadata = {
413
+ version: 1,
414
+ applications: 1,
415
+ presentation: { ...disposition.presentation, content: displayedText },
416
+ context: {
417
+ requestedMode: disposition.context.mode,
418
+ appliedMode: projection.appliedMode,
419
+ maxBytes: disposition.context.maxBytes,
420
+ ...(disposition.context.mode === "bounded" && disposition.context.excerpt !== undefined
421
+ ? { excerpt: disposition.context.excerpt }
422
+ : {}),
423
+ ...(disposition.context.mode === "full" && disposition.context.downgradeExcerpt !== undefined
424
+ ? { excerpt: disposition.context.downgradeExcerpt }
425
+ : {}),
426
+ ...(disposition.context.mode === "summary" && disposition.context.strategy !== undefined
427
+ ? { summaryStrategy: disposition.context.strategy }
428
+ : {}),
429
+ },
430
+ capturedBytes,
431
+ displayedBytes,
432
+ contextBytes: byteLength(projection.text),
433
+ presentationTruncated: resultWasTruncated(displayed),
434
+ contextTruncated: projection.truncated,
435
+ ...(projection.downgrade === undefined ? {} : { downgrade: projection.downgrade }),
436
+ ...(offloadPath === null ? {} : { offloadPath }),
437
+ retrieval:
438
+ offloadPath === null ? followUpHint(spec) : `read ${offloadPath} with offset and limit to retrieve omitted content`,
439
+ ...(projection.summaryProvenance === undefined ? {} : { summaryProvenance: projection.summaryProvenance }),
440
+ ...(disposition.fallback === undefined ? {} : { fallback: disposition.fallback }),
441
+ };
442
+ return {
443
+ ...displayed,
444
+ details: { ...(displayed.details ?? {}), resultDisposition: metadata },
445
+ modelContext: projection.text,
446
+ };
447
+ }
@@ -35,9 +35,11 @@ export function maxOutputArg(args: Record<string, unknown>): number {
35
35
  function resultDetails(result: SafeCommandResult): ToolResultDetails {
36
36
  return {
37
37
  command: [result.file, ...result.args].join(" "),
38
+ argv: [result.file, ...result.args],
38
39
  cwd: result.cwd,
39
40
  exitCode: result.exitCode,
40
41
  durationMs: result.durationMs,
42
+ aborted: result.aborted,
41
43
  timedOut: result.timedOut,
42
44
  outputCapped: result.outputCapped,
43
45
  };