@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,1110 @@
1
+ import { watch, type FSWatcher } from "node:fs";
2
+ import { realpath, stat } from "node:fs/promises";
3
+ import { isAbsolute, relative, resolve, sep } from "node:path";
4
+ import { getCodexHome } from "../codex/paths";
5
+ import type { CodexJsonRpcMessage } from "./codex-app-server";
6
+ import { fileChangesFromToolCall } from "./file-change-parser";
7
+ import { canonicalUserMessageText, publicUserMessageText } from "./user-message-identity";
8
+ import { boundedTurnErrorMessage } from "./turn-activity";
9
+
10
+ type JsonRecord = Record<string, unknown>;
11
+
12
+ const INITIAL_TAIL_BYTES = 16 * 1024 * 1024;
13
+ // Tool results in real Codex sessions can exceed 2 MiB. The projected Android
14
+ // row is bounded later, so accept a reasonably large source record here rather
15
+ // than silently losing the tool completion (or a long assistant message).
16
+ const MAX_RECORD_BYTES = 16 * 1024 * 1024;
17
+ const MAX_NESTED_TOOL_INPUT_BYTES = 128 * 1024;
18
+ const RECOVERY_POLL_MS = 1_000;
19
+
20
+ type SessionWatch = {
21
+ canonicalPath: string;
22
+ offset: number;
23
+ watcher: FSWatcher;
24
+ poller: ReturnType<typeof setInterval>;
25
+ projector: DesktopSessionRecordProjector;
26
+ draining: boolean;
27
+ pending: boolean;
28
+ };
29
+
30
+ export interface AndroidDesktopSessionStream {
31
+ watchThread(input: {
32
+ threadId: string;
33
+ sourcePath: string;
34
+ onMessage: (message: CodexJsonRpcMessage) => void;
35
+ }): Promise<boolean>;
36
+ isWatching(threadId: string): boolean;
37
+ /**
38
+ * Returns the projector's bounded live-turn identity without rereading the
39
+ * rollout or asking the Desktop IPC writer. This is intentionally optional
40
+ * so older test/dummy streams can continue to implement the interface.
41
+ */
42
+ activeTurnId?(threadId: string): string | null;
43
+ unwatchThread(threadId: string): void;
44
+ close(): void;
45
+ }
46
+
47
+ function record(value: unknown): JsonRecord | null {
48
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
49
+ }
50
+
51
+ function stringValue(value: unknown): string {
52
+ return typeof value === "string" ? value : "";
53
+ }
54
+
55
+ function inside(root: string, candidate: string): boolean {
56
+ const path = relative(root, candidate);
57
+ return path === "" || (path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path));
58
+ }
59
+
60
+ function timestampMs(value: unknown): number {
61
+ const parsed = Date.parse(stringValue(value));
62
+ return Number.isFinite(parsed) ? parsed : Date.now();
63
+ }
64
+
65
+ function turnIdFromPayload(payload: JsonRecord, fallback: string | null): string | null {
66
+ const metadata = record(payload.internal_chat_message_metadata_passthrough);
67
+ return stringValue(metadata?.turn_id) || stringValue(payload.turn_id) || fallback;
68
+ }
69
+
70
+ function responseText(payload: JsonRecord): string {
71
+ if (!Array.isArray(payload.content)) return "";
72
+ return payload.content
73
+ .flatMap((value) => {
74
+ const row = record(value);
75
+ return row && typeof row.text === "string" ? [row.text] : [];
76
+ })
77
+ .join("");
78
+ }
79
+
80
+ function normalizedItemType(value: unknown): string {
81
+ return stringValue(value)
82
+ .replace(/[^a-z0-9]/giu, "")
83
+ .toLowerCase();
84
+ }
85
+
86
+ function userMessageContent(payload: JsonRecord): JsonRecord[] {
87
+ if (!Array.isArray(payload.content)) return [];
88
+ return payload.content.flatMap((value) => {
89
+ const row = record(value);
90
+ const text = publicUserMessageText(row?.text);
91
+ return text ? [{ type: "text", text }] : [];
92
+ });
93
+ }
94
+
95
+ function reasoningSummary(payload: JsonRecord): string[] {
96
+ if (!Array.isArray(payload.summary)) return [];
97
+ return payload.summary.flatMap((value) => {
98
+ if (typeof value === "string" && value.trim()) return [value.trim()];
99
+ const row = record(value);
100
+ const text = stringValue(row?.text).trim();
101
+ return text ? [text] : [];
102
+ });
103
+ }
104
+
105
+ function decodeQuoted(value: string): string {
106
+ try {
107
+ return JSON.parse(`"${value}"`) as string;
108
+ } catch {
109
+ return value;
110
+ }
111
+ }
112
+
113
+ function nestedCommand(input: string): string | null {
114
+ const command = /\bcmd\s*:\s*"((?:\\.|[^"\\])*)"/su.exec(input)?.[1];
115
+ if (command) return decodeQuoted(command);
116
+ const sessionId = /\bsession_id\s*:\s*(\d+)/u.exec(input)?.[1];
117
+ if (/\btools\.write_stdin\s*\(/u.test(input)) {
118
+ return sessionId ? `Continue command session ${sessionId}` : "Continue command";
119
+ }
120
+ return null;
121
+ }
122
+
123
+ function parsedArguments(payload: JsonRecord): JsonRecord {
124
+ const direct = record(payload.arguments);
125
+ if (direct) return direct;
126
+ const encoded = stringValue(payload.arguments);
127
+ if (!encoded) return {};
128
+ try {
129
+ return record(JSON.parse(encoded)) ?? {};
130
+ } catch {
131
+ return {};
132
+ }
133
+ }
134
+
135
+ function nestedToolName(input: string): string {
136
+ return /\btools\.([A-Za-z0-9_]+)\s*\(/u.exec(input)?.[1] ?? "";
137
+ }
138
+
139
+ function decodedStringProperty(input: string, property: string): string {
140
+ const boundedInput = input.slice(0, MAX_NESTED_TOOL_INPUT_BYTES);
141
+ const match = new RegExp(
142
+ `\\b${property}\\s*:\\s*(?:"((?:\\\\.|[^"\\\\])*)"|'((?:\\\\.|[^'\\\\])*)'|\`((?:\\\\.|[^\`\\\\])*)\`)`,
143
+ "u",
144
+ ).exec(boundedInput);
145
+ const body = match?.[1] ?? match?.[2] ?? match?.[3];
146
+ if (body === undefined) return "";
147
+ if (match?.[1] !== undefined) return decodeQuoted(body);
148
+ const quote = match?.[2] !== undefined ? "'" : "`";
149
+ return body
150
+ .replaceAll(`\\${quote}`, quote)
151
+ .replaceAll("\\n", "\n")
152
+ .replaceAll("\\r", "\r")
153
+ .replaceAll("\\t", "\t")
154
+ .replaceAll("\\\\", "\\");
155
+ }
156
+
157
+ export function planUpdateFromToolCall(
158
+ payload: JsonRecord,
159
+ ): { plan: JsonRecord[]; explanation?: string } | null {
160
+ const input = stringValue(payload.input).slice(0, MAX_NESTED_TOOL_INPUT_BYTES);
161
+ const tool = nestedToolName(input) || stringValue(payload.name);
162
+ if (tool.split("__").at(-1) !== "update_plan") return null;
163
+ const direct = parsedArguments(payload);
164
+ const directPlan = Array.isArray(direct.plan) ? direct.plan : [];
165
+ const plan = directPlan.flatMap((value) => {
166
+ const step = record(value);
167
+ const title = stringValue(step?.step).trim();
168
+ const status = stringValue(step?.status).trim();
169
+ return title && status ? [{ step: title, status }] : [];
170
+ });
171
+ if (plan.length === 0) {
172
+ for (const match of input.matchAll(/\{([^{}]{0,8192})\}/gu)) {
173
+ const body = match[1] ?? "";
174
+ const step = decodedStringProperty(body, "step").trim();
175
+ const status = decodedStringProperty(body, "status").trim();
176
+ if (step && status) plan.push({ step, status });
177
+ }
178
+ }
179
+ const explanation =
180
+ stringValue(direct.explanation).trim() || decodedStringProperty(input, "explanation").trim();
181
+ return { plan, ...(explanation ? { explanation } : {}) };
182
+ }
183
+
184
+ function nestedToolArguments(input: string, tool: string, direct: JsonRecord): JsonRecord {
185
+ const boundedInput = input.slice(0, MAX_NESTED_TOOL_INPUT_BYTES);
186
+ const augmented: JsonRecord = { ...direct };
187
+ for (const property of [
188
+ "path",
189
+ "query",
190
+ "task_name",
191
+ "taskName",
192
+ "agent_name",
193
+ "agentName",
194
+ "target",
195
+ ] as const) {
196
+ if (augmented[property] !== undefined) continue;
197
+ const value = decodedStringProperty(boundedInput, property).trim();
198
+ if (value) augmented[property] = value;
199
+ }
200
+ const reviewTarget =
201
+ tool.split("__").at(-1) === "open_in_codex" &&
202
+ /\btarget\s*:\s*\{[^{}]{0,8192}\btype\s*:\s*["'`]review["'`]/u.test(boundedInput)
203
+ ? { type: "review" }
204
+ : null;
205
+ if (reviewTarget) augmented.target = reviewTarget;
206
+ return augmented;
207
+ }
208
+
209
+ export function toolItem(payload: JsonRecord): JsonRecord | null {
210
+ const input = stringValue(payload.input);
211
+ const name = stringValue(payload.name);
212
+ const args = parsedArguments(payload);
213
+ const id = stringValue(payload.id) || stringValue(payload.call_id);
214
+ if (!id) return null;
215
+ if (
216
+ (name === "exec" && /\btools\.(?:exec_command|write_stdin)\s*\(/u.test(input)) ||
217
+ ["exec_command", "write_stdin", "shell", "shell_command"].includes(name)
218
+ ) {
219
+ const directCommand =
220
+ typeof args.cmd === "string"
221
+ ? args.cmd
222
+ : Array.isArray(args.cmd)
223
+ ? args.cmd.flatMap((value) => (typeof value === "string" ? [value] : [])).join(" ")
224
+ : "";
225
+ const sessionId =
226
+ typeof args.session_id === "number" || typeof args.session_id === "string"
227
+ ? String(args.session_id)
228
+ : "";
229
+ return {
230
+ type: "commandExecution",
231
+ id,
232
+ command:
233
+ directCommand ||
234
+ nestedCommand(input) ||
235
+ (name === "write_stdin" && sessionId
236
+ ? `Continue command session ${sessionId}`
237
+ : "Command Execution"),
238
+ status: "inProgress",
239
+ aggregatedOutput: null,
240
+ exitCode: null,
241
+ durationMs: null,
242
+ processId: null,
243
+ commandActions: [],
244
+ };
245
+ }
246
+ if (name === "apply_patch" || (name === "exec" && /\btools\.apply_patch\s*\(/u.test(input))) {
247
+ return {
248
+ type: "fileChange",
249
+ id,
250
+ status: "inProgress",
251
+ changes: fileChangesFromToolCall(payload),
252
+ };
253
+ }
254
+ const nestedTool = nestedToolName(input);
255
+ // A bare custom `exec` record is Codex's internal orchestration wrapper
256
+ // (for example, tool discovery through ALL_TOOLS). It has no stable public
257
+ // activity name, so do not turn it into a misleading "Using Exec" row.
258
+ if (name === "exec" && !nestedTool) return null;
259
+ const tool = nestedTool || name || "tool";
260
+ return {
261
+ type: "dynamicToolCall",
262
+ id,
263
+ callId: stringValue(payload.call_id),
264
+ tool,
265
+ namespace: tool.includes("__") ? tool.split("__")[0] : "",
266
+ status: "inProgress",
267
+ arguments: nestedToolArguments(input, tool, args),
268
+ };
269
+ }
270
+
271
+ function outputString(payload: JsonRecord): string {
272
+ if (typeof payload.output === "string") return payload.output;
273
+ if (!Array.isArray(payload.output)) return "";
274
+ return payload.output
275
+ .flatMap((value) => {
276
+ const row = record(value);
277
+ return row && typeof row.text === "string" ? [row.text] : [];
278
+ })
279
+ .join("");
280
+ }
281
+
282
+ function failedOutput(output: string): boolean {
283
+ return /(?:Process|Command|Script) (?:exited|failed) with (?:code|exit code) (?!0\b)-?\d+/u.test(
284
+ output,
285
+ );
286
+ }
287
+
288
+ /** Converts append-only Codex session records into the same small notifications used by app-server. */
289
+ export class DesktopSessionRecordProjector {
290
+ private currentTurnId: string | null = null;
291
+ private currentTurnPlanMode = false;
292
+ private nextTurnPlanMode = false;
293
+ private settingsModel = "";
294
+ private settingsModelProviderId = "";
295
+ private settingsReasoningEffort = "";
296
+ private settingsServiceTier = "";
297
+ private settingsServiceTierKnown = false;
298
+ private lastPublishedSettings = "";
299
+ private readonly pendingTools = new Map<string, JsonRecord>();
300
+ private pendingFinalMessage: {
301
+ turnId: string;
302
+ item: JsonRecord;
303
+ completedAtMs: number;
304
+ } | null = null;
305
+ /**
306
+ * A Codex rollout writes one submitted user message twice in adjacent forms:
307
+ * first as the public response item, then as either a `user_message` identity
308
+ * event or a structured `item_completed` event carrying the durable
309
+ * Android/Desktop client id. Hold the first row long enough to join that
310
+ * identity instead of publishing two unrelated Android bubbles.
311
+ */
312
+ private pendingUserMessage: {
313
+ turnId: string;
314
+ item: JsonRecord;
315
+ completedAtMs: number;
316
+ comparisonText: string;
317
+ } | null = null;
318
+ private pendingCompactionId: string | null = null;
319
+
320
+ constructor(
321
+ private readonly threadId: string,
322
+ private readonly emit: (message: CodexJsonRpcMessage) => void,
323
+ ) {}
324
+
325
+ /**
326
+ * The canonical turn currently observed in the append-only rollout.
327
+ *
328
+ * Returning only the id keeps the recovery path metadata-only and avoids
329
+ * retaining or cloning any transcript content.
330
+ */
331
+ activeTurnId(): string | null {
332
+ return this.currentTurnId;
333
+ }
334
+
335
+ private flushPendingFinalMessage(): void {
336
+ const pending = this.pendingFinalMessage;
337
+ if (!pending) return;
338
+ this.pendingFinalMessage = null;
339
+ this.emit({
340
+ method: "item/completed",
341
+ params: {
342
+ threadId: this.threadId,
343
+ turnId: pending.turnId,
344
+ item: pending.item,
345
+ completedAtMs: pending.completedAtMs,
346
+ },
347
+ });
348
+ }
349
+
350
+ private flushPendingUserMessage(clientId = ""): void {
351
+ const pending = this.pendingUserMessage;
352
+ if (!pending) return;
353
+ this.pendingUserMessage = null;
354
+ this.emit({
355
+ method: "item/completed",
356
+ params: {
357
+ threadId: this.threadId,
358
+ turnId: pending.turnId,
359
+ item: {
360
+ ...pending.item,
361
+ ...(clientId ? { clientId } : {}),
362
+ },
363
+ completedAtMs: pending.completedAtMs,
364
+ },
365
+ });
366
+ }
367
+
368
+ private publishThreadSettings(
369
+ source: JsonRecord,
370
+ turnId: string | null,
371
+ updatedAtMs: number,
372
+ ): void {
373
+ const model = stringValue(source.model).trim().slice(0, 256);
374
+ const modelProviderId = stringValue(source.model_provider_id).trim().slice(0, 64);
375
+ const reasoningEffort = stringValue(source.reasoning_effort ?? source.effort)
376
+ .trim()
377
+ .slice(0, 64);
378
+ const serviceTierPresent = Object.hasOwn(source, "service_tier")
379
+ || Object.hasOwn(source, "serviceTier");
380
+ const rawServiceTier = serviceTierPresent
381
+ ? stringValue(source.service_tier ?? source.serviceTier).trim().slice(0, 64)
382
+ : "";
383
+ const serviceTier = rawServiceTier === "fast"
384
+ ? "priority"
385
+ : rawServiceTier || "default";
386
+ const modelIdentityChanged = (
387
+ Boolean(model && this.settingsModel && model !== this.settingsModel)
388
+ || Boolean(
389
+ modelProviderId
390
+ && this.settingsModelProviderId
391
+ && modelProviderId !== this.settingsModelProviderId,
392
+ )
393
+ );
394
+ if (modelIdentityChanged && !serviceTierPresent) {
395
+ // Never carry a prior model's Fast selection onto a new model when the
396
+ // new Desktop context provides no service-tier evidence.
397
+ this.settingsServiceTier = "";
398
+ this.settingsServiceTierKnown = false;
399
+ }
400
+ if (model) this.settingsModel = model;
401
+ if (modelProviderId) this.settingsModelProviderId = modelProviderId;
402
+ if (reasoningEffort) this.settingsReasoningEffort = reasoningEffort;
403
+ if (serviceTierPresent) {
404
+ this.settingsServiceTier = serviceTier;
405
+ this.settingsServiceTierKnown = true;
406
+ }
407
+ if (!this.settingsModel) return;
408
+ const fingerprint = JSON.stringify([
409
+ this.settingsModel,
410
+ this.settingsModelProviderId,
411
+ this.settingsReasoningEffort,
412
+ this.settingsServiceTierKnown ? this.settingsServiceTier : null,
413
+ ]);
414
+ if (fingerprint === this.lastPublishedSettings) return;
415
+ this.lastPublishedSettings = fingerprint;
416
+ this.emit({
417
+ method: "thread/settings/updated",
418
+ params: {
419
+ threadId: this.threadId,
420
+ turnId,
421
+ model: this.settingsModel,
422
+ ...(this.settingsModelProviderId ? { modelProviderId: this.settingsModelProviderId } : {}),
423
+ ...(this.settingsReasoningEffort ? { reasoningEffort: this.settingsReasoningEffort } : {}),
424
+ ...(this.settingsServiceTierKnown ? { serviceTier: this.settingsServiceTier } : {}),
425
+ updatedAtMs,
426
+ },
427
+ });
428
+ }
429
+
430
+ consume(value: unknown): void {
431
+ const row = record(value);
432
+ const payload = record(row?.payload);
433
+ if (!row || !payload) return;
434
+ const rowType = stringValue(row.type);
435
+ const payloadType = stringValue(payload.type);
436
+ const at = timestampMs(row.timestamp);
437
+ const userMessageEvent = rowType === "event_msg" && payloadType === "user_message";
438
+ const structuredCompletedItem =
439
+ rowType === "event_msg" && payloadType === "item_completed" ? record(payload.item) : null;
440
+ const structuredItemType = normalizedItemType(structuredCompletedItem?.type);
441
+ const structuredUserMessageEvent =
442
+ structuredCompletedItem !== null &&
443
+ (structuredItemType === "usermessage" ||
444
+ (structuredItemType === "message" &&
445
+ stringValue(structuredCompletedItem.role).toLowerCase() === "user"));
446
+ const userMessageIdentityEvent = userMessageEvent || structuredUserMessageEvent;
447
+
448
+ if (this.pendingUserMessage) {
449
+ const clientId = userMessageEvent
450
+ ? stringValue(payload.client_id ?? payload.clientId).trim()
451
+ : stringValue(
452
+ structuredCompletedItem?.clientId ??
453
+ structuredCompletedItem?.client_id ??
454
+ structuredCompletedItem?.clientUserMessageId ??
455
+ structuredCompletedItem?.client_user_message_id,
456
+ ).trim();
457
+ const comparisonText = userMessageEvent
458
+ ? canonicalUserMessageText(payload.message)
459
+ : canonicalUserMessageText(responseText(structuredCompletedItem ?? {}));
460
+ const identityTurnId = structuredUserMessageEvent
461
+ ? stringValue(payload.turn_id ?? payload.turnId) || this.currentTurnId
462
+ : this.pendingUserMessage.turnId;
463
+ if (
464
+ userMessageIdentityEvent &&
465
+ clientId &&
466
+ identityTurnId === this.pendingUserMessage.turnId &&
467
+ comparisonText === this.pendingUserMessage.comparisonText
468
+ ) {
469
+ this.flushPendingUserMessage(clientId);
470
+ return;
471
+ }
472
+ // Older Codex rollouts may not include the identity event. Preserve
473
+ // ordering by releasing the user row immediately before the next record.
474
+ this.flushPendingUserMessage();
475
+ }
476
+ // These records enrich the immediately preceding response item. They have
477
+ // no independent Android row, even when an older rollout omits an id.
478
+ if (userMessageIdentityEvent) return;
479
+
480
+ if (rowType === "event_msg" && payloadType === "thread_settings_applied") {
481
+ const settings = record(payload.thread_settings);
482
+ this.nextTurnPlanMode = stringValue(record(settings?.collaboration_mode)?.mode) === "plan";
483
+ if (settings) {
484
+ this.publishThreadSettings(
485
+ settings,
486
+ stringValue(payload.turn_id) || this.currentTurnId,
487
+ at,
488
+ );
489
+ }
490
+ return;
491
+ }
492
+
493
+ // Codex writes the private compaction answer as an ordinary-looking final
494
+ // assistant item immediately before the top-level `compacted` record. Hold
495
+ // final answers briefly so that private handoff text never reaches Android.
496
+ // Ordinary final answers are released before turn/completed below.
497
+ if (rowType === "compacted") {
498
+ this.pendingFinalMessage = null;
499
+ const turnId = this.currentTurnId;
500
+ if (turnId) {
501
+ const id = `context-compaction-${turnId}-${at}`;
502
+ // `compacted` is the replacement-history record written after Codex
503
+ // has finished compacting. Treating it as a start leaves Android stuck
504
+ // on "Compacting context" because current Codex rollouts do not also
505
+ // emit the older `context_compacted` event. Retain the id only to
506
+ // suppress that optional legacy duplicate when it is present.
507
+ this.pendingCompactionId = id;
508
+ this.emit({
509
+ method: "item/completed",
510
+ params: {
511
+ threadId: this.threadId,
512
+ turnId,
513
+ item: { type: "contextCompaction", id, status: "completed" },
514
+ completedAtMs: at,
515
+ },
516
+ });
517
+ }
518
+ return;
519
+ }
520
+ if (
521
+ this.pendingFinalMessage &&
522
+ !(
523
+ (rowType === "event_msg" && payloadType === "token_count") ||
524
+ rowType === "world_state" ||
525
+ rowType === "turn_context" ||
526
+ (rowType === "event_msg" && payloadType === "context_compacted")
527
+ )
528
+ ) {
529
+ this.flushPendingFinalMessage();
530
+ }
531
+
532
+ if (rowType === "turn_context") {
533
+ const turnId = stringValue(payload.turn_id) || this.currentTurnId;
534
+ // `turn_context` is durable settings metadata, not a lifecycle start.
535
+ // Completed tasks replay their last context so Android can restore the
536
+ // model and reasoning picker; treating that replay as active resurrects
537
+ // the completed turn and converts an ordinary phone send into Queue.
538
+ // A live append has already published `task_started`, so it may still
539
+ // refine that active turn's id without allowing idle metadata to start it.
540
+ if (this.currentTurnId !== null && turnId) this.currentTurnId = turnId;
541
+ const collaborationMode = record(payload.collaboration_mode);
542
+ if (collaborationMode) {
543
+ this.currentTurnPlanMode = stringValue(collaborationMode.mode) === "plan";
544
+ }
545
+ this.publishThreadSettings(payload, turnId, at);
546
+ return;
547
+ }
548
+ if (rowType === "event_msg" && payloadType === "token_count") {
549
+ const tokenUsage = record(payload.info);
550
+ if (!tokenUsage) return;
551
+ this.emit({
552
+ method: "thread/tokenUsage/updated",
553
+ params: {
554
+ threadId: this.threadId,
555
+ turnId: stringValue(payload.turn_id) || this.currentTurnId,
556
+ tokenUsage,
557
+ },
558
+ });
559
+ return;
560
+ }
561
+ if (rowType === "event_msg" && payloadType === "context_compacted") {
562
+ const turnId = stringValue(payload.turn_id) || this.currentTurnId;
563
+ this.pendingFinalMessage = null;
564
+ if (!turnId) return;
565
+ if (this.pendingCompactionId) {
566
+ this.pendingCompactionId = null;
567
+ return;
568
+ }
569
+ const id = `context-compaction-${turnId}-${at}`;
570
+ this.pendingCompactionId = null;
571
+ this.emit({
572
+ method: "item/completed",
573
+ params: {
574
+ threadId: this.threadId,
575
+ turnId,
576
+ item: {
577
+ type: "contextCompaction",
578
+ id,
579
+ },
580
+ completedAtMs: at,
581
+ },
582
+ });
583
+ return;
584
+ }
585
+ if (rowType === "event_msg" && payloadType === "task_started") {
586
+ const turnId = stringValue(payload.turn_id);
587
+ if (!turnId) return;
588
+ this.currentTurnId = turnId;
589
+ this.currentTurnPlanMode =
590
+ stringValue(payload.collaboration_mode_kind) === "plan" || this.nextTurnPlanMode;
591
+ this.nextTurnPlanMode = false;
592
+ this.emit({
593
+ method: "turn/started",
594
+ params: {
595
+ threadId: this.threadId,
596
+ turnId,
597
+ turn: { id: turnId, status: "inProgress" },
598
+ startedAtMs: at,
599
+ },
600
+ });
601
+ return;
602
+ }
603
+ if (rowType === "event_msg" && ["task_complete", "turn_aborted"].includes(payloadType)) {
604
+ const turnId = stringValue(payload.turn_id) || this.currentTurnId;
605
+ if (!turnId) return;
606
+ this.flushPendingFinalMessage();
607
+ const interrupted = payloadType === "turn_aborted";
608
+ const errorMessage =
609
+ payloadType === "task_complete" ? boundedTurnErrorMessage(payload.error) : "";
610
+ this.emit({
611
+ method: "turn/completed",
612
+ params: {
613
+ threadId: this.threadId,
614
+ turnId,
615
+ turn: {
616
+ id: turnId,
617
+ status: interrupted ? "interrupted" : errorMessage ? "failed" : "completed",
618
+ ...(errorMessage ? { error: { message: errorMessage } } : {}),
619
+ },
620
+ completedAtMs: at,
621
+ },
622
+ });
623
+ this.currentTurnId = null;
624
+ this.currentTurnPlanMode = false;
625
+ this.pendingTools.clear();
626
+ this.pendingCompactionId = null;
627
+ return;
628
+ }
629
+ if (rowType !== "response_item") return;
630
+ const turnId = turnIdFromPayload(payload, this.currentTurnId);
631
+ if (!turnId) return;
632
+ this.currentTurnId = turnId;
633
+ const id = stringValue(payload.id);
634
+
635
+ if (payloadType === "message" && payload.role === "user" && id) {
636
+ const text = responseText(payload);
637
+ if (!text) return;
638
+ const content = userMessageContent(payload);
639
+ const metadata = record(payload.internal_chat_message_metadata_passthrough);
640
+ const embeddedClientId = stringValue(metadata?.client_id).trim();
641
+ this.pendingUserMessage = {
642
+ turnId,
643
+ item: {
644
+ type: "userMessage",
645
+ id,
646
+ content,
647
+ },
648
+ completedAtMs: at,
649
+ comparisonText: canonicalUserMessageText(text),
650
+ };
651
+ // Some runtimes already put the durable id on the response item. There
652
+ // is nothing left to join in that case.
653
+ if (embeddedClientId) this.flushPendingUserMessage(embeddedClientId);
654
+ return;
655
+ }
656
+ if (payloadType === "message" && payload.role === "assistant" && id) {
657
+ const text = responseText(payload);
658
+ if (!text) return;
659
+ const item = {
660
+ type: "agentMessage",
661
+ id,
662
+ text,
663
+ phase: stringValue(payload.phase) || null,
664
+ ...(this.currentTurnPlanMode ? { androidRemotePlanMode: true } : {}),
665
+ };
666
+ if (payload.phase === "final_answer") {
667
+ this.pendingFinalMessage = { turnId, item, completedAtMs: at };
668
+ } else {
669
+ this.emit({
670
+ method: "item/completed",
671
+ params: { threadId: this.threadId, turnId, item, completedAtMs: at },
672
+ });
673
+ }
674
+ return;
675
+ }
676
+ if (payloadType === "reasoning" && id) {
677
+ const summary = reasoningSummary(payload);
678
+ this.emit({
679
+ method: "item/completed",
680
+ params: {
681
+ threadId: this.threadId,
682
+ turnId,
683
+ item: { type: "reasoning", id, summary },
684
+ completedAtMs: at,
685
+ },
686
+ });
687
+ return;
688
+ }
689
+ if (["custom_tool_call", "function_call"].includes(payloadType)) {
690
+ const planUpdate = planUpdateFromToolCall(payload);
691
+ if (planUpdate) {
692
+ this.emit({
693
+ method: "turn/plan/updated",
694
+ params: { threadId: this.threadId, turnId, ...planUpdate },
695
+ });
696
+ return;
697
+ }
698
+ const item = toolItem(payload);
699
+ const callId = stringValue(payload.call_id) || id;
700
+ if (!item || !callId) return;
701
+ this.pendingTools.set(callId, item);
702
+ this.emit({
703
+ method: "item/started",
704
+ params: { threadId: this.threadId, turnId, item, startedAtMs: at },
705
+ });
706
+ return;
707
+ }
708
+ if (["custom_tool_call_output", "function_call_output"].includes(payloadType)) {
709
+ const callId = stringValue(payload.call_id);
710
+ const pending = this.pendingTools.get(callId);
711
+ if (!pending) return;
712
+ this.pendingTools.delete(callId);
713
+ const output = outputString(payload);
714
+ const failed = failedOutput(output);
715
+ this.emit({
716
+ method: "item/completed",
717
+ params: {
718
+ threadId: this.threadId,
719
+ turnId,
720
+ item: {
721
+ ...pending,
722
+ status: failed ? "failed" : "completed",
723
+ ...(pending.type === "commandExecution"
724
+ ? { aggregatedOutput: output.slice(-64 * 1024), exitCode: failed ? 1 : 0 }
725
+ : {}),
726
+ },
727
+ completedAtMs: at,
728
+ },
729
+ });
730
+ }
731
+ }
732
+ }
733
+
734
+ async function consumeRange(
735
+ path: string,
736
+ startOffset: number,
737
+ projector: DesktopSessionRecordProjector,
738
+ ): Promise<number> {
739
+ const decoder = new TextDecoder();
740
+ const reader = Bun.file(path).slice(startOffset).stream().getReader();
741
+ let absoluteOffset = startOffset;
742
+ let lineStart = startOffset;
743
+ let parts: Uint8Array[] = [];
744
+ let lineBytes = 0;
745
+ let oversized = false;
746
+ const append = (bytes: Uint8Array) => {
747
+ lineBytes += bytes.byteLength;
748
+ if (lineBytes > MAX_RECORD_BYTES) {
749
+ oversized = true;
750
+ parts = [];
751
+ } else if (!oversized && bytes.byteLength > 0) {
752
+ parts.push(bytes.slice());
753
+ }
754
+ };
755
+ const emitLine = (nextOffset: number) => {
756
+ if (!oversized && lineBytes > 0) {
757
+ const joined = new Uint8Array(lineBytes);
758
+ let offset = 0;
759
+ for (const part of parts) {
760
+ joined.set(part, offset);
761
+ offset += part.byteLength;
762
+ }
763
+ try {
764
+ projector.consume(JSON.parse(decoder.decode(joined)));
765
+ } catch {
766
+ /* incomplete or malformed records are retried from the last newline */
767
+ }
768
+ }
769
+ lineStart = nextOffset;
770
+ parts = [];
771
+ lineBytes = 0;
772
+ oversized = false;
773
+ };
774
+ try {
775
+ for (;;) {
776
+ const { value, done } = await reader.read();
777
+ if (done) break;
778
+ const chunk = value instanceof Uint8Array ? value : new Uint8Array(value);
779
+ let segmentStart = 0;
780
+ for (let index = 0; index < chunk.byteLength; index += 1) {
781
+ if (chunk[index] !== 0x0a) continue;
782
+ append(chunk.subarray(segmentStart, index));
783
+ absoluteOffset += index - segmentStart + 1;
784
+ emitLine(absoluteOffset);
785
+ segmentStart = index + 1;
786
+ }
787
+ append(chunk.subarray(segmentStart));
788
+ absoluteOffset += chunk.byteLength - segmentStart;
789
+ }
790
+ } finally {
791
+ reader.releaseLock();
792
+ }
793
+ return lineStart;
794
+ }
795
+
796
+ function isThreadSettingsRecord(row: JsonRecord | null, payload: JsonRecord | null): boolean {
797
+ if (!row || !payload) return false;
798
+ if (row.type === "turn_context") return stringValue(payload.model).trim().length > 0;
799
+ if (row.type !== "event_msg" || stringValue(payload.type) !== "thread_settings_applied") {
800
+ return false;
801
+ }
802
+ return stringValue(record(payload.thread_settings)?.model).trim().length > 0;
803
+ }
804
+
805
+ function isTokenUsageRecord(row: JsonRecord | null, payload: JsonRecord | null): boolean {
806
+ return (
807
+ row?.type === "event_msg" &&
808
+ stringValue(payload?.type) === "token_count" &&
809
+ record(payload?.info) !== null
810
+ );
811
+ }
812
+
813
+ function recentActiveRows(
814
+ text: string,
815
+ startsInsideRecord: boolean,
816
+ ): {
817
+ boundaryFound: boolean;
818
+ settingsFound: boolean;
819
+ tokenUsageFound: boolean;
820
+ rows: unknown[];
821
+ } {
822
+ const lines = text.split("\n");
823
+ if (startsInsideRecord) lines.shift();
824
+ const rows = lines.flatMap((line) => {
825
+ if (!line) return [];
826
+ try {
827
+ return [JSON.parse(line) as unknown];
828
+ } catch {
829
+ return [];
830
+ }
831
+ });
832
+ let boundary = -1;
833
+ let latestSettings = -1;
834
+ let latestTokenUsage = -1;
835
+ let settingsSinceTerminal = -1;
836
+ let active = false;
837
+ let boundaryFound = false;
838
+ for (let index = 0; index < rows.length; index += 1) {
839
+ const row = record(rows[index]);
840
+ const payload = record(row?.payload);
841
+ if (isThreadSettingsRecord(row, payload)) {
842
+ latestSettings = index;
843
+ settingsSinceTerminal = index;
844
+ }
845
+ if (isTokenUsageRecord(row, payload)) latestTokenUsage = index;
846
+ if (row?.type !== "event_msg" || !payload) continue;
847
+ const type = stringValue(payload.type);
848
+ if (type === "task_started") {
849
+ boundary = settingsSinceTerminal >= 0 ? settingsSinceTerminal : index;
850
+ active = true;
851
+ boundaryFound = true;
852
+ }
853
+ if (["task_complete", "turn_aborted"].includes(type)) {
854
+ boundary = index;
855
+ active = false;
856
+ boundaryFound = true;
857
+ settingsSinceTerminal = -1;
858
+ }
859
+ }
860
+ const selected = new Set<number>();
861
+ if (active && boundary >= 0) {
862
+ for (let index = boundary; index < rows.length; index += 1) selected.add(index);
863
+ }
864
+ if (latestSettings >= 0) selected.add(latestSettings);
865
+ if (latestTokenUsage >= 0) selected.add(latestTokenUsage);
866
+ return {
867
+ boundaryFound,
868
+ settingsFound: latestSettings >= 0,
869
+ tokenUsageFound: latestTokenUsage >= 0,
870
+ // Completed Desktop tasks still own their last model/reasoning selection
871
+ // and their last context-window reading. Replaying only active turns
872
+ // discarded that durable metadata after every Remodex restart, so
873
+ // Android fell back to defaults and "Waiting for context usage" until a
874
+ // new turn happened to publish another token-count record.
875
+ rows: [...selected].sort((left, right) => left - right).map((index) => rows[index]!),
876
+ };
877
+ }
878
+
879
+ async function scanSessionReplayState(
880
+ path: string,
881
+ size: number,
882
+ ): Promise<{
883
+ activeTurnStartOffset: number | null;
884
+ latestSettingsRecord: unknown | null;
885
+ latestTokenUsageRecord: unknown | null;
886
+ latestTokenUsageOffset: number | null;
887
+ }> {
888
+ const reader = Bun.file(path).slice(0, size).stream().getReader();
889
+ let absoluteOffset = 0;
890
+ let lineStart = 0;
891
+ let parts: Uint8Array[] = [];
892
+ let lineBytes = 0;
893
+ let oversized = false;
894
+ let activeTurnStartOffset: number | null = null;
895
+ let latestSettingsRecord: unknown | null = null;
896
+ let latestTokenUsageRecord: unknown | null = null;
897
+ let latestTokenUsageOffset: number | null = null;
898
+ let active = false;
899
+ const decoder = new TextDecoder();
900
+ const append = (bytes: Uint8Array) => {
901
+ lineBytes += bytes.byteLength;
902
+ // A turn_context record can contain a large environment snapshot. Keep the
903
+ // same hard cap as ordinary replay so older sessions without the compact
904
+ // thread_settings_applied record can still recover their picker state.
905
+ if (lineBytes > MAX_RECORD_BYTES) {
906
+ oversized = true;
907
+ parts = [];
908
+ } else if (!oversized && bytes.byteLength > 0) {
909
+ parts.push(bytes.slice());
910
+ }
911
+ };
912
+ const inspectLine = (nextOffset: number) => {
913
+ if (!oversized && lineBytes > 0) {
914
+ const joined = new Uint8Array(lineBytes);
915
+ let offset = 0;
916
+ for (const part of parts) {
917
+ joined.set(part, offset);
918
+ offset += part.byteLength;
919
+ }
920
+ try {
921
+ const row = record(JSON.parse(decoder.decode(joined)));
922
+ const payload = record(row?.payload);
923
+ if (isThreadSettingsRecord(row, payload)) latestSettingsRecord = row;
924
+ if (isTokenUsageRecord(row, payload)) {
925
+ latestTokenUsageRecord = row;
926
+ latestTokenUsageOffset = lineStart;
927
+ }
928
+ if (row?.type === "event_msg" && payload) {
929
+ const type = stringValue(payload.type);
930
+ if (type === "task_started") {
931
+ activeTurnStartOffset = lineStart;
932
+ active = true;
933
+ } else if (["task_complete", "turn_aborted"].includes(type)) {
934
+ activeTurnStartOffset = null;
935
+ active = false;
936
+ }
937
+ }
938
+ } catch {
939
+ /* malformed boundary candidates are ignored */
940
+ }
941
+ }
942
+ lineStart = nextOffset;
943
+ parts = [];
944
+ lineBytes = 0;
945
+ oversized = false;
946
+ };
947
+ try {
948
+ for (;;) {
949
+ const { value, done } = await reader.read();
950
+ if (done) break;
951
+ const chunk = value instanceof Uint8Array ? value : new Uint8Array(value);
952
+ let segmentStart = 0;
953
+ for (let index = 0; index < chunk.byteLength; index += 1) {
954
+ if (chunk[index] !== 0x0a) continue;
955
+ append(chunk.subarray(segmentStart, index));
956
+ absoluteOffset += index - segmentStart + 1;
957
+ inspectLine(absoluteOffset);
958
+ segmentStart = index + 1;
959
+ }
960
+ append(chunk.subarray(segmentStart));
961
+ absoluteOffset += chunk.byteLength - segmentStart;
962
+ }
963
+ } finally {
964
+ reader.releaseLock();
965
+ }
966
+ return {
967
+ activeTurnStartOffset: active ? activeTurnStartOffset : null,
968
+ latestSettingsRecord,
969
+ latestTokenUsageRecord,
970
+ latestTokenUsageOffset,
971
+ };
972
+ }
973
+
974
+ async function replayActiveTail(
975
+ path: string,
976
+ size: number,
977
+ projector: DesktopSessionRecordProjector,
978
+ ): Promise<void> {
979
+ const start = Math.max(0, size - INITIAL_TAIL_BYTES);
980
+ const text = await Bun.file(path).slice(start, size).text();
981
+ const recent = recentActiveRows(text, start > 0);
982
+ if (start === 0 || (recent.boundaryFound && recent.settingsFound && recent.tokenUsageFound)) {
983
+ for (const row of recent.rows) projector.consume(row);
984
+ return;
985
+ }
986
+ // A long live or completed turn can push its model settings outside the fast
987
+ // 16 MiB tail. Find both pieces in one bounded-memory pass: publish only the
988
+ // latest durable settings and context usage, then replay the active turn (if
989
+ // any). Completed message history remains on the authoritative thread/read
990
+ // path and is not duplicated.
991
+ const scan = await scanSessionReplayState(path, size);
992
+ if (scan.latestSettingsRecord) projector.consume(scan.latestSettingsRecord);
993
+ if (
994
+ scan.latestTokenUsageRecord &&
995
+ (scan.activeTurnStartOffset === null ||
996
+ scan.latestTokenUsageOffset === null ||
997
+ scan.latestTokenUsageOffset < scan.activeTurnStartOffset)
998
+ ) {
999
+ projector.consume(scan.latestTokenUsageRecord);
1000
+ }
1001
+ if (scan.activeTurnStartOffset !== null) {
1002
+ await consumeRange(path, scan.activeTurnStartOffset, projector);
1003
+ }
1004
+ }
1005
+
1006
+ export class DesktopSessionStream implements AndroidDesktopSessionStream {
1007
+ private readonly watches = new Map<string, SessionWatch>();
1008
+ private readonly codexHome: string | undefined;
1009
+
1010
+ constructor(options: { codexHome?: string } = {}) {
1011
+ // Resolving CODEX_HOME validates that the directory already exists. Keep
1012
+ // construction side-effect free so a disabled Android gateway cannot make
1013
+ // unrelated Remodex management routes fail during server startup.
1014
+ this.codexHome = options.codexHome;
1015
+ }
1016
+
1017
+ isWatching(threadId: string): boolean {
1018
+ return this.watches.has(threadId);
1019
+ }
1020
+
1021
+ activeTurnId(threadId: string): string | null {
1022
+ return this.watches.get(threadId)?.projector.activeTurnId() ?? null;
1023
+ }
1024
+
1025
+ async watchThread(input: {
1026
+ threadId: string;
1027
+ sourcePath: string;
1028
+ onMessage: (message: CodexJsonRpcMessage) => void;
1029
+ }): Promise<boolean> {
1030
+ const existing = this.watches.get(input.threadId);
1031
+ try {
1032
+ const [home, canonicalPath] = await Promise.all([
1033
+ realpath(resolve(this.codexHome ?? getCodexHome())),
1034
+ realpath(resolve(input.sourcePath)),
1035
+ ]);
1036
+ const sessions = resolve(home, "sessions");
1037
+ const archived = resolve(home, "archived_sessions");
1038
+ if (!inside(sessions, canonicalPath) && !inside(archived, canonicalPath)) return false;
1039
+ if (existing?.canonicalPath === canonicalPath) return true;
1040
+ this.unwatchThread(input.threadId);
1041
+ const source = await stat(canonicalPath);
1042
+ if (!source.isFile()) return false;
1043
+ const projector = new DesktopSessionRecordProjector(input.threadId, input.onMessage);
1044
+ await replayActiveTail(canonicalPath, source.size, projector);
1045
+ const watcher = watch(canonicalPath, { persistent: false }, () =>
1046
+ this.queueDrain(input.threadId),
1047
+ );
1048
+ const poller = setInterval(() => this.queueDrain(input.threadId), RECOVERY_POLL_MS);
1049
+ poller.unref?.();
1050
+ this.watches.set(input.threadId, {
1051
+ canonicalPath,
1052
+ offset: source.size,
1053
+ watcher,
1054
+ poller,
1055
+ projector,
1056
+ draining: false,
1057
+ pending: false,
1058
+ });
1059
+ // Close the stat/watch race: anything appended after the stat is read now.
1060
+ this.queueDrain(input.threadId);
1061
+ return true;
1062
+ } catch {
1063
+ return false;
1064
+ }
1065
+ }
1066
+
1067
+ unwatchThread(threadId: string): void {
1068
+ const current = this.watches.get(threadId);
1069
+ if (!current) return;
1070
+ current.watcher.close();
1071
+ clearInterval(current.poller);
1072
+ this.watches.delete(threadId);
1073
+ }
1074
+
1075
+ close(): void {
1076
+ for (const threadId of [...this.watches.keys()]) this.unwatchThread(threadId);
1077
+ }
1078
+
1079
+ private queueDrain(threadId: string): void {
1080
+ const current = this.watches.get(threadId);
1081
+ if (!current) return;
1082
+ if (current.draining) {
1083
+ current.pending = true;
1084
+ return;
1085
+ }
1086
+ current.draining = true;
1087
+ void this.drain(threadId).finally(() => {
1088
+ const latest = this.watches.get(threadId);
1089
+ if (!latest) return;
1090
+ latest.draining = false;
1091
+ if (latest.pending) {
1092
+ latest.pending = false;
1093
+ this.queueDrain(threadId);
1094
+ }
1095
+ });
1096
+ }
1097
+
1098
+ private async drain(threadId: string): Promise<void> {
1099
+ const current = this.watches.get(threadId);
1100
+ if (!current) return;
1101
+ try {
1102
+ const source = await stat(current.canonicalPath);
1103
+ if (source.size < current.offset) current.offset = source.size;
1104
+ if (source.size === current.offset) return;
1105
+ current.offset = await consumeRange(current.canonicalPath, current.offset, current.projector);
1106
+ } catch {
1107
+ // A transient rename or concurrent write is retried by the recovery poll.
1108
+ }
1109
+ }
1110
+ }