@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,1384 @@
1
+ import { realpath, stat } from "node:fs/promises";
2
+ import { isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { getCodexHome } from "../codex/paths";
5
+ import { planUpdateFromToolCall, toolItem } from "./desktop-session-stream";
6
+ import { fileChangesFromToolCall, type AndroidRemoteFileChange } from "./file-change-parser";
7
+ import {
8
+ canonicalUserMessageText,
9
+ publicUserMessageText,
10
+ } from "./user-message-identity";
11
+ import { resolveThreadSourcePaths } from "./thread-source-paths";
12
+
13
+ type JsonRecord = Record<string, unknown>;
14
+
15
+ const MAX_RECORD_BYTES = 2 * 1024 * 1024;
16
+ const MAX_COMMAND_BYTES = 64 * 1024;
17
+ const MAX_OUTPUT_BYTES = 64 * 1024;
18
+ const MAX_CACHE_ENTRIES = 6;
19
+
20
+ type RecoveredCommand = {
21
+ callId: string;
22
+ itemId: string;
23
+ order: number;
24
+ command: string;
25
+ status: string;
26
+ output: string | null;
27
+ exitCode: number | null;
28
+ durationMs: number | null;
29
+ };
30
+
31
+ type RecoveredFileChange = {
32
+ callId: string;
33
+ itemId: string;
34
+ order: number;
35
+ status: string;
36
+ changes: AndroidRemoteFileChange[];
37
+ };
38
+
39
+ type RecoveredActivity = {
40
+ callId: string;
41
+ itemId: string;
42
+ order: number;
43
+ item: JsonRecord;
44
+ };
45
+
46
+ type RecoveredTurnItem = {
47
+ order: number;
48
+ item: JsonRecord;
49
+ };
50
+
51
+ type RecoveredTurn = {
52
+ id: string;
53
+ order: number;
54
+ status: "inProgress" | "completed" | "interrupted" | "failed";
55
+ startedAt: number | null;
56
+ completedAt: number | null;
57
+ durationMs: number | null;
58
+ error: JsonRecord | null;
59
+ collaborationMode: JsonRecord | null;
60
+ items: Map<string, RecoveredTurnItem>;
61
+ canonicalItemEvents: boolean;
62
+ durableUserMessages: boolean;
63
+ };
64
+
65
+ type PendingDurableUserMessage = {
66
+ turnId: string;
67
+ order: number;
68
+ item: JsonRecord;
69
+ comparisonText: string;
70
+ };
71
+
72
+ type TurnRecovery = {
73
+ commands: Map<string, RecoveredCommand>;
74
+ fileChanges: Map<string, RecoveredFileChange>;
75
+ activities: Map<string, RecoveredActivity>;
76
+ responseOrder: Map<string, number>;
77
+ anchors: Array<{
78
+ type: "userMessage" | "agentMessage" | "reasoning" | "mcpToolCall" | "fileChange";
79
+ order: number;
80
+ }>;
81
+ };
82
+
83
+ type SessionCache = {
84
+ canonicalPath: string;
85
+ device: number | bigint;
86
+ inode: number | bigint;
87
+ size: number;
88
+ completeOffset: number;
89
+ firstTimestamp: number | null;
90
+ lastTimestamp: number | null;
91
+ threadId: string | null;
92
+ currentTurnId: string | null;
93
+ nextTurnPlanMode: boolean;
94
+ turnsInOrder: string[];
95
+ recoveredTurns: Map<string, RecoveredTurn>;
96
+ turns: Map<string, TurnRecovery>;
97
+ pendingUserMessage: PendingDurableUserMessage | null;
98
+ touchedAt: number;
99
+ };
100
+
101
+ export interface AndroidRemoteCommandRecovery {
102
+ enrichThread(thread: JsonRecord, sourcePaths?: readonly string[]): Promise<JsonRecord>;
103
+ clear(): void;
104
+ }
105
+
106
+ export type AndroidRemoteSessionCommandRecoveryOptions = {
107
+ codexHome?: string;
108
+ now?: () => number;
109
+ };
110
+
111
+ function record(value: unknown): JsonRecord | null {
112
+ return value && typeof value === "object" && !Array.isArray(value)
113
+ ? value as JsonRecord
114
+ : null;
115
+ }
116
+
117
+ function stringValue(value: unknown): string {
118
+ return typeof value === "string" ? value : "";
119
+ }
120
+
121
+ function finiteNumber(value: unknown): number | null {
122
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
123
+ }
124
+
125
+ function bounded(value: string, maximumBytes: number): string {
126
+ const encoded = new TextEncoder().encode(value);
127
+ if (encoded.byteLength <= maximumBytes) return value;
128
+ return new TextDecoder().decode(encoded.subarray(0, maximumBytes)).replace(/\uFFFD$/u, "");
129
+ }
130
+
131
+ function turnRecovery(turns: Map<string, TurnRecovery>, turnId: string): TurnRecovery {
132
+ const existing = turns.get(turnId);
133
+ if (existing) return existing;
134
+ const created: TurnRecovery = {
135
+ commands: new Map(),
136
+ fileChanges: new Map(),
137
+ activities: new Map(),
138
+ responseOrder: new Map(),
139
+ anchors: [],
140
+ };
141
+ turns.set(turnId, created);
142
+ return created;
143
+ }
144
+
145
+ function recoveredTurn(cache: SessionCache, turnId: string, order: number): RecoveredTurn {
146
+ const existing = cache.recoveredTurns.get(turnId);
147
+ if (existing) return existing;
148
+ const created: RecoveredTurn = {
149
+ id: turnId,
150
+ order,
151
+ status: "inProgress",
152
+ startedAt: null,
153
+ completedAt: null,
154
+ durationMs: null,
155
+ error: null,
156
+ collaborationMode: null,
157
+ items: new Map(),
158
+ canonicalItemEvents: false,
159
+ durableUserMessages: false,
160
+ };
161
+ cache.recoveredTurns.set(turnId, created);
162
+ cache.turnsInOrder.push(turnId);
163
+ return created;
164
+ }
165
+
166
+ function timestampSeconds(value: unknown): number | null {
167
+ const numeric = finiteNumber(value);
168
+ if (numeric !== null) {
169
+ return numeric > 10_000_000_000 ? numeric / 1_000 : numeric;
170
+ }
171
+ const parsed = Date.parse(stringValue(value));
172
+ return Number.isFinite(parsed) ? parsed / 1_000 : null;
173
+ }
174
+
175
+ function durationMilliseconds(value: unknown): number | null {
176
+ const direct = finiteNumber(value);
177
+ if (direct !== null) return Math.max(0, Math.round(direct));
178
+ const row = record(value);
179
+ if (!row) return null;
180
+ const seconds = finiteNumber(row.secs) ?? finiteNumber(row.seconds) ?? 0;
181
+ const nanos = finiteNumber(row.nanos) ?? finiteNumber(row.nanoseconds) ?? 0;
182
+ return Math.max(0, Math.round(seconds * 1_000 + nanos / 1_000_000));
183
+ }
184
+
185
+ function camelKey(value: string): string {
186
+ return value.replace(/_([a-z0-9])/gu, (_match, letter: string) => letter.toUpperCase());
187
+ }
188
+
189
+ function camelEnum(value: string): string {
190
+ return camelKey(value.toLowerCase());
191
+ }
192
+
193
+ function normalizedStatus(value: unknown, fallback = ""): string {
194
+ const status = stringValue(value).trim();
195
+ return status ? camelEnum(status) : fallback;
196
+ }
197
+
198
+ function deepCamelValue(value: unknown, depth = 0): unknown {
199
+ if (depth > 8) return null;
200
+ if (Array.isArray(value)) return value.slice(0, 256).map(entry => deepCamelValue(entry, depth + 1));
201
+ const row = record(value);
202
+ if (!row) return value;
203
+ const output: JsonRecord = {};
204
+ for (const [key, entry] of Object.entries(row).slice(0, 256)) {
205
+ output[camelKey(key)] = deepCamelValue(entry, depth + 1);
206
+ }
207
+ return output;
208
+ }
209
+
210
+ function localPath(value: unknown): string {
211
+ const path = stringValue(value).trim();
212
+ if (!path.toLowerCase().startsWith("file://")) return path;
213
+ try {
214
+ return fileURLToPath(path);
215
+ } catch {
216
+ return path;
217
+ }
218
+ }
219
+
220
+ function itemType(value: unknown): string {
221
+ const raw = stringValue(value).trim();
222
+ const known: Record<string, string> = {
223
+ AgentMessage: "agentMessage",
224
+ CollabAgentToolCall: "collabAgentToolCall",
225
+ CommandExecution: "commandExecution",
226
+ ContextCompaction: "contextCompaction",
227
+ DynamicToolCall: "dynamicToolCall",
228
+ EnteredReviewMode: "reviewMarker",
229
+ ExitedReviewMode: "reviewMarker",
230
+ FileChange: "fileChange",
231
+ ImageGeneration: "imageGeneration",
232
+ ImageView: "imageView",
233
+ McpToolCall: "mcpToolCall",
234
+ Plan: "plan",
235
+ Reasoning: "reasoning",
236
+ ReviewMarker: "reviewMarker",
237
+ SubAgentActivity: "subAgentActivity",
238
+ UserInputRequest: "userInputRequest",
239
+ UserMessage: "userMessage",
240
+ WebSearch: "webSearch",
241
+ };
242
+ return known[raw] ?? (raw ? `${raw.charAt(0).toLowerCase()}${raw.slice(1)}` : "");
243
+ }
244
+
245
+ function normalizedFileChanges(value: unknown): JsonRecord[] {
246
+ const changes = record(value);
247
+ if (changes) {
248
+ return Object.entries(changes).slice(0, 64).flatMap(([path, rawChange]) => {
249
+ const change = record(deepCamelValue(rawChange));
250
+ if (!path || !change) return [];
251
+ const kind = stringValue(change.type).trim() || "update";
252
+ const diff = stringValue(change.unifiedDiff).trim();
253
+ const movePath = stringValue(change.movePath).trim();
254
+ return [{
255
+ path,
256
+ kind: {
257
+ type: kind,
258
+ ...(movePath ? { movePath } : {}),
259
+ },
260
+ ...(diff ? { diff: bounded(diff, 1024 * 1024) } : {}),
261
+ }];
262
+ });
263
+ }
264
+ if (!Array.isArray(value)) return [];
265
+ return value.slice(0, 64).flatMap(rawChange => {
266
+ const change = record(deepCamelValue(rawChange));
267
+ return change ? [change] : [];
268
+ });
269
+ }
270
+
271
+ /**
272
+ * `item_completed` rollout events contain Codex's already-structured public
273
+ * ThreadItem snapshot, but use Rust's persisted enum/key spelling. Normalize
274
+ * that durable snapshot instead of trying to infer UI rows from raw tool JSON.
275
+ */
276
+ function normalizedCompletedItem(value: unknown, completed: boolean): JsonRecord | null {
277
+ const raw = record(value);
278
+ const type = itemType(raw?.type);
279
+ const id = stringValue(raw?.id).trim();
280
+ if (!raw || !type || !id) return null;
281
+ const common = record(deepCamelValue(raw)) ?? {};
282
+
283
+ if (type === "userMessage") {
284
+ return {
285
+ type,
286
+ id,
287
+ clientId: stringValue(raw.client_id ?? raw.clientId).trim() || null,
288
+ content: Array.isArray(raw.content)
289
+ ? raw.content.slice(0, 128).map(entry => deepCamelValue(entry))
290
+ : [],
291
+ };
292
+ }
293
+ if (type === "agentMessage") {
294
+ const content = Array.isArray(raw.content) ? raw.content : [];
295
+ const text = content.flatMap(entry => {
296
+ const row = record(entry);
297
+ const part = stringValue(row?.text);
298
+ return part ? [part] : [];
299
+ }).join("");
300
+ return {
301
+ type,
302
+ id,
303
+ text: bounded(text || stringValue(raw.text), 2 * 1024 * 1024),
304
+ phase: stringValue(raw.phase).trim() || null,
305
+ memoryCitation: deepCamelValue(raw.memory_citation ?? raw.memoryCitation) ?? null,
306
+ };
307
+ }
308
+ if (type === "reasoning") {
309
+ const summary = Array.isArray(raw.summary_text ?? raw.summary)
310
+ ? (raw.summary_text ?? raw.summary) as unknown[]
311
+ : [];
312
+ const content = Array.isArray(raw.raw_content ?? raw.content)
313
+ ? (raw.raw_content ?? raw.content) as unknown[]
314
+ : [];
315
+ return {
316
+ type,
317
+ id,
318
+ summary: summary.flatMap(entry => typeof entry === "string" ? [entry] : []),
319
+ content: content.flatMap(entry => typeof entry === "string" ? [entry] : []),
320
+ };
321
+ }
322
+ if (type === "commandExecution") {
323
+ const command = Array.isArray(raw.command)
324
+ ? raw.command.flatMap(entry => typeof entry === "string" ? [entry] : []).join(" ")
325
+ : stringValue(raw.command);
326
+ const rawCommandActions = raw.parsed_cmd ?? raw.commandActions;
327
+ const output = stringValue(raw.aggregated_output ?? raw.aggregatedOutput)
328
+ || [stringValue(raw.stdout), stringValue(raw.stderr)].filter(Boolean).join("\n");
329
+ return {
330
+ type,
331
+ id,
332
+ pluginId: stringValue(raw.plugin_id ?? raw.pluginId).trim() || null,
333
+ scriptPath: stringValue(raw.script_path ?? raw.scriptPath).trim() || null,
334
+ command: bounded(command, MAX_COMMAND_BYTES),
335
+ cwd: localPath(raw.cwd),
336
+ processId: stringValue(raw.process_id ?? raw.processId).trim() || null,
337
+ source: camelEnum(stringValue(raw.source)),
338
+ status: normalizedStatus(raw.status, completed ? "completed" : "inProgress"),
339
+ commandActions: Array.isArray(rawCommandActions)
340
+ ? rawCommandActions.slice(0, 128).map(entry => deepCamelValue(entry))
341
+ : [],
342
+ aggregatedOutput: bounded(output, MAX_OUTPUT_BYTES),
343
+ exitCode: finiteNumber(raw.exit_code ?? raw.exitCode),
344
+ durationMs: durationMilliseconds(raw.duration_ms ?? raw.durationMs ?? raw.duration),
345
+ };
346
+ }
347
+ if (type === "fileChange") {
348
+ return {
349
+ type,
350
+ id,
351
+ status: normalizedStatus(raw.status, completed ? "completed" : "inProgress"),
352
+ changes: normalizedFileChanges(raw.changes),
353
+ };
354
+ }
355
+ if (type === "imageView") {
356
+ return { type, id, path: localPath(raw.path) };
357
+ }
358
+ if (type === "contextCompaction") {
359
+ return { type, id, status: completed ? "completed" : "inProgress" };
360
+ }
361
+ if (type === "reviewMarker") {
362
+ return {
363
+ type,
364
+ id,
365
+ state: raw.type === "EnteredReviewMode"
366
+ ? "entered"
367
+ : raw.type === "ExitedReviewMode"
368
+ ? "exited"
369
+ : stringValue(raw.state).trim(),
370
+ };
371
+ }
372
+
373
+ const normalized: JsonRecord = {
374
+ ...common,
375
+ type,
376
+ id,
377
+ };
378
+ if ("status" in raw || ["dynamicToolCall", "mcpToolCall", "collabAgentToolCall", "imageGeneration", "userInputRequest"].includes(type)) {
379
+ normalized.status = normalizedStatus(raw.status, completed ? "completed" : "inProgress");
380
+ }
381
+ const durationMs = durationMilliseconds(raw.duration_ms ?? raw.durationMs ?? raw.duration);
382
+ if (durationMs !== null) normalized.durationMs = durationMs;
383
+ delete normalized.duration;
384
+ return normalized;
385
+ }
386
+
387
+ function rememberRecoveredTurnItem(
388
+ cache: SessionCache,
389
+ turnId: string,
390
+ order: number,
391
+ value: unknown,
392
+ completed: boolean,
393
+ ): void {
394
+ const item = normalizedCompletedItem(value, completed);
395
+ if (!item) return;
396
+ const turn = recoveredTurn(cache, turnId, order);
397
+ const id = stringValue(item.id);
398
+ const existing = turn.items.get(id);
399
+ turn.items.set(id, {
400
+ order: existing?.order ?? order,
401
+ item: {
402
+ ...(existing?.item ?? {}),
403
+ ...item,
404
+ },
405
+ });
406
+ turn.canonicalItemEvents ||= completed;
407
+ if (item.type === "userMessage") turn.durableUserMessages = true;
408
+ }
409
+
410
+ function responseMessageText(payload: JsonRecord): string {
411
+ if (!Array.isArray(payload.content)) return "";
412
+ return payload.content.flatMap(value => {
413
+ const row = record(value);
414
+ return row && typeof row.text === "string" ? [row.text] : [];
415
+ }).join("");
416
+ }
417
+
418
+ function responseUserMessageContent(payload: JsonRecord): JsonRecord[] {
419
+ if (!Array.isArray(payload.content)) return [];
420
+ return payload.content.flatMap(value => {
421
+ const row = record(value);
422
+ const text = publicUserMessageText(row?.text);
423
+ return text ? [{ type: "text", text }] : [];
424
+ });
425
+ }
426
+
427
+ function recoveredUserMessageText(value: unknown): string {
428
+ const item = record(value);
429
+ if (!item || item.type !== "userMessage" || !Array.isArray(item.content)) return "";
430
+ return canonicalUserMessageText(item.content.flatMap(partValue => {
431
+ const part = record(partValue);
432
+ const text = stringValue(part?.text);
433
+ return text ? [text] : [];
434
+ }).join(""));
435
+ }
436
+
437
+ function rememberPendingUserMessage(
438
+ cache: SessionCache,
439
+ pending: PendingDurableUserMessage,
440
+ clientId = "",
441
+ ): void {
442
+ const turn = recoveredTurn(cache, pending.turnId, pending.order);
443
+ const item: JsonRecord = {
444
+ ...pending.item,
445
+ ...(clientId ? { clientId } : {}),
446
+ };
447
+ const matchingEntry = clientId
448
+ ? [...turn.items.entries()].find(([, existing]) =>
449
+ existing.item.type === "userMessage"
450
+ && stringValue(existing.item.clientId ?? existing.item.client_id).trim() === clientId)
451
+ : undefined;
452
+ const key = matchingEntry?.[0] ?? stringValue(item.id);
453
+ const existing = matchingEntry?.[1] ?? turn.items.get(key);
454
+ turn.items.set(key, {
455
+ order: existing?.order ?? pending.order,
456
+ item: {
457
+ ...(existing?.item ?? {}),
458
+ ...item,
459
+ },
460
+ });
461
+ turn.durableUserMessages = true;
462
+ }
463
+
464
+ function outputText(value: unknown): string | null {
465
+ if (typeof value === "string") return value;
466
+ if (!Array.isArray(value)) return null;
467
+ const parts: string[] = [];
468
+ for (const entry of value) {
469
+ const row = record(entry);
470
+ if (!row) continue;
471
+ const text = stringValue(row.text);
472
+ if (text) parts.push(text);
473
+ }
474
+ return parts.length > 0 ? parts.join("") : null;
475
+ }
476
+
477
+ function parseCommandOutput(value: string): {
478
+ output: string;
479
+ exitCode: number | null;
480
+ durationMs: number | null;
481
+ } {
482
+ let exitCode: number | null = null;
483
+ let durationMs: number | null = null;
484
+ let pastHeader = false;
485
+ const output: string[] = [];
486
+ for (const line of value.replaceAll("\r\n", "\n").split("\n")) {
487
+ if (!pastHeader) {
488
+ const exit = /^(?:Process|Command|Script) (?:exited|failed) with (?:code|exit code) (-?\d+)$/u.exec(line);
489
+ if (exit?.[1]) {
490
+ exitCode = Number.parseInt(exit[1], 10);
491
+ continue;
492
+ }
493
+ const wall = /^Wall time:?\s+([\d.]+)\s+(?:seconds?|s)$/u.exec(line);
494
+ if (wall?.[1]) {
495
+ durationMs = Math.round(Number.parseFloat(wall[1]) * 1_000);
496
+ continue;
497
+ }
498
+ if (
499
+ line === "Script completed" ||
500
+ line.startsWith("Command:") ||
501
+ line.startsWith("Chunk ID:") ||
502
+ line.startsWith("Original token count:")
503
+ ) {
504
+ continue;
505
+ }
506
+ if (line === "Output:" || line === "Final output:") {
507
+ pastHeader = true;
508
+ continue;
509
+ }
510
+ }
511
+ output.push(line);
512
+ }
513
+ return {
514
+ output: bounded(output.join("\n").trimEnd(), MAX_OUTPUT_BYTES),
515
+ exitCode,
516
+ durationMs,
517
+ };
518
+ }
519
+
520
+ function commandFromFunctionCall(payload: JsonRecord): string | null {
521
+ const raw = stringValue(payload.arguments);
522
+ if (!raw) return null;
523
+ try {
524
+ const args = record(JSON.parse(raw));
525
+ const command = args?.command;
526
+ if (typeof command === "string" && command.trim()) return command;
527
+ if (Array.isArray(command)) {
528
+ const joined = command.filter(value => typeof value === "string").join(" ").trim();
529
+ if (joined) return joined;
530
+ }
531
+ for (const key of ["cmd", "script", "input"] as const) {
532
+ const value = stringValue(args?.[key]).trim();
533
+ if (value) return value;
534
+ }
535
+ } catch {
536
+ return raw;
537
+ }
538
+ return raw;
539
+ }
540
+
541
+ function customExecCommand(payload: JsonRecord): string | null {
542
+ const input = stringValue(payload.input);
543
+ if (!input || !/\btools\.exec_command\s*\(/u.test(input)) return null;
544
+ return input;
545
+ }
546
+
547
+ function responseIdentity(payload: JsonRecord): string[] {
548
+ return [stringValue(payload.id), stringValue(payload.call_id)].filter(Boolean);
549
+ }
550
+
551
+ function consumeRecord(
552
+ parsed: JsonRecord,
553
+ order: number,
554
+ state: { currentTurnId: string | null; cache: SessionCache },
555
+ ): void {
556
+ const timestamp = timestampSeconds(parsed.timestamp);
557
+ if (timestamp !== null) {
558
+ state.cache.firstTimestamp = state.cache.firstTimestamp === null
559
+ ? timestamp
560
+ : Math.min(state.cache.firstTimestamp, timestamp);
561
+ state.cache.lastTimestamp = state.cache.lastTimestamp === null
562
+ ? timestamp
563
+ : Math.max(state.cache.lastTimestamp, timestamp);
564
+ }
565
+ const payload = record(parsed.payload);
566
+ if (!payload) return;
567
+ const rowType = stringValue(parsed.type);
568
+ const payloadType = stringValue(payload.type);
569
+ const userMessageIdentityEvent =
570
+ rowType === "event_msg" && payloadType === "user_message";
571
+ if (state.cache.pendingUserMessage) {
572
+ const pending = state.cache.pendingUserMessage;
573
+ const completedUserItem = rowType === "event_msg"
574
+ && (payloadType === "item_started" || payloadType === "item_completed")
575
+ ? normalizedCompletedItem(payload.item, payloadType === "item_completed")
576
+ : null;
577
+ const completedUserTurnId = stringValue(payload.turn_id) || state.currentTurnId;
578
+ if (
579
+ completedUserItem?.type === "userMessage"
580
+ && completedUserTurnId === pending.turnId
581
+ && recoveredUserMessageText(completedUserItem) === pending.comparisonText
582
+ ) {
583
+ // Current Codex rollouts persist one logical phone prompt twice: first
584
+ // as the native response item, then as the structured ThreadItem that
585
+ // carries the Android client id. They are adjacent representations of
586
+ // one message, not two chat bubbles. Preserve the earlier ordering while
587
+ // letting the structured item provide the stable visible identity.
588
+ state.cache.pendingUserMessage = null;
589
+ rememberRecoveredTurnItem(
590
+ state.cache,
591
+ pending.turnId,
592
+ pending.order,
593
+ payload.item,
594
+ payloadType === "item_completed",
595
+ );
596
+ return;
597
+ }
598
+ const clientId = userMessageIdentityEvent
599
+ ? stringValue(payload.client_id ?? payload.clientId).trim()
600
+ : "";
601
+ const comparisonText = userMessageIdentityEvent
602
+ ? canonicalUserMessageText(payload.message)
603
+ : "";
604
+ if (
605
+ userMessageIdentityEvent
606
+ && clientId
607
+ && comparisonText === pending.comparisonText
608
+ ) {
609
+ state.cache.pendingUserMessage = null;
610
+ rememberPendingUserMessage(state.cache, pending, clientId);
611
+ return;
612
+ }
613
+ state.cache.pendingUserMessage = null;
614
+ rememberPendingUserMessage(state.cache, pending);
615
+ }
616
+ // The identity event enriches the immediately preceding response item. It
617
+ // never represents another standalone Android message.
618
+ if (userMessageIdentityEvent) return;
619
+ if (rowType === "session_meta") {
620
+ const id = stringValue(payload.id) || stringValue(payload.session_id);
621
+ if (id && state.cache.threadId === null) state.cache.threadId = id;
622
+ return;
623
+ }
624
+ if (rowType === "turn_context") {
625
+ state.currentTurnId = stringValue(payload.turn_id) || null;
626
+ state.cache.currentTurnId = state.currentTurnId;
627
+ if (state.currentTurnId) {
628
+ const turn = recoveredTurn(state.cache, state.currentTurnId, order);
629
+ const collaborationMode = record(deepCamelValue(payload.collaboration_mode));
630
+ if (collaborationMode) turn.collaborationMode = collaborationMode;
631
+ }
632
+ return;
633
+ }
634
+ if (rowType === "event_msg") {
635
+ const event = stringValue(payload.type);
636
+ if (event === "thread_settings_applied") {
637
+ const settings = record(payload.thread_settings);
638
+ state.cache.nextTurnPlanMode =
639
+ stringValue(record(settings?.collaboration_mode)?.mode) === "plan";
640
+ return;
641
+ }
642
+ if (event === "task_started") {
643
+ state.currentTurnId = stringValue(payload.turn_id) || state.currentTurnId;
644
+ state.cache.currentTurnId = state.currentTurnId;
645
+ if (state.currentTurnId) {
646
+ const turn = recoveredTurn(state.cache, state.currentTurnId, order);
647
+ turn.status = "inProgress";
648
+ turn.startedAt = timestampSeconds(payload.started_at)
649
+ ?? timestampSeconds(parsed.timestamp)
650
+ ?? turn.startedAt;
651
+ if (
652
+ stringValue(payload.collaboration_mode_kind) === "plan"
653
+ || state.cache.nextTurnPlanMode
654
+ ) {
655
+ turn.collaborationMode = { mode: "plan" };
656
+ }
657
+ }
658
+ state.cache.nextTurnPlanMode = false;
659
+ return;
660
+ }
661
+ if (event === "task_complete" || event === "turn_aborted") {
662
+ const turnId = stringValue(payload.turn_id) || state.currentTurnId;
663
+ if (turnId) {
664
+ const turn = recoveredTurn(state.cache, turnId, order);
665
+ const error = record(deepCamelValue(payload.error));
666
+ turn.status = event === "turn_aborted"
667
+ ? "interrupted"
668
+ : error
669
+ ? "failed"
670
+ : "completed";
671
+ turn.error = error;
672
+ turn.startedAt = timestampSeconds(payload.started_at) ?? turn.startedAt;
673
+ turn.completedAt = timestampSeconds(payload.completed_at)
674
+ ?? timestampSeconds(parsed.timestamp)
675
+ ?? turn.completedAt;
676
+ turn.durationMs = durationMilliseconds(payload.duration_ms) ?? turn.durationMs;
677
+ }
678
+ if (!turnId || state.currentTurnId === turnId) {
679
+ state.currentTurnId = null;
680
+ state.cache.currentTurnId = null;
681
+ }
682
+ return;
683
+ }
684
+ if (event === "item_started" || event === "item_completed") {
685
+ const turnId = stringValue(payload.turn_id) || state.currentTurnId;
686
+ if (turnId) {
687
+ rememberRecoveredTurnItem(
688
+ state.cache,
689
+ turnId,
690
+ order,
691
+ payload.item,
692
+ event === "item_completed",
693
+ );
694
+ }
695
+ return;
696
+ }
697
+ if (event === "thread_rolled_back") {
698
+ const count = Math.max(0, Math.floor(finiteNumber(payload.num_turns) ?? 0));
699
+ const removed = state.cache.turnsInOrder.splice(Math.max(0, state.cache.turnsInOrder.length - count));
700
+ for (const turnId of removed) {
701
+ state.cache.recoveredTurns.delete(turnId);
702
+ state.cache.turns.delete(turnId);
703
+ }
704
+ state.currentTurnId = null;
705
+ state.cache.currentTurnId = null;
706
+ return;
707
+ }
708
+ if (event === "error" && state.currentTurnId) {
709
+ const turn = recoveredTurn(state.cache, state.currentTurnId, order);
710
+ turn.status = "failed";
711
+ turn.error = {
712
+ message: stringValue(payload.message) || "Codex task failed",
713
+ };
714
+ }
715
+ }
716
+ if (rowType === "compacted" && state.currentTurnId) {
717
+ const recovery = turnRecovery(state.cache.turns, state.currentTurnId);
718
+ const itemId = `context-compaction-${state.currentTurnId}`;
719
+ recovery.activities.set(itemId, {
720
+ callId: itemId,
721
+ itemId,
722
+ order,
723
+ item: { type: "contextCompaction", id: itemId, status: "inProgress" },
724
+ });
725
+ return;
726
+ }
727
+ if (rowType === "event_msg" && state.currentTurnId) {
728
+ const recovery = turnRecovery(state.cache.turns, state.currentTurnId);
729
+ const callId = stringValue(payload.call_id);
730
+ if (payload.type === "context_compacted") {
731
+ const itemId = `context-compaction-${state.currentTurnId}`;
732
+ const existing = recovery.activities.get(itemId);
733
+ recovery.activities.set(itemId, {
734
+ callId: itemId,
735
+ itemId,
736
+ order: existing?.order ?? order,
737
+ item: { type: "contextCompaction", id: itemId, status: "completed" },
738
+ });
739
+ } else if (payload.type === "mcp_tool_call_end") {
740
+ recovery.anchors.push({ type: "mcpToolCall", order });
741
+ if (callId) recovery.responseOrder.set(callId, order);
742
+ } else if (payload.type === "patch_apply_end") {
743
+ recovery.anchors.push({ type: "fileChange", order });
744
+ if (callId) recovery.responseOrder.set(callId, order);
745
+ }
746
+ return;
747
+ }
748
+ if (rowType === "response_item" && payloadType === "message" && payload.role === "user") {
749
+ const metadata = record(payload.internal_chat_message_metadata_passthrough);
750
+ const turnId = stringValue(metadata?.turn_id) || state.currentTurnId;
751
+ const id = stringValue(payload.id).trim();
752
+ const rawText = responseMessageText(payload);
753
+ if (!turnId || !id || !rawText) return;
754
+ state.currentTurnId = turnId;
755
+ state.cache.currentTurnId = turnId;
756
+ const recovery = turnRecovery(state.cache.turns, turnId);
757
+ recovery.anchors.push({ type: "userMessage", order });
758
+ recovery.responseOrder.set(id, order);
759
+ state.cache.pendingUserMessage = {
760
+ turnId,
761
+ order,
762
+ item: {
763
+ type: "userMessage",
764
+ id,
765
+ content: responseUserMessageContent(payload),
766
+ },
767
+ comparisonText: canonicalUserMessageText(rawText),
768
+ };
769
+ return;
770
+ }
771
+ if (rowType !== "response_item" || !state.currentTurnId) return;
772
+
773
+ const recovery = turnRecovery(state.cache.turns, state.currentTurnId);
774
+ for (const id of responseIdentity(payload)) {
775
+ if (!recovery.responseOrder.has(id)) recovery.responseOrder.set(id, order);
776
+ }
777
+ const type = payloadType;
778
+ const name = stringValue(payload.name);
779
+ if (type === "reasoning") {
780
+ recovery.anchors.push({ type: "reasoning", order });
781
+ } else if (type === "message") {
782
+ const role = stringValue(payload.role);
783
+ if (role === "user") recovery.anchors.push({ type: "userMessage", order });
784
+ else if (role === "assistant") recovery.anchors.push({ type: "agentMessage", order });
785
+ }
786
+ const callId = stringValue(payload.call_id) || stringValue(payload.id);
787
+ if (!callId) return;
788
+
789
+ const planUpdate = planUpdateFromToolCall(payload);
790
+ if (planUpdate) {
791
+ const itemId = `turn-plan-${state.currentTurnId}`;
792
+ recovery.activities.set(itemId, {
793
+ callId,
794
+ itemId,
795
+ order,
796
+ item: {
797
+ type: "planUpdate",
798
+ id: itemId,
799
+ status: "completed",
800
+ plan: planUpdate.plan,
801
+ ...(planUpdate.explanation ? { explanation: planUpdate.explanation } : {}),
802
+ },
803
+ });
804
+ return;
805
+ }
806
+
807
+ const fileChanges = fileChangesFromToolCall(payload);
808
+ if (fileChanges.length > 0) {
809
+ if (!recovery.fileChanges.has(callId)) {
810
+ recovery.fileChanges.set(callId, {
811
+ callId,
812
+ itemId: stringValue(payload.id) || callId,
813
+ order,
814
+ status: stringValue(payload.status) || "inProgress",
815
+ changes: fileChanges,
816
+ });
817
+ }
818
+ return;
819
+ }
820
+
821
+ let command: string | null = null;
822
+ if (type === "function_call" && ["exec_command", "shell", "shell_command"].includes(name)) {
823
+ command = commandFromFunctionCall(payload);
824
+ } else if (type === "custom_tool_call" && name === "exec") {
825
+ // New Codex sessions wrap every tool through a generic `exec` record.
826
+ // Only the wrappers that actually call exec_command are terminal rows;
827
+ // MCP calls and other nested tools must keep their own activity type.
828
+ command = customExecCommand(payload);
829
+ }
830
+ if (command) {
831
+ if (!recovery.commands.has(callId)) {
832
+ recovery.commands.set(callId, {
833
+ callId,
834
+ itemId: stringValue(payload.id) || callId,
835
+ order,
836
+ command: bounded(command, MAX_COMMAND_BYTES),
837
+ status: stringValue(payload.status) || "inProgress",
838
+ output: null,
839
+ exitCode: null,
840
+ durationMs: null,
841
+ });
842
+ }
843
+ return;
844
+ }
845
+
846
+ if (type === "function_call" || type === "custom_tool_call") {
847
+ const recoveredTool = toolItem(payload);
848
+ if (recoveredTool?.type === "dynamicToolCall") {
849
+ const recoveredToolName = stringValue(recoveredTool.tool);
850
+ // MCP calls already have a durable first-class `mcpToolCall` item in
851
+ // thread/read. Replaying their outer JavaScript router would duplicate
852
+ // one visible action under a second generic tool id.
853
+ if (!recoveredToolName.startsWith("mcp__")) {
854
+ const toolToken = recoveredToolName.split("__").at(-1);
855
+ const item = toolToken === "request_user_input"
856
+ ? {
857
+ type: "userInputRequest",
858
+ id: stringValue(recoveredTool.id) || callId,
859
+ status: "inProgress",
860
+ questions: record(recoveredTool.arguments)?.questions,
861
+ }
862
+ : recoveredTool;
863
+ recovery.activities.set(callId, {
864
+ callId,
865
+ itemId: stringValue(item.id) || callId,
866
+ order,
867
+ item,
868
+ });
869
+ }
870
+ }
871
+ return;
872
+ }
873
+
874
+ if (type !== "function_call_output" && type !== "custom_tool_call_output") return;
875
+ const rawOutput = outputText(payload.output);
876
+ const output = rawOutput === null ? null : parseCommandOutput(rawOutput);
877
+ const failed = typeof output?.exitCode === "number" && output.exitCode !== 0;
878
+ const recoveredActivity = recovery.activities.get(callId);
879
+ if (recoveredActivity) {
880
+ recoveredActivity.item = {
881
+ ...recoveredActivity.item,
882
+ status: failed ? "failed" : "completed",
883
+ };
884
+ }
885
+ const recoveredFileChange = recovery.fileChanges.get(callId);
886
+ if (recoveredFileChange) {
887
+ recoveredFileChange.status = failed ? "failed" : "completed";
888
+ }
889
+ const recovered = recovery.commands.get(callId);
890
+ if (!recovered || !output) return;
891
+ recovered.output = output.output;
892
+ recovered.exitCode = output.exitCode;
893
+ recovered.durationMs = output.durationMs;
894
+ recovered.status = failed ? "failed" : "completed";
895
+ }
896
+
897
+ async function scanSession(cache: SessionCache, startOffset: number): Promise<number> {
898
+ const decoder = new TextDecoder();
899
+ const stream = Bun.file(cache.canonicalPath).slice(startOffset).stream();
900
+ const reader = stream.getReader();
901
+ let absoluteOffset = startOffset;
902
+ let lineStart = startOffset;
903
+ let parts: Uint8Array[] = [];
904
+ let lineBytes = 0;
905
+ let oversized = false;
906
+ const state = { currentTurnId: cache.currentTurnId, cache };
907
+
908
+ const append = (bytes: Uint8Array) => {
909
+ lineBytes += bytes.byteLength;
910
+ if (lineBytes > MAX_RECORD_BYTES) {
911
+ oversized = true;
912
+ parts = [];
913
+ } else if (!oversized && bytes.byteLength > 0) {
914
+ parts.push(bytes.slice());
915
+ }
916
+ };
917
+
918
+ const emit = (nextOffset: number) => {
919
+ if (!oversized && lineBytes > 0) {
920
+ const joined = new Uint8Array(lineBytes);
921
+ let offset = 0;
922
+ for (const part of parts) {
923
+ joined.set(part, offset);
924
+ offset += part.byteLength;
925
+ }
926
+ try {
927
+ const parsed = record(JSON.parse(decoder.decode(joined)));
928
+ if (parsed) consumeRecord(parsed, lineStart, state);
929
+ } catch {
930
+ // A malformed or concurrently incomplete line is ignored. The next
931
+ // refresh rescans it because completeOffset advances only at newline.
932
+ }
933
+ }
934
+ lineStart = nextOffset;
935
+ parts = [];
936
+ lineBytes = 0;
937
+ oversized = false;
938
+ };
939
+
940
+ try {
941
+ for (;;) {
942
+ const { value, done } = await reader.read();
943
+ if (done) break;
944
+ const chunk = value instanceof Uint8Array ? value : new Uint8Array(value);
945
+ let segmentStart = 0;
946
+ for (let index = 0; index < chunk.byteLength; index += 1) {
947
+ if (chunk[index] !== 0x0a) continue;
948
+ append(chunk.subarray(segmentStart, index));
949
+ absoluteOffset += index - segmentStart + 1;
950
+ emit(absoluteOffset);
951
+ segmentStart = index + 1;
952
+ }
953
+ append(chunk.subarray(segmentStart));
954
+ absoluteOffset += chunk.byteLength - segmentStart;
955
+ }
956
+ } finally {
957
+ reader.releaseLock();
958
+ }
959
+ return lineStart;
960
+ }
961
+
962
+ function inside(root: string, candidate: string): boolean {
963
+ const path = relative(root, candidate);
964
+ return path === "" || (path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path));
965
+ }
966
+
967
+ function itemId(item: JsonRecord): string {
968
+ return stringValue(item.id) || stringValue(item.callId) || stringValue(item.call_id);
969
+ }
970
+
971
+ function isUserMessageItem(item: JsonRecord): boolean {
972
+ const type = itemType(item.type);
973
+ return type === "userMessage"
974
+ || (type === "message" && stringValue(item.role).toLowerCase() === "user");
975
+ }
976
+
977
+ function userMessageClientId(item: JsonRecord): string {
978
+ return stringValue(
979
+ item.clientId
980
+ ?? item.client_id
981
+ ?? item.clientUserMessageId
982
+ ?? item.client_user_message_id,
983
+ ).trim();
984
+ }
985
+
986
+ function commandText(item: JsonRecord): string {
987
+ return stringValue(item.command).trim();
988
+ }
989
+
990
+ function terminalTurnStatus(value: unknown): boolean {
991
+ return ["completed", "interrupted", "failed"].includes(stringValue(value));
992
+ }
993
+
994
+ function mergeRecoveredTurnItems(
995
+ existingValue: unknown,
996
+ recovered: RecoveredTurn,
997
+ ): JsonRecord[] {
998
+ const existing = Array.isArray(existingValue)
999
+ ? existingValue.flatMap(value => record(value) ? [record(value)!] : [])
1000
+ : [];
1001
+ if (recovered.items.size === 0) return existing;
1002
+ const unmatchedExisting = new Set(existing);
1003
+ const recoveredItems = [...recovered.items.values()]
1004
+ .sort((left, right) => left.order - right.order)
1005
+ .map(({ item }) => {
1006
+ const clientId = isUserMessageItem(item) ? userMessageClientId(item) : "";
1007
+ const current = clientId
1008
+ ? existing.find(candidate =>
1009
+ unmatchedExisting.has(candidate)
1010
+ && isUserMessageItem(candidate)
1011
+ && userMessageClientId(candidate) === clientId)
1012
+ : existing.find(candidate =>
1013
+ unmatchedExisting.has(candidate) && itemId(candidate) === itemId(item));
1014
+ if (current) unmatchedExisting.delete(current);
1015
+ return current ? { ...current, ...item } : item;
1016
+ });
1017
+ // Durable rollout user-message records are authoritative. Once at least one
1018
+ // exists for a turn, unmatched app-server-only user previews are discarded
1019
+ // instead of becoming permanent duplicate/failed prompt bubbles.
1020
+ return [
1021
+ ...recoveredItems,
1022
+ ...existing.filter(item =>
1023
+ unmatchedExisting.has(item)
1024
+ && (!recovered.durableUserMessages || !isUserMessageItem(item))),
1025
+ ];
1026
+ }
1027
+
1028
+ function mergeRecoveredTurns(thread: JsonRecord, cache: SessionCache): JsonRecord {
1029
+ if (cache.threadId !== stringValue(thread.id) || cache.recoveredTurns.size === 0) return thread;
1030
+ const existing = Array.isArray(thread.turns)
1031
+ ? thread.turns.flatMap(value => record(value) ? [record(value)!] : [])
1032
+ : [];
1033
+ const existingById = new Map(existing.map(turn => [stringValue(turn.id), turn]));
1034
+ const merged = [...existing];
1035
+ let changed = false;
1036
+
1037
+ for (const recovered of cache.recoveredTurns.values()) {
1038
+ const current = existingById.get(recovered.id);
1039
+ const currentStatus = stringValue(current?.status);
1040
+ const status = terminalTurnStatus(recovered.status)
1041
+ ? recovered.status
1042
+ : terminalTurnStatus(currentStatus)
1043
+ ? currentStatus
1044
+ : recovered.status;
1045
+ const next: JsonRecord = {
1046
+ ...(current ?? {}),
1047
+ id: recovered.id,
1048
+ status,
1049
+ startedAt: recovered.startedAt ?? finiteNumber(current?.startedAt),
1050
+ completedAt: terminalTurnStatus(status)
1051
+ ? recovered.completedAt ?? finiteNumber(current?.completedAt)
1052
+ : null,
1053
+ ...(recovered.durationMs !== null
1054
+ ? { durationMs: recovered.durationMs }
1055
+ : current?.durationMs === undefined
1056
+ ? {}
1057
+ : { durationMs: current.durationMs }),
1058
+ ...(recovered.error
1059
+ ? { error: recovered.error }
1060
+ : current?.error === undefined
1061
+ ? {}
1062
+ : { error: current.error }),
1063
+ ...(recovered.collaborationMode
1064
+ ? { collaborationMode: recovered.collaborationMode }
1065
+ : current?.collaborationMode === undefined
1066
+ ? {}
1067
+ : { collaborationMode: current.collaborationMode }),
1068
+ items: mergeRecoveredTurnItems(current?.items, recovered),
1069
+ _androidRemoteRecoveredOrder: recovered.order,
1070
+ };
1071
+ if (current) {
1072
+ const index = merged.indexOf(current);
1073
+ if (index >= 0) merged[index] = next;
1074
+ } else {
1075
+ merged.push(next);
1076
+ }
1077
+ changed = true;
1078
+ }
1079
+
1080
+ if (!changed) return thread;
1081
+ const ordered = merged.sort((left, right) => {
1082
+ const leftStarted = finiteNumber(left.startedAt);
1083
+ const rightStarted = finiteNumber(right.startedAt);
1084
+ if (leftStarted !== null && rightStarted !== null && leftStarted !== rightStarted) {
1085
+ return leftStarted - rightStarted;
1086
+ }
1087
+ if (leftStarted !== null) return -1;
1088
+ if (rightStarted !== null) return 1;
1089
+ return (finiteNumber(left._androidRemoteRecoveredOrder) ?? Number.MAX_SAFE_INTEGER)
1090
+ - (finiteNumber(right._androidRemoteRecoveredOrder) ?? Number.MAX_SAFE_INTEGER);
1091
+ }).map(turn => {
1092
+ if (!("_androidRemoteRecoveredOrder" in turn)) return turn;
1093
+ const { _androidRemoteRecoveredOrder: _order, ...publicTurn } = turn;
1094
+ return publicTurn;
1095
+ });
1096
+ const latestStartedAt = Math.max(
1097
+ ...ordered.map(turn => finiteNumber(turn.startedAt) ?? Number.NEGATIVE_INFINITY),
1098
+ );
1099
+ const latestCompletedAt = Math.max(
1100
+ ...ordered.map(turn => finiteNumber(turn.completedAt) ?? Number.NEGATIVE_INFINITY),
1101
+ );
1102
+ const latestActivityAt = Math.max(latestStartedAt, latestCompletedAt);
1103
+ return {
1104
+ ...thread,
1105
+ turns: ordered,
1106
+ ...(Number.isFinite(latestActivityAt)
1107
+ ? {
1108
+ updatedAt: Math.max(
1109
+ finiteNumber(thread.updatedAt) ?? Number.NEGATIVE_INFINITY,
1110
+ latestActivityAt,
1111
+ ),
1112
+ }
1113
+ : {}),
1114
+ };
1115
+ }
1116
+
1117
+ function mergeRecoveredCommands(thread: JsonRecord, cache: SessionCache): JsonRecord {
1118
+ if (cache.threadId !== stringValue(thread.id)) return thread;
1119
+ const turns = Array.isArray(thread.turns) ? thread.turns : [];
1120
+ let changed = false;
1121
+ const mergedTurns = turns.map(value => {
1122
+ const turn = record(value);
1123
+ if (!turn) return value;
1124
+ const recovery = cache.turns.get(stringValue(turn.id));
1125
+ if (cache.recoveredTurns.get(stringValue(turn.id))?.canonicalItemEvents) return value;
1126
+ if (
1127
+ !recovery ||
1128
+ (
1129
+ recovery.commands.size === 0 &&
1130
+ recovery.fileChanges.size === 0 &&
1131
+ recovery.activities.size === 0
1132
+ )
1133
+ ) return value;
1134
+ const items = Array.isArray(turn.items) ? turn.items : [];
1135
+ const originalItems = items.flatMap(value => record(value) ? [record(value)!] : []);
1136
+ const recoveredFileChanges = [...recovery.fileChanges.values()];
1137
+ const usedFileChanges = new Set<string>();
1138
+ let hydratedFileChanges = false;
1139
+ const existingItems = originalItems.map(item => {
1140
+ if (item.type !== "fileChange" || recoveredFileChanges.length === 0) return item;
1141
+ const id = itemId(item);
1142
+ const recovered = recoveredFileChanges.find(candidate =>
1143
+ !usedFileChanges.has(candidate.callId) && (candidate.itemId === id || candidate.callId === id),
1144
+ ) ?? recoveredFileChanges.find(candidate => !usedFileChanges.has(candidate.callId));
1145
+ if (!recovered) return item;
1146
+ usedFileChanges.add(recovered.callId);
1147
+ if (Array.isArray(item.changes) && item.changes.length > 0) return item;
1148
+ changed = true;
1149
+ hydratedFileChanges = true;
1150
+ return { ...item, changes: recovered.changes };
1151
+ });
1152
+ const existingIds = new Set(existingItems.map(itemId).filter(Boolean));
1153
+ const existingCallIds = new Set(
1154
+ existingItems.flatMap(item => {
1155
+ const callId = stringValue(item.callId) || stringValue(item.call_id);
1156
+ return callId ? [callId] : [];
1157
+ }),
1158
+ );
1159
+ const existingCommands = new Set(
1160
+ existingItems.filter(item => item.type === "commandExecution").map(commandText).filter(Boolean),
1161
+ );
1162
+ const recoveredCommandItems = [...recovery.commands.values()].flatMap(command => {
1163
+ if (existingIds.has(command.itemId) || existingIds.has(command.callId)) return [];
1164
+ if (existingCommands.has(command.command.trim())) return [];
1165
+ return [{
1166
+ type: "commandExecution",
1167
+ id: command.itemId,
1168
+ command: command.command,
1169
+ cwd: stringValue(thread.cwd),
1170
+ status: command.status,
1171
+ exitCode: command.exitCode,
1172
+ aggregatedOutput: command.output,
1173
+ durationMs: command.durationMs,
1174
+ processId: null,
1175
+ commandActions: [],
1176
+ _androidRemoteRecoveredOrder: command.order,
1177
+ }];
1178
+ });
1179
+ const recoveredFileItems = recoveredFileChanges.flatMap(fileChange => {
1180
+ if (usedFileChanges.has(fileChange.callId)) return [];
1181
+ if (existingIds.has(fileChange.itemId) || existingIds.has(fileChange.callId)) return [];
1182
+ return [{
1183
+ type: "fileChange",
1184
+ id: fileChange.itemId,
1185
+ status: fileChange.status,
1186
+ changes: fileChange.changes,
1187
+ _androidRemoteRecoveredOrder: fileChange.order,
1188
+ }];
1189
+ });
1190
+ const recoveredActivityItems = [...recovery.activities.values()].flatMap(activity => {
1191
+ if (
1192
+ existingIds.has(activity.itemId) ||
1193
+ existingIds.has(activity.callId) ||
1194
+ existingCallIds.has(activity.callId)
1195
+ ) return [];
1196
+ if (
1197
+ activity.item.type === "contextCompaction" &&
1198
+ existingItems.some(item => item.type === "contextCompaction")
1199
+ ) return [];
1200
+ if (
1201
+ activity.item.type === "planUpdate" &&
1202
+ existingItems.some(item => item.type === "planUpdate")
1203
+ ) return [];
1204
+ return [{ ...activity.item, _androidRemoteRecoveredOrder: activity.order }];
1205
+ });
1206
+ const recoveredItems = [
1207
+ ...recoveredCommandItems,
1208
+ ...recoveredFileItems,
1209
+ ...recoveredActivityItems,
1210
+ ];
1211
+ if (recoveredItems.length === 0) {
1212
+ return hydratedFileChanges ? { ...turn, items: existingItems } : value;
1213
+ }
1214
+ changed = true;
1215
+ let anchorIndex = 0;
1216
+ const positioned = existingItems.map((item, index) => {
1217
+ let order = recovery.responseOrder.get(itemId(item)) ?? null;
1218
+ if (
1219
+ order === null &&
1220
+ (
1221
+ item.type === "userMessage" ||
1222
+ item.type === "agentMessage" ||
1223
+ item.type === "reasoning" ||
1224
+ item.type === "mcpToolCall" ||
1225
+ item.type === "fileChange"
1226
+ )
1227
+ ) {
1228
+ for (; anchorIndex < recovery.anchors.length; anchorIndex += 1) {
1229
+ const anchor = recovery.anchors[anchorIndex];
1230
+ if (anchor?.type !== item.type) continue;
1231
+ order = anchor.order;
1232
+ anchorIndex += 1;
1233
+ break;
1234
+ }
1235
+ }
1236
+ return { item, index, order };
1237
+ });
1238
+ for (const item of recoveredItems) {
1239
+ const order = typeof item._androidRemoteRecoveredOrder === "number"
1240
+ ? item._androidRemoteRecoveredOrder
1241
+ : Number.MAX_SAFE_INTEGER;
1242
+ const nextKnown = positioned.findIndex(entry => entry.order !== null && entry.order > order);
1243
+ if (nextKnown >= 0) {
1244
+ positioned.splice(nextKnown, 0, { item, index: positioned.length, order });
1245
+ continue;
1246
+ }
1247
+ let insertion = positioned.length;
1248
+ for (let index = positioned.length - 1; index >= 0; index -= 1) {
1249
+ const candidateOrder = positioned[index]?.order ?? null;
1250
+ if (candidateOrder !== null && candidateOrder <= order) {
1251
+ insertion = index + 1;
1252
+ while (
1253
+ insertion < positioned.length &&
1254
+ positioned[insertion]?.order !== null &&
1255
+ (positioned[insertion]?.order ?? Number.MAX_SAFE_INTEGER) <= order
1256
+ ) {
1257
+ insertion += 1;
1258
+ }
1259
+ break;
1260
+ }
1261
+ }
1262
+ positioned.splice(insertion, 0, { item, index: positioned.length, order });
1263
+ }
1264
+ return {
1265
+ ...turn,
1266
+ items: positioned.map(({ item }) => {
1267
+ if (!("_androidRemoteRecoveredOrder" in item)) return item;
1268
+ const { _androidRemoteRecoveredOrder: _order, ...publicItem } = item;
1269
+ return publicItem;
1270
+ }),
1271
+ };
1272
+ });
1273
+ return changed ? { ...thread, turns: mergedTurns } : thread;
1274
+ }
1275
+
1276
+ export class AndroidRemoteSessionCommandRecovery implements AndroidRemoteCommandRecovery {
1277
+ private readonly caches = new Map<string, SessionCache>();
1278
+ private readonly codexHome: string;
1279
+ private readonly now: () => number;
1280
+
1281
+ constructor(options: AndroidRemoteSessionCommandRecoveryOptions = {}) {
1282
+ this.codexHome = options.codexHome ?? getCodexHome();
1283
+ this.now = options.now ?? Date.now;
1284
+ }
1285
+
1286
+ clear(): void {
1287
+ this.caches.clear();
1288
+ }
1289
+
1290
+ async enrichThread(thread: JsonRecord, sourcePaths: readonly string[] = []): Promise<JsonRecord> {
1291
+ const threadId = stringValue(thread.id).trim();
1292
+ if (!threadId) return thread;
1293
+ try {
1294
+ const candidates = await this.sourcePaths(threadId, [
1295
+ stringValue(thread.path),
1296
+ ...sourcePaths,
1297
+ ]);
1298
+ const caches: SessionCache[] = [];
1299
+ for (const sourcePath of candidates) {
1300
+ try {
1301
+ const cache = await this.updateCache(sourcePath);
1302
+ if (cache?.threadId === threadId) caches.push(cache);
1303
+ } catch {
1304
+ // A continuation can be archived or rotated while the sibling set is
1305
+ // being traversed. Keep every other readable rollout in the merge.
1306
+ }
1307
+ }
1308
+ caches.sort((left, right) =>
1309
+ (left.firstTimestamp ?? Number.MAX_SAFE_INTEGER)
1310
+ - (right.firstTimestamp ?? Number.MAX_SAFE_INTEGER)
1311
+ || left.canonicalPath.localeCompare(right.canonicalPath));
1312
+ let enriched = thread;
1313
+ for (const cache of caches) enriched = mergeRecoveredTurns(enriched, cache);
1314
+ for (const cache of caches) enriched = mergeRecoveredCommands(enriched, cache);
1315
+ this.prune();
1316
+ return enriched;
1317
+ } catch {
1318
+ // Session enrichment is optional. A missing, moving, or unreadable file
1319
+ // must never stop Android from receiving Codex's normal thread result.
1320
+ return thread;
1321
+ }
1322
+ }
1323
+
1324
+ private async sourcePaths(threadId: string, explicit: readonly string[]): Promise<string[]> {
1325
+ return resolveThreadSourcePaths(threadId, explicit, {
1326
+ codexHome: this.codexHome,
1327
+ now: this.now,
1328
+ });
1329
+ }
1330
+
1331
+ private async updateCache(sourcePath: string): Promise<SessionCache | null> {
1332
+ if (!sourcePath.toLowerCase().endsWith(".jsonl")) return null;
1333
+ const [canonicalHome, canonicalPath] = await Promise.all([
1334
+ realpath(resolve(this.codexHome)),
1335
+ realpath(resolve(sourcePath)),
1336
+ ]);
1337
+ const sessionsRoot = resolve(canonicalHome, "sessions");
1338
+ const archivedRoot = resolve(canonicalHome, "archived_sessions");
1339
+ if (!inside(sessionsRoot, canonicalPath) && !inside(archivedRoot, canonicalPath)) return null;
1340
+ const source = await stat(canonicalPath, { bigint: true });
1341
+ if (!source.isFile() || source.size > BigInt(Number.MAX_SAFE_INTEGER)) return null;
1342
+ const size = Number(source.size);
1343
+ let cache = this.caches.get(canonicalPath);
1344
+ if (
1345
+ !cache ||
1346
+ cache.device !== source.dev ||
1347
+ cache.inode !== source.ino ||
1348
+ size < cache.completeOffset
1349
+ ) {
1350
+ cache = {
1351
+ canonicalPath,
1352
+ device: source.dev,
1353
+ inode: source.ino,
1354
+ size: 0,
1355
+ completeOffset: 0,
1356
+ firstTimestamp: null,
1357
+ lastTimestamp: null,
1358
+ threadId: null,
1359
+ currentTurnId: null,
1360
+ nextTurnPlanMode: false,
1361
+ turnsInOrder: [],
1362
+ recoveredTurns: new Map(),
1363
+ turns: new Map(),
1364
+ pendingUserMessage: null,
1365
+ touchedAt: this.now(),
1366
+ };
1367
+ this.caches.set(canonicalPath, cache);
1368
+ }
1369
+ if (size > cache.completeOffset) {
1370
+ cache.completeOffset = await scanSession(cache, cache.completeOffset);
1371
+ cache.size = size;
1372
+ }
1373
+ cache.touchedAt = this.now();
1374
+ return cache;
1375
+ }
1376
+
1377
+ private prune(): void {
1378
+ if (this.caches.size <= MAX_CACHE_ENTRIES) return;
1379
+ const oldest = [...this.caches.entries()]
1380
+ .sort((first, second) => first[1].touchedAt - second[1].touchedAt)
1381
+ .slice(0, this.caches.size - MAX_CACHE_ENTRIES);
1382
+ for (const [path] of oldest) this.caches.delete(path);
1383
+ }
1384
+ }