@iislee/opencodex 2.11.0 → 2.31.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 (675) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +114 -19
  3. package/bin/ocx.mjs +164 -36
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-BF38heuV.js +104 -0
  6. package/gui/dist/assets/index-DMiI18Kv.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +19 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +376 -52
  44. package/src/adapters/base.ts +54 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +601 -0
  48. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  49. package/src/adapters/cursor/cursor-errors.ts +129 -6
  50. package/src/adapters/cursor/discovery.ts +61 -4
  51. package/src/adapters/cursor/effort-map.ts +27 -3
  52. package/src/adapters/cursor/framing.ts +39 -0
  53. package/src/adapters/cursor/h2-pool.ts +123 -0
  54. package/src/adapters/cursor/http1-bidi.ts +361 -0
  55. package/src/adapters/cursor/images.ts +704 -0
  56. package/src/adapters/cursor/live-models.ts +174 -58
  57. package/src/adapters/cursor/live-transport.ts +609 -170
  58. package/src/adapters/cursor/native-exec-common.ts +23 -2
  59. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  60. package/src/adapters/cursor/native-exec-network.ts +1 -1
  61. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  62. package/src/adapters/cursor/native-exec.ts +101 -14
  63. package/src/adapters/cursor/protobuf-events.ts +829 -11
  64. package/src/adapters/cursor/protobuf-request.ts +383 -65
  65. package/src/adapters/cursor/request-builder.ts +237 -31
  66. package/src/adapters/cursor/tool-definitions.ts +242 -12
  67. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  68. package/src/adapters/cursor/transport.ts +22 -0
  69. package/src/adapters/cursor/types.ts +28 -1
  70. package/src/adapters/cursor.ts +133 -7
  71. package/src/adapters/google-antigravity-replay.ts +558 -34
  72. package/src/adapters/google-antigravity-wire.ts +43 -10
  73. package/src/adapters/google-http.ts +38 -10
  74. package/src/adapters/google-tool-schema.ts +80 -15
  75. package/src/adapters/google-truncation.ts +11 -0
  76. package/src/adapters/google.ts +618 -74
  77. package/src/adapters/identity.ts +39 -6
  78. package/src/adapters/kiro-errors.ts +11 -0
  79. package/src/adapters/kiro-events.ts +19 -1
  80. package/src/adapters/kiro-thinking.ts +18 -2
  81. package/src/adapters/kiro-tools.ts +10 -1
  82. package/src/adapters/kiro.ts +92 -54
  83. package/src/adapters/mimo-free.ts +17 -0
  84. package/src/adapters/openai-chat-url.ts +11 -0
  85. package/src/adapters/openai-chat.ts +1311 -336
  86. package/src/adapters/openai-responses-url.ts +16 -0
  87. package/src/adapters/openai-responses.ts +830 -56
  88. package/src/adapters/registry.ts +175 -0
  89. package/src/adapters/responses-tool-schema.ts +67 -0
  90. package/src/adapters/tool-call-id.ts +119 -0
  91. package/src/adapters/tool-catalog-nudge.ts +104 -21
  92. package/src/adapters/xai-web-search.ts +185 -0
  93. package/src/bridge.ts +428 -61
  94. package/src/chat/inbound.ts +43 -19
  95. package/src/chat/outbound.ts +82 -26
  96. package/src/claude/agents-inject.ts +32 -9
  97. package/src/claude/context-windows.ts +21 -5
  98. package/src/claude/desktop-3p.ts +243 -9
  99. package/src/claude/gateway-cache.ts +41 -4
  100. package/src/claude/inbound.ts +72 -3
  101. package/src/claude/model-info.ts +38 -15
  102. package/src/claude/outbound.ts +70 -16
  103. package/src/cli/account-api.ts +35 -1
  104. package/src/cli/account-auth.ts +33 -6
  105. package/src/cli/account-catalog-refresh.ts +14 -0
  106. package/src/cli/account-extended.ts +389 -2
  107. package/src/cli/account-main.ts +317 -0
  108. package/src/cli/account.ts +30 -6
  109. package/src/cli/agent.ts +45 -1
  110. package/src/cli/claude-agent-startup-sync.ts +73 -0
  111. package/src/cli/claude-desktop.ts +29 -4
  112. package/src/cli/claude.ts +103 -34
  113. package/src/cli/codex-log-guard-doctor.ts +103 -0
  114. package/src/cli/codex-shim-autorestore.ts +2 -0
  115. package/src/cli/codex-shim-readiness.ts +76 -0
  116. package/src/cli/combo.ts +8 -0
  117. package/src/cli/config-command.ts +74 -10
  118. package/src/cli/dispatch.ts +593 -0
  119. package/src/cli/doctor.ts +315 -43
  120. package/src/cli/ensure-desired-integrations.ts +152 -0
  121. package/src/cli/export-command.ts +46 -20
  122. package/src/cli/help.ts +20 -271
  123. package/src/cli/index.ts +378 -519
  124. package/src/cli/init.ts +4 -17
  125. package/src/cli/integrations.ts +120 -2
  126. package/src/cli/lab.ts +607 -0
  127. package/src/cli/launcher-context.ts +77 -0
  128. package/src/cli/minimax.ts +497 -0
  129. package/src/cli/models-runtime.ts +35 -2
  130. package/src/cli/models.ts +100 -14
  131. package/src/cli/observe.ts +92 -3
  132. package/src/cli/opencode.ts +4 -2
  133. package/src/cli/provider-runtime.ts +18 -1
  134. package/src/cli/provider.ts +24 -3
  135. package/src/cli/ready.ts +301 -0
  136. package/src/cli/registry.ts +437 -0
  137. package/src/cli/root.ts +86 -0
  138. package/src/cli/route-policy.ts +92 -0
  139. package/src/cli/runtime-api.ts +6 -3
  140. package/src/cli/star-prompt.ts +71 -15
  141. package/src/cli/status.ts +10 -3
  142. package/src/cli/system-restart-client.ts +146 -0
  143. package/src/cli/tray-proxy.ts +153 -6
  144. package/src/cli/v2.ts +105 -10
  145. package/src/cli.ts +1 -1
  146. package/src/clients/config-export.ts +1358 -21
  147. package/src/codex/account-label.ts +14 -1
  148. package/src/codex/account-lifecycle.ts +130 -13
  149. package/src/codex/account-namespaces.ts +49 -3
  150. package/src/codex/account-priority.ts +83 -0
  151. package/src/codex/account-store.ts +29 -2
  152. package/src/codex/account-usability.ts +25 -2
  153. package/src/codex/admission.ts +256 -0
  154. package/src/codex/affinity-debug.ts +162 -0
  155. package/src/codex/app-server-processes.ts +493 -106
  156. package/src/codex/app-server-restart-service.ts +232 -0
  157. package/src/codex/auth-api.ts +849 -242
  158. package/src/codex/auth-collision.ts +5 -3
  159. package/src/codex/auth-context.ts +345 -32
  160. package/src/codex/autostart-health.ts +8 -1
  161. package/src/codex/catalog/account-models.ts +67 -0
  162. package/src/codex/catalog/aggregation.ts +68 -10
  163. package/src/codex/catalog/bundled.ts +331 -33
  164. package/src/codex/catalog/effort.ts +121 -30
  165. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  166. package/src/codex/catalog/kinds.ts +2 -0
  167. package/src/codex/catalog/metadata.ts +529 -45
  168. package/src/codex/catalog/native-models.ts +72 -0
  169. package/src/codex/catalog/parsing.ts +224 -30
  170. package/src/codex/catalog/provider-fetch.ts +1460 -134
  171. package/src/codex/catalog/sync.ts +1449 -186
  172. package/src/codex/catalog-admission.ts +199 -0
  173. package/src/codex/catalog-refresh-status.ts +105 -0
  174. package/src/codex/catalog-write-serialization.ts +242 -0
  175. package/src/codex/catalog.ts +6 -3
  176. package/src/codex/codex-write-lock.ts +384 -0
  177. package/src/codex/convergence-types.ts +614 -0
  178. package/src/codex/convergence.ts +651 -0
  179. package/src/codex/coordinator-doctor.ts +332 -0
  180. package/src/codex/custom-model-catalog-migration.ts +176 -0
  181. package/src/codex/desired-state.ts +230 -0
  182. package/src/codex/features.ts +636 -39
  183. package/src/codex/generation.ts +202 -0
  184. package/src/codex/history-job.ts +407 -0
  185. package/src/codex/history-lock.ts +242 -0
  186. package/src/codex/history-migration-guardian.ts +26 -20
  187. package/src/codex/history-provider.ts +231 -28
  188. package/src/codex/history-transition.ts +105 -0
  189. package/src/codex/history-worker.ts +220 -0
  190. package/src/codex/inject-coordination.ts +290 -0
  191. package/src/codex/inject.ts +1073 -152
  192. package/src/codex/injected-marker.ts +37 -3
  193. package/src/codex/integration-record.ts +266 -0
  194. package/src/codex/internal/catalog-writer.ts +203 -0
  195. package/src/codex/internal/history-writer.ts +80 -0
  196. package/src/codex/journal.ts +66 -4
  197. package/src/codex/log-guard/inspect.ts +506 -0
  198. package/src/codex/log-guard/lock.ts +150 -0
  199. package/src/codex/log-guard/maintenance.ts +403 -0
  200. package/src/codex/log-guard/path-safety.ts +88 -0
  201. package/src/codex/log-guard/policy.ts +44 -0
  202. package/src/codex/log-guard/processes.ts +205 -0
  203. package/src/codex/log-guard/protection.ts +489 -0
  204. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  205. package/src/codex/main-account-cache.ts +24 -0
  206. package/src/codex/main-account.ts +29 -1
  207. package/src/codex/management-convergence.ts +167 -0
  208. package/src/codex/model-cache.ts +56 -10
  209. package/src/codex/model-entitlements.ts +353 -0
  210. package/src/codex/native-main-admission.ts +47 -0
  211. package/src/codex/native-main-auth-temp.ts +187 -0
  212. package/src/codex/native-main-claim.ts +178 -0
  213. package/src/codex/native-main-lock-file.ts +162 -0
  214. package/src/codex/native-main-owner.ts +329 -0
  215. package/src/codex/native-profile-api.ts +247 -0
  216. package/src/codex/native-profile-manager.ts +1531 -0
  217. package/src/codex/native-profile-processes.ts +121 -0
  218. package/src/codex/native-profile-recovery.ts +99 -0
  219. package/src/codex/native-profile-stage-store.ts +387 -0
  220. package/src/codex/native-profile-startup.ts +492 -0
  221. package/src/codex/native-profile-store.ts +855 -0
  222. package/src/codex/native-profile-types.ts +120 -0
  223. package/src/codex/native-residue.ts +682 -0
  224. package/src/codex/paths.ts +80 -1
  225. package/src/codex/plan-from-token.ts +140 -0
  226. package/src/codex/plan.ts +40 -0
  227. package/src/codex/plugins-doctor.ts +1 -1
  228. package/src/codex/pool-rotation.ts +74 -4
  229. package/src/codex/project-config-warnings.ts +20 -6
  230. package/src/codex/prompt-journal.ts +352 -0
  231. package/src/codex/prompt-layers.ts +967 -0
  232. package/src/codex/prompt-lock.ts +143 -0
  233. package/src/codex/quota-rejection.ts +298 -0
  234. package/src/codex/quota.ts +175 -13
  235. package/src/codex/refresh.ts +11 -2
  236. package/src/codex/reset-credit-recovery.ts +1044 -0
  237. package/src/codex/routing.ts +505 -94
  238. package/src/codex/runtime.ts +159 -38
  239. package/src/codex/shim.ts +1009 -28
  240. package/src/codex/subagent-model-fallback.ts +350 -35
  241. package/src/codex/sync.ts +191 -2
  242. package/src/codex/transition-state.ts +612 -0
  243. package/src/codex/upstream-host-health.ts +368 -0
  244. package/src/codex/user-identity.ts +557 -0
  245. package/src/codex/warmup.ts +187 -81
  246. package/src/codex/write-coordination.ts +114 -0
  247. package/src/combos/failover.ts +20 -0
  248. package/src/combos/index.ts +4 -0
  249. package/src/combos/request.ts +32 -0
  250. package/src/combos/types.ts +81 -9
  251. package/src/config/provider-name.ts +24 -0
  252. package/src/config.ts +1762 -140
  253. package/src/generated/compatibility-version.json +3164 -0
  254. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  255. package/src/grok/inject.ts +16 -5
  256. package/src/grok/inspect.ts +45 -0
  257. package/src/grok/sync.ts +2 -2
  258. package/src/images/loop.ts +152 -29
  259. package/src/images/plan.ts +23 -13
  260. package/src/integrations/config-io.ts +269 -0
  261. package/src/integrations/journal.ts +315 -0
  262. package/src/integrations/merge.ts +135 -0
  263. package/src/integrations/mutation-flight.ts +71 -0
  264. package/src/integrations/native/ownership-preflight.ts +202 -0
  265. package/src/integrations/omp-yaml-source.ts +358 -0
  266. package/src/integrations/owned-refresh.ts +74 -0
  267. package/src/integrations/ownership.ts +111 -0
  268. package/src/integrations/registry.ts +159 -0
  269. package/src/integrations/serialize.ts +314 -0
  270. package/src/integrations/state.ts +361 -0
  271. package/src/integrations/store.ts +103 -0
  272. package/src/integrations/writer-lock.ts +98 -0
  273. package/src/integrations/writer.ts +691 -0
  274. package/src/lab/artifacts/sanitize.ts +586 -0
  275. package/src/lab/artifacts/secure-fs.ts +475 -0
  276. package/src/lab/artifacts/store.ts +310 -0
  277. package/src/lab/automation/budgets.ts +78 -0
  278. package/src/lab/automation/config-persistence.ts +256 -0
  279. package/src/lab/automation/constants.ts +39 -0
  280. package/src/lab/automation/cooldown.ts +103 -0
  281. package/src/lab/automation/dispatch.ts +211 -0
  282. package/src/lab/automation/index.ts +13 -0
  283. package/src/lab/automation/orchestrator.ts +499 -0
  284. package/src/lab/automation/persistence.ts +512 -0
  285. package/src/lab/automation/planner.ts +371 -0
  286. package/src/lab/automation/policy.ts +136 -0
  287. package/src/lab/automation/queue.ts +191 -0
  288. package/src/lab/automation/recovery.ts +24 -0
  289. package/src/lab/automation/route-context.ts +21 -0
  290. package/src/lab/automation/run-key.ts +44 -0
  291. package/src/lab/automation/runs-query.ts +34 -0
  292. package/src/lab/automation/types.ts +160 -0
  293. package/src/lab/conformance/assertion.ts +325 -0
  294. package/src/lab/conformance/digest.ts +22 -0
  295. package/src/lab/conformance/executor.ts +741 -0
  296. package/src/lab/conformance/fixture-provider.ts +27 -0
  297. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  298. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  299. package/src/lab/conformance/harness-budget.ts +47 -0
  300. package/src/lab/conformance/index.ts +5 -0
  301. package/src/lab/conformance/jcs.ts +64 -0
  302. package/src/lab/conformance/json-pointer.ts +39 -0
  303. package/src/lab/conformance/manifest.ts +180 -0
  304. package/src/lab/conformance/mcp-stub.ts +179 -0
  305. package/src/lab/conformance/negative-controls.ts +164 -0
  306. package/src/lab/conformance/observation.ts +355 -0
  307. package/src/lab/conformance/runner.ts +68 -0
  308. package/src/lab/conformance/sse-normalize.ts +59 -0
  309. package/src/lab/conformance/suite-manifest.ts +78 -0
  310. package/src/lab/conformance/types.ts +214 -0
  311. package/src/lab/constants.ts +126 -0
  312. package/src/lab/digest.ts +64 -0
  313. package/src/lab/events/errors.ts +9 -0
  314. package/src/lab/events/limits.ts +117 -0
  315. package/src/lab/events/types.ts +229 -0
  316. package/src/lab/events/validate.ts +781 -0
  317. package/src/lab/fabric/constants.ts +40 -0
  318. package/src/lab/fabric/executor.ts +492 -0
  319. package/src/lab/fabric/index.ts +80 -0
  320. package/src/lab/fabric/manifest.ts +222 -0
  321. package/src/lab/fabric/observe.ts +489 -0
  322. package/src/lab/fabric/patch.ts +79 -0
  323. package/src/lab/fabric/producer-child.ts +139 -0
  324. package/src/lab/fabric/producer-isolate.ts +276 -0
  325. package/src/lab/fabric/producer-protocol.ts +61 -0
  326. package/src/lab/fabric/scratch.ts +439 -0
  327. package/src/lab/fabric/subject.ts +106 -0
  328. package/src/lab/fabric/types.ts +134 -0
  329. package/src/lab/fabric/verifier.ts +98 -0
  330. package/src/lab/index.ts +54 -0
  331. package/src/lab/ledger/artifact-refs.ts +127 -0
  332. package/src/lab/ledger/invalidation.ts +136 -0
  333. package/src/lab/ledger/purge.ts +310 -0
  334. package/src/lab/ledger/store.ts +532 -0
  335. package/src/lab/live/credential-lease.ts +53 -0
  336. package/src/lab/live/destination.ts +155 -0
  337. package/src/lab/live/executor.ts +336 -0
  338. package/src/lab/live/inert-tools.ts +56 -0
  339. package/src/lab/live/manifest.ts +85 -0
  340. package/src/lab/live/mcp-loopback.ts +57 -0
  341. package/src/lab/live/runner.ts +19 -0
  342. package/src/lab/live/sandbox.ts +61 -0
  343. package/src/lab/live/suite-manifest.ts +41 -0
  344. package/src/lab/live/transport.ts +118 -0
  345. package/src/lab/live/types.ts +197 -0
  346. package/src/lab/observe/from-conformance.ts +301 -0
  347. package/src/lab/observe/from-live.ts +117 -0
  348. package/src/lab/paths.ts +153 -0
  349. package/src/lab/projection/rebuild.ts +495 -0
  350. package/src/lab/projection/schema.ts +135 -0
  351. package/src/lab/projection/verdicts.ts +474 -0
  352. package/src/lab/projection/verification.ts +412 -0
  353. package/src/lab/public/bundle.ts +217 -0
  354. package/src/lab/public/community-authority.ts +175 -0
  355. package/src/lab/public/community-files.ts +29 -0
  356. package/src/lab/public/community.ts +479 -0
  357. package/src/lab/public/file-safety.ts +155 -0
  358. package/src/lab/public/ids.ts +26 -0
  359. package/src/lab/public/index.ts +16 -0
  360. package/src/lab/public/mutation-lock.ts +424 -0
  361. package/src/lab/public/operator.ts +353 -0
  362. package/src/lab/public/origin-purge.ts +79 -0
  363. package/src/lab/public/origin.ts +203 -0
  364. package/src/lab/public/privacy.ts +143 -0
  365. package/src/lab/public/private-file.ts +261 -0
  366. package/src/lab/public/project.ts +124 -0
  367. package/src/lab/public/purge-test-fault.ts +21 -0
  368. package/src/lab/public/purge.ts +223 -0
  369. package/src/lab/public/registry.ts +44 -0
  370. package/src/lab/public/revocation.ts +252 -0
  371. package/src/lab/public/signature.ts +243 -0
  372. package/src/lab/public/storage.ts +105 -0
  373. package/src/lab/public/strict-json.ts +206 -0
  374. package/src/lab/public/time.ts +26 -0
  375. package/src/lab/public/types.ts +172 -0
  376. package/src/lab/public/validate.ts +391 -0
  377. package/src/lab/query/catalog.ts +101 -0
  378. package/src/lab/query/connection.ts +107 -0
  379. package/src/lab/query/constants.ts +4 -0
  380. package/src/lab/query/cursor.ts +132 -0
  381. package/src/lab/query/dto-map.ts +277 -0
  382. package/src/lab/query/errors.ts +22 -0
  383. package/src/lab/query/freshness.ts +53 -0
  384. package/src/lab/query/index.ts +45 -0
  385. package/src/lab/query/latest-observation.ts +59 -0
  386. package/src/lab/query/passive-production.ts +159 -0
  387. package/src/lab/query/queries.ts +444 -0
  388. package/src/lab/query/types.ts +266 -0
  389. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  390. package/src/lab/subject/installation-salt.ts +112 -0
  391. package/src/lab/subject/protocol-subject.ts +80 -0
  392. package/src/lab/subject/route-subject.ts +74 -0
  393. package/src/lib/app-owned-memory-stores.ts +22 -0
  394. package/src/lib/bounded-body.ts +153 -9
  395. package/src/lib/bun-runtime.ts +125 -12
  396. package/src/lib/bun-stream-caps.ts +13 -9
  397. package/src/lib/codex-restart-contract.ts +120 -0
  398. package/src/lib/config-ownership.ts +6 -2
  399. package/src/lib/destination-policy.ts +65 -1
  400. package/src/lib/errors.ts +44 -2
  401. package/src/lib/fabric-task-execution-authority.ts +7 -0
  402. package/src/lib/fabric-task-host.ts +29 -0
  403. package/src/lib/lab-activation.ts +223 -0
  404. package/src/lib/lab-live-execution-authority.ts +13 -0
  405. package/src/lib/lab-live-host.ts +30 -0
  406. package/src/lib/lab-live-pinned-sender.ts +56 -0
  407. package/src/lib/lab-live-route-production.ts +130 -0
  408. package/src/lib/lab-passive-linker-registration.ts +26 -0
  409. package/src/lib/local-management-attestation.ts +51 -0
  410. package/src/lib/local-management-capability.ts +100 -0
  411. package/src/lib/local-provider-reload-contract.ts +100 -0
  412. package/src/lib/optional-shutdown-hooks.ts +57 -0
  413. package/src/lib/pinned-http.ts +145 -26
  414. package/src/lib/process-control.ts +4 -1
  415. package/src/lib/provider-outbound.ts +49 -9
  416. package/src/lib/redact.ts +419 -3
  417. package/src/lib/self-launch-argv.ts +15 -0
  418. package/src/lib/server-resource-ownership.ts +71 -0
  419. package/src/lib/shadow-call.ts +35 -4
  420. package/src/lib/sse-decoder.ts +41 -0
  421. package/src/lib/state-store-registrations.ts +10 -2
  422. package/src/lib/system-restart-contract.ts +73 -0
  423. package/src/lib/token-estimate.ts +19 -2
  424. package/src/lib/tool-argument-integers.ts +202 -0
  425. package/src/lib/translator-budget.ts +44 -0
  426. package/src/lib/upstream-http-version.ts +57 -0
  427. package/src/lib/upstream-reachability.ts +95 -0
  428. package/src/lib/upstream-retry.ts +156 -3
  429. package/src/lib/windows-atomic-replace.ts +155 -0
  430. package/src/lib/windows-elevation.ts +70 -2
  431. package/src/lib/windows-secret-acl.ts +409 -69
  432. package/src/lib/windows-service-wrappers.ts +72 -0
  433. package/src/lib/windows-text.ts +106 -0
  434. package/src/lib/windows-user-principal.ts +341 -0
  435. package/src/lib/winsw.ts +33 -5
  436. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  437. package/src/oauth/account-import/index.ts +15 -0
  438. package/src/oauth/account-import/parser.ts +83 -0
  439. package/src/oauth/account-import/registry.ts +18 -0
  440. package/src/oauth/account-import/service.ts +75 -0
  441. package/src/oauth/account-import/types.ts +91 -0
  442. package/src/oauth/anthropic.ts +12 -1
  443. package/src/oauth/callback-server.ts +8 -2
  444. package/src/oauth/chatgpt.ts +12 -1
  445. package/src/oauth/command-code.ts +239 -0
  446. package/src/oauth/cursor.ts +46 -5
  447. package/src/oauth/google-antigravity.ts +35 -3
  448. package/src/oauth/health.ts +20 -12
  449. package/src/oauth/index.ts +398 -66
  450. package/src/oauth/key-providers.ts +16 -0
  451. package/src/oauth/kimi.ts +16 -2
  452. package/src/oauth/kiro.ts +50 -6
  453. package/src/oauth/local-token-detect.ts +11 -2
  454. package/src/oauth/log.ts +3 -1
  455. package/src/oauth/login-cli.ts +88 -28
  456. package/src/oauth/nous.ts +798 -0
  457. package/src/oauth/store.ts +119 -21
  458. package/src/oauth/token-guardian.ts +9 -3
  459. package/src/pi/models.ts +2 -2
  460. package/src/providers/alibaba-region-migration.ts +1 -1
  461. package/src/providers/antigravity-models.ts +521 -31
  462. package/src/providers/base-url-choices.ts +10 -0
  463. package/src/providers/codex-capacity.ts +292 -0
  464. package/src/providers/command-code-efforts.ts +144 -0
  465. package/src/providers/context-cap.ts +22 -5
  466. package/src/providers/cursor-pool.ts +72 -0
  467. package/src/providers/derive.ts +253 -6
  468. package/src/providers/fastwire.ts +501 -0
  469. package/src/providers/free-directory.ts +10 -7
  470. package/src/providers/google-vertex-location.ts +14 -0
  471. package/src/providers/key-failover.ts +71 -3
  472. package/src/providers/label.ts +1 -1
  473. package/src/providers/model-discovery-limits.ts +16 -0
  474. package/src/providers/model-discovery.ts +115 -22
  475. package/src/providers/model-rename-migration.ts +255 -0
  476. package/src/providers/model-rename-startup.ts +28 -0
  477. package/src/providers/openai-sidecar.ts +72 -4
  478. package/src/providers/openai-tier-startup.ts +31 -2
  479. package/src/providers/openai-tiers.ts +119 -4
  480. package/src/providers/openai-virtual-models.ts +1 -0
  481. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  482. package/src/providers/provider-id-rewrite.ts +29 -0
  483. package/src/providers/quota.ts +1319 -38
  484. package/src/providers/registry.ts +1429 -111
  485. package/src/providers/request-pacing.ts +310 -0
  486. package/src/providers/service-tier.ts +277 -0
  487. package/src/providers/slug-codec.ts +42 -6
  488. package/src/providers/static-model-discovery.ts +86 -0
  489. package/src/providers/xai-responses-opt-in.ts +15 -0
  490. package/src/providers/xai-transport.ts +11 -4
  491. package/src/reasoning-effort.ts +49 -1
  492. package/src/responses/compaction.ts +26 -1
  493. package/src/responses/custom-tool-compat.ts +266 -0
  494. package/src/responses/hosted-tool-policy.ts +9 -0
  495. package/src/responses/namespace-tool-compat.ts +355 -0
  496. package/src/responses/parser.ts +220 -38
  497. package/src/responses/provider-continuation.ts +98 -0
  498. package/src/responses/provider-opaque-metadata.ts +73 -0
  499. package/src/responses/reasoning-envelope.ts +9 -1
  500. package/src/responses/reasoning-replay-cache.ts +426 -0
  501. package/src/responses/schema.ts +7 -1
  502. package/src/responses/spill-store.ts +75 -10
  503. package/src/responses/state.ts +565 -27
  504. package/src/responses/thought-signature-replay.ts +347 -0
  505. package/src/responses/tool-search-compat.ts +301 -0
  506. package/src/responses/truncated-stop-reason.ts +60 -0
  507. package/src/router.ts +366 -30
  508. package/src/routing/analytics.ts +378 -0
  509. package/src/routing/capability.ts +244 -0
  510. package/src/routing/compatibility/assemble.ts +73 -0
  511. package/src/routing/compatibility/behavior.ts +278 -0
  512. package/src/routing/compatibility/catalog.ts +99 -0
  513. package/src/routing/compatibility/endpoint.ts +52 -0
  514. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  515. package/src/routing/compatibility/policy.ts +181 -0
  516. package/src/routing/compatibility/provider-slot.ts +56 -0
  517. package/src/routing/compatibility/reader.ts +110 -0
  518. package/src/routing/compatibility/subject.ts +191 -0
  519. package/src/routing/compatibility/types.ts +64 -0
  520. package/src/routing/compatibility/version.ts +104 -0
  521. package/src/routing/cost.ts +77 -0
  522. package/src/routing/evaluator.ts +495 -0
  523. package/src/routing/health.ts +412 -0
  524. package/src/routing/history/cursor.ts +43 -0
  525. package/src/routing/history/indexer.ts +605 -0
  526. package/src/routing/history/schema.ts +72 -0
  527. package/src/routing/profile-namespace.ts +15 -0
  528. package/src/routing/profile.ts +547 -0
  529. package/src/routing/quota.ts +145 -0
  530. package/src/routing/request-evidence.ts +45 -0
  531. package/src/routing/trace.ts +776 -0
  532. package/src/server/adapter-resolve.ts +2 -29
  533. package/src/server/auth-cors.ts +267 -46
  534. package/src/server/background-lifecycle.ts +182 -0
  535. package/src/server/chat-completions.ts +130 -56
  536. package/src/server/chat-native-sse.ts +331 -0
  537. package/src/server/chat-native.ts +426 -0
  538. package/src/server/claude-messages.ts +159 -43
  539. package/src/server/direct-local-http.ts +347 -0
  540. package/src/server/effort-policy.ts +18 -0
  541. package/src/server/github-copilot-responses-repair.ts +338 -0
  542. package/src/server/gui-static.ts +39 -10
  543. package/src/server/images.ts +94 -12
  544. package/src/server/index.ts +865 -181
  545. package/src/server/lifecycle.ts +284 -13
  546. package/src/server/live.ts +136 -17
  547. package/src/server/local-management-read-client.ts +90 -0
  548. package/src/server/local-provider-reload-client.ts +137 -0
  549. package/src/server/management/agent-settings-routes.ts +398 -103
  550. package/src/server/management/api-key-usage.ts +31 -5
  551. package/src/server/management/body.ts +6 -0
  552. package/src/server/management/combo-routes.ts +62 -24
  553. package/src/server/management/config-routes.ts +464 -51
  554. package/src/server/management/context.ts +80 -2
  555. package/src/server/management/integration-routes.ts +498 -0
  556. package/src/server/management/lab-automation-routes.ts +206 -0
  557. package/src/server/management/lab-routes.ts +563 -0
  558. package/src/server/management/logs-usage-routes.ts +101 -32
  559. package/src/server/management/model-routes.ts +189 -131
  560. package/src/server/management/model-rows.ts +163 -0
  561. package/src/server/management/native-integration-routes.ts +769 -0
  562. package/src/server/management/oauth-account-routes.ts +80 -4
  563. package/src/server/management/provider-capability-config.ts +48 -0
  564. package/src/server/management/provider-routes.ts +764 -157
  565. package/src/server/management/request-history-routes.ts +191 -0
  566. package/src/server/management/routing-analytics-routes.ts +74 -0
  567. package/src/server/management/routing-profile-routes.ts +380 -0
  568. package/src/server/management/shared.ts +27 -11
  569. package/src/server/management/sidebar-routes.ts +47 -31
  570. package/src/server/management/storage-log-guard-routes.ts +186 -0
  571. package/src/server/management/sync-response.ts +69 -0
  572. package/src/server/management/system-restart.ts +288 -32
  573. package/src/server/management/system-routes.ts +77 -0
  574. package/src/server/management/usage-summary-cache.ts +9 -1
  575. package/src/server/management/vision-sidecar-options.ts +167 -0
  576. package/src/server/management/web-search-sidecar-options.ts +120 -0
  577. package/src/server/management-api.ts +115 -14
  578. package/src/server/management-auth.ts +220 -5
  579. package/src/server/passive-route-linker.ts +66 -0
  580. package/src/server/ports.ts +41 -1
  581. package/src/server/proxy-liveness.ts +132 -5
  582. package/src/server/readiness.ts +99 -0
  583. package/src/server/relay-eager.ts +82 -42
  584. package/src/server/relay.ts +236 -76
  585. package/src/server/request-decompress.ts +113 -6
  586. package/src/server/request-log.ts +235 -22
  587. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  588. package/src/server/responses/agent-task-recovery.ts +465 -0
  589. package/src/server/responses/collaboration.ts +204 -35
  590. package/src/server/responses/compact.ts +442 -55
  591. package/src/server/responses/core.ts +2872 -331
  592. package/src/server/responses/empty-completion-guard.ts +276 -0
  593. package/src/server/responses/encrypted-payload.ts +62 -39
  594. package/src/server/responses/fetch-helpers.ts +79 -4
  595. package/src/server/responses/input-admission.ts +185 -0
  596. package/src/server/responses/pacing-overload.ts +13 -0
  597. package/src/server/responses/policy-fallback.ts +178 -0
  598. package/src/server/responses/responses-field-backfill.ts +251 -0
  599. package/src/server/responses/terminal-guard.ts +26 -5
  600. package/src/server/responses/upstream-error.ts +5 -0
  601. package/src/server/responses/ws-upstream.ts +308 -0
  602. package/src/server/responses-custom-tool-repair.ts +282 -0
  603. package/src/server/responses-item-id-repair.ts +54 -6
  604. package/src/server/responses-json-events.ts +90 -0
  605. package/src/server/responses-model-rewrite.ts +29 -0
  606. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  607. package/src/server/responses-snapshot-repair.ts +621 -0
  608. package/src/server/responses-terminal-repair.ts +342 -0
  609. package/src/server/responses-tool-search-repair.ts +267 -0
  610. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  611. package/src/server/responses.ts +18 -2
  612. package/src/server/search.ts +78 -13
  613. package/src/server/sse-frame-buffer.ts +292 -0
  614. package/src/server/sse-payload-rewrite.ts +110 -22
  615. package/src/server/startup-action-control.ts +8 -1
  616. package/src/server/startup-health-cache.ts +19 -1
  617. package/src/server/system-env.ts +80 -9
  618. package/src/server/ws-bridge.ts +39 -38
  619. package/src/service-manager-probe.ts +892 -0
  620. package/src/service.ts +1111 -90
  621. package/src/sidecar/auth.ts +92 -0
  622. package/src/sidecar/candidates.ts +83 -0
  623. package/src/storage/cleanup.ts +2 -2
  624. package/src/storage/scanner.ts +1 -1
  625. package/src/storage/worker-lifecycle.ts +14 -14
  626. package/src/tray/windows-tray.ps1 +83 -9
  627. package/src/tray/windows.ts +43 -16
  628. package/src/types/accounts.ts +37 -0
  629. package/src/types/config.ts +845 -0
  630. package/src/types/provider.ts +545 -0
  631. package/src/types/request.ts +384 -0
  632. package/src/types/tools.ts +131 -0
  633. package/src/types/wire.ts +80 -0
  634. package/src/types.ts +104 -1236
  635. package/src/update/index.ts +32 -19
  636. package/src/update/job.ts +442 -67
  637. package/src/update/notify.ts +12 -6
  638. package/src/update/npm-cache-preflight.d.mts +47 -0
  639. package/src/update/npm-cache-preflight.mjs +201 -0
  640. package/src/update/transactional-install.d.mts +22 -0
  641. package/src/update/transactional-install.mjs +259 -0
  642. package/src/usage/cost.ts +0 -0
  643. package/src/usage/expected-prices.ts +268 -16
  644. package/src/usage/log.ts +606 -41
  645. package/src/usage/summary.ts +177 -9
  646. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  647. package/src/usage/user-cost-overlays.ts +314 -0
  648. package/src/vision/anthropic-describe.ts +10 -6
  649. package/src/vision/backends.ts +97 -0
  650. package/src/vision/describe.ts +9 -3
  651. package/src/vision/eligibility.ts +250 -0
  652. package/src/vision/index.ts +238 -24
  653. package/src/vision/reasoning.ts +55 -0
  654. package/src/vision/routed-describe.ts +175 -0
  655. package/src/vision/timeout-bounds.ts +9 -0
  656. package/src/web-search/anthropic-executor.ts +13 -7
  657. package/src/web-search/backends.ts +108 -0
  658. package/src/web-search/exa-executor.ts +88 -0
  659. package/src/web-search/executor.ts +11 -3
  660. package/src/web-search/gemini-executor.ts +141 -0
  661. package/src/web-search/index.ts +150 -15
  662. package/src/web-search/loop.ts +279 -50
  663. package/src/web-search/parse.ts +125 -30
  664. package/src/web-search/sources.ts +60 -0
  665. package/src/web-search/xai-executor.ts +219 -0
  666. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  667. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  668. package/gui/dist/provider-icons/antigravity.svg +0 -1
  669. package/gui/dist/provider-icons/claude.svg +0 -1
  670. package/gui/dist/provider-icons/copilot.svg +0 -1
  671. package/gui/dist/provider-icons/cursor.svg +0 -2
  672. package/gui/dist/provider-icons/gemini.svg +0 -1
  673. package/gui/dist/provider-icons/grok-color.svg +0 -1
  674. package/gui/dist/provider-icons/kiro.svg +0 -14
  675. package/src/cli/internal-dispatch.ts +0 -20
