@remodex/rmx 1.0.2

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 (588) hide show
  1. package/AGENTS_INSTALL.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +242 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +584 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-CZqebSPQ.css +1 -0
  11. package/gui/dist/assets/index-CkETtt7P.js +71 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/fonts/google-sans-cyrillic.woff2 +0 -0
  14. package/gui/dist/fonts/google-sans-latin.woff2 +0 -0
  15. package/gui/dist/icons.svg +24 -0
  16. package/gui/dist/index.html +25 -0
  17. package/gui/dist/logo.png +0 -0
  18. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  19. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  20. package/gui/dist/provider-icons/claude-color.svg +1 -0
  21. package/gui/dist/provider-icons/cline-color.svg +16 -0
  22. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  23. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  24. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  25. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  26. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  27. package/gui/dist/provider-icons/discord.svg +1 -0
  28. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  29. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini-color.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.svg +1 -0
  34. package/gui/dist/provider-icons/groq-color.svg +1 -0
  35. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  36. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  37. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  38. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  39. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  40. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  41. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  42. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  43. package/gui/dist/provider-icons/openai.svg +1 -0
  44. package/gui/dist/provider-icons/opencode.svg +2 -0
  45. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  46. package/gui/dist/provider-icons/pi.svg +21 -0
  47. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  48. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  49. package/gui/dist/provider-icons/telegram.svg +1 -0
  50. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  51. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  52. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  53. package/package.json +118 -0
  54. package/src/AGENTS.md +28 -0
  55. package/src/adapters/anthropic-image-guard.ts +251 -0
  56. package/src/adapters/anthropic-image-normalize.ts +518 -0
  57. package/src/adapters/anthropic.ts +1205 -0
  58. package/src/adapters/azure.ts +36 -0
  59. package/src/adapters/base.ts +83 -0
  60. package/src/adapters/client-fingerprint.ts +59 -0
  61. package/src/adapters/command-code.ts +453 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/cursor-errors.ts +165 -0
  65. package/src/adapters/cursor/discovery.ts +276 -0
  66. package/src/adapters/cursor/effort-map.ts +139 -0
  67. package/src/adapters/cursor/exec-policy.ts +88 -0
  68. package/src/adapters/cursor/framing.ts +250 -0
  69. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  70. package/src/adapters/cursor/kv-store.ts +52 -0
  71. package/src/adapters/cursor/live-models.ts +153 -0
  72. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  73. package/src/adapters/cursor/live-transport.ts +1235 -0
  74. package/src/adapters/cursor/mcp-config.ts +42 -0
  75. package/src/adapters/cursor/mcp-manager.ts +333 -0
  76. package/src/adapters/cursor/message-mapper.ts +49 -0
  77. package/src/adapters/cursor/native-exec-common.ts +59 -0
  78. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  79. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  80. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  81. package/src/adapters/cursor/native-exec-network.ts +43 -0
  82. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  83. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  84. package/src/adapters/cursor/native-exec.ts +604 -0
  85. package/src/adapters/cursor/protobuf-events.ts +735 -0
  86. package/src/adapters/cursor/protobuf-request.ts +719 -0
  87. package/src/adapters/cursor/request-builder.ts +280 -0
  88. package/src/adapters/cursor/thread-continuity.ts +67 -0
  89. package/src/adapters/cursor/tool-definitions.ts +621 -0
  90. package/src/adapters/cursor/transport-retry.ts +132 -0
  91. package/src/adapters/cursor/transport.ts +57 -0
  92. package/src/adapters/cursor/types.ts +59 -0
  93. package/src/adapters/cursor.ts +196 -0
  94. package/src/adapters/google-antigravity-replay.ts +520 -0
  95. package/src/adapters/google-antigravity-wire.ts +140 -0
  96. package/src/adapters/google-errors.ts +85 -0
  97. package/src/adapters/google-http.ts +100 -0
  98. package/src/adapters/google-tool-schema.ts +173 -0
  99. package/src/adapters/google-truncation.ts +24 -0
  100. package/src/adapters/google-wire-compiler.ts +232 -0
  101. package/src/adapters/google.ts +859 -0
  102. package/src/adapters/identity.ts +77 -0
  103. package/src/adapters/image.ts +23 -0
  104. package/src/adapters/kiro-constants.ts +16 -0
  105. package/src/adapters/kiro-errors.ts +208 -0
  106. package/src/adapters/kiro-events.ts +197 -0
  107. package/src/adapters/kiro-images.ts +129 -0
  108. package/src/adapters/kiro-retry.ts +312 -0
  109. package/src/adapters/kiro-thinking.ts +104 -0
  110. package/src/adapters/kiro-tool-fallback.ts +36 -0
  111. package/src/adapters/kiro-tools.ts +224 -0
  112. package/src/adapters/kiro-truncation.ts +33 -0
  113. package/src/adapters/kiro-wire.ts +129 -0
  114. package/src/adapters/kiro.ts +1924 -0
  115. package/src/adapters/mimo-free.ts +263 -0
  116. package/src/adapters/openai-chat.ts +1265 -0
  117. package/src/adapters/openai-responses.ts +1309 -0
  118. package/src/adapters/run-turn-queue.ts +114 -0
  119. package/src/adapters/tool-catalog-nudge.ts +71 -0
  120. package/src/adapters/upstream-http-error.ts +48 -0
  121. package/src/android-remote/assets.ts +218 -0
  122. package/src/android-remote/attachments.ts +168 -0
  123. package/src/android-remote/auth.ts +237 -0
  124. package/src/android-remote/cloudflare-provisioning.ts +409 -0
  125. package/src/android-remote/cloudflare-secret.ts +106 -0
  126. package/src/android-remote/cloudflare-tunnel.ts +488 -0
  127. package/src/android-remote/cloudflared.ts +286 -0
  128. package/src/android-remote/codex-app-server.ts +565 -0
  129. package/src/android-remote/desktop-history-page.ts +936 -0
  130. package/src/android-remote/desktop-ipc.ts +3643 -0
  131. package/src/android-remote/desktop-ownership-store.ts +98 -0
  132. package/src/android-remote/desktop-project-registration.ts +129 -0
  133. package/src/android-remote/desktop-session-stream.ts +1110 -0
  134. package/src/android-remote/desktop-workspace-state.ts +443 -0
  135. package/src/android-remote/file-change-parser.ts +112 -0
  136. package/src/android-remote/gateway.ts +9108 -0
  137. package/src/android-remote/mutation-store.ts +299 -0
  138. package/src/android-remote/projection.ts +1780 -0
  139. package/src/android-remote/queued-turn-store.ts +248 -0
  140. package/src/android-remote/session-command-recovery.ts +1384 -0
  141. package/src/android-remote/store.ts +466 -0
  142. package/src/android-remote/thread-reconciliation.ts +133 -0
  143. package/src/android-remote/thread-source-paths.ts +307 -0
  144. package/src/android-remote/thread-stream.ts +546 -0
  145. package/src/android-remote/turn-activity.ts +235 -0
  146. package/src/android-remote/user-message-identity.ts +94 -0
  147. package/src/bridge.ts +1793 -0
  148. package/src/chat/inbound.ts +295 -0
  149. package/src/chat/outbound.ts +821 -0
  150. package/src/claude/agents-inject.ts +267 -0
  151. package/src/claude/alias.ts +149 -0
  152. package/src/claude/auth-detect.ts +229 -0
  153. package/src/claude/auth-mode-migration.ts +32 -0
  154. package/src/claude/auth-mode.ts +62 -0
  155. package/src/claude/context-windows.ts +189 -0
  156. package/src/claude/desktop-3p-guard.ts +35 -0
  157. package/src/claude/desktop-3p-paths.ts +84 -0
  158. package/src/claude/desktop-3p.ts +601 -0
  159. package/src/claude/desktop-health.ts +26 -0
  160. package/src/claude/desktop-profile.ts +263 -0
  161. package/src/claude/gateway-cache.ts +70 -0
  162. package/src/claude/inbound-debug.ts +163 -0
  163. package/src/claude/inbound.ts +519 -0
  164. package/src/claude/model-info.ts +154 -0
  165. package/src/claude/outbound.ts +898 -0
  166. package/src/cli/access.ts +108 -0
  167. package/src/cli/account-api.ts +296 -0
  168. package/src/cli/account-auth.ts +250 -0
  169. package/src/cli/account-catalog-refresh.ts +14 -0
  170. package/src/cli/account-extended.ts +476 -0
  171. package/src/cli/account-main.ts +317 -0
  172. package/src/cli/account.ts +297 -0
  173. package/src/cli/agent-driven.ts +70 -0
  174. package/src/cli/agent.ts +184 -0
  175. package/src/cli/catalog-prewarm.ts +27 -0
  176. package/src/cli/claude-desktop.ts +211 -0
  177. package/src/cli/claude.ts +302 -0
  178. package/src/cli/codex-shim-autorestore.ts +45 -0
  179. package/src/cli/codex-shim-readiness.ts +69 -0
  180. package/src/cli/combo.ts +124 -0
  181. package/src/cli/config-command.ts +183 -0
  182. package/src/cli/debug.ts +228 -0
  183. package/src/cli/desktop-first-run.ts +25 -0
  184. package/src/cli/doctor.ts +1022 -0
  185. package/src/cli/export-command.ts +201 -0
  186. package/src/cli/help.ts +370 -0
  187. package/src/cli/index.ts +1565 -0
  188. package/src/cli/init.ts +224 -0
  189. package/src/cli/integrations.ts +225 -0
  190. package/src/cli/interactive-confirm.ts +133 -0
  191. package/src/cli/internal-dispatch.ts +35 -0
  192. package/src/cli/launcher-context.ts +77 -0
  193. package/src/cli/models-runtime.ts +224 -0
  194. package/src/cli/models.ts +340 -0
  195. package/src/cli/observe.ts +170 -0
  196. package/src/cli/opencode.ts +587 -0
  197. package/src/cli/provider-runtime.ts +179 -0
  198. package/src/cli/provider.ts +476 -0
  199. package/src/cli/ready.ts +301 -0
  200. package/src/cli/route-policy.ts +92 -0
  201. package/src/cli/runtime-api.ts +328 -0
  202. package/src/cli/star-prompt.ts +211 -0
  203. package/src/cli/status-oauth.ts +78 -0
  204. package/src/cli/status.ts +321 -0
  205. package/src/cli/system-command.ts +196 -0
  206. package/src/cli/system-restart-client.ts +146 -0
  207. package/src/cli/tray-proxy.ts +205 -0
  208. package/src/cli/v2.ts +200 -0
  209. package/src/cli.ts +10 -0
  210. package/src/clients/config-export.ts +1109 -0
  211. package/src/codex/account-id.ts +34 -0
  212. package/src/codex/account-label.ts +34 -0
  213. package/src/codex/account-lifecycle.ts +172 -0
  214. package/src/codex/account-namespace-match.ts +63 -0
  215. package/src/codex/account-namespaces.ts +195 -0
  216. package/src/codex/account-pause.ts +20 -0
  217. package/src/codex/account-priority.ts +83 -0
  218. package/src/codex/account-runtime-state.ts +31 -0
  219. package/src/codex/account-store.ts +517 -0
  220. package/src/codex/account-usability.ts +40 -0
  221. package/src/codex/admission.ts +263 -0
  222. package/src/codex/app-server-processes.ts +799 -0
  223. package/src/codex/auth-api.ts +2098 -0
  224. package/src/codex/auth-collision.ts +107 -0
  225. package/src/codex/auth-context.ts +480 -0
  226. package/src/codex/autostart-health.ts +156 -0
  227. package/src/codex/catalog/account-models.ts +67 -0
  228. package/src/codex/catalog/aggregation.ts +471 -0
  229. package/src/codex/catalog/bundled.ts +533 -0
  230. package/src/codex/catalog/effort.ts +432 -0
  231. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  232. package/src/codex/catalog/kinds.ts +2 -0
  233. package/src/codex/catalog/metadata.ts +287 -0
  234. package/src/codex/catalog/native-models.ts +7 -0
  235. package/src/codex/catalog/parsing.ts +503 -0
  236. package/src/codex/catalog/provider-fetch.ts +2267 -0
  237. package/src/codex/catalog/sync.ts +1606 -0
  238. package/src/codex/catalog-admission.ts +197 -0
  239. package/src/codex/catalog-refresh-status.ts +87 -0
  240. package/src/codex/catalog-write-serialization.ts +242 -0
  241. package/src/codex/catalog.ts +15 -0
  242. package/src/codex/codex-write-lock.ts +384 -0
  243. package/src/codex/convergence-types.ts +593 -0
  244. package/src/codex/convergence.ts +580 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/desktop-client-processes.ts +521 -0
  249. package/src/codex/exec-invocation.ts +22 -0
  250. package/src/codex/features.ts +1091 -0
  251. package/src/codex/generation.ts +202 -0
  252. package/src/codex/history-job.ts +347 -0
  253. package/src/codex/history-lock.ts +242 -0
  254. package/src/codex/history-migration-guardian.ts +115 -0
  255. package/src/codex/history-provider.ts +1075 -0
  256. package/src/codex/history-transition.ts +105 -0
  257. package/src/codex/history-worker.ts +204 -0
  258. package/src/codex/home.ts +206 -0
  259. package/src/codex/inject-coordination.ts +257 -0
  260. package/src/codex/inject.ts +1857 -0
  261. package/src/codex/injected-marker.ts +79 -0
  262. package/src/codex/integration-record.ts +266 -0
  263. package/src/codex/internal/catalog-writer.ts +203 -0
  264. package/src/codex/internal/history-writer.ts +105 -0
  265. package/src/codex/journal.ts +172 -0
  266. package/src/codex/main-account-cache.ts +56 -0
  267. package/src/codex/main-account.ts +40 -0
  268. package/src/codex/management-convergence.ts +114 -0
  269. package/src/codex/model-cache.ts +267 -0
  270. package/src/codex/native-main-admission.ts +47 -0
  271. package/src/codex/native-main-auth-temp.ts +187 -0
  272. package/src/codex/native-main-claim.ts +167 -0
  273. package/src/codex/native-main-lock-file.ts +162 -0
  274. package/src/codex/native-main-owner.ts +329 -0
  275. package/src/codex/native-profile-api.ts +247 -0
  276. package/src/codex/native-profile-manager.ts +1531 -0
  277. package/src/codex/native-profile-processes.ts +121 -0
  278. package/src/codex/native-profile-recovery.ts +99 -0
  279. package/src/codex/native-profile-stage-store.ts +387 -0
  280. package/src/codex/native-profile-startup.ts +348 -0
  281. package/src/codex/native-profile-store.ts +855 -0
  282. package/src/codex/native-profile-types.ts +120 -0
  283. package/src/codex/native-residue.ts +691 -0
  284. package/src/codex/paths.ts +78 -0
  285. package/src/codex/plugins-doctor.ts +242 -0
  286. package/src/codex/pool-rotation.ts +295 -0
  287. package/src/codex/project-config-warnings.ts +426 -0
  288. package/src/codex/prompt-journal.ts +311 -0
  289. package/src/codex/prompt-layers.ts +967 -0
  290. package/src/codex/prompt-lock.ts +143 -0
  291. package/src/codex/provider-adoption.ts +242 -0
  292. package/src/codex/quota-rejection.ts +224 -0
  293. package/src/codex/quota.ts +494 -0
  294. package/src/codex/refresh.ts +60 -0
  295. package/src/codex/routing.ts +1855 -0
  296. package/src/codex/runtime.ts +659 -0
  297. package/src/codex/shim.ts +1215 -0
  298. package/src/codex/subagent-defaults.ts +557 -0
  299. package/src/codex/subagent-model-fallback.ts +560 -0
  300. package/src/codex/sync.ts +238 -0
  301. package/src/codex/transition-state.ts +612 -0
  302. package/src/codex/upstream-host-health.ts +368 -0
  303. package/src/codex/user-identity.ts +374 -0
  304. package/src/codex/warmup.ts +192 -0
  305. package/src/codex/websocket-registry.ts +100 -0
  306. package/src/codex/write-coordination.ts +114 -0
  307. package/src/combos/failover.ts +140 -0
  308. package/src/combos/index.ts +44 -0
  309. package/src/combos/request.ts +64 -0
  310. package/src/combos/resolve.ts +232 -0
  311. package/src/combos/types.ts +392 -0
  312. package/src/config.ts +3270 -0
  313. package/src/generated/model-metadata.ts +144 -0
  314. package/src/github/star-state.ts +203 -0
  315. package/src/grok/inject.ts +540 -0
  316. package/src/grok/inspect.ts +45 -0
  317. package/src/grok/status.ts +127 -0
  318. package/src/grok/sync.ts +66 -0
  319. package/src/images/artifacts.ts +516 -0
  320. package/src/images/fulfill-video.ts +163 -0
  321. package/src/images/fulfill.ts +149 -0
  322. package/src/images/index.ts +4 -0
  323. package/src/images/loop.ts +922 -0
  324. package/src/images/plan.ts +133 -0
  325. package/src/images/synthetic-tool.ts +133 -0
  326. package/src/images/types.ts +41 -0
  327. package/src/images/xai-client.ts +141 -0
  328. package/src/images/xai-video-client.ts +163 -0
  329. package/src/index.ts +22 -0
  330. package/src/integrations/config-io.ts +151 -0
  331. package/src/integrations/journal.ts +315 -0
  332. package/src/integrations/merge.ts +135 -0
  333. package/src/integrations/native/ownership-preflight.ts +202 -0
  334. package/src/integrations/ownership.ts +111 -0
  335. package/src/integrations/registry.ts +108 -0
  336. package/src/integrations/serialize.ts +235 -0
  337. package/src/integrations/state.ts +290 -0
  338. package/src/integrations/store.ts +103 -0
  339. package/src/integrations/writer.ts +492 -0
  340. package/src/lib/abort.ts +146 -0
  341. package/src/lib/admin-secrets.ts +25 -0
  342. package/src/lib/admission.ts +83 -0
  343. package/src/lib/app-owned-memory-stores.ts +173 -0
  344. package/src/lib/app-owned-memory.ts +265 -0
  345. package/src/lib/bounded-body.ts +242 -0
  346. package/src/lib/bun-binary-validator.d.mts +3 -0
  347. package/src/lib/bun-binary-validator.mjs +18 -0
  348. package/src/lib/bun-runtime.ts +184 -0
  349. package/src/lib/bun-stream-caps.ts +127 -0
  350. package/src/lib/config-ownership.ts +438 -0
  351. package/src/lib/crash-guard.ts +344 -0
  352. package/src/lib/debug-log-buffer.ts +83 -0
  353. package/src/lib/debug-settings.ts +108 -0
  354. package/src/lib/debug.ts +31 -0
  355. package/src/lib/destination-policy.ts +316 -0
  356. package/src/lib/errors.ts +364 -0
  357. package/src/lib/eventstream-decoder.ts +253 -0
  358. package/src/lib/gcp-adc.ts +341 -0
  359. package/src/lib/injection-debug-log.ts +58 -0
  360. package/src/lib/local-management-attestation.ts +51 -0
  361. package/src/lib/open-url.ts +25 -0
  362. package/src/lib/pinned-http.ts +182 -0
  363. package/src/lib/privacy.ts +20 -0
  364. package/src/lib/process-control.ts +168 -0
  365. package/src/lib/provider-environment.ts +470 -0
  366. package/src/lib/provider-outbound.ts +203 -0
  367. package/src/lib/provider-url.ts +14 -0
  368. package/src/lib/proxy-env.ts +18 -0
  369. package/src/lib/redact.ts +510 -0
  370. package/src/lib/remodex-home.ts +616 -0
  371. package/src/lib/retry-after.ts +55 -0
  372. package/src/lib/service-secrets.ts +178 -0
  373. package/src/lib/shadow-call.ts +54 -0
  374. package/src/lib/sidecar-tracker.ts +52 -0
  375. package/src/lib/sse-decoder.ts +364 -0
  376. package/src/lib/state-store-registrations.ts +109 -0
  377. package/src/lib/state-store-sweeper.ts +184 -0
  378. package/src/lib/system-restart-contract.ts +73 -0
  379. package/src/lib/test-home-guard.ts +98 -0
  380. package/src/lib/token-estimate.ts +69 -0
  381. package/src/lib/translator-budget.ts +366 -0
  382. package/src/lib/upstream-reachability.ts +91 -0
  383. package/src/lib/upstream-retry.ts +508 -0
  384. package/src/lib/win-exec.ts +115 -0
  385. package/src/lib/win-paths.ts +68 -0
  386. package/src/lib/windows-elevation.ts +705 -0
  387. package/src/lib/windows-secret-acl.ts +817 -0
  388. package/src/lib/windows-user-principal.ts +283 -0
  389. package/src/lib/winsw.ts +402 -0
  390. package/src/model-sources.ts +73 -0
  391. package/src/oauth/anthropic-routing.ts +594 -0
  392. package/src/oauth/anthropic.ts +177 -0
  393. package/src/oauth/callback-server.ts +294 -0
  394. package/src/oauth/chatgpt.ts +150 -0
  395. package/src/oauth/command-code.ts +239 -0
  396. package/src/oauth/cursor.ts +231 -0
  397. package/src/oauth/github-copilot.ts +428 -0
  398. package/src/oauth/google-antigravity.ts +230 -0
  399. package/src/oauth/health.ts +443 -0
  400. package/src/oauth/index.ts +1280 -0
  401. package/src/oauth/key-providers.ts +128 -0
  402. package/src/oauth/kimi.ts +213 -0
  403. package/src/oauth/kiro-credentials.ts +726 -0
  404. package/src/oauth/kiro.ts +621 -0
  405. package/src/oauth/local-token-detect.ts +121 -0
  406. package/src/oauth/log.ts +48 -0
  407. package/src/oauth/login-cli.ts +163 -0
  408. package/src/oauth/pkce.ts +15 -0
  409. package/src/oauth/store.ts +655 -0
  410. package/src/oauth/token-guardian.ts +309 -0
  411. package/src/oauth/types.ts +62 -0
  412. package/src/oauth/xai.ts +241 -0
  413. package/src/providers/alibaba-region-backup.ts +75 -0
  414. package/src/providers/alibaba-region-migration.ts +156 -0
  415. package/src/providers/alibaba-region-startup.ts +36 -0
  416. package/src/providers/antigravity-models.ts +317 -0
  417. package/src/providers/api-keys.ts +140 -0
  418. package/src/providers/base-url-choices.ts +64 -0
  419. package/src/providers/codex-capacity.ts +288 -0
  420. package/src/providers/command-code-efforts.ts +85 -0
  421. package/src/providers/context-cap.ts +73 -0
  422. package/src/providers/derive.ts +451 -0
  423. package/src/providers/free-directory.ts +187 -0
  424. package/src/providers/github-copilot-transport.ts +56 -0
  425. package/src/providers/google-vertex-location.ts +14 -0
  426. package/src/providers/key-failover.ts +271 -0
  427. package/src/providers/kiro-models.ts +67 -0
  428. package/src/providers/label.ts +19 -0
  429. package/src/providers/model-discovery-limits.ts +16 -0
  430. package/src/providers/model-discovery.ts +361 -0
  431. package/src/providers/openai-sidecar.ts +235 -0
  432. package/src/providers/openai-tier-startup.ts +27 -0
  433. package/src/providers/openai-tiers.ts +301 -0
  434. package/src/providers/openai-virtual-models.ts +83 -0
  435. package/src/providers/openrouter-routing.ts +102 -0
  436. package/src/providers/provider-id-rewrite.ts +179 -0
  437. package/src/providers/quota.ts +1942 -0
  438. package/src/providers/reasoning-capabilities.ts +336 -0
  439. package/src/providers/registry.ts +2375 -0
  440. package/src/providers/slug-codec.ts +74 -0
  441. package/src/providers/xai-transport.ts +149 -0
  442. package/src/reasoning-effort.ts +243 -0
  443. package/src/responses/compaction.ts +124 -0
  444. package/src/responses/hosted-tool-policy.ts +9 -0
  445. package/src/responses/parser.ts +714 -0
  446. package/src/responses/reasoning-envelope.ts +60 -0
  447. package/src/responses/reasoning-replay-cache.ts +106 -0
  448. package/src/responses/schema.ts +159 -0
  449. package/src/responses/spill-store.ts +431 -0
  450. package/src/responses/state.ts +1039 -0
  451. package/src/responses/tool-groups.ts +19 -0
  452. package/src/router.ts +802 -0
  453. package/src/routing/analytics.ts +377 -0
  454. package/src/routing/capability.ts +205 -0
  455. package/src/routing/cost.ts +77 -0
  456. package/src/routing/evaluator.ts +444 -0
  457. package/src/routing/health.ts +401 -0
  458. package/src/routing/history/cursor.ts +43 -0
  459. package/src/routing/history/indexer.ts +605 -0
  460. package/src/routing/history/schema.ts +72 -0
  461. package/src/routing/profile-namespace.ts +15 -0
  462. package/src/routing/profile.ts +424 -0
  463. package/src/routing/quota.ts +145 -0
  464. package/src/routing/request-evidence.ts +45 -0
  465. package/src/routing/trace.ts +686 -0
  466. package/src/server/adapter-resolve.ts +83 -0
  467. package/src/server/auth-cors.ts +606 -0
  468. package/src/server/chat-completions.ts +379 -0
  469. package/src/server/claude-messages.ts +980 -0
  470. package/src/server/effort-policy.ts +251 -0
  471. package/src/server/github-copilot-responses-repair.ts +338 -0
  472. package/src/server/gui-static.ts +152 -0
  473. package/src/server/image-retry.ts +42 -0
  474. package/src/server/images.ts +485 -0
  475. package/src/server/index.ts +1633 -0
  476. package/src/server/lifecycle.ts +482 -0
  477. package/src/server/live.ts +609 -0
  478. package/src/server/management/agent-settings-routes.ts +1180 -0
  479. package/src/server/management/android-remote-routes.ts +390 -0
  480. package/src/server/management/api-access.ts +141 -0
  481. package/src/server/management/api-key-usage.ts +167 -0
  482. package/src/server/management/body.ts +35 -0
  483. package/src/server/management/combo-routes.ts +244 -0
  484. package/src/server/management/config-routes.ts +602 -0
  485. package/src/server/management/context.ts +88 -0
  486. package/src/server/management/integration-routes.ts +538 -0
  487. package/src/server/management/logs-usage-routes.ts +516 -0
  488. package/src/server/management/model-routes.ts +519 -0
  489. package/src/server/management/model-rows.ts +143 -0
  490. package/src/server/management/native-integration-routes.ts +781 -0
  491. package/src/server/management/oauth-account-routes.ts +573 -0
  492. package/src/server/management/provider-routes.ts +781 -0
  493. package/src/server/management/request-history-routes.ts +191 -0
  494. package/src/server/management/routing-analytics-routes.ts +74 -0
  495. package/src/server/management/routing-profile-routes.ts +384 -0
  496. package/src/server/management/shared.ts +277 -0
  497. package/src/server/management/sidebar-routes.ts +106 -0
  498. package/src/server/management/sync-response.ts +69 -0
  499. package/src/server/management/system-restart.ts +433 -0
  500. package/src/server/management/system-routes.ts +141 -0
  501. package/src/server/management/usage-summary-cache.ts +86 -0
  502. package/src/server/management-api.ts +269 -0
  503. package/src/server/management-auth.ts +353 -0
  504. package/src/server/memory-watchdog.ts +156 -0
  505. package/src/server/port-reclaim.ts +307 -0
  506. package/src/server/ports.ts +156 -0
  507. package/src/server/proxy-liveness.ts +326 -0
  508. package/src/server/proxy-stop.ts +92 -0
  509. package/src/server/readiness.ts +99 -0
  510. package/src/server/relay-eager.ts +353 -0
  511. package/src/server/relay.ts +1179 -0
  512. package/src/server/request-decompress.ts +132 -0
  513. package/src/server/request-log-conversation.ts +168 -0
  514. package/src/server/request-log.ts +1072 -0
  515. package/src/server/responses/collaboration.ts +409 -0
  516. package/src/server/responses/compact.ts +710 -0
  517. package/src/server/responses/core.ts +3561 -0
  518. package/src/server/responses/encrypted-payload.ts +308 -0
  519. package/src/server/responses/fetch-helpers.ts +171 -0
  520. package/src/server/responses/passthrough-error.ts +78 -0
  521. package/src/server/responses/policy-fallback.ts +152 -0
  522. package/src/server/responses/terminal-guard.ts +230 -0
  523. package/src/server/responses/upstream-error.ts +48 -0
  524. package/src/server/responses-image-gen-repair.ts +132 -0
  525. package/src/server/responses-item-id-repair.ts +272 -0
  526. package/src/server/responses-json-events.ts +52 -0
  527. package/src/server/responses-model-rewrite.ts +29 -0
  528. package/src/server/responses-snapshot-repair.ts +621 -0
  529. package/src/server/responses.ts +10 -0
  530. package/src/server/search.ts +181 -0
  531. package/src/server/sse-frame-buffer.ts +292 -0
  532. package/src/server/sse-payload-rewrite.ts +263 -0
  533. package/src/server/startup-action-control.ts +308 -0
  534. package/src/server/startup-health-cache.ts +119 -0
  535. package/src/server/system-env.ts +418 -0
  536. package/src/server/windows-tcp-drop.ts +184 -0
  537. package/src/server/windows-tray-control.ts +41 -0
  538. package/src/server/ws-bridge.ts +470 -0
  539. package/src/service-manager-probe.ts +824 -0
  540. package/src/service.ts +3011 -0
  541. package/src/stall-timeout.ts +20 -0
  542. package/src/storage/cleanup-job.ts +57 -0
  543. package/src/storage/cleanup.ts +3085 -0
  544. package/src/storage/policy-job.ts +457 -0
  545. package/src/storage/policy-scheduler.ts +40 -0
  546. package/src/storage/policy-worker.ts +59 -0
  547. package/src/storage/policy.ts +527 -0
  548. package/src/storage/restore-job.ts +299 -0
  549. package/src/storage/restore-worker.ts +58 -0
  550. package/src/storage/scanner.ts +238 -0
  551. package/src/storage/storage-mutation-coordinator.ts +139 -0
  552. package/src/storage/worker-lifecycle.ts +215 -0
  553. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  554. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  555. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  556. package/src/tray/assets/opencodex-tray.png +0 -0
  557. package/src/tray/windows-tray.ps1 +364 -0
  558. package/src/tray/windows.ts +738 -0
  559. package/src/types.ts +1531 -0
  560. package/src/update/badge.ts +72 -0
  561. package/src/update/desktop-release.ts +1620 -0
  562. package/src/update/index.ts +402 -0
  563. package/src/update/job.ts +1906 -0
  564. package/src/update/notify.ts +261 -0
  565. package/src/update/npm-cache-preflight.d.mts +47 -0
  566. package/src/update/npm-cache-preflight.mjs +201 -0
  567. package/src/update/npm-invocation.d.mts +23 -0
  568. package/src/update/npm-invocation.mjs +94 -0
  569. package/src/update/tray-update-plan.d.mts +18 -0
  570. package/src/update/tray-update-plan.mjs +38 -0
  571. package/src/usage/cost.ts +0 -0
  572. package/src/usage/debug.ts +97 -0
  573. package/src/usage/expected-prices.ts +283 -0
  574. package/src/usage/log.ts +695 -0
  575. package/src/usage/summary.ts +585 -0
  576. package/src/usage/totals.ts +14 -0
  577. package/src/vision/anthropic-describe.ts +185 -0
  578. package/src/vision/describe.ts +127 -0
  579. package/src/vision/index.ts +558 -0
  580. package/src/vision/reasoning.ts +55 -0
  581. package/src/web-search/anthropic-executor.ts +189 -0
  582. package/src/web-search/executor.ts +105 -0
  583. package/src/web-search/format-result.ts +89 -0
  584. package/src/web-search/index.ts +196 -0
  585. package/src/web-search/loop.ts +791 -0
  586. package/src/web-search/parse.ts +235 -0
  587. package/src/web-search/progress-stream.ts +342 -0
  588. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,1075 @@
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
+ /**
18
+ * The manifest that shadows one state database.
19
+ *
20
+ * Exported because the history job must resolve it at CALL time for a Worker
21
+ * that does not inherit this module's load-time constants — and must resolve it
22
+ * the same way, since a manifest addressed differently is a different manifest.
23
+ */
24
+ export function historyBackupPathFor(stateDbPath: string): string {
25
+ const normalized = process.platform === "win32" ? resolve(stateDbPath).toLowerCase() : resolve(stateDbPath);
26
+ const id = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
27
+ return join(getConfigDir(), `codex-history-backup-${id}.json`);
28
+ }
29
+ const HISTORY_BACKUP_PATH = historyBackupPathFor(STATE_DB_PATH);
30
+ const RESUMABLE_SOURCES = ["cli", "vscode"] as const;
31
+
32
+ /**
33
+ * Open the live `state_5.sqlite` the way the Codex app expects a *secondary* writer to behave:
34
+ * wait on the WAL/file lock instead of failing instantly, so we never race the app's own
35
+ * connection pool into a half-applied checkpoint. The app opens this DB with `busy_timeout=5s`
36
+ * (see codex-rs `state::runtime::base_sqlite_options`); we mirror that here.
37
+ */
38
+ let historyDbBusyTimeoutMs = 5000;
39
+
40
+ /**
41
+ * Test-only knob: Windows CI can spend the FULL busy timeout on a transient file lock, which
42
+ * alone exceeds bun's 5s default per-test timeout. Tests shrink this so a busy DB fails fast
43
+ * into withHistoryRetry instead of stalling; production keeps the codex-rs-matching 5s.
44
+ */
45
+ export function setHistoryDbBusyTimeoutForTests(ms: number): void {
46
+ historyDbBusyTimeoutMs = ms;
47
+ }
48
+
49
+ function openStateDb(stateDbPath: string): Database {
50
+ const db = new Database(stateDbPath);
51
+ try {
52
+ db.exec(`PRAGMA busy_timeout = ${historyDbBusyTimeoutMs}`);
53
+ } catch {
54
+ /* best-effort: an older sqlite without busy_timeout still works, just less politely */
55
+ }
56
+ return db;
57
+ }
58
+
59
+ /**
60
+ * Append one JSONL line to a rollout using an O_APPEND handle, exactly like the Codex app's own
61
+ * metadata writer (`append_rollout_item_to_path` in codex-rs `rollout/src/recorder.rs`).
62
+ *
63
+ * Why append instead of rewriting line 1:
64
+ * - The app caches the live session's append handle and only reopens it when the handle is gone
65
+ * (codex-rs `RolloutWriterState::ensure_writer_open`). A temp+rename swap would orphan that
66
+ * handle; an in-place truncate would race the app's concurrent appends and clip new turns.
67
+ * - The app folds metadata by replaying every `session_meta` line in file order, last-writer-wins
68
+ * (codex-rs `apply_session_meta_from_item`), so a trailing `session_meta` overrides earlier ones.
69
+ * Real rollouts already contain multiple `session_meta` lines for this reason.
70
+ * For legacy unnumbered rollouts, O_APPEND makes each write land at EOF atomically, so it composes
71
+ * with the app appending concurrently. Ordered/paginated rollouts remain writer-owned: a second
72
+ * writer cannot safely allocate their next ordinal. We do not touch mtime: a fresh mtime is correct
73
+ * here (the app uses mtime as the rollout's updated_at), and forcing it backwards could hide a real
74
+ * edit from list ordering.
75
+ */
76
+ function appendRolloutLine(path: string, line: string): void {
77
+ const fd = openSync(path, "a");
78
+ try {
79
+ const buf = Buffer.from(line.endsWith("\n") ? line : `${line}\n`, "utf8");
80
+ let offset = 0;
81
+ while (offset < buf.length) {
82
+ offset += writeSync(fd, buf, offset, buf.length - offset, null);
83
+ }
84
+ try { fsyncSync(fd); } catch { /* best-effort durability */ }
85
+ } finally {
86
+ closeSync(fd);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Patch the `model_provider` value inside the FIRST line of a rollout *in place, length-preserving*.
92
+ *
93
+ * Why this exists in addition to {@link appendRolloutLine}: Codex resolves a thread's provider via
94
+ * two different readers. The SQLite replay path folds every `session_meta` line last-writer-wins
95
+ * (covered by appending a trailing meta), but `read_session_meta_line` reads only the FIRST line
96
+ * and `update_thread_metadata` clones it when the app later writes git/memory-mode metadata
97
+ * (codex-rs `thread-store/src/local/update_thread_metadata.rs`). If the first line still says
98
+ * `opencodex` after a native restore, that clone re-appends `opencodex` and last-writer-wins
99
+ * resurrects the routed provider. So a durable restore must also fix line 1.
100
+ *
101
+ * Safety: Codex parses each rollout line as `serde_json::from_str(line.trim())`, which tolerates
102
+ * insignificant JSON whitespace. We therefore replace the provider value and pad the removed bytes
103
+ * with spaces so the line's byte length is unchanged. Equal length means we can write at offset 0
104
+ * with no truncate and no inode swap, so this composes safely with the app's cached append handle.
105
+ * Only length-preserving shrinks are handled (e.g. "opencodex" -> "openai"); callers that would
106
+ * grow the value fall back to append-only, which is correct for the opencodex direction.
107
+ *
108
+ * Returns true when line 1 was patched, false when it could not be done safely (missing file,
109
+ * non-`session_meta` first line, id mismatch, value already correct, or a length-growing change).
110
+ */
111
+ function patchFirstLineProviderInPlace(path: string, expectedId: string, provider: string): boolean {
112
+ if (!existsSync(path)) return false;
113
+ const fd = openSync(path, "r+");
114
+ try {
115
+ // Read the first line by growing the probe until we hit a newline. session_meta lines embed
116
+ // base_instructions and can be tens of KB; a fixed cap would silently skip the in-place patch
117
+ // (and fall back to append-only, re-opening the first-line-clone resurrection gap), so we read
118
+ // until the line actually ends rather than guessing a ceiling.
119
+ const CHUNK = 1 << 16;
120
+ const MAX_FIRST_LINE = 1 << 24; // 16 MiB hard stop so a newline-less/corrupt file can't OOM us.
121
+ let collected = Buffer.alloc(0);
122
+ let nlIndex = -1;
123
+ let pos = 0;
124
+ while (nlIndex === -1) {
125
+ const chunk = Buffer.alloc(CHUNK);
126
+ const read = readSync(fd, chunk, 0, CHUNK, pos);
127
+ if (read === 0) break; // EOF with no newline: single-line file, skip
128
+ collected = Buffer.concat([collected, chunk.subarray(0, read)]);
129
+ nlIndex = collected.indexOf(0x0a);
130
+ pos += read;
131
+ if (collected.length > MAX_FIRST_LINE) return false;
132
+ }
133
+ if (nlIndex === -1) return false; // no newline anywhere: skip
134
+ const firstLine = collected.subarray(0, nlIndex).toString("utf8");
135
+
136
+ const meta = parseSessionMetaLine(firstLine);
137
+ if (!meta) return false;
138
+ if (meta.record.payload.id !== expectedId) return false;
139
+ if (meta.record.payload.model_provider === provider) return false;
140
+
141
+ // Locate the exact `"model_provider":"<value>"` token (allowing whitespace after the colon).
142
+ const match = firstLine.match(/"model_provider"\s*:\s*"([^"\\]*)"/);
143
+ if (!match || match.index === undefined) return false;
144
+ const oldToken = match[0];
145
+ const newCore = `"model_provider":"${provider}"`;
146
+ if (Buffer.byteLength(newCore, "utf8") > Buffer.byteLength(oldToken, "utf8")) return false; // grow: not length-preserving
147
+ const pad = " ".repeat(Buffer.byteLength(oldToken, "utf8") - Buffer.byteLength(newCore, "utf8"));
148
+ const newToken = `${newCore}${pad}`;
149
+
150
+ const patchedLine = firstLine.slice(0, match.index) + newToken + firstLine.slice(match.index + oldToken.length);
151
+ // Length must be identical so the trailing bytes (newline + rest of file) are untouched.
152
+ if (Buffer.byteLength(patchedLine, "utf8") !== Buffer.byteLength(firstLine, "utf8")) return false;
153
+ // Sanity: the patched line must still parse and carry the new provider.
154
+ const reparsed = parseSessionMetaLine(patchedLine);
155
+ if (!reparsed || reparsed.record.payload.model_provider !== provider) return false;
156
+
157
+ const out = Buffer.from(patchedLine, "utf8");
158
+ let offset = 0;
159
+ while (offset < out.length) {
160
+ offset += writeSync(fd, out, offset, out.length - offset, offset);
161
+ }
162
+ try { fsyncSync(fd); } catch { /* best-effort durability */ }
163
+ return true;
164
+ } finally {
165
+ closeSync(fd);
166
+ }
167
+ }
168
+
169
+ export type CodexHistoryProvider = "openai" | "opencodex";
170
+
171
+ export type CodexHistoryFailureReason = "busy" | "permission";
172
+
173
+ export interface CodexHistorySyncResult {
174
+ rows: number;
175
+ files: number;
176
+ ejectedRows?: number;
177
+ /** Set when a lock/busy error survived retries and the sync was SKIPPED, not empty. */
178
+ failed?: true;
179
+ /** Why the retry budget was exhausted when `failed` is set. */
180
+ failureReason?: CodexHistoryFailureReason;
181
+ }
182
+
183
+ interface ThreadRow {
184
+ id: string;
185
+ rollout_path: string;
186
+ model_provider: string;
187
+ source: string;
188
+ has_user_event: number;
189
+ }
190
+
191
+ interface BackupEntry {
192
+ id: string;
193
+ rolloutPath: string;
194
+ modelProvider: string;
195
+ source: string;
196
+ hasUserEvent: number;
197
+ /** Model observed while the original codex-lb row was captured; used for compare-and-swap. */
198
+ codexLbOriginalModel?: string | null;
199
+ /** One-time marker: the adopted provider identity is now encoded in the task model slug. */
200
+ codexLbModelQualified?: boolean;
201
+ }
202
+
203
+ interface BackupManifest {
204
+ version: 1;
205
+ stateDbPath?: string;
206
+ entries: Record<string, BackupEntry>;
207
+ }
208
+
209
+ interface NativeRestoreTarget {
210
+ modelProvider: string;
211
+ source: string;
212
+ hasUserEvent: number;
213
+ }
214
+
215
+ interface ThreadRowWithModel extends ThreadRow {
216
+ model: string | null;
217
+ }
218
+
219
+ function threadsHaveModelColumn(db: Database): boolean {
220
+ const row = db.query<{ n: number }, []>(`
221
+ SELECT count(*) AS n
222
+ FROM pragma_table_info('threads')
223
+ WHERE name = 'model'
224
+ `).get();
225
+ return (row?.n ?? 0) > 0;
226
+ }
227
+
228
+ function qualifyCodexLbModel(model: string | null): string | null {
229
+ if (!model || model.includes("/")) return model;
230
+ return `codex-lb/${model}`;
231
+ }
232
+
233
+ function samePath(a: string, b: string): boolean {
234
+ const left = resolve(a);
235
+ const right = resolve(b);
236
+ return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
237
+ }
238
+
239
+ function readBackup(path: string, stateDbPath?: string): BackupManifest {
240
+ if (!existsSync(path)) return { version: 1, stateDbPath, entries: {} };
241
+ try {
242
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<BackupManifest>;
243
+ if (parsed.version !== 1 || !parsed.entries || typeof parsed.entries !== "object") {
244
+ return { version: 1, stateDbPath, entries: {} };
245
+ }
246
+ if (stateDbPath && typeof parsed.stateDbPath === "string" && !samePath(parsed.stateDbPath, stateDbPath)) {
247
+ return { version: 1, stateDbPath, entries: {} };
248
+ }
249
+ return { version: 1, stateDbPath: parsed.stateDbPath ?? stateDbPath, entries: parsed.entries };
250
+ } catch {
251
+ return { version: 1, stateDbPath, entries: {} };
252
+ }
253
+ }
254
+
255
+ function writeBackup(path: string, manifest: BackupManifest, stateDbPath?: string): void {
256
+ if (Object.keys(manifest.entries).length === 0) {
257
+ if (existsSync(path)) unlinkSync(path);
258
+ return;
259
+ }
260
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
261
+ atomicWriteFile(path, JSON.stringify({ ...manifest, stateDbPath: manifest.stateDbPath ?? stateDbPath }, null, 2) + "\n");
262
+ }
263
+
264
+ function rememberOriginal(manifest: BackupManifest, row: ThreadRow): void {
265
+ if (manifest.entries[row.id]) return;
266
+ const model = "model" in row
267
+ ? (typeof (row as ThreadRowWithModel).model === "string" ? (row as ThreadRowWithModel).model : null)
268
+ : undefined;
269
+ manifest.entries[row.id] = {
270
+ id: row.id,
271
+ rolloutPath: row.rollout_path,
272
+ modelProvider: row.model_provider,
273
+ source: row.source,
274
+ hasUserEvent: Number(row.has_user_event) || 0,
275
+ ...(model !== undefined ? { codexLbOriginalModel: model } : {}),
276
+ };
277
+ }
278
+
279
+ interface ParsedSessionMeta {
280
+ record: {
281
+ type?: unknown;
282
+ timestamp?: unknown;
283
+ ordinal?: unknown;
284
+ payload: { model_provider?: unknown; source?: unknown } & Record<string, unknown>;
285
+ };
286
+ }
287
+
288
+ /** Parse one JSONL line into a `session_meta` record, or null if it isn't one. */
289
+ function parseSessionMetaLine(line: string): ParsedSessionMeta | null {
290
+ let parsed: unknown;
291
+ try {
292
+ parsed = JSON.parse(line);
293
+ } catch {
294
+ return null;
295
+ }
296
+ if (!parsed || typeof parsed !== "object") return null;
297
+ const record = parsed as ParsedSessionMeta["record"];
298
+ if (record.type !== "session_meta" || !record.payload || typeof record.payload !== "object") return null;
299
+ return { record };
300
+ }
301
+
302
+ /**
303
+ * Find the LAST `session_meta` line in a rollout, mirroring the app's last-writer-wins fold
304
+ * (codex-rs `apply_session_meta_from_item`). We base our patch on the most recent metadata so we
305
+ * never resurrect a stale provider that a later app-written `session_meta` already changed.
306
+ */
307
+ export function readLatestSessionMeta(path: string): ParsedSessionMeta | null {
308
+ const raw = readFileSync(path, "utf8");
309
+ const lines = raw.split("\n");
310
+ for (let i = lines.length - 1; i >= 0; i--) {
311
+ const line = lines[i];
312
+ if (!line) continue;
313
+ if (!line.includes("\"session_meta\"")) continue;
314
+ const meta = parseSessionMetaLine(line);
315
+ if (meta) return meta;
316
+ }
317
+ return null;
318
+ }
319
+
320
+ /** Find the newest session metadata that belongs to one canonical task id. */
321
+ function readLatestSessionMetaForId(path: string, expectedId: string): ParsedSessionMeta | null {
322
+ const raw = readFileSync(path, "utf8");
323
+ const lines = raw.split("\n");
324
+ for (let i = lines.length - 1; i >= 0; i--) {
325
+ const line = lines[i];
326
+ if (!line || !line.includes("\"session_meta\"")) continue;
327
+ const meta = parseSessionMetaLine(line);
328
+ if (meta?.record.payload.id === expectedId) return meta;
329
+ }
330
+ return null;
331
+ }
332
+
333
+ /** Ordered rollouts must only be changed by Codex, which owns their ordinal allocation. */
334
+ function isWriterOwnedRollout(record: ParsedSessionMeta["record"]): boolean {
335
+ return record.payload.history_mode === "paginated" || Object.hasOwn(record, "ordinal");
336
+ }
337
+
338
+ /**
339
+ * Fields needed to re-insert a production-shaped `threads` row from a rollout JSONL when a
340
+ * Phase-2 quarantine predates full `satellite-backup.json` thread snapshots.
341
+ *
342
+ * Uses the same last-writer-wins `session_meta` fold as {@link readLatestSessionMeta}, plus the
343
+ * first user-message preview (codex-rs `list.rs` / `EventMsg::UserMessage` path).
344
+ */
345
+ export interface RolloutThreadFields {
346
+ id: string;
347
+ modelProvider: string;
348
+ source: string;
349
+ firstUserMessage: string;
350
+ hasUserEvent: number;
351
+ cwd?: string;
352
+ historyMode?: string;
353
+ cliVersion?: string;
354
+ }
355
+
356
+ function textFromContentParts(content: unknown): string | null {
357
+ if (typeof content === "string" && content.trim()) return content.trim();
358
+ if (!Array.isArray(content)) return null;
359
+ const parts: string[] = [];
360
+ for (const part of content) {
361
+ if (!part || typeof part !== "object") continue;
362
+ const p = part as Record<string, unknown>;
363
+ if (typeof p.text === "string" && p.text.trim()) parts.push(p.text.trim());
364
+ else if (typeof p.input_text === "string" && p.input_text.trim()) parts.push(p.input_text.trim());
365
+ }
366
+ const joined = parts.join("\n").trim();
367
+ return joined || null;
368
+ }
369
+
370
+ /** Extract the first user-message preview from a rollout line, or null. */
371
+ function extractUserMessagePreview(line: string): string | null {
372
+ let parsed: unknown;
373
+ try {
374
+ parsed = JSON.parse(line);
375
+ } catch {
376
+ return null;
377
+ }
378
+ if (!parsed || typeof parsed !== "object") return null;
379
+ const record = parsed as { type?: unknown; payload?: unknown };
380
+ const payload = record.payload;
381
+ if (!payload || typeof payload !== "object") return null;
382
+ const p = payload as Record<string, unknown>;
383
+
384
+ if (record.type === "event_msg") {
385
+ // codex-rs EventMsg::UserMessage — payload.type is "user_message" (or omitted in fixtures).
386
+ if (p.type === "user_message" || typeof p.message === "string") {
387
+ if (typeof p.message === "string" && p.message.trim()) return p.message.trim();
388
+ const fromContent = textFromContentParts(p.content);
389
+ if (fromContent) return fromContent;
390
+ }
391
+ return null;
392
+ }
393
+
394
+ if (record.type === "response_item") {
395
+ if (p.type === "message" && p.role === "user") {
396
+ return textFromContentParts(p.content);
397
+ }
398
+ }
399
+ return null;
400
+ }
401
+
402
+ /**
403
+ * Reconstruct thread identity + listing fields from a staged/restored rollout JSONL.
404
+ * Returns null when the file is missing or has no parseable `session_meta`.
405
+ *
406
+ * Accepts plain `.jsonl` or a lone `.jsonl.zst` (legacy Phase-2 quarantine). Compressed
407
+ * rollouts are decompressed in memory with {@link MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES};
408
+ * no decompressed copy is written to disk.
409
+ */
410
+ export function readThreadFieldsFromRollout(path: string): RolloutThreadFields | null {
411
+ if (!path || !existsSync(path)) return null;
412
+ let raw: string;
413
+ try {
414
+ raw = path.endsWith(".zst")
415
+ ? decompressRolloutZstUtf8(path)
416
+ : readFileSync(path, "utf8");
417
+ } catch {
418
+ return null;
419
+ }
420
+ return parseThreadFieldsFromRolloutText(raw);
421
+ }
422
+
423
+ function decompressRolloutZstUtf8(
424
+ path: string,
425
+ maxBytes: number = MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES,
426
+ ): string {
427
+ const compressed = readFileSync(path);
428
+ const decoded = zstdDecompressSync(compressed as Uint8Array<ArrayBuffer>, {
429
+ maxOutputLength: maxBytes,
430
+ });
431
+ if (decoded.byteLength > maxBytes) {
432
+ throw new Error("rollout_zst_too_large");
433
+ }
434
+ return new TextDecoder().decode(decoded);
435
+ }
436
+
437
+ function parseThreadFieldsFromRolloutText(raw: string): RolloutThreadFields | null {
438
+ const lines = raw.split("\n");
439
+ let latest: ParsedSessionMeta | null = null;
440
+ let firstUserMessage = "";
441
+ for (const line of lines) {
442
+ if (!line) continue;
443
+ if (line.includes("\"session_meta\"")) {
444
+ const meta = parseSessionMetaLine(line);
445
+ if (meta) latest = meta;
446
+ }
447
+ if (!firstUserMessage) {
448
+ const preview = extractUserMessagePreview(line);
449
+ if (preview) firstUserMessage = preview;
450
+ }
451
+ }
452
+ if (!latest) return null;
453
+ const payload = latest.record.payload;
454
+ const id = typeof payload.id === "string" ? payload.id : "";
455
+ if (!id) return null;
456
+ const modelProvider = typeof payload.model_provider === "string" && payload.model_provider
457
+ ? payload.model_provider
458
+ : "openai";
459
+ const source = typeof payload.source === "string" && payload.source
460
+ ? payload.source
461
+ : "cli";
462
+ return {
463
+ id,
464
+ modelProvider,
465
+ source,
466
+ firstUserMessage,
467
+ hasUserEvent: firstUserMessage.trim() ? 1 : 0,
468
+ ...(typeof payload.cwd === "string" ? { cwd: payload.cwd } : {}),
469
+ ...(typeof payload.history_mode === "string" ? { historyMode: payload.history_mode } : {}),
470
+ ...(typeof payload.cli_version === "string" ? { cliVersion: payload.cli_version } : {}),
471
+ };
472
+ }
473
+
474
+ /**
475
+ * Make a thread's rollout reflect a provider/source change by APPENDING a new `session_meta` line,
476
+ * rather than rewriting line 1. The appended line clones the latest metadata payload (so no field
477
+ * is accidentally reset to empty) and applies only the requested changes. Returns false when the
478
+ * rollout is missing, has no parseable `session_meta`, its latest `session_meta` belongs to a
479
+ * different thread id, it uses Codex-owned ordinal ordering, or it already matches the desired
480
+ * values.
481
+ */
482
+ function updateSessionMeta(
483
+ path: string,
484
+ expectedId: string,
485
+ patch: { provider?: string; source?: string },
486
+ options: { patchFirstLine?: boolean; findLatestMatchingId?: boolean } = {},
487
+ ): boolean {
488
+ if (!path || !existsSync(path)) return false;
489
+
490
+ const latest = options.findLatestMatchingId
491
+ ? readLatestSessionMetaForId(path, expectedId)
492
+ : readLatestSessionMeta(path);
493
+ if (!latest) return false;
494
+ const record = latest.record;
495
+
496
+ // The app ignores `session_meta` lines whose payload id != the canonical thread id
497
+ // (codex-rs `apply_session_meta_from_item`). Forked rollouts can embed a source session's
498
+ // metadata, so an id-mismatched latest line means we'd be cloning the wrong thread's meta and
499
+ // appending a line the app would discard. Skip rather than write a no-op/misleading line.
500
+ const payloadId = record.payload.id;
501
+ if (typeof payloadId !== "string" || payloadId !== expectedId) return false;
502
+
503
+ // Paginated rollouts require one globally increasing ordinal per record. Cloning this metadata
504
+ // would clone its old ordinal, while computing max + 1 would race Codex's live writer. Leave the
505
+ // rollout byte-identical and let the SQLite/provider migration proceed independently.
506
+ if (isWriterOwnedRollout(record)) return false;
507
+
508
+ let changed = false;
509
+ if (patch.provider !== undefined && record.payload.model_provider !== patch.provider) {
510
+ record.payload.model_provider = patch.provider;
511
+ changed = true;
512
+ }
513
+ if (patch.source !== undefined && record.payload.source !== patch.source) {
514
+ record.payload.source = patch.source;
515
+ changed = true;
516
+ }
517
+ if (!changed) return false;
518
+
519
+ // Cover Codex's *other* provider reader: `read_session_meta_line` reads only line 1, and the
520
+ // app clones it when writing later git/memory-mode metadata. Appending alone leaves a stale
521
+ // line-1 provider that the clone would re-append, so for a length-preserving provider change we
522
+ // also patch line 1 in place (no inode swap, no truncate). Best-effort: when it can't be done
523
+ // safely (e.g. a length-growing change), the trailing append below is still correct for the
524
+ // SQLite replay path.
525
+ if (patch.provider !== undefined && options.patchFirstLine !== false) {
526
+ try { patchFirstLineProviderInPlace(path, expectedId, patch.provider); } catch { /* best-effort line-1 patch */ }
527
+ }
528
+
529
+ // Refresh the line timestamp so the appended record reads as the newest metadata.
530
+ record.timestamp = new Date().toISOString();
531
+ appendRolloutLine(path, JSON.stringify(record));
532
+ return true;
533
+ }
534
+
535
+ function toNativeRestoreTarget(entry: BackupEntry): NativeRestoreTarget {
536
+ if (entry.modelProvider !== "opencodex") {
537
+ return {
538
+ modelProvider: entry.modelProvider,
539
+ source: entry.source,
540
+ hasUserEvent: entry.hasUserEvent,
541
+ };
542
+ }
543
+ return {
544
+ modelProvider: "openai",
545
+ source: entry.source === "exec" ? "cli" : entry.source,
546
+ hasUserEvent: 1,
547
+ };
548
+ }
549
+
550
+ function ejectRemainingOpencodexHistory(db: Database): { rows: number; files: number } {
551
+ const rows = db
552
+ .query<ThreadRow, []>(`
553
+ SELECT id, rollout_path, model_provider, source, has_user_event
554
+ FROM threads
555
+ WHERE model_provider = 'opencodex'
556
+ AND trim(coalesce(first_user_message, '')) != ''
557
+ `)
558
+ .all();
559
+
560
+ let files = 0;
561
+ for (const row of rows) {
562
+ try {
563
+ if (updateSessionMeta(row.rollout_path, row.id, {
564
+ provider: "openai",
565
+ source: row.source === "exec" ? "cli" : undefined,
566
+ })) files++;
567
+ } catch {
568
+ /* native restore should continue even if an old rollout is missing */
569
+ }
570
+ }
571
+
572
+ const restore = db.transaction(() => {
573
+ const update = db.query(`
574
+ UPDATE threads
575
+ SET model_provider = 'openai',
576
+ source = CASE WHEN source = 'exec' THEN 'cli' ELSE source END,
577
+ has_user_event = 1
578
+ WHERE id = ?
579
+ `);
580
+ for (const row of rows) update.run(row.id);
581
+ });
582
+ restore();
583
+ return { rows: rows.length, files };
584
+ }
585
+
586
+ /**
587
+ * Move Desktop tasks that still carry the adopted codex-lb provider identity onto the built-in
588
+ * openai identity used by loopback injection. The original rows are retained in the ordinary
589
+ * history manifest so restore can put codex-lb back exactly. Unlike the legacy opencodex eject,
590
+ * keep line 1 unchanged: it already contains the native codex-lb identity that restore needs,
591
+ * while the appended openai metadata is the active last-writer-wins value during mediation.
592
+ */
593
+ export function migrateCodexLbHistoryToOpenai(
594
+ stateDbPath = STATE_DB_PATH,
595
+ backupPath = HISTORY_BACKUP_PATH,
596
+ ): CodexHistorySyncResult {
597
+ if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
598
+ const retried = withHistoryRetryResult(() => {
599
+ const db = openStateDb(stateDbPath);
600
+ try {
601
+ const manifest = readBackup(backupPath, stateDbPath);
602
+ const hasModelColumn = threadsHaveModelColumn(db);
603
+ const backedTaskIds = new Set(Object.values(manifest.entries)
604
+ .filter(entry => entry.modelProvider === "codex-lb")
605
+ .map(entry => entry.id));
606
+ const rows = db
607
+ .query<ThreadRowWithModel, []>(`
608
+ SELECT id, rollout_path, model_provider, source, has_user_event,
609
+ ${hasModelColumn ? "model" : "NULL"} AS model
610
+ FROM threads
611
+ WHERE model_provider IN ('codex-lb', 'openai')
612
+ `)
613
+ .all()
614
+ .filter(row => row.model_provider === "codex-lb" || backedTaskIds.has(row.id));
615
+ if (rows.length === 0) return { rows: 0, files: 0 };
616
+
617
+ const pendingRows = rows.filter(row => row.model_provider === "codex-lb");
618
+ if (pendingRows.length > 0) {
619
+ for (const row of pendingRows) rememberOriginal(manifest, row);
620
+ writeBackup(backupPath, manifest, stateDbPath);
621
+ }
622
+ // Backfill manifests written by versions that did not retain the selected model. Once this
623
+ // observation is persisted, every retry uses a compare-and-swap and cannot overwrite a newer
624
+ // Desktop/Android selection made between the database read and migration write.
625
+ let backfilledOriginalModel = false;
626
+ for (const row of rows) {
627
+ const entry = manifest.entries[row.id];
628
+ if (entry?.modelProvider !== "codex-lb"
629
+ || entry.codexLbModelQualified === true
630
+ || Object.hasOwn(entry, "codexLbOriginalModel")) continue;
631
+ entry.codexLbOriginalModel = row.model;
632
+ backfilledOriginalModel = true;
633
+ }
634
+ if (backfilledOriginalModel) writeBackup(backupPath, manifest, stateDbPath);
635
+ const modelNormalizationRows = rows.filter(row => {
636
+ const entry = manifest.entries[row.id];
637
+ return entry?.modelProvider === "codex-lb" && entry.codexLbModelQualified !== true;
638
+ });
639
+
640
+ let files = 0;
641
+ for (const row of rows) {
642
+ try {
643
+ if (updateSessionMeta(
644
+ row.rollout_path,
645
+ row.id,
646
+ { provider: "openai" },
647
+ { patchFirstLine: false, findLatestMatchingId: true },
648
+ )) files++;
649
+ } catch {
650
+ /* keep the database migration moving if an old rollout is missing or malformed */
651
+ }
652
+ }
653
+
654
+ if (pendingRows.length > 0 || modelNormalizationRows.length > 0) {
655
+ const update = db.transaction(() => {
656
+ const providerStatement = db.query(`
657
+ UPDATE threads
658
+ SET model_provider = 'openai'
659
+ WHERE id = ?
660
+ AND model_provider = 'codex-lb'
661
+ `);
662
+ for (const row of pendingRows) providerStatement.run(row.id);
663
+ if (hasModelColumn) {
664
+ const modelStatement = db.query(`
665
+ UPDATE threads
666
+ SET model = ?
667
+ WHERE id = ?
668
+ AND model IS ?
669
+ `);
670
+ for (const row of modelNormalizationRows) {
671
+ const entry = manifest.entries[row.id];
672
+ const original = entry?.codexLbOriginalModel ?? row.model;
673
+ const qualified = qualifyCodexLbModel(original);
674
+ if (row.model === original) modelStatement.run(qualified, row.id, original);
675
+ }
676
+ }
677
+ });
678
+ update();
679
+ }
680
+ if (modelNormalizationRows.length > 0) {
681
+ for (const row of modelNormalizationRows) {
682
+ const entry = manifest.entries[row.id];
683
+ if (entry) entry.codexLbModelQualified = true;
684
+ }
685
+ // Persist the idempotency marker only after the database transaction. If the process
686
+ // exits between them, the next pass safely observes an already-qualified model slug.
687
+ writeBackup(backupPath, manifest, stateDbPath);
688
+ }
689
+ const changedIds = new Set([
690
+ ...pendingRows.map(row => row.id),
691
+ ...modelNormalizationRows.map(row => row.id),
692
+ ]);
693
+ return { rows: changedIds.size, files };
694
+ } finally {
695
+ db.close();
696
+ }
697
+ });
698
+ return retried.ok
699
+ ? retried.value
700
+ : { rows: 0, files: 0, failed: true, failureReason: retried.reason };
701
+ }
702
+
703
+ export function classifyRecoverableHistoryError(error: unknown): CodexHistoryFailureReason | null {
704
+ const code = typeof error === "object" && error && "code" in error ? String((error as { code?: unknown }).code) : "";
705
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
706
+ if (code === "SQLITE_BUSY"
707
+ || code === "SQLITE_LOCKED"
708
+ || code === "EBUSY"
709
+ || message.includes("database is locked")
710
+ || message.includes("database is busy")
711
+ || message.includes("resource busy")) return "busy";
712
+ if (code === "EPERM"
713
+ || code === "EACCES"
714
+ || message.includes("operation not permitted")
715
+ || message.includes("permission denied")) return "permission";
716
+ return null;
717
+ }
718
+
719
+ export function isRecoverableHistoryError(error: unknown): boolean {
720
+ return classifyRecoverableHistoryError(error) !== null;
721
+ }
722
+
723
+ const HISTORY_RETRY_DELAY_MS = 500;
724
+ const HISTORY_RETRY_ATTEMPTS = 2;
725
+
726
+ /**
727
+ * Run a history mutation with one retry across recoverable lock/busy errors (the app's own
728
+ * connection holds `state_5.sqlite` in WAL with a 5 s busy_timeout; a transient writer
729
+ * usually clears within that window). Returns null when both attempts hit a recoverable
730
+ * error — callers surface that as `failed: true` instead of a silent no-op. Hard errors
731
+ * (corruption, programming bugs) still throw.
732
+ */
733
+ function withHistoryRetryResult<T>(fn: () => T, io: { sleepFn?: (ms: number) => void; attempts?: number; delayMs?: number } = {}):
734
+ | { ok: true; value: T }
735
+ | { ok: false; reason: CodexHistoryFailureReason } {
736
+ const sleepFn = io.sleepFn ?? Bun.sleepSync;
737
+ const attempts = Math.max(1, io.attempts ?? HISTORY_RETRY_ATTEMPTS);
738
+ const delayMs = io.delayMs ?? HISTORY_RETRY_DELAY_MS;
739
+ for (let attempt = 0; ; attempt++) {
740
+ try {
741
+ return { ok: true, value: fn() };
742
+ } catch (error) {
743
+ const reason = classifyRecoverableHistoryError(error);
744
+ if (!reason) throw error;
745
+ if (attempt >= attempts - 1) return { ok: false, reason };
746
+ try { sleepFn(delayMs); } catch { /* sleep is best-effort */ }
747
+ }
748
+ }
749
+ }
750
+
751
+ export function withHistoryRetry<T>(fn: () => T, io: { sleepFn?: (ms: number) => void; attempts?: number; delayMs?: number } = {}): T | null {
752
+ const result = withHistoryRetryResult(fn, io);
753
+ return result.ok ? result.value : null;
754
+ }
755
+
756
+ /**
757
+ * True when a READONLY probe proves the openai-direction restore would be a no-op:
758
+ * zero threads still tagged opencodex AND an empty backup manifest. Used to skip the
759
+ * write-open entirely in the Design B steady state — on Windows the Codex app holds
760
+ * `state_5.sqlite` (WAL, busy_timeout 5s), so an unnecessary write open can stall for
761
+ * seconds and surface a false lock warning, while WAL always admits readers. A failed
762
+ * probe (locked even for readers / schema drift) returns false so callers fall through
763
+ * to the write attempt and keep today's behavior for genuinely unknown state.
764
+ */
765
+ function openaiRestoreIsNoop(stateDbPath: string, backupPath: string): boolean {
766
+ const pending = countPendingOpencodexHistory(stateDbPath, backupPath);
767
+ return !pending.failed && pending.pendingRows === 0 && pending.backupEntries === 0;
768
+ }
769
+
770
+ export function syncCodexHistoryProvider(
771
+ provider: CodexHistoryProvider,
772
+ stateDbPath = STATE_DB_PATH,
773
+ backupPath = HISTORY_BACKUP_PATH,
774
+ opts: { skipWhenProvablyNoop?: boolean } = {},
775
+ ): CodexHistorySyncResult {
776
+ // Opt-in steady-state gate (Design B loopback callers only): default semantics of
777
+ // this exported API are unchanged — legacy stop/restore paths never pass the flag.
778
+ if (opts.skipWhenProvablyNoop && provider === "openai" && existsSync(stateDbPath)
779
+ && openaiRestoreIsNoop(stateDbPath, backupPath)) {
780
+ return { rows: 0, files: 0 };
781
+ }
782
+ const retried = withHistoryRetryResult(() => syncCodexHistoryProviderUnsafe(provider, stateDbPath, backupPath));
783
+ return retried.ok ? retried.value : { rows: 0, files: 0, failed: true, failureReason: retried.reason };
784
+ }
785
+
786
+ function syncCodexHistoryProviderUnsafe(provider: CodexHistoryProvider, stateDbPath: string, backupPath: string): CodexHistorySyncResult {
787
+ if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
788
+ if (provider === "openai") return restoreCodexHistoryProvider(stateDbPath, backupPath);
789
+
790
+ const db = openStateDb(stateDbPath);
791
+ try {
792
+ const placeholders = RESUMABLE_SOURCES.map(() => "?").join(",");
793
+ const openaiRows = db
794
+ .query<ThreadRow, string[]>(`
795
+ SELECT id, rollout_path, model_provider, source, has_user_event
796
+ FROM threads
797
+ WHERE model_provider = 'openai'
798
+ AND source IN (${placeholders})
799
+ `)
800
+ .all(...RESUMABLE_SOURCES);
801
+ const execRows = db
802
+ .query<ThreadRow, []>(`
803
+ SELECT id, rollout_path, model_provider, source, has_user_event
804
+ FROM threads
805
+ WHERE model_provider = 'opencodex'
806
+ AND source = 'exec'
807
+ AND trim(coalesce(first_user_message, '')) != ''
808
+ `)
809
+ .all();
810
+
811
+ const manifest = readBackup(backupPath, stateDbPath);
812
+ for (const row of [...openaiRows, ...execRows]) rememberOriginal(manifest, row);
813
+ writeBackup(backupPath, manifest, stateDbPath);
814
+
815
+ let files = 0;
816
+ for (const row of openaiRows) {
817
+ try {
818
+ if (updateSessionMeta(row.rollout_path, row.id, { provider: "opencodex" })) files++;
819
+ } catch {
820
+ /* best-effort; keep DB migration moving even if one old rollout is malformed */
821
+ }
822
+ }
823
+ for (const row of execRows) {
824
+ try {
825
+ if (updateSessionMeta(row.rollout_path, row.id, { source: "cli" })) files++;
826
+ } catch {
827
+ /* best-effort; keep DB migration moving even if one old rollout is malformed */
828
+ }
829
+ }
830
+
831
+ const update = db.transaction(() => {
832
+ const markUserEvent = db.query(`
833
+ UPDATE threads
834
+ SET has_user_event = 1
835
+ WHERE id = ?
836
+ AND trim(coalesce(first_user_message, '')) != ''
837
+ `);
838
+ for (const row of [...openaiRows, ...execRows]) markUserEvent.run(row.id);
839
+ db.query(`
840
+ UPDATE threads
841
+ SET model_provider = 'opencodex'
842
+ WHERE model_provider = 'openai'
843
+ AND source IN (${placeholders})
844
+ `).run(...RESUMABLE_SOURCES);
845
+ db.query(`
846
+ UPDATE threads
847
+ SET source = 'cli'
848
+ WHERE model_provider = 'opencodex'
849
+ AND source = 'exec'
850
+ AND trim(coalesce(first_user_message, '')) != ''
851
+ `).run();
852
+ });
853
+ update();
854
+
855
+ return { rows: openaiRows.length + execRows.length, files };
856
+ } finally {
857
+ db.close();
858
+ }
859
+ }
860
+
861
+ function restoreCodexHistoryProvider(
862
+ stateDbPath: string,
863
+ backupPath: string,
864
+ options: { preserveOriginalProviders?: ReadonlySet<string> } = {},
865
+ ): CodexHistorySyncResult {
866
+ const manifest = readBackup(backupPath, stateDbPath);
867
+ const preservedEntries: Record<string, BackupEntry> = {};
868
+ const entries: BackupEntry[] = [];
869
+ for (const entry of Object.values(manifest.entries)) {
870
+ if (options.preserveOriginalProviders?.has(entry.modelProvider)) {
871
+ preservedEntries[entry.id] = entry;
872
+ } else {
873
+ entries.push(entry);
874
+ }
875
+ }
876
+
877
+ const db = openStateDb(stateDbPath);
878
+ try {
879
+ if (entries.length === 0) {
880
+ const ejected = ejectRemainingOpencodexHistory(db);
881
+ return ejected.rows > 0 ? { rows: 0, files: ejected.files, ejectedRows: ejected.rows } : { rows: 0, files: 0 };
882
+ }
883
+
884
+ let files = 0;
885
+ for (const entry of entries) {
886
+ const target = toNativeRestoreTarget(entry);
887
+ try {
888
+ if (updateSessionMeta(entry.rolloutPath, entry.id, { provider: target.modelProvider, source: target.source })) files++;
889
+ } catch {
890
+ /* best-effort; keep DB restore moving even if one rollout disappeared */
891
+ }
892
+ }
893
+
894
+ const restore = db.transaction(() => {
895
+ if (threadsHaveModelColumn(db)) {
896
+ const update = db.query(`
897
+ UPDATE threads
898
+ SET model_provider = ?,
899
+ source = ?,
900
+ has_user_event = ?,
901
+ model = CASE
902
+ WHEN ? = 1 AND model LIKE 'codex-lb/%' THEN substr(model, length('codex-lb/') + 1)
903
+ ELSE model
904
+ END
905
+ WHERE id = ?
906
+ `);
907
+ for (const entry of entries) {
908
+ const target = toNativeRestoreTarget(entry);
909
+ update.run(
910
+ target.modelProvider,
911
+ target.source,
912
+ target.hasUserEvent,
913
+ entry.codexLbModelQualified === true ? 1 : 0,
914
+ entry.id,
915
+ );
916
+ }
917
+ } else {
918
+ const update = db.query(`
919
+ UPDATE threads
920
+ SET model_provider = ?,
921
+ source = ?,
922
+ has_user_event = ?
923
+ WHERE id = ?
924
+ `);
925
+ for (const entry of entries) {
926
+ const target = toNativeRestoreTarget(entry);
927
+ update.run(target.modelProvider, target.source, target.hasUserEvent, entry.id);
928
+ }
929
+ }
930
+ });
931
+ restore();
932
+ writeBackup(backupPath, { version: 1, stateDbPath, entries: preservedEntries }, stateDbPath);
933
+ const ejected = ejectRemainingOpencodexHistory(db);
934
+ return ejected.rows > 0
935
+ ? { rows: entries.length, files: files + ejected.files, ejectedRows: ejected.rows }
936
+ : { rows: entries.length, files };
937
+ } finally {
938
+ db.close();
939
+ }
940
+ }
941
+
942
+ /**
943
+ * Restore legacy Remodex history while mediation stays active, without consuming the native
944
+ * codex-lb identities retained for a later full restore. This is deliberately separate from the
945
+ * public native restore: stop/eject must still restore every provider exactly.
946
+ */
947
+ export function restoreManagedOpenaiHistory(
948
+ stateDbPath = STATE_DB_PATH,
949
+ backupPath = HISTORY_BACKUP_PATH,
950
+ ): CodexHistorySyncResult {
951
+ if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
952
+ const retried = withHistoryRetryResult(() => restoreCodexHistoryProvider(
953
+ stateDbPath,
954
+ backupPath,
955
+ { preserveOriginalProviders: new Set(["codex-lb"]) },
956
+ ));
957
+ return retried.ok
958
+ ? retried.value
959
+ : { rows: 0, files: 0, failed: true, failureReason: retried.reason };
960
+ }
961
+
962
+ export function restoreLegacyOpenaiHistory(stateDbPath = STATE_DB_PATH): CodexHistorySyncResult {
963
+ if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
964
+ const retried = withHistoryRetryResult(() => {
965
+ const db = openStateDb(stateDbPath);
966
+ try {
967
+ return ejectRemainingOpencodexHistory(db);
968
+ } finally {
969
+ db.close();
970
+ }
971
+ });
972
+ return retried.ok ? retried.value : { rows: 0, files: 0, failed: true, failureReason: retried.reason };
973
+ }
974
+
975
+ /**
976
+ * One-time Design-B migration: restore backed-up originals, then eject any remaining
977
+ * opencodex-tagged threads to openai. Thin wrapper over the restore path with a
978
+ * configurable retry budget — the daemon migration guardian uses `{ attempts: 1 }`
979
+ * per tick so a locked DB never stalls the event loop beyond one sqlite busy wait.
980
+ */
981
+ export function migrateHistoryToOpenai(
982
+ stateDbPath = STATE_DB_PATH,
983
+ backupPath = HISTORY_BACKUP_PATH,
984
+ opts: { attempts?: number; delayMs?: number; sleepFn?: (ms: number) => void } = {},
985
+ ): CodexHistorySyncResult {
986
+ if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
987
+ // Steady-state gate: this migration is Design-B-specific (inject + guardian callers),
988
+ // and after the one-time migration every start would otherwise write-open the DB for
989
+ // nothing. A missing DB with a leftover backup manifest does NOT satisfy the gate
990
+ // (backupEntries > 0), so the guardian's fresh-reinstall re-count protection holds.
991
+ if (openaiRestoreIsNoop(stateDbPath, backupPath)) return { rows: 0, files: 0 };
992
+ const retried = withHistoryRetryResult(() => syncCodexHistoryProviderUnsafe("openai", stateDbPath, backupPath), opts);
993
+ return retried.ok ? retried.value : { rows: 0, files: 0, failed: true, failureReason: retried.reason };
994
+ }
995
+
996
+ export interface PendingHistoryCount {
997
+ /** Threads still tagged opencodex that the eject path WOULD move (mirrors its WHERE). */
998
+ pendingRows: number;
999
+ /** Entries still recorded in the backup manifest (restore targets). */
1000
+ backupEntries: number;
1001
+ /** Set when the DB could not be opened/read (locked); counts are then unknown, not zero. */
1002
+ failed?: true;
1003
+ }
1004
+
1005
+ /**
1006
+ * Read-only migration progress probe for the guardian and `rmx doctor`. Opens sqlite
1007
+ * readonly with a SHORT busy timeout so a locked DB cannot stall a daemon tick. The
1008
+ * pending predicate mirrors ejectRemainingOpencodexHistory exactly — rows eject ignores
1009
+ * (empty first_user_message) are not counted, so 0 really means "migration done".
1010
+ */
1011
+ export function countPendingOpencodexHistory(stateDbPath = STATE_DB_PATH, backupPath = HISTORY_BACKUP_PATH): PendingHistoryCount {
1012
+ let backupEntries = 0;
1013
+ try {
1014
+ const manifest = readBackup(backupPath, stateDbPath);
1015
+ backupEntries = Object.keys(manifest.entries).length;
1016
+ } catch { /* unreadable manifest counts as 0 — restore treats it the same way */ }
1017
+
1018
+ if (!existsSync(stateDbPath)) return { pendingRows: 0, backupEntries };
1019
+ try {
1020
+ const db = new Database(stateDbPath, { readonly: true });
1021
+ try {
1022
+ db.exec("PRAGMA busy_timeout = 100");
1023
+ const row = db.query<{ n: number }, []>(`
1024
+ SELECT count(*) AS n
1025
+ FROM threads
1026
+ WHERE model_provider = 'opencodex'
1027
+ AND trim(coalesce(first_user_message, '')) != ''
1028
+ `).get();
1029
+ return { pendingRows: row?.n ?? 0, backupEntries };
1030
+ } finally {
1031
+ db.close();
1032
+ }
1033
+ } catch (error) {
1034
+ if (isRecoverableHistoryError(error)) return { pendingRows: 0, backupEntries, failed: true };
1035
+ // Schema drift (e.g. a future codex renames the table) is a "cannot know" too, not a crash.
1036
+ return { pendingRows: 0, backupEntries, failed: true };
1037
+ }
1038
+ }
1039
+
1040
+ /**
1041
+ * Read-only progress for the managed OpenAI migration. Preserved codex-lb manifest entries are
1042
+ * restore state, not pending migration work; newly discovered codex-lb database rows are work.
1043
+ */
1044
+ export function countPendingManagedOpenaiHistory(
1045
+ stateDbPath = STATE_DB_PATH,
1046
+ backupPath = HISTORY_BACKUP_PATH,
1047
+ ): PendingHistoryCount {
1048
+ let backupEntries = 0;
1049
+ try {
1050
+ const manifest = readBackup(backupPath, stateDbPath);
1051
+ backupEntries = Object.values(manifest.entries)
1052
+ .filter(entry => entry.modelProvider !== "codex-lb" || entry.codexLbModelQualified !== true)
1053
+ .length;
1054
+ } catch { /* unreadable manifest counts as 0 — migration treats it the same way */ }
1055
+
1056
+ if (!existsSync(stateDbPath)) return { pendingRows: 0, backupEntries };
1057
+ try {
1058
+ const db = new Database(stateDbPath, { readonly: true });
1059
+ try {
1060
+ db.exec("PRAGMA busy_timeout = 100");
1061
+ const row = db.query<{ n: number }, []>(`
1062
+ SELECT count(*) AS n
1063
+ FROM threads
1064
+ WHERE model_provider = 'codex-lb'
1065
+ OR (model_provider = 'opencodex'
1066
+ AND trim(coalesce(first_user_message, '')) != '')
1067
+ `).get();
1068
+ return { pendingRows: row?.n ?? 0, backupEntries };
1069
+ } finally {
1070
+ db.close();
1071
+ }
1072
+ } catch {
1073
+ return { pendingRows: 0, backupEntries, failed: true };
1074
+ }
1075
+ }