@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
@@ -0,0 +1,268 @@
1
+ import type { ActionClass } from "./action-classifier.js";
2
+ import type { AutonomyExposure } from "./autonomy.js";
3
+
4
+ /** Closed consequence categories used only to explain an already-made decision request. */
5
+ export const DECISION_TIERS = ["conversation", "workspace", "outward", "safety-net", "system", "worker"] as const;
6
+
7
+ export type DecisionTier = (typeof DECISION_TIERS)[number];
8
+ export type DecisionSemanticToken = "accent" | "action" | "warning";
9
+ export type DecisionRequestKind = "question" | "permission";
10
+ export type DecisionAffectedScope = "conversation" | "workspace" | "outward" | "system";
11
+ export type DecisionReversibility = "reversible" | "limited" | "unknown";
12
+ export type DecisionAuthorityEffect = "records-answer" | "grants-once";
13
+ export type DecisionActionId = "record-answer" | "cancel" | "approve-once" | "deny" | "stop";
14
+
15
+ export type DecisionAxis =
16
+ | { kind: "answer" }
17
+ | { kind: "autonomy"; level: string }
18
+ | { kind: "safety-net"; ruleId: string };
19
+
20
+ export type DecisionOrigin = { kind: "main" } | { kind: "worker"; agentId: string; runId: string };
21
+
22
+ /**
23
+ * Facts the host has already authenticated or derived from enforced policy.
24
+ * No model-authored title, question, reason, summary, or option label belongs
25
+ * here, so prose has no path to the tier selection.
26
+ */
27
+ export interface TrustedDecisionFacts {
28
+ requestKind: DecisionRequestKind;
29
+ axis: DecisionAxis;
30
+ exposure: AutonomyExposure;
31
+ affectedScope: DecisionAffectedScope;
32
+ reversibility: DecisionReversibility;
33
+ origin: DecisionOrigin;
34
+ authorityEffect: DecisionAuthorityEffect;
35
+ tool?: string;
36
+ actionClass?: ActionClass;
37
+ }
38
+
39
+ export interface DecisionPresentationAction {
40
+ id: DecisionActionId;
41
+ label: string;
42
+ consequence: string;
43
+ }
44
+
45
+ /**
46
+ * This projection contains display data only. Registry admission and interview
47
+ * settlement never consume it, and it contains no allow, deny, or autonomy
48
+ * disposition that an execution layer could mistake for authority.
49
+ */
50
+ export interface DecisionPresentation {
51
+ tier: DecisionTier;
52
+ tierLabel: string;
53
+ title: string;
54
+ semanticToken: DecisionSemanticToken;
55
+ authorizationCopy: string;
56
+ consequenceCopy: string;
57
+ reversibilityCopy: string;
58
+ requestedByCopy: string;
59
+ requiredActions: ReadonlyArray<DecisionPresentationAction>;
60
+ }
61
+
62
+ const ACTION_CLASSES = new Set<ActionClass>([
63
+ "read",
64
+ "write",
65
+ "execute",
66
+ "dispatch",
67
+ "system_modify",
68
+ "git_destructive",
69
+ "unknown",
70
+ ]);
71
+
72
+ /** Unknown or malformed action classes classify conservatively as unknown. */
73
+ export function decisionActionClass(value: unknown): ActionClass {
74
+ return typeof value === "string" && ACTION_CLASSES.has(value as ActionClass) ? (value as ActionClass) : "unknown";
75
+ }
76
+
77
+ function actionConsequence(actionClass: ActionClass): {
78
+ affectedScope: DecisionAffectedScope;
79
+ reversibility: DecisionReversibility;
80
+ } {
81
+ switch (actionClass) {
82
+ case "read":
83
+ return { affectedScope: "workspace", reversibility: "reversible" };
84
+ case "write":
85
+ return { affectedScope: "workspace", reversibility: "reversible" };
86
+ case "execute":
87
+ case "dispatch":
88
+ return { affectedScope: "workspace", reversibility: "limited" };
89
+ case "system_modify":
90
+ case "git_destructive":
91
+ case "unknown":
92
+ return { affectedScope: "system", reversibility: "unknown" };
93
+ }
94
+ }
95
+
96
+ export function decisionFactsForPermission(input: {
97
+ tool: string;
98
+ actionClass: ActionClass;
99
+ axis: Exclude<DecisionAxis, { kind: "answer" }>;
100
+ origin: DecisionOrigin;
101
+ exposure?: AutonomyExposure;
102
+ }): TrustedDecisionFacts {
103
+ const exposure = input.exposure ?? "local";
104
+ const consequence = actionConsequence(input.actionClass);
105
+ return {
106
+ requestKind: "permission",
107
+ axis: input.axis,
108
+ exposure,
109
+ affectedScope: exposure === "outward" ? "outward" : consequence.affectedScope,
110
+ reversibility: exposure === "outward" ? "limited" : consequence.reversibility,
111
+ origin: input.origin,
112
+ authorityEffect: "grants-once",
113
+ tool: input.tool,
114
+ actionClass: input.actionClass,
115
+ };
116
+ }
117
+
118
+ export function decisionFactsForAnswer(exposure: AutonomyExposure): TrustedDecisionFacts {
119
+ return {
120
+ requestKind: "question",
121
+ axis: { kind: "answer" },
122
+ exposure,
123
+ affectedScope: exposure === "outward" ? "outward" : "conversation",
124
+ reversibility: exposure === "outward" ? "limited" : "reversible",
125
+ origin: { kind: "main" },
126
+ authorityEffect: "records-answer",
127
+ };
128
+ }
129
+
130
+ function classifyTier(facts: TrustedDecisionFacts): DecisionTier {
131
+ if (facts.origin.kind === "worker") return "worker";
132
+ if (facts.affectedScope === "system") return "system";
133
+ if (facts.exposure === "outward" || facts.affectedScope === "outward") return "outward";
134
+ if (facts.axis.kind === "safety-net") return "safety-net";
135
+ if (facts.authorityEffect === "grants-once") return "workspace";
136
+ return "conversation";
137
+ }
138
+
139
+ function tierIdentity(tier: DecisionTier): {
140
+ tierLabel: string;
141
+ title: string;
142
+ semanticToken: DecisionSemanticToken;
143
+ } {
144
+ switch (tier) {
145
+ case "conversation":
146
+ return { tierLabel: "Conversational answer", title: "Answer a question", semanticToken: "accent" };
147
+ case "workspace":
148
+ return { tierLabel: "Workspace authority", title: "Approve workspace action", semanticToken: "action" };
149
+ case "outward":
150
+ return { tierLabel: "Outward consequence", title: "Confirm outward consequence", semanticToken: "warning" };
151
+ case "safety-net":
152
+ return { tierLabel: "Safety-net confirmation", title: "Safety-net confirmation", semanticToken: "warning" };
153
+ case "system":
154
+ return { tierLabel: "System change", title: "Approve system change", semanticToken: "warning" };
155
+ case "worker":
156
+ return { tierLabel: "Worker escalation", title: "Worker needs approval", semanticToken: "action" };
157
+ }
158
+ }
159
+
160
+ function requestedByCopy(facts: TrustedDecisionFacts): string {
161
+ const requester =
162
+ facts.origin.kind === "worker" ? `worker ${facts.origin.agentId} (run ${facts.origin.runId})` : "main agent";
163
+ if (facts.axis.kind === "answer") return requester;
164
+ if (facts.axis.kind === "safety-net") return `${requester} through safety-net rail ${facts.axis.ruleId}`;
165
+ return `${requester} through autonomy level (${facts.axis.level})`;
166
+ }
167
+
168
+ function authorizationCopy(facts: TrustedDecisionFacts): string {
169
+ if (facts.authorityEffect === "records-answer") {
170
+ return facts.exposure === "outward"
171
+ ? "Your response records an answer about an outward step. It does not publish or send anything by itself."
172
+ : "Your response records an answer for this interview. It grants no tool authority.";
173
+ }
174
+ const tool = facts.tool ?? "the presented tool";
175
+ const actionClass = facts.actionClass ?? "unknown";
176
+ if (tool === "ask_user" && facts.exposure === "outward") {
177
+ return "Approval opens this one outward-decision interview. It does not publish or send anything by itself.";
178
+ }
179
+ return `Approval authorizes one ${actionClass} call to ${tool}. It does not change the autonomy level.`;
180
+ }
181
+
182
+ function consequenceCopy(facts: TrustedDecisionFacts, tier: DecisionTier): string {
183
+ switch (tier) {
184
+ case "conversation":
185
+ return "The answer stays in this session's decision record.";
186
+ case "workspace":
187
+ return facts.actionClass === "read"
188
+ ? "The call reads within the workspace without mutating it."
189
+ : "The call can affect files or processes inside the workspace.";
190
+ case "outward":
191
+ return "The resulting step can reach people or systems outside the workspace.";
192
+ case "safety-net":
193
+ return "An always-on safety-net rail requires a one-shot operator decision before this call can run.";
194
+ case "system":
195
+ return "The call can change state outside the workspace or state the classifier cannot safely bound.";
196
+ case "worker":
197
+ return "A dispatched worker is parked. Your answer returns only to that run's exact request.";
198
+ }
199
+ }
200
+
201
+ function reversibilityCopy(facts: TrustedDecisionFacts): string {
202
+ if (facts.requestKind === "question") {
203
+ return facts.reversibility === "reversible"
204
+ ? "Reversible: yes. The recorded answer can be superseded later."
205
+ : "Reversible: not guaranteed. An outward step that later uses this answer may have lasting effects.";
206
+ }
207
+ if (facts.actionClass === "read" && facts.reversibility === "reversible") {
208
+ return "Reversible: yes. This call does not mutate the workspace.";
209
+ }
210
+ switch (facts.reversibility) {
211
+ case "reversible":
212
+ return "Reversible: yes. Workspace changes can be reviewed and reverted.";
213
+ case "limited":
214
+ return "Reversible: not guaranteed. Effects that already ran or reached an outward system may remain.";
215
+ case "unknown":
216
+ return "Reversible: unknown. Treat the effect as not safely reversible.";
217
+ }
218
+ }
219
+
220
+ function requiredActions(facts: TrustedDecisionFacts, tier: DecisionTier): ReadonlyArray<DecisionPresentationAction> {
221
+ if (facts.authorityEffect === "records-answer") {
222
+ return [
223
+ {
224
+ id: "record-answer",
225
+ label: facts.exposure === "outward" ? "Record outward answer" : "Record answer",
226
+ consequence: "Records the selected or typed answer without granting tool authority.",
227
+ },
228
+ { id: "cancel", label: "Cancel", consequence: "Closes the interview without recording an answer for this round." },
229
+ ];
230
+ }
231
+ const approveLabel =
232
+ tier === "system"
233
+ ? "Approve system change once"
234
+ : tier === "safety-net"
235
+ ? "Approve guarded action once"
236
+ : tier === "worker"
237
+ ? "Approve worker request once"
238
+ : tier === "outward"
239
+ ? "Approve outward decision once"
240
+ : "Approve workspace action once";
241
+ const stopConsequence =
242
+ facts.origin.kind === "worker"
243
+ ? "Denies this worker request and ends the active main-agent turn. Other worker requests retain their timeout policy."
244
+ : "Denies every parked request from this turn and ends the run.";
245
+ return [
246
+ {
247
+ id: "approve-once",
248
+ label: approveLabel,
249
+ consequence: "Runs only the presented request and does not change the autonomy level.",
250
+ },
251
+ { id: "deny", label: "Deny this request", consequence: "Denies only the presented request and advances the queue." },
252
+ { id: "stop", label: "Deny and stop", consequence: stopConsequence },
253
+ ];
254
+ }
255
+
256
+ /** Pure presentation classifier. It performs no I/O and has no execution authority. */
257
+ export function classifyDecisionPresentation(facts: TrustedDecisionFacts): DecisionPresentation {
258
+ const tier = classifyTier(facts);
259
+ return {
260
+ tier,
261
+ ...tierIdentity(tier),
262
+ authorizationCopy: authorizationCopy(facts),
263
+ consequenceCopy: consequenceCopy(facts, tier),
264
+ reversibilityCopy: reversibilityCopy(facts),
265
+ requestedByCopy: requestedByCopy(facts),
266
+ requiredActions: requiredActions(facts, tier),
267
+ };
268
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Display-time secret redaction for tool arguments.
3
+ *
4
+ * Two surfaces need the same rules and must not drift. The transcript's tool
5
+ * renderer projects a call's arguments for the operator's own screen, and the
6
+ * worker tool-admission seam composes a bounded action descriptor that crosses
7
+ * the NDJSON stdout boundary. A credential that one of them scrubs and the
8
+ * other prints is a leak, so the patterns live here and both import them.
9
+ *
10
+ * This is presentation only. Execution, receipts, and the safety verdict all
11
+ * keep the original argument values; nothing here changes what a tool runs.
12
+ */
13
+
14
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
15
+ return value !== null && typeof value === "object" && !Array.isArray(value);
16
+ }
17
+
18
+ const SECRET_KEY_RE =
19
+ /(?:api[-_]?key|access[-_]?key|auth(?:orization)?|credential|password|private[-_]?key|secret|token)/iu;
20
+ const ENV_KEY_RE = /^env(?:ironment)?$/iu;
21
+ const SECRET_STRING_RE = /(https?:\/\/[^\s/@]+:)[^\s/@]+@/giu;
22
+ const SECRET_URL_PARAM_RE =
23
+ /([?&](?:api[-_]?key|access[-_]?token|auth(?:orization)?|credential|password|secret|token)[^=\s]*=)[^&#\s]+/giu;
24
+ const SECRET_ASSIGNMENT_RE =
25
+ /(?<![A-Z0-9_])((?:[A-Z0-9_]*(?:ACCESS|API|AUTH|CREDENTIAL|KEY|PASS(?:WORD)?|PRIVATE|SECRET|TOKEN)[A-Z0-9_]*)=)[^\s;&|]+/giu;
26
+ const SECRET_FLAG_RE =
27
+ /(--?(?:[a-z0-9-]*(?:api[-_]?key|access[-_]?token|auth(?:orization)?|credential|password|secret|token)[a-z0-9-]*))(=|\s+)([^\s;&|]+)/giu;
28
+
29
+ /** Scrub credentials embedded in one string: URL userinfo, query params, `KEY=value`, and `--flag value`. */
30
+ export function redactSecretString(value: string): string {
31
+ return value
32
+ .replace(SECRET_STRING_RE, "$1[redacted]@")
33
+ .replace(SECRET_URL_PARAM_RE, "$1[redacted]")
34
+ .replace(SECRET_ASSIGNMENT_RE, "$1[redacted]")
35
+ .replace(SECRET_FLAG_RE, "$1$2[redacted]");
36
+ }
37
+
38
+ /** Whether an argument key names a secret outright, so its value never renders. */
39
+ export function isSecretArgKey(key: string): boolean {
40
+ return SECRET_KEY_RE.test(key);
41
+ }
42
+
43
+ /** Whether an argument key names an environment block, whose every string value is a candidate secret. */
44
+ function isEnvironmentArgKey(key: string): boolean {
45
+ return ENV_KEY_RE.test(key);
46
+ }
47
+
48
+ function redactEnvironmentValue(value: unknown, depth: number): unknown {
49
+ if (depth > 8) return "[redacted nested values]";
50
+ if (typeof value === "string") return "[redacted]";
51
+ if (Array.isArray(value)) return value.map((item) => redactEnvironmentValue(item, depth + 1));
52
+ if (!isPlainObject(value)) return value;
53
+ const out: Record<string, unknown> = {};
54
+ for (const [childKey, childValue] of Object.entries(value)) {
55
+ out[childKey] = redactEnvironmentValue(childValue, depth + 1);
56
+ }
57
+ return out;
58
+ }
59
+
60
+ /** Project tool arguments for display only; execution and receipts retain the original values. */
61
+ export function redactToolArgs(value: unknown, key = "", depth = 0): unknown {
62
+ if (isSecretArgKey(key)) return "[redacted]";
63
+ if (isEnvironmentArgKey(key)) return redactEnvironmentValue(value, depth);
64
+ if (typeof value === "string") return redactSecretString(value);
65
+ if (depth > 8) return "[redacted nested values]";
66
+ if (Array.isArray(value)) return value.map((item) => redactToolArgs(item, key, depth + 1));
67
+ if (!isPlainObject(value)) return value;
68
+ const out: Record<string, unknown> = {};
69
+ for (const [childKey, childValue] of Object.entries(value)) {
70
+ out[childKey] = redactToolArgs(childValue, childKey, depth + 1);
71
+ }
72
+ return out;
73
+ }
@@ -16,7 +16,7 @@
16
16
  * proportional reconciliation the footer bar already performs.
17
17
  */
18
18
 
19
- import type { ContextWindowSource } from "../providers/index.js";
19
+ import type { ContextWindowSlots, ContextWindowSource } from "../providers/index.js";
20
20
  import { DEFAULT_COMPACTION_THRESHOLD } from "./compaction/auto.js";
21
21
 
22
22
  /** Distinct buckets a context window is divided into for display. */
@@ -46,6 +46,8 @@ export interface BuildContextLedgerInput {
46
46
  contextWindow: number | null;
47
47
  /** Which layer answered `contextWindow`; omitted before a runtime resolves. */
48
48
  contextWindowSource?: ContextWindowSource | null;
49
+ /** The server-side split the probed window is a share of, when there is one. */
50
+ contextWindowSlots?: ContextWindowSlots | null;
49
51
  /**
50
52
  * Per-segment token estimates from the prompt compiler's segment manifest.
51
53
  * When empty, `systemPromptTokens` is used as a single opaque system bucket.
@@ -146,6 +148,12 @@ export interface ContextLedger {
146
148
  * the difference decides whether autocompact fires in time.
147
149
  */
148
150
  contextWindowSource: ContextWindowSource | null;
151
+ /**
152
+ * Present when `contextWindow` is one slot's share of the server's KV
153
+ * budget, so the overlay can show `196,608 (786,432 / 4 slots)` instead of
154
+ * a number that looks like the whole server.
155
+ */
156
+ contextWindowSlots: ContextWindowSlots | null;
149
157
  /** Sum of every content category (system..pending). */
150
158
  usedTokens: number;
151
159
  /** Autocompact headroom held in reserve above the conversation. */
@@ -346,6 +354,7 @@ export function buildContextLedger(input: BuildContextLedgerInput): ContextLedge
346
354
  model: input.model ?? null,
347
355
  contextWindow: window,
348
356
  contextWindowSource: input.contextWindowSource ?? null,
357
+ contextWindowSlots: input.contextWindowSlots ?? null,
349
358
  usedTokens,
350
359
  reserveTokens,
351
360
  freeTokens,
@@ -1,4 +1,5 @@
1
1
  import type { AskUserToolPolicy } from "../../tools/registry.js";
2
+ import type { AutonomyExposure } from "../safety/autonomy.js";
2
3
  import type { DecisionLedgerEntry, DecisionRecord } from "./entries.js";
3
4
 
4
5
  export interface DecisionLedgerEntryFields {
@@ -11,6 +12,7 @@ export interface DecisionLedgerEntryFields {
11
12
  roundCount: number;
12
13
  summary?: string;
13
14
  transcriptPath?: string;
15
+ exposure?: AutonomyExposure;
14
16
  decisions: DecisionRecord[];
15
17
  }
16
18
 
@@ -83,6 +85,7 @@ export function finalizedInterviewEntryFields(policy: AskUserToolPolicy): Decisi
83
85
  roundCount: policy.rounds.length,
84
86
  ...(policy.summary ? { summary: policy.summary } : {}),
85
87
  ...(policy.transcriptPath ? { transcriptPath: policy.transcriptPath } : {}),
88
+ exposure: policy.exposure ?? "local",
86
89
  decisions: [...decisionsByKey.values()],
87
90
  };
88
91
  }
@@ -172,6 +175,7 @@ export function createDecisionBoardStore(deps: DecisionBoardStoreDeps = {}): Dec
172
175
  roundCount: interview.roundCount,
173
176
  ...(interview.summary ? { summary: interview.summary } : {}),
174
177
  ...(interview.transcriptPath ? { transcriptPath: interview.transcriptPath } : {}),
178
+ ...(interview.exposure ? { exposure: interview.exposure } : {}),
175
179
  decisions: interview.decisions.map((decision) => {
176
180
  if (decision.key !== selected.key) return { ...decision };
177
181
  const next: DecisionRecord = { ...decision, status: "superseded", revisedAt };
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { isSkillActivation, type SkillActivation } from "../../core/skill-activation.js";
15
15
  import type { ClioTurnRecord } from "../../engine/session.js";
16
+ import { AUTONOMY_EXPOSURES, type AutonomyExposure } from "../safety/autonomy.js";
16
17
 
17
18
  export interface SessionHeader {
18
19
  type: "session";
@@ -229,6 +230,7 @@ export interface DecisionLedgerEntry extends BaseSessionEntry {
229
230
  roundCount: number;
230
231
  summary?: string;
231
232
  transcriptPath?: string;
233
+ exposure?: AutonomyExposure;
232
234
  decisions: DecisionRecord[];
233
235
  }
234
236
 
@@ -643,6 +645,7 @@ export function isSessionEntry(value: unknown): value is SessionEntry {
643
645
  v.roundCount >= 0 &&
644
646
  isOptionalString(v.summary) &&
645
647
  isOptionalString(v.transcriptPath) &&
648
+ (v.exposure === undefined || isOneOf(v.exposure, AUTONOMY_EXPOSURES)) &&
646
649
  Array.isArray(v.decisions) &&
647
650
  v.decisions.every(isDecisionRecord)
648
651
  );
@@ -48,21 +48,15 @@ function collapseWhitespace(text: string): string {
48
48
  }
49
49
 
50
50
  /**
51
- * Pull the first non-empty piece of user-authored text out of a payload.
52
- * Structured message payloads may contain a bare string, a text property,
53
- * or pi-ai content blocks.
51
+ * Pull the first non-empty piece of text out of a message payload. Structured
52
+ * message payloads may contain a bare string, a text property, or pi-ai
53
+ * content blocks.
54
54
  */
55
- function extractUserText(payload: unknown): string | null {
56
- if (typeof payload === "string") {
57
- const collapsed = collapseWhitespace(payload);
58
- return collapsed.length > 0 ? collapsed : null;
59
- }
55
+ function rawMessageText(payload: unknown): string | null {
56
+ if (typeof payload === "string") return payload.trim().length > 0 ? payload : null;
60
57
  if (!payload || typeof payload !== "object") return null;
61
58
  const obj = payload as Record<string, unknown>;
62
- if (typeof obj.text === "string") {
63
- const collapsed = collapseWhitespace(obj.text);
64
- if (collapsed.length > 0) return collapsed;
65
- }
59
+ if (typeof obj.text === "string" && obj.text.trim().length > 0) return obj.text;
66
60
  if (Array.isArray(obj.content)) {
67
61
  const parts: string[] = [];
68
62
  for (const part of obj.content) {
@@ -75,14 +69,65 @@ function extractUserText(payload: unknown): string | null {
75
69
  parts.push((part as { text: string }).text);
76
70
  }
77
71
  }
78
- const joined = collapseWhitespace(parts.join(" "));
79
- if (joined.length > 0) return joined;
72
+ const joined = parts.join("\n");
73
+ if (joined.trim().length > 0) return joined;
80
74
  }
81
75
  return null;
82
76
  }
83
77
 
78
+ function extractMessageText(payload: unknown): string | null {
79
+ const raw = rawMessageText(payload);
80
+ if (raw === null) return null;
81
+ const collapsed = collapseWhitespace(raw);
82
+ return collapsed.length > 0 ? collapsed : null;
83
+ }
84
+
85
+ /**
86
+ * What the operator typed for a user turn, for the picker preview.
87
+ *
88
+ * The persisted text is the composed prompt: a `<system-reminder>` block and
89
+ * any `[Skill request]` preamble ride ahead of the operator's words as visible
90
+ * text the model receives. Reading that verbatim made every row of the
91
+ * `/resume` picker read `<system-reminder> [Skills] 9 installed…`, so sessions
92
+ * were indistinguishable and the type-to-filter matched them all (issue #188).
93
+ * Entries written since the operator text was persisted carry it as
94
+ * `operatorText`; older ones drop the leading scaffolding. Null when nothing
95
+ * operator-authored remains, so the caller moves on to the next turn.
96
+ */
97
+ function operatorTextOfUserPayload(payload: unknown): string | null {
98
+ if (
99
+ payload &&
100
+ typeof payload === "object" &&
101
+ typeof (payload as { operatorText?: unknown }).operatorText === "string"
102
+ ) {
103
+ const collapsed = collapseWhitespace((payload as { operatorText: string }).operatorText);
104
+ return collapsed.length > 0 ? collapsed : null;
105
+ }
106
+ // Strip before collapsing: the preamble's line structure is what the
107
+ // patterns anchor on.
108
+ const text = rawMessageText(payload);
109
+ if (text === null) return null;
110
+ const stripped = collapseWhitespace(stripInjectedPreamble(text));
111
+ return stripped.length > 0 ? stripped : null;
112
+ }
113
+
114
+ const LEADING_SYSTEM_REMINDER = /^\s*<system-reminder>[\s\S]*?<\/system-reminder>\s*/u;
115
+ const LEADING_SKILL_REQUEST =
116
+ /^\s*\[Skill request\][\s\S]*?Only these pending skill names are allowed this turn\.[^\n]*\s*/u;
117
+
118
+ function stripInjectedPreamble(text: string): string {
119
+ let current = text;
120
+ for (;;) {
121
+ const next = current.replace(LEADING_SYSTEM_REMINDER, "").replace(LEADING_SKILL_REQUEST, "");
122
+ if (next === current) return current;
123
+ current = next;
124
+ }
125
+ }
126
+
84
127
  interface ScanResult {
85
128
  firstUserMessage: string | null;
129
+ /** The first assistant text, the preview when no turn carries operator words. */
130
+ firstAssistantMessage: string | null;
86
131
  messageCount: number;
87
132
  lastTimestamp: string | null;
88
133
  name: string | null;
@@ -92,6 +137,7 @@ interface ScanResult {
92
137
  function scanCurrentJsonl(currentPath: string): ScanResult {
93
138
  const result: ScanResult = {
94
139
  firstUserMessage: null,
140
+ firstAssistantMessage: null,
95
141
  messageCount: 0,
96
142
  lastTimestamp: null,
97
143
  name: null,
@@ -106,10 +152,14 @@ function scanCurrentJsonl(currentPath: string): ScanResult {
106
152
  if (parsed.kind === "message" && parsed.role === "user") {
107
153
  result.messageCount += 1;
108
154
  if (result.firstUserMessage === null) {
109
- const text = extractUserText(parsed.payload);
155
+ const text = operatorTextOfUserPayload(parsed.payload);
110
156
  if (text !== null) result.firstUserMessage = text;
111
157
  }
112
158
  }
159
+ if (parsed.kind === "message" && parsed.role === "assistant" && result.firstAssistantMessage === null) {
160
+ const text = extractMessageText(parsed.payload);
161
+ if (text !== null) result.firstAssistantMessage = text;
162
+ }
113
163
  if (parsed.kind === "sessionInfo") {
114
164
  const info = parsed as SessionInfoEntry;
115
165
  if (info.name !== undefined) {
@@ -139,11 +189,10 @@ function enrichMetaForListing(meta: SessionMeta, currentPath: string): void {
139
189
  if (scan.name) meta.name = scan.name;
140
190
  const labelValues = [...scan.labels.values()].map((entry) => entry.label.trim()).filter((label) => label.length > 0);
141
191
  if (labelValues.length > 0) meta.labels = labelValues;
142
- if (scan.firstUserMessage) {
192
+ const preview = scan.firstUserMessage ?? scan.firstAssistantMessage;
193
+ if (preview) {
143
194
  meta.firstMessagePreview =
144
- scan.firstUserMessage.length > PREVIEW_MAX_CHARS
145
- ? `${scan.firstUserMessage.slice(0, PREVIEW_MAX_CHARS - 1)}…`
146
- : scan.firstUserMessage;
195
+ preview.length > PREVIEW_MAX_CHARS ? `${preview.slice(0, PREVIEW_MAX_CHARS - 1)}…` : preview;
147
196
  }
148
197
  if (scan.messageCount > 0) meta.messageCount = scan.messageCount;
149
198
  const fallbackMtime = readMtimeIso(currentPath);
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Fold a session ledger's assistant turns into the per-call usage the rest of
3
3
  * Clio reports: tokens in and out, cache reads and writes, reasoning tokens,
4
- * and provider-reported cost, attributed to the target and wire model that
5
- * served each call.
4
+ * and provider-reported cost, attributed to the target and observed model id
5
+ * for each call.
6
6
  *
7
7
  * This lives in the session domain because the ledger is the only durable
8
8
  * record of what a session spent, and three surfaces read it: the `/cost`
@@ -19,12 +19,22 @@
19
19
  * counting it would add an API call worth nothing.
20
20
  */
21
21
 
22
+ import {
23
+ attributedModelId,
24
+ type ResponseModelIdObservation,
25
+ responseModelIdObservationFromRecord,
26
+ } from "../../core/response-model-id.js";
22
27
  import type { SessionEntry } from "./entries.js";
23
28
 
24
29
  /** One completed assistant API call, as the ledger recorded it. */
25
30
  export interface LedgerUsageCall {
26
31
  providerId: string;
27
- modelId: string;
32
+ /** Model id the accounting row uses, or `unknown` when an observed response omitted it. */
33
+ attributedModelId: string;
34
+ /** The id the session asked for (the configured model, or the message's own `model`). */
35
+ requestedModelId: string;
36
+ /** Direct presence observation, with the pre-#193 difference-only shape labeled separately. */
37
+ responseModelIdObservation: ResponseModelIdObservation;
28
38
  input: number;
29
39
  output: number;
30
40
  cacheRead: number;
@@ -71,8 +81,8 @@ function stringAt(source: Record<string, unknown>, ...keys: string[]): string |
71
81
  * payload instead split one endpoint into two blocks in `/cost`, so a single
72
82
  * `node-a` target on `llamacpp` rendered as two providers whose turn counts
73
83
  * diverged with every resume. `modelChange` rows are replayed in order so a
74
- * session that switched targets mid-way attributes each call to the one that
75
- * served it.
84
+ * session that switched targets mid-way attributes each call to the active
85
+ * target.
76
86
  */
77
87
  export function ledgerUsageCalls(
78
88
  entries: ReadonlyArray<SessionEntry>,
@@ -92,7 +102,9 @@ export function ledgerUsageCalls(
92
102
  if (!usage || usage.totalTokens <= 0) continue;
93
103
  calls.push({
94
104
  providerId: currentTarget ?? "unknown",
95
- modelId: currentModel ?? "unknown",
105
+ attributedModelId: currentModel ?? "unknown",
106
+ requestedModelId: currentModel ?? "unknown",
107
+ responseModelIdObservation: { state: "not-observed" },
96
108
  input: usage.input,
97
109
  output: usage.output,
98
110
  cacheRead: usage.cacheRead,
@@ -132,9 +144,14 @@ export function ledgerUsageCalls(
132
144
  if (totalTokens === 0) continue;
133
145
  const cost = usage.cost;
134
146
  const costUsd = cost && typeof cost === "object" ? numberAt(cost as Record<string, unknown>, "total") : 0;
147
+ const requestedModelId = currentModel ?? stringAt(record, "model") ?? "unknown";
148
+ const differingResponseModelId = stringAt(record, "responseModel");
149
+ const responseModelIdObservation = responseModelIdObservationFromRecord(record, "legacy-difference-only");
135
150
  calls.push({
136
151
  providerId: currentTarget ?? stringAt(record, "provider", "api") ?? "unknown",
137
- modelId: currentModel ?? stringAt(record, "responseModel", "model") ?? "unknown",
152
+ attributedModelId: attributedModelId(responseModelIdObservation, requestedModelId, differingResponseModelId),
153
+ requestedModelId,
154
+ responseModelIdObservation,
138
155
  input,
139
156
  output,
140
157
  cacheRead,
@@ -1,4 +1,5 @@
1
1
  import { performance } from "node:perf_hooks";
2
+ import { describeCallAction } from "../../domains/safety/call-target.js";
2
3
  import type { AgentEvent, AgentMessage } from "../types.js";
3
4
  import type { ClioWorkerEvent } from "../worker-events.js";
4
5
  import type { AcpPromptResponse, AcpSessionUpdateParams, AcpToolCallUpdate } from "./types.js";
@@ -111,12 +112,17 @@ export class AcpEventMapper {
111
112
  // keeps the monotonic twin, which is what the duration below spans.
112
113
  const startedAt = Date.now();
113
114
  this.toolStarts.set(toolCallId, performance.now());
115
+ // A peer's `rawInput` is its own argument object and never reaches an
116
+ // operator surface. The descriptor composed from it here does.
117
+ const action = describeCallAction(update.kind ?? title, update.rawInput);
114
118
  out.push({
115
119
  type: "clio_tool_start",
116
120
  payload: {
117
121
  tool: update.kind ?? title,
122
+ toolCallId,
118
123
  posture: "operating",
119
124
  startedAt,
125
+ ...(action !== null ? { action } : {}),
120
126
  },
121
127
  });
122
128
  out.push({
@@ -142,6 +148,7 @@ export class AcpEventMapper {
142
148
  type: "clio_tool_finish",
143
149
  payload: {
144
150
  tool: update.kind ?? title,
151
+ toolCallId,
145
152
  posture: "operating",
146
153
  durationMs: Math.round(performance.now() - startedAtClock),
147
154
  outcome: isError ? "error" : "ok",