package/src/usage/log.ts CHANGED
@@ -1,19 +1,36 @@
1
+ import { createHash, type Hash } from "node:crypto";
1
2
  import { chmodSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, appendFileSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import { getConfigDir } from "../config";
5
+ import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
4
6
  import { recordOwnedConfigPath } from "../lib/config-ownership";
7
+ import { sanitizeLogMetadataString } from "../lib/redact";
5
8
  import { usageDisplayTotalTokens } from "./totals";
6
- import type { OcxUsage } from "../types";
9
+ import type { AttemptTierOutcome, OcxUsage } from "../types";
10
+ import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routing/trace";
11
+ import { CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
7
12
 
8
13
  export type UsageStatus = "reported" | "unreported" | "unsupported" | "estimated";
14
+ export type CodexUsageAccountLogLabel = "main" | `p${string}`;
9
15
 
16
+ export function isCodexUsageAccountLogLabel(value: unknown): value is CodexUsageAccountLogLabel {
17
+ return value === "main" || (typeof value === "string" && CODEX_ACCOUNT_LOG_LABEL_RE.test(value));
18
+ }
19
+
20
+ /**
21
+ * Recovery kinds recorded per attempt in the usage log; the GUI renders localized labels
22
+ * for these wire values.
23
+ */
10
24
  export type AttemptRecoveryKind =
11
25
  | "transient-5xx"
12
26
  | "connection-reset"
13
27
  | "oauth-401"
14
28
  | "key-429"
29
+ | "rate-limit-429"
15
30
  | "anthropic-oauth-429"
16
- | "image-413";
31
+ | "image-413"
32
+ | "opaque-blob-rejection"
33
+ | "empty-completion";
17
34
 
18
35
  export interface PersistedUsageAttempt {
19
36
  ordinal: number;
@@ -22,20 +39,32 @@ export interface PersistedUsageAttempt {
22
39
  adapter: string;
23
40
  status: number;
24
41
  durationMs: number;
42
+ /**
43
+ * True only when the upstream stream died after its 200 head was committed,
44
+ * so the row must not meter as a success the client never received.
45
+ * Absent on ordinary attempts so old rows keep their exact shape.
46
+ */
47
+ streamAborted?: boolean;
25
48
  /** TTFT relative to THIS attempt's start (WP4); unset for non-streaming/tool-only. */
26
49
  firstOutputMs?: number;
27
50
  sendCount: number;
28
51
  recoveryKinds: AttemptRecoveryKind[];
29
52
  usageStatus: UsageStatus;
53
+ /** Stable non-PII identity for the Codex pool account that served this attempt. */
54
+ accountLogLabel?: CodexUsageAccountLogLabel;
30
55
  inputTokenEstimate?: number;
31
56
  usage?: OcxUsage;
32
57
  totalTokens?: number;
33
58
  errorCode?: string;
59
+ /** Installation-local exact Compatibility Lab route-subject digest for this attempt. */
60
+ labRouteSubjectId?: string;
34
61
  /** Target-specific reasoning intent and exact adapter-normalized wire parameter. */
35
62
  requestedEffort?: string;
36
63
  effectiveEffort?: string;
37
64
  reasoningWireField?: string;
38
- reasoningWireValue?: string | number;
65
+ reasoningWireValue?: string | number | boolean;
66
+ /** Adapter-produced tier fact for this physical attempt; absent on pre-B0 rows. */
67
+ tierOutcome?: AttemptTierOutcome;
39
68
  }
40
69
 
41
70
  export interface PersistedUsageEntry {
@@ -50,22 +79,30 @@ export interface PersistedUsageEntry {
50
79
  admissionKind?: "configured" | "environment" | "loopback";
51
80
  /** The inbound wire, not the client product — see `surface`. */
52
81
  inboundProtocol?: "responses" | "chat" | "messages";
82
+ /** Stable non-PII identity for Codex Pool usage; absent for Direct/non-Codex traffic. */
83
+ accountLogLabel?: CodexUsageAccountLogLabel;
53
84
  /** Best-effort chat/session correlation for Logs grouping (#330). */
54
85
  conversationId?: string;
55
86
  resolvedModel?: string;
56
87
  requestedModel?: string;
88
+ /** Original bare helper model when the opt-in shadow-call route rewrote this request. */
89
+ shadowCallRewrittenFrom?: string;
57
90
  /** Reasoning effort / service-tier metadata for GUI Logs after restart. */
58
91
  requestedEffort?: string;
59
92
  /** Adapter-normalized tier and exact upstream parameter emitted for this request. */
60
93
  effectiveEffort?: string;
61
94
  reasoningWireField?: string;
62
- reasoningWireValue?: string | number;
95
+ reasoningWireValue?: string | number | boolean;
96
+ /** Raw caller tier captured before routing, sanitized and bounded for durable logs. */
97
+ callerServiceTier?: string;
63
98
  requestedServiceTier?: string;
64
99
  requestedSpeedLabel?: string;
65
100
  configuredServiceTier?: string;
66
101
  configuredSpeedLabel?: string;
67
102
  modelSupportsServiceTier?: boolean;
68
103
  responseServiceTier?: string;
104
+ /** Summary of the final physical attempt for dashboard consumers. */
105
+ tierOutcome?: AttemptTierOutcome;
69
106
  status: number;
70
107
  durationMs: number;
71
108
  /** TTFT relative to the request start (WP4); unset for non-streaming/tool-only. */
@@ -81,6 +118,12 @@ export interface PersistedUsageEntry {
81
118
  closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow";
82
119
  /** Already redacted + capped at capture (request-log.ts redactSecretString().slice(0,500)). */
83
120
  upstreamError?: string;
121
+ /**
122
+ * Bounded route-decision trace (RI-01): why this provider/model/account was
123
+ * selected. Additive field; old rows without it parse unchanged. Never
124
+ * contains prompts, credentials, or hidden reasoning.
125
+ */
126
+ routeDecision?: RouteDecisionTraceV1;
84
127
  }
85
128
 
86
129
  const KNOWN_USAGE_SURFACES = new Set<NonNullable<PersistedUsageEntry["surface"]>>([
@@ -118,8 +161,8 @@ export function isKnownInboundProtocol(value: unknown): value is NonNullable<Per
118
161
  return typeof value === "string" && KNOWN_INBOUND_PROTOCOLS.has(value as NonNullable<PersistedUsageEntry["inboundProtocol"]>);
119
162
  }
120
163
 
121
- export function usageLogPath(): string {
122
- return join(getConfigDir(), "usage.jsonl");
164
+ export function usageLogPath(configDir?: string): string {
165
+ return join(configDir ?? getConfigDir(), "usage.jsonl");
123
166
  }
124
167
 
125
168
  export function usageTotalTokens(usage: OcxUsage | undefined): number | undefined {
@@ -173,8 +216,11 @@ const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
173
216
  "connection-reset",
174
217
  "oauth-401",
175
218
  "key-429",
219
+ "rate-limit-429",
176
220
  "anthropic-oauth-429",
177
221
  "image-413",
222
+ "opaque-blob-rejection",
223
+ "empty-completion",
178
224
  ]);
179
225
  const USAGE_STATUSES = new Set<UsageStatus>([
180
226
  "reported",
@@ -182,6 +228,20 @@ const USAGE_STATUSES = new Set<UsageStatus>([
182
228
  "unsupported",
183
229
  "estimated",
184
230
  ]);
231
+ const LAB_ROUTE_SUBJECT_ID_RE = /^[0-9a-f]{64}$/;
232
+ const FAST_OUTCOMES = new Set<AttemptTierOutcome["fastOutcome"]>([
233
+ "not-requested", "applied", "downgraded", "unknown",
234
+ ]);
235
+ const TIER_CONFIRMATIONS = new Set<AttemptTierOutcome["confirmation"]>([
236
+ "confirmed", "assumed", "downgraded", "unknown",
237
+ ]);
238
+ const FAST_DOWNGRADE_REASONS = new Set<NonNullable<AttemptTierOutcome["fastDowngradeReason"]>>([
239
+ "route-unsupported", "wire-unavailable", "response-declined",
240
+ ]);
241
+
242
+ export function isLabRouteSubjectId(value: unknown): value is string {
243
+ return typeof value === "string" && LAB_ROUTE_SUBJECT_ID_RE.test(value);
244
+ }
185
245
 
186
246
  function isNonNegativeFiniteNumber(value: unknown): value is number {
187
247
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
@@ -206,6 +266,53 @@ function normalizeAttemptUsage(raw: unknown): OcxUsage | null {
206
266
  return normalizeUsageValue(usage as unknown as OcxUsage) ?? null;
207
267
  }
208
268
 
269
+ function normalizeAttemptTierOutcome(raw: unknown): AttemptTierOutcome | null {
270
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
271
+ const outcome = raw as Record<string, unknown>;
272
+ if (typeof outcome.fastOutcome !== "string"
273
+ || !FAST_OUTCOMES.has(outcome.fastOutcome as AttemptTierOutcome["fastOutcome"])
274
+ || typeof outcome.confirmation !== "string"
275
+ || !TIER_CONFIRMATIONS.has(outcome.confirmation as AttemptTierOutcome["confirmation"])) {
276
+ return null;
277
+ }
278
+ if ("canonical" in outcome && outcome.canonical !== "priority") return null;
279
+ if ("wireKind" in outcome
280
+ && outcome.wireKind !== null
281
+ && outcome.wireKind !== "service-tier"
282
+ && outcome.wireKind !== "anthropic-speed") return null;
283
+ if ("wireValue" in outcome && outcome.wireValue !== null && typeof outcome.wireValue !== "string") return null;
284
+ if ("fastDowngradeReason" in outcome
285
+ && (typeof outcome.fastDowngradeReason !== "string"
286
+ || !FAST_DOWNGRADE_REASONS.has(outcome.fastDowngradeReason as NonNullable<AttemptTierOutcome["fastDowngradeReason"]>))) {
287
+ return null;
288
+ }
289
+ if ("callerTierDropped" in outcome && typeof outcome.callerTierDropped !== "boolean") return null;
290
+ if ("callerFastSuppressedByConfig" in outcome
291
+ && typeof outcome.callerFastSuppressedByConfig !== "boolean") return null;
292
+ if ("responseServiceTier" in outcome && typeof outcome.responseServiceTier !== "string") return null;
293
+ const wireValue = sanitizeLogMetadataString(outcome.wireValue);
294
+ const responseServiceTier = sanitizeLogMetadataString(outcome.responseServiceTier);
295
+ return {
296
+ ...(outcome.canonical === "priority" ? { canonical: "priority" as const } : {}),
297
+ ...(outcome.wireKind === null || outcome.wireKind === "service-tier" || outcome.wireKind === "anthropic-speed"
298
+ ? { wireKind: outcome.wireKind }
299
+ : {}),
300
+ ...(outcome.wireValue === null
301
+ ? { wireValue: null }
302
+ : wireValue ? { wireValue } : {}),
303
+ fastOutcome: outcome.fastOutcome as AttemptTierOutcome["fastOutcome"],
304
+ ...(typeof outcome.fastDowngradeReason === "string"
305
+ ? { fastDowngradeReason: outcome.fastDowngradeReason as NonNullable<AttemptTierOutcome["fastDowngradeReason"]> }
306
+ : {}),
307
+ ...(typeof outcome.callerTierDropped === "boolean" ? { callerTierDropped: outcome.callerTierDropped } : {}),
308
+ ...(typeof outcome.callerFastSuppressedByConfig === "boolean"
309
+ ? { callerFastSuppressedByConfig: outcome.callerFastSuppressedByConfig }
310
+ : {}),
311
+ confirmation: outcome.confirmation as AttemptTierOutcome["confirmation"],
312
+ ...(responseServiceTier ? { responseServiceTier } : {}),
313
+ };
314
+ }
315
+
209
316
  function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
210
317
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
211
318
  const attempt = raw as Record<string, unknown>;
@@ -232,6 +339,9 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
232
339
  && !isNonNegativeFiniteNumber(attempt.totalTokens)) return null;
233
340
  const usage = "usage" in attempt ? normalizeAttemptUsage(attempt.usage) : undefined;
234
341
  if ("usage" in attempt && usage === null) return null;
342
+ const tierOutcome = "tierOutcome" in attempt
343
+ ? normalizeAttemptTierOutcome(attempt.tierOutcome)
344
+ : undefined;
235
345
  const recoveryKinds = Array.isArray(attempt.recoveryKinds)
236
346
  ? [...new Set(attempt.recoveryKinds.filter(
237
347
  (value): value is AttemptRecoveryKind => typeof value === "string"
@@ -245,12 +355,17 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
245
355
  adapter: attempt.adapter,
246
356
  status: attempt.status,
247
357
  durationMs: attempt.durationMs,
358
+ // Absent by default; only the literal `true` marker survives the round trip.
359
+ ...(attempt.streamAborted === true ? { streamAborted: true } : {}),
248
360
  ...(isNonNegativeFiniteNumber(attempt.firstOutputMs)
249
361
  ? { firstOutputMs: attempt.firstOutputMs }
250
362
  : {}),
251
363
  sendCount: attempt.sendCount as number,
252
364
  recoveryKinds,
253
365
  usageStatus: attempt.usageStatus as UsageStatus,
366
+ ...(isCodexUsageAccountLogLabel(attempt.accountLogLabel)
367
+ ? { accountLogLabel: attempt.accountLogLabel }
368
+ : {}),
254
369
  ...(isNonNegativeFiniteNumber(attempt.inputTokenEstimate)
255
370
  ? { inputTokenEstimate: attempt.inputTokenEstimate }
256
371
  : {}),
@@ -259,6 +374,9 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
259
374
  ? { totalTokens: attempt.totalTokens }
260
375
  : {}),
261
376
  ...(typeof attempt.errorCode === "string" ? { errorCode: attempt.errorCode } : {}),
377
+ ...(isLabRouteSubjectId(attempt.labRouteSubjectId)
378
+ ? { labRouteSubjectId: attempt.labRouteSubjectId }
379
+ : {}),
262
380
  ...(typeof attempt.requestedEffort === "string" && attempt.requestedEffort
263
381
  ? { requestedEffort: capMetadataString(attempt.requestedEffort) }
264
382
  : {}),
@@ -268,14 +386,30 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
268
386
  ...(typeof attempt.reasoningWireField === "string" && attempt.reasoningWireField
269
387
  ? { reasoningWireField: capMetadataString(attempt.reasoningWireField) }
270
388
  : {}),
271
- ...(typeof attempt.reasoningWireValue === "string" && attempt.reasoningWireValue
272
- ? { reasoningWireValue: capMetadataString(attempt.reasoningWireValue) }
273
- : isNonNegativeFiniteNumber(attempt.reasoningWireValue)
274
- ? { reasoningWireValue: attempt.reasoningWireValue }
275
- : {}),
389
+ ...(isValidReasoningWireValue(attempt.reasoningWireField, attempt.reasoningWireValue)
390
+ ? typeof attempt.reasoningWireValue === "string"
391
+ ? { reasoningWireValue: capMetadataString(attempt.reasoningWireValue) }
392
+ : { reasoningWireValue: attempt.reasoningWireValue }
393
+ : {}),
394
+ ...(tierOutcome ? { tierOutcome } : {}),
276
395
  };
277
396
  }
278
397
 
398
+ /**
399
+ * Pairing rule for reasoning diagnostics, shared with the live request-log capture path:
400
+ * a non-empty string, a non-negative finite number, or a boolean only for
401
+ * `reasoning.enabled`. The field name itself is validated separately at capture time;
402
+ * persisted rows may carry legacy field names, so this checks only the value shape.
403
+ */
404
+ export function isValidReasoningWireValue(
405
+ wireField: unknown,
406
+ wireValue: unknown,
407
+ ): wireValue is string | number | boolean {
408
+ return (typeof wireValue === "string" && wireValue.length > 0)
409
+ || (typeof wireValue === "number" && Number.isFinite(wireValue) && wireValue >= 0)
410
+ || (wireField === "reasoning.enabled" && typeof wireValue === "boolean");
411
+ }
412
+
279
413
  function normalizedAttempts(raw: unknown): PersistedUsageAttempt[] {
280
414
  if (!Array.isArray(raw)) return [];
281
415
  return raw.map(normalizeUsageAttempt)
@@ -294,6 +428,13 @@ export function normalizeUsageEntryForTest(entry: PersistedUsageEntry): Persiste
294
428
 
295
429
  function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
296
430
  const attempts = normalizedAttempts(entry.attempts);
431
+ const tierOutcome = entry.tierOutcome ? normalizeAttemptTierOutcome(entry.tierOutcome) : undefined;
432
+ const callerServiceTier = sanitizeLogMetadataString(entry.callerServiceTier);
433
+ const responseServiceTier = sanitizeLogMetadataString(entry.responseServiceTier);
434
+ const shadowCallRewrittenFrom = sanitizeLogMetadataString(entry.shadowCallRewrittenFrom);
435
+ const routeDecision = entry.routeDecision
436
+ ? normalizeRouteDecisionTrace(entry.routeDecision)
437
+ : undefined;
297
438
  return {
298
439
  requestId: entry.requestId,
299
440
  timestamp: entry.timestamp,
@@ -309,11 +450,15 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
309
450
  : {}),
310
451
  ...(isKnownAdmissionKind(entry.admissionKind) ? { admissionKind: entry.admissionKind } : {}),
311
452
  ...(isKnownInboundProtocol(entry.inboundProtocol) ? { inboundProtocol: entry.inboundProtocol } : {}),
453
+ ...(isCodexUsageAccountLogLabel(entry.accountLogLabel)
454
+ ? { accountLogLabel: entry.accountLogLabel }
455
+ : {}),
312
456
  ...(typeof entry.conversationId === "string" && entry.conversationId.trim()
313
457
  ? { conversationId: entry.conversationId.trim().slice(0, 128) }
314
458
  : {}),
315
459
  ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
316
460
  ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
461
+ ...(shadowCallRewrittenFrom ? { shadowCallRewrittenFrom } : {}),
317
462
  ...(typeof entry.requestedEffort === "string" && entry.requestedEffort
318
463
  ? { requestedEffort: capMetadataString(entry.requestedEffort) }
319
464
  : {}),
@@ -323,11 +468,12 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
323
468
  ...(typeof entry.reasoningWireField === "string" && entry.reasoningWireField
324
469
  ? { reasoningWireField: capMetadataString(entry.reasoningWireField) }
325
470
  : {}),
326
- ...(typeof entry.reasoningWireValue === "string" && entry.reasoningWireValue
327
- ? { reasoningWireValue: capMetadataString(entry.reasoningWireValue) }
328
- : isNonNegativeFiniteNumber(entry.reasoningWireValue)
329
- ? { reasoningWireValue: entry.reasoningWireValue }
330
- : {}),
471
+ ...(isValidReasoningWireValue(entry.reasoningWireField, entry.reasoningWireValue)
472
+ ? typeof entry.reasoningWireValue === "string"
473
+ ? { reasoningWireValue: capMetadataString(entry.reasoningWireValue) }
474
+ : { reasoningWireValue: entry.reasoningWireValue }
475
+ : {}),
476
+ ...(callerServiceTier ? { callerServiceTier } : {}),
331
477
  ...(typeof entry.requestedServiceTier === "string" && entry.requestedServiceTier
332
478
  ? { requestedServiceTier: capMetadataString(entry.requestedServiceTier) }
333
479
  : {}),
@@ -343,9 +489,8 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
343
489
  ...(typeof entry.modelSupportsServiceTier === "boolean"
344
490
  ? { modelSupportsServiceTier: entry.modelSupportsServiceTier }
345
491
  : {}),
346
- ...(typeof entry.responseServiceTier === "string" && entry.responseServiceTier
347
- ? { responseServiceTier: capMetadataString(entry.responseServiceTier) }
348
- : {}),
492
+ ...(responseServiceTier ? { responseServiceTier } : {}),
493
+ ...(tierOutcome ? { tierOutcome } : {}),
349
494
  status: entry.status,
350
495
  durationMs: entry.durationMs,
351
496
  ...(isNonNegativeFiniteNumber(entry.firstOutputMs)
@@ -354,11 +499,12 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
354
499
  usageStatus: entry.usageStatus,
355
500
  ...(entry.usage ? { usage: normalizeUsageValue(entry.usage) } : {}),
356
501
  ...(typeof entry.totalTokens === "number" ? { totalTokens: entry.totalTokens } : {}),
357
- ...(attempts.length > 0 ? { attempts } : {}),
502
+ ...(Array.isArray(entry.attempts) ? { attempts } : {}),
358
503
  ...(entry.errorCode ? { errorCode: entry.errorCode } : {}),
359
504
  ...(entry.terminalStatus ? { terminalStatus: entry.terminalStatus } : {}),
360
505
  ...(entry.closeReason ? { closeReason: entry.closeReason } : {}),
361
506
  ...(entry.upstreamError ? { upstreamError: entry.upstreamError } : {}),
507
+ ...(routeDecision ? { routeDecision } : {}),
362
508
  };
363
509
  }
364
510
 
@@ -388,8 +534,18 @@ export type UsageLogRevision = {
388
534
 
389
535
  let usageReadCacheStats = { fullReads: 0, tailReads: 0, parsedLines: 0 };
390
536
  const MANAGEMENT_USAGE_MAX_READ_BYTES = 64 * 1024 * 1024;
537
+ const RECENT_USAGE_MAX_READ_BYTES = 64 * 1024 * 1024;
391
538
  const MANAGEMENT_USAGE_READ_CHUNK_BYTES = 1024 * 1024;
392
- const MANAGEMENT_USAGE_MAX_ENTRIES = 200_000;
539
+ const MANAGEMENT_USAGE_MAX_ENTRIES_DEFAULT = 500_000;
540
+ /**
541
+ * Row cap for a management snapshot. Overridable only so tests can reach the cap without
542
+ * building a half-million-row fixture; production always uses the default.
543
+ */
544
+ let MANAGEMENT_USAGE_MAX_ENTRIES = MANAGEMENT_USAGE_MAX_ENTRIES_DEFAULT;
545
+
546
+ export function setManagementUsageMaxEntriesForTests(value: number | null): void {
547
+ MANAGEMENT_USAGE_MAX_ENTRIES = value ?? MANAGEMENT_USAGE_MAX_ENTRIES_DEFAULT;
548
+ }
393
549
  const MANAGEMENT_USAGE_FLIGHT_STALE_MS = 30_000;
394
550
  export interface ManagementUsageSnapshot {
395
551
  entries: PersistedUsageEntry[];
@@ -397,14 +553,156 @@ export interface ManagementUsageSnapshot {
397
553
  truncatedPrefixBytes: number;
398
554
  entriesTruncated: boolean;
399
555
  entriesDropped: number;
556
+ /** Digest of the covered prefix, used to detect an in-place rewrite before reuse. */
557
+ prefixDigest: string;
558
+ /**
559
+ * Byte offset where the RETAINED ROWS begin.
560
+ *
561
+ * Distinct from `truncatedPrefixBytes`, which is the API-visible "bytes skipped by the
562
+ * byte window" signal and must stay independent of entry-count truncation. When the
563
+ * entry cap drops rows, those bytes are not window truncation, but the retained rows do
564
+ * start later -- this field tracks that so the byte accounting stays exact.
565
+ */
566
+ rowsBeginAtBytes: number;
567
+ /**
568
+ * Byte length of each returned row, in order, including its newline.
569
+ *
570
+ * Lets a later read trim rows that have fallen out of the bounded window without
571
+ * re-reading or re-parsing them, which is what keeps the returned set equal to the
572
+ * window the caller asked for.
573
+ */
574
+ entryLengths: number[];
575
+ /** Unparseable bytes after the last returned row; the next read folds them forward. */
576
+ trailingSkippedBytes: number;
400
577
  }
401
578
  let managementUsageReadInflight: {
402
579
  key: string;
580
+ openedSize: number;
403
581
  promise: Promise<ManagementUsageSnapshot>;
404
582
  startedAt: number;
405
583
  abort: AbortController;
406
584
  } | null = null;
407
585
 
586
+ /**
587
+ * Append-tolerant snapshot of the last management read.
588
+ *
589
+ * The management reader parses a 64 MiB tail into ~53k objects, which costs roughly
590
+ * 640 MB of transient RSS per cold call. The JS objects are collected promptly, but
591
+ * the allocator does not return those pages, so every cold miss ratchets process RSS
592
+ * upward and never comes back down (observed: 7.9 GiB RSS against a 130 MB JS heap).
593
+ *
594
+ * Reparsing an unchanged prefix is what makes that transient recur. `usage.jsonl` is
595
+ * append-only under a stable identity, so when the file has only grown we keep the
596
+ * previously parsed rows and parse just the appended bytes. This is retained state, so
597
+ * it is registered with the app-owned memory budget and is evictable under pressure.
598
+ */
599
+ interface RetainedUsageSnapshot {
600
+ identityKey: string;
601
+ maxReadBytes: number;
602
+ /** Absolute end offset in the file that `entries` already covers. */
603
+ coveredThroughBytes: number;
604
+ /**
605
+ * Digest of the last bytes of the covered prefix, re-verified before extending.
606
+ *
607
+ * Identity (path/dev/ino/birthtime) intentionally ignores size and mtime so appends
608
+ * can share work, which also means an in-place rewrite that keeps the inode is
609
+ * invisible to it. A hand-edit or external compaction can therefore replace history
610
+ * under a stable identity without shrinking the file. Re-reading this trailing window
611
+ * catches that: if the bytes behind `coveredThroughBytes` changed, the retained rows
612
+ * no longer describe the file and must not be extended.
613
+ */
614
+ prefixDigest: string;
615
+ /** Bytes of the file skipped ahead of the retained window. */
616
+ truncatedPrefixBytes: number;
617
+ /** Byte length of each retained row, so out-of-window rows can be trimmed exactly. */
618
+ entryLengths: number[];
619
+ /** Unparseable bytes after the last retained row; folded into the next row's span. */
620
+ trailingSkippedBytes: number;
621
+ /** Byte offset where the retained rows begin; see ManagementUsageSnapshot. */
622
+ rowsBeginAtBytes: number;
623
+ entries: PersistedUsageEntry[];
624
+ entriesTruncated: boolean;
625
+ entriesDropped: number;
626
+ revision: UsageLogRevision;
627
+ retainedAt: number;
628
+ approxBytes: number;
629
+ }
630
+ let retainedUsageSnapshot: RetainedUsageSnapshot | null = null;
631
+
632
+ /** Rough per-row retained cost; exact sizing would cost another full serialization pass. */
633
+ const RETAINED_USAGE_ENTRY_BYTES = 512;
634
+
635
+ /** Chunk size used when digesting a retained region. */
636
+ const RETAINED_USAGE_DIGEST_CHUNK_BYTES = 1024 * 1024;
637
+
638
+ /**
639
+ * Digest a byte range into `hash`; false when it cannot be read.
640
+ *
641
+ * Deliberately not sampled. A sampled digest covers a vanishing fraction of a large
642
+ * prefix (32 KiB of 64 MiB is 0.05%), so an ordinary fixed-width in-place edit -- a
643
+ * redaction script fixing one field, a compaction rewriting a middle region -- lands in
644
+ * a gap by default and the stale rows are served.
645
+ *
646
+ * Hashing from byte 0 on every call is also wrong: that is O(file) per poll while the
647
+ * read it protects is capped at maxReadBytes, so the ratio degrades as the ledger grows
648
+ * and becomes SLOWER than a full read past roughly 1-2 GB. Only the retained REGION
649
+ * (truncatedPrefixBytes..coveredThroughBytes) is hashed. It is never wider than the
650
+ * window, and bytes below the retained start describe no retained row, so reading them
651
+ * would prove nothing.
652
+ */
653
+ function updateUsageDigest(hash: Hash, fd: number, from: number, to: number): boolean {
654
+ if (to <= from) return true;
655
+ const buffer = Buffer.allocUnsafe(Math.min(RETAINED_USAGE_DIGEST_CHUNK_BYTES, to - from));
656
+ for (let position = from; position < to;) {
657
+ const length = Math.min(buffer.byteLength, to - position);
658
+ let offset = 0;
659
+ while (offset < length) {
660
+ const read = readSync(fd, buffer, offset, length - offset, position + offset);
661
+ if (read === 0) return false;
662
+ offset += read;
663
+ }
664
+ hash.update(buffer.subarray(0, length));
665
+ position += length;
666
+ }
667
+ return true;
668
+ }
669
+
670
+ /**
671
+ * Digest of `from`..`to`; null when it cannot be read.
672
+ *
673
+ * The range is bound into the digest so a region cannot be confused with an equal-length
674
+ * region at a different offset.
675
+ */
676
+ function usageRegionDigest(fd: number, from: number, to: number): string | null {
677
+ if (to <= from) return `${from}:${to}:empty`;
678
+ const hash = createHash("sha256");
679
+ if (!updateUsageDigest(hash, fd, from, to)) return null;
680
+ return `${from}:${to}:${hash.digest("hex")}`;
681
+ }
682
+
683
+ function retainedUsageSnapshotBytes(entries: PersistedUsageEntry[]): number {
684
+ return entries.length * RETAINED_USAGE_ENTRY_BYTES;
685
+ }
686
+
687
+ export function discardRetainedUsageSnapshot(): number {
688
+ const released = retainedUsageSnapshot?.approxBytes ?? 0;
689
+ retainedUsageSnapshot = null;
690
+ return released;
691
+ }
692
+
693
+ export function retainedUsageSnapshotStats(): {
694
+ count: number;
695
+ bytes: number;
696
+ oldestAt: number | null;
697
+ } {
698
+ if (!retainedUsageSnapshot) return { count: 0, bytes: 0, oldestAt: null };
699
+ return {
700
+ count: 1,
701
+ bytes: retainedUsageSnapshot.approxBytes,
702
+ oldestAt: retainedUsageSnapshot.retainedAt,
703
+ };
704
+ }
705
+
408
706
  /** Test-only observability for proving that unchanged prefixes are not reparsed. */
409
707
  export function usageReadCacheStatsForTests(): Readonly<typeof usageReadCacheStats> {
410
708
  return { ...usageReadCacheStats };
@@ -414,6 +712,7 @@ export function resetUsageReadCacheForTests(): void {
414
712
  usageReadCacheStats = { fullReads: 0, tailReads: 0, parsedLines: 0 };
415
713
  managementUsageReadInflight?.abort.abort();
416
714
  managementUsageReadInflight = null;
715
+ retainedUsageSnapshot = null;
417
716
  }
418
717
 
419
718
  function readExactly(fd: number, length: number, position: number): Buffer | null {
@@ -453,6 +752,12 @@ export function usageLogRevisionKey(revision: UsageLogRevision | null): string {
453
752
  ].join("\0");
454
753
  }
455
754
 
755
+ /** Identity of the usage ledger file, excluding size/mtime/ctime so appends can share work. */
756
+ export function usageLogIdentityKey(revision: UsageLogRevision | null): string {
757
+ if (!revision) return "missing";
758
+ return [revision.path, revision.dev, revision.ino, revision.birthtimeMs].join("\0");
759
+ }
760
+
456
761
  export function currentUsageLogRevision(): UsageLogRevision | null {
457
762
  const path = usageLogPath();
458
763
  if (!existsSync(path)) return null;
@@ -468,23 +773,71 @@ export function currentUsageLogRevision(): UsageLogRevision | null {
468
773
  async function parseUsageTextCooperatively(text: string, signal: AbortSignal): Promise<{
469
774
  entries: PersistedUsageEntry[];
470
775
  entriesDropped: number;
776
+ entryLengths: number[];
777
+ /** Bytes of unparseable lines after the final accepted row. */
778
+ trailingSkippedBytes: number;
779
+ /** Bytes of rows removed by the entry cap, which move into the skipped prefix. */
780
+ cappedPrefixBytes: number;
471
781
  }> {
472
- const lines = text.split(/\r?\n/);
782
+ // Split on "\n" only, so a CRLF line keeps its "\r" and its byte length stays exact.
783
+ // Splitting on /\r?\n/ consumes two bytes but leaves no way to tell that it did, which
784
+ // made the recorded lengths short by one byte per line on a CRLF ledger and failed the
785
+ // accounting self-check. JSON.parse tolerates the trailing "\r".
786
+ const lines = text.split("\n");
473
787
  usageReadCacheStats.parsedLines += lines.filter(line => line.trim()).length;
474
788
  const entries: PersistedUsageEntry[] = [];
789
+ // Byte length of each accepted row including its newline, so a later read can trim
790
+ // rows that fall out of the bounded window without re-reading the file.
791
+ const entryLengths: number[] = [];
475
792
  const batchSize = 1_000;
793
+ // Bytes of lines that did not yield an entry (malformed JSON, missing requestId, a
794
+ // torn final write). They still occupy space in the file, so they are folded into the
795
+ // next accepted row's recorded length. Dropping them would make the recorded lengths
796
+ // sum to less than the real byte span, and the window trim -- which walks forward by
797
+ // summing those lengths -- would consume extra rows to reach the window start,
798
+ // silently hiding history and desynchronizing truncatedPrefixBytes.
799
+ let pendingSkippedBytes = 0;
476
800
  for (let offset = 0; offset < lines.length; offset += batchSize) {
477
801
  if (signal.aborted) throw signal.reason;
478
- entries.push(...parseUsageLines(lines.slice(offset, offset + batchSize)));
802
+ const batch = lines.slice(offset, offset + batchSize);
803
+ for (let index = 0; index < batch.length; index++) {
804
+ const line = batch[index]!;
805
+ // The split leaves a trailing "" after the final newline; it occupies no bytes.
806
+ const isLastLine = offset + index === lines.length - 1;
807
+ const lineBytes = Buffer.byteLength(line, "utf-8") + (isLastLine && line === "" ? 0 : 1);
808
+ const parsed = parseUsageLines([line]);
809
+ if (parsed.length === 0) {
810
+ pendingSkippedBytes += lineBytes;
811
+ continue;
812
+ }
813
+ entries.push(parsed[0]!);
814
+ entryLengths.push(lineBytes + pendingSkippedBytes);
815
+ pendingSkippedBytes = 0;
816
+ }
479
817
  if (offset + batchSize < lines.length) {
480
818
  // JSON parsing dominates large-log startup. Yield between bounded batches so
481
819
  // Bun can continue serving health and settings requests on the same thread.
482
820
  await new Promise<void>(resolve => setTimeout(resolve, 0));
483
821
  }
484
822
  }
485
- if (entries.length <= MANAGEMENT_USAGE_MAX_ENTRIES) return { entries, entriesDropped: 0 };
823
+ // Skipped bytes AFTER the last accepted row belong to no entry length, so report them
824
+ // separately; the trim arithmetic adds them back to keep lengths summing to the span.
825
+ const trailingSkippedBytes = pendingSkippedBytes;
826
+ if (entries.length <= MANAGEMENT_USAGE_MAX_ENTRIES) {
827
+ return { entries, entriesDropped: 0, entryLengths, trailingSkippedBytes, cappedPrefixBytes: 0 };
828
+ }
486
829
  const entriesDropped = entries.length - MANAGEMENT_USAGE_MAX_ENTRIES;
487
- return { entries: entries.slice(-MANAGEMENT_USAGE_MAX_ENTRIES), entriesDropped };
830
+ return {
831
+ entries: entries.slice(-MANAGEMENT_USAGE_MAX_ENTRIES),
832
+ entriesDropped,
833
+ entryLengths: entryLengths.slice(-MANAGEMENT_USAGE_MAX_ENTRIES),
834
+ trailingSkippedBytes,
835
+ // Bytes of the rows the cap removed. The caller adds them to its skipped prefix so
836
+ // the recorded lengths keep summing to the byte span they describe.
837
+ cappedPrefixBytes: entryLengths
838
+ .slice(0, entryLengths.length - MANAGEMENT_USAGE_MAX_ENTRIES)
839
+ .reduce((total, length) => total + length, 0),
840
+ };
488
841
  }
489
842
 
490
843
  async function readUsageEntriesFullCooperatively(
@@ -525,12 +878,181 @@ async function readUsageEntriesFullCooperatively(
525
878
  }
526
879
  const parsed = await parseUsageTextCooperatively(bytes.toString("utf-8"), signal);
527
880
  usageReadCacheStats.fullReads += 1;
881
+ // Rows removed by the entry cap start the retained rows later in the file. That is
882
+ // NOT byte-window truncation, so it must not move truncatedPrefixBytes -- the two
883
+ // signals are independent in the API. It is tracked separately for the byte
884
+ // accounting the incremental reader relies on.
885
+ const rowsBeginAtBytes = truncatedPrefixBytes + parsed.cappedPrefixBytes;
886
+ // Digest the exact prefix these rows describe, so a later incremental read can
887
+ // prove the file was appended to rather than rewritten under the same inode.
888
+ const prefixDigest = usageRegionDigest(fd, rowsBeginAtBytes, Number(stat.size));
889
+ if (prefixDigest === null) throw new Error("usage log changed while it was being read");
528
890
  return {
529
891
  entries: parsed.entries,
530
892
  revision: usageLogRevision(path, stat),
531
893
  truncatedPrefixBytes,
532
894
  entriesTruncated: parsed.entriesDropped > 0,
533
895
  entriesDropped: parsed.entriesDropped,
896
+ prefixDigest,
897
+ entryLengths: parsed.entryLengths,
898
+ trailingSkippedBytes: parsed.trailingSkippedBytes,
899
+ rowsBeginAtBytes,
900
+ };
901
+ } finally {
902
+ if (fd !== undefined) closeSync(fd);
903
+ }
904
+ }
905
+
906
+ /**
907
+ * Parse only the bytes appended since the retained snapshot's covered offset.
908
+ *
909
+ * Returns null when the retained snapshot cannot be extended safely — a different
910
+ * identity or read window, a file that shrank (replacement/truncation), or a covered
911
+ * offset that no longer sits on a record boundary. Callers then fall back to a full
912
+ * bounded read.
913
+ */
914
+ async function readUsageEntriesIncrementally(
915
+ path: string,
916
+ signal: AbortSignal,
917
+ maxReadBytes: number,
918
+ retained: RetainedUsageSnapshot,
919
+ ): Promise<ManagementUsageSnapshot | null> {
920
+ let fd: number | undefined;
921
+ try {
922
+ fd = openSync(path, "r");
923
+ const stat = fstatSync(fd);
924
+ const revision = usageLogRevision(path, stat);
925
+ if (usageLogIdentityKey(revision) !== retained.identityKey) return null;
926
+ const size = Number(stat.size);
927
+ // A shrink means truncation or replacement-in-place; the retained rows may no
928
+ // longer correspond to file contents, so refuse to extend them.
929
+ if (size < retained.coveredThroughBytes) return null;
930
+ // Verify the retained REGION is unchanged before anything is reused. Identity keeps
931
+ // dev/ino/birthtime, and an append and an in-place rewrite both move mtime/ctime
932
+ // forward, so only the bytes themselves settle it.
933
+ //
934
+ // Only `truncatedPrefixBytes..coveredThroughBytes` is hashed: that is exactly the
935
+ // span the retained rows were parsed from, and after trimming it is never wider than
936
+ // maxReadBytes. Hashing from byte 0 instead would make every poll O(file) -- cheaper
937
+ // than a reparse on a 245 MB ledger but MORE expensive past roughly 1-2 GB, turning
938
+ // this optimization into a pessimization on exactly the growth curve an append-only
939
+ // ledger follows. Bytes before the retained start are not described by any retained
940
+ // row, so re-reading them proves nothing.
941
+ const covered = usageRegionDigest(fd, retained.rowsBeginAtBytes, retained.coveredThroughBytes);
942
+ if (covered === null || covered !== retained.prefixDigest) return null;
943
+ // Read and parse ONLY the appended bytes.
944
+ let appendedEntries: PersistedUsageEntry[] = [];
945
+ let appendedLengths: number[] = [];
946
+ let appendedDropped = 0;
947
+ let appendedTrailingSkipped = retained.trailingSkippedBytes;
948
+ if (size > retained.coveredThroughBytes) {
949
+ // The covered offset must land immediately after a newline, or the retained rows
950
+ // and the appended text do not join on a record boundary.
951
+ if (retained.coveredThroughBytes > 0) {
952
+ const preceding = readExactly(fd, 1, retained.coveredThroughBytes - 1);
953
+ if (preceding === null || preceding[0] !== 0x0a) return null;
954
+ }
955
+ const chunks: Buffer[] = [];
956
+ for (let position = retained.coveredThroughBytes; position < size;) {
957
+ if (signal.aborted) throw signal.reason;
958
+ const length = Math.min(MANAGEMENT_USAGE_READ_CHUNK_BYTES, size - position);
959
+ const chunk = readExactly(fd, length, position);
960
+ if (chunk === null) throw new Error("usage log changed while it was being read");
961
+ chunks.push(chunk);
962
+ position += length;
963
+ }
964
+ const appended = await parseUsageTextCooperatively(Buffer.concat(chunks).toString("utf-8"), signal);
965
+ appendedEntries = appended.entries;
966
+ appendedLengths = appended.entryLengths;
967
+ appendedDropped = appended.entriesDropped;
968
+ // A capped appended chunk is not joinable: its dropped rows sit between the
969
+ // retained rows and the kept ones, so the lengths no longer describe a contiguous
970
+ // span. Fall back to a full read.
971
+ if (appended.cappedPrefixBytes > 0) return null;
972
+ // If the appended chunk produced rows, its own trailing skipped bytes become the
973
+ // new trailing remainder; otherwise the earlier remainder still stands and the new
974
+ // skipped bytes add to it.
975
+ appendedTrailingSkipped = appended.entries.length > 0
976
+ ? appended.trailingSkippedBytes
977
+ : retained.trailingSkippedBytes + appended.trailingSkippedBytes;
978
+ }
979
+ // Re-anchor the window in place. Rows that have fallen outside `size - maxReadBytes`
980
+ // are dropped using their recorded byte lengths, so the result is exactly the rows a
981
+ // fresh bounded read would load -- no superset, and truncatedPrefixBytes and
982
+ // snapshotWindow keep describing the read honestly. Refusing here instead would make
983
+ // this path dead code on any ledger past the window, which is precisely the case it
984
+ // exists for.
985
+ const windowStart = Math.max(0, size - maxReadBytes);
986
+ let entries = retained.entries.concat(appendedEntries);
987
+ // Skipped bytes trailing the retained rows sit BETWEEN them and the appended rows, so
988
+ // they belong to the first appended row's span. Folding them in keeps the recorded
989
+ // lengths summing to the true byte distance, which is what the trim walk relies on.
990
+ const joinedLengths = appendedLengths.slice();
991
+ if (retained.trailingSkippedBytes > 0 && joinedLengths.length > 0) {
992
+ joinedLengths[0] = joinedLengths[0]! + retained.trailingSkippedBytes;
993
+ }
994
+ let lengths = retained.entryLengths.concat(joinedLengths);
995
+ let rowsBeginAtBytes = retained.rowsBeginAtBytes;
996
+ // Byte-window truncation advances ONLY here, so it stays exactly what a cold read of
997
+ // this window reports. The entry cap below is entry-count truncation and must not
998
+ // move it -- the two are independent signals in the API.
999
+ let windowTruncatedBytes = retained.truncatedPrefixBytes;
1000
+ let dropIndex = 0;
1001
+ while (dropIndex < lengths.length && rowsBeginAtBytes < windowStart) {
1002
+ rowsBeginAtBytes += lengths[dropIndex]!;
1003
+ windowTruncatedBytes += lengths[dropIndex]!;
1004
+ dropIndex += 1;
1005
+ }
1006
+ // If every row is gone and a trailing unparseable remainder still sits before the
1007
+ // window start, nothing is left to advance the offset with: the retained span would
1008
+ // keep growing past maxReadBytes on each malformed-only append while the accounting
1009
+ // still balanced. Re-anchor with a full read instead.
1010
+ if (rowsBeginAtBytes < windowStart) return null;
1011
+ if (dropIndex > 0) {
1012
+ entries = entries.slice(dropIndex);
1013
+ lengths = lengths.slice(dropIndex);
1014
+ }
1015
+ let entriesDropped = retained.entriesDropped + appendedDropped;
1016
+ if (entries.length > MANAGEMENT_USAGE_MAX_ENTRIES) {
1017
+ // A cold read applies the entry cap to the whole window and reports byte
1018
+ // truncation for the window boundary alone. An incremental read arrives at the cap
1019
+ // by a different route and cannot reconstruct that ordering from retained state, so
1020
+ // continuing here would report a truncatedPrefixBytes that disagrees with a fresh
1021
+ // read of the same window. Re-anchor instead.
1022
+ //
1023
+ // This is reachable in production, not a theoretical branch: rows average ~118
1024
+ // bytes on a real ledger, so 500,000 of them occupy ~56 MiB and fit inside the
1025
+ // 64 MiB window. Both truncations can therefore apply at once.
1026
+ return null;
1027
+ }
1028
+ // The recorded lengths plus the trailing remainder must account for every byte from
1029
+ // the retained rows' start to EOF; if they do not, the lengths and the file have
1030
+ // diverged and the retained rows cannot be trusted.
1031
+ let accounted = appendedTrailingSkipped;
1032
+ for (const length of lengths) accounted += length;
1033
+ if (rowsBeginAtBytes + accounted !== size) return null;
1034
+ usageReadCacheStats.tailReads += 1;
1035
+ // Byte-window truncation is what the API reports, and it stays independent of
1036
+ // entry-count truncation. A cold read reports the record boundary it actually landed
1037
+ // on, which is where the rows begin MINUS whatever the entry cap removed -- the cap
1038
+ // is not window truncation. When nothing was skipped by the window at all, a cold
1039
+ // read reports 0.
1040
+ const truncatedPrefixBytes = windowTruncatedBytes;
1041
+ return {
1042
+ entries,
1043
+ revision,
1044
+ truncatedPrefixBytes,
1045
+ // ENTRY-count truncation only. Byte-window truncation is reported by
1046
+ // truncatedPrefixBytes, and the route ORs the two itself; folding bytes in here
1047
+ // would make a byte-truncated read claim rows were dropped when none were.
1048
+ entriesTruncated: entriesDropped > 0,
1049
+ entriesDropped,
1050
+ // The digest must describe exactly the region the returned rows came from, which
1051
+ // is the post-trim window, not the pre-trim one.
1052
+ prefixDigest: usageRegionDigest(fd, rowsBeginAtBytes, size) ?? "",
1053
+ entryLengths: lengths,
1054
+ trailingSkippedBytes: appendedTrailingSkipped,
1055
+ rowsBeginAtBytes,
534
1056
  };
535
1057
  } finally {
536
1058
  if (fd !== undefined) closeSync(fd);
@@ -539,8 +1061,10 @@ async function readUsageEntriesFullCooperatively(
539
1061
 
540
1062
  /**
541
1063
  * Management API reader: full parses yield between bounded batches and concurrent
542
- * callers share work only when they observed the same exact file revision. Parsed rows
543
- * are returned to the request and never retained in module state.
1064
+ * callers share work when they observe the same ledger identity and byte window.
1065
+ * Appends keep that identity; replacements (inode/birthtime change) start a new flight.
1066
+ * The parsed tail is retained under the app-owned memory budget so an append reparses
1067
+ * only the appended bytes; the retained rows are evictable and are copied per caller.
544
1068
  */
545
1069
  export async function readUsageSnapshotForManagement(maxReadBytes = MANAGEMENT_USAGE_MAX_READ_BYTES): Promise<{
546
1070
  entries: PersistedUsageEntry[];
@@ -553,18 +1077,57 @@ export async function readUsageSnapshotForManagement(maxReadBytes = MANAGEMENT_U
553
1077
  const path = usageLogPath();
554
1078
  if (!existsSync(path)) return { entries: [], revision: null, truncatedPrefixBytes: 0, entriesTruncated: false, entriesDropped: 0 };
555
1079
  const observed = currentUsageLogRevision();
556
- const key = `${usageLogRevisionKey(observed)}\0${maxReadBytes}`;
1080
+ const key = `${usageLogIdentityKey(observed)}\0${maxReadBytes}`;
1081
+ const observedSize = observed?.size ?? 0;
557
1082
  const existing = managementUsageReadInflight;
558
- if (existing?.key === key && Date.now() - existing.startedAt <= MANAGEMENT_USAGE_FLIGHT_STALE_MS) {
1083
+ const replacement = Boolean(existing && observedSize < existing.openedSize);
1084
+ if (!replacement && existing?.key === key && Date.now() - existing.startedAt <= MANAGEMENT_USAGE_FLIGHT_STALE_MS) {
1085
+ const shared = await existing.promise;
1086
+ return { ...shared, entries: shared.entries.slice() };
1087
+ }
1088
+ if (existing && (existing.key !== key || replacement || Date.now() - existing.startedAt > MANAGEMENT_USAGE_FLIGHT_STALE_MS)) {
1089
+ existing.abort.abort(new Error("management usage read superseded"));
1090
+ } else if (existing) {
559
1091
  const shared = await existing.promise;
560
1092
  return { ...shared, entries: shared.entries.slice() };
561
1093
  }
562
- existing?.abort.abort(new Error("management usage read superseded"));
563
1094
  const abort = new AbortController();
564
- const promise = readUsageEntriesFullCooperatively(path, abort.signal, maxReadBytes);
565
- managementUsageReadInflight = { key, promise, startedAt: Date.now(), abort };
1095
+ const retained = retainedUsageSnapshot;
1096
+ const reusable = retained
1097
+ && retained.identityKey === usageLogIdentityKey(observed)
1098
+ && retained.maxReadBytes === maxReadBytes
1099
+ ? retained
1100
+ : null;
1101
+ const promise = (async (): Promise<ManagementUsageSnapshot> => {
1102
+ if (reusable) {
1103
+ const incremental = await readUsageEntriesIncrementally(path, abort.signal, maxReadBytes, reusable);
1104
+ if (incremental) return incremental;
1105
+ // The retained rows could not be extended safely; drop them before the full read
1106
+ // so a stale window is never combined with freshly parsed bytes.
1107
+ discardRetainedUsageSnapshot();
1108
+ }
1109
+ return readUsageEntriesFullCooperatively(path, abort.signal, maxReadBytes);
1110
+ })();
1111
+ managementUsageReadInflight = { key, openedSize: observedSize, promise, startedAt: Date.now(), abort };
566
1112
  try {
567
1113
  const snapshot = await promise;
1114
+ retainedUsageSnapshot = {
1115
+ identityKey: usageLogIdentityKey(snapshot.revision),
1116
+ maxReadBytes,
1117
+ coveredThroughBytes: snapshot.revision.size,
1118
+ prefixDigest: snapshot.prefixDigest,
1119
+ truncatedPrefixBytes: snapshot.truncatedPrefixBytes,
1120
+ entryLengths: snapshot.entryLengths,
1121
+ trailingSkippedBytes: snapshot.trailingSkippedBytes,
1122
+ rowsBeginAtBytes: snapshot.rowsBeginAtBytes,
1123
+ entries: snapshot.entries,
1124
+ entriesTruncated: snapshot.entriesTruncated,
1125
+ entriesDropped: snapshot.entriesDropped,
1126
+ revision: snapshot.revision,
1127
+ retainedAt: Date.now(),
1128
+ approxBytes: retainedUsageSnapshotBytes(snapshot.entries),
1129
+ };
1130
+ enforceAppOwnedMemoryBudget();
568
1131
  return { ...snapshot, entries: snapshot.entries.slice() };
569
1132
  } finally {
570
1133
  if (managementUsageReadInflight?.promise === promise) managementUsageReadInflight = null;
@@ -614,18 +1177,20 @@ function parseUsageLines(lines: string[]): PersistedUsageEntry[] {
614
1177
  * Read only the newest `limit` usage.jsonl rows without loading the whole append-only
615
1178
  * file into memory. Used by request-log hydration on `ocx start`.
616
1179
  */
617
- export function readRecentUsageEntries(limit: number): PersistedUsageEntry[] {
1180
+ export function readRecentUsageEntries(limit: number, configDir?: string): PersistedUsageEntry[] {
618
1181
  if (!Number.isFinite(limit) || limit <= 0) return [];
619
- const path = usageLogPath();
1182
+ const path = usageLogPath(configDir);
620
1183
  if (!existsSync(path)) return [];
621
1184
  let fd: number | undefined;
622
1185
  try {
623
1186
  fd = openSync(path, "r");
624
1187
  const size = fstatSync(fd).size;
625
1188
  if (size <= 0) return [];
626
- // ~4 KiB/row budget with a floor; expand once if the window yields too few lines.
627
- let windowBytes = Math.min(size, Math.max(64 * 1024, Math.ceil(limit) * 4 * 1024));
628
- for (let attempt = 0; attempt < 2; attempt++) {
1189
+ // Trace-sized rows need a larger per-row budget than the pre-trace ledger,
1190
+ // but startup hydration must never grow into an unbounded whole-file read.
1191
+ const maxWindowBytes = Math.min(size, RECENT_USAGE_MAX_READ_BYTES);
1192
+ let windowBytes = Math.min(maxWindowBytes, Math.max(64 * 1024, Math.ceil(limit) * 20 * 1024));
1193
+ while (true) {
629
1194
  const start = Math.max(0, size - windowBytes);
630
1195
  const buf = Buffer.alloc(size - start);
631
1196
  readSync(fd, buf, 0, buf.length, start);
@@ -633,8 +1198,8 @@ export function readRecentUsageEntries(limit: number): PersistedUsageEntry[] {
633
1198
  if (start > 0) {
634
1199
  const nl = text.indexOf("\n");
635
1200
  if (nl < 0) {
636
- if (start === 0) break;
637
- windowBytes = Math.min(size, windowBytes * 4);
1201
+ if (start === 0 || windowBytes >= maxWindowBytes) break;
1202
+ windowBytes = Math.min(maxWindowBytes, windowBytes * 4);
638
1203
  continue;
639
1204
  }
640
1205
  text = text.slice(nl + 1);
@@ -644,8 +1209,8 @@ export function readRecentUsageEntries(limit: number): PersistedUsageEntry[] {
644
1209
  // or partial lines are filtered out during parsing and we always return the
645
1210
  // most recent N valid rows (not N physical lines minus corrupt ones).
646
1211
  const entries = parseUsageLines(lines);
647
- if (entries.length >= limit || start === 0 || windowBytes >= size) return entries.slice(-limit);
648
- windowBytes = Math.min(size, windowBytes * 4);
1212
+ if (entries.length >= limit || start === 0 || windowBytes >= maxWindowBytes) return entries.slice(-limit);
1213
+ windowBytes = Math.min(maxWindowBytes, windowBytes * 4);
649
1214
  }
650
1215
  return [];
651
1216
  } catch {