@bitkyc08/opencodex 2.84.0 → 2.87.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 (518) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  4. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  5. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DCd-A1az.js} +1 -1
  6. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  7. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  8. package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -2
  11. package/src/adapters/anthropic/beta-allowlist.ts +91 -11
  12. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  13. package/src/adapters/anthropic/client-identity.ts +1 -1
  14. package/src/adapters/anthropic/passthrough.ts +16 -3
  15. package/src/adapters/anthropic-progress.ts +84 -0
  16. package/src/adapters/anthropic.ts +108 -21
  17. package/src/adapters/azure.ts +19 -1
  18. package/src/adapters/base.ts +2 -0
  19. package/src/adapters/claude-cli/adapter.ts +111 -23
  20. package/src/adapters/claude-cli/profiles.ts +2 -1
  21. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  22. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  23. package/src/adapters/client-fingerprint.ts +24 -0
  24. package/src/adapters/coding-agent/protocol.ts +1 -1
  25. package/src/adapters/coding-agent/turn.ts +40 -11
  26. package/src/adapters/cursor/arg-codec.ts +17 -5
  27. package/src/adapters/cursor/catalog.ts +8 -0
  28. package/src/adapters/cursor/effort-map.ts +3 -0
  29. package/src/adapters/cursor/freeform-args.ts +75 -0
  30. package/src/adapters/cursor/protobuf-events.ts +36 -23
  31. package/src/adapters/dsh-account.ts +97 -0
  32. package/src/adapters/google-antigravity-wire.ts +22 -1
  33. package/src/adapters/google.ts +37 -10
  34. package/src/adapters/openai-chat/messages.ts +4 -3
  35. package/src/adapters/openai-chat/passthrough.ts +8 -2
  36. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  37. package/src/adapters/openai-chat/response-events.ts +59 -1
  38. package/src/adapters/openai-chat.ts +8 -5
  39. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  40. package/src/adapters/openai-responses/passthrough.ts +57 -125
  41. package/src/adapters/openai-responses/request-strips.ts +25 -3
  42. package/src/adapters/openai-responses/transport.ts +81 -0
  43. package/src/adapters/qoder/adapter.ts +44 -3
  44. package/src/adapters/registry.ts +5 -0
  45. package/src/adapters/zed.ts +26 -1
  46. package/src/advisor/activity.ts +55 -0
  47. package/src/advisor/budget.ts +62 -0
  48. package/src/advisor/consult.ts +223 -0
  49. package/src/advisor/context.ts +238 -0
  50. package/src/advisor/disclosure.ts +20 -0
  51. package/src/advisor/evidence-tool.ts +21 -0
  52. package/src/advisor/evidence.ts +117 -0
  53. package/src/advisor/runtime.ts +353 -0
  54. package/src/advisor/settings.ts +144 -0
  55. package/src/advisor/state.ts +369 -0
  56. package/src/advisor/synthetic-tool.ts +33 -0
  57. package/src/bridge/errors.ts +24 -1
  58. package/src/bridge/internal.ts +3 -1
  59. package/src/bridge/response-json.ts +18 -3
  60. package/src/bridge/sse.ts +58 -8
  61. package/src/bridge/terminal-refusal.ts +59 -0
  62. package/src/chat/inbound.ts +30 -6
  63. package/src/chat/outbound.ts +5 -0
  64. package/src/claude/desktop-3p-library.ts +72 -7
  65. package/src/claude/desktop-3p.ts +75 -19
  66. package/src/claude/desktop-first-party.ts +1 -1
  67. package/src/claude/desktop-picker-platform.ts +4 -0
  68. package/src/claude/desktop-picker-profile.ts +11 -4
  69. package/src/claude/desktop-picker.ts +15 -8
  70. package/src/claude/desktop-profile.ts +10 -1
  71. package/src/claude/desktop-remote-store-io.ts +5 -3
  72. package/src/claude/desktop-system-proxy.ts +50 -4
  73. package/src/claude/inbound-content-options.ts +11 -0
  74. package/src/claude/inbound.ts +154 -50
  75. package/src/claude/intercept/local-ca.ts +8 -1
  76. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  77. package/src/claude/intercept/picker-runtime.ts +6 -5
  78. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  79. package/src/claude/intercept/picker-trust.ts +19 -0
  80. package/src/claude/intercept/port-policy.ts +30 -0
  81. package/src/claude/intercept/runtime.ts +97 -94
  82. package/src/claude/model-default-settings.ts +154 -0
  83. package/src/claude/model-info.ts +7 -7
  84. package/src/claude/outbound.ts +51 -19
  85. package/src/cli/account-api.ts +1 -0
  86. package/src/cli/account-policy-dto.ts +6 -0
  87. package/src/cli/account-policy.ts +34 -5
  88. package/src/cli/account-preservation.ts +44 -0
  89. package/src/cli/account.ts +8 -2
  90. package/src/cli/advisor.ts +165 -0
  91. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  92. package/src/cli/capabilities-accounts.ts +20 -1
  93. package/src/cli/capabilities-agents-routing.ts +23 -2
  94. package/src/cli/capabilities-base.ts +39 -11
  95. package/src/cli/capabilities-observe-system.ts +63 -3
  96. package/src/cli/capabilities-provider-models.ts +11 -0
  97. package/src/cli/chatgpt-command.ts +35 -14
  98. package/src/cli/claude.ts +46 -6
  99. package/src/cli/codex-cli-update.ts +151 -41
  100. package/src/cli/codex-provider.ts +44 -0
  101. package/src/cli/combo-input.ts +10 -1
  102. package/src/cli/combo-result.ts +1 -1
  103. package/src/cli/combo.ts +2 -1
  104. package/src/cli/cross-home-owner.ts +17 -14
  105. package/src/cli/dispatch.ts +30 -5
  106. package/src/cli/doctor.ts +15 -1
  107. package/src/cli/ensure-desktop-cold-start.ts +382 -0
  108. package/src/cli/export-command.ts +1 -1
  109. package/src/cli/help.ts +4 -2
  110. package/src/cli/index.ts +14 -5
  111. package/src/cli/integration-plan-dto.ts +3 -0
  112. package/src/cli/log-history.ts +172 -0
  113. package/src/cli/models-runtime-subcommands.ts +1 -0
  114. package/src/cli/models-runtime.ts +72 -8
  115. package/src/cli/observe-stream.ts +2 -0
  116. package/src/cli/observe.ts +16 -4
  117. package/src/cli/provider.ts +27 -5
  118. package/src/cli/registry.ts +41 -9
  119. package/src/cli/restart-scope.ts +3 -1
  120. package/src/cli/runtime-api.ts +5 -3
  121. package/src/cli/system-command.ts +40 -8
  122. package/src/cli/uninstall-client-state.ts +32 -0
  123. package/src/cli/usage-compare.ts +309 -0
  124. package/src/cli/usage-model-search.ts +4 -4
  125. package/src/cli/usage-report.ts +2 -2
  126. package/src/cli/v2.ts +1 -1
  127. package/src/client/machine-listener.ts +1 -3
  128. package/src/client/runtime.ts +1 -0
  129. package/src/clients/client-path.ts +51 -0
  130. package/src/clients/config-export/contracts.ts +3 -1
  131. package/src/clients/config-export/model-metadata.ts +3 -2
  132. package/src/clients/config-export/qoder.ts +93 -0
  133. package/src/clients/config-export.ts +35 -48
  134. package/src/clients/gentle-shell-path.ts +87 -0
  135. package/src/codex/account-auto-switch.ts +2 -0
  136. package/src/codex/account-identity.ts +29 -0
  137. package/src/codex/account-label.ts +1 -1
  138. package/src/codex/account-lifecycle.ts +21 -5
  139. package/src/codex/account-preservation.ts +28 -0
  140. package/src/codex/account-use-remaining.ts +23 -0
  141. package/src/codex/app-server-processes.ts +97 -7
  142. package/src/codex/app-server-restart-service.ts +55 -4
  143. package/src/codex/auth-api/account-list.ts +4 -0
  144. package/src/codex/auth-api/account-pause-group.ts +6 -50
  145. package/src/codex/auth-api/account-preservation.ts +57 -0
  146. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  147. package/src/codex/auth-api/routes.ts +8 -0
  148. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  149. package/src/codex/auth-api.ts +0 -1
  150. package/src/codex/auth-context.ts +81 -7
  151. package/src/codex/autostart-health.ts +4 -0
  152. package/src/codex/catalog/auto-review.ts +20 -3
  153. package/src/codex/catalog/build-entries.ts +19 -2
  154. package/src/codex/catalog/combo-member.ts +14 -5
  155. package/src/codex/catalog/configured-tiers.ts +50 -0
  156. package/src/codex/catalog/effort.ts +4 -1
  157. package/src/codex/catalog/gather-capture.ts +2 -0
  158. package/src/codex/catalog/metadata.ts +11 -9
  159. package/src/codex/catalog/model-hints.ts +42 -19
  160. package/src/codex/catalog/model-visibility.ts +3 -1
  161. package/src/codex/catalog/native-ultrafast.ts +106 -0
  162. package/src/codex/catalog/parsing.ts +35 -86
  163. package/src/codex/catalog/provider-models.ts +40 -34
  164. package/src/codex/catalog/retained-sync.ts +2 -0
  165. package/src/codex/catalog/routed-gather.ts +33 -16
  166. package/src/codex/catalog/vendor-context.ts +82 -0
  167. package/src/codex/cli-install-provenance.ts +105 -17
  168. package/src/codex/cli-update-plan.ts +545 -0
  169. package/src/codex/config-write-lock.ts +63 -4
  170. package/src/codex/convergence.ts +2 -0
  171. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  172. package/src/codex/desktop-app/types.ts +2 -0
  173. package/src/codex/desktop-app/windows.ts +29 -16
  174. package/src/codex/desktop-app-restart.ts +25 -2
  175. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  176. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  177. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  178. package/src/codex/desktop-compatibility/installed-build.ts +73 -0
  179. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  180. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  181. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  182. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  183. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  184. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  185. package/src/codex/desktop-compatibility/runtime.ts +182 -0
  186. package/src/codex/desktop-compatibility/service.ts +30 -0
  187. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  188. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  189. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  190. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  191. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  192. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  193. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  194. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  195. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  196. package/src/codex/desktop-compatibility/windows-key-protection.ts +165 -0
  197. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  198. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  199. package/src/codex/features.ts +85 -45
  200. package/src/codex/history-provider.ts +1 -1
  201. package/src/codex/inject/config-write-section.ts +4 -1
  202. package/src/codex/inject/history-failure.ts +27 -0
  203. package/src/codex/inject/remove.ts +18 -6
  204. package/src/codex/inject/restore.ts +24 -2
  205. package/src/codex/inject/routing-classify.ts +4 -3
  206. package/src/codex/inject.ts +18 -27
  207. package/src/codex/journal.ts +16 -1
  208. package/src/codex/main-account-hard-lock.ts +30 -1
  209. package/src/codex/main-account.ts +99 -15
  210. package/src/codex/native-main-refresh-mode.ts +217 -0
  211. package/src/codex/native-profile-startup.ts +1 -1
  212. package/src/codex/ocx-compaction-history.ts +12 -10
  213. package/src/codex/paths.ts +86 -1
  214. package/src/codex/provider-routing.ts +174 -0
  215. package/src/codex/refresh.ts +2 -2
  216. package/src/codex/routing/health-store.ts +2 -2
  217. package/src/codex/routing/pool-recovery.ts +79 -0
  218. package/src/codex/routing/selection.ts +4 -3
  219. package/src/codex/routing/thread-affinity.ts +1 -1
  220. package/src/codex/shim-diagnostics.ts +6 -0
  221. package/src/codex/shim-overlay.ts +7 -2
  222. package/src/codex/signed-in-identity.ts +43 -0
  223. package/src/codex/subagent-model-fallback.ts +1 -1
  224. package/src/codex/sync.ts +6 -1
  225. package/src/codex/user-identity.ts +16 -5
  226. package/src/codex/windows-local-appdata.ts +84 -0
  227. package/src/combos/failover.ts +6 -1
  228. package/src/combos/index.ts +1 -0
  229. package/src/combos/resolve.ts +46 -10
  230. package/src/combos/types.ts +15 -0
  231. package/src/config/diagnostics.ts +35 -7
  232. package/src/config/live-reconcile.ts +51 -2
  233. package/src/config/load-degrade.ts +23 -1
  234. package/src/config/opencode-wire-upgrade.ts +54 -0
  235. package/src/config/opencode-zen-upgrade.ts +52 -0
  236. package/src/config/owner-registry.ts +22 -17
  237. package/src/config/paths.ts +10 -3
  238. package/src/config/provider-validation.ts +45 -2
  239. package/src/config/schema/config-schema.ts +39 -0
  240. package/src/config/schema/desktop-compatibility.ts +9 -0
  241. package/src/config/schema/leaf-validators.ts +39 -9
  242. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  243. package/src/config/schema/native-reasoning-retention.ts +26 -0
  244. package/src/config.ts +2 -0
  245. package/src/generated/compatibility-version.json +962 -350
  246. package/src/generated/model-metadata.ts +1 -1
  247. package/src/grok/sync.ts +1 -1
  248. package/src/images/loop.ts +46 -8
  249. package/src/integrations/catalog-refresh.ts +1 -1
  250. package/src/integrations/config-io.ts +20 -2
  251. package/src/integrations/journal.ts +15 -1
  252. package/src/integrations/mutation-plan.ts +2 -0
  253. package/src/integrations/registry.ts +15 -0
  254. package/src/integrations/store.ts +3 -0
  255. package/src/integrations/writer.ts +10 -1
  256. package/src/lib/advisor-activation.ts +16 -0
  257. package/src/lib/background-work.ts +65 -0
  258. package/src/lib/bounded-file-read.ts +58 -0
  259. package/src/lib/bun-path-runtime.d.mts +10 -1
  260. package/src/lib/bun-path-runtime.mjs +62 -3
  261. package/src/lib/codex-restart-contract.ts +15 -0
  262. package/src/lib/desktop-proxy-route.ts +30 -0
  263. package/src/lib/desktop-upstream-tunnel.ts +72 -0
  264. package/src/lib/empty-tool-catalog.ts +62 -0
  265. package/src/lib/errors.ts +26 -3
  266. package/src/lib/gcp-adc.ts +96 -9
  267. package/src/lib/gcp-credential-input.ts +12 -0
  268. package/src/lib/lab-live-route-production.ts +5 -0
  269. package/src/lib/local-internal-call-capability.ts +55 -0
  270. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  271. package/src/lib/package-tree-integrity.ts +2 -2
  272. package/src/lib/redact.ts +2 -2
  273. package/src/lib/socks5-fetch.ts +48 -71
  274. package/src/lib/socks5-handshake.ts +112 -0
  275. package/src/lib/translator-budget.ts +7 -1
  276. package/src/oauth/account-quota-rank.ts +69 -2
  277. package/src/oauth/anthropic-account-refusal.ts +20 -2
  278. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  279. package/src/oauth/anthropic-routing.ts +1 -21
  280. package/src/oauth/anthropic.ts +38 -7
  281. package/src/oauth/callback-server.ts +27 -1
  282. package/src/oauth/chatgpt-device.ts +1 -1
  283. package/src/oauth/chatgpt-shared.ts +83 -0
  284. package/src/oauth/chatgpt.ts +8 -72
  285. package/src/oauth/dsh.ts +203 -0
  286. package/src/oauth/generic-account-failover.ts +62 -12
  287. package/src/oauth/generic-session-affinity.ts +125 -0
  288. package/src/oauth/index.ts +188 -111
  289. package/src/oauth/kiro-device-login.ts +2 -1
  290. package/src/oauth/kiro.ts +7 -1
  291. package/src/oauth/local-token-detect.ts +21 -1
  292. package/src/oauth/login-cli-errors.ts +44 -0
  293. package/src/oauth/login-cli.ts +69 -7
  294. package/src/oauth/login-flow-state.ts +371 -23
  295. package/src/oauth/meta-muse-errors.ts +41 -0
  296. package/src/oauth/meta-muse-keychain.ts +52 -0
  297. package/src/oauth/meta-muse.ts +51 -84
  298. package/src/oauth/model-discovery-auth.ts +3 -0
  299. package/src/oauth/pool-settings-capability.ts +4 -2
  300. package/src/oauth/refresh-flight-retirement.ts +48 -0
  301. package/src/oauth/session-affinity-key.ts +23 -0
  302. package/src/oauth/store-anthropic-instance.ts +9 -2
  303. package/src/oauth/store.ts +12 -5
  304. package/src/protocols/codecs/chat-ir.ts +467 -0
  305. package/src/protocols/codecs/chat.ts +10 -1
  306. package/src/protocols/codecs/messages-ir.ts +500 -0
  307. package/src/protocols/encoders/adapter-events.ts +5 -4
  308. package/src/protocols/encoders/messages.ts +9 -6
  309. package/src/protocols/features.ts +10 -2
  310. package/src/protocols/path.ts +11 -1
  311. package/src/protocols/plan-snapshot.ts +39 -16
  312. package/src/protocols/plan.ts +9 -1
  313. package/src/protocols/provider-summary.ts +2 -1
  314. package/src/protocols/settings.ts +8 -0
  315. package/src/providers/antigravity-account-roster.ts +102 -0
  316. package/src/providers/antigravity-models.ts +85 -7
  317. package/src/providers/api-key-cooldowns.ts +49 -0
  318. package/src/providers/api-key-resolve.ts +11 -6
  319. package/src/providers/api-key-response-ownership.ts +57 -0
  320. package/src/providers/api-key-selection.ts +31 -6
  321. package/src/providers/api-keys.ts +214 -3
  322. package/src/providers/claude-cli-usage.ts +96 -0
  323. package/src/providers/dsh-account-destination.ts +31 -0
  324. package/src/providers/dsh-account-models.ts +41 -0
  325. package/src/providers/gcp-credential-cleanup.ts +59 -0
  326. package/src/providers/key-failover.ts +54 -50
  327. package/src/providers/key-store.ts +7 -2
  328. package/src/providers/model-rename-fields.ts +5 -0
  329. package/src/providers/model-rename-migration.ts +7 -5
  330. package/src/providers/openai-tiers-destination.ts +11 -2
  331. package/src/providers/openai-tiers.ts +5 -12
  332. package/src/providers/opencode-go-transport.ts +3 -1
  333. package/src/providers/quota/account-cache.ts +6 -5
  334. package/src/providers/quota/account-row.ts +34 -0
  335. package/src/providers/quota/antigravity.ts +28 -7
  336. package/src/providers/quota/dsh-account.ts +70 -0
  337. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  338. package/src/providers/quota-types.ts +2 -0
  339. package/src/providers/quota.ts +14 -20
  340. package/src/providers/registry/entries-core.ts +44 -14
  341. package/src/providers/registry/entries-extended.ts +19 -0
  342. package/src/providers/registry/types.ts +1 -1
  343. package/src/providers/request-pacing.ts +77 -19
  344. package/src/providers/routed-config.ts +339 -0
  345. package/src/providers/service-tier.ts +2 -1
  346. package/src/quota/window-mapping.ts +5 -2
  347. package/src/responses/anthropic-progress.ts +34 -0
  348. package/src/responses/assistant-replay.ts +35 -0
  349. package/src/responses/compaction-history-framing.ts +65 -0
  350. package/src/responses/compaction.ts +6 -1
  351. package/src/responses/incomplete-reason.ts +31 -0
  352. package/src/responses/parser.ts +78 -22
  353. package/src/responses/reasoning-envelope.ts +4 -0
  354. package/src/responses/reasoning-replay-cache.ts +24 -2
  355. package/src/responses/reasoning-retention.ts +173 -0
  356. package/src/responses/retained-compaction.ts +46 -0
  357. package/src/responses/schema.ts +23 -0
  358. package/src/responses/state.ts +32 -29
  359. package/src/router.ts +8 -321
  360. package/src/routing/health.ts +37 -22
  361. package/src/routing/history/build-file.ts +92 -0
  362. package/src/routing/history/indexer.ts +254 -221
  363. package/src/routing/history/ingest.ts +242 -0
  364. package/src/routing/history/rebuild-job.ts +111 -0
  365. package/src/routing/history/rebuild-protocol.ts +15 -0
  366. package/src/routing/history/rebuild-worker.ts +34 -0
  367. package/src/routing/profile.ts +1 -1
  368. package/src/server/admission-model-scope.ts +21 -1
  369. package/src/server/antigravity-direct-eligibility.ts +208 -0
  370. package/src/server/auth-cors.ts +15 -0
  371. package/src/server/chat-completions.ts +82 -10
  372. package/src/server/chat-native-reasoning.ts +123 -0
  373. package/src/server/chat-native-sse.ts +110 -7
  374. package/src/server/chat-native.ts +76 -17
  375. package/src/server/claude-messages.ts +164 -433
  376. package/src/server/effort-policy.ts +1 -1
  377. package/src/server/images.ts +15 -2
  378. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  379. package/src/server/index/model-alias-id.ts +36 -0
  380. package/src/server/index/optional-listeners.ts +2 -0
  381. package/src/server/index/serve-options.ts +11 -3
  382. package/src/server/index/startup-warnings.ts +11 -0
  383. package/src/server/index/websocket-handler.ts +9 -0
  384. package/src/server/index.ts +14 -11
  385. package/src/server/inference/advisor-diagnostics.ts +31 -0
  386. package/src/server/inference/client-encoder-delivery.ts +6 -5
  387. package/src/server/login-shell-path.ts +146 -0
  388. package/src/server/management/advisor-routes.ts +202 -0
  389. package/src/server/management/agent-settings-routes.ts +22 -4
  390. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  391. package/src/server/management/combo-routes.ts +176 -50
  392. package/src/server/management/companion-routes.ts +4 -0
  393. package/src/server/management/config-routes.ts +77 -6
  394. package/src/server/management/context.ts +4 -0
  395. package/src/server/management/cursor-integration-routes.ts +1 -1
  396. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  397. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  398. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  399. package/src/server/management/model-routes.ts +13 -8
  400. package/src/server/management/model-rows.ts +14 -0
  401. package/src/server/management/native-integration-routes.ts +2 -7
  402. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  403. package/src/server/management/oauth-account-routes.ts +87 -14
  404. package/src/server/management/protocol-settings-patch.ts +1 -0
  405. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  406. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  407. package/src/server/management/provider-overwrite-carry.ts +7 -0
  408. package/src/server/management/provider-routes.ts +25 -30
  409. package/src/server/management/route-registry.ts +21 -1
  410. package/src/server/management/sibling-guard.ts +1 -0
  411. package/src/server/management-api.ts +9 -1
  412. package/src/server/messages-antigravity-direct.ts +195 -0
  413. package/src/server/messages-native-tool-guard.ts +185 -0
  414. package/src/server/messages-native.ts +106 -22
  415. package/src/server/messages-shared.ts +431 -0
  416. package/src/server/relay.ts +1 -1
  417. package/src/server/request-log-terminal-status.ts +1 -1
  418. package/src/server/request-log.ts +13 -1
  419. package/src/server/responses/account-change-state.ts +8 -2
  420. package/src/server/responses/adapter-continuation.ts +48 -19
  421. package/src/server/responses/adapter-delivery.ts +43 -8
  422. package/src/server/responses/adapter-dispatch.ts +48 -4
  423. package/src/server/responses/advisor-plan-slot.ts +27 -0
  424. package/src/server/responses/advisor-retention.ts +117 -0
  425. package/src/server/responses/advisor-slot.ts +368 -0
  426. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  427. package/src/server/responses/codex-ws-wire.ts +15 -8
  428. package/src/server/responses/compact.ts +303 -125
  429. package/src/server/responses/compaction-recovery.ts +18 -10
  430. package/src/server/responses/compaction-retention.ts +61 -0
  431. package/src/server/responses/context-overflow.ts +11 -6
  432. package/src/server/responses/core-auth.ts +6 -0
  433. package/src/server/responses/core-codex-account.ts +11 -1
  434. package/src/server/responses/core-combo-failure.ts +12 -1
  435. package/src/server/responses/core-combo.ts +141 -36
  436. package/src/server/responses/core-errors.ts +3 -3
  437. package/src/server/responses/core-lifetime.ts +7 -2
  438. package/src/server/responses/core-normalize.ts +11 -0
  439. package/src/server/responses/core-opaque-recovery.ts +35 -0
  440. package/src/server/responses/core-options.ts +51 -0
  441. package/src/server/responses/core-pipeline.ts +141 -0
  442. package/src/server/responses/core-replay.ts +25 -6
  443. package/src/server/responses/core.ts +3 -116
  444. package/src/server/responses/fetch-helpers.ts +73 -19
  445. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  446. package/src/server/responses/native-response-control.ts +4 -2
  447. package/src/server/responses/passthrough-delivery.ts +19 -11
  448. package/src/server/responses/passthrough-dispatch.ts +29 -16
  449. package/src/server/responses/request-prepare.ts +144 -17
  450. package/src/server/responses/request-sidecar-auth.ts +2 -2
  451. package/src/server/responses/request-transport.ts +120 -27
  452. package/src/server/responses/response-effects.ts +1 -0
  453. package/src/server/responses/run-turn-execution.ts +166 -38
  454. package/src/server/responses/sidecar-execution.ts +98 -5
  455. package/src/server/responses/terminal-guard.ts +4 -17
  456. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  457. package/src/server/startup-action-control.ts +16 -2
  458. package/src/server/startup-health-cache.ts +1 -0
  459. package/src/service/auth-preflight.ts +71 -0
  460. package/src/service/cli.ts +4 -2
  461. package/src/service/definition.ts +97 -0
  462. package/src/service/desktop-startup.ts +14 -0
  463. package/src/service/guards.ts +1 -61
  464. package/src/service/health-sentinel-linux.ts +112 -0
  465. package/src/service/health-sentinel-main.ts +99 -61
  466. package/src/service/health-sentinel-probe.ts +24 -7
  467. package/src/service/health.ts +5 -89
  468. package/src/service/launchd.ts +27 -7
  469. package/src/service/logging.ts +135 -0
  470. package/src/service/repair.ts +2 -1
  471. package/src/service/state.ts +7 -8
  472. package/src/service/systemd.ts +1 -1
  473. package/src/service/windows-guarded-manager.ts +2 -1
  474. package/src/service/windows-ops.ts +1 -1
  475. package/src/service/windows-scheduler.ts +2 -7
  476. package/src/service/windows-taskxml.ts +2 -2
  477. package/src/service/windows-wscript.ts +14 -0
  478. package/src/service.ts +4 -2
  479. package/src/sidecar/auth.ts +2 -0
  480. package/src/tray/windows-tray.ps1 +43 -24
  481. package/src/types/config.ts +124 -6
  482. package/src/types/opencode-go.ts +61 -0
  483. package/src/types/opencode-zen.ts +29 -0
  484. package/src/types/provider.ts +39 -6
  485. package/src/types/request.ts +38 -2
  486. package/src/types/tools.ts +2 -0
  487. package/src/types/wire.ts +29 -37
  488. package/src/types.ts +1 -0
  489. package/src/update/async-check.ts +1 -1
  490. package/src/update/badge.ts +2 -2
  491. package/src/update/index.ts +88 -15
  492. package/src/update/job-supervised-restart.ts +107 -0
  493. package/src/update/job.ts +32 -32
  494. package/src/update/notify.ts +1 -1
  495. package/src/update/npm-invocation.mjs +12 -2
  496. package/src/update/refresh-scheduler.ts +2 -2
  497. package/src/update/supervised-recovery.d.mts +27 -0
  498. package/src/update/supervised-recovery.mjs +157 -0
  499. package/src/usage/cache-diagnostic.ts +2 -0
  500. package/src/usage/cost.ts +48 -12
  501. package/src/usage/expected-prices.ts +15 -0
  502. package/src/usage/ledger-retention.ts +2 -1
  503. package/src/usage/ledger-scanner.ts +6 -5
  504. package/src/usage/log.ts +17 -8
  505. package/src/usage/summary.ts +22 -9
  506. package/src/usage/user-cost-overlays.ts +14 -19
  507. package/src/usage/user-cost-validation.ts +38 -0
  508. package/src/vision/eligibility.ts +13 -0
  509. package/src/web-search/gemini-executor.ts +2 -2
  510. package/src/web-search/loop.ts +55 -5
  511. package/src/web-search/progress-stream.ts +4 -1
  512. package/src/web-search/run-turn-loop.ts +9 -1
  513. package/assets/sponsors/packycode.png +0 -0
  514. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  515. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  516. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  517. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  518. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The private build file of a worker rebuild (#6990).
3
+ *
4
+ * The worker writes the whole index into `<index>.rebuild-<pid>-<n>` next to
5
+ * the published file; the serving thread swaps it in only once it is complete,
6
+ * so no reader ever sees a partial index.
7
+ */
8
+
9
+ import { Database } from "bun:sqlite";
10
+ import { readdirSync, rmSync } from "node:fs";
11
+ import { basename, dirname, join } from "node:path";
12
+ import type { UsageLogRevision } from "../../usage/log";
13
+ import { ingestSourceTail, recordSourceMeta, resetAndCreateSchema, setMeta } from "./ingest";
14
+ import { HISTORY_META_KEYS } from "./schema";
15
+
16
+ const BUILD_SIDE_FILES = ["", "-journal", "-wal", "-shm"] as const;
17
+
18
+ export function buildFilePath(indexPath: string, pid: number, sequence: number): string {
19
+ return `${indexPath}.rebuild-${pid}-${sequence}`;
20
+ }
21
+
22
+ export function removeBuildFiles(buildPath: string): void {
23
+ for (const suffix of BUILD_SIDE_FILES) {
24
+ try { rmSync(`${buildPath}${suffix}`, { force: true }); } catch { /* best effort; swept on a later build */ }
25
+ }
26
+ }
27
+
28
+ function processIsAlive(pid: number): boolean {
29
+ try {
30
+ process.kill(pid, 0);
31
+ return true;
32
+ } catch (error) {
33
+ // EPERM: the process exists under another user. Anything but ESRCH is treated as alive.
34
+ return (error as { code?: unknown } | null)?.code !== "ESRCH";
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Remove build files a crashed process left beside the index. A build of this
40
+ * process, or of another live process, is left alone; this process removes
41
+ * its own builds once their worker has exited.
42
+ */
43
+ export function removeAbandonedBuilds(indexPath: string): number {
44
+ const prefix = `${basename(indexPath)}.rebuild-`;
45
+ let names: string[];
46
+ try {
47
+ names = readdirSync(dirname(indexPath));
48
+ } catch {
49
+ return 0;
50
+ }
51
+ const abandoned = new Set<string>();
52
+ for (const name of names) {
53
+ if (!name.startsWith(prefix)) continue;
54
+ const match = /^(\d+)-(\d+)/.exec(name.slice(prefix.length));
55
+ if (!match) continue;
56
+ const pid = Number(match[1]);
57
+ if (pid === process.pid || processIsAlive(pid)) continue;
58
+ abandoned.add(`${prefix}${match[1]}-${match[2]}`);
59
+ }
60
+ for (const name of abandoned) removeBuildFiles(join(dirname(indexPath), name));
61
+ return abandoned.size;
62
+ }
63
+
64
+ /**
65
+ * Build a complete index of the ledger snapshot `revision` describes into
66
+ * `buildPath`, leave it in WAL mode and close it. Returns the rows ingested.
67
+ * A private build needs no crash durability: an interrupted one is discarded.
68
+ */
69
+ export function buildIndexFile(buildPath: string, reason: string, revision: UsageLogRevision): number {
70
+ removeBuildFiles(buildPath);
71
+ const build = new Database(buildPath, { create: true, readwrite: true });
72
+ try {
73
+ build.exec("PRAGMA journal_mode = MEMORY");
74
+ build.exec("PRAGMA synchronous = OFF");
75
+ resetAndCreateSchema(build);
76
+ setMeta(build, HISTORY_META_KEYS.lastError, reason);
77
+ recordSourceMeta(build, revision);
78
+ const inserted = ingestSourceTail(build, revision.path, 0, {
79
+ expected: revision,
80
+ endOffset: Number(revision.size),
81
+ });
82
+ // The serving thread opens the published file in WAL mode; switching here
83
+ // keeps that first open from rewriting the database header.
84
+ build.exec("PRAGMA journal_mode = WAL");
85
+ build.close();
86
+ return inserted;
87
+ } catch (error) {
88
+ try { build.close(); } catch { /* already unusable */ }
89
+ removeBuildFiles(buildPath);
90
+ throw error;
91
+ }
92
+ }
@@ -7,17 +7,15 @@
7
7
  * JSONL rows arrived since the last index. Missing/corrupt/stale index or a
8
8
  * replaced/truncated source triggers an automatic full rebuild; canonical
9
9
  * history is never touched.
10
+ *
11
+ * A full rebuild of a ledger of at least `REQUEST_HISTORY_WORKER_REBUILD_MIN_BYTES`
12
+ * runs on a worker thread (#6990): the worker writes a private build file and
13
+ * this thread swaps it in once complete. Async readers wait for it; the
14
+ * routing-time sync entry never waits and throws `RequestHistoryIndexBusyError`.
10
15
  */
11
16
 
12
17
  import { Database } from "bun:sqlite";
13
- import {
14
- closeSync,
15
- existsSync,
16
- fstatSync,
17
- openSync,
18
- readSync,
19
- unlinkSync,
20
- } from "node:fs";
18
+ import { existsSync, renameSync, unlinkSync } from "node:fs";
21
19
  import { getConfigDir } from "../../config";
22
20
  import { recordOwnedConfigPath } from "../../lib/config-ownership";
23
21
  import {
@@ -40,6 +38,22 @@ import {
40
38
  InvalidCursorError,
41
39
  type HistoryCursor,
42
40
  } from "./cursor";
41
+ import {
42
+ ingestSourceTail,
43
+ metaValue,
44
+ recordSourceMeta,
45
+ RequestHistoryIndexStaleError,
46
+ resetAndCreateSchema,
47
+ setMeta,
48
+ } from "./ingest";
49
+ import { buildFilePath, removeBuildFiles } from "./build-file";
50
+ import { RebuildWorkerJob } from "./rebuild-job";
51
+
52
+ export {
53
+ REQUEST_HISTORY_INSERT_BATCH,
54
+ REQUEST_HISTORY_MAX_RECORD_BYTES,
55
+ REQUEST_HISTORY_READ_CHUNK_BYTES,
56
+ } from "./ingest";
43
57
 
44
58
  export interface RequestHistoryIndexMeta {
45
59
  schemaVersion: number;
@@ -76,16 +90,74 @@ export interface RequestHistoryPage {
76
90
 
77
91
  export const REQUEST_HISTORY_MAX_PAGE_SIZE = 100;
78
92
  export const REQUEST_HISTORY_DEFAULT_PAGE_SIZE = 50;
79
- export const REQUEST_HISTORY_INSERT_BATCH = 500;
80
- export const REQUEST_HISTORY_READ_CHUNK_BYTES = 64 * 1024;
81
- // The SQLite index is a disposable projection. Complete JSONL records above
82
- // this bound are omitted from the projection; the canonical usage.jsonl is
83
- // never truncated or rewritten by the indexer.
84
- export const REQUEST_HISTORY_MAX_RECORD_BYTES = 1024 * 1024;
93
+ // Ledgers at least this large rebuild on the worker; smaller ones rebuild
94
+ // inline, where a worker spawn would cost more than the ingest.
95
+ export const REQUEST_HISTORY_WORKER_REBUILD_MIN_BYTES = 1024 * 1024;
96
+ // A worker build that failed for any reason but a replaced ledger is not
97
+ // retried by routing-time reads for this long, so a persistent failure (disk
98
+ // full, unreadable ledger) cannot spawn a worker on every routing decision.
99
+ const WORKER_FAILURE_COOLDOWN_MS = 30_000;
100
+ // Async readers wait for at most this many worker rebuilds before reporting the
101
+ // index busy (a ledger replaced again during every build).
102
+ const MAX_ASYNC_REFRESH_BUILDS = 4;
103
+
104
+ /** The index is being rebuilt in the background; routing treats history as unknown meanwhile. */
105
+ export class RequestHistoryIndexBusyError extends Error {
106
+ readonly code = "request_history_index_busy";
107
+ constructor(message = "request-history index is rebuilding") {
108
+ super(message);
109
+ this.name = "RequestHistoryIndexBusyError";
110
+ }
111
+ }
112
+
113
+ /** The index was closed while a reader waited for its rebuild. */
114
+ export class RequestHistoryIndexClosedError extends Error {
115
+ readonly code = "request_history_index_closed";
116
+ constructor(message = "request-history index was closed during a rebuild") {
117
+ super(message);
118
+ this.name = "RequestHistoryIndexClosedError";
119
+ }
120
+ }
121
+
122
+ interface WorkerRebuild {
123
+ readonly job: RebuildJob;
124
+ readonly promise: Promise<void>;
125
+ }
126
+
127
+ /** The part of a rebuild worker job the indexer drives; tests substitute a failing one. */
128
+ type RebuildJob = Pick<RebuildWorkerJob, "build" | "dispose" | "afterExit">;
129
+ let createRebuildJob: () => RebuildJob = () => new RebuildWorkerJob();
130
+
131
+ /** Test seam: replace the worker job factory; `undefined` restores the real worker. */
132
+ export function setRequestHistoryRebuildJobFactoryForTests(factory: (() => RebuildJob) | undefined): void {
133
+ createRebuildJob = factory ?? (() => new RebuildWorkerJob());
134
+ }
135
+
136
+ /** Internal signal: the refresh needs a rebuild that is running (or now started) on the worker. */
137
+ class RebuildHandedOff {
138
+ constructor(readonly rebuild: WorkerRebuild) {}
139
+ }
85
140
 
86
141
  let db: Database | null = null;
87
142
  let dbPath = "";
88
143
  let openPromise: Promise<RequestHistoryIndexMeta> | null = null;
144
+ let workerRebuild: WorkerRebuild | null = null;
145
+ let workerFailure: { at: number; error: unknown } | null = null;
146
+ let workerRebuildMinBytes = REQUEST_HISTORY_WORKER_REBUILD_MIN_BYTES;
147
+ let maxAsyncRefreshBuilds = MAX_ASYNC_REFRESH_BUILDS;
148
+ let buildSequence = 0;
149
+ // Bumped by closeRequestHistoryIndex, so a rebuild finishing after a close is discarded.
150
+ let indexLifetime = 0;
151
+
152
+ /** Test seam: move the worker threshold; `undefined` restores the default. */
153
+ export function setRequestHistoryWorkerRebuildMinBytesForTests(bytes: number | undefined): void {
154
+ workerRebuildMinBytes = bytes ?? REQUEST_HISTORY_WORKER_REBUILD_MIN_BYTES;
155
+ }
156
+
157
+ /** Test seam: how many worker rebuilds one async refresh waits for; `undefined` restores the default. */
158
+ export function setRequestHistoryAsyncRefreshBuildsForTests(builds: number | undefined): void {
159
+ maxAsyncRefreshBuilds = builds ?? MAX_ASYNC_REFRESH_BUILDS;
160
+ }
89
161
 
90
162
  function indexDbPath(): string {
91
163
  const dir = getConfigDir();
@@ -94,19 +166,6 @@ function indexDbPath(): string {
94
166
  return path;
95
167
  }
96
168
 
97
- function metaValue(dbHandle: Database, key: string): string | null {
98
- const row = dbHandle.query("SELECT value FROM schema_meta WHERE key = ?").get(key) as
99
- | { value: string }
100
- | undefined;
101
- return row?.value ?? null;
102
- }
103
-
104
- function setMeta(dbHandle: Database, key: string, value: string | number): void {
105
- dbHandle.query(
106
- "INSERT INTO schema_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
107
- ).run(key, String(value));
108
- }
109
-
110
169
  function readIndexedMeta(dbHandle: Database): Omit<RequestHistoryIndexMeta, "dbPath"> {
111
170
  const asNumber = (key: string): number => {
112
171
  const value = metaValue(dbHandle, key);
@@ -136,186 +195,21 @@ function readStoredMetaField(dbHandle: Database, key: string): string {
136
195
  return metaValue(dbHandle, key) ?? "";
137
196
  }
138
197
 
139
- function sourceIdentityMatches(dbHandle: Database, revision: UsageLogRevision | null): boolean {
198
+ export function sourceIdentityMatches(dbHandle: Database, revision: UsageLogRevision | null): boolean {
140
199
  const stored = readIndexedMeta(dbHandle);
141
200
  if (revision === null) return stored.sourceSize === 0;
142
201
  const storedPath = readStoredMetaField(dbHandle, HISTORY_META_KEYS.sourcePath);
143
202
  if (!storedPath) return false;
144
- const storedDev = Number(readStoredMetaField(dbHandle, HISTORY_META_KEYS.sourceDev));
145
- const storedIno = Number(readStoredMetaField(dbHandle, HISTORY_META_KEYS.sourceIno));
203
+ // Compared as exact decimal strings; see UsageLogRevision for why ids are not Numbers.
204
+ const storedDev = readStoredMetaField(dbHandle, HISTORY_META_KEYS.sourceDev);
205
+ const storedIno = readStoredMetaField(dbHandle, HISTORY_META_KEYS.sourceIno);
146
206
  const storedBirthtimeMs = Number(readStoredMetaField(dbHandle, HISTORY_META_KEYS.sourceBirthtimeMs));
147
207
  return storedPath === revision.path
148
- && storedDev === Number(revision.dev)
149
- && storedIno === Number(revision.ino)
208
+ && storedDev === revision.dev
209
+ && storedIno === revision.ino
150
210
  && storedBirthtimeMs === Number(revision.birthtimeMs);
151
211
  }
152
212
 
153
- /** Extract the `requests` row columns from a canonical persisted entry. */
154
- function extractRow(entry: PersistedUsageEntry): Array<string | number | null> {
155
- const attempts = entry.attempts;
156
- return [
157
- entry.requestId,
158
- entry.timestamp,
159
- entry.provider,
160
- entry.model,
161
- // Old canonical JSONL rows can predate bounded selector persistence. Encode
162
- // the disposable projection on rebuild so exact filters match across versions.
163
- typeof entry.requestedModel === "string" ? encodePersistedRequestedModel(entry.requestedModel) : null,
164
- entry.status,
165
- entry.surface ?? null,
166
- entry.inboundProtocol ?? null,
167
- entry.apiKeyId ?? null,
168
- entry.conversationId ?? null,
169
- entry.routeDecision?.routeKind ?? null,
170
- entry.routeDecision?.profile?.id ?? null,
171
- entry.routeDecision?.profile?.revision ?? null,
172
- (attempts?.length ?? 0) > 1 ? 1 : 0,
173
- entry.durationMs,
174
- entry.firstOutputMs ?? null,
175
- entry.usageStatus,
176
- entry.usage ? JSON.stringify(entry.usage) : null,
177
- entry.totalTokens ?? null,
178
- entry.errorCode ?? null,
179
- entry.terminalStatus ?? null,
180
- entry.closeReason ?? null,
181
- attempts?.length ?? 1,
182
- entry.routeDecision ? JSON.stringify(entry.routeDecision) : null,
183
- JSON.stringify(entry),
184
- ];
185
- }
186
-
187
- const ROW_INSERT = `
188
- INSERT OR IGNORE INTO requests (
189
- request_id, timestamp, provider, model, requested_model, status, surface,
190
- inbound_protocol, api_key_id, conversation_id, route_kind, profile_id,
191
- profile_revision, fallback, duration_ms, first_output_ms, usage_status,
192
- usage_json, total_tokens, error_code, terminal_status, close_reason,
193
- attempt_count, decision_json, row_json
194
- ) VALUES (
195
- ?, ?, ?, ?, ?, ?, ?,
196
- ?, ?, ?, ?, ?, ?,
197
- ?, ?, ?, ?, ?, ?, ?,
198
- ?, ?, ?, ?, ?
199
- )`;
200
-
201
- function parsedEntryFromLine(line: string): PersistedUsageEntry | null {
202
- if (!line.trim()) return null;
203
- try {
204
- const parsed = JSON.parse(line) as PersistedUsageEntry;
205
- if (parsed && typeof parsed === "object"
206
- && typeof parsed.requestId === "string"
207
- && typeof parsed.timestamp === "number"
208
- && typeof parsed.provider === "string"
209
- && typeof parsed.model === "string"
210
- && typeof parsed.status === "number"
211
- && typeof parsed.durationMs === "number") {
212
- return parsed;
213
- }
214
- } catch {
215
- /* skip partial / hand-edited lines, same as every other reader */
216
- }
217
- return null;
218
- }
219
-
220
- function ingestSourceTail(dbHandle: Database, path: string, fromOffset: number): number {
221
- let inserted = 0;
222
- let pending: Array<Array<string | number | null>> = [];
223
- const insert = dbHandle.prepare(ROW_INSERT);
224
- let fd: number | undefined;
225
- try {
226
- const commitBatch = () => {
227
- dbHandle.transaction((rows: Array<Array<string | number | null>>) => {
228
- for (const row of rows) {
229
- // `changes` counts real inserts only; INSERT OR IGNORE replays add 0.
230
- inserted += insert.run(...row).changes;
231
- }
232
- })(pending);
233
- pending = [];
234
- };
235
- const ingestLine = (line: Buffer) => {
236
- const entry = parsedEntryFromLine(line.toString("utf-8"));
237
- if (!entry) return;
238
- pending.push(extractRow(entry));
239
- if (pending.length >= REQUEST_HISTORY_INSERT_BATCH) commitBatch();
240
- };
241
-
242
- fd = openSync(path, "r");
243
- const stat = fstatSync(fd);
244
- const size = Number(stat.size);
245
- const readBuffer = Buffer.allocUnsafe(REQUEST_HISTORY_READ_CHUNK_BYTES);
246
- let position = fromOffset;
247
- let nextOffset = fromOffset;
248
- let fragments: Buffer[] = [];
249
- let fragmentBytes = 0;
250
- let oversized = false;
251
- while (position < size) {
252
- const requested = Math.min(readBuffer.length, size - position);
253
- const bytesRead = readSync(fd, readBuffer, 0, requested, position);
254
- if (bytesRead === 0) throw new Error("usage log changed while indexing");
255
- let lineStart = 0;
256
- for (let index = 0; index < bytesRead; index++) {
257
- if (readBuffer[index] !== 0x0a) continue;
258
- const segment = readBuffer.subarray(lineStart, index);
259
- if (!oversized && fragmentBytes + segment.length <= REQUEST_HISTORY_MAX_RECORD_BYTES) {
260
- const line = fragments.length === 0
261
- ? segment
262
- : Buffer.concat([...fragments, segment], fragmentBytes + segment.length);
263
- ingestLine(line);
264
- }
265
- fragments = [];
266
- fragmentBytes = 0;
267
- oversized = false;
268
- lineStart = index + 1;
269
- nextOffset = position + index + 1;
270
- }
271
- const remainder = readBuffer.subarray(lineStart, bytesRead);
272
- if (!oversized && fragmentBytes + remainder.length <= REQUEST_HISTORY_MAX_RECORD_BYTES) {
273
- // Copy because readBuffer is reused on the next iteration.
274
- fragments.push(Buffer.from(remainder));
275
- fragmentBytes += remainder.length;
276
- } else if (remainder.length > 0) {
277
- fragments = [];
278
- fragmentBytes = 0;
279
- oversized = true;
280
- }
281
- position += bytesRead;
282
- }
283
- if (pending.length > 0) commitBatch();
284
- const current = readIndexedMeta(dbHandle);
285
- setMeta(dbHandle, HISTORY_META_KEYS.indexedOffset, nextOffset);
286
- setMeta(dbHandle, HISTORY_META_KEYS.indexedRows, current.indexedRows + inserted);
287
- setMeta(dbHandle, HISTORY_META_KEYS.sourceSize, Number(stat.size));
288
- setMeta(dbHandle, HISTORY_META_KEYS.sourceMtimeMs, Number(stat.mtimeMs));
289
- setMeta(dbHandle, HISTORY_META_KEYS.builtAtMs, Date.now());
290
- return inserted;
291
- } finally {
292
- if (fd !== undefined) closeSync(fd);
293
- // Windows file locks: an unterminated prepared statement keeps the DB
294
- // file busy after close (verified on Bun 1.3.14). Finalize always.
295
- insert.finalize();
296
- }
297
- }
298
-
299
- function resetAndCreateSchema(dbHandle: Database): void {
300
- dbHandle.exec("DROP TABLE IF EXISTS requests");
301
- dbHandle.exec("DROP TABLE IF EXISTS schema_meta");
302
- dbHandle.exec(HISTORY_DDL);
303
- setMeta(dbHandle, HISTORY_META_KEYS.schemaVersion, HISTORY_SCHEMA_VERSION);
304
- setMeta(dbHandle, HISTORY_META_KEYS.indexedOffset, 0);
305
- setMeta(dbHandle, HISTORY_META_KEYS.indexedRows, 0);
306
- setMeta(dbHandle, HISTORY_META_KEYS.builtAtMs, Date.now());
307
- setMeta(dbHandle, HISTORY_META_KEYS.lastError, "rebuilt");
308
- }
309
-
310
- function recordSourceMeta(dbHandle: Database, revision: UsageLogRevision | null): void {
311
- setMeta(dbHandle, HISTORY_META_KEYS.sourcePath, revision?.path ?? "");
312
- setMeta(dbHandle, HISTORY_META_KEYS.sourceDev, revision?.dev ?? 0);
313
- setMeta(dbHandle, HISTORY_META_KEYS.sourceIno, revision?.ino ?? 0);
314
- setMeta(dbHandle, HISTORY_META_KEYS.sourceBirthtimeMs, revision?.birthtimeMs ?? 0);
315
- setMeta(dbHandle, HISTORY_META_KEYS.sourceSize, revision?.size ?? 0);
316
- setMeta(dbHandle, HISTORY_META_KEYS.sourceMtimeMs, revision?.mtimeMs ?? 0);
317
- }
318
-
319
213
  function isHealthy(dbHandle: Database): boolean {
320
214
  try {
321
215
  const row = dbHandle.query("PRAGMA quick_check").get() as { quick_check?: string } | undefined;
@@ -325,11 +219,8 @@ function isHealthy(dbHandle: Database): boolean {
325
219
  }
326
220
  }
327
221
 
328
- function destroyAndRecreate(path: string, reason: string): Database {
329
- if (db) {
330
- try { db.close(); } catch { /* already closed */ }
331
- db = null;
332
- }
222
+ /** Remove the index file and its WAL side files. */
223
+ function removeIndexFiles(path: string): void {
333
224
  // Windows: a partially-opened handle from a failed `new Database` can hold
334
225
  // the file briefly after the throw. Retry the unlink before recreating.
335
226
  for (let attempt = 0; attempt < 5; attempt++) {
@@ -337,13 +228,21 @@ function destroyAndRecreate(path: string, reason: string): Database {
337
228
  for (const suffix of ["-wal", "-shm"] as const) {
338
229
  try { unlinkSync(`${path}${suffix}`); } catch { /* sidecar may not exist */ }
339
230
  }
340
- unlinkSync(path);
341
- break;
231
+ if (existsSync(path)) unlinkSync(path);
232
+ return;
342
233
  } catch {
343
- if (attempt === 4) break;
234
+ if (attempt === 4) return;
344
235
  Bun.sleepSync(50);
345
236
  }
346
237
  }
238
+ }
239
+
240
+ function destroyAndRecreate(path: string, reason: string): Database {
241
+ if (db) {
242
+ try { db.close(); } catch { /* already closed */ }
243
+ db = null;
244
+ }
245
+ removeIndexFiles(path);
347
246
  const fresh = new Database(path, { create: true });
348
247
  fresh.exec("PRAGMA journal_mode = WAL");
349
248
  fresh.exec("PRAGMA busy_timeout = 5000");
@@ -429,14 +328,92 @@ function fullRebuild(dbHandle: Database, reason: string): void {
429
328
  setMeta(dbHandle, HISTORY_META_KEYS.builtAtMs, Date.now());
430
329
  }
431
330
 
432
- function refreshLockedSync(): RequestHistoryIndexMeta {
331
+ /**
332
+ * Start a worker rebuild of `revision`, or return null when no worker can be
333
+ * spawned (the caller then rebuilds inline, as before #6990).
334
+ */
335
+ function workerCoolingDown(): boolean {
336
+ return workerFailure !== null && Date.now() - workerFailure.at < WORKER_FAILURE_COOLDOWN_MS;
337
+ }
338
+
339
+ function startWorkerRebuild(reason: string, revision: UsageLogRevision): WorkerRebuild | null {
340
+ let job: RebuildJob;
341
+ try {
342
+ job = createRebuildJob();
343
+ } catch {
344
+ return null;
345
+ }
346
+ const lifetime = indexLifetime;
347
+ const indexPath = dbPath || indexDbPath();
348
+ const buildPath = buildFilePath(indexPath, process.pid, ++buildSequence);
349
+ const rebuild: { job: RebuildJob; promise: Promise<void> } = { job, promise: Promise.resolve() };
350
+ rebuild.promise = job.build(indexPath, buildPath, reason, revision)
351
+ .then(() => {
352
+ if (lifetime !== indexLifetime) throw new RequestHistoryIndexClosedError();
353
+ installBuild(buildPath, indexPath);
354
+ workerFailure = null;
355
+ })
356
+ .catch((error: unknown) => {
357
+ // A replaced ledger is retried at once; a close is nobody's failure.
358
+ if (lifetime === indexLifetime && !(error instanceof RequestHistoryIndexStaleError)) {
359
+ workerFailure = { at: Date.now(), error };
360
+ }
361
+ throw error;
362
+ })
363
+ .finally(() => {
364
+ if (workerRebuild === rebuild) workerRebuild = null;
365
+ void job.afterExit(() => removeBuildFiles(buildPath));
366
+ });
367
+ // A rebuild started by the routing-time entry has no awaiting reader.
368
+ rebuild.promise.catch(() => undefined);
369
+ workerRebuild = rebuild;
370
+ return rebuild;
371
+ }
372
+
373
+ /** Swap a complete worker build in for the current index file. */
374
+ function installBuild(buildPath: string, indexPath: string): void {
375
+ if (db) {
376
+ try { db.close(); } catch { /* already closed */ }
377
+ db = null;
378
+ }
379
+ removeIndexFiles(indexPath);
380
+ renameSync(buildPath, indexPath);
381
+ }
382
+
383
+ /**
384
+ * Who is refreshing. Routing-time reads never rebuild a large ledger inline;
385
+ * async readers do once the worker has just failed, which is the pre-#6990
386
+ * behaviour and better than leaving history unavailable.
387
+ */
388
+ type RefreshCaller = "routing" | "reader";
389
+
390
+ /** Full rebuild, on the worker when the ledger is large enough to stall the event loop. */
391
+ function rebuildFromLedger(handle: Database, reason: string, caller: RefreshCaller): void {
392
+ const revision = sourceIdentity();
393
+ if (revision !== null && Number(revision.size) >= workerRebuildMinBytes) {
394
+ if (workerCoolingDown()) {
395
+ if (caller === "routing") {
396
+ const error = workerFailure!.error;
397
+ const cause = error instanceof Error ? error.message : String(error);
398
+ throw new RequestHistoryIndexBusyError(`request-history index rebuild failed recently: ${cause}`);
399
+ }
400
+ } else {
401
+ const rebuild = startWorkerRebuild(reason, revision);
402
+ if (rebuild) throw new RebuildHandedOff(rebuild);
403
+ }
404
+ }
405
+ fullRebuild(handle, reason);
406
+ }
407
+
408
+ function refreshLockedSync(caller: RefreshCaller): RequestHistoryIndexMeta {
409
+ if (workerRebuild) throw new RebuildHandedOff(workerRebuild);
433
410
  openIndexDb();
434
411
  const state = ensureSchemaAndIdentity(db!);
435
412
  const handle = db!;
436
413
  const revision = sourceIdentity();
437
414
  if (state === "rebuilt") {
438
- fullRebuild(db!, "rebuilt after identity/schema mismatch");
439
- return metaFor(db!);
415
+ rebuildFromLedger(handle, "rebuilt after identity/schema mismatch", caller);
416
+ return metaFor(handle);
440
417
  }
441
418
  const current = readIndexedMeta(handle);
442
419
  if (revision === null) {
@@ -447,31 +424,63 @@ function refreshLockedSync(): RequestHistoryIndexMeta {
447
424
  const tailNextOffset = current.indexedOffset;
448
425
  if (Number(revision.size) < tailNextOffset) {
449
426
  // Truncated source: offsets no longer make sense.
450
- fullRebuild(handle, "source truncated; index rebuilt");
427
+ rebuildFromLedger(handle, "source truncated; index rebuilt", caller);
451
428
  return metaFor(handle);
452
429
  }
453
- if (tailNextOffset < Number(revision.size)) {
454
- const inserted = ingestSourceTail(handle, revision.path, tailNextOffset);
455
- // A clean tail ingest proves the index is healthy: clear any earlier
456
- // rebuild marker so status readers can distinguish rebuilds from tails.
457
- if (inserted > 0) setMeta(handle, HISTORY_META_KEYS.lastError, "");
458
- }
430
+ if (tailNextOffset < Number(revision.size)) {
431
+ const inserted = ingestSourceTail(handle, revision.path, tailNextOffset);
432
+ // A clean tail ingest proves the index is healthy: clear any earlier
433
+ // rebuild marker so status readers can distinguish rebuilds from tails.
434
+ if (inserted > 0) setMeta(handle, HISTORY_META_KEYS.lastError, "");
435
+ }
459
436
  return metaFor(handle);
460
437
  }
461
438
 
462
- /** Synchronous refresh for routing-time evidence reads (RI-06+). */
439
+ /**
440
+ * Synchronous refresh for routing-time evidence reads (RI-06+). Never waits
441
+ * for a worker rebuild: while one runs (this call may start it), throws
442
+ * `RequestHistoryIndexBusyError`, which health scoring reads as unknown evidence.
443
+ */
463
444
  export function openRequestHistoryIndexSync(): RequestHistoryIndexMeta {
464
- return refreshLockedSync();
445
+ try {
446
+ return refreshLockedSync("routing");
447
+ } catch (error) {
448
+ if (error instanceof RebuildHandedOff) throw new RequestHistoryIndexBusyError();
449
+ throw error;
450
+ }
451
+ }
452
+
453
+ async function refreshWaitingForWorker(): Promise<RequestHistoryIndexMeta> {
454
+ const lifetime = indexLifetime;
455
+ for (let builds = 0; ; ) {
456
+ let handedOff: RebuildHandedOff;
457
+ try {
458
+ return refreshLockedSync("reader");
459
+ } catch (error) {
460
+ if (!(error instanceof RebuildHandedOff)) throw error;
461
+ handedOff = error;
462
+ }
463
+ // Only a refresh that needs yet another build hits the limit; one that
464
+ // follows a successful build returns that index above.
465
+ if (builds >= maxAsyncRefreshBuilds) throw new RequestHistoryIndexBusyError();
466
+ let failure: unknown = null;
467
+ await handedOff.rebuild.promise.catch((error: unknown) => { failure = error ?? new Error("rebuild failed"); });
468
+ builds += 1;
469
+ if (lifetime !== indexLifetime) throw new RequestHistoryIndexClosedError();
470
+ if (failure !== null && builds >= maxAsyncRefreshBuilds) throw failure;
471
+ }
465
472
  }
466
473
 
467
474
  /**
468
475
  * Open (and refresh) the index. Single-flight: concurrent callers share one
469
- * refresh. Never throws for missing/corrupt index or ledger state; those are
470
- * repaired or reflected in the returned meta.
476
+ * refresh, and wait for a worker rebuild without blocking the event loop.
477
+ * Missing/corrupt index or ledger state is repaired or reflected in the
478
+ * returned meta; a close during a rebuild rejects with
479
+ * `RequestHistoryIndexClosedError`.
471
480
  */
472
481
  export function openRequestHistoryIndex(): Promise<RequestHistoryIndexMeta> {
473
482
  if (!openPromise) {
474
- openPromise = Promise.resolve(refreshLockedSync()).finally(() => {
483
+ openPromise = refreshWaitingForWorker().finally(() => {
475
484
  openPromise = null;
476
485
  });
477
486
  }
@@ -479,6 +488,12 @@ export function openRequestHistoryIndex(): Promise<RequestHistoryIndexMeta> {
479
488
  }
480
489
 
481
490
  export function closeRequestHistoryIndex(): void {
491
+ indexLifetime += 1;
492
+ const rebuild = workerRebuild;
493
+ workerRebuild = null;
494
+ workerFailure = null;
495
+ // The rebuild's own settlement removes its build files once the thread exits.
496
+ if (rebuild) void rebuild.job.dispose();
482
497
  if (db) {
483
498
  try { db.close(); } catch { /* ignore */ }
484
499
  db = null;
@@ -488,7 +503,25 @@ export function closeRequestHistoryIndex(): void {
488
503
 
489
504
  /** Force a full rebuild from the canonical ledger (CLI / tests). */
490
505
  export async function rebuildRequestHistoryIndex(): Promise<RequestHistoryIndexMeta> {
506
+ while (workerRebuild) await workerRebuild.promise.catch(() => undefined);
491
507
  const handle = openIndexDb();
508
+ const revision = sourceIdentity();
509
+ if (revision !== null && Number(revision.size) >= workerRebuildMinBytes) {
510
+ workerFailure = null;
511
+ const rebuild = startWorkerRebuild("manual rebuild requested", revision);
512
+ if (rebuild) {
513
+ const lifetime = indexLifetime;
514
+ try {
515
+ await rebuild.promise;
516
+ return openRequestHistoryIndex();
517
+ } catch (error) {
518
+ // A close is final; any other worker failure falls back to the inline rebuild below.
519
+ if (lifetime !== indexLifetime || error instanceof RequestHistoryIndexClosedError) throw error;
520
+ }
521
+ fullRebuild(openIndexDb(), "manual rebuild requested");
522
+ return metaFor(db!);
523
+ }
524
+ }
492
525
  fullRebuild(handle, "manual rebuild requested");
493
526
  return metaFor(handle);
494
527
  }