@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
@@ -2,9 +2,13 @@ import { isAbsolute, resolve } from "node:path";
2
2
  import {
3
3
  MEMORY_SCOPES,
4
4
  MEMORY_VERSION,
5
+ type MemoryAgentIdentity,
6
+ type MemoryPromotionRedaction,
5
7
  type MemoryRecord,
8
+ type MemoryRecordProvenance,
6
9
  type MemoryRecordValidationResult,
7
10
  type MemoryRepositoryIdentity,
11
+ type MemoryRuntimeIdentity,
8
12
  type MemoryScope,
9
13
  type MemoryStoreValidationResult,
10
14
  type MemoryValidationIssue,
@@ -65,6 +69,9 @@ function readMemoryRecord(value: unknown, path: string, issues: MemoryValidation
65
69
  "approved",
66
70
  "rejectedAt",
67
71
  "repository",
72
+ "runtime",
73
+ "agent",
74
+ "provenance",
68
75
  ],
69
76
  issues,
70
77
  );
@@ -93,6 +100,9 @@ function readMemoryRecord(value: unknown, path: string, issues: MemoryValidation
93
100
  const approved = readBoolean(value, `${path}.approved`, issues);
94
101
  const rejectedAt = readOptionalIsoString(value, `${path}.rejectedAt`, issues);
95
102
  const repository = readOptionalMemoryRepositoryIdentity(value, `${path}.repository`, issues);
103
+ const runtime = readOptionalNamedIdentity(value, `${path}.runtime`, "runtime", issues);
104
+ const agent = readOptionalNamedIdentity(value, `${path}.agent`, "agent", issues);
105
+ const provenance = readOptionalMemoryProvenance(value, `${path}.provenance`, issues);
96
106
  if (approved === true && rejectedAt !== undefined) {
97
107
  issues.push({ path: `${path}.rejectedAt`, message: "approved records must not be rejected" });
98
108
  }
@@ -102,6 +112,18 @@ function readMemoryRecord(value: unknown, path: string, issues: MemoryValidation
102
112
  if (repository === undefined && scope === "repo") {
103
113
  issues.push({ path: `${path}.repository`, message: "repo scope requires repository applicability" });
104
114
  }
115
+ if (runtime !== undefined && scope !== null && scope !== "runtime") {
116
+ issues.push({ path: `${path}.runtime`, message: "runtime applicability requires runtime scope" });
117
+ }
118
+ if (runtime === undefined && scope === "runtime") {
119
+ issues.push({ path: `${path}.runtime`, message: "runtime scope requires runtime applicability" });
120
+ }
121
+ if (agent !== undefined && scope !== null && scope !== "agent") {
122
+ issues.push({ path: `${path}.agent`, message: "agent applicability requires agent scope" });
123
+ }
124
+ if (agent === undefined && scope === "agent") {
125
+ issues.push({ path: `${path}.agent`, message: "agent scope requires agent applicability" });
126
+ }
105
127
  if (
106
128
  id === null ||
107
129
  scope === null ||
@@ -132,6 +154,9 @@ function readMemoryRecord(value: unknown, path: string, issues: MemoryValidation
132
154
  if (regressions !== undefined) record.regressions = regressions;
133
155
  if (rejectedAt !== undefined) record.rejectedAt = rejectedAt;
134
156
  if (repository !== undefined) record.repository = repository;
157
+ if (runtime !== undefined) record.runtime = runtime;
158
+ if (agent !== undefined) record.agent = agent;
159
+ if (provenance !== undefined) record.provenance = provenance;
135
160
  return record;
136
161
  }
137
162
 
@@ -160,10 +185,163 @@ function readOptionalMemoryRepositoryIdentity(
160
185
  return { kind, key };
161
186
  }
162
187
 
188
+ function readOptionalNamedIdentity<T extends "runtime" | "agent">(
189
+ record: Record<string, unknown>,
190
+ path: string,
191
+ kind: T,
192
+ issues: MemoryValidationIssue[],
193
+ ): (T extends "runtime" ? MemoryRuntimeIdentity : MemoryAgentIdentity) | undefined {
194
+ const field = fieldName(path);
195
+ if (!Object.hasOwn(record, field)) return undefined;
196
+ const value = record[field];
197
+ if (!isRecord(value)) {
198
+ issues.push({ path, message: "expected object" });
199
+ return undefined;
200
+ }
201
+ rejectUnexpectedFields(value, path, ["kind", "key"], issues);
202
+ const actualKind = readString(value, `${path}.kind`, issues);
203
+ if (actualKind !== null && actualKind !== kind) {
204
+ issues.push({ path: `${path}.kind`, message: `expected ${kind}` });
205
+ }
206
+ const key = readString(value, `${path}.key`, issues);
207
+ if (key !== null && !isValidNamedIdentityKey(key)) {
208
+ issues.push({ path: `${path}.key`, message: `expected valid ${kind} identity` });
209
+ }
210
+ if (actualKind !== kind || key === null || !isValidNamedIdentityKey(key)) return undefined;
211
+ return { kind, key } as T extends "runtime" ? MemoryRuntimeIdentity : MemoryAgentIdentity;
212
+ }
213
+
214
+ function readOptionalMemoryProvenance(
215
+ record: Record<string, unknown>,
216
+ path: string,
217
+ issues: MemoryValidationIssue[],
218
+ ): MemoryRecordProvenance | undefined {
219
+ const field = fieldName(path);
220
+ if (!Object.hasOwn(record, field)) return undefined;
221
+ const value = record[field];
222
+ if (!isRecord(value)) {
223
+ issues.push({ path, message: "expected object" });
224
+ return undefined;
225
+ }
226
+ rejectUnexpectedFields(
227
+ value,
228
+ path,
229
+ [
230
+ "sourceKind",
231
+ "evidenceId",
232
+ "sourceSessionId",
233
+ "sourceEntryId",
234
+ "sourceEntryKind",
235
+ "sourceEntryCreatedAt",
236
+ "sourceEntryLastTouchedAt",
237
+ "redaction",
238
+ ],
239
+ issues,
240
+ );
241
+ const sourceKind = readString(value, `${path}.sourceKind`, issues);
242
+ if (sourceKind !== null && !["evidence", "task-bank-entry", "handoff-snapshot"].includes(sourceKind)) {
243
+ issues.push({ path: `${path}.sourceKind`, message: "expected evidence, task-bank-entry, or handoff-snapshot" });
244
+ }
245
+ const evidenceId = readOptionalSourceString(value, `${path}.evidenceId`, issues);
246
+ const sourceSessionId = readOptionalSourceString(value, `${path}.sourceSessionId`, issues);
247
+ const sourceEntryId = readOptionalSourceString(value, `${path}.sourceEntryId`, issues);
248
+ const sourceEntryKind = readOptionalSourceString(value, `${path}.sourceEntryKind`, issues);
249
+ if (sourceEntryKind !== undefined && sourceEntryKind !== "knowledge" && sourceEntryKind !== "procedural") {
250
+ issues.push({ path: `${path}.sourceEntryKind`, message: "expected knowledge or procedural" });
251
+ }
252
+ const sourceEntryCreatedAt = readOptionalIsoString(value, `${path}.sourceEntryCreatedAt`, issues);
253
+ const sourceEntryLastTouchedAt = readOptionalIsoString(value, `${path}.sourceEntryLastTouchedAt`, issues);
254
+ const redaction = readOptionalPromotionRedaction(value, `${path}.redaction`, issues);
255
+ if (sourceKind === "evidence") {
256
+ if (evidenceId === undefined)
257
+ issues.push({ path: `${path}.evidenceId`, message: "evidence source requires evidence id" });
258
+ if (sourceEntryId !== undefined || sourceEntryKind !== undefined) {
259
+ issues.push({ path, message: "evidence source must not carry task-memory entry fields" });
260
+ }
261
+ }
262
+ if (sourceKind === "task-bank-entry" || sourceKind === "handoff-snapshot") {
263
+ if (sourceSessionId === undefined) {
264
+ issues.push({ path: `${path}.sourceSessionId`, message: "promotion source requires session id" });
265
+ }
266
+ if (sourceEntryId === undefined) {
267
+ issues.push({ path: `${path}.sourceEntryId`, message: "promotion source requires entry id" });
268
+ }
269
+ if (sourceEntryKind !== "knowledge" && sourceEntryKind !== "procedural") {
270
+ issues.push({ path: `${path}.sourceEntryKind`, message: "promotion source requires entry kind" });
271
+ }
272
+ if (redaction === undefined) {
273
+ issues.push({ path: `${path}.redaction`, message: "promotion source requires redaction provenance" });
274
+ }
275
+ }
276
+ if (sourceKind === null || !["evidence", "task-bank-entry", "handoff-snapshot"].includes(sourceKind)) {
277
+ return undefined;
278
+ }
279
+ const provenance: MemoryRecordProvenance = { sourceKind: sourceKind as MemoryRecordProvenance["sourceKind"] };
280
+ if (evidenceId !== undefined) provenance.evidenceId = evidenceId;
281
+ if (sourceSessionId !== undefined) provenance.sourceSessionId = sourceSessionId;
282
+ if (sourceEntryId !== undefined) provenance.sourceEntryId = sourceEntryId;
283
+ if (sourceEntryKind === "knowledge" || sourceEntryKind === "procedural") {
284
+ provenance.sourceEntryKind = sourceEntryKind;
285
+ }
286
+ if (sourceEntryCreatedAt !== undefined) provenance.sourceEntryCreatedAt = sourceEntryCreatedAt;
287
+ if (sourceEntryLastTouchedAt !== undefined) provenance.sourceEntryLastTouchedAt = sourceEntryLastTouchedAt;
288
+ if (redaction !== undefined) provenance.redaction = redaction;
289
+ return provenance;
290
+ }
291
+
292
+ function readOptionalPromotionRedaction(
293
+ record: Record<string, unknown>,
294
+ path: string,
295
+ issues: MemoryValidationIssue[],
296
+ ): MemoryPromotionRedaction | undefined {
297
+ const field = fieldName(path);
298
+ if (!Object.hasOwn(record, field)) return undefined;
299
+ const value = record[field];
300
+ if (!isRecord(value)) {
301
+ issues.push({ path, message: "expected object" });
302
+ return undefined;
303
+ }
304
+ rejectUnexpectedFields(value, path, ["appliedBeforePersistence", "replacementCount", "sourceFields"], issues);
305
+ const appliedBeforePersistence = readBoolean(value, `${path}.appliedBeforePersistence`, issues);
306
+ if (appliedBeforePersistence !== null && appliedBeforePersistence !== true) {
307
+ issues.push({ path: `${path}.appliedBeforePersistence`, message: "expected true" });
308
+ }
309
+ const replacementCount = readNumber(value, `${path}.replacementCount`, issues);
310
+ if (replacementCount !== null && (!Number.isInteger(replacementCount) || replacementCount < 0)) {
311
+ issues.push({ path: `${path}.replacementCount`, message: "expected non-negative integer" });
312
+ }
313
+ const sourceFields = readStringArray(value, `${path}.sourceFields`, issues);
314
+ if (appliedBeforePersistence !== true || replacementCount === null || sourceFields === null) return undefined;
315
+ return { appliedBeforePersistence, replacementCount, sourceFields };
316
+ }
317
+
318
+ function readOptionalSourceString(
319
+ record: Record<string, unknown>,
320
+ path: string,
321
+ issues: MemoryValidationIssue[],
322
+ ): string | undefined {
323
+ const field = fieldName(path);
324
+ if (!Object.hasOwn(record, field)) return undefined;
325
+ const value = record[field];
326
+ if (typeof value !== "string" || !isValidSourceValue(value)) {
327
+ issues.push({ path, message: "expected valid non-empty source value" });
328
+ return undefined;
329
+ }
330
+ return value;
331
+ }
332
+
163
333
  function isNormalizedAbsoluteRepositoryPath(value: string): boolean {
164
334
  return !/[\0\r\n]/u.test(value) && isAbsolute(value) && resolve(value) === value;
165
335
  }
166
336
 
337
+ function isValidNamedIdentityKey(value: string): boolean {
338
+ return value.length > 0 && value.length <= 256 && value.trim() === value && !/[\0\r\n\t ]/u.test(value);
339
+ }
340
+
341
+ function isValidSourceValue(value: string): boolean {
342
+ return value.length > 0 && value.length <= 1_024 && value.trim() === value && !/[\0\r\n]/u.test(value);
343
+ }
344
+
167
345
  function rejectUnexpectedFields(
168
346
  record: Record<string, unknown>,
169
347
  path: string,
@@ -1,4 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
+ import {
3
+ legacyToolResultDigest,
4
+ sanitizeToolResultDigest,
5
+ type ToolResultDigest,
6
+ } from "../../tools/result-disposition.js";
2
7
  import { TASK_MEMORY_DEFAULT_PROCEDURAL_CAP, type TaskMemoryBank } from "../memory/task-bank.js";
3
8
  import {
4
9
  runTaskMemoryPolicy,
@@ -40,7 +45,6 @@ export interface MemoryInterventionSettings {
40
45
  timeoutMs: number;
41
46
  }
42
47
 
43
- const RESULT_DIGEST_MAX_CHARS = 240;
44
48
  const CALL_DESCRIPTION_MAX_CHARS = 180;
45
49
  const NO_EFFECTS: ReadonlyArray<MiddlewareEffect> = [];
46
50
 
@@ -48,7 +52,7 @@ type ToolOutcome = "ok" | "error";
48
52
 
49
53
  interface PendingToolStep {
50
54
  toolName: string;
51
- fingerprint: string;
55
+ operationFingerprint: string;
52
56
  callDescription: string;
53
57
  }
54
58
 
@@ -145,7 +149,7 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
145
149
  let toolStep = 0;
146
150
  let lastTurnEndStep = 0;
147
151
  let reactivateAfterCompaction = false;
148
- let lastInjectedFingerprint: string | null = null;
152
+ let lastInjectedOperationFingerprint: string | null = null;
149
153
  let currentTask = "(current task unavailable)";
150
154
  let toolsSinceMemoryStep = 0;
151
155
  let consecutiveErrors = 0;
@@ -467,13 +471,13 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
467
471
  function observeBeforeTool(input: MiddlewareHookInput): void {
468
472
  const prepared = prepareToolStep(input);
469
473
  if (prepared === null) return;
470
- setBounded(pending, pendingKey(input, prepared.fingerprint), prepared, settings().windowSteps);
474
+ setBounded(pending, pendingKey(input, prepared.operationFingerprint), prepared, settings().windowSteps);
471
475
  }
472
476
 
473
477
  function observeAfterTool(input: MiddlewareHookInput): ReadonlyArray<MiddlewareEffect> {
474
478
  const fallback = prepareToolStep(input);
475
479
  if (fallback === null) return NO_EFFECTS;
476
- const key = pendingKey(input, fallback.fingerprint);
480
+ const key = pendingKey(input, fallback.operationFingerprint);
477
481
  const prepared = pending.get(key) ?? fallback;
478
482
  pending.delete(key);
479
483
  const outcome = input.metadata?.resultKind === "error" ? "error" : "ok";
@@ -487,11 +491,13 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
487
491
  } else {
488
492
  consecutiveErrors = 0;
489
493
  }
494
+ const digest = resultDigest(input, outcome);
490
495
  const step: TrajectoryStep = {
491
496
  ...prepared,
492
497
  step: toolStep,
493
498
  outcome,
494
- resultDigest: resultDigest(input, outcome),
499
+ resultDigest: digest.text,
500
+ resultDigestProvenance: digest.provenance,
495
501
  };
496
502
  trajectory.push(step);
497
503
  if (trajectory.length > live.windowSteps) trajectory.splice(0, trajectory.length - live.windowSteps);
@@ -507,20 +513,20 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
507
513
  * itself, which the model reads on its very next round.
508
514
  */
509
515
  function annotateRepeatedFailure(step: TrajectoryStep): ReadonlyArray<MiddlewareEffect> {
510
- if (annotatedThisTurn.has(step.fingerprint)) return NO_EFFECTS;
516
+ if (annotatedThisTurn.has(step.operationFingerprint)) return NO_EFFECTS;
511
517
  const occurrences = trajectory.filter(
512
- (candidate) => candidate.outcome === "error" && candidate.fingerprint === step.fingerprint,
518
+ (candidate) => candidate.outcome === "error" && candidate.operationFingerprint === step.operationFingerprint,
513
519
  ).length;
514
- const failure = failures.get(step.fingerprint);
520
+ const failure = failures.get(step.operationFingerprint);
515
521
  if (occurrences < 2 || failure === undefined) return NO_EFFECTS;
516
522
  const message = boundedReminder(
517
523
  `Memory: [${failure.entryId}] you already tried ${failure.callDescription} at step ${failure.firstStep} and it failed with ${failure.errorDigest}. Change the approach rather than repeating it.`,
518
524
  settings().maxTokens,
519
525
  );
520
526
  if (message.length === 0) return NO_EFFECTS;
521
- annotatedThisTurn.add(step.fingerprint);
527
+ annotatedThisTurn.add(step.operationFingerprint);
522
528
  // Claim the turn-end channel too, so one repeated failure is surfaced once.
523
- lastInjectedFingerprint = step.fingerprint;
529
+ lastInjectedOperationFingerprint = step.operationFingerprint;
524
530
  lastDecision = "injected";
525
531
  rulesInjectedSincePromptedStep = true;
526
532
  annotatedSinceTurnEnd = true;
@@ -530,7 +536,7 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
530
536
  }
531
537
 
532
538
  function rememberFailure(step: TrajectoryStep): void {
533
- const previous = failures.get(step.fingerprint);
539
+ const previous = failures.get(step.operationFingerprint);
534
540
  const attempts = (previous?.attempts ?? 0) + 1;
535
541
  const firstStep = previous?.firstStep ?? step.step;
536
542
  const content = proceduralContent(step.callDescription, attempts, firstStep, step.resultDigest);
@@ -546,7 +552,7 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
546
552
  }
547
553
  setBounded(
548
554
  failures,
549
- step.fingerprint,
555
+ step.operationFingerprint,
550
556
  {
551
557
  entryId,
552
558
  attempts,
@@ -566,21 +572,21 @@ export function createMemoryInterventionRegistration(deps: MemoryInterventionDep
566
572
  step === undefined ||
567
573
  step.outcome !== "error" ||
568
574
  step.step <= lastTurnEndStep ||
569
- step.fingerprint === lastInjectedFingerprint
575
+ step.operationFingerprint === lastInjectedOperationFingerprint
570
576
  ) {
571
577
  continue;
572
578
  }
573
579
  const occurrences = trajectory.filter(
574
- (candidate) => candidate.outcome === "error" && candidate.fingerprint === step.fingerprint,
580
+ (candidate) => candidate.outcome === "error" && candidate.operationFingerprint === step.operationFingerprint,
575
581
  ).length;
576
- const failure = failures.get(step.fingerprint);
582
+ const failure = failures.get(step.operationFingerprint);
577
583
  if (occurrences < 2 || failure === undefined) continue;
578
584
  const message = boundedReminder(
579
585
  `Memory: [${failure.entryId}] you already tried ${failure.callDescription} at step ${failure.firstStep} and it failed with ${failure.errorDigest}.`,
580
586
  settings().maxTokens,
581
587
  );
582
588
  if (message.length === 0) return NO_EFFECTS;
583
- lastInjectedFingerprint = step.fingerprint;
589
+ lastInjectedOperationFingerprint = step.operationFingerprint;
584
590
  lastInjectedMessage = message;
585
591
  lastDecision = "injected";
586
592
  rulesInjectedSincePromptedStep = true;
@@ -624,31 +630,35 @@ function prepareToolStep(input: MiddlewareHookInput): PendingToolStep | null {
624
630
  const canonical = hashToolCall(toolName, input.toolArgs ?? {});
625
631
  return {
626
632
  toolName,
627
- fingerprint: createHash("sha256").update(canonical).digest("hex").slice(0, 16),
633
+ operationFingerprint: createHash("sha256").update(canonical).digest("hex").slice(0, 16),
628
634
  callDescription: shortText(canonical, CALL_DESCRIPTION_MAX_CHARS),
629
635
  };
630
636
  }
631
637
 
632
- function pendingKey(input: MiddlewareHookInput, fingerprint: string): string {
633
- return input.toolCallId ?? `anonymous:${fingerprint}`;
638
+ function pendingKey(input: MiddlewareHookInput, operationFingerprint: string): string {
639
+ return input.toolCallId ?? `anonymous:${operationFingerprint}`;
634
640
  }
635
641
 
636
- function resultDigest(input: MiddlewareHookInput, outcome: ToolOutcome): string {
642
+ function resultDigest(input: MiddlewareHookInput, outcome: ToolOutcome): ToolResultDigest {
643
+ if (input.toolResultDigest !== undefined) return sanitizeToolResultDigest(input.toolResultDigest);
637
644
  if (outcome === "error") {
638
645
  const metadataMessage = input.metadata?.errorMessage;
639
646
  if (typeof metadataMessage === "string" && metadataMessage.trim().length > 0) {
640
- return shortText(diagnosticLine(metadataMessage), RESULT_DIGEST_MAX_CHARS);
647
+ return legacyToolResultDigest(diagnosticLine(metadataMessage), { outcome });
641
648
  }
642
649
  for (const key of ["error", "message"] as const) {
643
650
  const value = input.toolResultDetails?.[key];
644
651
  if (typeof value === "string" && value.trim().length > 0) {
645
- return shortText(diagnosticLine(value), RESULT_DIGEST_MAX_CHARS);
652
+ return legacyToolResultDigest(diagnosticLine(value), { outcome });
646
653
  }
647
654
  }
648
- return "an unknown tool error";
655
+ return legacyToolResultDigest("", { outcome });
649
656
  }
650
657
  const resultFingerprint = input.metadata?.resultFingerprint;
651
- return typeof resultFingerprint === "string" ? `ok result ${resultFingerprint.slice(0, 16)}` : "ok";
658
+ return legacyToolResultDigest(
659
+ typeof resultFingerprint === "string" ? `ok result ${resultFingerprint.slice(0, 16)}` : "ok",
660
+ { outcome },
661
+ );
652
662
  }
653
663
 
654
664
  const DIAGNOSTIC_HINT =
@@ -328,6 +328,12 @@ function cloneHookInput(input: MiddlewareHookInput): MiddlewareHookInput {
328
328
  if (input.metadata !== undefined) cloned.metadata = { ...input.metadata };
329
329
  if (input.toolArgs !== undefined) cloned.toolArgs = { ...input.toolArgs };
330
330
  if (input.toolResultDetails !== undefined) cloned.toolResultDetails = { ...input.toolResultDetails };
331
+ if (input.toolResultDigest !== undefined) {
332
+ cloned.toolResultDigest = {
333
+ text: input.toolResultDigest.text,
334
+ provenance: { ...input.toolResultDigest.provenance },
335
+ };
336
+ }
331
337
  if (input.text !== undefined) cloned.text = input.text.slice(0, MIDDLEWARE_HOOK_TEXT_MAX_CHARS);
332
338
  return cloned;
333
339
  }
@@ -1,5 +1,6 @@
1
1
  import { SKILL_SUGGESTION_ANCHOR } from "../../core/skill-activation.js";
2
2
  import type { MiddlewareHookRegistration } from "./runtime.js";
3
+ import { isSkillSuggestionWait, SKILL_SUGGESTION_WAIT_CONTINUATION_MESSAGE } from "./stalled-turn.js";
3
4
  import { type MiddlewareEffect, metadataNumber } from "./types.js";
4
5
 
5
6
  /**
@@ -32,12 +33,17 @@ export function skillsReminderMessage(installed: number, installable = 0): strin
32
33
  installable > 0
33
34
  ? `${installed} installed, ${installable} installable from the marketplace`
34
35
  : `${installed} installed`;
36
+ // Inline and non-blocking (issue #184): a 27B model read "and wait for the
37
+ // operator" as the whole job, spent a 40-second turn deliberating over the
38
+ // suggestion, and ended without one repository read. The suggestion is one
39
+ // line, the task continues in the same turn, and only the operator loads.
35
40
  return (
36
41
  `[Skills] ${counts}. Start this task by listing them with context(scope="skills") ` +
37
42
  "and checking for a match; if one matches, open your reply with the line " +
38
43
  `\`${SKILL_SUGGESTION_ANCHOR}\` (a comma-separated sequence, in order, when several compose) ` +
39
- "and wait for the operator; load only on operator request, and a marketplace skill is offered for " +
40
- "install when the operator runs it. If none match, do not mention skills and continue with the task."
44
+ "and then continue the task in the same turn without the skill. Only the operator loads a skill, and a " +
45
+ "marketplace skill is offered for install when the operator runs it. If none match, do not mention skills " +
46
+ "and continue with the task."
41
47
  );
42
48
  }
43
49
 
@@ -135,9 +141,18 @@ export function createSkillsReminderRegistration(deps: SkillsReminderDeps): Midd
135
141
 
136
142
  return {
137
143
  id: SKILLS_REMINDER_REGISTRATION_ID,
138
- description: "once per session, on the first substantive turn, teaches the skill-suggestion reply protocol",
139
- hooks: ["turn_start"],
144
+ description:
145
+ "once per session, on the first substantive turn, teaches the skill-suggestion reply protocol; keeps a turn going that stopped on the suggestion",
146
+ hooks: ["turn_start", "turn_end"],
140
147
  evaluate(input): ReadonlyArray<MiddlewareEffect> {
148
+ // The registration that teaches "suggest, then continue" owns the
149
+ // consequence when a model suggests and stops. The generic stalled-turn
150
+ // rule cannot see this case: the listing call counts as a tool call.
151
+ if (input.hook === "turn_end") {
152
+ return isSkillSuggestionWait(input)
153
+ ? [{ kind: "request_continuation", message: SKILL_SUGGESTION_WAIT_CONTINUATION_MESSAGE }]
154
+ : NO_EFFECTS;
155
+ }
141
156
  const sessionId = input.sessionId ?? null;
142
157
  if (lastSeenSessionId === undefined) {
143
158
  lastSeenSessionId = sessionId;
@@ -1,11 +1,16 @@
1
+ import { SKILL_SUGGESTION_PREFIX } from "../../core/skill-activation.js";
2
+ import { ToolNames } from "../../core/tool-names.js";
1
3
  import type { MiddlewareRuleDefinition } from "./runtime.js";
2
- import type { MiddlewareHookInput } from "./types.js";
4
+ import { type MiddlewareHookInput, metadataNumber, metadataString } from "./types.js";
3
5
 
4
6
  export const STALLED_TURN_REGISTRATION_ID = "nudge.stalled-turn";
5
7
 
6
8
  export const STALLED_TURN_REQUEST_CONTINUATION_MESSAGE =
7
9
  "You ended your turn after announcing an action without executing it: no tools were called. Continue now and perform the announced action, or state plainly that you are finished and waiting for the user.";
8
10
 
11
+ export const SKILL_SUGGESTION_WAIT_CONTINUATION_MESSAGE =
12
+ "You ended your turn on a skill suggestion without doing the task. The suggestion line has already made the offer and only the operator loads a skill, so nothing is needed from them before you proceed. Continue the task now without the skill.";
13
+
9
14
  const INTENT_PATTERN = /\b(let me|i['’]ll|i will|i am going to|i['’]m going to|now i|next i|let's|let us)\b/i;
10
15
 
11
16
  // Bare stems only. Inflected forms are derived below so that "running",
@@ -133,6 +138,43 @@ export const STALLED_TURN_RULE_DEFINITION: MiddlewareRuleDefinition = {
133
138
  predicate: shouldRequestStalledTurnContinuation,
134
139
  };
135
140
 
141
+ // "run /skill X or say skip", "Suggested skill: /skill x", "want me to use
142
+ // the skill?": the sentence hands the operator a decision the protocol does
143
+ // not require of them.
144
+ const SKILL_WAIT_PATTERN = /\/skill\b|\bskills?\b|\bskip\b/i;
145
+
146
+ /**
147
+ * A turn that made the skill suggestion and then stopped, with only the
148
+ * listing call behind it (issue #184). The generic stalled-turn predicate
149
+ * cannot see this: `context(scope="skills")` is a tool call, so the turn
150
+ * looks busy, and "say skip and I'll explore the tree" reads as a
151
+ * conditional offer rather than an announcement. The suggestion is supposed
152
+ * to be one inline line before the work, so a reply that is the suggestion
153
+ * plus a wait has not done the task.
154
+ */
155
+ export function isSkillSuggestionWait(input: MiddlewareHookInput): boolean {
156
+ if (input.hook !== "turn_end") return false;
157
+ if (!isNormalStopReason(input.metadata?.stopReason)) return false;
158
+ const text = input.text ?? "";
159
+ if (!text.includes(SKILL_SUGGESTION_PREFIX)) return false;
160
+ const toolNames = metadataString(input, "turnToolNames");
161
+ if (toolNames === null) {
162
+ if ((metadataNumber(input, "turnToolCalls") ?? 0) !== 0) return false;
163
+ } else if (
164
+ toolNames
165
+ .split(",")
166
+ .filter((name) => name.length > 0)
167
+ .some((name) => name !== ToolNames.Context)
168
+ ) {
169
+ return false;
170
+ }
171
+ const lastLine = lastNonEmptyLine(text);
172
+ if (lastLine === null) return false;
173
+ const finalSentence = lastSentence(lastLine);
174
+ if (finalSentence.length === 0) return false;
175
+ return SKILL_WAIT_PATTERN.test(finalSentence) || isQuestion(finalSentence) || LET_ME_KNOW_PATTERN.test(finalSentence);
176
+ }
177
+
136
178
  export function shouldRequestStalledTurnContinuation(input: MiddlewareHookInput): boolean {
137
179
  if (input.hook !== "turn_end") return false;
138
180
  if (input.metadata?.turnToolCalls !== 0) return false;
@@ -1,3 +1,5 @@
1
+ import type { ToolResultDigest } from "../../tools/result-disposition.js";
2
+
1
3
  /**
2
4
  * The five lifecycle events of the unified middleware layer (w3 cutover).
3
5
  * Every event has a live producer: the registry fires before_tool/after_tool
@@ -86,6 +88,8 @@ export interface MiddlewareHookInput {
86
88
  toolArgs?: Readonly<Record<string, unknown>>;
87
89
  /** Structured result details, supplied by the registry on after_tool. Read-only by contract. */
88
90
  toolResultDetails?: Readonly<Record<string, unknown>>;
91
+ /** Bounded deterministic diagnostic supplied by the result-disposition layer on after_tool. */
92
+ toolResultDigest?: Readonly<ToolResultDigest>;
89
93
  /**
90
94
  * Free text attached to the hook occurrence; on turn_end this is the final
91
95
  * assistant message text. The one typed exception to the scalar `metadata`
@@ -114,6 +118,12 @@ export function metadataNumber(input: MiddlewareHookInput, key: string): number
114
118
  return typeof value === "number" && Number.isFinite(value) ? value : null;
115
119
  }
116
120
 
121
+ /** The string under `key`, or null when the caller did not report one. */
122
+ export function metadataString(input: MiddlewareHookInput, key: string): string | null {
123
+ const value = input.metadata?.[key];
124
+ return typeof value === "string" ? value : null;
125
+ }
126
+
117
127
  export function isMiddlewareHook(value: string): value is MiddlewareHook {
118
128
  return (MIDDLEWARE_HOOKS as ReadonlyArray<string>).includes(value);
119
129
  }
@@ -1,3 +1,4 @@
1
+ import type { ResponseModelIdObservationCounts } from "../../core/response-model-id.js";
1
2
  import type { TargetStatus } from "../providers/contract.js";
2
3
  import type { CostProvenance } from "../providers/index.js";
3
4
  import type { AccountabilitySummary } from "./accountability.js";
@@ -132,11 +133,15 @@ export interface ObservabilityContract {
132
133
  */
133
134
  recordTokens(
134
135
  providerId: string,
135
- modelId: string,
136
+ attributedModelId: string,
136
137
  tokens: number,
137
138
  costUsd?: number,
138
139
  breakdown?: Partial<UsageBreakdown>,
139
140
  costProvenance?: CostProvenance,
141
+ modelIdFacts?: {
142
+ requestedModelIds: ReadonlyArray<string>;
143
+ responseModelIdObservationCounts: Readonly<ResponseModelIdObservationCounts>;
144
+ },
140
145
  ): void;
141
146
  /** Record final output token throughput for one completed assistant stream. */
142
147
  recordTokenThroughput(snapshot: TokenThroughputSnapshot): void;
@@ -11,6 +11,7 @@
11
11
  * footer consumes the session sum through `ObservabilityContract.sessionTokens()`.
12
12
  */
13
13
 
14
+ import type { ResponseModelIdObservationCounts } from "../../core/response-model-id.js";
14
15
  import { type CostProvenance, normalizeCostProvenance } from "../providers/index.js";
15
16
 
16
17
  export interface CostAmount {
@@ -112,7 +113,9 @@ export interface UsageBreakdown {
112
113
 
113
114
  export interface CostEntry {
114
115
  providerId: string;
115
- modelId: string;
116
+ attributedModelId: string;
117
+ requestedModelIds: string[];
118
+ responseModelIdObservationCounts: ResponseModelIdObservationCounts;
116
119
  tokens: number;
117
120
  usd: number;
118
121
  provenance: CostProvenance;
@@ -127,11 +130,15 @@ export interface CostEntry {
127
130
  export interface CostTracker {
128
131
  accumulate(
129
132
  providerId: string,
130
- modelId: string,
133
+ attributedModelId: string,
131
134
  tokens: number,
132
135
  usd?: number,
133
136
  breakdown?: Partial<UsageBreakdown>,
134
137
  provenance?: CostProvenance,
138
+ modelIdFacts?: {
139
+ requestedModelIds: ReadonlyArray<string>;
140
+ responseModelIdObservationCounts: Readonly<ResponseModelIdObservationCounts>;
141
+ },
135
142
  ): number;
136
143
  sessionTotal(): number;
137
144
  sessionCost(): CostAggregate;
@@ -149,7 +156,7 @@ export function createCostTracker(): CostTracker {
149
156
  let total = 0;
150
157
  const totals = emptyBreakdown();
151
158
  return {
152
- accumulate(providerId, modelId, tokens, usd, breakdown, costProvenance) {
159
+ accumulate(providerId, attributedModelId, tokens, usd, breakdown, costProvenance, modelIdFacts) {
153
160
  const resolvedUsd = usd ?? 0;
154
161
  const provenance = normalizeCostProvenance(costProvenance);
155
162
  const input = breakdown?.input ?? 0;
@@ -160,7 +167,16 @@ export function createCostTracker(): CostTracker {
160
167
  const apiCalls = breakdown?.apiCalls;
161
168
  log.push({
162
169
  providerId,
163
- modelId,
170
+ attributedModelId,
171
+ requestedModelIds: [...(modelIdFacts?.requestedModelIds ?? [attributedModelId])],
172
+ responseModelIdObservationCounts: modelIdFacts
173
+ ? { ...modelIdFacts.responseModelIdObservationCounts }
174
+ : {
175
+ reportedCalls: 0,
176
+ notReportedCalls: 0,
177
+ notObservedCalls: apiCalls ?? 1,
178
+ legacyDifferenceOnlyCalls: 0,
179
+ },
164
180
  tokens,
165
181
  usd: resolvedUsd,
166
182
  provenance,
@@ -259,9 +259,9 @@ export function createObservabilityBundle(
259
259
  latestThroughput = null;
260
260
  projection.refresh();
261
261
  },
262
- recordTokens(providerId, modelId, tokens, costUsd, breakdown, costProvenance) {
262
+ recordTokens(providerId, attributedModelId, tokens, costUsd, breakdown, costProvenance, modelIdFacts) {
263
263
  telemetry.record("counter", "tokens.total", tokens);
264
- cost.accumulate(providerId, modelId, tokens, costUsd, breakdown, costProvenance);
264
+ cost.accumulate(providerId, attributedModelId, tokens, costUsd, breakdown, costProvenance, modelIdFacts);
265
265
  projection.refresh();
266
266
  },
267
267
  recordSessionTurn(sessionTurn) {