@iislee/opencodex 2.11.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 (476) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/assets/architecture.png +0 -0
  4. package/assets/banner.png +0 -0
  5. package/assets/claude-code-models.gif +0 -0
  6. package/assets/codex-app-picker.png +0 -0
  7. package/bin/ocx.mjs +451 -0
  8. package/bin/package-main.mjs +9 -0
  9. package/gui/dist/assets/index-DTpMHS4F.js +67 -0
  10. package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
  11. package/gui/dist/favicon.png +0 -0
  12. package/gui/dist/icons.svg +24 -0
  13. package/gui/dist/index.html +25 -0
  14. package/gui/dist/logo.png +0 -0
  15. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  16. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/claude.svg +1 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/cursor.svg +2 -0
  25. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  26. package/gui/dist/provider-icons/discord.svg +1 -0
  27. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  28. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  29. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok-color.svg +1 -0
  34. package/gui/dist/provider-icons/grok.svg +1 -0
  35. package/gui/dist/provider-icons/groq-color.svg +1 -0
  36. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  37. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  38. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  39. package/gui/dist/provider-icons/kiro.svg +14 -0
  40. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  41. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  42. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  43. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  44. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  45. package/gui/dist/provider-icons/openai.svg +1 -0
  46. package/gui/dist/provider-icons/opencode.svg +1 -0
  47. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  48. package/gui/dist/provider-icons/pi.svg +21 -0
  49. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  50. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  51. package/gui/dist/provider-icons/telegram.svg +1 -0
  52. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  53. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  54. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  55. package/package.json +102 -0
  56. package/src/AGENTS.md +28 -0
  57. package/src/adapters/anthropic-image-guard.ts +251 -0
  58. package/src/adapters/anthropic-image-normalize.ts +518 -0
  59. package/src/adapters/anthropic.ts +1003 -0
  60. package/src/adapters/azure.ts +36 -0
  61. package/src/adapters/base.ts +72 -0
  62. package/src/adapters/client-fingerprint.ts +59 -0
  63. package/src/adapters/cursor/arg-codec.ts +38 -0
  64. package/src/adapters/cursor/arg-normalize.ts +104 -0
  65. package/src/adapters/cursor/cursor-errors.ts +165 -0
  66. package/src/adapters/cursor/discovery.ts +276 -0
  67. package/src/adapters/cursor/effort-map.ts +127 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +211 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/kv-store.ts +52 -0
  72. package/src/adapters/cursor/live-models.ts +153 -0
  73. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  74. package/src/adapters/cursor/live-transport.ts +1214 -0
  75. package/src/adapters/cursor/mcp-config.ts +42 -0
  76. package/src/adapters/cursor/mcp-manager.ts +333 -0
  77. package/src/adapters/cursor/message-mapper.ts +49 -0
  78. package/src/adapters/cursor/native-exec-common.ts +55 -0
  79. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  80. package/src/adapters/cursor/native-exec-fs.ts +329 -0
  81. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  82. package/src/adapters/cursor/native-exec-network.ts +43 -0
  83. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  84. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  85. package/src/adapters/cursor/native-exec.ts +576 -0
  86. package/src/adapters/cursor/protobuf-events.ts +563 -0
  87. package/src/adapters/cursor/protobuf-request.ts +714 -0
  88. package/src/adapters/cursor/request-builder.ts +255 -0
  89. package/src/adapters/cursor/thread-continuity.ts +67 -0
  90. package/src/adapters/cursor/tool-definitions.ts +505 -0
  91. package/src/adapters/cursor/transport-retry.ts +132 -0
  92. package/src/adapters/cursor/transport.ts +57 -0
  93. package/src/adapters/cursor/types.ts +52 -0
  94. package/src/adapters/cursor.ts +196 -0
  95. package/src/adapters/google-antigravity-replay.ts +303 -0
  96. package/src/adapters/google-antigravity-wire.ts +108 -0
  97. package/src/adapters/google-errors.ts +85 -0
  98. package/src/adapters/google-http.ts +100 -0
  99. package/src/adapters/google-tool-schema.ts +173 -0
  100. package/src/adapters/google-truncation.ts +13 -0
  101. package/src/adapters/google-wire-compiler.ts +232 -0
  102. package/src/adapters/google.ts +758 -0
  103. package/src/adapters/identity.ts +44 -0
  104. package/src/adapters/image.ts +23 -0
  105. package/src/adapters/kiro-constants.ts +16 -0
  106. package/src/adapters/kiro-errors.ts +197 -0
  107. package/src/adapters/kiro-events.ts +179 -0
  108. package/src/adapters/kiro-images.ts +129 -0
  109. package/src/adapters/kiro-retry.ts +312 -0
  110. package/src/adapters/kiro-thinking.ts +96 -0
  111. package/src/adapters/kiro-tool-fallback.ts +36 -0
  112. package/src/adapters/kiro-tools.ts +215 -0
  113. package/src/adapters/kiro-truncation.ts +33 -0
  114. package/src/adapters/kiro-wire.ts +129 -0
  115. package/src/adapters/kiro.ts +1898 -0
  116. package/src/adapters/mimo-free.ts +263 -0
  117. package/src/adapters/openai-chat.ts +1005 -0
  118. package/src/adapters/openai-responses.ts +1137 -0
  119. package/src/adapters/run-turn-queue.ts +114 -0
  120. package/src/adapters/tool-catalog-nudge.ts +71 -0
  121. package/src/adapters/upstream-http-error.ts +48 -0
  122. package/src/bridge.ts +1619 -0
  123. package/src/chat/inbound.ts +295 -0
  124. package/src/chat/outbound.ts +765 -0
  125. package/src/claude/agents-inject.ts +243 -0
  126. package/src/claude/alias.ts +149 -0
  127. package/src/claude/auth-detect.ts +229 -0
  128. package/src/claude/auth-mode-migration.ts +32 -0
  129. package/src/claude/auth-mode.ts +62 -0
  130. package/src/claude/context-windows.ts +189 -0
  131. package/src/claude/desktop-3p-guard.ts +35 -0
  132. package/src/claude/desktop-3p-paths.ts +84 -0
  133. package/src/claude/desktop-3p.ts +381 -0
  134. package/src/claude/desktop-health.ts +26 -0
  135. package/src/claude/desktop-profile.ts +263 -0
  136. package/src/claude/gateway-cache.ts +70 -0
  137. package/src/claude/inbound-debug.ts +163 -0
  138. package/src/claude/inbound.ts +509 -0
  139. package/src/claude/model-info.ts +151 -0
  140. package/src/claude/outbound.ts +872 -0
  141. package/src/cli/access.ts +108 -0
  142. package/src/cli/account-api.ts +268 -0
  143. package/src/cli/account-auth.ts +223 -0
  144. package/src/cli/account-extended.ts +350 -0
  145. package/src/cli/account.ts +275 -0
  146. package/src/cli/agent-driven.ts +70 -0
  147. package/src/cli/agent.ts +184 -0
  148. package/src/cli/catalog-prewarm.ts +27 -0
  149. package/src/cli/claude-desktop.ts +188 -0
  150. package/src/cli/claude.ts +286 -0
  151. package/src/cli/codex-shim-autorestore.ts +45 -0
  152. package/src/cli/combo.ts +119 -0
  153. package/src/cli/config-command.ts +145 -0
  154. package/src/cli/debug.ts +228 -0
  155. package/src/cli/doctor.ts +930 -0
  156. package/src/cli/export-command.ts +187 -0
  157. package/src/cli/help.ts +354 -0
  158. package/src/cli/index.ts +1113 -0
  159. package/src/cli/init.ts +224 -0
  160. package/src/cli/integrations.ts +142 -0
  161. package/src/cli/interactive-confirm.ts +133 -0
  162. package/src/cli/internal-dispatch.ts +20 -0
  163. package/src/cli/models-runtime.ts +212 -0
  164. package/src/cli/models.ts +336 -0
  165. package/src/cli/observe.ts +117 -0
  166. package/src/cli/opencode.ts +586 -0
  167. package/src/cli/pi.ts +188 -0
  168. package/src/cli/provider-runtime.ts +162 -0
  169. package/src/cli/provider.ts +463 -0
  170. package/src/cli/runtime-api.ts +325 -0
  171. package/src/cli/star-prompt.ts +155 -0
  172. package/src/cli/status-oauth.ts +78 -0
  173. package/src/cli/status.ts +321 -0
  174. package/src/cli/sync-cloud.ts +283 -0
  175. package/src/cli/system-command.ts +112 -0
  176. package/src/cli/tray-proxy.ts +52 -0
  177. package/src/cli/v2.ts +173 -0
  178. package/src/cli.ts +10 -0
  179. package/src/clients/config-export.ts +377 -0
  180. package/src/clients/effective-status.ts +385 -0
  181. package/src/clients/probes/agy.ts +55 -0
  182. package/src/clients/probes/cc-switch.ts +110 -0
  183. package/src/clients/probes/claude.ts +90 -0
  184. package/src/clients/probes/codex.ts +125 -0
  185. package/src/clients/probes/grok.ts +29 -0
  186. package/src/clients/probes/opencode.ts +109 -0
  187. package/src/clients/probes/paseo.ts +55 -0
  188. package/src/clients/probes/pi.ts +55 -0
  189. package/src/cloud/onedrive-auth.ts +666 -0
  190. package/src/cloud/onedrive-graph.ts +108 -0
  191. package/src/cloud/settings.ts +75 -0
  192. package/src/cloud/sync.ts +212 -0
  193. package/src/cloud/types.ts +56 -0
  194. package/src/cloud/vault.ts +89 -0
  195. package/src/codex/account-id.ts +34 -0
  196. package/src/codex/account-label.ts +34 -0
  197. package/src/codex/account-lifecycle.ts +55 -0
  198. package/src/codex/account-namespace-match.ts +63 -0
  199. package/src/codex/account-namespaces.ts +149 -0
  200. package/src/codex/account-pause.ts +20 -0
  201. package/src/codex/account-runtime-state.ts +31 -0
  202. package/src/codex/account-store.ts +517 -0
  203. package/src/codex/account-usability.ts +20 -0
  204. package/src/codex/app-server-processes.ts +756 -0
  205. package/src/codex/auth-api.ts +1540 -0
  206. package/src/codex/auth-collision.ts +107 -0
  207. package/src/codex/auth-context.ts +352 -0
  208. package/src/codex/autostart-health.ts +149 -0
  209. package/src/codex/catalog/aggregation.ts +378 -0
  210. package/src/codex/catalog/bundled.ts +251 -0
  211. package/src/codex/catalog/effort.ts +355 -0
  212. package/src/codex/catalog/metadata.ts +180 -0
  213. package/src/codex/catalog/parsing.ts +456 -0
  214. package/src/codex/catalog/provider-fetch.ts +902 -0
  215. package/src/codex/catalog/sync.ts +620 -0
  216. package/src/codex/catalog.ts +12 -0
  217. package/src/codex/data/upstream-models.json +830 -0
  218. package/src/codex/exec-invocation.ts +22 -0
  219. package/src/codex/features.ts +969 -0
  220. package/src/codex/history-migration-guardian.ts +102 -0
  221. package/src/codex/history-provider.ts +776 -0
  222. package/src/codex/home.ts +206 -0
  223. package/src/codex/inject.ts +799 -0
  224. package/src/codex/injected-marker.ts +72 -0
  225. package/src/codex/journal.ts +163 -0
  226. package/src/codex/main-account-cache.ts +32 -0
  227. package/src/codex/main-account.ts +40 -0
  228. package/src/codex/model-cache.ts +227 -0
  229. package/src/codex/paths.ts +65 -0
  230. package/src/codex/plugins-doctor.ts +242 -0
  231. package/src/codex/pool-rotation.ts +225 -0
  232. package/src/codex/project-config-warnings.ts +411 -0
  233. package/src/codex/quota.ts +411 -0
  234. package/src/codex/refresh.ts +53 -0
  235. package/src/codex/routing.ts +1477 -0
  236. package/src/codex/runtime.ts +538 -0
  237. package/src/codex/shim.ts +1189 -0
  238. package/src/codex/subagent-defaults.ts +550 -0
  239. package/src/codex/subagent-model-fallback.ts +469 -0
  240. package/src/codex/sync.ts +130 -0
  241. package/src/codex/warmup.ts +192 -0
  242. package/src/codex/websocket-registry.ts +100 -0
  243. package/src/combos/failover.ts +140 -0
  244. package/src/combos/index.ts +41 -0
  245. package/src/combos/request.ts +62 -0
  246. package/src/combos/resolve.ts +232 -0
  247. package/src/combos/types.ts +326 -0
  248. package/src/config.ts +2356 -0
  249. package/src/generated/jawcode-model-metadata.ts +104 -0
  250. package/src/github/star-state.ts +203 -0
  251. package/src/grok/inject.ts +545 -0
  252. package/src/grok/status.ts +121 -0
  253. package/src/grok/sync.ts +103 -0
  254. package/src/grok/usage-hook/report.mjs +348 -0
  255. package/src/grok/usage-hook.ts +278 -0
  256. package/src/images/artifacts.ts +516 -0
  257. package/src/images/fulfill-video.ts +163 -0
  258. package/src/images/fulfill.ts +149 -0
  259. package/src/images/index.ts +4 -0
  260. package/src/images/loop.ts +829 -0
  261. package/src/images/plan.ts +133 -0
  262. package/src/images/synthetic-tool.ts +133 -0
  263. package/src/images/types.ts +41 -0
  264. package/src/images/xai-client.ts +141 -0
  265. package/src/images/xai-video-client.ts +163 -0
  266. package/src/index.ts +22 -0
  267. package/src/lib/abort.ts +146 -0
  268. package/src/lib/admin-secrets.ts +25 -0
  269. package/src/lib/admission.ts +83 -0
  270. package/src/lib/app-owned-memory-stores.ts +173 -0
  271. package/src/lib/app-owned-memory.ts +265 -0
  272. package/src/lib/bounded-body.ts +202 -0
  273. package/src/lib/bun-binary-validator.d.mts +3 -0
  274. package/src/lib/bun-binary-validator.mjs +18 -0
  275. package/src/lib/bun-runtime.ts +71 -0
  276. package/src/lib/bun-stream-caps.ts +126 -0
  277. package/src/lib/config-ownership.ts +360 -0
  278. package/src/lib/crash-guard.ts +344 -0
  279. package/src/lib/debug-log-buffer.ts +83 -0
  280. package/src/lib/debug-settings.ts +108 -0
  281. package/src/lib/debug.ts +31 -0
  282. package/src/lib/destination-policy.ts +316 -0
  283. package/src/lib/errors.ts +364 -0
  284. package/src/lib/eventstream-decoder.ts +253 -0
  285. package/src/lib/gcp-adc.ts +341 -0
  286. package/src/lib/injection-debug-log.ts +58 -0
  287. package/src/lib/open-url.ts +25 -0
  288. package/src/lib/pinned-http.ts +151 -0
  289. package/src/lib/privacy.ts +20 -0
  290. package/src/lib/process-control.ts +165 -0
  291. package/src/lib/provider-outbound.ts +170 -0
  292. package/src/lib/provider-url.ts +14 -0
  293. package/src/lib/proxy-env.ts +18 -0
  294. package/src/lib/redact.ts +105 -0
  295. package/src/lib/retry-after.ts +55 -0
  296. package/src/lib/service-secrets.ts +25 -0
  297. package/src/lib/shadow-call.ts +30 -0
  298. package/src/lib/sidecar-tracker.ts +52 -0
  299. package/src/lib/sse-decoder.ts +323 -0
  300. package/src/lib/state-store-registrations.ts +109 -0
  301. package/src/lib/state-store-sweeper.ts +184 -0
  302. package/src/lib/test-home-guard.ts +90 -0
  303. package/src/lib/token-estimate.ts +69 -0
  304. package/src/lib/translator-budget.ts +356 -0
  305. package/src/lib/upstream-retry.ts +239 -0
  306. package/src/lib/win-exec.ts +115 -0
  307. package/src/lib/win-paths.ts +68 -0
  308. package/src/lib/windows-elevation.ts +705 -0
  309. package/src/lib/windows-secret-acl.ts +514 -0
  310. package/src/lib/winsw.ts +375 -0
  311. package/src/oauth/anthropic-routing.ts +594 -0
  312. package/src/oauth/anthropic.ts +177 -0
  313. package/src/oauth/callback-server.ts +294 -0
  314. package/src/oauth/chatgpt.ts +150 -0
  315. package/src/oauth/cursor.ts +211 -0
  316. package/src/oauth/github-copilot.ts +428 -0
  317. package/src/oauth/google-antigravity.ts +230 -0
  318. package/src/oauth/health.ts +399 -0
  319. package/src/oauth/index.ts +1174 -0
  320. package/src/oauth/key-providers.ts +108 -0
  321. package/src/oauth/kimi.ts +213 -0
  322. package/src/oauth/kiro-credentials.ts +726 -0
  323. package/src/oauth/kiro.ts +577 -0
  324. package/src/oauth/local-token-detect.ts +121 -0
  325. package/src/oauth/log.ts +48 -0
  326. package/src/oauth/login-cli.ts +163 -0
  327. package/src/oauth/pkce.ts +15 -0
  328. package/src/oauth/store.ts +630 -0
  329. package/src/oauth/token-guardian.ts +303 -0
  330. package/src/oauth/types.ts +62 -0
  331. package/src/oauth/xai.ts +241 -0
  332. package/src/pi/extensions.ts +72 -0
  333. package/src/pi/home.ts +42 -0
  334. package/src/pi/index.ts +40 -0
  335. package/src/pi/models.ts +278 -0
  336. package/src/pi/packages.ts +219 -0
  337. package/src/pi/settings.ts +365 -0
  338. package/src/pi/status.ts +68 -0
  339. package/src/pi/sync.ts +75 -0
  340. package/src/providers/alibaba-region-backup.ts +75 -0
  341. package/src/providers/alibaba-region-migration.ts +156 -0
  342. package/src/providers/alibaba-region-startup.ts +36 -0
  343. package/src/providers/antigravity-models.ts +205 -0
  344. package/src/providers/api-keys.ts +140 -0
  345. package/src/providers/base-url-choices.ts +64 -0
  346. package/src/providers/context-cap.ts +65 -0
  347. package/src/providers/derive.ts +339 -0
  348. package/src/providers/free-directory.ts +184 -0
  349. package/src/providers/github-copilot-transport.ts +56 -0
  350. package/src/providers/key-failover.ts +203 -0
  351. package/src/providers/kiro-models.ts +67 -0
  352. package/src/providers/label.ts +19 -0
  353. package/src/providers/model-discovery.ts +356 -0
  354. package/src/providers/openai-sidecar.ts +175 -0
  355. package/src/providers/openai-tier-startup.ts +27 -0
  356. package/src/providers/openai-tiers.ts +301 -0
  357. package/src/providers/openai-virtual-models.ts +82 -0
  358. package/src/providers/openrouter-routing.ts +102 -0
  359. package/src/providers/provider-id-rewrite.ts +150 -0
  360. package/src/providers/quota.ts +1260 -0
  361. package/src/providers/registry.ts +1600 -0
  362. package/src/providers/slug-codec.ts +67 -0
  363. package/src/providers/xai-transport.ts +141 -0
  364. package/src/reasoning-effort.ts +135 -0
  365. package/src/responses/compaction.ts +117 -0
  366. package/src/responses/parser.ts +656 -0
  367. package/src/responses/reasoning-envelope.ts +52 -0
  368. package/src/responses/schema.ts +159 -0
  369. package/src/responses/spill-store.ts +394 -0
  370. package/src/responses/state.ts +895 -0
  371. package/src/responses/tool-groups.ts +19 -0
  372. package/src/router.ts +425 -0
  373. package/src/server/adapter-resolve.ts +80 -0
  374. package/src/server/auth-cors.ts +530 -0
  375. package/src/server/chat-completions.ts +368 -0
  376. package/src/server/claude-messages.ts +914 -0
  377. package/src/server/effort-policy.ts +172 -0
  378. package/src/server/gui-static.ts +123 -0
  379. package/src/server/image-retry.ts +42 -0
  380. package/src/server/images.ts +476 -0
  381. package/src/server/index.ts +1126 -0
  382. package/src/server/lifecycle.ts +227 -0
  383. package/src/server/live.ts +598 -0
  384. package/src/server/management/agent-settings-routes.ts +1169 -0
  385. package/src/server/management/api-access.ts +141 -0
  386. package/src/server/management/api-key-usage.ts +167 -0
  387. package/src/server/management/body.ts +35 -0
  388. package/src/server/management/clients-routes.ts +63 -0
  389. package/src/server/management/cloud-sync-routes.ts +266 -0
  390. package/src/server/management/combo-routes.ts +220 -0
  391. package/src/server/management/config-routes.ts +422 -0
  392. package/src/server/management/context.ts +31 -0
  393. package/src/server/management/logs-usage-routes.ts +707 -0
  394. package/src/server/management/model-routes.ts +525 -0
  395. package/src/server/management/oauth-account-routes.ts +563 -0
  396. package/src/server/management/provider-routes.ts +556 -0
  397. package/src/server/management/shared.ts +277 -0
  398. package/src/server/management/sidebar-routes.ts +90 -0
  399. package/src/server/management/system-restart.ts +179 -0
  400. package/src/server/management/system-routes.ts +117 -0
  401. package/src/server/management/usage-summary-cache.ts +86 -0
  402. package/src/server/management-api.ts +215 -0
  403. package/src/server/management-auth.ts +267 -0
  404. package/src/server/memory-watchdog.ts +156 -0
  405. package/src/server/port-reclaim.ts +307 -0
  406. package/src/server/ports.ts +116 -0
  407. package/src/server/proxy-liveness.ts +201 -0
  408. package/src/server/relay-eager.ts +313 -0
  409. package/src/server/relay.ts +1049 -0
  410. package/src/server/request-decompress.ts +132 -0
  411. package/src/server/request-log-conversation.ts +168 -0
  412. package/src/server/request-log.ts +1046 -0
  413. package/src/server/responses/collaboration.ts +354 -0
  414. package/src/server/responses/compact.ts +384 -0
  415. package/src/server/responses/core.ts +2758 -0
  416. package/src/server/responses/encrypted-payload.ts +308 -0
  417. package/src/server/responses/fetch-helpers.ts +157 -0
  418. package/src/server/responses/passthrough-error.ts +78 -0
  419. package/src/server/responses/terminal-guard.ts +230 -0
  420. package/src/server/responses/upstream-error.ts +48 -0
  421. package/src/server/responses-image-gen-repair.ts +132 -0
  422. package/src/server/responses-item-id-repair.ts +224 -0
  423. package/src/server/responses.ts +9 -0
  424. package/src/server/search.ts +136 -0
  425. package/src/server/sse-payload-rewrite.ts +175 -0
  426. package/src/server/startup-action-control.ts +308 -0
  427. package/src/server/startup-health-cache.ts +113 -0
  428. package/src/server/system-env.ts +413 -0
  429. package/src/server/windows-tcp-drop.ts +184 -0
  430. package/src/server/windows-tray-control.ts +41 -0
  431. package/src/server/ws-bridge.ts +471 -0
  432. package/src/service.ts +2554 -0
  433. package/src/stall-timeout.ts +20 -0
  434. package/src/storage/cleanup-job.ts +57 -0
  435. package/src/storage/cleanup.ts +3085 -0
  436. package/src/storage/policy-job.ts +457 -0
  437. package/src/storage/policy-scheduler.ts +40 -0
  438. package/src/storage/policy-worker.ts +59 -0
  439. package/src/storage/policy.ts +527 -0
  440. package/src/storage/restore-job.ts +299 -0
  441. package/src/storage/restore-worker.ts +58 -0
  442. package/src/storage/scanner.ts +238 -0
  443. package/src/storage/storage-mutation-coordinator.ts +139 -0
  444. package/src/storage/worker-lifecycle.ts +215 -0
  445. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  446. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  447. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  448. package/src/tray/assets/opencodex-tray.png +0 -0
  449. package/src/tray/windows-tray.ps1 +290 -0
  450. package/src/tray/windows.ts +730 -0
  451. package/src/types.ts +1237 -0
  452. package/src/update/badge.ts +72 -0
  453. package/src/update/index.ts +407 -0
  454. package/src/update/job.ts +1520 -0
  455. package/src/update/notify.ts +257 -0
  456. package/src/update/npm-invocation.d.mts +23 -0
  457. package/src/update/npm-invocation.mjs +94 -0
  458. package/src/update/tray-update-plan.d.mts +18 -0
  459. package/src/update/tray-update-plan.mjs +38 -0
  460. package/src/usage/cost.ts +0 -0
  461. package/src/usage/debug.ts +97 -0
  462. package/src/usage/expected-prices.ts +164 -0
  463. package/src/usage/log.ts +658 -0
  464. package/src/usage/summary.ts +585 -0
  465. package/src/usage/totals.ts +14 -0
  466. package/src/vision/anthropic-describe.ts +185 -0
  467. package/src/vision/describe.ts +125 -0
  468. package/src/vision/index.ts +467 -0
  469. package/src/web-search/anthropic-executor.ts +189 -0
  470. package/src/web-search/executor.ts +105 -0
  471. package/src/web-search/format-result.ts +89 -0
  472. package/src/web-search/index.ts +196 -0
  473. package/src/web-search/loop.ts +664 -0
  474. package/src/web-search/parse.ts +220 -0
  475. package/src/web-search/progress-stream.ts +342 -0
  476. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,658 @@
