@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.0

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 (285) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +4659 -4261
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/model-registry.d.ts +3 -1
  10. package/dist/types/config/models-config-schema.d.ts +18 -6
  11. package/dist/types/config/models-config.d.ts +14 -5
  12. package/dist/types/config/provider-globals.d.ts +2 -2
  13. package/dist/types/config/service-tier.d.ts +5 -1
  14. package/dist/types/config/settings-schema.d.ts +316 -47
  15. package/dist/types/config/settings.d.ts +7 -0
  16. package/dist/types/dap/client.d.ts +20 -0
  17. package/dist/types/exec/bash-executor.d.ts +38 -0
  18. package/dist/types/exec/direnv.d.ts +33 -0
  19. package/dist/types/export/html/args.d.ts +15 -0
  20. package/dist/types/export/html/index.d.ts +11 -26
  21. package/dist/types/export/html/web-palette.d.ts +55 -116
  22. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  23. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
  25. package/dist/types/live/controller.d.ts +40 -0
  26. package/dist/types/live/protocol.d.ts +85 -0
  27. package/dist/types/live/protocol.test.d.ts +1 -0
  28. package/dist/types/live/transport.d.ts +48 -0
  29. package/dist/types/live/visualizer.d.ts +33 -0
  30. package/dist/types/lsp/client.d.ts +16 -1
  31. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  32. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  33. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  34. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  35. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  36. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  37. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  38. package/dist/types/modes/components/settings-defs.d.ts +7 -1
  39. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  40. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  41. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  43. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  44. package/dist/types/modes/interactive-mode.d.ts +5 -0
  45. package/dist/types/modes/theme/theme.d.ts +1 -1
  46. package/dist/types/modes/types.d.ts +3 -1
  47. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  48. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  49. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  50. package/dist/types/registry/agent-registry.d.ts +11 -4
  51. package/dist/types/sdk.d.ts +15 -1
  52. package/dist/types/secrets/index.d.ts +15 -2
  53. package/dist/types/secrets/obfuscator.d.ts +113 -15
  54. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  55. package/dist/types/session/agent-session-events.d.ts +84 -0
  56. package/dist/types/session/agent-session-types.d.ts +314 -0
  57. package/dist/types/session/agent-session.d.ts +118 -755
  58. package/dist/types/session/async-job-delivery.d.ts +28 -0
  59. package/dist/types/session/bash-runner.d.ts +75 -0
  60. package/dist/types/session/blob-store.d.ts +11 -1
  61. package/dist/types/session/blob-store.test.d.ts +1 -0
  62. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  63. package/dist/types/session/eval-runner.d.ts +49 -0
  64. package/dist/types/session/irc-bridge.d.ts +46 -0
  65. package/dist/types/session/messages.d.ts +22 -0
  66. package/dist/types/session/model-controls.d.ts +180 -0
  67. package/dist/types/session/prewalk.d.ts +46 -0
  68. package/dist/types/session/queued-messages.d.ts +22 -0
  69. package/dist/types/session/redis-session-storage.d.ts +1 -0
  70. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  71. package/dist/types/session/role-models.d.ts +15 -0
  72. package/dist/types/session/session-advisors.d.ts +228 -0
  73. package/dist/types/session/session-entries.d.ts +8 -0
  74. package/dist/types/session/session-handoff.d.ts +70 -0
  75. package/dist/types/session/session-history-format.d.ts +6 -1
  76. package/dist/types/session/session-maintenance.d.ts +246 -0
  77. package/dist/types/session/session-manager.d.ts +41 -0
  78. package/dist/types/session/session-memory.d.ts +56 -0
  79. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  80. package/dist/types/session/session-stats.d.ts +48 -0
  81. package/dist/types/session/session-tools.d.ts +143 -0
  82. package/dist/types/session/session-workspace.d.ts +32 -0
  83. package/dist/types/session/stream-guards.d.ts +44 -0
  84. package/dist/types/session/todo-tracker.d.ts +56 -0
  85. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  86. package/dist/types/session/turn-recovery.d.ts +225 -0
  87. package/dist/types/system-prompt.d.ts +2 -0
  88. package/dist/types/task/executor.d.ts +4 -1
  89. package/dist/types/task/output-manager.d.ts +3 -1
  90. package/dist/types/tools/approval.d.ts +1 -0
  91. package/dist/types/tools/browser/launch.d.ts +5 -0
  92. package/dist/types/tools/image-gen.d.ts +4 -4
  93. package/dist/types/tools/image-providers.d.ts +38 -0
  94. package/dist/types/tools/index.d.ts +5 -0
  95. package/dist/types/tools/path-utils.d.ts +8 -0
  96. package/dist/types/tools/todo.d.ts +10 -4
  97. package/dist/types/tools/xdev.d.ts +5 -1
  98. package/dist/types/tts/streaming-player.d.ts +14 -0
  99. package/dist/types/tts/vocalizer.d.ts +5 -0
  100. package/dist/types/utils/jj.d.ts +29 -0
  101. package/dist/types/utils/lang-from-path.d.ts +1 -0
  102. package/dist/types/utils/title-generator.d.ts +30 -0
  103. package/dist/types/vibe/runtime.d.ts +36 -8
  104. package/dist/types/web/search/index.d.ts +1 -1
  105. package/dist/types/web/search/provider.d.ts +15 -5
  106. package/dist/types/web/search/types.d.ts +94 -0
  107. package/package.json +12 -12
  108. package/scripts/generate-share-viewer.ts +4 -6
  109. package/src/advisor/__tests__/advisor.test.ts +643 -0
  110. package/src/advisor/runtime.ts +144 -43
  111. package/src/async/job-manager.ts +90 -2
  112. package/src/cli/args.ts +2 -0
  113. package/src/cli/auth-gateway-cli.ts +18 -3
  114. package/src/cli/bench-cli.ts +395 -29
  115. package/src/cli/flag-tables.ts +3 -0
  116. package/src/cli/gc-cli.ts +2 -2
  117. package/src/cli/usage-cli.ts +10 -1
  118. package/src/collab/host.ts +3 -2
  119. package/src/commands/bench.ts +18 -4
  120. package/src/commands/launch.ts +4 -0
  121. package/src/config/model-registry.ts +10 -2
  122. package/src/config/models-config-schema.ts +15 -4
  123. package/src/config/provider-globals.ts +9 -9
  124. package/src/config/service-tier.ts +26 -1
  125. package/src/config/settings-schema.ts +209 -51
  126. package/src/config/settings.ts +81 -2
  127. package/src/dap/client.ts +37 -4
  128. package/src/edit/index.ts +49 -12
  129. package/src/eval/py/executor.ts +103 -33
  130. package/src/exec/bash-executor.ts +89 -6
  131. package/src/exec/direnv.ts +145 -0
  132. package/src/export/html/args.ts +20 -0
  133. package/src/export/html/index.ts +45 -47
  134. package/src/export/html/template.css +19 -1
  135. package/src/export/html/template.html +6 -0
  136. package/src/export/html/template.js +21 -0
  137. package/src/export/html/tool-views.generated.js +31 -31
  138. package/src/export/html/web-palette.ts +116 -132
  139. package/src/export/share.ts +249 -49
  140. package/src/extensibility/extensions/loader.ts +29 -1
  141. package/src/extensibility/extensions/runner.ts +6 -0
  142. package/src/extensibility/extensions/types.ts +33 -2
  143. package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
  144. package/src/hindsight/state.ts +64 -6
  145. package/src/internal-urls/local-protocol.ts +2 -1
  146. package/src/internal-urls/memory-protocol.ts +2 -2
  147. package/src/internal-urls/skill-protocol.ts +2 -2
  148. package/src/internal-urls/ssh-protocol.ts +2 -1
  149. package/src/internal-urls/vault-protocol.ts +2 -1
  150. package/src/live/audio-worklet.txt +59 -0
  151. package/src/live/browser-runtime.txt +221 -0
  152. package/src/live/controller.ts +464 -0
  153. package/src/live/prompts/agent-final-message.md +3 -0
  154. package/src/live/prompts/live-instructions.md +23 -0
  155. package/src/live/protocol.test.ts +140 -0
  156. package/src/live/protocol.ts +233 -0
  157. package/src/live/transport.ts +490 -0
  158. package/src/live/visualizer.ts +234 -0
  159. package/src/lsp/client.ts +29 -9
  160. package/src/lsp/index.ts +19 -4
  161. package/src/main.ts +8 -1
  162. package/src/mcp/config-writer.test.ts +43 -0
  163. package/src/mcp/config-writer.ts +109 -82
  164. package/src/mcp/oauth-discovery.ts +10 -2
  165. package/src/mcp/smithery-auth.test.ts +29 -0
  166. package/src/mcp/smithery-auth.ts +3 -2
  167. package/src/memories/index.ts +40 -20
  168. package/src/memory-backend/tool-names.ts +2 -0
  169. package/src/mnemopi/backend.ts +24 -7
  170. package/src/modes/acp/acp-agent.ts +56 -10
  171. package/src/modes/acp/acp-event-mapper.ts +8 -1
  172. package/src/modes/components/agent-hub.ts +1 -1
  173. package/src/modes/components/login-dialog.ts +14 -4
  174. package/src/modes/components/plan-review-overlay.ts +350 -35
  175. package/src/modes/components/settings-defs.ts +23 -1
  176. package/src/modes/components/settings-selector.ts +169 -1
  177. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  178. package/src/modes/components/status-line/component.ts +123 -2
  179. package/src/modes/components/tool-execution.ts +11 -2
  180. package/src/modes/controllers/command-controller.ts +56 -15
  181. package/src/modes/controllers/event-controller.ts +111 -16
  182. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  183. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  184. package/src/modes/controllers/live-command-controller.ts +178 -0
  185. package/src/modes/controllers/mcp-command-controller.ts +10 -1
  186. package/src/modes/controllers/omfg-controller.ts +44 -40
  187. package/src/modes/controllers/selector-controller.ts +18 -10
  188. package/src/modes/controllers/session-focus-controller.ts +6 -1
  189. package/src/modes/interactive-mode.ts +157 -38
  190. package/src/modes/runtime-init.ts +2 -0
  191. package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
  192. package/src/modes/theme/theme.ts +1 -1
  193. package/src/modes/types.ts +3 -1
  194. package/src/modes/utils/context-usage.ts +23 -7
  195. package/src/modes/utils/ui-helpers.ts +10 -5
  196. package/src/plan-mode/plan-files.ts +40 -0
  197. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  198. package/src/prompts/bench/cache-prefix.md +3 -0
  199. package/src/prompts/bench/cache-suffix.md +1 -0
  200. package/src/prompts/system/custom-system-prompt.md +1 -1
  201. package/src/prompts/system/project-prompt.md +8 -1
  202. package/src/prompts/system/subagent-async-pending.md +6 -0
  203. package/src/prompts/system/system-prompt.md +1 -1
  204. package/src/prompts/system/workflow-notice.md +48 -40
  205. package/src/prompts/system/xdev-mount-notice.md +4 -0
  206. package/src/prompts/tools/todo.md +3 -1
  207. package/src/registry/agent-lifecycle.ts +116 -46
  208. package/src/registry/agent-registry.ts +35 -8
  209. package/src/registry/persisted-agents.ts +26 -2
  210. package/src/sdk.ts +210 -136
  211. package/src/secrets/index.ts +127 -4
  212. package/src/secrets/obfuscator.ts +2328 -105
  213. package/src/session/acp-permission-gate.ts +165 -0
  214. package/src/session/agent-session-error-log.test.ts +1 -1
  215. package/src/session/agent-session-events.ts +66 -0
  216. package/src/session/agent-session-types.ts +334 -0
  217. package/src/session/agent-session.ts +1892 -12226
  218. package/src/session/artifacts.ts +6 -5
  219. package/src/session/async-job-delivery.ts +74 -0
  220. package/src/session/bash-runner.ts +326 -0
  221. package/src/session/blob-store.test.ts +56 -0
  222. package/src/session/blob-store.ts +18 -2
  223. package/src/session/checkpoint-entries.ts +81 -0
  224. package/src/session/eval-runner.ts +212 -0
  225. package/src/session/indexed-session-storage.ts +9 -2
  226. package/src/session/irc-bridge.ts +203 -0
  227. package/src/session/messages.ts +263 -1
  228. package/src/session/model-controls.ts +714 -0
  229. package/src/session/prewalk.ts +252 -0
  230. package/src/session/queued-messages.ts +93 -0
  231. package/src/session/redis-session-storage.ts +51 -11
  232. package/src/session/retry-fallback-chains.ts +455 -0
  233. package/src/session/role-models.ts +85 -0
  234. package/src/session/session-advisors.ts +1679 -0
  235. package/src/session/session-context.ts +6 -3
  236. package/src/session/session-entries.ts +8 -0
  237. package/src/session/session-handoff.ts +308 -0
  238. package/src/session/session-history-format.ts +20 -9
  239. package/src/session/session-loader.ts +0 -46
  240. package/src/session/session-maintenance.ts +2983 -0
  241. package/src/session/session-manager.ts +390 -30
  242. package/src/session/session-memory.ts +222 -0
  243. package/src/session/session-provider-boundary.ts +307 -0
  244. package/src/session/session-stats.ts +293 -0
  245. package/src/session/session-tools.ts +943 -0
  246. package/src/session/session-workspace.ts +53 -0
  247. package/src/session/stream-guards.ts +417 -0
  248. package/src/session/todo-tracker.ts +380 -0
  249. package/src/session/ttsr-coordinator.ts +496 -0
  250. package/src/session/turn-recovery.ts +1629 -0
  251. package/src/slash-commands/builtin-registry.ts +92 -11
  252. package/src/slash-commands/helpers/usage-report.ts +22 -3
  253. package/src/system-prompt.ts +41 -17
  254. package/src/task/executor.ts +285 -70
  255. package/src/task/index.ts +98 -105
  256. package/src/task/output-manager.ts +25 -3
  257. package/src/task/persisted-revive.ts +4 -3
  258. package/src/task/structured-subagent.ts +1 -0
  259. package/src/tools/approval.ts +55 -10
  260. package/src/tools/bash-interactive.ts +26 -0
  261. package/src/tools/bash-skill-urls.ts +28 -2
  262. package/src/tools/bash.ts +339 -123
  263. package/src/tools/browser/launch.ts +9 -1
  264. package/src/tools/browser/tab-supervisor.ts +8 -5
  265. package/src/tools/browser.ts +94 -47
  266. package/src/tools/image-gen.ts +20 -28
  267. package/src/tools/image-providers.ts +50 -0
  268. package/src/tools/index.ts +9 -0
  269. package/src/tools/path-utils.ts +49 -2
  270. package/src/tools/read.ts +156 -88
  271. package/src/tools/renderers.ts +7 -1
  272. package/src/tools/todo.ts +101 -11
  273. package/src/tools/vibe.ts +1 -2
  274. package/src/tools/write.ts +51 -1
  275. package/src/tools/xdev.ts +67 -3
  276. package/src/tts/streaming-player.ts +62 -4
  277. package/src/tts/vocalizer.ts +18 -1
  278. package/src/utils/jj.ts +125 -4
  279. package/src/utils/lang-from-path.ts +7 -0
  280. package/src/utils/title-generator.ts +137 -1
  281. package/src/vibe/runtime.ts +852 -72
  282. package/src/web/search/index.ts +3 -6
  283. package/src/web/search/provider.ts +31 -16
  284. package/src/web/search/providers/xai.ts +7 -1
  285. package/src/web/search/types.ts +3 -0
