@yansigit/opencodex 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 (826) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/LICENSE +21 -0
  3. package/README.md +303 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +587 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
  11. package/gui/dist/assets/index-CGoDO3uO.css +1 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/icons.svg +24 -0
  14. package/gui/dist/index.html +25 -0
  15. package/gui/dist/logo.png +0 -0
  16. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/cline-color.svg +16 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  25. package/gui/dist/provider-icons/discord.svg +1 -0
  26. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  27. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  28. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  29. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  30. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  31. package/gui/dist/provider-icons/grok.svg +1 -0
  32. package/gui/dist/provider-icons/groq-color.svg +1 -0
  33. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  34. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  35. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  36. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  37. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  38. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  39. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  40. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  41. package/gui/dist/provider-icons/openai.svg +1 -0
  42. package/gui/dist/provider-icons/opencode.svg +2 -0
  43. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  44. package/gui/dist/provider-icons/pi.svg +21 -0
  45. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  46. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  47. package/gui/dist/provider-icons/telegram.svg +1 -0
  48. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  49. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  50. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  51. package/package.json +108 -0
  52. package/src/AGENTS.md +28 -0
  53. package/src/adapters/anthropic-image-guard.ts +251 -0
  54. package/src/adapters/anthropic-image-normalize.ts +518 -0
  55. package/src/adapters/anthropic-output-schema.ts +137 -0
  56. package/src/adapters/anthropic.ts +1327 -0
  57. package/src/adapters/azure.ts +36 -0
  58. package/src/adapters/base.ts +121 -0
  59. package/src/adapters/client-fingerprint.ts +65 -0
  60. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  61. package/src/adapters/command-code.ts +601 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  65. package/src/adapters/cursor/cursor-errors.ts +288 -0
  66. package/src/adapters/cursor/discovery.ts +333 -0
  67. package/src/adapters/cursor/effort-map.ts +151 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +250 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/h2-pool.ts +123 -0
  72. package/src/adapters/cursor/http1-bidi.ts +361 -0
  73. package/src/adapters/cursor/images.ts +704 -0
  74. package/src/adapters/cursor/kv-store.ts +52 -0
  75. package/src/adapters/cursor/live-models.ts +269 -0
  76. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  77. package/src/adapters/cursor/live-transport.ts +1653 -0
  78. package/src/adapters/cursor/mcp-config.ts +42 -0
  79. package/src/adapters/cursor/mcp-manager.ts +333 -0
  80. package/src/adapters/cursor/message-mapper.ts +49 -0
  81. package/src/adapters/cursor/native-exec-common.ts +76 -0
  82. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  83. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  84. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  85. package/src/adapters/cursor/native-exec-network.ts +43 -0
  86. package/src/adapters/cursor/native-exec-shell.ts +547 -0
  87. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  88. package/src/adapters/cursor/native-exec.ts +663 -0
  89. package/src/adapters/cursor/protobuf-events.ts +1381 -0
  90. package/src/adapters/cursor/protobuf-request.ts +1032 -0
  91. package/src/adapters/cursor/request-builder.ts +461 -0
  92. package/src/adapters/cursor/thread-continuity.ts +67 -0
  93. package/src/adapters/cursor/tool-definitions.ts +735 -0
  94. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  95. package/src/adapters/cursor/transport-retry.ts +132 -0
  96. package/src/adapters/cursor/transport.ts +79 -0
  97. package/src/adapters/cursor/types.ts +79 -0
  98. package/src/adapters/cursor.ts +322 -0
  99. package/src/adapters/google-antigravity-hosts.ts +48 -0
  100. package/src/adapters/google-antigravity-replay.ts +827 -0
  101. package/src/adapters/google-antigravity-tools.ts +105 -0
  102. package/src/adapters/google-antigravity-wire.ts +141 -0
  103. package/src/adapters/google-errors.ts +92 -0
  104. package/src/adapters/google-http.ts +460 -0
  105. package/src/adapters/google-tool-schema.ts +238 -0
  106. package/src/adapters/google-truncation.ts +24 -0
  107. package/src/adapters/google-wire-compiler.ts +232 -0
  108. package/src/adapters/google.ts +1377 -0
  109. package/src/adapters/identity.ts +77 -0
  110. package/src/adapters/image.ts +23 -0
  111. package/src/adapters/kiro-constants.ts +16 -0
  112. package/src/adapters/kiro-errors.ts +208 -0
  113. package/src/adapters/kiro-events.ts +197 -0
  114. package/src/adapters/kiro-images.ts +129 -0
  115. package/src/adapters/kiro-retry.ts +312 -0
  116. package/src/adapters/kiro-thinking.ts +112 -0
  117. package/src/adapters/kiro-tool-fallback.ts +36 -0
  118. package/src/adapters/kiro-tools.ts +224 -0
  119. package/src/adapters/kiro-truncation.ts +33 -0
  120. package/src/adapters/kiro-wire.ts +129 -0
  121. package/src/adapters/kiro.ts +1936 -0
  122. package/src/adapters/mimo-free.ts +280 -0
  123. package/src/adapters/openai-chat-url.ts +11 -0
  124. package/src/adapters/openai-chat.ts +1980 -0
  125. package/src/adapters/openai-responses-url.ts +16 -0
  126. package/src/adapters/openai-responses.ts +1911 -0
  127. package/src/adapters/registry.ts +175 -0
  128. package/src/adapters/responses-tool-schema.ts +67 -0
  129. package/src/adapters/run-turn-queue.ts +114 -0
  130. package/src/adapters/tool-call-id.ts +119 -0
  131. package/src/adapters/tool-catalog-nudge.ts +154 -0
  132. package/src/adapters/upstream-http-error.ts +48 -0
  133. package/src/adapters/xai-web-search.ts +185 -0
  134. package/src/bridge.ts +1986 -0
  135. package/src/chat/inbound.ts +319 -0
  136. package/src/chat/outbound.ts +821 -0
  137. package/src/claude/agents-inject.ts +266 -0
  138. package/src/claude/alias.ts +149 -0
  139. package/src/claude/auth-detect.ts +229 -0
  140. package/src/claude/auth-mode-migration.ts +32 -0
  141. package/src/claude/auth-mode.ts +62 -0
  142. package/src/claude/context-windows.ts +205 -0
  143. package/src/claude/desktop-3p-guard.ts +35 -0
  144. package/src/claude/desktop-3p-paths.ts +84 -0
  145. package/src/claude/desktop-3p.ts +615 -0
  146. package/src/claude/desktop-health.ts +26 -0
  147. package/src/claude/desktop-profile.ts +263 -0
  148. package/src/claude/gateway-cache.ts +107 -0
  149. package/src/claude/inbound-debug.ts +163 -0
  150. package/src/claude/inbound.ts +578 -0
  151. package/src/claude/model-info.ts +174 -0
  152. package/src/claude/outbound.ts +926 -0
  153. package/src/cli/access.ts +108 -0
  154. package/src/cli/account-api.ts +302 -0
  155. package/src/cli/account-auth.ts +250 -0
  156. package/src/cli/account-catalog-refresh.ts +14 -0
  157. package/src/cli/account-extended.ts +737 -0
  158. package/src/cli/account-main.ts +317 -0
  159. package/src/cli/account.ts +299 -0
  160. package/src/cli/agent-driven.ts +70 -0
  161. package/src/cli/agent.ts +290 -0
  162. package/src/cli/catalog-prewarm.ts +27 -0
  163. package/src/cli/claude-agent-startup-sync.ts +73 -0
  164. package/src/cli/claude-desktop.ts +213 -0
  165. package/src/cli/claude.ts +355 -0
  166. package/src/cli/codex-log-guard-doctor.ts +103 -0
  167. package/src/cli/codex-shim-autorestore.ts +47 -0
  168. package/src/cli/codex-shim-readiness.ts +76 -0
  169. package/src/cli/combo.ts +127 -0
  170. package/src/cli/config-command.ts +209 -0
  171. package/src/cli/debug.ts +228 -0
  172. package/src/cli/dispatch.ts +585 -0
  173. package/src/cli/doctor.ts +1202 -0
  174. package/src/cli/ensure-desired-integrations.ts +152 -0
  175. package/src/cli/export-command.ts +213 -0
  176. package/src/cli/help.ts +101 -0
  177. package/src/cli/index.ts +973 -0
  178. package/src/cli/init.ts +211 -0
  179. package/src/cli/integrations.ts +260 -0
  180. package/src/cli/interactive-confirm.ts +133 -0
  181. package/src/cli/lab.ts +607 -0
  182. package/src/cli/launcher-context.ts +77 -0
  183. package/src/cli/minimax.ts +497 -0
  184. package/src/cli/models-runtime.ts +245 -0
  185. package/src/cli/models.ts +422 -0
  186. package/src/cli/observe.ts +206 -0
  187. package/src/cli/opencode.ts +588 -0
  188. package/src/cli/provider-replit.ts +232 -0
  189. package/src/cli/provider-runtime.ts +179 -0
  190. package/src/cli/provider.ts +492 -0
  191. package/src/cli/ready.ts +301 -0
  192. package/src/cli/registry.ts +422 -0
  193. package/src/cli/replit-gateway-key-input.ts +138 -0
  194. package/src/cli/root.ts +86 -0
  195. package/src/cli/route-policy.ts +92 -0
  196. package/src/cli/runtime-api.ts +328 -0
  197. package/src/cli/star-prompt.ts +211 -0
  198. package/src/cli/status-oauth.ts +78 -0
  199. package/src/cli/status.ts +328 -0
  200. package/src/cli/system-command.ts +112 -0
  201. package/src/cli/system-restart-client.ts +146 -0
  202. package/src/cli/tray-proxy.ts +199 -0
  203. package/src/cli/v2.ts +268 -0
  204. package/src/cli.ts +10 -0
  205. package/src/clients/config-export.ts +1704 -0
  206. package/src/codex/account-id.ts +34 -0
  207. package/src/codex/account-label.ts +47 -0
  208. package/src/codex/account-lifecycle.ts +172 -0
  209. package/src/codex/account-namespace-match.ts +63 -0
  210. package/src/codex/account-namespaces.ts +195 -0
  211. package/src/codex/account-pause.ts +20 -0
  212. package/src/codex/account-priority.ts +83 -0
  213. package/src/codex/account-runtime-state.ts +31 -0
  214. package/src/codex/account-store.ts +544 -0
  215. package/src/codex/account-usability.ts +43 -0
  216. package/src/codex/admission.ts +256 -0
  217. package/src/codex/affinity-debug.ts +162 -0
  218. package/src/codex/agent-roles-sync.ts +225 -0
  219. package/src/codex/agent-roles.ts +238 -0
  220. package/src/codex/app-server-processes.ts +1143 -0
  221. package/src/codex/app-server-restart-service.ts +232 -0
  222. package/src/codex/auth-api.ts +2147 -0
  223. package/src/codex/auth-collision.ts +109 -0
  224. package/src/codex/auth-context.ts +665 -0
  225. package/src/codex/autostart-health.ts +156 -0
  226. package/src/codex/catalog/account-models.ts +67 -0
  227. package/src/codex/catalog/aggregation.ts +436 -0
  228. package/src/codex/catalog/bundled.ts +549 -0
  229. package/src/codex/catalog/effort.ts +446 -0
  230. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  231. package/src/codex/catalog/kinds.ts +2 -0
  232. package/src/codex/catalog/metadata.ts +664 -0
  233. package/src/codex/catalog/native-models.ts +72 -0
  234. package/src/codex/catalog/parsing.ts +650 -0
  235. package/src/codex/catalog/provider-fetch.ts +2064 -0
  236. package/src/codex/catalog/sync.ts +1883 -0
  237. package/src/codex/catalog-admission.ts +199 -0
  238. package/src/codex/catalog-refresh-status.ts +105 -0
  239. package/src/codex/catalog-write-serialization.ts +242 -0
  240. package/src/codex/catalog.ts +14 -0
  241. package/src/codex/codex-write-lock.ts +384 -0
  242. package/src/codex/convergence-types.ts +614 -0
  243. package/src/codex/convergence.ts +651 -0
  244. package/src/codex/coordinator-doctor.ts +332 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/exec-invocation.ts +22 -0
  249. package/src/codex/features.ts +1566 -0
  250. package/src/codex/generation.ts +202 -0
  251. package/src/codex/history-job.ts +407 -0
  252. package/src/codex/history-lock.ts +242 -0
  253. package/src/codex/history-migration-guardian.ts +108 -0
  254. package/src/codex/history-provider.ts +979 -0
  255. package/src/codex/history-transition.ts +105 -0
  256. package/src/codex/history-worker.ts +220 -0
  257. package/src/codex/home.ts +206 -0
  258. package/src/codex/inject-coordination.ts +290 -0
  259. package/src/codex/inject.ts +1733 -0
  260. package/src/codex/injected-marker.ts +106 -0
  261. package/src/codex/integration-record.ts +266 -0
  262. package/src/codex/internal/catalog-writer.ts +203 -0
  263. package/src/codex/internal/history-writer.ts +80 -0
  264. package/src/codex/journal.ts +225 -0
  265. package/src/codex/log-guard/inspect.ts +506 -0
  266. package/src/codex/log-guard/lock.ts +150 -0
  267. package/src/codex/log-guard/maintenance.ts +403 -0
  268. package/src/codex/log-guard/path-safety.ts +88 -0
  269. package/src/codex/log-guard/policy.ts +44 -0
  270. package/src/codex/log-guard/processes.ts +205 -0
  271. package/src/codex/log-guard/protection.ts +489 -0
  272. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  273. package/src/codex/main-account-cache.ts +56 -0
  274. package/src/codex/main-account.ts +68 -0
  275. package/src/codex/management-convergence.ts +167 -0
  276. package/src/codex/model-cache.ts +273 -0
  277. package/src/codex/model-entitlements.ts +353 -0
  278. package/src/codex/native-main-admission.ts +47 -0
  279. package/src/codex/native-main-auth-temp.ts +187 -0
  280. package/src/codex/native-main-claim.ts +178 -0
  281. package/src/codex/native-main-lock-file.ts +162 -0
  282. package/src/codex/native-main-owner.ts +329 -0
  283. package/src/codex/native-profile-api.ts +247 -0
  284. package/src/codex/native-profile-manager.ts +1531 -0
  285. package/src/codex/native-profile-processes.ts +121 -0
  286. package/src/codex/native-profile-recovery.ts +99 -0
  287. package/src/codex/native-profile-stage-store.ts +387 -0
  288. package/src/codex/native-profile-startup.ts +492 -0
  289. package/src/codex/native-profile-store.ts +855 -0
  290. package/src/codex/native-profile-types.ts +120 -0
  291. package/src/codex/native-residue.ts +682 -0
  292. package/src/codex/paths.ts +144 -0
  293. package/src/codex/plan-from-token.ts +140 -0
  294. package/src/codex/plan.ts +40 -0
  295. package/src/codex/plugins-doctor.ts +242 -0
  296. package/src/codex/pool-rotation.ts +295 -0
  297. package/src/codex/project-config-warnings.ts +425 -0
  298. package/src/codex/prompt-journal.ts +352 -0
  299. package/src/codex/prompt-layers.ts +967 -0
  300. package/src/codex/prompt-lock.ts +143 -0
  301. package/src/codex/quota-rejection.ts +298 -0
  302. package/src/codex/quota.ts +573 -0
  303. package/src/codex/refresh.ts +62 -0
  304. package/src/codex/reset-credit-recovery.ts +1044 -0
  305. package/src/codex/routing.ts +1888 -0
  306. package/src/codex/runtime.ts +659 -0
  307. package/src/codex/shim.ts +2170 -0
  308. package/src/codex/subagent-defaults.ts +550 -0
  309. package/src/codex/subagent-model-fallback.ts +784 -0
  310. package/src/codex/sync.ts +319 -0
  311. package/src/codex/transition-state.ts +612 -0
  312. package/src/codex/upstream-host-health.ts +368 -0
  313. package/src/codex/user-identity.ts +557 -0
  314. package/src/codex/warmup.ts +298 -0
  315. package/src/codex/websocket-registry.ts +100 -0
  316. package/src/codex/write-coordination.ts +114 -0
  317. package/src/combos/failover.ts +160 -0
  318. package/src/combos/index.ts +45 -0
  319. package/src/combos/request.ts +94 -0
  320. package/src/combos/resolve.ts +232 -0
  321. package/src/combos/types.ts +398 -0
  322. package/src/config/provider-name.ts +24 -0
  323. package/src/config.ts +4041 -0
  324. package/src/fork/register.ts +3 -0
  325. package/src/generated/compatibility-version.json +3116 -0
  326. package/src/generated/model-metadata.ts +106 -0
  327. package/src/github/star-state.ts +203 -0
  328. package/src/grok/inject.ts +530 -0
  329. package/src/grok/inspect.ts +45 -0
  330. package/src/grok/status.ts +121 -0
  331. package/src/grok/sync.ts +66 -0
  332. package/src/images/artifacts.ts +516 -0
  333. package/src/images/fulfill-video.ts +163 -0
  334. package/src/images/fulfill.ts +149 -0
  335. package/src/images/index.ts +4 -0
  336. package/src/images/loop.ts +955 -0
  337. package/src/images/plan.ts +143 -0
  338. package/src/images/synthetic-tool.ts +133 -0
  339. package/src/images/types.ts +41 -0
  340. package/src/images/xai-client.ts +141 -0
  341. package/src/images/xai-video-client.ts +163 -0
  342. package/src/index.ts +22 -0
  343. package/src/integrations/config-io.ts +269 -0
  344. package/src/integrations/journal.ts +315 -0
  345. package/src/integrations/merge.ts +135 -0
  346. package/src/integrations/mutation-flight.ts +71 -0
  347. package/src/integrations/native/ownership-preflight.ts +202 -0
  348. package/src/integrations/omp-yaml-source.ts +358 -0
  349. package/src/integrations/owned-refresh.ts +74 -0
  350. package/src/integrations/ownership.ts +111 -0
  351. package/src/integrations/registry.ts +159 -0
  352. package/src/integrations/serialize.ts +314 -0
  353. package/src/integrations/state.ts +361 -0
  354. package/src/integrations/store.ts +103 -0
  355. package/src/integrations/writer-lock.ts +98 -0
  356. package/src/integrations/writer.ts +691 -0
  357. package/src/lab/artifacts/sanitize.ts +586 -0
  358. package/src/lab/artifacts/secure-fs.ts +475 -0
  359. package/src/lab/artifacts/store.ts +310 -0
  360. package/src/lab/automation/budgets.ts +78 -0
  361. package/src/lab/automation/config-persistence.ts +256 -0
  362. package/src/lab/automation/constants.ts +39 -0
  363. package/src/lab/automation/cooldown.ts +103 -0
  364. package/src/lab/automation/dispatch.ts +211 -0
  365. package/src/lab/automation/index.ts +13 -0
  366. package/src/lab/automation/orchestrator.ts +499 -0
  367. package/src/lab/automation/persistence.ts +512 -0
  368. package/src/lab/automation/planner.ts +371 -0
  369. package/src/lab/automation/policy.ts +136 -0
  370. package/src/lab/automation/queue.ts +191 -0
  371. package/src/lab/automation/recovery.ts +24 -0
  372. package/src/lab/automation/route-context.ts +21 -0
  373. package/src/lab/automation/run-key.ts +44 -0
  374. package/src/lab/automation/runs-query.ts +34 -0
  375. package/src/lab/automation/types.ts +160 -0
  376. package/src/lab/conformance/assertion.ts +325 -0
  377. package/src/lab/conformance/digest.ts +22 -0
  378. package/src/lab/conformance/executor.ts +741 -0
  379. package/src/lab/conformance/fixture-provider.ts +27 -0
  380. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  381. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  382. package/src/lab/conformance/harness-budget.ts +47 -0
  383. package/src/lab/conformance/index.ts +5 -0
  384. package/src/lab/conformance/jcs.ts +64 -0
  385. package/src/lab/conformance/json-pointer.ts +39 -0
  386. package/src/lab/conformance/manifest.ts +180 -0
  387. package/src/lab/conformance/mcp-stub.ts +179 -0
  388. package/src/lab/conformance/negative-controls.ts +164 -0
  389. package/src/lab/conformance/observation.ts +355 -0
  390. package/src/lab/conformance/runner.ts +68 -0
  391. package/src/lab/conformance/sse-normalize.ts +59 -0
  392. package/src/lab/conformance/suite-manifest.ts +78 -0
  393. package/src/lab/conformance/types.ts +214 -0
  394. package/src/lab/constants.ts +126 -0
  395. package/src/lab/digest.ts +64 -0
  396. package/src/lab/events/errors.ts +9 -0
  397. package/src/lab/events/limits.ts +117 -0
  398. package/src/lab/events/types.ts +229 -0
  399. package/src/lab/events/validate.ts +781 -0
  400. package/src/lab/fabric/constants.ts +40 -0
  401. package/src/lab/fabric/executor.ts +492 -0
  402. package/src/lab/fabric/index.ts +80 -0
  403. package/src/lab/fabric/manifest.ts +222 -0
  404. package/src/lab/fabric/observe.ts +489 -0
  405. package/src/lab/fabric/patch.ts +79 -0
  406. package/src/lab/fabric/producer-child.ts +139 -0
  407. package/src/lab/fabric/producer-isolate.ts +276 -0
  408. package/src/lab/fabric/producer-protocol.ts +61 -0
  409. package/src/lab/fabric/scratch.ts +439 -0
  410. package/src/lab/fabric/subject.ts +106 -0
  411. package/src/lab/fabric/types.ts +134 -0
  412. package/src/lab/fabric/verifier.ts +98 -0
  413. package/src/lab/index.ts +54 -0
  414. package/src/lab/ledger/artifact-refs.ts +127 -0
  415. package/src/lab/ledger/invalidation.ts +136 -0
  416. package/src/lab/ledger/purge.ts +310 -0
  417. package/src/lab/ledger/store.ts +532 -0
  418. package/src/lab/live/credential-lease.ts +53 -0
  419. package/src/lab/live/destination.ts +155 -0
  420. package/src/lab/live/executor.ts +336 -0
  421. package/src/lab/live/inert-tools.ts +56 -0
  422. package/src/lab/live/manifest.ts +85 -0
  423. package/src/lab/live/mcp-loopback.ts +57 -0
  424. package/src/lab/live/runner.ts +19 -0
  425. package/src/lab/live/sandbox.ts +61 -0
  426. package/src/lab/live/suite-manifest.ts +41 -0
  427. package/src/lab/live/transport.ts +118 -0
  428. package/src/lab/live/types.ts +197 -0
  429. package/src/lab/observe/from-conformance.ts +301 -0
  430. package/src/lab/observe/from-live.ts +117 -0
  431. package/src/lab/paths.ts +153 -0
  432. package/src/lab/projection/rebuild.ts +495 -0
  433. package/src/lab/projection/schema.ts +135 -0
  434. package/src/lab/projection/verdicts.ts +474 -0
  435. package/src/lab/projection/verification.ts +412 -0
  436. package/src/lab/public/bundle.ts +217 -0
  437. package/src/lab/public/community-authority.ts +175 -0
  438. package/src/lab/public/community-files.ts +29 -0
  439. package/src/lab/public/community.ts +479 -0
  440. package/src/lab/public/file-safety.ts +155 -0
  441. package/src/lab/public/ids.ts +26 -0
  442. package/src/lab/public/index.ts +16 -0
  443. package/src/lab/public/mutation-lock.ts +424 -0
  444. package/src/lab/public/operator.ts +353 -0
  445. package/src/lab/public/origin-purge.ts +79 -0
  446. package/src/lab/public/origin.ts +203 -0
  447. package/src/lab/public/privacy.ts +143 -0
  448. package/src/lab/public/private-file.ts +261 -0
  449. package/src/lab/public/project.ts +124 -0
  450. package/src/lab/public/purge-test-fault.ts +21 -0
  451. package/src/lab/public/purge.ts +223 -0
  452. package/src/lab/public/registry.ts +44 -0
  453. package/src/lab/public/revocation.ts +252 -0
  454. package/src/lab/public/signature.ts +243 -0
  455. package/src/lab/public/storage.ts +105 -0
  456. package/src/lab/public/strict-json.ts +206 -0
  457. package/src/lab/public/time.ts +26 -0
  458. package/src/lab/public/types.ts +172 -0
  459. package/src/lab/public/validate.ts +391 -0
  460. package/src/lab/query/catalog.ts +101 -0
  461. package/src/lab/query/connection.ts +107 -0
  462. package/src/lab/query/constants.ts +4 -0
  463. package/src/lab/query/cursor.ts +132 -0
  464. package/src/lab/query/dto-map.ts +277 -0
  465. package/src/lab/query/errors.ts +22 -0
  466. package/src/lab/query/freshness.ts +53 -0
  467. package/src/lab/query/index.ts +45 -0
  468. package/src/lab/query/latest-observation.ts +59 -0
  469. package/src/lab/query/passive-production.ts +159 -0
  470. package/src/lab/query/queries.ts +444 -0
  471. package/src/lab/query/types.ts +266 -0
  472. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  473. package/src/lab/subject/installation-salt.ts +112 -0
  474. package/src/lab/subject/protocol-subject.ts +80 -0
  475. package/src/lab/subject/route-subject.ts +74 -0
  476. package/src/lib/abort.ts +146 -0
  477. package/src/lib/admin-secrets.ts +25 -0
  478. package/src/lib/admission.ts +83 -0
  479. package/src/lib/app-owned-memory-stores.ts +195 -0
  480. package/src/lib/app-owned-memory.ts +265 -0
  481. package/src/lib/bounded-body.ts +346 -0
  482. package/src/lib/bun-binary-validator.d.mts +3 -0
  483. package/src/lib/bun-binary-validator.mjs +18 -0
  484. package/src/lib/bun-runtime.ts +184 -0
  485. package/src/lib/bun-stream-caps.ts +130 -0
  486. package/src/lib/codex-restart-contract.ts +120 -0
  487. package/src/lib/config-ownership.ts +364 -0
  488. package/src/lib/crash-guard.ts +344 -0
  489. package/src/lib/debug-log-buffer.ts +83 -0
  490. package/src/lib/debug-settings.ts +108 -0
  491. package/src/lib/debug.ts +31 -0
  492. package/src/lib/destination-policy.ts +380 -0
  493. package/src/lib/errors.ts +406 -0
  494. package/src/lib/eventstream-decoder.ts +253 -0
  495. package/src/lib/fabric-task-execution-authority.ts +7 -0
  496. package/src/lib/fabric-task-host.ts +29 -0
  497. package/src/lib/gcp-adc.ts +341 -0
  498. package/src/lib/injection-debug-log.ts +58 -0
  499. package/src/lib/lab-activation.ts +223 -0
  500. package/src/lib/lab-live-execution-authority.ts +13 -0
  501. package/src/lib/lab-live-host.ts +30 -0
  502. package/src/lib/lab-live-pinned-sender.ts +56 -0
  503. package/src/lib/lab-live-route-production.ts +130 -0
  504. package/src/lib/lab-passive-linker-registration.ts +26 -0
  505. package/src/lib/local-management-attestation.ts +51 -0
  506. package/src/lib/local-management-capability.ts +100 -0
  507. package/src/lib/local-provider-reload-contract.ts +100 -0
  508. package/src/lib/open-url.ts +25 -0
  509. package/src/lib/optional-shutdown-hooks.ts +57 -0
  510. package/src/lib/pinned-http.ts +270 -0
  511. package/src/lib/privacy.ts +20 -0
  512. package/src/lib/process-control.ts +168 -0
  513. package/src/lib/provider-outbound.ts +210 -0
  514. package/src/lib/provider-url.ts +14 -0
  515. package/src/lib/proxy-env.ts +18 -0
  516. package/src/lib/redact.ts +521 -0
  517. package/src/lib/retry-after.ts +55 -0
  518. package/src/lib/self-launch-argv.ts +15 -0
  519. package/src/lib/server-resource-ownership.ts +71 -0
  520. package/src/lib/service-secrets.ts +25 -0
  521. package/src/lib/shadow-call.ts +61 -0
  522. package/src/lib/sidecar-tracker.ts +52 -0
  523. package/src/lib/sse-decoder.ts +364 -0
  524. package/src/lib/state-store-registrations.ts +119 -0
  525. package/src/lib/state-store-sweeper.ts +184 -0
  526. package/src/lib/system-restart-contract.ts +73 -0
  527. package/src/lib/test-home-guard.ts +90 -0
  528. package/src/lib/token-estimate.ts +86 -0
  529. package/src/lib/tool-argument-integers.ts +202 -0
  530. package/src/lib/translator-budget.ts +400 -0
  531. package/src/lib/upstream-http-version.ts +57 -0
  532. package/src/lib/upstream-reachability.ts +95 -0
  533. package/src/lib/upstream-retry.ts +392 -0
  534. package/src/lib/win-exec.ts +115 -0
  535. package/src/lib/win-paths.ts +68 -0
  536. package/src/lib/windows-atomic-replace.ts +156 -0
  537. package/src/lib/windows-elevation.ts +773 -0
  538. package/src/lib/windows-secret-acl.ts +854 -0
  539. package/src/lib/windows-service-wrappers.ts +72 -0
  540. package/src/lib/windows-text.ts +106 -0
  541. package/src/lib/windows-user-principal.ts +341 -0
  542. package/src/lib/winsw.ts +403 -0
  543. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  544. package/src/oauth/account-import/index.ts +15 -0
  545. package/src/oauth/account-import/parser.ts +83 -0
  546. package/src/oauth/account-import/registry.ts +18 -0
  547. package/src/oauth/account-import/service.ts +75 -0
  548. package/src/oauth/account-import/types.ts +91 -0
  549. package/src/oauth/anthropic-routing.ts +594 -0
  550. package/src/oauth/anthropic.ts +188 -0
  551. package/src/oauth/antigravity-routing.ts +151 -0
  552. package/src/oauth/callback-server.ts +300 -0
  553. package/src/oauth/chatgpt.ts +161 -0
  554. package/src/oauth/command-code.ts +239 -0
  555. package/src/oauth/cursor.ts +252 -0
  556. package/src/oauth/github-copilot.ts +428 -0
  557. package/src/oauth/google-antigravity.ts +262 -0
  558. package/src/oauth/health.ts +407 -0
  559. package/src/oauth/index.ts +1504 -0
  560. package/src/oauth/key-providers.ts +124 -0
  561. package/src/oauth/kimi.ts +227 -0
  562. package/src/oauth/kiro-credentials.ts +726 -0
  563. package/src/oauth/kiro.ts +621 -0
  564. package/src/oauth/local-token-detect.ts +130 -0
  565. package/src/oauth/log.ts +50 -0
  566. package/src/oauth/login-cli.ts +223 -0
  567. package/src/oauth/nous.ts +798 -0
  568. package/src/oauth/pkce.ts +15 -0
  569. package/src/oauth/store.ts +728 -0
  570. package/src/oauth/token-guardian.ts +309 -0
  571. package/src/oauth/types.ts +62 -0
  572. package/src/oauth/xai.ts +241 -0
  573. package/src/providers/alibaba-region-backup.ts +75 -0
  574. package/src/providers/alibaba-region-migration.ts +156 -0
  575. package/src/providers/alibaba-region-startup.ts +36 -0
  576. package/src/providers/antigravity-models.ts +695 -0
  577. package/src/providers/antigravity-quota.ts +216 -0
  578. package/src/providers/api-keys.ts +140 -0
  579. package/src/providers/base-url-choices.ts +74 -0
  580. package/src/providers/codex-capacity.ts +292 -0
  581. package/src/providers/command-code-efforts.ts +144 -0
  582. package/src/providers/context-cap.ts +82 -0
  583. package/src/providers/cursor-pool.ts +72 -0
  584. package/src/providers/derive.ts +586 -0
  585. package/src/providers/fastwire.ts +501 -0
  586. package/src/providers/free-directory.ts +187 -0
  587. package/src/providers/github-copilot-transport.ts +56 -0
  588. package/src/providers/google-vertex-location.ts +14 -0
  589. package/src/providers/key-failover.ts +271 -0
  590. package/src/providers/kiro-models.ts +67 -0
  591. package/src/providers/label.ts +19 -0
  592. package/src/providers/model-discovery-limits.ts +16 -0
  593. package/src/providers/model-discovery.ts +449 -0
  594. package/src/providers/model-rename-migration.ts +255 -0
  595. package/src/providers/model-rename-startup.ts +28 -0
  596. package/src/providers/openai-sidecar.ts +243 -0
  597. package/src/providers/openai-tier-startup.ts +56 -0
  598. package/src/providers/openai-tiers.ts +423 -0
  599. package/src/providers/openai-virtual-models.ts +83 -0
  600. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  601. package/src/providers/openrouter-routing.ts +102 -0
  602. package/src/providers/provider-id-rewrite.ts +185 -0
  603. package/src/providers/quota.ts +2345 -0
  604. package/src/providers/registry.ts +2918 -0
  605. package/src/providers/replit/constants.ts +27 -0
  606. package/src/providers/replit/derive.ts +85 -0
  607. package/src/providers/replit/headers.ts +28 -0
  608. package/src/providers/replit/origin.ts +55 -0
  609. package/src/providers/replit/pair-install-response.ts +72 -0
  610. package/src/providers/replit/probe.ts +199 -0
  611. package/src/providers/replit/setup.ts +350 -0
  612. package/src/providers/request-pacing.ts +310 -0
  613. package/src/providers/service-tier.ts +277 -0
  614. package/src/providers/slug-codec.ts +103 -0
  615. package/src/providers/static-model-discovery.ts +86 -0
  616. package/src/providers/xai-responses-opt-in.ts +15 -0
  617. package/src/providers/xai-transport.ts +148 -0
  618. package/src/reasoning-effort.ts +183 -0
  619. package/src/responses/compaction.ts +142 -0
  620. package/src/responses/custom-tool-compat.ts +266 -0
  621. package/src/responses/hosted-tool-policy.ts +9 -0
  622. package/src/responses/namespace-tool-compat.ts +355 -0
  623. package/src/responses/parser.ts +838 -0
  624. package/src/responses/provider-continuation.ts +98 -0
  625. package/src/responses/provider-opaque-metadata.ts +73 -0
  626. package/src/responses/reasoning-envelope.ts +60 -0
  627. package/src/responses/reasoning-replay-cache.ts +426 -0
  628. package/src/responses/schema.ts +165 -0
  629. package/src/responses/spill-store.ts +459 -0
  630. package/src/responses/state.ts +1433 -0
  631. package/src/responses/thought-signature-replay.ts +347 -0
  632. package/src/responses/tool-groups.ts +19 -0
  633. package/src/responses/tool-search-compat.ts +301 -0
  634. package/src/responses/truncated-stop-reason.ts +60 -0
  635. package/src/router.ts +761 -0
  636. package/src/routing/analytics.ts +378 -0
  637. package/src/routing/capability.ts +244 -0
  638. package/src/routing/compatibility/assemble.ts +73 -0
  639. package/src/routing/compatibility/behavior.ts +278 -0
  640. package/src/routing/compatibility/catalog.ts +99 -0
  641. package/src/routing/compatibility/endpoint.ts +52 -0
  642. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  643. package/src/routing/compatibility/policy.ts +181 -0
  644. package/src/routing/compatibility/provider-slot.ts +56 -0
  645. package/src/routing/compatibility/reader.ts +110 -0
  646. package/src/routing/compatibility/subject.ts +191 -0
  647. package/src/routing/compatibility/types.ts +64 -0
  648. package/src/routing/compatibility/version.ts +104 -0
  649. package/src/routing/cost.ts +77 -0
  650. package/src/routing/evaluator.ts +495 -0
  651. package/src/routing/health.ts +412 -0
  652. package/src/routing/history/cursor.ts +43 -0
  653. package/src/routing/history/indexer.ts +605 -0
  654. package/src/routing/history/schema.ts +72 -0
  655. package/src/routing/profile-namespace.ts +15 -0
  656. package/src/routing/profile.ts +547 -0
  657. package/src/routing/quota.ts +145 -0
  658. package/src/routing/request-evidence.ts +45 -0
  659. package/src/routing/trace.ts +776 -0
  660. package/src/server/adapter-resolve.ts +53 -0
  661. package/src/server/auth-cors.ts +751 -0
  662. package/src/server/background-lifecycle.ts +182 -0
  663. package/src/server/chat-completions.ts +442 -0
  664. package/src/server/chat-native-sse.ts +331 -0
  665. package/src/server/chat-native.ts +426 -0
  666. package/src/server/claude-messages.ts +1030 -0
  667. package/src/server/direct-local-http.ts +347 -0
  668. package/src/server/effort-policy.ts +190 -0
  669. package/src/server/github-copilot-responses-repair.ts +338 -0
  670. package/src/server/gui-static.ts +152 -0
  671. package/src/server/image-retry.ts +42 -0
  672. package/src/server/images.ts +568 -0
  673. package/src/server/index.ts +1813 -0
  674. package/src/server/lifecycle.ts +498 -0
  675. package/src/server/live.ts +717 -0
  676. package/src/server/local-management-read-client.ts +90 -0
  677. package/src/server/local-provider-reload-client.ts +137 -0
  678. package/src/server/management/agent-settings-routes.ts +1433 -0
  679. package/src/server/management/api-access.ts +141 -0
  680. package/src/server/management/api-key-usage.ts +193 -0
  681. package/src/server/management/body.ts +41 -0
  682. package/src/server/management/combo-routes.ts +263 -0
  683. package/src/server/management/config-routes.ts +835 -0
  684. package/src/server/management/context.ts +113 -0
  685. package/src/server/management/integration-routes.ts +498 -0
  686. package/src/server/management/lab-automation-routes.ts +206 -0
  687. package/src/server/management/lab-routes.ts +563 -0
  688. package/src/server/management/logs-usage-routes.ts +586 -0
  689. package/src/server/management/model-routes.ts +560 -0
  690. package/src/server/management/model-rows.ts +163 -0
  691. package/src/server/management/native-integration-routes.ts +769 -0
  692. package/src/server/management/oauth-account-routes.ts +637 -0
  693. package/src/server/management/provider-capability-config.ts +48 -0
  694. package/src/server/management/provider-routes.ts +1033 -0
  695. package/src/server/management/replit-provider-routes.ts +86 -0
  696. package/src/server/management/request-history-routes.ts +191 -0
  697. package/src/server/management/routing-analytics-routes.ts +74 -0
  698. package/src/server/management/routing-profile-routes.ts +385 -0
  699. package/src/server/management/shared.ts +286 -0
  700. package/src/server/management/sidebar-routes.ts +106 -0
  701. package/src/server/management/storage-log-guard-routes.ts +186 -0
  702. package/src/server/management/sync-response.ts +69 -0
  703. package/src/server/management/system-restart.ts +435 -0
  704. package/src/server/management/system-routes.ts +194 -0
  705. package/src/server/management/usage-summary-cache.ts +94 -0
  706. package/src/server/management/vision-sidecar-options.ts +167 -0
  707. package/src/server/management/web-search-sidecar-options.ts +120 -0
  708. package/src/server/management-api.ts +314 -0
  709. package/src/server/management-auth.ts +482 -0
  710. package/src/server/memory-watchdog.ts +156 -0
  711. package/src/server/passive-route-linker.ts +66 -0
  712. package/src/server/port-reclaim.ts +307 -0
  713. package/src/server/ports.ts +156 -0
  714. package/src/server/proxy-liveness.ts +328 -0
  715. package/src/server/readiness.ts +99 -0
  716. package/src/server/relay-eager.ts +353 -0
  717. package/src/server/relay.ts +1209 -0
  718. package/src/server/request-decompress.ts +239 -0
  719. package/src/server/request-log-conversation.ts +168 -0
  720. package/src/server/request-log.ts +1259 -0
  721. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  722. package/src/server/responses/agent-task-recovery.ts +465 -0
  723. package/src/server/responses/collaboration.ts +551 -0
  724. package/src/server/responses/compact.ts +771 -0
  725. package/src/server/responses/core.ts +5389 -0
  726. package/src/server/responses/empty-completion-guard.ts +276 -0
  727. package/src/server/responses/encrypted-payload.ts +331 -0
  728. package/src/server/responses/fetch-helpers.ts +232 -0
  729. package/src/server/responses/input-admission.ts +185 -0
  730. package/src/server/responses/pacing-overload.ts +13 -0
  731. package/src/server/responses/passthrough-error.ts +78 -0
  732. package/src/server/responses/policy-fallback.ts +178 -0
  733. package/src/server/responses/responses-field-backfill.ts +251 -0
  734. package/src/server/responses/terminal-guard.ts +251 -0
  735. package/src/server/responses/upstream-error.ts +53 -0
  736. package/src/server/responses/ws-upstream.ts +308 -0
  737. package/src/server/responses-custom-tool-repair.ts +282 -0
  738. package/src/server/responses-image-gen-repair.ts +132 -0
  739. package/src/server/responses-item-id-repair.ts +272 -0
  740. package/src/server/responses-json-events.ts +90 -0
  741. package/src/server/responses-model-rewrite.ts +29 -0
  742. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  743. package/src/server/responses-snapshot-repair.ts +621 -0
  744. package/src/server/responses-terminal-repair.ts +342 -0
  745. package/src/server/responses-tool-search-repair.ts +267 -0
  746. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  747. package/src/server/responses.ts +25 -0
  748. package/src/server/search.ts +201 -0
  749. package/src/server/sse-frame-buffer.ts +292 -0
  750. package/src/server/sse-payload-rewrite.ts +263 -0
  751. package/src/server/startup-action-control.ts +315 -0
  752. package/src/server/startup-health-cache.ts +131 -0
  753. package/src/server/system-env.ts +484 -0
  754. package/src/server/windows-tcp-drop.ts +184 -0
  755. package/src/server/windows-tray-control.ts +41 -0
  756. package/src/server/ws-bridge.ts +472 -0
  757. package/src/service-manager-probe.ts +892 -0
  758. package/src/service.ts +3575 -0
  759. package/src/sidecar/auth.ts +92 -0
  760. package/src/sidecar/candidates.ts +83 -0
  761. package/src/stall-timeout.ts +20 -0
  762. package/src/storage/cleanup-job.ts +57 -0
  763. package/src/storage/cleanup.ts +3085 -0
  764. package/src/storage/policy-job.ts +457 -0
  765. package/src/storage/policy-scheduler.ts +40 -0
  766. package/src/storage/policy-worker.ts +59 -0
  767. package/src/storage/policy.ts +527 -0
  768. package/src/storage/restore-job.ts +299 -0
  769. package/src/storage/restore-worker.ts +58 -0
  770. package/src/storage/scanner.ts +238 -0
  771. package/src/storage/storage-mutation-coordinator.ts +139 -0
  772. package/src/storage/worker-lifecycle.ts +215 -0
  773. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  774. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  775. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  776. package/src/tray/assets/opencodex-tray.png +0 -0
  777. package/src/tray/windows-tray.ps1 +364 -0
  778. package/src/tray/windows.ts +757 -0
  779. package/src/types/accounts.ts +37 -0
  780. package/src/types/config.ts +876 -0
  781. package/src/types/provider.ts +545 -0
  782. package/src/types/request.ts +384 -0
  783. package/src/types/tools.ts +131 -0
  784. package/src/types/wire.ts +80 -0
  785. package/src/types.ts +106 -0
  786. package/src/update/badge.ts +72 -0
  787. package/src/update/index.ts +415 -0
  788. package/src/update/job.ts +1887 -0
  789. package/src/update/notify.ts +263 -0
  790. package/src/update/npm-cache-preflight.d.mts +47 -0
  791. package/src/update/npm-cache-preflight.mjs +201 -0
  792. package/src/update/npm-invocation.d.mts +23 -0
  793. package/src/update/npm-invocation.mjs +94 -0
  794. package/src/update/transactional-install.d.mts +22 -0
  795. package/src/update/transactional-install.mjs +259 -0
  796. package/src/update/tray-update-plan.d.mts +18 -0
  797. package/src/update/tray-update-plan.mjs +38 -0
  798. package/src/usage/cost.ts +625 -0
  799. package/src/usage/debug.ts +97 -0
  800. package/src/usage/expected-prices.ts +416 -0
  801. package/src/usage/log.ts +1223 -0
  802. package/src/usage/summary.ts +753 -0
  803. package/src/usage/totals.ts +14 -0
  804. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  805. package/src/usage/user-cost-overlays.ts +314 -0
  806. package/src/vision/anthropic-describe.ts +189 -0
  807. package/src/vision/backends.ts +97 -0
  808. package/src/vision/describe.ts +131 -0
  809. package/src/vision/eligibility.ts +250 -0
  810. package/src/vision/index.ts +681 -0
  811. package/src/vision/reasoning.ts +55 -0
  812. package/src/vision/routed-describe.ts +175 -0
  813. package/src/vision/timeout-bounds.ts +9 -0
  814. package/src/web-search/anthropic-executor.ts +195 -0
  815. package/src/web-search/backends.ts +108 -0
  816. package/src/web-search/exa-executor.ts +88 -0
  817. package/src/web-search/executor.ts +113 -0
  818. package/src/web-search/format-result.ts +89 -0
  819. package/src/web-search/gemini-executor.ts +141 -0
  820. package/src/web-search/index.ts +331 -0
  821. package/src/web-search/loop.ts +896 -0
  822. package/src/web-search/parse.ts +315 -0
  823. package/src/web-search/progress-stream.ts +342 -0
  824. package/src/web-search/sources.ts +60 -0
  825. package/src/web-search/synthetic-tool.ts +47 -0
  826. package/src/web-search/xai-executor.ts +219 -0