1
+ import { chmodSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, appendFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getConfigDir } from "../config";
4
+ import { recordOwnedConfigPath } from "../lib/config-ownership";
5
+ import { usageDisplayTotalTokens } from "./totals";
6
+ import type { OcxUsage } from "../types";
7
+
8
+ export type UsageStatus = "reported" | "unreported" | "unsupported" | "estimated";
9
+
10
+ export type AttemptRecoveryKind =
11
+ | "transient-5xx"
12
+ | "connection-reset"
13
+ | "oauth-401"
14
+ | "key-429"
15
+ | "anthropic-oauth-429"
16
+ | "image-413";
17
+
18
+ export interface PersistedUsageAttempt {
19
+ ordinal: number;
20
+ provider: string;
21
+ model: string;
22
+ adapter: string;
23
+ status: number;
24
+ durationMs: number;
25
+ /** TTFT relative to THIS attempt's start (WP4); unset for non-streaming/tool-only. */
26
+ firstOutputMs?: number;
27
+ sendCount: number;
28
+ recoveryKinds: AttemptRecoveryKind[];
29
+ usageStatus: UsageStatus;
30
+ inputTokenEstimate?: number;
31
+ usage?: OcxUsage;
32
+ totalTokens?: number;
33
+ errorCode?: string;
34
+ /** Target-specific reasoning intent and exact adapter-normalized wire parameter. */
35
+ requestedEffort?: string;
36
+ effectiveEffort?: string;
37
+ reasoningWireField?: string;
38
+ reasoningWireValue?: string | number;
39
+ }
40
+
41
+ export interface PersistedUsageEntry {
42
+ requestId: string;
43
+ timestamp: number;
44
+ provider: string;
45
+ model: string;
46
+ surface?: "claude" | "claude-desktop" | "grok";
47
+ /** Matched configured key id; absent for environment/loopback admissions and
48
+ * for every row written before attribution existed. */
49
+ apiKeyId?: string;
50
+ admissionKind?: "configured" | "environment" | "loopback";
51
+ /** The inbound wire, not the client product — see `surface`. */
52
+ inboundProtocol?: "responses" | "chat" | "messages";
53
+ /** Best-effort chat/session correlation for Logs grouping (#330). */
54
+ conversationId?: string;
55
+ resolvedModel?: string;
56
+ requestedModel?: string;
57
+ /** Reasoning effort / service-tier metadata for GUI Logs after restart. */
58
+ requestedEffort?: string;
59
+ /** Adapter-normalized tier and exact upstream parameter emitted for this request. */
60
+ effectiveEffort?: string;
61
+ reasoningWireField?: string;
62
+ reasoningWireValue?: string | number;
63
+ requestedServiceTier?: string;
64
+ requestedSpeedLabel?: string;
65
+ configuredServiceTier?: string;
66
+ configuredSpeedLabel?: string;
67
+ modelSupportsServiceTier?: boolean;
68
+ responseServiceTier?: string;
69
+ status: number;
70
+ durationMs: number;
71
+ /** TTFT relative to the request start (WP4); unset for non-streaming/tool-only. */
72
+ firstOutputMs?: number;
73
+ usageStatus: UsageStatus;
74
+ usage?: OcxUsage;
75
+ totalTokens?: number;
76
+ attempts?: PersistedUsageAttempt[];
77
+ // Failure diagnostics (devlog/_plan/260716_claudecode_hardening/030): persisted for
78
+ // status>=400 or non-completed terminals so incidents survive the in-memory ring buffer.
79
+ errorCode?: string;
80
+ terminalStatus?: string;
81
+ closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow";
82
+ /** Already redacted + capped at capture (request-log.ts redactSecretString().slice(0,500)). */
83
+ upstreamError?: string;
84
+ }
85
+
86
+ const KNOWN_USAGE_SURFACES = new Set<NonNullable<PersistedUsageEntry["surface"]>>([
87
+ "claude",
88
+ "claude-desktop",
89
+ "grok",
90
+ ]);
91
+
92
+ /**
93
+ * The serializer guard for `surface`. Two failure modes shaped this: a literal
94
+ * whitelist ("claude" | "claude-desktop" only) silently dropped every NEW surface at
95
+ * write time, while a plain truthy spread would persist junk values from hand-edited
96
+ * logs. Membership in this set is the middle path: adding a surface here is one edit,
97
+ * and unknown values are still dropped.
98
+ */
99
+ export function isKnownUsageSurface(value: unknown): value is NonNullable<PersistedUsageEntry["surface"]> {
100
+ return typeof value === "string" && KNOWN_USAGE_SURFACES.has(value as NonNullable<PersistedUsageEntry["surface"]>);
101
+ }
102
+
103
+ const KNOWN_ADMISSION_KINDS = new Set<NonNullable<PersistedUsageEntry["admissionKind"]>>([
104
+ "configured", "environment", "loopback",
105
+ ]);
106
+
107
+ const KNOWN_INBOUND_PROTOCOLS = new Set<NonNullable<PersistedUsageEntry["inboundProtocol"]>>([
108
+ "responses", "chat", "messages",
109
+ ]);
110
+
111
+ /** Same closed-set discipline as `isKnownUsageSurface`: an old or corrupted row
112
+ * carrying an unexpected value drops the field instead of poisoning the enum. */
113
+ export function isKnownAdmissionKind(value: unknown): value is NonNullable<PersistedUsageEntry["admissionKind"]> {
114
+ return typeof value === "string" && KNOWN_ADMISSION_KINDS.has(value as NonNullable<PersistedUsageEntry["admissionKind"]>);
115
+ }
116
+
117
+ export function isKnownInboundProtocol(value: unknown): value is NonNullable<PersistedUsageEntry["inboundProtocol"]> {
118
+ return typeof value === "string" && KNOWN_INBOUND_PROTOCOLS.has(value as NonNullable<PersistedUsageEntry["inboundProtocol"]>);
119
+ }
120
+
121
+ export function usageLogPath(): string {
122
+ return join(getConfigDir(), "usage.jsonl");
123
+ }
124
+
125
+ export function usageTotalTokens(usage: OcxUsage | undefined): number | undefined {
126
+ return usageDisplayTotalTokens(usage);
127
+ }
128
+
129
+ /**
130
+ * Providers whose adapters can only estimate usage (no authoritative per-turn frame).
131
+ * Callers should pass the route ADAPTER when available; the name-prefix match is a
132
+ * fallback for paths that only know the configured provider name (e.g. "cursor-mykey").
133
+ */
134
+ function isEstimatedUsageProvider(providerOrAdapter: string): boolean {
135
+ return providerOrAdapter === "kiro" || providerOrAdapter.startsWith("kiro-")
136
+ || providerOrAdapter === "cursor" || providerOrAdapter.startsWith("cursor-");
137
+ }
138
+
139
+ export function usageForFinalLog(provider: string, usage: OcxUsage | undefined): OcxUsage | undefined {
140
+ if (!usage) return undefined;
141
+ if (usage.estimated || isEstimatedUsageProvider(provider)) return { ...usage, estimated: true };
142
+ return usage;
143
+ }
144
+
145
+ export function usageStatusForFinalLog(usage: OcxUsage | undefined): UsageStatus {
146
+ if (!usage) return "unreported";
147
+ return usage.estimated ? "estimated" : "reported";
148
+ }
149
+
150
+ function normalizeUsageValue(usage: OcxUsage | undefined): OcxUsage | undefined {
151
+ if (!usage) return undefined;
152
+ return {
153
+ inputTokens: usage.inputTokens,
154
+ outputTokens: usage.outputTokens,
155
+ // Absolute active-context checkpoint (types.ts). Stateful providers such as Kiro report
156
+ // per-attempt usage only, so this field is the ONLY carrier of the cumulative context
157
+ // figure once the log records raw adapter usage instead of re-parsing the bridged wire
158
+ // (usageFromBridge, request-log.ts). Omitting it here silently dropped Kiro's context
159
+ // growth from every persisted row. It is deliberately NOT folded into totalTokens:
160
+ // a checkpoint is not a per-request total and must never be summed across requests.
161
+ ...(typeof usage.contextTotalTokens === "number" ? { contextTotalTokens: usage.contextTotalTokens } : {}),
162
+ ...(typeof usage.totalTokens === "number" ? { totalTokens: usage.totalTokens } : {}),
163
+ ...(typeof usage.cachedInputTokens === "number" ? { cachedInputTokens: usage.cachedInputTokens } : {}),
164
+ ...(typeof usage.cacheReadInputTokens === "number" ? { cacheReadInputTokens: usage.cacheReadInputTokens } : {}),
165
+ ...(typeof usage.cacheCreationInputTokens === "number" ? { cacheCreationInputTokens: usage.cacheCreationInputTokens } : {}),
166
+ ...(typeof usage.reasoningOutputTokens === "number" ? { reasoningOutputTokens: usage.reasoningOutputTokens } : {}),
167
+ ...(usage.estimated ? { estimated: true } : {}),
168
+ };
169
+ }
170
+
171
+ const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
172
+ "transient-5xx",
173
+ "connection-reset",
174
+ "oauth-401",
175
+ "key-429",
176
+ "anthropic-oauth-429",
177
+ "image-413",
178
+ ]);
179
+ const USAGE_STATUSES = new Set<UsageStatus>([
180
+ "reported",
181
+ "unreported",
182
+ "unsupported",
183
+ "estimated",
184
+ ]);
185
+
186
+ function isNonNegativeFiniteNumber(value: unknown): value is number {
187
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
188
+ }
189
+
190
+ function normalizeAttemptUsage(raw: unknown): OcxUsage | null {
191
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
192
+ const usage = raw as Record<string, unknown>;
193
+ if (!isNonNegativeFiniteNumber(usage.inputTokens)
194
+ || !isNonNegativeFiniteNumber(usage.outputTokens)) return null;
195
+ for (const key of [
196
+ "contextTotalTokens",
197
+ "totalTokens",
198
+ "cachedInputTokens",
199
+ "cacheReadInputTokens",
200
+ "cacheCreationInputTokens",
201
+ "reasoningOutputTokens",
202
+ ] as const) {
203
+ if (key in usage && !isNonNegativeFiniteNumber(usage[key])) return null;
204
+ }
205
+ if ("estimated" in usage && typeof usage.estimated !== "boolean") return null;
206
+ return normalizeUsageValue(usage as unknown as OcxUsage) ?? null;
207
+ }
208
+
209
+ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
210
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
211
+ const attempt = raw as Record<string, unknown>;
212
+ if (typeof attempt.ordinal !== "number" || !Number.isInteger(attempt.ordinal)
213
+ || attempt.ordinal < 1
214
+ || typeof attempt.provider !== "string" || !attempt.provider
215
+ || typeof attempt.model !== "string" || !attempt.model
216
+ || typeof attempt.adapter !== "string" || !attempt.adapter
217
+ || typeof attempt.status !== "number" || !Number.isInteger(attempt.status)
218
+ || attempt.status < 100 || attempt.status > 599
219
+ || typeof attempt.durationMs !== "number" || !Number.isFinite(attempt.durationMs)
220
+ || attempt.durationMs < 0
221
+ || typeof attempt.sendCount !== "number" || !Number.isInteger(attempt.sendCount)
222
+ || attempt.sendCount < 0
223
+ || typeof attempt.usageStatus !== "string"
224
+ || !USAGE_STATUSES.has(attempt.usageStatus as UsageStatus)) {
225
+ return null;
226
+ }
227
+ if ("inputTokenEstimate" in attempt
228
+ && !isNonNegativeFiniteNumber(attempt.inputTokenEstimate)) return null;
229
+ if ("firstOutputMs" in attempt
230
+ && !isNonNegativeFiniteNumber(attempt.firstOutputMs)) return null;
231
+ if ("totalTokens" in attempt
232
+ && !isNonNegativeFiniteNumber(attempt.totalTokens)) return null;
233
+ const usage = "usage" in attempt ? normalizeAttemptUsage(attempt.usage) : undefined;
234
+ if ("usage" in attempt && usage === null) return null;
235
+ const recoveryKinds = Array.isArray(attempt.recoveryKinds)
236
+ ? [...new Set(attempt.recoveryKinds.filter(
237
+ (value): value is AttemptRecoveryKind => typeof value === "string"
238
+ && ATTEMPT_RECOVERY_KINDS.has(value as AttemptRecoveryKind),
239
+ ))]
240
+ : [];
241
+ return {
242
+ ordinal: attempt.ordinal as number,
243
+ provider: attempt.provider,
244
+ model: attempt.model,
245
+ adapter: attempt.adapter,
246
+ status: attempt.status,
247
+ durationMs: attempt.durationMs,
248
+ ...(isNonNegativeFiniteNumber(attempt.firstOutputMs)
249
+ ? { firstOutputMs: attempt.firstOutputMs }
250
+ : {}),
251
+ sendCount: attempt.sendCount as number,
252
+ recoveryKinds,
253
+ usageStatus: attempt.usageStatus as UsageStatus,
254
+ ...(isNonNegativeFiniteNumber(attempt.inputTokenEstimate)
255
+ ? { inputTokenEstimate: attempt.inputTokenEstimate }
256
+ : {}),
257
+ ...(usage ? { usage } : {}),
258
+ ...(isNonNegativeFiniteNumber(attempt.totalTokens)
259
+ ? { totalTokens: attempt.totalTokens }
260
+ : {}),
261
+ ...(typeof attempt.errorCode === "string" ? { errorCode: attempt.errorCode } : {}),
262
+ ...(typeof attempt.requestedEffort === "string" && attempt.requestedEffort
263
+ ? { requestedEffort: capMetadataString(attempt.requestedEffort) }
264
+ : {}),
265
+ ...(typeof attempt.effectiveEffort === "string" && attempt.effectiveEffort
266
+ ? { effectiveEffort: capMetadataString(attempt.effectiveEffort) }
267
+ : {}),
268
+ ...(typeof attempt.reasoningWireField === "string" && attempt.reasoningWireField
269
+ ? { reasoningWireField: capMetadataString(attempt.reasoningWireField) }
270
+ : {}),
271
+ ...(typeof attempt.reasoningWireValue === "string" && attempt.reasoningWireValue
272
+ ? { reasoningWireValue: capMetadataString(attempt.reasoningWireValue) }
273
+ : isNonNegativeFiniteNumber(attempt.reasoningWireValue)
274
+ ? { reasoningWireValue: attempt.reasoningWireValue }
275
+ : {}),
276
+ };
277
+ }
278
+
279
+ function normalizedAttempts(raw: unknown): PersistedUsageAttempt[] {
280
+ if (!Array.isArray(raw)) return [];
281
+ return raw.map(normalizeUsageAttempt)
282
+ .filter((attempt): attempt is PersistedUsageAttempt => attempt !== null);
283
+ }
284
+
285
+ const MAX_METADATA_STRING_LEN = 64;
286
+ function capMetadataString(s: string): string {
287
+ return s.length > MAX_METADATA_STRING_LEN ? s.slice(0, MAX_METADATA_STRING_LEN) : s;
288
+ }
289
+
290
+ /** Test seam: the normalization branch old rows take is worth asserting directly. */
291
+ export function normalizeUsageEntryForTest(entry: PersistedUsageEntry): PersistedUsageEntry {
292
+ return normalizeUsageEntry(entry);
293
+ }
294
+
295
+ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
296
+ const attempts = normalizedAttempts(entry.attempts);
297
+ return {
298
+ requestId: entry.requestId,
299
+ timestamp: entry.timestamp,
300
+ provider: entry.provider,
301
+ model: entry.model,
302
+ ...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
303
+ ...(typeof entry.apiKeyId === "string" && entry.apiKeyId.trim()
304
+ // Deliberately NOT capped. `capMetadataString` protects free-form metadata
305
+ // from unbounded growth, but this is a lookup key: truncating it makes the
306
+ // persisted id stop matching the configured one, and the rollup silently
307
+ // reports zero for a key that is very much in use.
308
+ ? { apiKeyId: entry.apiKeyId }
309
+ : {}),
310
+ ...(isKnownAdmissionKind(entry.admissionKind) ? { admissionKind: entry.admissionKind } : {}),
311
+ ...(isKnownInboundProtocol(entry.inboundProtocol) ? { inboundProtocol: entry.inboundProtocol } : {}),
312
+ ...(typeof entry.conversationId === "string" && entry.conversationId.trim()
313
+ ? { conversationId: entry.conversationId.trim().slice(0, 128) }
314
+ : {}),
315
+ ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
316
+ ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
317
+ ...(typeof entry.requestedEffort === "string" && entry.requestedEffort
318
+ ? { requestedEffort: capMetadataString(entry.requestedEffort) }
319
+ : {}),
320
+ ...(typeof entry.effectiveEffort === "string" && entry.effectiveEffort
321
+ ? { effectiveEffort: capMetadataString(entry.effectiveEffort) }
322
+ : {}),
323
+ ...(typeof entry.reasoningWireField === "string" && entry.reasoningWireField
324
+ ? { reasoningWireField: capMetadataString(entry.reasoningWireField) }
325
+ : {}),
326
+ ...(typeof entry.reasoningWireValue === "string" && entry.reasoningWireValue
327
+ ? { reasoningWireValue: capMetadataString(entry.reasoningWireValue) }
328
+ : isNonNegativeFiniteNumber(entry.reasoningWireValue)
329
+ ? { reasoningWireValue: entry.reasoningWireValue }
330
+ : {}),
331
+ ...(typeof entry.requestedServiceTier === "string" && entry.requestedServiceTier
332
+ ? { requestedServiceTier: capMetadataString(entry.requestedServiceTier) }
333
+ : {}),
334
+ ...(typeof entry.requestedSpeedLabel === "string" && entry.requestedSpeedLabel
335
+ ? { requestedSpeedLabel: capMetadataString(entry.requestedSpeedLabel) }
336
+ : {}),
337
+ ...(typeof entry.configuredServiceTier === "string" && entry.configuredServiceTier
338
+ ? { configuredServiceTier: capMetadataString(entry.configuredServiceTier) }
339
+ : {}),
340
+ ...(typeof entry.configuredSpeedLabel === "string" && entry.configuredSpeedLabel
341
+ ? { configuredSpeedLabel: capMetadataString(entry.configuredSpeedLabel) }
342
+ : {}),
343
+ ...(typeof entry.modelSupportsServiceTier === "boolean"
344
+ ? { modelSupportsServiceTier: entry.modelSupportsServiceTier }
345
+ : {}),
346
+ ...(typeof entry.responseServiceTier === "string" && entry.responseServiceTier
347
+ ? { responseServiceTier: capMetadataString(entry.responseServiceTier) }
348
+ : {}),
349
+ status: entry.status,
350
+ durationMs: entry.durationMs,
351
+ ...(isNonNegativeFiniteNumber(entry.firstOutputMs)
352
+ ? { firstOutputMs: entry.firstOutputMs }
353
+ : {}),
354
+ usageStatus: entry.usageStatus,
355
+ ...(entry.usage ? { usage: normalizeUsageValue(entry.usage) } : {}),
356
+ ...(typeof entry.totalTokens === "number" ? { totalTokens: entry.totalTokens } : {}),
357
+ ...(attempts.length > 0 ? { attempts } : {}),
358
+ ...(entry.errorCode ? { errorCode: entry.errorCode } : {}),
359
+ ...(entry.terminalStatus ? { terminalStatus: entry.terminalStatus } : {}),
360
+ ...(entry.closeReason ? { closeReason: entry.closeReason } : {}),
361
+ ...(entry.upstreamError ? { upstreamError: entry.upstreamError } : {}),
362
+ };
363
+ }
364
+
365
+ function ensureUsageLogDir(): void {
366
+ const dir = getConfigDir();
367
+ recordOwnedConfigPath(dir, usageLogPath());
368
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
369
+ try { chmodSync(dir, 0o700); } catch { /* best-effort on platforms that ignore chmod */ }
370
+ }
371
+
372
+ export function appendUsageEntry(entry: PersistedUsageEntry): void {
373
+ ensureUsageLogDir();
374
+ const path = usageLogPath();
375
+ appendFileSync(path, `${JSON.stringify(normalizeUsageEntry(entry))}\n`, { encoding: "utf-8", mode: 0o600 });
376
+ try { chmodSync(path, 0o600); } catch { /* best-effort on platforms that ignore chmod */ }
377
+ }
378
+
379
+ export type UsageLogRevision = {
380
+ path: string;
381
+ dev: number;
382
+ ino: number;
383
+ birthtimeMs: number;
384
+ size: number;
385
+ mtimeMs: number;
386
+ ctimeMs: number;
387
+ };
388
+
389
+ let usageReadCacheStats = { fullReads: 0, tailReads: 0, parsedLines: 0 };
390
+ const MANAGEMENT_USAGE_MAX_READ_BYTES = 64 * 1024 * 1024;
391
+ const MANAGEMENT_USAGE_READ_CHUNK_BYTES = 1024 * 1024;
392
+ const MANAGEMENT_USAGE_MAX_ENTRIES = 200_000;
393
+ const MANAGEMENT_USAGE_FLIGHT_STALE_MS = 30_000;
394
+ export interface ManagementUsageSnapshot {
395
+ entries: PersistedUsageEntry[];
396
+ revision: UsageLogRevision;
397
+ truncatedPrefixBytes: number;
398
+ entriesTruncated: boolean;
399
+ entriesDropped: number;
400
+ }
401
+ let managementUsageReadInflight: {
402
+ key: string;
403
+ promise: Promise<ManagementUsageSnapshot>;
404
+ startedAt: number;
405
+ abort: AbortController;
406
+ } | null = null;
407
+
408
+ /** Test-only observability for proving that unchanged prefixes are not reparsed. */
409
+ export function usageReadCacheStatsForTests(): Readonly<typeof usageReadCacheStats> {
410
+ return { ...usageReadCacheStats };
411
+ }
412
+
413
+ export function resetUsageReadCacheForTests(): void {
414
+ usageReadCacheStats = { fullReads: 0, tailReads: 0, parsedLines: 0 };
415
+ managementUsageReadInflight?.abort.abort();
416
+ managementUsageReadInflight = null;
417
+ }
418
+
419
+ function readExactly(fd: number, length: number, position: number): Buffer | null {
420
+ const output = Buffer.allocUnsafe(length);
421
+ let offset = 0;
422
+ while (offset < length) {
423
+ const read = readSync(fd, output, offset, length - offset, position + offset);
424
+ if (read === 0) return null;
425
+ offset += read;
426
+ }
427
+ return output;
428
+ }
429
+
430
+ function usageLogRevision(path: string, stat: ReturnType<typeof fstatSync>): UsageLogRevision {
431
+ if (!stat.isFile()) throw new Error("usage log is not a regular file");
432
+ return {
433
+ path,
434
+ dev: Number(stat.dev),
435
+ ino: Number(stat.ino),
436
+ birthtimeMs: Number(stat.birthtimeMs),
437
+ size: Number(stat.size),
438
+ mtimeMs: Number(stat.mtimeMs),
439
+ ctimeMs: Number(stat.ctimeMs),
440
+ };
441
+ }
442
+
443
+ export function usageLogRevisionKey(revision: UsageLogRevision | null): string {
444
+ if (!revision) return "missing";
445
+ return [
446
+ revision.path,
447
+ revision.dev,
448
+ revision.ino,
449
+ revision.birthtimeMs,
450
+ revision.size,
451
+ revision.mtimeMs,
452
+ revision.ctimeMs,
453
+ ].join("\0");
454
+ }
455
+
456
+ export function currentUsageLogRevision(): UsageLogRevision | null {
457
+ const path = usageLogPath();
458
+ if (!existsSync(path)) return null;
459
+ let fd: number | undefined;
460
+ try {
461
+ fd = openSync(path, "r");
462
+ return usageLogRevision(path, fstatSync(fd));
463
+ } finally {
464
+ if (fd !== undefined) closeSync(fd);
465
+ }
466
+ }
467
+
468
+ async function parseUsageTextCooperatively(text: string, signal: AbortSignal): Promise<{
469
+ entries: PersistedUsageEntry[];
470
+ entriesDropped: number;
471
+ }> {
472
+ const lines = text.split(/\r?\n/);
473
+ usageReadCacheStats.parsedLines += lines.filter(line => line.trim()).length;
474
+ const entries: PersistedUsageEntry[] = [];
475
+ const batchSize = 1_000;
476
+ for (let offset = 0; offset < lines.length; offset += batchSize) {
477
+ if (signal.aborted) throw signal.reason;
478
+ entries.push(...parseUsageLines(lines.slice(offset, offset + batchSize)));
479
+ if (offset + batchSize < lines.length) {
480
+ // JSON parsing dominates large-log startup. Yield between bounded batches so
481
+ // Bun can continue serving health and settings requests on the same thread.
482
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
483
+ }
484
+ }
485
+ if (entries.length <= MANAGEMENT_USAGE_MAX_ENTRIES) return { entries, entriesDropped: 0 };
486
+ const entriesDropped = entries.length - MANAGEMENT_USAGE_MAX_ENTRIES;
487
+ return { entries: entries.slice(-MANAGEMENT_USAGE_MAX_ENTRIES), entriesDropped };
488
+ }
489
+
490
+ async function readUsageEntriesFullCooperatively(
491
+ path: string,
492
+ signal: AbortSignal,
493
+ maxReadBytes: number,
494
+ ): Promise<ManagementUsageSnapshot> {
495
+ let fd: number | undefined;
496
+ try {
497
+ fd = openSync(path, "r");
498
+ const stat = fstatSync(fd);
499
+ const size = Number(stat.size);
500
+ const start = Math.max(0, size - maxReadBytes);
501
+ const chunks: Buffer[] = [];
502
+ for (let position = start; position < size;) {
503
+ if (signal.aborted) throw signal.reason;
504
+ const length = Math.min(MANAGEMENT_USAGE_READ_CHUNK_BYTES, size - position);
505
+ const chunk = readExactly(fd, length, position);
506
+ if (chunk === null) throw new Error("usage log changed while it was being read");
507
+ chunks.push(chunk);
508
+ position += length;
509
+ }
510
+ let bytes = Buffer.concat(chunks);
511
+ let truncatedPrefixBytes = start;
512
+ if (start > 0) {
513
+ const preceding = readExactly(fd, 1, start - 1);
514
+ if (preceding === null) throw new Error("usage log changed while it was being read");
515
+ if (preceding[0] !== 0x0a) {
516
+ const newline = bytes.indexOf(0x0a);
517
+ if (newline < 0) {
518
+ truncatedPrefixBytes += bytes.byteLength;
519
+ bytes = Buffer.alloc(0);
520
+ } else {
521
+ truncatedPrefixBytes += newline + 1;
522
+ bytes = bytes.subarray(newline + 1);
523
+ }
524
+ }
525
+ }
526
+ const parsed = await parseUsageTextCooperatively(bytes.toString("utf-8"), signal);
527
+ usageReadCacheStats.fullReads += 1;
528
+ return {
529
+ entries: parsed.entries,
530
+ revision: usageLogRevision(path, stat),
531
+ truncatedPrefixBytes,
532
+ entriesTruncated: parsed.entriesDropped > 0,
533
+ entriesDropped: parsed.entriesDropped,
534
+ };
535
+ } finally {
536
+ if (fd !== undefined) closeSync(fd);
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Management API reader: full parses yield between bounded batches and concurrent
542
+ * callers share work only when they observed the same exact file revision. Parsed rows
543
+ * are returned to the request and never retained in module state.
544
+ */
545
+ export async function readUsageSnapshotForManagement(maxReadBytes = MANAGEMENT_USAGE_MAX_READ_BYTES): Promise<{
546
+ entries: PersistedUsageEntry[];
547
+ revision: UsageLogRevision | null;
548
+ truncatedPrefixBytes: number;
549
+ entriesTruncated: boolean;
550
+ entriesDropped: number;
551
+ }> {
552
+ if (!Number.isSafeInteger(maxReadBytes) || maxReadBytes <= 0) throw new RangeError("management usage max read bytes must be positive");
553
+ const path = usageLogPath();
554
+ if (!existsSync(path)) return { entries: [], revision: null, truncatedPrefixBytes: 0, entriesTruncated: false, entriesDropped: 0 };
555
+ const observed = currentUsageLogRevision();
556
+ const key = `${usageLogRevisionKey(observed)}\0${maxReadBytes}`;
557
+ const existing = managementUsageReadInflight;
558
+ if (existing?.key === key && Date.now() - existing.startedAt <= MANAGEMENT_USAGE_FLIGHT_STALE_MS) {
559
+ const shared = await existing.promise;
560
+ return { ...shared, entries: shared.entries.slice() };
561
+ }
562
+ existing?.abort.abort(new Error("management usage read superseded"));
563
+ const abort = new AbortController();
564
+ const promise = readUsageEntriesFullCooperatively(path, abort.signal, maxReadBytes);
565
+ managementUsageReadInflight = { key, promise, startedAt: Date.now(), abort };
566
+ try {
567
+ const snapshot = await promise;
568
+ return { ...snapshot, entries: snapshot.entries.slice() };
569
+ } finally {
570
+ if (managementUsageReadInflight?.promise === promise) managementUsageReadInflight = null;
571
+ }
572
+ }
573
+
574
+ export async function readUsageEntriesForManagement(): Promise<PersistedUsageEntry[]> {
575
+ return (await readUsageSnapshotForManagement()).entries;
576
+ }
577
+
578
+ export function readUsageEntries(): PersistedUsageEntry[] {
579
+ const path = usageLogPath();
580
+ if (!existsSync(path)) return [];
581
+ const lines = readFileSync(path, "utf-8").split(/\r?\n/);
582
+ const entries: PersistedUsageEntry[] = [];
583
+ for (const line of lines) {
584
+ if (!line.trim()) continue;
585
+ try {
586
+ const parsed = JSON.parse(line) as PersistedUsageEntry;
587
+ if (parsed && typeof parsed === "object" && typeof parsed.requestId === "string") {
588
+ entries.push(normalizeUsageEntry(parsed));
589
+ }
590
+ } catch {
591
+ /* keep reading after a partially written or hand-edited line */
592
+ }
593
+ }
594
+ return entries;
595
+ }
596
+
597
+ function parseUsageLines(lines: string[]): PersistedUsageEntry[] {
598
+ const entries: PersistedUsageEntry[] = [];
599
+ for (const line of lines) {
600
+ if (!line.trim()) continue;
601
+ try {
602
+ const parsed = JSON.parse(line) as PersistedUsageEntry;
603
+ if (parsed && typeof parsed === "object" && typeof parsed.requestId === "string") {
604
+ entries.push(normalizeUsageEntry(parsed));
605
+ }
606
+ } catch {
607
+ /* skip partial / hand-edited lines */
608
+ }
609
+ }
610
+ return entries;
611
+ }
612
+
613
+ /**
614
+ * Read only the newest `limit` usage.jsonl rows without loading the whole append-only
615
+ * file into memory. Used by request-log hydration on `ocx start`.
616
+ */
617
+ export function readRecentUsageEntries(limit: number): PersistedUsageEntry[] {
618
+ if (!Number.isFinite(limit) || limit <= 0) return [];
619
+ const path = usageLogPath();
620
+ if (!existsSync(path)) return [];
621
+ let fd: number | undefined;
622
+ try {
623
+ fd = openSync(path, "r");
624
+ const size = fstatSync(fd).size;
625
+ if (size <= 0) return [];
626
+ // ~4 KiB/row budget with a floor; expand once if the window yields too few lines.
627
+ let windowBytes = Math.min(size, Math.max(64 * 1024, Math.ceil(limit) * 4 * 1024));
628
+ for (let attempt = 0; attempt < 2; attempt++) {
629
+ const start = Math.max(0, size - windowBytes);
630
+ const buf = Buffer.alloc(size - start);
631
+ readSync(fd, buf, 0, buf.length, start);
632
+ let text = buf.toString("utf-8");
633
+ if (start > 0) {
634
+ const nl = text.indexOf("\n");
635
+ if (nl < 0) {
636
+ if (start === 0) break;
637
+ windowBytes = Math.min(size, windowBytes * 4);
638
+ continue;
639
+ }
640
+ text = text.slice(nl + 1);
641
+ }
642
+ const lines = text.split(/\r?\n/).filter(line => line.trim());
643
+ // Parse ALL lines first, then take the last N valid entries. This way corrupt
644
+ // or partial lines are filtered out during parsing and we always return the
645
+ // most recent N valid rows (not N physical lines minus corrupt ones).
646
+ const entries = parseUsageLines(lines);
647
+ if (entries.length >= limit || start === 0 || windowBytes >= size) return entries.slice(-limit);
648
+ windowBytes = Math.min(size, windowBytes * 4);
649
+ }
650
+ return [];
651
+ } catch {
652
+ return [];
653
+ } finally {
654
+ if (fd !== undefined) {
655
+ try { closeSync(fd); } catch { /* ignore */ }
656
+ }
657
+ }
658
+ }