@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,776 @@
1
+ import { createHash } from "node:crypto";
2
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readSync, unlinkSync, writeSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { zstdDecompressSync } from "node:zlib";
5
+ import { Database } from "bun:sqlite";
6
+ import { CODEX_HOME } from "./paths";
7
+ import { atomicWriteFile, getConfigDir } from "../config";
8
+
9
+ /**
10
+ * Cap for decompressing a lone `.jsonl.zst` rollout during quarantine restore.
11
+ * Bounds peak memory while reconstructing thread rows; never write the decoded
12
+ * JSONL to disk.
13
+ */
14
+ export const MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
15
+
16
+ const STATE_DB_PATH = join(CODEX_HOME, "state_5.sqlite");
17
+ function historyBackupPathFor(stateDbPath: string): string {
18
+ const normalized = process.platform === "win32" ? resolve(stateDbPath).toLowerCase() : resolve(stateDbPath);
19
+ const id = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
20
+ return join(getConfigDir(), `codex-history-backup-${id}.json`);
21
+ }
22
+ const HISTORY_BACKUP_PATH = historyBackupPathFor(STATE_DB_PATH);
23
+ const RESUMABLE_SOURCES = ["cli", "vscode"] as const;
24
+
25
+ /**
26
+ * Open the live `state_5.sqlite` the way the Codex app expects a *secondary* writer to behave:
27
+ * wait on the WAL/file lock instead of failing instantly, so we never race the app's own
28
+ * connection pool into a half-applied checkpoint. The app opens this DB with `busy_timeout=5s`
29
+ * (see codex-rs `state::runtime::base_sqlite_options`); we mirror that here.
30
+ */
31
+ let historyDbBusyTimeoutMs = 5000;
32
+
33
+ /**
34
+ * Test-only knob: Windows CI can spend the FULL busy timeout on a transient file lock, which
35
+ * alone exceeds bun's 5s default per-test timeout. Tests shrink this so a busy DB fails fast
36
+ * into withHistoryRetry instead of stalling; production keeps the codex-rs-matching 5s.
37
+ */
38
+ export function setHistoryDbBusyTimeoutForTests(ms: number): void {
39
+ historyDbBusyTimeoutMs = ms;
40
+ }
41
+
42
+ function openStateDb(stateDbPath: string): Database {
43
+ const db = new Database(stateDbPath);
44
+ try {
45
+ db.exec(`PRAGMA busy_timeout = ${historyDbBusyTimeoutMs}`);
46
+ } catch {
47
+ /* best-effort: an older sqlite without busy_timeout still works, just less politely */
48
+ }
49
+ return db;
50
+ }
51
+
52
+ /**
53
+ * Append one JSONL line to a rollout using an O_APPEND handle, exactly like the Codex app's own
54
+ * metadata writer (`append_rollout_item_to_path` in codex-rs `rollout/src/recorder.rs`).
55
+ *
56
+ * Why append instead of rewriting line 1:
57
+ * - The app caches the live session's append handle and only reopens it when the handle is gone
58
+ * (codex-rs `RolloutWriterState::ensure_writer_open`). A temp+rename swap would orphan that
59
+ * handle; an in-place truncate would race the app's concurrent appends and clip new turns.
60
+ * - The app folds metadata by replaying every `session_meta` line in file order, last-writer-wins
61
+ * (codex-rs `apply_session_meta_from_item`), so a trailing `session_meta` overrides earlier ones.
62
+ * Real rollouts already contain multiple `session_meta` lines for this reason.
63
+ * O_APPEND makes each write land at EOF atomically, so it composes safely with the app appending
64
+ * concurrently. We do not touch mtime: a fresh mtime is correct here (the app uses mtime as the
65
+ * rollout's updated_at), and forcing it backwards could hide a real edit from list ordering.
66
+ */
67
+ function appendRolloutLine(path: string, line: string): void {
68
+ const fd = openSync(path, "a");
69
+ try {
70
+ const buf = Buffer.from(line.endsWith("\n") ? line : `${line}\n`, "utf8");
71
+ let offset = 0;
72
+ while (offset < buf.length) {
73
+ offset += writeSync(fd, buf, offset, buf.length - offset, null);
74
+ }
75
+ try { fsyncSync(fd); } catch { /* best-effort durability */ }
76
+ } finally {
77
+ closeSync(fd);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Patch the `model_provider` value inside the FIRST line of a rollout *in place, length-preserving*.
83
+ *
84
+ * Why this exists in addition to {@link appendRolloutLine}: Codex resolves a thread's provider via
85
+ * two different readers. The SQLite replay path folds every `session_meta` line last-writer-wins
86
+ * (covered by appending a trailing meta), but `read_session_meta_line` reads only the FIRST line
87
+ * and `update_thread_metadata` clones it when the app later writes git/memory-mode metadata
88
+ * (codex-rs `thread-store/src/local/update_thread_metadata.rs`). If the first line still says
89
+ * `opencodex` after a native restore, that clone re-appends `opencodex` and last-writer-wins
90
+ * resurrects the routed provider. So a durable restore must also fix line 1.
91
+ *
92
+ * Safety: Codex parses each rollout line as `serde_json::from_str(line.trim())`, which tolerates
93
+ * insignificant JSON whitespace. We therefore replace the provider value and pad the removed bytes
94
+ * with spaces so the line's byte length is unchanged. Equal length means we can write at offset 0
95
+ * with no truncate and no inode swap, so this composes safely with the app's cached append handle.
96
+ * Only length-preserving shrinks are handled (e.g. "opencodex" -> "openai"); callers that would
97
+ * grow the value fall back to append-only, which is correct for the opencodex direction.
98
+ *
99
+ * Returns true when line 1 was patched, false when it could not be done safely (missing file,
100
+ * non-`session_meta` first line, id mismatch, value already correct, or a length-growing change).
101
+ */
102
+ function patchFirstLineProviderInPlace(path: string, expectedId: string, provider: string): boolean {
103
+ if (!existsSync(path)) return false;
104
+ const fd = openSync(path, "r+");
105
+ try {
106
+ // Read the first line by growing the probe until we hit a newline. session_meta lines embed
107
+ // base_instructions and can be tens of KB; a fixed cap would silently skip the in-place patch
108
+ // (and fall back to append-only, re-opening the first-line-clone resurrection gap), so we read
109
+ // until the line actually ends rather than guessing a ceiling.
110
+ const CHUNK = 1 << 16;
111
+ const MAX_FIRST_LINE = 1 << 24; // 16 MiB hard stop so a newline-less/corrupt file can't OOM us.
112
+ let collected = Buffer.alloc(0);
113
+ let nlIndex = -1;
114
+ let pos = 0;
115
+ while (nlIndex === -1) {
116
+ const chunk = Buffer.alloc(CHUNK);
117
+ const read = readSync(fd, chunk, 0, CHUNK, pos);
118
+ if (read === 0) break; // EOF with no newline: single-line file, skip
119
+ collected = Buffer.concat([collected, chunk.subarray(0, read)]);
120
+ nlIndex = collected.indexOf(0x0a);
121
+ pos += read;
122
+ if (collected.length > MAX_FIRST_LINE) return false;
123
+ }
124
+ if (nlIndex === -1) return false; // no newline anywhere: skip
125
+ const firstLine = collected.subarray(0, nlIndex).toString("utf8");
126
+
127
+ const meta = parseSessionMetaLine(firstLine);
128
+ if (!meta) return false;
129
+ if (meta.record.payload.id !== expectedId) return false;
130
+ if (meta.record.payload.model_provider === provider) return false;
131
+
132
+ // Locate the exact `"model_provider":"<value>"` token (allowing whitespace after the colon).
133
+ const match = firstLine.match(/"model_provider"\s*:\s*"([^"\\]*)"/);
134
+ if (!match || match.index === undefined) return false;
135
+ const oldToken = match[0];
136
+ const newCore = `"model_provider":"${provider}"`;
137
+ if (Buffer.byteLength(newCore, "utf8") > Buffer.byteLength(oldToken, "utf8")) return false; // grow: not length-preserving
138
+ const pad = " ".repeat(Buffer.byteLength(oldToken, "utf8") - Buffer.byteLength(newCore, "utf8"));
139
+ const newToken = `${newCore}${pad}`;
140
+
141
+ const patchedLine = firstLine.slice(0, match.index) + newToken + firstLine.slice(match.index + oldToken.length);
142
+ // Length must be identical so the trailing bytes (newline + rest of file) are untouched.
143
+ if (Buffer.byteLength(patchedLine, "utf8") !== Buffer.byteLength(firstLine, "utf8")) return false;
144
+ // Sanity: the patched line must still parse and carry the new provider.
145
+ const reparsed = parseSessionMetaLine(patchedLine);
146
+ if (!reparsed || reparsed.record.payload.model_provider !== provider) return false;
147
+
148
+ const out = Buffer.from(patchedLine, "utf8");
149
+ let offset = 0;
150
+ while (offset < out.length) {
151
+ offset += writeSync(fd, out, offset, out.length - offset, offset);
152
+ }
153
+ try { fsyncSync(fd); } catch { /* best-effort durability */ }
154
+ return true;
155
+ } finally {
156
+ closeSync(fd);
157
+ }
158
+ }
159
+
160
+ type CodexHistoryProvider = "openai" | "opencodex";
161
+
162
+ export interface CodexHistorySyncResult {
163
+ rows: number;
164
+ files: number;
165
+ ejectedRows?: number;
166
+ /** Set when a lock/busy error survived retries and the sync was SKIPPED, not empty. */
167
+ failed?: true;
168
+ }
169
+
170
+ interface ThreadRow {
171
+ id: string;
172
+ rollout_path: string;
173
+ model_provider: string;
174
+ source: string;
175
+ has_user_event: number;
176
+ }
177
+
178
+ interface BackupEntry {
179
+ id: string;
180
+ rolloutPath: string;
181
+ modelProvider: string;
182
+ source: string;
183
+ hasUserEvent: number;
184
+ }
185
+
186
+ interface BackupManifest {
187
+ version: 1;
188
+ stateDbPath?: string;
189
+ entries: Record<string, BackupEntry>;
190
+ }
191
+
192
+ interface NativeRestoreTarget {
193
+ modelProvider: string;
194
+ source: string;
195
+ hasUserEvent: number;
196
+ }
197
+
198
+ function samePath(a: string, b: string): boolean {
199
+ const left = resolve(a);
200
+ const right = resolve(b);
201
+ return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
202
+ }
203
+
204
+ function readBackup(path: string, stateDbPath?: string): BackupManifest {
205
+ if (!existsSync(path)) return { version: 1, stateDbPath, entries: {} };
206
+ try {
207
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<BackupManifest>;
208
+ if (parsed.version !== 1 || !parsed.entries || typeof parsed.entries !== "object") {
209
+ return { version: 1, stateDbPath, entries: {} };
210
+ }
211
+ if (stateDbPath && typeof parsed.stateDbPath === "string" && !samePath(parsed.stateDbPath, stateDbPath)) {
212
+ return { version: 1, stateDbPath, entries: {} };
213
+ }
214
+ return { version: 1, stateDbPath: parsed.stateDbPath ?? stateDbPath, entries: parsed.entries };
215
+ } catch {
216
+ return { version: 1, stateDbPath, entries: {} };
217
+ }
218
+ }
219
+
220
+ function writeBackup(path: string, manifest: BackupManifest, stateDbPath?: string): void {
221
+ if (Object.keys(manifest.entries).length === 0) {
222
+ if (existsSync(path)) unlinkSync(path);
223
+ return;
224
+ }
225
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
226
+ atomicWriteFile(path, JSON.stringify({ ...manifest, stateDbPath: manifest.stateDbPath ?? stateDbPath }, null, 2) + "\n");
227
+ }
228
+
229
+ function rememberOriginal(manifest: BackupManifest, row: ThreadRow): void {
230
+ if (manifest.entries[row.id]) return;
231
+ manifest.entries[row.id] = {
232
+ id: row.id,
233
+ rolloutPath: row.rollout_path,
234
+ modelProvider: row.model_provider,
235
+ source: row.source,
236
+ hasUserEvent: Number(row.has_user_event) || 0,
237
+ };
238
+ }
239
+
240
+ interface ParsedSessionMeta {
241
+ record: { type?: unknown; timestamp?: unknown; payload: { model_provider?: unknown; source?: unknown } & Record<string, unknown> };
242
+ }
243
+
244
+ /** Parse one JSONL line into a `session_meta` record, or null if it isn't one. */
245
+ function parseSessionMetaLine(line: string): ParsedSessionMeta | null {
246
+ let parsed: unknown;
247
+ try {
248
+ parsed = JSON.parse(line);
249
+ } catch {
250
+ return null;
251
+ }
252
+ if (!parsed || typeof parsed !== "object") return null;
253
+ const record = parsed as ParsedSessionMeta["record"];
254
+ if (record.type !== "session_meta" || !record.payload || typeof record.payload !== "object") return null;
255
+ return { record };
256
+ }
257
+
258
+ /**
259
+ * Find the LAST `session_meta` line in a rollout, mirroring the app's last-writer-wins fold
260
+ * (codex-rs `apply_session_meta_from_item`). We base our patch on the most recent metadata so we
261
+ * never resurrect a stale provider that a later app-written `session_meta` already changed.
262
+ */
263
+ export function readLatestSessionMeta(path: string): ParsedSessionMeta | null {
264
+ const raw = readFileSync(path, "utf8");
265
+ const lines = raw.split("\n");
266
+ for (let i = lines.length - 1; i >= 0; i--) {
267
+ const line = lines[i];
268
+ if (!line) continue;
269
+ if (!line.includes("\"session_meta\"")) continue;
270
+ const meta = parseSessionMetaLine(line);
271
+ if (meta) return meta;
272
+ }
273
+ return null;
274
+ }
275
+
276
+ /**
277
+ * Fields needed to re-insert a production-shaped `threads` row from a rollout JSONL when a
278
+ * Phase-2 quarantine predates full `satellite-backup.json` thread snapshots.
279
+ *
280
+ * Uses the same last-writer-wins `session_meta` fold as {@link readLatestSessionMeta}, plus the
281
+ * first user-message preview (codex-rs `list.rs` / `EventMsg::UserMessage` path).
282
+ */
283
+ export interface RolloutThreadFields {
284
+ id: string;
285
+ modelProvider: string;
286
+ source: string;
287
+ firstUserMessage: string;
288
+ hasUserEvent: number;
289
+ cwd?: string;
290
+ historyMode?: string;
291
+ cliVersion?: string;
292
+ }
293
+
294
+ function textFromContentParts(content: unknown): string | null {
295
+ if (typeof content === "string" && content.trim()) return content.trim();
296
+ if (!Array.isArray(content)) return null;
297
+ const parts: string[] = [];
298
+ for (const part of content) {
299
+ if (!part || typeof part !== "object") continue;
300
+ const p = part as Record<string, unknown>;
301
+ if (typeof p.text === "string" && p.text.trim()) parts.push(p.text.trim());
302
+ else if (typeof p.input_text === "string" && p.input_text.trim()) parts.push(p.input_text.trim());
303
+ }
304
+ const joined = parts.join("\n").trim();
305
+ return joined || null;
306
+ }
307
+
308
+ /** Extract the first user-message preview from a rollout line, or null. */
309
+ function extractUserMessagePreview(line: string): string | null {
310
+ let parsed: unknown;
311
+ try {
312
+ parsed = JSON.parse(line);
313
+ } catch {
314
+ return null;
315
+ }
316
+ if (!parsed || typeof parsed !== "object") return null;
317
+ const record = parsed as { type?: unknown; payload?: unknown };
318
+ const payload = record.payload;
319
+ if (!payload || typeof payload !== "object") return null;
320
+ const p = payload as Record<string, unknown>;
321
+
322
+ if (record.type === "event_msg") {
323
+ // codex-rs EventMsg::UserMessage — payload.type is "user_message" (or omitted in fixtures).
324
+ if (p.type === "user_message" || typeof p.message === "string") {
325
+ if (typeof p.message === "string" && p.message.trim()) return p.message.trim();
326
+ const fromContent = textFromContentParts(p.content);
327
+ if (fromContent) return fromContent;
328
+ }
329
+ return null;
330
+ }
331
+
332
+ if (record.type === "response_item") {
333
+ if (p.type === "message" && p.role === "user") {
334
+ return textFromContentParts(p.content);
335
+ }
336
+ }
337
+ return null;
338
+ }
339
+
340
+ /**
341
+ * Reconstruct thread identity + listing fields from a staged/restored rollout JSONL.
342
+ * Returns null when the file is missing or has no parseable `session_meta`.
343
+ *
344
+ * Accepts plain `.jsonl` or a lone `.jsonl.zst` (legacy Phase-2 quarantine). Compressed
345
+ * rollouts are decompressed in memory with {@link MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES};
346
+ * no decompressed copy is written to disk.
347
+ */
348
+ export function readThreadFieldsFromRollout(path: string): RolloutThreadFields | null {
349
+ if (!path || !existsSync(path)) return null;
350
+ let raw: string;
351
+ try {
352
+ raw = path.endsWith(".zst")
353
+ ? decompressRolloutZstUtf8(path)
354
+ : readFileSync(path, "utf8");
355
+ } catch {
356
+ return null;
357
+ }
358
+ return parseThreadFieldsFromRolloutText(raw);
359
+ }
360
+
361
+ function decompressRolloutZstUtf8(
362
+ path: string,
363
+ maxBytes: number = MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES,
364
+ ): string {
365
+ const compressed = readFileSync(path);
366
+ const decoded = zstdDecompressSync(compressed as Uint8Array<ArrayBuffer>, {
367
+ maxOutputLength: maxBytes,
368
+ });
369
+ if (decoded.byteLength > maxBytes) {
370
+ throw new Error("rollout_zst_too_large");
371
+ }
372
+ return new TextDecoder().decode(decoded);
373
+ }
374
+
375
+ function parseThreadFieldsFromRolloutText(raw: string): RolloutThreadFields | null {
376
+ const lines = raw.split("\n");
377
+ let latest: ParsedSessionMeta | null = null;
378
+ let firstUserMessage = "";
379
+ for (const line of lines) {
380
+ if (!line) continue;
381
+ if (line.includes("\"session_meta\"")) {
382
+ const meta = parseSessionMetaLine(line);
383
+ if (meta) latest = meta;
384
+ }
385
+ if (!firstUserMessage) {
386
+ const preview = extractUserMessagePreview(line);
387
+ if (preview) firstUserMessage = preview;
388
+ }
389
+ }
390
+ if (!latest) return null;
391
+ const payload = latest.record.payload;
392
+ const id = typeof payload.id === "string" ? payload.id : "";
393
+ if (!id) return null;
394
+ const modelProvider = typeof payload.model_provider === "string" && payload.model_provider
395
+ ? payload.model_provider
396
+ : "openai";
397
+ const source = typeof payload.source === "string" && payload.source
398
+ ? payload.source
399
+ : "cli";
400
+ return {
401
+ id,
402
+ modelProvider,
403
+ source,
404
+ firstUserMessage,
405
+ hasUserEvent: firstUserMessage.trim() ? 1 : 0,
406
+ ...(typeof payload.cwd === "string" ? { cwd: payload.cwd } : {}),
407
+ ...(typeof payload.history_mode === "string" ? { historyMode: payload.history_mode } : {}),
408
+ ...(typeof payload.cli_version === "string" ? { cliVersion: payload.cli_version } : {}),
409
+ };
410
+ }
411
+
412
+ /**
413
+ * Make a thread's rollout reflect a provider/source change by APPENDING a new `session_meta` line,
414
+ * rather than rewriting line 1. The appended line clones the latest metadata payload (so no field
415
+ * is accidentally reset to empty) and applies only the requested changes. Returns false when the
416
+ * rollout is missing, has no parseable `session_meta`, its latest `session_meta` belongs to a
417
+ * different thread id, or it already matches the desired values.
418
+ */
419
+ function updateSessionMeta(path: string, expectedId: string, patch: { provider?: string; source?: string }): boolean {
420
+ if (!path || !existsSync(path)) return false;
421
+
422
+ const latest = readLatestSessionMeta(path);
423
+ if (!latest) return false;
424
+ const record = latest.record;
425
+
426
+ // The app ignores `session_meta` lines whose payload id != the canonical thread id
427
+ // (codex-rs `apply_session_meta_from_item`). Forked rollouts can embed a source session's
428
+ // metadata, so an id-mismatched latest line means we'd be cloning the wrong thread's meta and
429
+ // appending a line the app would discard. Skip rather than write a no-op/misleading line.
430
+ const payloadId = record.payload.id;
431
+ if (typeof payloadId !== "string" || payloadId !== expectedId) return false;
432
+
433
+ let changed = false;
434
+ if (patch.provider !== undefined && record.payload.model_provider !== patch.provider) {
435
+ record.payload.model_provider = patch.provider;
436
+ changed = true;
437
+ }
438
+ if (patch.source !== undefined && record.payload.source !== patch.source) {
439
+ record.payload.source = patch.source;
440
+ changed = true;
441
+ }
442
+ if (!changed) return false;
443
+
444
+ // Cover Codex's *other* provider reader: `read_session_meta_line` reads only line 1, and the
445
+ // app clones it when writing later git/memory-mode metadata. Appending alone leaves a stale
446
+ // line-1 provider that the clone would re-append, so for a length-preserving provider change we
447
+ // also patch line 1 in place (no inode swap, no truncate). Best-effort: when it can't be done
448
+ // safely (e.g. a length-growing change), the trailing append below is still correct for the
449
+ // SQLite replay path.
450
+ if (patch.provider !== undefined) {
451
+ try { patchFirstLineProviderInPlace(path, expectedId, patch.provider); } catch { /* best-effort line-1 patch */ }
452
+ }
453
+
454
+ // Refresh the line timestamp so the appended record reads as the newest metadata.
455
+ record.timestamp = new Date().toISOString();
456
+ appendRolloutLine(path, JSON.stringify(record));
457
+ return true;
458
+ }
459
+
460
+ function toNativeRestoreTarget(entry: BackupEntry): NativeRestoreTarget {
461
+ if (entry.modelProvider !== "opencodex") {
462
+ return {
463
+ modelProvider: entry.modelProvider,
464
+ source: entry.source,
465
+ hasUserEvent: entry.hasUserEvent,
466
+ };
467
+ }
468
+ return {
469
+ modelProvider: "openai",
470
+ source: entry.source === "exec" ? "cli" : entry.source,
471
+ hasUserEvent: 1,
472
+ };
473
+ }
474
+
475
+ function ejectRemainingOpencodexHistory(db: Database): { rows: number; files: number } {
476
+ const rows = db
477
+ .query<ThreadRow, []>(`
478
+ SELECT id, rollout_path, model_provider, source, has_user_event
479
+ FROM threads
480
+ WHERE model_provider = 'opencodex'
481
+ AND trim(coalesce(first_user_message, '')) != ''
482
+ `)
483
+ .all();
484
+
485
+ let files = 0;
486
+ for (const row of rows) {
487
+ try {
488
+ if (updateSessionMeta(row.rollout_path, row.id, {
489
+ provider: "openai",
490
+ source: row.source === "exec" ? "cli" : undefined,
491
+ })) files++;
492
+ } catch {
493
+ /* native restore should continue even if an old rollout is missing */
494
+ }
495
+ }
496
+
497
+ const restore = db.transaction(() => {
498
+ const update = db.query(`
499
+ UPDATE threads
500
+ SET model_provider = 'openai',
501
+ source = CASE WHEN source = 'exec' THEN 'cli' ELSE source END,
502
+ has_user_event = 1
503
+ WHERE id = ?
504
+ `);
505
+ for (const row of rows) update.run(row.id);
506
+ });
507
+ restore();
508
+ return { rows: rows.length, files };
509
+ }
510
+
511
+ export function isRecoverableHistoryError(error: unknown): boolean {
512
+ const code = typeof error === "object" && error && "code" in error ? String((error as { code?: unknown }).code) : "";
513
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
514
+ return code === "SQLITE_BUSY"
515
+ || code === "SQLITE_LOCKED"
516
+ || code === "EBUSY"
517
+ || code === "EPERM"
518
+ || code === "EACCES"
519
+ || message.includes("database is locked")
520
+ || message.includes("database is busy")
521
+ || message.includes("resource busy")
522
+ || message.includes("operation not permitted")
523
+ || message.includes("permission denied");
524
+ }
525
+
526
+ const HISTORY_RETRY_DELAY_MS = 500;
527
+ const HISTORY_RETRY_ATTEMPTS = 2;
528
+
529
+ /**
530
+ * Run a history mutation with one retry across recoverable lock/busy errors (the app's own
531
+ * connection holds `state_5.sqlite` in WAL with a 5 s busy_timeout; a transient writer
532
+ * usually clears within that window). Returns null when both attempts hit a recoverable
533
+ * error — callers surface that as `failed: true` instead of a silent no-op. Hard errors
534
+ * (corruption, programming bugs) still throw.
535
+ */
536
+ export function withHistoryRetry<T>(fn: () => T, io: { sleepFn?: (ms: number) => void; attempts?: number; delayMs?: number } = {}): T | null {
537
+ const sleepFn = io.sleepFn ?? Bun.sleepSync;
538
+ const attempts = Math.max(1, io.attempts ?? HISTORY_RETRY_ATTEMPTS);
539
+ const delayMs = io.delayMs ?? HISTORY_RETRY_DELAY_MS;
540
+ for (let attempt = 0; ; attempt++) {
541
+ try {
542
+ return fn();
543
+ } catch (error) {
544
+ if (!isRecoverableHistoryError(error)) throw error;
545
+ if (attempt >= attempts - 1) return null;
546
+ try { sleepFn(delayMs); } catch { /* sleep is best-effort */ }
547
+ }
548
+ }
549
+ }
550
+
551
+ /**
552
+ * True when a READONLY probe proves the openai-direction restore would be a no-op:
553
+ * zero threads still tagged opencodex AND an empty backup manifest. Used to skip the
554
+ * write-open entirely in the Design B steady state — on Windows the Codex app holds
555
+ * `state_5.sqlite` (WAL, busy_timeout 5s), so an unnecessary write open can stall for
556
+ * seconds and surface a false lock warning, while WAL always admits readers. A failed
557
+ * probe (locked even for readers / schema drift) returns false so callers fall through
558
+ * to the write attempt and keep today's behavior for genuinely unknown state.
559
+ */
560
+ function openaiRestoreIsNoop(stateDbPath: string, backupPath: string): boolean {
561
+ const pending = countPendingOpencodexHistory(stateDbPath, backupPath);
562
+ return !pending.failed && pending.pendingRows === 0 && pending.backupEntries === 0;
563
+ }
564
+
565
+ export function syncCodexHistoryProvider(
566
+ provider: CodexHistoryProvider,
567
+ stateDbPath = STATE_DB_PATH,
568
+ backupPath = HISTORY_BACKUP_PATH,
569
+ opts: { skipWhenProvablyNoop?: boolean } = {},
570
+ ): CodexHistorySyncResult {
571
+ // Opt-in steady-state gate (Design B loopback callers only): default semantics of
572
+ // this exported API are unchanged — legacy stop/restore paths never pass the flag.
573
+ if (opts.skipWhenProvablyNoop && provider === "openai" && existsSync(stateDbPath)
574
+ && openaiRestoreIsNoop(stateDbPath, backupPath)) {
575
+ return { rows: 0, files: 0 };
576
+ }
577
+ return withHistoryRetry(() => syncCodexHistoryProviderUnsafe(provider, stateDbPath, backupPath))
578
+ ?? { rows: 0, files: 0, failed: true };
579
+ }
580
+
581
+ function syncCodexHistoryProviderUnsafe(provider: CodexHistoryProvider, stateDbPath: string, backupPath: string): CodexHistorySyncResult {
582
+ if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
583
+ if (provider === "openai") return restoreCodexHistoryProvider(stateDbPath, backupPath);
584
+
585
+ const db = openStateDb(stateDbPath);
586
+ try {
587
+ const placeholders = RESUMABLE_SOURCES.map(() => "?").join(",");
588
+ const openaiRows = db
589
+ .query<ThreadRow, string[]>(`
590
+ SELECT id, rollout_path, model_provider, source, has_user_event
591
+ FROM threads
592
+ WHERE model_provider = 'openai'
593
+ AND source IN (${placeholders})
594
+ `)
595
+ .all(...RESUMABLE_SOURCES);
596
+ const execRows = db
597
+ .query<ThreadRow, []>(`
598
+ SELECT id, rollout_path, model_provider, source, has_user_event
599
+ FROM threads
600
+ WHERE model_provider = 'opencodex'
601
+ AND source = 'exec'
602
+ AND trim(coalesce(first_user_message, '')) != ''
603
+ `)
604
+ .all();
605
+
606
+ const manifest = readBackup(backupPath, stateDbPath);
607
+ for (const row of [...openaiRows, ...execRows]) rememberOriginal(manifest, row);
608
+ writeBackup(backupPath, manifest, stateDbPath);
609
+
610
+ let files = 0;
611
+ for (const row of openaiRows) {
612
+ try {
613
+ if (updateSessionMeta(row.rollout_path, row.id, { provider: "opencodex" })) files++;
614
+ } catch {
615
+ /* best-effort; keep DB migration moving even if one old rollout is malformed */
616
+ }
617
+ }
618
+ for (const row of execRows) {
619
+ try {
620
+ if (updateSessionMeta(row.rollout_path, row.id, { source: "cli" })) files++;
621
+ } catch {
622
+ /* best-effort; keep DB migration moving even if one old rollout is malformed */
623
+ }
624
+ }
625
+
626
+ const update = db.transaction(() => {
627
+ const markUserEvent = db.query(`
628
+ UPDATE threads
629
+ SET has_user_event = 1
630
+ WHERE id = ?
631
+ AND trim(coalesce(first_user_message, '')) != ''
632
+ `);
633
+ for (const row of [...openaiRows, ...execRows]) markUserEvent.run(row.id);
634
+ db.query(`
635
+ UPDATE threads
636
+ SET model_provider = 'opencodex'
637
+ WHERE model_provider = 'openai'
638
+ AND source IN (${placeholders})
639
+ `).run(...RESUMABLE_SOURCES);
640
+ db.query(`
641
+ UPDATE threads
642
+ SET source = 'cli'
643
+ WHERE model_provider = 'opencodex'
644
+ AND source = 'exec'
645
+ AND trim(coalesce(first_user_message, '')) != ''
646
+ `).run();
647
+ });
648
+ update();
649
+
650
+ return { rows: openaiRows.length + execRows.length, files };
651
+ } finally {
652
+ db.close();
653
+ }
654
+ }
655
+
656
+ function restoreCodexHistoryProvider(stateDbPath: string, backupPath: string): CodexHistorySyncResult {
657
+ const manifest = readBackup(backupPath, stateDbPath);
658
+ const entries = Object.values(manifest.entries);
659
+
660
+ const db = openStateDb(stateDbPath);
661
+ try {
662
+ if (entries.length === 0) {
663
+ const ejected = ejectRemainingOpencodexHistory(db);
664
+ return ejected.rows > 0 ? { rows: 0, files: ejected.files, ejectedRows: ejected.rows } : { rows: 0, files: 0 };
665
+ }
666
+
667
+ let files = 0;
668
+ for (const entry of entries) {
669
+ const target = toNativeRestoreTarget(entry);
670
+ try {
671
+ if (updateSessionMeta(entry.rolloutPath, entry.id, { provider: target.modelProvider, source: target.source })) files++;
672
+ } catch {
673
+ /* best-effort; keep DB restore moving even if one rollout disappeared */
674
+ }
675
+ }
676
+
677
+ const restore = db.transaction(() => {
678
+ const update = db.query(`
679
+ UPDATE threads
680
+ SET model_provider = ?,
681
+ source = ?,
682
+ has_user_event = ?
683
+ WHERE id = ?
684
+ `);
685
+ for (const entry of entries) {
686
+ const target = toNativeRestoreTarget(entry);
687
+ update.run(target.modelProvider, target.source, target.hasUserEvent, entry.id);
688
+ }
689
+ });
690
+ restore();
691
+ writeBackup(backupPath, { version: 1, stateDbPath, entries: {} }, stateDbPath);
692
+ const ejected = ejectRemainingOpencodexHistory(db);
693
+ return ejected.rows > 0
694
+ ? { rows: entries.length, files: files + ejected.files, ejectedRows: ejected.rows }
695
+ : { rows: entries.length, files };
696
+ } finally {
697
+ db.close();
698
+ }
699
+ }
700
+
701
+ export function restoreLegacyOpenaiHistory(stateDbPath = STATE_DB_PATH): { rows: number; files: number; failed?: true } {
702
+ if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
703
+ return withHistoryRetry(() => {
704
+ const db = openStateDb(stateDbPath);
705
+ try {
706
+ return ejectRemainingOpencodexHistory(db);
707
+ } finally {
708
+ db.close();
709
+ }
710
+ }) ?? { rows: 0, files: 0, failed: true };
711
+ }
712
+
713
+ /**
714
+ * One-time Design-B migration: restore backed-up originals, then eject any remaining
715
+ * opencodex-tagged threads to openai. Thin wrapper over the restore path with a
716
+ * configurable retry budget — the daemon migration guardian uses `{ attempts: 1 }`
717
+ * per tick so a locked DB never stalls the event loop beyond one sqlite busy wait.
718
+ */
719
+ export function migrateHistoryToOpenai(
720
+ stateDbPath = STATE_DB_PATH,
721
+ backupPath = HISTORY_BACKUP_PATH,
722
+ opts: { attempts?: number; delayMs?: number; sleepFn?: (ms: number) => void } = {},
723
+ ): CodexHistorySyncResult {
724
+ if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
725
+ // Steady-state gate: this migration is Design-B-specific (inject + guardian callers),
726
+ // and after the one-time migration every start would otherwise write-open the DB for
727
+ // nothing. A missing DB with a leftover backup manifest does NOT satisfy the gate
728
+ // (backupEntries > 0), so the guardian's fresh-reinstall re-count protection holds.
729
+ if (openaiRestoreIsNoop(stateDbPath, backupPath)) return { rows: 0, files: 0 };
730
+ return withHistoryRetry(() => syncCodexHistoryProviderUnsafe("openai", stateDbPath, backupPath), opts)
731
+ ?? { rows: 0, files: 0, failed: true };
732
+ }
733
+
734
+ export interface PendingHistoryCount {
735
+ /** Threads still tagged opencodex that the eject path WOULD move (mirrors its WHERE). */
736
+ pendingRows: number;
737
+ /** Entries still recorded in the backup manifest (restore targets). */
738
+ backupEntries: number;
739
+ /** Set when the DB could not be opened/read (locked); counts are then unknown, not zero. */
740
+ failed?: true;
741
+ }
742
+
743
+ /**
744
+ * Read-only migration progress probe for the guardian and `ocx doctor`. Opens sqlite
745
+ * readonly with a SHORT busy timeout so a locked DB cannot stall a daemon tick. The
746
+ * pending predicate mirrors ejectRemainingOpencodexHistory exactly — rows eject ignores
747
+ * (empty first_user_message) are not counted, so 0 really means "migration done".
748
+ */
749
+ export function countPendingOpencodexHistory(stateDbPath = STATE_DB_PATH, backupPath = HISTORY_BACKUP_PATH): PendingHistoryCount {
750
+ let backupEntries = 0;
751
+ try {
752
+ const manifest = readBackup(backupPath, stateDbPath);
753
+ backupEntries = Object.keys(manifest.entries).length;
754
+ } catch { /* unreadable manifest counts as 0 — restore treats it the same way */ }
755
+
756
+ if (!existsSync(stateDbPath)) return { pendingRows: 0, backupEntries };
757
+ try {
758
+ const db = new Database(stateDbPath, { readonly: true });
759
+ try {
760
+ db.exec("PRAGMA busy_timeout = 100");
761
+ const row = db.query<{ n: number }, []>(`
762
+ SELECT count(*) AS n
763
+ FROM threads
764
+ WHERE model_provider = 'opencodex'
765
+ AND trim(coalesce(first_user_message, '')) != ''
766
+ `).get();
767
+ return { pendingRows: row?.n ?? 0, backupEntries };
768
+ } finally {
769
+ db.close();
770
+ }
771
+ } catch (error) {
772
+ if (isRecoverableHistoryError(error)) return { pendingRows: 0, backupEntries, failed: true };
773
+ // Schema drift (e.g. a future codex renames the table) is a "cannot know" too, not a crash.
774
+ return { pendingRows: 0, backupEntries, failed: true };
775
+ }
776
+ }