@@ -16,7 +16,7 @@ import type {
16
16
  import { resolveModelServiceTier, streamSimple } from "@oh-my-pi/pi-ai";
17
17
  import { buildModelProviderPriorityRank } from "@oh-my-pi/pi-catalog/identity";
18
18
  import { replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
19
- import { formatDuration, getProjectDir } from "@oh-my-pi/pi-utils";
19
+ import { formatDuration, getProjectDir, prompt } from "@oh-my-pi/pi-utils";
20
20
  import chalk from "chalk";
21
21
  import type { ApiKeyResolverModel } from "../config/api-key-resolver";
22
22
  import { ModelRegistry } from "../config/model-registry";
@@ -28,6 +28,9 @@ import {
28
28
  } from "../config/model-resolver";
29
29
  import { buildServiceTierByFamily, serviceTierForAllFamilies, serviceTierSettingToTier } from "../config/service-tier";
30
30
  import { Settings } from "../config/settings";
31
+ import cachePrefixTemplate from "../prompts/bench/cache-prefix.md" with { type: "text" };
32
+ import cachePrefixChunk from "../prompts/bench/cache-prefix-chunk.md" with { type: "text" };
33
+ import cacheSuffixTemplate from "../prompts/bench/cache-suffix.md" with { type: "text" };
31
34
  import benchPrompt from "../prompts/bench.md" with { type: "text" };
32
35
  import { discoverAuthStorage, loadCliExtensionProviders } from "../sdk";
33
36
  import {
@@ -40,8 +43,18 @@ import {
40
43
  const DEFAULT_RUNS = 10;
41
44
  const DEFAULT_PAR = 4;
42
45
  const DEFAULT_MAX_TOKENS = 512;
46
+ const DEFAULT_CACHE_MAX_TOKENS = 64;
47
+ const DEFAULT_CACHE_PREFIX_BYTES = 8_192;
48
+ const DEFAULT_CACHE_PAIRS = 1;
49
+ const DEFAULT_CACHE_CONCURRENCY = 1;
43
50
  const ERROR_WIDTH = 110;
44
51
  const BENCH_PROMPT = benchPrompt.trim();
52
+ const UTF8_ENCODER = new TextEncoder();
53
+ const UTF8_DECODER = new TextDecoder();
54
+ const CACHE_PREFIX_CHUNK = cachePrefixChunk;
55
+ const CACHE_PREFIX_PLACEHOLDER = "__OMP_CACHE_BENCH_RAW_PREFIX__";
56
+ const CACHE_PREFIX_CHUNK_BYTES = UTF8_ENCODER.encode(CACHE_PREFIX_CHUNK).byteLength;
57
+ const RESPONSE_CACHE_STATUS_HEADERS = ["cf-aig-cache-status"] as const;
45
58
 
46
59
  export interface BenchCommandArgs {
47
60
  models: string[];
@@ -53,6 +66,11 @@ export interface BenchCommandArgs {
53
66
  serviceTier?: string;
54
67
  json?: boolean;
55
68
  par?: number;
69
+ cache?: boolean;
70
+ cachePrefixFile?: string;
71
+ cachePrefixBytes?: number;
72
+ cachePairs?: number;
73
+ cacheConcurrency?: number;
56
74
  };
57
75
  }
58
76
 
@@ -85,6 +103,44 @@ export interface BenchRunFailure {
85
103
 
86
104
  export type BenchRunResult = BenchRunSuccess | BenchRunFailure;
87
105
 
106
+ export type CacheObservation =
107
+ | "prompt_cache_read_observed"
108
+ | "prompt_cache_write_observed"
109
+ | "response_cache_hit_observed"
110
+ | "no_provider_proof";
111
+
112
+ export interface BenchCacheUsage {
113
+ inputTokens: number;
114
+ outputTokens: number;
115
+ cacheReadTokens: number;
116
+ cacheWriteTokens: number;
117
+ totalTokens: number;
118
+ cost: number;
119
+ }
120
+
121
+ export interface BenchCacheRunReport {
122
+ phase: "cold" | "warm";
123
+ result: BenchRunResult;
124
+ usage?: BenchCacheUsage;
125
+ requestIdObserved: boolean;
126
+ observations: CacheObservation[];
127
+ }
128
+
129
+ export interface BenchCachePairReport {
130
+ cold: BenchCacheRunReport;
131
+ warm: BenchCacheRunReport;
132
+ /** The nominal cold request showed cache reuse, so it is not a true cold baseline. */
133
+ coldAlreadyWarm: boolean;
134
+ /** Structural-only comparisons: prompt text and cache keys are never emitted. */
135
+ stablePrefix: true;
136
+ suffixChanged: true;
137
+ promptCacheKeyStable: true;
138
+ statefulResponsesDisabled: true;
139
+ freshProviderSessionState: true;
140
+ /** "unavailable" when a transport does not expose the provider payload locally. */
141
+ payloadStructureStable: boolean | "unavailable";
142
+ }
143
+
88
144
  export interface BenchAverages {
89
145
  ttftMs: number;
90
146
  durationMs: number;
@@ -102,6 +158,7 @@ export interface BenchModelReport {
102
158
  results: BenchRunResult[];
103
159
  /** Averages over successful runs; null when every run failed. */
104
160
  average: BenchAverages | null;
161
+ cachePairs?: BenchCachePairReport[];
105
162
  }
106
163
 
107
164
  export interface BenchSummary {
@@ -111,6 +168,10 @@ export interface BenchSummary {
111
168
  failures: number;
112
169
  /** Requested per-family service tiers, resolved per model before reaching the wire. */
113
170
  serviceTierByFamily?: ServiceTierByFamily;
171
+ cache?: {
172
+ pairs: number;
173
+ concurrency: number;
174
+ };
114
175
  }
115
176
 
116
177
  type BenchStreamSimple = (
@@ -127,6 +188,7 @@ export interface BenchDependencies {
127
188
  setExitCode?: (code: number) => void;
128
189
  streamSimple?: BenchStreamSimple;
129
190
  now?: () => number;
191
+ readTextFile?: (path: string, maxBytes: number) => Promise<string>;
130
192
  stdoutIsTTY?: boolean;
131
193
  }
132
194
 
@@ -184,10 +246,176 @@ function hasVisibleFinalContent(message: AssistantMessage): boolean {
184
246
  });
185
247
  }
186
248
 
249
+ interface CacheRequestCapture {
250
+ payloadStructure?: string;
251
+ requestIdObserved: boolean;
252
+ responseCacheHit: boolean;
253
+ usage?: BenchCacheUsage;
254
+ }
255
+
256
+ function payloadStructure(payload: unknown): string {
257
+ if (payload === null) return "null";
258
+ if (Array.isArray(payload)) return `[${payload.map(payloadStructure).join(",")}]`;
259
+ if (typeof payload === "object") {
260
+ const record = payload as Record<string, unknown>;
261
+ return `{${Object.keys(record)
262
+ .sort()
263
+ .map(key => `${key}:${payloadStructure(record[key])}`)
264
+ .join(",")}}`;
265
+ }
266
+ return typeof payload;
267
+ }
268
+
269
+ function asNonNegativeNumber(value: unknown): number {
270
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
271
+ }
272
+
273
+ function captureUsage(message: AssistantMessage): BenchCacheUsage {
274
+ const usage = message.usage;
275
+ return {
276
+ inputTokens: asNonNegativeNumber(usage.input),
277
+ outputTokens: asNonNegativeNumber(usage.output),
278
+ cacheReadTokens: asNonNegativeNumber(usage.cacheRead),
279
+ cacheWriteTokens: asNonNegativeNumber(usage.cacheWrite),
280
+ totalTokens: asNonNegativeNumber(usage.totalTokens),
281
+ cost: asNonNegativeNumber(usage.cost?.total),
282
+ };
283
+ }
284
+
285
+ function cacheObservations(capture: CacheRequestCapture): CacheObservation[] {
286
+ const observations: CacheObservation[] = [];
287
+ if ((capture.usage?.cacheReadTokens ?? 0) > 0) observations.push("prompt_cache_read_observed");
288
+ if ((capture.usage?.cacheWriteTokens ?? 0) > 0) observations.push("prompt_cache_write_observed");
289
+ if (capture.responseCacheHit) observations.push("response_cache_hit_observed");
290
+ return observations.length > 0 ? observations : ["no_provider_proof"];
291
+ }
292
+
293
+ function cacheRunReport(
294
+ phase: BenchCacheRunReport["phase"],
295
+ result: BenchRunResult,
296
+ capture: CacheRequestCapture,
297
+ ): BenchCacheRunReport {
298
+ return {
299
+ phase,
300
+ result,
301
+ usage: capture.usage,
302
+ requestIdObserved: capture.requestIdObserved,
303
+ observations: cacheObservations(capture),
304
+ };
305
+ }
306
+
307
+ function truncateUtf8ByteLength(bytes: Uint8Array, maxBytes: number): number {
308
+ const end = Math.min(bytes.byteLength, maxBytes);
309
+ if (end === 0) return 0;
310
+
311
+ let sequenceStart = end - 1;
312
+ while (sequenceStart > 0 && (bytes[sequenceStart]! & 0b1100_0000) === 0b1000_0000) sequenceStart--;
313
+
314
+ const leadingByte = bytes[sequenceStart]!;
315
+ const sequenceLength =
316
+ leadingByte <= 0b0111_1111
317
+ ? 1
318
+ : leadingByte >= 0b1100_0010 && leadingByte <= 0b1101_1111
319
+ ? 2
320
+ : leadingByte >= 0b1110_0000 && leadingByte <= 0b1110_1111
321
+ ? 3
322
+ : leadingByte >= 0b1111_0000 && leadingByte <= 0b1111_0100
323
+ ? 4
324
+ : 1;
325
+ return sequenceLength > end - sequenceStart ? sequenceStart : end;
326
+ }
327
+
328
+ async function readBoundedUtf8File(path: string, maxBytes: number): Promise<string> {
329
+ const file = Bun.file(path);
330
+ const bytes = new Uint8Array(await file.slice(0, maxBytes).arrayBuffer());
331
+ const end = truncateUtf8ByteLength(bytes, maxBytes);
332
+ return UTF8_DECODER.decode(bytes.subarray(0, end));
333
+ }
334
+
335
+ function truncateUtf8(text: string, maxBytes: number): string {
336
+ const bytes = UTF8_ENCODER.encode(text);
337
+ const end = truncateUtf8ByteLength(bytes, maxBytes);
338
+ return end === bytes.byteLength ? text : UTF8_DECODER.decode(bytes.subarray(0, end));
339
+ }
340
+
341
+ function generatedCachePrefix(bytes: number): string {
342
+ return truncateUtf8(CACHE_PREFIX_CHUNK.repeat(Math.ceil(bytes / CACHE_PREFIX_CHUNK_BYTES)), bytes);
343
+ }
344
+
345
+ function renderCacheBenchmarkPrefix(prefix: string, namespace: string): string {
346
+ const rendered = prompt.render(cachePrefixTemplate, {
347
+ prefix: CACHE_PREFIX_PLACEHOLDER,
348
+ namespace,
349
+ });
350
+ if (!rendered.includes(CACHE_PREFIX_PLACEHOLDER)) {
351
+ throw new Error("Cache benchmark prefix template is missing its raw prefix placeholder");
352
+ }
353
+ // Render the static wrapper first, then inject caller bytes so prompt
354
+ // normalization cannot trim spaces or collapse blank lines in prefix files.
355
+ // Function replacer: a prefix containing `$&`/`$'`/`` $` `` must not be
356
+ // expanded as a string-replacement pattern.
357
+ return rendered.replace(CACHE_PREFIX_PLACEHOLDER, () => prefix);
358
+ }
359
+
360
+ async function resolveCachePrefix(
361
+ flags: BenchCommandArgs["flags"],
362
+ readTextFile: (path: string, maxBytes: number) => Promise<string>,
363
+ ): Promise<string> {
364
+ const bytes = normalizePositiveInteger("cache-prefix-bytes", flags.cachePrefixBytes, DEFAULT_CACHE_PREFIX_BYTES);
365
+ const prefix = flags.cachePrefixFile
366
+ ? await readTextFile(flags.cachePrefixFile, bytes)
367
+ : generatedCachePrefix(bytes);
368
+ return truncateUtf8(prefix, bytes);
369
+ }
370
+
371
+ function cacheBenchmarkMessages(stablePrefix: string, suffix: string): Context["messages"] {
372
+ const timestamp = Date.now();
373
+ return [
374
+ { role: "user", content: stablePrefix, timestamp, attribution: "user" },
375
+ { role: "user", content: suffix, timestamp, attribution: "user" },
376
+ ];
377
+ }
378
+
379
+ async function runWithConcurrency<T>(
380
+ count: number,
381
+ concurrency: number,
382
+ run: (index: number) => Promise<T>,
383
+ ): Promise<T[]> {
384
+ const results = new Array<T>(count);
385
+ let next = 0;
386
+ const worker = async (): Promise<void> => {
387
+ while (next < count) {
388
+ const index = next++;
389
+ results[index] = await run(index);
390
+ }
391
+ };
392
+ await Promise.all(Array.from({ length: Math.min(count, concurrency) }, worker));
393
+ return results;
394
+ }
395
+
396
+ function formatCacheCost(cost: number): string {
397
+ if (cost < 0.01) return `$${cost.toFixed(4)}`;
398
+ if (cost < 1) return `$${cost.toFixed(3)}`;
399
+ return `$${cost.toFixed(2)}`;
400
+ }
401
+
402
+ function formatCachePairLine(pair: BenchCachePairReport, index: number, total: number): string {
403
+ const formatPhase = (run: BenchCacheRunReport, alreadyWarm = false) => {
404
+ if (!run.result.ok) {
405
+ return `${run.phase} failed: ${truncateToWidth(replaceTabs(run.result.error), ERROR_WIDTH)}`;
406
+ }
407
+ const usage = run.usage;
408
+ return `${run.phase}${alreadyWarm ? " (already warm)" : ""} ${run.observations.join(", ")} ${chalk.dim("input")} ${usage?.inputTokens ?? 0} ${chalk.dim("cache-read")} ${usage?.cacheReadTokens ?? 0} ${chalk.dim("cache-write")} ${usage?.cacheWriteTokens ?? 0} ${chalk.dim("output")} ${usage?.outputTokens ?? run.result.outputTokens} ${chalk.dim("total")} ${usage?.totalTokens ?? 0} ${chalk.dim("cost")} ${formatCacheCost(usage?.cost ?? 0)} ${chalk.dim("TTFT")} ${formatMs(run.result.ttftMs)} ${chalk.dim("duration")} ${formatMs(run.result.durationMs)} ${chalk.dim("throughput")} ${run.result.tokensPerSecond.toFixed(1)}/s`;
409
+ };
410
+ return ` ${chalk.dim(`pair ${index + 1}/${total}`)} ${formatPhase(pair.cold, pair.coldAlreadyWarm)}; ${formatPhase(pair.warm)}`;
411
+ }
412
+
187
413
  interface BenchRequestOptions {
188
414
  apiKey: ApiKeyResolver;
189
415
  sessionId: string;
190
416
  prompt: string;
417
+ /** Native OMP messages; cache mode splits the stable prefix from the suffix. */
418
+ contextMessages?: Context["messages"];
191
419
  maxTokens: number;
192
420
  /** Explicit effort from a `:level` selector suffix; absent = provider default. */
193
421
  reasoning?: Effort;
@@ -195,6 +423,9 @@ interface BenchRequestOptions {
195
423
  disableReasoning?: boolean;
196
424
  /** Requested service tier passed to `streamSimple`; absent omits the option. The provider layer applies scope/support gating before it reaches the wire. */
197
425
  serviceTier?: ServiceTier;
426
+ promptCacheKey?: string;
427
+ statefulResponses?: false;
428
+ cacheCapture?: CacheRequestCapture;
198
429
  }
199
430
 
200
431
  async function runBenchRequest(
@@ -211,7 +442,9 @@ async function runBenchRequest(
211
442
  // Codex's Responses endpoint 400s with "Instructions are required" when no
212
443
  // system prompt is present — same guard as eval's completion bridge.
213
444
  systemPrompt: ["You are a helpful assistant."],
214
- messages: [{ role: "user", content: options.prompt, timestamp: Date.now(), attribution: "user" }],
445
+ messages: options.contextMessages ?? [
446
+ { role: "user", content: options.prompt, timestamp: Date.now(), attribution: "user" },
447
+ ],
215
448
  };
216
449
  const stream = streamFn(model, context, {
217
450
  apiKey: options.apiKey,
@@ -221,6 +454,22 @@ async function runBenchRequest(
221
454
  ? Math.min(options.maxTokens, model.maxTokens)
222
455
  : options.maxTokens,
223
456
  reasoning: options.reasoning,
457
+ promptCacheKey: options.promptCacheKey,
458
+ statefulResponses: options.statefulResponses,
459
+ onPayload: options.cacheCapture
460
+ ? payload => {
461
+ options.cacheCapture!.payloadStructure = payloadStructure(payload);
462
+ return undefined;
463
+ }
464
+ : undefined,
465
+ onResponse: options.cacheCapture
466
+ ? response => {
467
+ options.cacheCapture!.requestIdObserved = Boolean(response.requestId);
468
+ options.cacheCapture!.responseCacheHit = RESPONSE_CACHE_STATUS_HEADERS.some(
469
+ header => response.headers[header]?.trim().toLowerCase() === "hit",
470
+ );
471
+ }
472
+ : undefined,
224
473
  disableReasoning: options.disableReasoning,
225
474
  serviceTier: options.serviceTier,
226
475
  providerSessionState,
@@ -265,6 +514,7 @@ async function runBenchRequest(
265
514
  error: `provider returned no output (0 tokens, empty stream; stop reason: ${message.stopReason ?? "unknown"})`,
266
515
  };
267
516
  }
517
+ if (options.cacheCapture) options.cacheCapture.usage = captureUsage(message);
268
518
  return {
269
519
  ok: true,
270
520
  ttftMs,
@@ -480,14 +730,48 @@ function resolveBenchModels(
480
730
  }
481
731
  return resolved;
482
732
  }
733
+ function assertCacheModeSupported(targets: BenchTarget[]): void {
734
+ if (targets.some(({ model }) => model.api === "openai-codex-responses")) {
735
+ throw new Error(
736
+ "--cache is not supported for openai-codex-responses because Codex WebSocket chaining cannot produce independent prompt-cache pairs",
737
+ );
738
+ }
739
+ }
740
+
483
741
  export async function runBenchCommand(command: BenchCommandArgs, deps: BenchDependencies = {}): Promise<BenchSummary> {
484
- const runs = normalizePositiveInteger("runs", command.flags.runs, DEFAULT_RUNS);
485
- const maxTokens = normalizePositiveInteger("max-tokens", command.flags.maxTokens, DEFAULT_MAX_TOKENS);
742
+ const cacheMode = command.flags.cache === true;
743
+ const cacheFlagsUsed =
744
+ command.flags.cachePrefixFile !== undefined ||
745
+ command.flags.cachePrefixBytes !== undefined ||
746
+ command.flags.cachePairs !== undefined ||
747
+ command.flags.cacheConcurrency !== undefined;
748
+ if (!cacheMode && cacheFlagsUsed) throw new Error("Cache flags require --cache");
749
+ if (cacheMode && command.flags.runs !== undefined)
750
+ throw new Error("Use --cache-pairs instead of --runs with --cache");
751
+ if (cacheMode && command.flags.prompt !== undefined) throw new Error("--cache builds its own stable-prefix prompts");
752
+ if (cacheMode && (command.flags.par ?? 1) > 1) {
753
+ throw new Error("--par cannot parallelize cold/warm pairs; use --cache-concurrency instead");
754
+ }
755
+
756
+ const cachePairs = cacheMode
757
+ ? normalizePositiveInteger("cache-pairs", command.flags.cachePairs, DEFAULT_CACHE_PAIRS)
758
+ : undefined;
759
+ const cacheConcurrency = cacheMode
760
+ ? normalizePositiveInteger("cache-concurrency", command.flags.cacheConcurrency, DEFAULT_CACHE_CONCURRENCY)
761
+ : undefined;
762
+ const runs = cacheMode ? cachePairs! * 2 : normalizePositiveInteger("runs", command.flags.runs, DEFAULT_RUNS);
763
+ const maxTokens = normalizePositiveInteger(
764
+ "max-tokens",
765
+ command.flags.maxTokens,
766
+ cacheMode ? DEFAULT_CACHE_MAX_TOKENS : DEFAULT_MAX_TOKENS,
767
+ );
486
768
  const par =
487
769
  command.flags.par !== undefined ? normalizePositiveInteger("par", command.flags.par, DEFAULT_PAR) : DEFAULT_PAR;
488
- const prompt = command.flags.prompt?.trim() || BENCH_PROMPT;
770
+ const benchmarkPrompt = command.flags.prompt?.trim() || BENCH_PROMPT;
489
771
  const json = command.flags.json === true;
490
772
  const randomSessionId = deps.randomSessionId ?? (() => Bun.randomUUIDv7());
773
+ const readTextFile = deps.readTextFile ?? readBoundedUtf8File;
774
+ const cachePrefix = cacheMode ? await resolveCachePrefix(command.flags, readTextFile) : undefined;
491
775
  const writeStdout = deps.writeStdout ?? ((text: string) => process.stdout.write(text));
492
776
  const writeStderr = deps.writeStderr ?? ((text: string) => process.stderr.write(text));
493
777
  const setExitCode =
@@ -505,6 +789,7 @@ export async function runBenchCommand(command: BenchCommandArgs, deps: BenchDepe
505
789
  const runtime = await (deps.createRuntime ?? createDefaultRuntime)();
506
790
  try {
507
791
  const targets = resolveBenchModels(command.models, runtime.modelRegistry, runtime.settings, writeStderr);
792
+ if (cacheMode) assertCacheModeSupported(targets);
508
793
  // Explicit `--service-tier` (a single value broadcast across families) wins;
509
794
  // otherwise fall back to the configured per-family `tier.*` settings. Each
510
795
  // model resolves its own family's tier below before reaching the wire.
@@ -538,14 +823,103 @@ export async function runBenchCommand(command: BenchCommandArgs, deps: BenchDepe
538
823
  };
539
824
  results.push(failure);
540
825
  if (!json) writeStdout(`${formatRunLine(failure, 0, runs)}\n`);
541
- reports.push(buildModelReport(selector, model, thinking, results));
826
+ const report = buildModelReport(selector, model, thinking, results);
827
+ if (cacheMode) report.cachePairs = [];
828
+ reports.push(report);
829
+ continue;
830
+ }
831
+
832
+ const serviceTier = resolveModelServiceTier(serviceTierByFamily, model);
833
+ if (cacheMode) {
834
+ const pairs = await runWithConcurrency(
835
+ cachePairs!,
836
+ cacheConcurrency!,
837
+ async (pairIndex): Promise<BenchCachePairReport> => {
838
+ const cacheNamespace = randomSessionId();
839
+ const promptCacheKey = `bench-cache:${cacheNamespace}`;
840
+ const stablePrefix = renderCacheBenchmarkPrefix(cachePrefix!, cacheNamespace);
841
+ const coldSuffix = prompt.render(cacheSuffixTemplate, { variant: "A" }).trim();
842
+ const warmSuffix = prompt.render(cacheSuffixTemplate, { variant: "B" }).trim();
843
+ const coldCapture: CacheRequestCapture = {
844
+ requestIdObserved: false,
845
+ responseCacheHit: false,
846
+ };
847
+ // Keep the gateway's credential selection stable for both phases.
848
+ // Provider-session state is still recreated by runBenchRequest and
849
+ // stateful Responses chaining is disabled below.
850
+ const credentialAffinitySessionId = pairIndex === 0 ? testSessionId : randomSessionId();
851
+ const credentialResolver = runtime.modelRegistry.resolver(model, credentialAffinitySessionId);
852
+ const coldResult = await runBenchRequest(
853
+ model,
854
+ {
855
+ apiKey: credentialResolver,
856
+ sessionId: credentialAffinitySessionId,
857
+ prompt: coldSuffix,
858
+ contextMessages: cacheBenchmarkMessages(stablePrefix, coldSuffix),
859
+ maxTokens,
860
+ reasoning: toReasoningEffort(thinking),
861
+ disableReasoning: shouldDisableReasoning(thinking) ? true : undefined,
862
+ serviceTier,
863
+ promptCacheKey,
864
+ statefulResponses: false,
865
+ cacheCapture: coldCapture,
866
+ },
867
+ streamFn,
868
+ now,
869
+ );
870
+ const warmCapture: CacheRequestCapture = {
871
+ requestIdObserved: false,
872
+ responseCacheHit: false,
873
+ };
874
+ const warmResult = await runBenchRequest(
875
+ model,
876
+ {
877
+ apiKey: credentialResolver,
878
+ sessionId: credentialAffinitySessionId,
879
+ prompt: warmSuffix,
880
+ contextMessages: cacheBenchmarkMessages(stablePrefix, warmSuffix),
881
+ maxTokens,
882
+ reasoning: toReasoningEffort(thinking),
883
+ disableReasoning: shouldDisableReasoning(thinking) ? true : undefined,
884
+ serviceTier,
885
+ promptCacheKey,
886
+ statefulResponses: false,
887
+ cacheCapture: warmCapture,
888
+ },
889
+ streamFn,
890
+ now,
891
+ );
892
+ return {
893
+ cold: cacheRunReport("cold", coldResult, coldCapture),
894
+ warm: cacheRunReport("warm", warmResult, warmCapture),
895
+ coldAlreadyWarm: (coldCapture.usage?.cacheReadTokens ?? 0) > 0 || coldCapture.responseCacheHit,
896
+ stablePrefix: true,
897
+ suffixChanged: true,
898
+ promptCacheKeyStable: true,
899
+ statefulResponsesDisabled: true,
900
+ freshProviderSessionState: true,
901
+ payloadStructureStable:
902
+ coldCapture.payloadStructure === undefined || warmCapture.payloadStructure === undefined
903
+ ? "unavailable"
904
+ : coldCapture.payloadStructure === warmCapture.payloadStructure,
905
+ };
906
+ },
907
+ );
908
+ for (const pair of pairs) results.push(pair.cold.result, pair.warm.result);
909
+ const report = buildModelReport(selector, model, thinking, results);
910
+ report.cachePairs = pairs;
911
+ reports.push(report);
912
+ if (!json) {
913
+ for (const [index, pair] of pairs.entries()) {
914
+ writeStdout(`${formatCachePairLine(pair, index, pairs.length)}\n`);
915
+ }
916
+ }
542
917
  continue;
543
918
  }
544
919
 
545
920
  // We will launch up to `par` workers/requests concurrently.
546
921
  // To keep output clean, non-JSON output emits entries in correct index order.
547
922
  let nextToPrint = 0;
548
-
549
923
  const runWorker = async (index: number) => {
550
924
  const sessionId = index === 0 ? testSessionId : randomSessionId();
551
925
  const result = await runBenchRequest(
@@ -553,56 +927,48 @@ export async function runBenchCommand(command: BenchCommandArgs, deps: BenchDepe
553
927
  {
554
928
  apiKey: runtime.modelRegistry.resolver(model, sessionId),
555
929
  sessionId,
556
- prompt,
930
+ prompt: benchmarkPrompt,
557
931
  maxTokens,
558
932
  reasoning: toReasoningEffort(thinking),
559
933
  disableReasoning: shouldDisableReasoning(thinking) ? true : undefined,
560
- serviceTier: resolveModelServiceTier(serviceTierByFamily, model),
934
+ serviceTier,
561
935
  },
562
936
  streamFn,
563
937
  now,
564
938
  );
565
939
  results[index] = result;
566
940
  };
567
-
568
- // Concurrency-limited running pool
569
941
  const queue = Array.from({ length: runs }, (_, i) => i);
570
942
  const activeWorkers: Promise<void>[] = [];
571
-
572
943
  const processNext = async (): Promise<void> => {
573
944
  if (queue.length === 0) return;
574
945
  const index = queue.shift()!;
575
-
576
- // Pre-print a status update if requested and interactive
577
- if (!json && interactive) {
578
- writeStdout(chalk.dim(` … run ${index + 1}/${runs} streaming\n`));
579
- }
580
-
946
+ if (!json && interactive) writeStdout(chalk.dim(` … run ${index + 1}/${runs} streaming\n`));
581
947
  await runWorker(index);
582
-
583
- // Attempt to print completed results that are in-order
584
948
  if (!json) {
585
949
  while (nextToPrint < runs && results[nextToPrint] !== undefined) {
586
- const res = results[nextToPrint];
587
- writeStdout(`${formatRunLine(res, nextToPrint, runs)}\n`);
950
+ writeStdout(`${formatRunLine(results[nextToPrint], nextToPrint, runs)}\n`);
588
951
  nextToPrint++;
589
952
  }
590
953
  }
591
-
592
954
  await processNext();
593
955
  };
594
-
595
- for (let w = 0; w < Math.min(par, runs); w++) {
596
- activeWorkers.push(processNext());
597
- }
956
+ for (let worker = 0; worker < Math.min(par, runs); worker++) activeWorkers.push(processNext());
598
957
  await Promise.all(activeWorkers);
599
958
  reports.push(buildModelReport(selector, model, thinking, results));
600
959
  }
601
960
  const failures = reports.reduce((sum, report) => sum + report.results.filter(result => !result.ok).length, 0);
602
- const summary: BenchSummary = { runs, maxTokens, models: reports, failures, serviceTierByFamily };
961
+ const summary: BenchSummary = {
962
+ runs,
963
+ maxTokens,
964
+ models: reports,
965
+ failures,
966
+ serviceTierByFamily,
967
+ ...(cacheMode ? { cache: { pairs: cachePairs!, concurrency: cacheConcurrency! } } : {}),
968
+ };
603
969
  if (json) {
604
970
  writeStdout(`${JSON.stringify(summary, null, 2)}\n`);
605
- } else if (reports.length > 1 || runs > 1) {
971
+ } else if (!cacheMode && (reports.length > 1 || runs > 1)) {
606
972
  writeStdout(`\n${formatBenchTable(summary)}`);
607
973
  }
608
974
  if (failures > 0) setExitCode(1);
@@ -117,6 +117,9 @@ export const STRING_SETTERS: Record<string, StringSetter> = {
117
117
  "--config": (result, value) => {
118
118
  result.config = [...(result.config ?? []), value];
119
119
  },
120
+ "--add-dir": (result, value) => {
121
+ result.addDir = [...(result.addDir ?? []), value];
122
+ },
120
123
  "--mode": (result, value) => {
121
124
  if (value === "text" || value === "json" || value === "rpc" || value === "acp" || value === "rpc-ui") {
122
125
  result.mode = value;
package/src/cli/gc-cli.ts CHANGED
@@ -5,10 +5,10 @@ import { gunzipSync, gzipSync } from "node:zlib";
5
5
  import { getAgentDir, getBlobsDir, getHistoryDbPath, getModelDbPath, getSessionsDir } from "@oh-my-pi/pi-utils";
6
6
  import { Settings } from "../config/settings";
7
7
  import { getDefault } from "../config/settings-schema";
8
+ import { BLOB_HASH_RE } from "../session/blob-store";
8
9
  import { listSessionsReadOnly, type SessionInfo, type SessionStatus } from "../session/session-listing";
9
10
  import { FileSessionStorage } from "../session/session-storage";
10
11
 
11
- const HASH_RE = /^[a-f0-9]{64}$/;
12
12
  const BLOB_FILE_RE = /^([a-f0-9]{64})(?:\.[A-Za-z0-9][A-Za-z0-9._-]{0,31})?$/;
13
13
  const BLOB_REF_RE = /\bblob:sha256:([a-f0-9]{64})\b/gi;
14
14
  const JSONL_GLOB = new Bun.Glob("**/*.jsonl");
@@ -268,7 +268,7 @@ async function collectReferencedBlobHashes(sessionRoots: string[]): Promise<Set<
268
268
  const text = await readTextIfPresent(file);
269
269
  for (const match of text.matchAll(BLOB_REF_RE)) {
270
270
  const hash = match[1]?.toLowerCase();
271
- if (hash && HASH_RE.test(hash)) hashes.add(hash);
271
+ if (hash && BLOB_HASH_RE.test(hash)) hashes.add(hash);
272
272
  }
273
273
  }
274
274
  }
@@ -208,14 +208,23 @@ function describeAmount(limit: UsageLimit): string {
208
208
  const amount = limit.amount;
209
209
  const parts: string[] = [];
210
210
  const absoluteUnit = amount.unit !== "percent" && amount.unit !== "unknown";
211
+ const fraction = resolveUsedFraction(limit);
211
212
  if (absoluteUnit && amount.used !== undefined && amount.limit !== undefined) {
212
213
  parts.push(
213
214
  `${formatUnitValue(amount.used, amount.unit)} / ${formatUnitValue(amount.limit, amount.unit)}${UNIT_SUFFIX[amount.unit]}`,
214
215
  );
215
216
  } else if (absoluteUnit && amount.remaining !== undefined) {
216
217
  parts.push(`${formatUnitValue(amount.remaining, amount.unit)}${UNIT_SUFFIX[amount.unit]} left`);
218
+ } else if (
219
+ absoluteUnit &&
220
+ amount.used !== undefined &&
221
+ Number.isFinite(amount.used) &&
222
+ amount.limit === undefined &&
223
+ amount.remaining === undefined &&
224
+ fraction === undefined
225
+ ) {
226
+ parts.push(`${formatUnitValue(amount.used, amount.unit)}${UNIT_SUFFIX[amount.unit]} used`);
217
227
  }
218
- const fraction = resolveUsedFraction(limit);
219
228
  if (fraction !== undefined) {
220
229
  parts.push(`${(fraction * 100).toFixed(1)}% used`);
221
230
  } else if (amount.remainingFraction !== undefined) {
@@ -614,10 +614,11 @@ export class CollabHost {
614
614
  case "kill": {
615
615
  const kill = async () => {
616
616
  const ref = AgentRegistry.global().get(agentId);
617
- if (ref && ref.status === "running" && ref.session) {
617
+ if (!ref) return;
618
+ if (ref.status === "running" && ref.session) {
618
619
  await ref.session.abort({ reason: USER_INTERRUPT_LABEL });
619
620
  }
620
- await AgentLifecycleManager.global().release(agentId);
621
+ await AgentLifecycleManager.global().release(agentId, ref);
621
622
  };
622
623
  kill().catch(fail);
623
624
  break;