@@ -0,0 +1,1223 @@
1
+ import { createHash, type Hash } from "node:crypto";
2
+ import { chmodSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, appendFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { getConfigDir } from "../config";
5
+ import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
6
+ import { recordOwnedConfigPath } from "../lib/config-ownership";
7
+ import { sanitizeLogMetadataString } from "../lib/redact";
8
+ import { usageDisplayTotalTokens } from "./totals";
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";
12
+
13
+ export type UsageStatus = "reported" | "unreported" | "unsupported" | "estimated";
14
+ export type CodexUsageAccountLogLabel = "main" | `p${string}`;
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
+ */
24
+ export type AttemptRecoveryKind =
25
+ | "transient-5xx"
26
+ | "connection-reset"
27
+ | "oauth-401"
28
+ | "key-429"
29
+ | "rate-limit-429"
30
+ | "anthropic-oauth-429"
31
+ | "image-413"
32
+ | "opaque-blob-rejection"
33
+ | "empty-completion";
34
+
35
+ export interface PersistedUsageAttempt {
36
+ ordinal: number;
37
+ provider: string;
38
+ model: string;
39
+ adapter: string;
40
+ status: number;
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;
48
+ /** TTFT relative to THIS attempt's start (WP4); unset for non-streaming/tool-only. */
49
+ firstOutputMs?: number;
50
+ sendCount: number;
51
+ recoveryKinds: AttemptRecoveryKind[];
52
+ usageStatus: UsageStatus;
53
+ /** Stable non-PII identity for the Codex pool account that served this attempt. */
54
+ accountLogLabel?: CodexUsageAccountLogLabel;
55
+ inputTokenEstimate?: number;
56
+ usage?: OcxUsage;
57
+ totalTokens?: number;
58
+ errorCode?: string;
59
+ /** Installation-local exact Compatibility Lab route-subject digest for this attempt. */
60
+ labRouteSubjectId?: string;
61
+ /** Target-specific reasoning intent and exact adapter-normalized wire parameter. */
62
+ requestedEffort?: string;
63
+ effectiveEffort?: string;
64
+ reasoningWireField?: string;
65
+ reasoningWireValue?: string | number | boolean;
66
+ /** Adapter-produced tier fact for this physical attempt; absent on pre-B0 rows. */
67
+ tierOutcome?: AttemptTierOutcome;
68
+ }
69
+
70
+ export interface PersistedUsageEntry {
71
+ requestId: string;
72
+ timestamp: number;
73
+ provider: string;
74
+ model: string;
75
+ surface?: "claude" | "claude-desktop" | "grok";
76
+ /** Matched configured key id; absent for environment/loopback admissions and
77
+ * for every row written before attribution existed. */
78
+ apiKeyId?: string;
79
+ admissionKind?: "configured" | "environment" | "loopback";
80
+ /** The inbound wire, not the client product — see `surface`. */
81
+ inboundProtocol?: "responses" | "chat" | "messages";
82
+ /** Stable non-PII identity for Codex Pool usage; absent for Direct/non-Codex traffic. */
83
+ accountLogLabel?: CodexUsageAccountLogLabel;
84
+ /** Best-effort chat/session correlation for Logs grouping (#330). */
85
+ conversationId?: string;
86
+ resolvedModel?: string;
87
+ requestedModel?: string;
88
+ /** Original bare helper model when the opt-in shadow-call route rewrote this request. */
89
+ shadowCallRewrittenFrom?: string;
90
+ /** Reasoning effort / service-tier metadata for GUI Logs after restart. */
91
+ requestedEffort?: string;
92
+ /** Adapter-normalized tier and exact upstream parameter emitted for this request. */
93
+ effectiveEffort?: string;
94
+ reasoningWireField?: string;
95
+ reasoningWireValue?: string | number | boolean;
96
+ /** Raw caller tier captured before routing, sanitized and bounded for durable logs. */
97
+ callerServiceTier?: string;
98
+ requestedServiceTier?: string;
99
+ requestedSpeedLabel?: string;
100
+ configuredServiceTier?: string;
101
+ configuredSpeedLabel?: string;
102
+ modelSupportsServiceTier?: boolean;
103
+ responseServiceTier?: string;
104
+ /** Summary of the final physical attempt for dashboard consumers. */
105
+ tierOutcome?: AttemptTierOutcome;
106
+ status: number;
107
+ durationMs: number;
108
+ /** TTFT relative to the request start (WP4); unset for non-streaming/tool-only. */
109
+ firstOutputMs?: number;
110
+ usageStatus: UsageStatus;
111
+ usage?: OcxUsage;
112
+ totalTokens?: number;
113
+ attempts?: PersistedUsageAttempt[];
114
+ // Failure diagnostics (devlog/_plan/260716_claudecode_hardening/030): persisted for
115
+ // status>=400 or non-completed terminals so incidents survive the in-memory ring buffer.
116
+ errorCode?: string;
117
+ terminalStatus?: string;
118
+ closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow";
119
+ /** Already redacted + capped at capture (request-log.ts redactSecretString().slice(0,500)). */
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;
127
+ }
128
+
129
+ const KNOWN_USAGE_SURFACES = new Set<NonNullable<PersistedUsageEntry["surface"]>>([
130
+ "claude",
131
+ "claude-desktop",
132
+ "grok",
133
+ ]);
134
+
135
+ /**
136
+ * The serializer guard for `surface`. Two failure modes shaped this: a literal
137
+ * whitelist ("claude" | "claude-desktop" only) silently dropped every NEW surface at
138
+ * write time, while a plain truthy spread would persist junk values from hand-edited
139
+ * logs. Membership in this set is the middle path: adding a surface here is one edit,
140
+ * and unknown values are still dropped.
141
+ */
142
+ export function isKnownUsageSurface(value: unknown): value is NonNullable<PersistedUsageEntry["surface"]> {
143
+ return typeof value === "string" && KNOWN_USAGE_SURFACES.has(value as NonNullable<PersistedUsageEntry["surface"]>);
144
+ }
145
+
146
+ const KNOWN_ADMISSION_KINDS = new Set<NonNullable<PersistedUsageEntry["admissionKind"]>>([
147
+ "configured", "environment", "loopback",
148
+ ]);
149
+
150
+ const KNOWN_INBOUND_PROTOCOLS = new Set<NonNullable<PersistedUsageEntry["inboundProtocol"]>>([
151
+ "responses", "chat", "messages",
152
+ ]);
153
+
154
+ /** Same closed-set discipline as `isKnownUsageSurface`: an old or corrupted row
155
+ * carrying an unexpected value drops the field instead of poisoning the enum. */
156
+ export function isKnownAdmissionKind(value: unknown): value is NonNullable<PersistedUsageEntry["admissionKind"]> {
157
+ return typeof value === "string" && KNOWN_ADMISSION_KINDS.has(value as NonNullable<PersistedUsageEntry["admissionKind"]>);
158
+ }
159
+
160
+ export function isKnownInboundProtocol(value: unknown): value is NonNullable<PersistedUsageEntry["inboundProtocol"]> {
161
+ return typeof value === "string" && KNOWN_INBOUND_PROTOCOLS.has(value as NonNullable<PersistedUsageEntry["inboundProtocol"]>);
162
+ }
163
+
164
+ export function usageLogPath(configDir?: string): string {
165
+ return join(configDir ?? getConfigDir(), "usage.jsonl");
166
+ }
167
+
168
+ export function usageTotalTokens(usage: OcxUsage | undefined): number | undefined {
169
+ return usageDisplayTotalTokens(usage);
170
+ }
171
+
172
+ /**
173
+ * Providers whose adapters can only estimate usage (no authoritative per-turn frame).
174
+ * Callers should pass the route ADAPTER when available; the name-prefix match is a
175
+ * fallback for paths that only know the configured provider name (e.g. "cursor-mykey").
176
+ */
177
+ function isEstimatedUsageProvider(providerOrAdapter: string): boolean {
178
+ return providerOrAdapter === "kiro" || providerOrAdapter.startsWith("kiro-")
179
+ || providerOrAdapter === "cursor" || providerOrAdapter.startsWith("cursor-");
180
+ }
181
+
182
+ export function usageForFinalLog(provider: string, usage: OcxUsage | undefined): OcxUsage | undefined {
183
+ if (!usage) return undefined;
184
+ if (usage.estimated || isEstimatedUsageProvider(provider)) return { ...usage, estimated: true };
185
+ return usage;
186
+ }
187
+
188
+ export function usageStatusForFinalLog(usage: OcxUsage | undefined): UsageStatus {
189
+ if (!usage) return "unreported";
190
+ return usage.estimated ? "estimated" : "reported";
191
+ }
192
+
193
+ function normalizeUsageValue(usage: OcxUsage | undefined): OcxUsage | undefined {
194
+ if (!usage) return undefined;
195
+ return {
196
+ inputTokens: usage.inputTokens,
197
+ outputTokens: usage.outputTokens,
198
+ // Absolute active-context checkpoint (types.ts). Stateful providers such as Kiro report
199
+ // per-attempt usage only, so this field is the ONLY carrier of the cumulative context
200
+ // figure once the log records raw adapter usage instead of re-parsing the bridged wire
201
+ // (usageFromBridge, request-log.ts). Omitting it here silently dropped Kiro's context
202
+ // growth from every persisted row. It is deliberately NOT folded into totalTokens:
203
+ // a checkpoint is not a per-request total and must never be summed across requests.
204
+ ...(typeof usage.contextTotalTokens === "number" ? { contextTotalTokens: usage.contextTotalTokens } : {}),
205
+ ...(typeof usage.totalTokens === "number" ? { totalTokens: usage.totalTokens } : {}),
206
+ ...(typeof usage.cachedInputTokens === "number" ? { cachedInputTokens: usage.cachedInputTokens } : {}),
207
+ ...(typeof usage.cacheReadInputTokens === "number" ? { cacheReadInputTokens: usage.cacheReadInputTokens } : {}),
208
+ ...(typeof usage.cacheCreationInputTokens === "number" ? { cacheCreationInputTokens: usage.cacheCreationInputTokens } : {}),
209
+ ...(typeof usage.reasoningOutputTokens === "number" ? { reasoningOutputTokens: usage.reasoningOutputTokens } : {}),
210
+ ...(usage.estimated ? { estimated: true } : {}),
211
+ };
212
+ }
213
+
214
+ const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
215
+ "transient-5xx",
216
+ "connection-reset",
217
+ "oauth-401",
218
+ "key-429",
219
+ "rate-limit-429",
220
+ "anthropic-oauth-429",
221
+ "image-413",
222
+ "opaque-blob-rejection",
223
+ "empty-completion",
224
+ ]);
225
+ const USAGE_STATUSES = new Set<UsageStatus>([
226
+ "reported",
227
+ "unreported",
228
+ "unsupported",
229
+ "estimated",
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
+ }
245
+
246
+ function isNonNegativeFiniteNumber(value: unknown): value is number {
247
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
248
+ }
249
+
250
+ function normalizeAttemptUsage(raw: unknown): OcxUsage | null {
251
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
252
+ const usage = raw as Record<string, unknown>;
253
+ if (!isNonNegativeFiniteNumber(usage.inputTokens)
254
+ || !isNonNegativeFiniteNumber(usage.outputTokens)) return null;
255
+ for (const key of [
256
+ "contextTotalTokens",
257
+ "totalTokens",
258
+ "cachedInputTokens",
259
+ "cacheReadInputTokens",
260
+ "cacheCreationInputTokens",
261
+ "reasoningOutputTokens",
262
+ ] as const) {
263
+ if (key in usage && !isNonNegativeFiniteNumber(usage[key])) return null;
264
+ }
265
+ if ("estimated" in usage && typeof usage.estimated !== "boolean") return null;
266
+ return normalizeUsageValue(usage as unknown as OcxUsage) ?? null;
267
+ }
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
+
316
+ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
317
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
318
+ const attempt = raw as Record<string, unknown>;
319
+ if (typeof attempt.ordinal !== "number" || !Number.isInteger(attempt.ordinal)
320
+ || attempt.ordinal < 1
321
+ || typeof attempt.provider !== "string" || !attempt.provider
322
+ || typeof attempt.model !== "string" || !attempt.model
323
+ || typeof attempt.adapter !== "string" || !attempt.adapter
324
+ || typeof attempt.status !== "number" || !Number.isInteger(attempt.status)
325
+ || attempt.status < 100 || attempt.status > 599
326
+ || typeof attempt.durationMs !== "number" || !Number.isFinite(attempt.durationMs)
327
+ || attempt.durationMs < 0
328
+ || typeof attempt.sendCount !== "number" || !Number.isInteger(attempt.sendCount)
329
+ || attempt.sendCount < 0
330
+ || typeof attempt.usageStatus !== "string"
331
+ || !USAGE_STATUSES.has(attempt.usageStatus as UsageStatus)) {
332
+ return null;
333
+ }
334
+ if ("inputTokenEstimate" in attempt
335
+ && !isNonNegativeFiniteNumber(attempt.inputTokenEstimate)) return null;
336
+ if ("firstOutputMs" in attempt
337
+ && !isNonNegativeFiniteNumber(attempt.firstOutputMs)) return null;
338
+ if ("totalTokens" in attempt
339
+ && !isNonNegativeFiniteNumber(attempt.totalTokens)) return null;
340
+ const usage = "usage" in attempt ? normalizeAttemptUsage(attempt.usage) : undefined;
341
+ if ("usage" in attempt && usage === null) return null;
342
+ const tierOutcome = "tierOutcome" in attempt
343
+ ? normalizeAttemptTierOutcome(attempt.tierOutcome)
344
+ : undefined;
345
+ const recoveryKinds = Array.isArray(attempt.recoveryKinds)
346
+ ? [...new Set(attempt.recoveryKinds.filter(
347
+ (value): value is AttemptRecoveryKind => typeof value === "string"
348
+ && ATTEMPT_RECOVERY_KINDS.has(value as AttemptRecoveryKind),
349
+ ))]
350
+ : [];
351
+ return {
352
+ ordinal: attempt.ordinal as number,
353
+ provider: attempt.provider,
354
+ model: attempt.model,
355
+ adapter: attempt.adapter,
356
+ status: attempt.status,
357
+ durationMs: attempt.durationMs,
358
+ // Absent by default; only the literal `true` marker survives the round trip.
359
+ ...(attempt.streamAborted === true ? { streamAborted: true } : {}),
360
+ ...(isNonNegativeFiniteNumber(attempt.firstOutputMs)
361
+ ? { firstOutputMs: attempt.firstOutputMs }
362
+ : {}),
363
+ sendCount: attempt.sendCount as number,
364
+ recoveryKinds,
365
+ usageStatus: attempt.usageStatus as UsageStatus,
366
+ ...(isCodexUsageAccountLogLabel(attempt.accountLogLabel)
367
+ ? { accountLogLabel: attempt.accountLogLabel }
368
+ : {}),
369
+ ...(isNonNegativeFiniteNumber(attempt.inputTokenEstimate)
370
+ ? { inputTokenEstimate: attempt.inputTokenEstimate }
371
+ : {}),
372
+ ...(usage ? { usage } : {}),
373
+ ...(isNonNegativeFiniteNumber(attempt.totalTokens)
374
+ ? { totalTokens: attempt.totalTokens }
375
+ : {}),
376
+ ...(typeof attempt.errorCode === "string" ? { errorCode: attempt.errorCode } : {}),
377
+ ...(isLabRouteSubjectId(attempt.labRouteSubjectId)
378
+ ? { labRouteSubjectId: attempt.labRouteSubjectId }
379
+ : {}),
380
+ ...(typeof attempt.requestedEffort === "string" && attempt.requestedEffort
381
+ ? { requestedEffort: capMetadataString(attempt.requestedEffort) }
382
+ : {}),
383
+ ...(typeof attempt.effectiveEffort === "string" && attempt.effectiveEffort
384
+ ? { effectiveEffort: capMetadataString(attempt.effectiveEffort) }
385
+ : {}),
386
+ ...(typeof attempt.reasoningWireField === "string" && attempt.reasoningWireField
387
+ ? { reasoningWireField: capMetadataString(attempt.reasoningWireField) }
388
+ : {}),
389
+ ...(isValidReasoningWireValue(attempt.reasoningWireField, attempt.reasoningWireValue)
390
+ ? typeof attempt.reasoningWireValue === "string"
391
+ ? { reasoningWireValue: capMetadataString(attempt.reasoningWireValue) }
392
+ : { reasoningWireValue: attempt.reasoningWireValue }
393
+ : {}),
394
+ ...(tierOutcome ? { tierOutcome } : {}),
395
+ };
396
+ }
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
+
413
+ function normalizedAttempts(raw: unknown): PersistedUsageAttempt[] {
414
+ if (!Array.isArray(raw)) return [];
415
+ return raw.map(normalizeUsageAttempt)
416
+ .filter((attempt): attempt is PersistedUsageAttempt => attempt !== null);
417
+ }
418
+
419
+ const MAX_METADATA_STRING_LEN = 64;
420
+ function capMetadataString(s: string): string {
421
+ return s.length > MAX_METADATA_STRING_LEN ? s.slice(0, MAX_METADATA_STRING_LEN) : s;
422
+ }
423
+
424
+ /** Test seam: the normalization branch old rows take is worth asserting directly. */
425
+ export function normalizeUsageEntryForTest(entry: PersistedUsageEntry): PersistedUsageEntry {
426
+ return normalizeUsageEntry(entry);
427
+ }
428
+
429
+ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
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;
438
+ return {
439
+ requestId: entry.requestId,
440
+ timestamp: entry.timestamp,
441
+ provider: entry.provider,
442
+ model: entry.model,
443
+ ...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
444
+ ...(typeof entry.apiKeyId === "string" && entry.apiKeyId.trim()
445
+ // Deliberately NOT capped. `capMetadataString` protects free-form metadata
446
+ // from unbounded growth, but this is a lookup key: truncating it makes the
447
+ // persisted id stop matching the configured one, and the rollup silently
448
+ // reports zero for a key that is very much in use.
449
+ ? { apiKeyId: entry.apiKeyId }
450
+ : {}),
451
+ ...(isKnownAdmissionKind(entry.admissionKind) ? { admissionKind: entry.admissionKind } : {}),
452
+ ...(isKnownInboundProtocol(entry.inboundProtocol) ? { inboundProtocol: entry.inboundProtocol } : {}),
453
+ ...(isCodexUsageAccountLogLabel(entry.accountLogLabel)
454
+ ? { accountLogLabel: entry.accountLogLabel }
455
+ : {}),
456
+ ...(typeof entry.conversationId === "string" && entry.conversationId.trim()
457
+ ? { conversationId: entry.conversationId.trim().slice(0, 128) }
458
+ : {}),
459
+ ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
460
+ ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
461
+ ...(shadowCallRewrittenFrom ? { shadowCallRewrittenFrom } : {}),
462
+ ...(typeof entry.requestedEffort === "string" && entry.requestedEffort
463
+ ? { requestedEffort: capMetadataString(entry.requestedEffort) }
464
+ : {}),
465
+ ...(typeof entry.effectiveEffort === "string" && entry.effectiveEffort
466
+ ? { effectiveEffort: capMetadataString(entry.effectiveEffort) }
467
+ : {}),
468
+ ...(typeof entry.reasoningWireField === "string" && entry.reasoningWireField
469
+ ? { reasoningWireField: capMetadataString(entry.reasoningWireField) }
470
+ : {}),
471
+ ...(isValidReasoningWireValue(entry.reasoningWireField, entry.reasoningWireValue)
472
+ ? typeof entry.reasoningWireValue === "string"
473
+ ? { reasoningWireValue: capMetadataString(entry.reasoningWireValue) }
474
+ : { reasoningWireValue: entry.reasoningWireValue }
475
+ : {}),
476
+ ...(callerServiceTier ? { callerServiceTier } : {}),
477
+ ...(typeof entry.requestedServiceTier === "string" && entry.requestedServiceTier
478
+ ? { requestedServiceTier: capMetadataString(entry.requestedServiceTier) }
479
+ : {}),
480
+ ...(typeof entry.requestedSpeedLabel === "string" && entry.requestedSpeedLabel
481
+ ? { requestedSpeedLabel: capMetadataString(entry.requestedSpeedLabel) }
482
+ : {}),
483
+ ...(typeof entry.configuredServiceTier === "string" && entry.configuredServiceTier
484
+ ? { configuredServiceTier: capMetadataString(entry.configuredServiceTier) }
485
+ : {}),
486
+ ...(typeof entry.configuredSpeedLabel === "string" && entry.configuredSpeedLabel
487
+ ? { configuredSpeedLabel: capMetadataString(entry.configuredSpeedLabel) }
488
+ : {}),
489
+ ...(typeof entry.modelSupportsServiceTier === "boolean"
490
+ ? { modelSupportsServiceTier: entry.modelSupportsServiceTier }
491
+ : {}),
492
+ ...(responseServiceTier ? { responseServiceTier } : {}),
493
+ ...(tierOutcome ? { tierOutcome } : {}),
494
+ status: entry.status,
495
+ durationMs: entry.durationMs,
496
+ ...(isNonNegativeFiniteNumber(entry.firstOutputMs)
497
+ ? { firstOutputMs: entry.firstOutputMs }
498
+ : {}),
499
+ usageStatus: entry.usageStatus,
500
+ ...(entry.usage ? { usage: normalizeUsageValue(entry.usage) } : {}),
501
+ ...(typeof entry.totalTokens === "number" ? { totalTokens: entry.totalTokens } : {}),
502
+ ...(Array.isArray(entry.attempts) ? { attempts } : {}),
503
+ ...(entry.errorCode ? { errorCode: entry.errorCode } : {}),
504
+ ...(entry.terminalStatus ? { terminalStatus: entry.terminalStatus } : {}),
505
+ ...(entry.closeReason ? { closeReason: entry.closeReason } : {}),
506
+ ...(entry.upstreamError ? { upstreamError: entry.upstreamError } : {}),
507
+ ...(routeDecision ? { routeDecision } : {}),
508
+ };
509
+ }
510
+
511
+ function ensureUsageLogDir(): void {
512
+ const dir = getConfigDir();
513
+ recordOwnedConfigPath(dir, usageLogPath());
514
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
515
+ try { chmodSync(dir, 0o700); } catch { /* best-effort on platforms that ignore chmod */ }
516
+ }
517
+
518
+ export function appendUsageEntry(entry: PersistedUsageEntry): void {
519
+ ensureUsageLogDir();
520
+ const path = usageLogPath();
521
+ appendFileSync(path, `${JSON.stringify(normalizeUsageEntry(entry))}\n`, { encoding: "utf-8", mode: 0o600 });
522
+ try { chmodSync(path, 0o600); } catch { /* best-effort on platforms that ignore chmod */ }
523
+ }
524
+
525
+ export type UsageLogRevision = {
526
+ path: string;
527
+ dev: number;
528
+ ino: number;
529
+ birthtimeMs: number;
530
+ size: number;
531
+ mtimeMs: number;
532
+ ctimeMs: number;
533
+ };
534
+
535
+ let usageReadCacheStats = { fullReads: 0, tailReads: 0, parsedLines: 0 };
536
+ const MANAGEMENT_USAGE_MAX_READ_BYTES = 64 * 1024 * 1024;
537
+ const RECENT_USAGE_MAX_READ_BYTES = 64 * 1024 * 1024;
538
+ const MANAGEMENT_USAGE_READ_CHUNK_BYTES = 1024 * 1024;
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
+ }
549
+ const MANAGEMENT_USAGE_FLIGHT_STALE_MS = 30_000;
550
+ export interface ManagementUsageSnapshot {
551
+ entries: PersistedUsageEntry[];
552
+ revision: UsageLogRevision;
553
+ truncatedPrefixBytes: number;
554
+ entriesTruncated: boolean;
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;
577
+ }
578
+ let managementUsageReadInflight: {
579
+ key: string;
580
+ openedSize: number;
581
+ promise: Promise<ManagementUsageSnapshot>;
582
+ startedAt: number;
583
+ abort: AbortController;
584
+ } | null = null;
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
+
706
+ /** Test-only observability for proving that unchanged prefixes are not reparsed. */
707
+ export function usageReadCacheStatsForTests(): Readonly<typeof usageReadCacheStats> {
708
+ return { ...usageReadCacheStats };
709
+ }
710
+
711
+ export function resetUsageReadCacheForTests(): void {
712
+ usageReadCacheStats = { fullReads: 0, tailReads: 0, parsedLines: 0 };
713
+ managementUsageReadInflight?.abort.abort();
714
+ managementUsageReadInflight = null;
715
+ retainedUsageSnapshot = null;
716
+ }
717
+
718
+ function readExactly(fd: number, length: number, position: number): Buffer | null {
719
+ const output = Buffer.allocUnsafe(length);
720
+ let offset = 0;
721
+ while (offset < length) {
722
+ const read = readSync(fd, output, offset, length - offset, position + offset);
723
+ if (read === 0) return null;
724
+ offset += read;
725
+ }
726
+ return output;
727
+ }
728
+
729
+ function usageLogRevision(path: string, stat: ReturnType<typeof fstatSync>): UsageLogRevision {
730
+ if (!stat.isFile()) throw new Error("usage log is not a regular file");
731
+ return {
732
+ path,
733
+ dev: Number(stat.dev),
734
+ ino: Number(stat.ino),
735
+ birthtimeMs: Number(stat.birthtimeMs),
736
+ size: Number(stat.size),
737
+ mtimeMs: Number(stat.mtimeMs),
738
+ ctimeMs: Number(stat.ctimeMs),
739
+ };
740
+ }
741
+
742
+ export function usageLogRevisionKey(revision: UsageLogRevision | null): string {
743
+ if (!revision) return "missing";
744
+ return [
745
+ revision.path,
746
+ revision.dev,
747
+ revision.ino,
748
+ revision.birthtimeMs,
749
+ revision.size,
750
+ revision.mtimeMs,
751
+ revision.ctimeMs,
752
+ ].join("\0");
753
+ }
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
+
761
+ export function currentUsageLogRevision(): UsageLogRevision | null {
762
+ const path = usageLogPath();
763
+ if (!existsSync(path)) return null;
764
+ let fd: number | undefined;
765
+ try {
766
+ fd = openSync(path, "r");
767
+ return usageLogRevision(path, fstatSync(fd));
768
+ } finally {
769
+ if (fd !== undefined) closeSync(fd);
770
+ }
771
+ }
772
+
773
+ async function parseUsageTextCooperatively(text: string, signal: AbortSignal): Promise<{
774
+ entries: PersistedUsageEntry[];
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;
781
+ }> {
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");
787
+ usageReadCacheStats.parsedLines += lines.filter(line => line.trim()).length;
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[] = [];
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;
800
+ for (let offset = 0; offset < lines.length; offset += batchSize) {
801
+ if (signal.aborted) throw signal.reason;
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
+ }
817
+ if (offset + batchSize < lines.length) {
818
+ // JSON parsing dominates large-log startup. Yield between bounded batches so
819
+ // Bun can continue serving health and settings requests on the same thread.
820
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
821
+ }
822
+ }
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
+ }
829
+ const entriesDropped = entries.length - MANAGEMENT_USAGE_MAX_ENTRIES;
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
+ };
841
+ }
842
+
843
+ async function readUsageEntriesFullCooperatively(
844
+ path: string,
845
+ signal: AbortSignal,
846
+ maxReadBytes: number,
847
+ ): Promise<ManagementUsageSnapshot> {
848
+ let fd: number | undefined;
849
+ try {
850
+ fd = openSync(path, "r");
851
+ const stat = fstatSync(fd);
852
+ const size = Number(stat.size);
853
+ const start = Math.max(0, size - maxReadBytes);
854
+ const chunks: Buffer[] = [];
855
+ for (let position = start; position < size;) {
856
+ if (signal.aborted) throw signal.reason;
857
+ const length = Math.min(MANAGEMENT_USAGE_READ_CHUNK_BYTES, size - position);
858
+ const chunk = readExactly(fd, length, position);
859
+ if (chunk === null) throw new Error("usage log changed while it was being read");
860
+ chunks.push(chunk);
861
+ position += length;
862
+ }
863
+ let bytes = Buffer.concat(chunks);
864
+ let truncatedPrefixBytes = start;
865
+ if (start > 0) {
866
+ const preceding = readExactly(fd, 1, start - 1);
867
+ if (preceding === null) throw new Error("usage log changed while it was being read");
868
+ if (preceding[0] !== 0x0a) {
869
+ const newline = bytes.indexOf(0x0a);
870
+ if (newline < 0) {
871
+ truncatedPrefixBytes += bytes.byteLength;
872
+ bytes = Buffer.alloc(0);
873
+ } else {
874
+ truncatedPrefixBytes += newline + 1;
875
+ bytes = bytes.subarray(newline + 1);
876
+ }
877
+ }
878
+ }
879
+ const parsed = await parseUsageTextCooperatively(bytes.toString("utf-8"), signal);
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");
890
+ return {
891
+ entries: parsed.entries,
892
+ revision: usageLogRevision(path, stat),
893
+ truncatedPrefixBytes,
894
+ entriesTruncated: parsed.entriesDropped > 0,
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,
1056
+ };
1057
+ } finally {
1058
+ if (fd !== undefined) closeSync(fd);
1059
+ }
1060
+ }
1061
+
1062
+ /**
1063
+ * Management API reader: full parses yield between bounded batches and concurrent
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.
1068
+ */
1069
+ export async function readUsageSnapshotForManagement(maxReadBytes = MANAGEMENT_USAGE_MAX_READ_BYTES): Promise<{
1070
+ entries: PersistedUsageEntry[];
1071
+ revision: UsageLogRevision | null;
1072
+ truncatedPrefixBytes: number;
1073
+ entriesTruncated: boolean;
1074
+ entriesDropped: number;
1075
+ }> {
1076
+ if (!Number.isSafeInteger(maxReadBytes) || maxReadBytes <= 0) throw new RangeError("management usage max read bytes must be positive");
1077
+ const path = usageLogPath();
1078
+ if (!existsSync(path)) return { entries: [], revision: null, truncatedPrefixBytes: 0, entriesTruncated: false, entriesDropped: 0 };
1079
+ const observed = currentUsageLogRevision();
1080
+ const key = `${usageLogIdentityKey(observed)}\0${maxReadBytes}`;
1081
+ const observedSize = observed?.size ?? 0;
1082
+ const existing = managementUsageReadInflight;
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) {
1091
+ const shared = await existing.promise;
1092
+ return { ...shared, entries: shared.entries.slice() };
1093
+ }
1094
+ const abort = new AbortController();
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 };
1112
+ try {
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();
1131
+ return { ...snapshot, entries: snapshot.entries.slice() };
1132
+ } finally {
1133
+ if (managementUsageReadInflight?.promise === promise) managementUsageReadInflight = null;
1134
+ }
1135
+ }
1136
+
1137
+ export async function readUsageEntriesForManagement(): Promise<PersistedUsageEntry[]> {
1138
+ return (await readUsageSnapshotForManagement()).entries;
1139
+ }
1140
+
1141
+ export function readUsageEntries(): PersistedUsageEntry[] {
1142
+ const path = usageLogPath();
1143
+ if (!existsSync(path)) return [];
1144
+ const lines = readFileSync(path, "utf-8").split(/\r?\n/);
1145
+ const entries: PersistedUsageEntry[] = [];
1146
+ for (const line of lines) {
1147
+ if (!line.trim()) continue;
1148
+ try {
1149
+ const parsed = JSON.parse(line) as PersistedUsageEntry;
1150
+ if (parsed && typeof parsed === "object" && typeof parsed.requestId === "string") {
1151
+ entries.push(normalizeUsageEntry(parsed));
1152
+ }
1153
+ } catch {
1154
+ /* keep reading after a partially written or hand-edited line */
1155
+ }
1156
+ }
1157
+ return entries;
1158
+ }
1159
+
1160
+ function parseUsageLines(lines: string[]): PersistedUsageEntry[] {
1161
+ const entries: PersistedUsageEntry[] = [];
1162
+ for (const line of lines) {
1163
+ if (!line.trim()) continue;
1164
+ try {
1165
+ const parsed = JSON.parse(line) as PersistedUsageEntry;
1166
+ if (parsed && typeof parsed === "object" && typeof parsed.requestId === "string") {
1167
+ entries.push(normalizeUsageEntry(parsed));
1168
+ }
1169
+ } catch {
1170
+ /* skip partial / hand-edited lines */
1171
+ }
1172
+ }
1173
+ return entries;
1174
+ }
1175
+
1176
+ /**
1177
+ * Read only the newest `limit` usage.jsonl rows without loading the whole append-only
1178
+ * file into memory. Used by request-log hydration on `ocx start`.
1179
+ */
1180
+ export function readRecentUsageEntries(limit: number, configDir?: string): PersistedUsageEntry[] {
1181
+ if (!Number.isFinite(limit) || limit <= 0) return [];
1182
+ const path = usageLogPath(configDir);
1183
+ if (!existsSync(path)) return [];
1184
+ let fd: number | undefined;
1185
+ try {
1186
+ fd = openSync(path, "r");
1187
+ const size = fstatSync(fd).size;
1188
+ if (size <= 0) return [];
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) {
1194
+ const start = Math.max(0, size - windowBytes);
1195
+ const buf = Buffer.alloc(size - start);
1196
+ readSync(fd, buf, 0, buf.length, start);
1197
+ let text = buf.toString("utf-8");
1198
+ if (start > 0) {
1199
+ const nl = text.indexOf("\n");
1200
+ if (nl < 0) {
1201
+ if (start === 0 || windowBytes >= maxWindowBytes) break;
1202
+ windowBytes = Math.min(maxWindowBytes, windowBytes * 4);
1203
+ continue;
1204
+ }
1205
+ text = text.slice(nl + 1);
1206
+ }
1207
+ const lines = text.split(/\r?\n/).filter(line => line.trim());
1208
+ // Parse ALL lines first, then take the last N valid entries. This way corrupt
1209
+ // or partial lines are filtered out during parsing and we always return the
1210
+ // most recent N valid rows (not N physical lines minus corrupt ones).
1211
+ const entries = parseUsageLines(lines);
1212
+ if (entries.length >= limit || start === 0 || windowBytes >= maxWindowBytes) return entries.slice(-limit);
1213
+ windowBytes = Math.min(maxWindowBytes, windowBytes * 4);
1214
+ }
1215
+ return [];
1216
+ } catch {
1217
+ return [];
1218
+ } finally {
1219
+ if (fd !== undefined) {
1220
+ try { closeSync(fd); } catch { /* ignore */ }
1221
+ }
1222
+ }
1223
+ }