@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
@@ -183,6 +183,7 @@ import { isGitRepository, recoverCleanupReadyCompeteGroups } from "../tools/comp
183
183
  import { createDispatchBackgroundRegistry } from "../tools/dispatch-background.js";
184
184
  import { createFileMutationObserver, createSkillActivationObserver } from "../tools/observers.js";
185
185
  import { createRegistry } from "../tools/registry.js";
186
+ import { sweepExpiredToolOffloads } from "../tools/result-shaping.js";
186
187
 
187
188
  export interface BootResult {
188
189
  exitCode: number;
@@ -726,6 +727,7 @@ export async function bootOrchestrator(options: BootOptions = {}): Promise<BootR
726
727
  termination.installSignalHandlers();
727
728
 
728
729
  ensureClioState();
730
+ sweepExpiredToolOffloads();
729
731
  timer.mark("install check");
730
732
 
731
733
  // A hard-killed coordinator cannot run domain drains. Reconcile its compete
@@ -1553,8 +1555,13 @@ export async function bootOrchestrator(options: BootOptions = {}): Promise<BootR
1553
1555
  getMemorySection: () => {
1554
1556
  try {
1555
1557
  const records = loadMemoryRecordsSync(clioDataDir());
1558
+ const settings = getCurrentSettings();
1559
+ const targetId = session?.current()?.target ?? settings.orchestrator?.target;
1560
+ const runtimeId = targetId ? providers.getTarget(targetId)?.runtime : undefined;
1556
1561
  return buildMemoryPromptSection(records, {
1562
+ scopes: ["global", "repo", "runtime"],
1557
1563
  activeRepository: canonicalMemoryRepositoryIdentity(process.cwd()),
1564
+ activeRuntime: runtimeId === undefined ? null : { kind: "runtime", key: runtimeId },
1558
1565
  }).section;
1559
1566
  } catch {
1560
1567
  return "";
@@ -1562,7 +1569,17 @@ export async function bootOrchestrator(options: BootOptions = {}): Promise<BootR
1562
1569
  },
1563
1570
  getTaskMemoryHandoffSource: () => {
1564
1571
  ensureTaskMemorySession();
1565
- return renderTaskMemoryHandoffSource(taskMemoryBank.snapshot());
1572
+ const meta = session?.current();
1573
+ if (!meta) throw new Error("task memory handoff requires an active session");
1574
+ const settings = getCurrentSettings();
1575
+ const targetId = meta.target ?? settings.orchestrator?.target;
1576
+ const runtimeId = targetId ? providers.getTarget(targetId)?.runtime : undefined;
1577
+ return renderTaskMemoryHandoffSource(taskMemoryBank.snapshot(), {
1578
+ sessionId: meta.id,
1579
+ evidenceRefs: [`session-${meta.id}`],
1580
+ runtimeIds: runtimeId === undefined ? [] : [runtimeId],
1581
+ agentIds: [],
1582
+ });
1566
1583
  },
1567
1584
  registerDeferredReminderSink: (sink) => {
1568
1585
  deferredMemoryReminderSink = sink;
@@ -7,6 +7,13 @@
7
7
 
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { settingsPath } from "../core/config.js";
10
+ import {
11
+ addResponseModelIdObservationCount,
12
+ emptyResponseModelIdObservationCounts,
13
+ type ResponseModelIdObservation,
14
+ type ResponseModelIdObservationCounts,
15
+ responseModelIdObservationFromRecord,
16
+ } from "../core/response-model-id.js";
10
17
  import type { PendingSkillRequest, PendingSkillToolPolicy, SkillDeclaredToolPolicy } from "../core/skill-activation.js";
11
18
  import { ToolNames } from "../core/tool-names.js";
12
19
  import { canonicalJson, sha256 } from "../domains/prompts/hash.js";
@@ -334,6 +341,7 @@ export function createAskUserToolPolicy(activeTools: ReadonlyArray<{ name: strin
334
341
  status: "idle",
335
342
  startedAt: now,
336
343
  updatedAt: now,
344
+ exposure: "local",
337
345
  rounds: [],
338
346
  decisions: [],
339
347
  inFlight: false,
@@ -356,6 +364,9 @@ export interface RunUsageSummary {
356
364
  apiCalls: number;
357
365
  hadReasoning: boolean;
358
366
  hadUsage: boolean;
367
+ responseModelIdObservationCounts: ResponseModelIdObservationCounts;
368
+ lastResponseModelIdObservation: ResponseModelIdObservation;
369
+ lastDifferingResponseModelId: string | null;
359
370
  }
360
371
 
361
372
  /**
@@ -380,6 +391,9 @@ export function sumRunUsage(messages: ReadonlyArray<AgentMessage>): RunUsageSumm
380
391
  apiCalls: 0,
381
392
  hadReasoning: false,
382
393
  hadUsage: false,
394
+ responseModelIdObservationCounts: emptyResponseModelIdObservationCounts(),
395
+ lastResponseModelIdObservation: { state: "not-observed" },
396
+ lastDifferingResponseModelId: null,
383
397
  };
384
398
  for (const raw of messages) {
385
399
  const message = raw as
@@ -397,8 +411,15 @@ export function sumRunUsage(messages: ReadonlyArray<AgentMessage>): RunUsageSumm
397
411
  };
398
412
  if (!message || typeof message !== "object") continue;
399
413
  if (message.role !== "assistant") continue;
414
+ const record = message as unknown as Record<string, unknown>;
400
415
  const usage = message.usage;
401
416
  if (!usage || typeof usage !== "object") continue;
417
+ summary.lastResponseModelIdObservation = responseModelIdObservationFromRecord(record, "not-observed");
418
+ addResponseModelIdObservationCount(summary.responseModelIdObservationCounts, summary.lastResponseModelIdObservation);
419
+ summary.lastDifferingResponseModelId =
420
+ typeof record.responseModel === "string" && record.responseModel.trim().length > 0
421
+ ? record.responseModel.trim()
422
+ : null;
402
423
  summary.hadUsage = true;
403
424
  summary.apiCalls += 1;
404
425
  const input = typeof usage.input === "number" ? usage.input : 0;
@@ -459,6 +480,7 @@ export function assistantSessionPayload(
459
480
  const raw = message as unknown as Record<string, unknown>;
460
481
  if (Array.isArray(raw.content)) payload.content = raw.content;
461
482
  if (thinking.length > 0) payload.thinking = thinking;
483
+ payload.responseModelIdObservation = responseModelIdObservationFromRecord(raw, "not-observed");
462
484
  for (const key of ["usage", "api", "provider", "model", "responseModel", "responseId", "diagnostics"]) {
463
485
  if (raw[key] !== undefined) payload[key] = raw[key];
464
486
  }
@@ -549,6 +549,12 @@ export function createChatLoop(deps: CreateChatLoopDeps): ChatLoop {
549
549
  emitNotice,
550
550
  toolStartTimes,
551
551
  });
552
+ // A fresh ledger and footer render before the first submit. Start the same
553
+ // live capability probe that submit and resume use so those boot surfaces
554
+ // refresh to the probed model window as soon as provider discovery lands.
555
+ // The turn runtime coalesces the first submit onto this request, then its
556
+ // target and model TTL keeps later submits from repeating it.
557
+ void turnRuntime.ensureLiveCapabilitiesForSelectedModel().catch(() => {});
552
558
 
553
559
  // --- bus subscriptions --------------------------------------------------
554
560
 
@@ -664,6 +670,7 @@ export function createChatLoop(deps: CreateChatLoopDeps): ChatLoop {
664
670
  // 1. Accept the prompt: reset per-turn accounting, freeze the tool
665
671
  // surface, fire turn_start, and assemble the submitted text.
666
672
  state.turnToolCalls = 0;
673
+ state.turnToolNames = [];
667
674
  state.turnSharedWorkerNote = isWorkerShareNote(text);
668
675
  middlewareToolChoice.reset();
669
676
  if (options.requestContinuation !== true) state.stalledTurnNudgeSpent = false;
@@ -988,6 +995,12 @@ export function createChatLoop(deps: CreateChatLoopDeps): ChatLoop {
988
995
  middlewareToolChoice.reset();
989
996
  state.lastTurnId = leafTurnId;
990
997
  context.resetForSession();
998
+ // The resumed ledger renders before the first new turn (issue #189),
999
+ // and the window it renders against should be the probed one rather
1000
+ // than the catalog's, so the live capability probe the first submit
1001
+ // would run runs now, under the same TTL; the footer refreshes on the
1002
+ // provider-health event the probe publishes.
1003
+ void turnRuntime.ensureLiveCapabilitiesForSelectedModel().catch(() => {});
991
1004
  state.replayedContextMessages = replayMessages ? [...replayMessages] : [];
992
1005
  if (state.runtime) {
993
1006
  state.runtime.agent.state.messages = [...state.replayedContextMessages];
@@ -13,6 +13,7 @@
13
13
  * `isTransparentAssistantWrapper`.
14
14
  */
15
15
 
16
+ import { existsSync } from "node:fs";
16
17
  import { ToolNames } from "../core/tool-names.js";
17
18
  import { foldWorkingSet } from "../domains/context/working-set/fold.js";
18
19
  import { compactionCut } from "../domains/context/working-set/visible.js";
@@ -44,7 +45,7 @@ import type { AgentMessage } from "../engine/types.js";
44
45
  import { toolPresentationPolicy } from "../tools/presentation.js";
45
46
  import { toolResultPresentationText } from "../tools/result-disposition.js";
46
47
  import type { ChatLoopEvent, RetryStatusPayload } from "./chat-loop.js";
47
- import { isSelfExplainingAbort } from "./chat-loop-messages.js";
48
+ import { isSelfExplainingAbort, toolResultSummary } from "./chat-loop-messages.js";
48
49
  import type { ChatPanel } from "./chat-panel.js";
49
50
  import { renderBranchSummaryEntry } from "./renderers/branch-summary.js";
50
51
  import { renderCompactionSummaryEntry } from "./renderers/compaction-summary.js";
@@ -440,6 +441,7 @@ function richMessageFromEntry(entry: MessageEntry, maxTextChars?: number): Agent
440
441
  "api",
441
442
  "provider",
442
443
  "model",
444
+ "responseModelIdObservation",
443
445
  "responseModel",
444
446
  "responseId",
445
447
  "diagnostics",
@@ -671,6 +673,19 @@ interface ReplayToolResult {
671
673
  blockReason?: string;
672
674
  }
673
675
 
676
+ /**
677
+ * Resolve a replayed offload pointer once, when the ledger row becomes a
678
+ * display event. Terminal repaints consume the recorded fact and never touch
679
+ * the filesystem.
680
+ */
681
+ function replayResultSummary(result: ReplayToolResult): Record<string, unknown> | undefined {
682
+ const summary = result.resultSummary ?? toolResultSummary(result.result);
683
+ const offloadPath =
684
+ typeof summary.offloadPath === "string" && summary.offloadPath.length > 0 ? summary.offloadPath : null;
685
+ if (offloadPath === null) return result.resultSummary;
686
+ return { ...summary, offloadFileMissing: !existsSync(offloadPath) };
687
+ }
688
+
674
689
  function extractToolResult(entry: MessageEntry): ReplayToolResult {
675
690
  const payload = entry.payload;
676
691
  const obj = payloadObject(payload);
@@ -1131,6 +1146,7 @@ export function rehydrateChatPanelFromTurns(
1131
1146
  }
1132
1147
  if (entry.role === "tool_result") {
1133
1148
  const result = extractToolResult(entry);
1149
+ const resultSummary = replayResultSummary(result);
1134
1150
  const evictedReason = workingSet.evicted.get(entry.turnId)?.reason;
1135
1151
  const fallbackId = result.id ?? pendingToolIds.pop() ?? null;
1136
1152
  if (fallbackId) {
@@ -1143,7 +1159,7 @@ export function rehydrateChatPanelFromTurns(
1143
1159
  result: displayReplayToolResult(result.result, options.unboundedToolBodies === true),
1144
1160
  isError: result.isError,
1145
1161
  ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}),
1146
- ...(result.resultSummary !== undefined ? { resultSummary: result.resultSummary } : {}),
1162
+ ...(resultSummary !== undefined ? { resultSummary } : {}),
1147
1163
  ...(result.outcome !== undefined ? { outcome: result.outcome } : {}),
1148
1164
  ...(result.blockReason !== undefined ? { blockReason: result.blockReason } : {}),
1149
1165
  ...(evictedReason !== undefined ? { evictedReason } : {}),
@@ -1157,7 +1173,7 @@ export function rehydrateChatPanelFromTurns(
1157
1173
  result: displayReplayToolResult(result.result, options.unboundedToolBodies === true),
1158
1174
  isError: result.isError,
1159
1175
  ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}),
1160
- ...(result.resultSummary !== undefined ? { resultSummary: result.resultSummary } : {}),
1176
+ ...(resultSummary !== undefined ? { resultSummary } : {}),
1161
1177
  ...(result.outcome === "blocked" ? { outcome: "blocked" as const } : {}),
1162
1178
  ...(result.blockReason !== undefined ? { blockReason: result.blockReason } : {}),
1163
1179
  ...(evictedReason !== undefined ? { evictedReason } : {}),
@@ -1,9 +1,12 @@
1
1
  import { Editor, stripTerminalSequences, type TUI, truncateToWidth, visibleWidth } from "../engine/tui.js";
2
+ import { fitHintEntries } from "./overlay-frame.js";
3
+ import { permissionHintEntries } from "./permission-hint.js";
2
4
  import type { ClioTheme } from "./theme/index.js";
3
5
  import { clioTheme, editorTheme, GLYPH, rule } from "./theme/index.js";
4
6
 
5
7
  const REVERSE_VIDEO_BLANK = `${String.fromCharCode(27)}[7m ${String.fromCharCode(27)}[0m`;
6
8
  const EMPTY_PROMPT = "Ask Clio… / for commands";
9
+ const CONFIRM_PROMPT = "A parked call is waiting for your decision";
7
10
  const MIN_HINT_WIDTH = 60;
8
11
 
9
12
  function hasScrollIndicator(line: string): boolean {
@@ -18,6 +21,13 @@ export interface EditorChrome {
18
21
  getThinkingLabel: () => string;
19
22
  /** Whether Enter currently targets the active Clio response. */
20
23
  isStreaming?: () => boolean;
24
+ /**
25
+ * Whether a permission prompt owns the keyboard. The dialog once sat at the
26
+ * vertical center of a tall viewport, far from the composer. It now anchors
27
+ * above that composer, while the rail still says CONFIRM and carries the
28
+ * dialog's keys whenever the prompt owns input (issues #186 and #194).
29
+ */
30
+ isAwaitingApproval?: () => boolean;
21
31
  /** Whether the current draft will actually steer Clio or live dispatch work on Enter. */
22
32
  willEnterSteer?: (text: string) => boolean;
23
33
  /** Resolved submit binding, formatted for display. */
@@ -26,7 +36,7 @@ export interface EditorChrome {
26
36
  getNewlineKeyLabel?: () => string;
27
37
  }
28
38
 
29
- type ComposerMode = "MESSAGE" | "FOLLOW-UP" | "STEER";
39
+ type ComposerMode = "MESSAGE" | "FOLLOW-UP" | "STEER" | "CONFIRM";
30
40
 
31
41
  function normalizeThinkingHint(value: string): string {
32
42
  return value
@@ -61,6 +71,7 @@ function styledRailLabel(theme: ClioTheme, chrome: EditorChrome): string {
61
71
  }
62
72
 
63
73
  function composerMode(chrome: EditorChrome, text: string): ComposerMode {
74
+ if (chrome.isAwaitingApproval?.() ?? false) return "CONFIRM";
64
75
  if (!(chrome.isStreaming?.() ?? false)) return "MESSAGE";
65
76
  const willSteer = chrome.willEnterSteer?.(text) ?? text.trim().length > 0;
66
77
  return willSteer ? "STEER" : "FOLLOW-UP";
@@ -73,12 +84,27 @@ function lowerRailHint(theme: ClioTheme, chrome: EditorChrome): string {
73
84
  );
74
85
  }
75
86
 
76
- function renderEmptyPrompt(line: string, width: number, theme: ClioTheme): string {
87
+ /**
88
+ * The permission keys on the composer rail, fitted like the dialog footer so
89
+ * both surfaces narrow in the same order and never drop allow or stop first.
90
+ * The rule spends three columns around a right label, hence the subtraction.
91
+ */
92
+ function confirmRailHint(theme: ClioTheme, width: number, hasDraft: boolean): string {
93
+ return theme.fg("warning", fitHintEntries(permissionHintEntries(hasDraft), Math.max(1, width - 3)));
94
+ }
95
+
96
+ function modeToken(mode: ComposerMode): "action" | "accentDeep" | "warning" {
97
+ if (mode === "STEER") return "action";
98
+ if (mode === "CONFIRM") return "warning";
99
+ return "accentDeep";
100
+ }
101
+
102
+ function renderEmptyPrompt(line: string, width: number, theme: ClioTheme, text = EMPTY_PROMPT): string {
77
103
  const cursorAt = line.indexOf(REVERSE_VIDEO_BLANK);
78
104
  if (cursorAt < 0) return line;
79
105
  const afterCursorAt = cursorAt + REVERSE_VIDEO_BLANK.length;
80
106
  const available = Math.max(0, width - 1);
81
- const prompt = truncateToWidth(theme.fg("dim", EMPTY_PROMPT), available, "…", false);
107
+ const prompt = truncateToWidth(theme.fg("dim", text), available, "…", false);
82
108
  const consumed = visibleWidth(prompt);
83
109
  return `${line.slice(0, afterCursorAt)}${prompt}${line.slice(afterCursorAt + consumed)}`;
84
110
  }
@@ -111,12 +137,12 @@ export class ClioEditor extends Editor {
111
137
  // The base editor has already fitted the scroll indicator to this width.
112
138
  // Prefix the mode and trim only the indicator rail's trailing fill, keeping
113
139
  // its direction/count text and its own narrow-width fallback intact.
114
- const modeLabel = theme.style(mode === "STEER" ? "action" : "accentDeep", mode, { bold: true });
140
+ const modeLabel = theme.style(modeToken(mode), mode, { bold: true });
115
141
  lines[0] = truncateToWidth(`${modeLabel} ${lines[0] ?? ""}`, safeWidth, "", true);
116
142
  } else {
117
143
  lines[0] = rule(theme, safeWidth, {
118
144
  left: mode,
119
- leftToken: mode === "STEER" ? "action" : "accentDeep",
145
+ leftToken: modeToken(mode),
120
146
  right: styledRailLabel(theme, this.chrome),
121
147
  fillToken: "frameStrong",
122
148
  rightRaw: true,
@@ -124,10 +150,21 @@ export class ClioEditor extends Editor {
124
150
  });
125
151
  }
126
152
 
127
- if (text.length === 0 && lines[1]) lines[1] = renderEmptyPrompt(lines[1], safeWidth, theme);
153
+ if (text.length === 0 && lines[1]) {
154
+ lines[1] = renderEmptyPrompt(lines[1], safeWidth, theme, mode === "CONFIRM" ? CONFIRM_PROMPT : EMPTY_PROMPT);
155
+ }
128
156
 
129
157
  const bottomRail = findBottomRail(lines, safeWidth);
130
- if (bottomRail >= 0 && safeWidth >= MIN_HINT_WIDTH) {
158
+ // The confirm keys render at every width: the send hint is a convenience
159
+ // that a narrow composer can drop, the allow and deny keys are not.
160
+ if (bottomRail >= 0 && mode === "CONFIRM") {
161
+ lines[bottomRail] = rule(theme, safeWidth, {
162
+ right: confirmRailHint(theme, safeWidth, text.length > 0),
163
+ fillToken: "frameStrong",
164
+ rightRaw: true,
165
+ rightTail: theme.style("frameStrong", "─", { bold: true }),
166
+ });
167
+ } else if (bottomRail >= 0 && safeWidth >= MIN_HINT_WIDTH) {
131
168
  lines[bottomRail] = rule(theme, safeWidth, {
132
169
  right: lowerRailHint(theme, this.chrome),
133
170
  fillToken: "frameStrong",
@@ -3,6 +3,7 @@ import { relative } from "node:path";
3
3
  import { BusChannels } from "../core/bus-events.js";
4
4
  import type { SafeEventBus } from "../core/event-bus.js";
5
5
  import type { WorkingSetView } from "../domains/context/working-set/contract.js";
6
+ import { formatContextWindowSlots } from "../domains/providers/index.js";
6
7
  import type { ContextLedger, ContextLedgerGroup } from "../domains/session/context-ledger.js";
7
8
  import { type OverlayHandle, Text, type TUI, visibleWidth } from "../engine/tui.js";
8
9
  import { contextCategorySwatch, renderContextMeterGrid, renderEvictedTokensLine } from "./context-meter.js";
@@ -112,12 +113,34 @@ function coldReasonLabel(reason: string): string {
112
113
  }
113
114
  }
114
115
 
116
+ /** The operator's working-set configuration, as `/context` states it. */
117
+ export interface WorkingSetConfigView {
118
+ enabled: boolean;
119
+ policy: string;
120
+ }
121
+
115
122
  /**
116
123
  * The working-set section: what the projection has taken out of the window
117
124
  * and how often the model has asked for it back. Churn is recalls over items
118
125
  * evicted; a high number means the policy evicts what is still needed.
119
126
  */
120
- function renderWorkingSetLines(view: WorkingSetView): string[] {
127
+ /**
128
+ * The policy line names what is configured, not what last ran. `lastPolicyId`
129
+ * is stamped by the first applied event, and eviction runs only when pressure
130
+ * crosses `compaction.threshold`, so a whole session at the shipped default
131
+ * read `policy none` (issue #190). With no configuration in hand (older
132
+ * callers) the last applied policy is still the best available claim.
133
+ */
134
+ function workingSetPolicyLabel(view: WorkingSetView, config: WorkingSetConfigView | null | undefined): string {
135
+ if (config === null || config === undefined) return `policy ${view.lastPolicyId ?? "none"}`;
136
+ if (!config.enabled) return "disabled";
137
+ const state = view.evictionEvents === 0 ? " · no events yet" : "";
138
+ const lastRan =
139
+ view.lastPolicyId !== null && view.lastPolicyId !== config.policy ? ` (last event by ${view.lastPolicyId})` : "";
140
+ return `policy ${config.policy}${lastRan}${state}`;
141
+ }
142
+
143
+ function renderWorkingSetLines(view: WorkingSetView, config: WorkingSetConfigView | null | undefined): string[] {
121
144
  const theme = clioTheme();
122
145
  const items = view.evicted.size;
123
146
  const summary = [
@@ -128,7 +151,7 @@ function renderWorkingSetLines(view: WorkingSetView): string[] {
128
151
  `churn ${formatChurn(view)}`,
129
152
  ].join(" · ");
130
153
  return [
131
- `${theme.fg("muted", "working set")} ${theme.fg("dim", "·")} ${theme.fg("accent", `policy ${view.lastPolicyId ?? "none"}`)}`,
154
+ `${theme.fg("muted", "working set")} ${theme.fg("dim", "·")} ${theme.fg("accent", workingSetPolicyLabel(view, config))}`,
132
155
  theme.fg("dim", summary),
133
156
  ];
134
157
  }
@@ -137,6 +160,7 @@ export function renderContextLedgerLines(
137
160
  ledger: ContextLedger,
138
161
  contentWidth: number,
139
162
  workingSet?: WorkingSetView | null,
163
+ workingSetConfig?: WorkingSetConfigView | null,
140
164
  ): string[] {
141
165
  const theme = clioTheme();
142
166
  const lines: string[] = [];
@@ -154,7 +178,14 @@ export function renderContextLedgerLines(
154
178
 
155
179
  if (ledger.contextWindow > 0) {
156
180
  const source = ledger.measured ? "measured" : "≈ estimated";
157
- const summary = `${formatTokens(ledger.usedTokens)} / ${formatTokens(ledger.contextWindow)} tokens (${formatContextPercent(ledger.percent)})`;
181
+ // `196,608 (786,432 / 4 slots)`: a llama.cpp window that is one slot's
182
+ // share of the server's KV budget names the split, because the bare
183
+ // number reads as the whole server and the meter is measured against
184
+ // the share (issue #187).
185
+ const window = ledger.contextWindowSlots
186
+ ? formatContextWindowSlots(ledger.contextWindow, ledger.contextWindowSlots)
187
+ : formatTokens(ledger.contextWindow);
188
+ const summary = `${formatTokens(ledger.usedTokens)} / ${window} tokens (${formatContextPercent(ledger.percent)})`;
158
189
  const provenance = contextWindowProvenanceLabel(ledger.contextWindowSource);
159
190
  const trailer = provenance ? `${provenance} window · ${source}` : source;
160
191
  lines.push(`${theme.fg("title", summary)} ${theme.fg("dim", "·")} ${theme.fg("muted", trailer)}`);
@@ -170,7 +201,7 @@ export function renderContextLedgerLines(
170
201
 
171
202
  lines.push("");
172
203
  if (workingSet) {
173
- for (const line of renderWorkingSetLines(workingSet)) lines.push(line);
204
+ for (const line of renderWorkingSetLines(workingSet, workingSetConfig)) lines.push(line);
174
205
  lines.push("");
175
206
  }
176
207
  if (ledger.projectPreload && ledger.groups.some((group) => group.category === "project")) {
@@ -243,6 +274,8 @@ export interface OpenContextOverlayOptions {
243
274
  };
244
275
  /** Working-set fold at the live leaf; null or absent hides the section. */
245
276
  getWorkingSet?: () => WorkingSetView | null;
277
+ /** The configured `context.workingSet` block, so the policy line states what is set. */
278
+ getWorkingSetConfig?: () => WorkingSetConfigView | null;
246
279
  }
247
280
 
248
281
  /**
@@ -257,7 +290,12 @@ export function openContextOverlay(
257
290
  options?: OpenContextOverlayOptions,
258
291
  ): OverlayHandle {
259
292
  const render = (): string =>
260
- renderContextLedgerLines(getLedger(), DEFAULT_CONTENT_WIDTH, options?.getWorkingSet?.() ?? null).join("\n");
293
+ renderContextLedgerLines(
294
+ getLedger(),
295
+ DEFAULT_CONTENT_WIDTH,
296
+ options?.getWorkingSet?.() ?? null,
297
+ options?.getWorkingSetConfig?.() ?? null,
298
+ ).join("\n");
261
299
  const text = new Text(render(), 0, 0);
262
300
  const handle = showClioOverlayFrame(tui, text, {
263
301
  anchor: "center",
@@ -1,3 +1,8 @@
1
+ import {
2
+ addResponseModelIdObservationCounts,
3
+ type ResponseModelIdObservationCounts,
4
+ responseModelIdObservationCountsLabel,
5
+ } from "../core/response-model-id.js";
1
6
  import {
2
7
  aggregateCostAmounts,
3
8
  type CostAggregate,
@@ -16,7 +21,9 @@ export const COST_OVERLAY_WIDTH = DEFAULT_CONTENT_WIDTH + 4;
16
21
 
17
22
  export interface CostRow {
18
23
  providerId: string;
19
- modelId: string;
24
+ attributedModelId: string;
25
+ requestedModelIds: string[];
26
+ responseModelIdObservationCounts: ResponseModelIdObservationCounts;
20
27
  runs: number;
21
28
  tokens: number;
22
29
  input: number;
@@ -33,9 +40,17 @@ function formatTokens(n: number): string {
33
40
  }
34
41
 
35
42
  export function aggregateCostEntries(entries: ReadonlyArray<CostEntry>): CostRow[] {
36
- const grouped = new Map<string, { row: Omit<CostRow, "cost">; entries: CostEntry[] }>();
43
+ const grouped = new Map<
44
+ string,
45
+ {
46
+ row: Omit<CostRow, "cost" | "requestedModelIds" | "responseModelIdObservationCounts">;
47
+ requestedModelIds: Set<string>;
48
+ responseModelIdObservationCounts: ResponseModelIdObservationCounts;
49
+ entries: CostEntry[];
50
+ }
51
+ >();
37
52
  for (const entry of entries) {
38
- const key = `${entry.providerId}::${entry.modelId}`;
53
+ const key = `${entry.providerId}::${entry.attributedModelId}`;
39
54
  const existing = grouped.get(key);
40
55
  if (existing) {
41
56
  existing.row.runs += 1;
@@ -46,13 +61,18 @@ export function aggregateCostEntries(entries: ReadonlyArray<CostEntry>): CostRow
46
61
  existing.row.cacheWrite += entry.cacheWrite;
47
62
  existing.row.reasoningTokens += entry.reasoningTokens;
48
63
  existing.row.apiCalls += entry.apiCalls ?? 1;
64
+ for (const requestedModelId of entry.requestedModelIds) existing.requestedModelIds.add(requestedModelId);
65
+ addResponseModelIdObservationCounts(
66
+ existing.responseModelIdObservationCounts,
67
+ entry.responseModelIdObservationCounts,
68
+ );
49
69
  existing.entries.push(entry);
50
70
  continue;
51
71
  }
52
72
  grouped.set(key, {
53
73
  row: {
54
74
  providerId: entry.providerId,
55
- modelId: entry.modelId,
75
+ attributedModelId: entry.attributedModelId,
56
76
  runs: 1,
57
77
  tokens: entry.tokens,
58
78
  input: entry.input,
@@ -62,22 +82,31 @@ export function aggregateCostEntries(entries: ReadonlyArray<CostEntry>): CostRow
62
82
  reasoningTokens: entry.reasoningTokens,
63
83
  apiCalls: entry.apiCalls ?? 1,
64
84
  },
85
+ requestedModelIds: new Set(entry.requestedModelIds),
86
+ responseModelIdObservationCounts: { ...entry.responseModelIdObservationCounts },
65
87
  entries: [entry],
66
88
  });
67
89
  }
68
- const rows = Array.from(grouped.values(), ({ row, entries }) => ({
90
+ const rows = Array.from(grouped.values(), ({ row, entries, requestedModelIds, responseModelIdObservationCounts }) => ({
69
91
  ...row,
92
+ requestedModelIds: [...requestedModelIds].sort(),
93
+ responseModelIdObservationCounts,
70
94
  cost: aggregateCostAmounts(entries.map((entry) => ({ usd: entry.usd, provenance: entry.provenance }))),
71
95
  }));
72
96
  rows.sort((a, b) => {
73
97
  if (a.providerId !== b.providerId) return a.providerId < b.providerId ? -1 : 1;
74
- if (a.modelId !== b.modelId) return a.modelId < b.modelId ? -1 : 1;
98
+ if (a.attributedModelId !== b.attributedModelId) return a.attributedModelId < b.attributedModelId ? -1 : 1;
75
99
  return 0;
76
100
  });
77
101
  return rows;
78
102
  }
79
103
 
80
- function sumRows(rows: ReadonlyArray<CostRow>): Omit<CostRow, "providerId" | "modelId" | "cost"> {
104
+ function sumRows(
105
+ rows: ReadonlyArray<CostRow>,
106
+ ): Omit<
107
+ CostRow,
108
+ "providerId" | "attributedModelId" | "requestedModelIds" | "responseModelIdObservationCounts" | "cost"
109
+ > {
81
110
  return rows.reduce(
82
111
  (acc, row) => ({
83
112
  runs: acc.runs + row.runs,
@@ -156,6 +185,8 @@ function modelBlock(row: CostRow): string[] {
156
185
  // block with token rows and no cost row.
157
186
  const cost = formatCostAggregate(row.cost);
158
187
  return kvBlock([
188
+ ["requested model ids", row.requestedModelIds.join(", ")],
189
+ ["response model id observation", responseModelIdObservationCountsLabel(row.responseModelIdObservationCounts)],
159
190
  ["turns", formatTokens(row.runs)],
160
191
  ["model calls", formatTokens(row.apiCalls)],
161
192
  ...(cost === null ? [] : [["cost", cost] as const]),
@@ -185,7 +216,7 @@ export function formatCostOverlayBodyLines(
185
216
  } else {
186
217
  for (const [index, row] of rows.entries()) {
187
218
  if (index > 0) lines.push("");
188
- lines.push(theme.style("accent", `${row.providerId} · ${row.modelId}`, { bold: true }));
219
+ lines.push(theme.style("accent", `${row.providerId} · attributed model ${row.attributedModelId}`, { bold: true }));
189
220
  for (const line of modelBlock(row)) {
190
221
  lines.push(line);
191
222
  }