@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,1780 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Buffer } from "node:buffer";
3
+ import { basename } from "node:path";
4
+ import {
5
+ attachmentUserMessageIdentityKey,
6
+ publicUserMessageText,
7
+ } from "./user-message-identity";
8
+ import {
9
+ boundedTurnErrorMessage,
10
+ canonicalTurnActivitySnapshot,
11
+ canonicalTurnStatusIsActive,
12
+ canonicalTurnStatusIsTerminal,
13
+ canonicalTurnsNewestFirst,
14
+ normalizedCanonicalTurnStatus,
15
+ projectedTurnLifecycleWins,
16
+ } from "./turn-activity";
17
+
18
+ type JsonRecord = Record<string, unknown>;
19
+
20
+ export const ANDROID_REMOTE_TOOL_TEXT_LIMIT_BYTES = 64 * 1024;
21
+ const ANDROID_REMOTE_FILE_DIFF_TOTAL_LIMIT_BYTES = 512 * 1024;
22
+ const ANDROID_REMOTE_FILE_DIFF_LIMIT_BYTES = 64 * 1024;
23
+ const TRUNCATION_SUFFIX = "\n[truncated]";
24
+
25
+ type PublicFileChange = {
26
+ path: string;
27
+ additions: number;
28
+ deletions: number;
29
+ diff?: string;
30
+ diffTruncated?: boolean;
31
+ };
32
+
33
+ function record(value: unknown): JsonRecord | null {
34
+ return value && typeof value === "object" && !Array.isArray(value)
35
+ ? value as JsonRecord
36
+ : null;
37
+ }
38
+
39
+ function text(value: unknown, fallback = ""): string {
40
+ return typeof value === "string" ? value : fallback;
41
+ }
42
+
43
+ function finite(value: unknown): number | null {
44
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
45
+ }
46
+
47
+ function nonNegativeInteger(value: unknown): number | null {
48
+ const number = finite(value);
49
+ return number !== null && number >= 0 ? Math.floor(number) : null;
50
+ }
51
+
52
+ function utf8Prefix(value: string, maximumBytes: number): string {
53
+ let low = 0;
54
+ let high = value.length;
55
+ while (low < high) {
56
+ const middle = Math.ceil((low + high) / 2);
57
+ if (Buffer.byteLength(value.slice(0, middle), "utf8") <= maximumBytes) low = middle;
58
+ else high = middle - 1;
59
+ }
60
+ let end = low;
61
+ if (end > 0 && end < value.length) {
62
+ const last = value.charCodeAt(end - 1);
63
+ if (last >= 0xd800 && last <= 0xdbff) end -= 1;
64
+ }
65
+ return value.slice(0, end);
66
+ }
67
+
68
+ function boundedText(value: unknown, maximumBytes = ANDROID_REMOTE_TOOL_TEXT_LIMIT_BYTES): {
69
+ value: string;
70
+ truncated: boolean;
71
+ } {
72
+ const source = typeof value === "string" ? value : "";
73
+ if (Buffer.byteLength(source, "utf8") <= maximumBytes) {
74
+ return { value: source, truncated: false };
75
+ }
76
+ const suffixBytes = Buffer.byteLength(TRUNCATION_SUFFIX, "utf8");
77
+ return {
78
+ value: `${utf8Prefix(source, Math.max(0, maximumBytes - suffixBytes))}${TRUNCATION_SUFFIX}`,
79
+ truncated: true,
80
+ };
81
+ }
82
+
83
+ function countUnifiedDiffLines(value: unknown): { additions: number; deletions: number } {
84
+ if (typeof value !== "string") return { additions: 0, deletions: 0 };
85
+ let additions = 0;
86
+ let deletions = 0;
87
+ for (const line of value.split(/\r?\n/u)) {
88
+ if (line.startsWith("+") && !line.startsWith("+++")) additions += 1;
89
+ else if (line.startsWith("-") && !line.startsWith("---")) deletions += 1;
90
+ }
91
+ return { additions, deletions };
92
+ }
93
+
94
+ function publicFileChanges(item: JsonRecord): PublicFileChange[] {
95
+ if (!Array.isArray(item.changes)) return [];
96
+ const changes = item.changes.slice(0, 64);
97
+ const perFileDiffLimit = Math.min(
98
+ ANDROID_REMOTE_FILE_DIFF_LIMIT_BYTES,
99
+ Math.max(8 * 1024, Math.floor(ANDROID_REMOTE_FILE_DIFF_TOTAL_LIMIT_BYTES / changes.length)),
100
+ );
101
+ let remainingDiffBytes = ANDROID_REMOTE_FILE_DIFF_TOTAL_LIMIT_BYTES;
102
+ return changes.flatMap((value) => {
103
+ const change = record(value);
104
+ const path = text(change?.path).trim()
105
+ || text(change?.filePath).trim()
106
+ || text(change?.relativePath).trim();
107
+ if (!path) return [];
108
+ const rawDiffValue = change?.diff ?? change?.patch ?? change?.unifiedDiff ?? change?.content;
109
+ const diffCounts = countUnifiedDiffLines(rawDiffValue);
110
+ const rawDiff = typeof rawDiffValue === "string" && rawDiffValue.trim().length > 0
111
+ ? rawDiffValue
112
+ : null;
113
+ const projectedDiff = rawDiff && remainingDiffBytes > 0
114
+ ? boundedText(rawDiff, Math.min(perFileDiffLimit, remainingDiffBytes))
115
+ : null;
116
+ if (projectedDiff) {
117
+ remainingDiffBytes -= Buffer.byteLength(projectedDiff.value, "utf8");
118
+ }
119
+ return [{
120
+ path: boundedText(path, 8 * 1024).value,
121
+ additions: Math.max(0, Math.floor(finite(change?.additions) ?? diffCounts.additions)),
122
+ deletions: Math.max(0, Math.floor(finite(change?.deletions) ?? diffCounts.deletions)),
123
+ ...(projectedDiff
124
+ ? {
125
+ diff: projectedDiff.value,
126
+ ...(projectedDiff.truncated ? { diffTruncated: true } : {}),
127
+ }
128
+ : {}),
129
+ }];
130
+ });
131
+ }
132
+
133
+ function humanizeToolToken(value: unknown): string {
134
+ const raw = text(value).trim();
135
+ if (!raw) return "";
136
+ const token = raw
137
+ .split(/__|[./:]/u)
138
+ .filter(Boolean)
139
+ .at(-1)
140
+ ?.replace(/^(?:mcp|functions?|tools?)_+/iu, "")
141
+ .replace(/[_-]+/gu, " ")
142
+ .replace(/([a-z\d])([A-Z])/gu, "$1 $2")
143
+ .replace(/\s+/gu, " ")
144
+ .trim() ?? "";
145
+ return token ? `${token.charAt(0).toUpperCase()}${token.slice(1)}` : "";
146
+ }
147
+
148
+ function concisePublicText(value: unknown, maximumCharacters = 64): string {
149
+ const normalized = text(value).replace(/\s+/gu, " ").trim();
150
+ if (normalized.length <= maximumCharacters) return normalized;
151
+ return `${normalized.slice(0, Math.max(1, maximumCharacters - 1)).trimEnd()}…`;
152
+ }
153
+
154
+ function concisePublicBasename(value: unknown): string {
155
+ const normalized = text(value)
156
+ .trim()
157
+ .replace(/^["'`(<]+|["'`>),;]+$/gu, "")
158
+ .replace(/:\d+(?::\d+)?$/u, "");
159
+ if (!normalized || /^[a-z][a-z\d+.-]*:\/\//iu.test(normalized)) return "";
160
+ const basename = normalized.split(/[\\/]/u).filter(Boolean).at(-1) ?? "";
161
+ if (!basename || basename === "." || basename === "..") return "";
162
+ return concisePublicText(basename, 48);
163
+ }
164
+
165
+ function publicReadFileBasename(value: unknown): string {
166
+ const normalized = text(value)
167
+ .trim()
168
+ .replace(/^["'`(<]+|["'`>),;]+$/gu, "")
169
+ .replace(/:\d+(?::\d+)?$/u, "");
170
+ if (!normalized || /^[a-z][a-z\d+.-]*:\/\//iu.test(normalized)) return "";
171
+ const name = normalized.split(/[\\/]/u).filter(Boolean).at(-1) ?? "";
172
+ if (!name || name === "." || name === "..") return "";
173
+ // Normal filesystem names fit comfortably inside this safety bound. Unlike
174
+ // the compact labels used by other activity types, reading activity labels
175
+ // retain the complete filename so Android never has to replace it with +N.
176
+ return boundedText(name, 1024).value;
177
+ }
178
+
179
+ function withConciseDetail(title: string, detail: string): string {
180
+ const normalized = concisePublicText(detail, Math.max(16, 92 - title.length));
181
+ return normalized ? `${title} · ${normalized}` : title;
182
+ }
183
+
184
+ function commandReadFileNames(command: string): string[] {
185
+ const tokens = command.match(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|[^\s;&|]+/gu) ?? [];
186
+ const names: string[] = [];
187
+ for (const rawToken of tokens) {
188
+ const token = rawToken
189
+ .replace(/^["'`]+|["'`,)]+$/gu, "")
190
+ .replaceAll("\\\"", "\"")
191
+ .trim();
192
+ if (
193
+ !token ||
194
+ token.startsWith("-") ||
195
+ token.includes("*") ||
196
+ token.includes("${") ||
197
+ /^\d+(?:\.\d+)?$/u.test(token) ||
198
+ /^(?:\d*>)?\/(?:dev\/(?:null|zero|random|urandom|stdin|stdout|stderr|fd(?:\/.*)?)|proc\/[^/]+\/(?:cwd|root|exe|fd(?:\/.*)?))$/u.test(token) ||
199
+ /^[a-z][a-z\d+.-]*:\/\//iu.test(token)
200
+ ) continue;
201
+ const fileName = publicReadFileBasename(token);
202
+ if (!fileName) continue;
203
+ const pathLike = /[\\/]/u.test(token)
204
+ || /\.[a-z\d]{1,12}(?::\d+(?::\d+)?)?$/iu.test(token)
205
+ || /^(?:AGENTS|CHANGELOG|Dockerfile|LICENSE|Makefile|README)(?:\.[a-z\d]+)?$/iu.test(fileName);
206
+ if (!pathLike || names.includes(fileName)) continue;
207
+ names.push(fileName);
208
+ }
209
+ return names;
210
+ }
211
+
212
+ function argumentPathNames(argumentsRecord: JsonRecord | null): string[] {
213
+ if (!argumentsRecord) return [];
214
+ const values: unknown[] = [];
215
+ for (const key of ["path", "file", "filePath", "filename", "paths", "files"] as const) {
216
+ const value = argumentsRecord[key];
217
+ if (Array.isArray(value)) values.push(...value);
218
+ else if (value !== undefined) values.push(value);
219
+ }
220
+ return values
221
+ .flatMap(value => typeof value === "string" ? [publicReadFileBasename(value)] : [])
222
+ .filter((value, index, all) => value && all.indexOf(value) === index);
223
+ }
224
+
225
+ function argumentPathDetail(argumentsRecord: JsonRecord | null): string {
226
+ const names = argumentPathNames(argumentsRecord);
227
+ if (names.length === 0) return "";
228
+ return names.length === 1 ? names[0]! : `${names[0]} +${names.length - 1}`;
229
+ }
230
+
231
+ function readingFilesTitle(fileNames: readonly string[]): string {
232
+ if (fileNames.length === 0) return "Reading files";
233
+ return boundedText(`Reading ${fileNames.join(", ")}`, 16 * 1024).value;
234
+ }
235
+
236
+ function collaborationSubject(item: JsonRecord): string {
237
+ const argumentsRecord = toolArguments(item.arguments);
238
+ const target = record(argumentsRecord?.target);
239
+ const candidates: unknown[] = [
240
+ argumentsRecord?.task_name,
241
+ argumentsRecord?.taskName,
242
+ argumentsRecord?.agent_name,
243
+ argumentsRecord?.agentName,
244
+ target?.task_name,
245
+ target?.name,
246
+ typeof argumentsRecord?.target === "string" ? argumentsRecord.target : undefined,
247
+ item.taskName,
248
+ item.agentName,
249
+ ];
250
+ for (const candidate of candidates) {
251
+ const value = concisePublicText(candidate, 48);
252
+ if (!value || /^[\da-f-]{16,}$/iu.test(value)) continue;
253
+ return value.startsWith("/") ? concisePublicBasename(value) : value;
254
+ }
255
+ return "";
256
+ }
257
+
258
+ function collaborationLabel(value: unknown, item: JsonRecord = {}): string {
259
+ const normalized = text(value).toLowerCase().replace(/[^a-z\d]+/gu, "_");
260
+ const title = normalized.includes("spawn") || normalized.includes("create_agent")
261
+ ? "Starting agent"
262
+ : normalized.includes("wait")
263
+ ? "Waiting for agent"
264
+ : normalized.includes("send_message") || normalized.includes("followup")
265
+ ? "Messaging agent"
266
+ : normalized.includes("interrupt")
267
+ ? "Interrupting agent"
268
+ : normalized.includes("close") || normalized.includes("finish")
269
+ ? "Closing agent"
270
+ : normalized.includes("list_agents")
271
+ ? "Listing agents"
272
+ : "Agent activity";
273
+ return withConciseDetail(title, collaborationSubject(item));
274
+ }
275
+
276
+ type ToolPresentation = {
277
+ itemType:
278
+ | "codebase-search"
279
+ | "collab_agent_tool_call"
280
+ | "command_execution"
281
+ | "dynamic_tool_call"
282
+ | "file-read"
283
+ | "image-generation"
284
+ | "image_view"
285
+ | "review-marker"
286
+ | "web_search";
287
+ title: string;
288
+ fileNames?: string[];
289
+ requestKind?: "command" | "file-read";
290
+ };
291
+
292
+ function normalizedToolName(value: unknown): string {
293
+ return text(value)
294
+ .split(/__|[./:]/u)
295
+ .filter(Boolean)
296
+ .at(-1)
297
+ ?.replace(/[^a-z\d]+/giu, "_")
298
+ .replace(/^_+|_+$/gu, "")
299
+ .toLowerCase() ?? "";
300
+ }
301
+
302
+ function hiddenDynamicTool(item: JsonRecord): boolean {
303
+ const tool = normalizedToolName(item.tool);
304
+ // These calls have a richer first-class event emitted alongside the router
305
+ // row. Keeping both produced duplicate generic activities on Android.
306
+ return tool === "exec"
307
+ || tool === "exec_command"
308
+ || tool === "write_stdin"
309
+ || tool === "update_plan"
310
+ || tool === "request_user_input";
311
+ }
312
+
313
+ function toolArguments(value: unknown): JsonRecord | null {
314
+ const direct = record(value);
315
+ if (direct) return direct;
316
+ if (typeof value !== "string" || value.length > ANDROID_REMOTE_TOOL_TEXT_LIMIT_BYTES) return null;
317
+ try {
318
+ return record(JSON.parse(value));
319
+ } catch {
320
+ return null;
321
+ }
322
+ }
323
+
324
+ function mcpToolTitle(serverValue: unknown, toolValue: unknown): string {
325
+ const server = humanizeToolToken(serverValue);
326
+ const tool = humanizeToolToken(toolValue);
327
+ if (server && tool && server.toLowerCase() !== tool.toLowerCase()) {
328
+ return withConciseDetail(`Using ${server}`, tool);
329
+ }
330
+ return server || tool ? `Using ${server || tool}` : "Using connected tool";
331
+ }
332
+
333
+ function dynamicToolPresentation(item: JsonRecord): ToolPresentation | null {
334
+ const tool = normalizedToolName(item.tool);
335
+ const argumentsRecord = toolArguments(item.arguments);
336
+ const rawToolParts = text(item.tool).split("__").filter(Boolean);
337
+ if (rawToolParts[0]?.toLowerCase() === "mcp" && rawToolParts.length >= 3) {
338
+ return {
339
+ itemType: "dynamic_tool_call",
340
+ title: mcpToolTitle(rawToolParts.at(-2), rawToolParts.at(-1)),
341
+ };
342
+ }
343
+ if (tool === "view_image" || tool === "image_view") {
344
+ return {
345
+ itemType: "image_view",
346
+ title: withConciseDetail("Viewing Image", argumentPathDetail(argumentsRecord)),
347
+ };
348
+ }
349
+ if (tool === "imagegen" || tool === "generate_image" || tool === "generated_image") {
350
+ return { itemType: "image-generation", title: "Image generation" };
351
+ }
352
+ if (tool === "web_search" || tool === "search_web" || tool === "browse_web") {
353
+ return {
354
+ itemType: "web_search",
355
+ title: withConciseDetail("Web Search", concisePublicText(argumentsRecord?.query, 56)),
356
+ };
357
+ }
358
+ if (
359
+ tool === "spawn_agent"
360
+ || tool === "create_agent"
361
+ || tool === "wait_agent"
362
+ || tool === "close_agent"
363
+ || tool === "send_message"
364
+ || tool === "followup_task"
365
+ || tool === "interrupt_agent"
366
+ || tool === "list_agents"
367
+ ) {
368
+ return {
369
+ itemType: "collab_agent_tool_call",
370
+ title: collaborationLabel(tool, item),
371
+ };
372
+ }
373
+ const target = record(argumentsRecord?.target);
374
+ if (
375
+ (tool === "open_in_codex" && text(target?.type).toLowerCase() === "review")
376
+ || tool === "review"
377
+ || tool === "start_review"
378
+ ) {
379
+ return {
380
+ itemType: "review-marker",
381
+ title: withConciseDetail("Review activity", argumentPathDetail(argumentsRecord)),
382
+ };
383
+ }
384
+ if (tool === "read_file" || tool === "read_text_file" || tool === "read_many_files") {
385
+ const fileNames = argumentPathNames(argumentsRecord);
386
+ return {
387
+ itemType: "file-read",
388
+ title: readingFilesTitle(fileNames),
389
+ fileNames,
390
+ requestKind: "file-read",
391
+ };
392
+ }
393
+ if (tool === "search_codebase" || tool === "search_files" || tool === "grep_search") {
394
+ return {
395
+ itemType: "codebase-search",
396
+ title: "Searching the codebase",
397
+ requestKind: "command",
398
+ };
399
+ }
400
+ return null;
401
+ }
402
+
403
+ function commandText(value: unknown): string {
404
+ if (typeof value === "string") return value;
405
+ if (Array.isArray(value)) return value.filter(entry => typeof entry === "string").join(" ");
406
+ return "";
407
+ }
408
+
409
+ function decodeEmbeddedCommand(body: string, quote: '"' | "'" | "`"): string {
410
+ if (quote === '"') {
411
+ try { return JSON.parse(`"${body}"`) as string; }
412
+ catch { return body; }
413
+ }
414
+ return body
415
+ .replaceAll(`\\${quote}`, quote)
416
+ .replaceAll("\\n", "\n")
417
+ .replaceAll("\\r", "\r")
418
+ .replaceAll("\\t", "\t")
419
+ .replaceAll("\\\\", "\\");
420
+ }
421
+
422
+ function publicCommandText(value: unknown): string {
423
+ const command = commandText(value);
424
+ const wrapperIndex = command.search(/\btools\.exec_command\s*\(/u);
425
+ if (wrapperIndex < 0) return command;
426
+ const wrapper = command.slice(wrapperIndex, wrapperIndex + 128 * 1024);
427
+ const match = /\bcmd\s*:\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|`((?:\\.|[^`\\])*)`)/u.exec(wrapper);
428
+ const body = match?.[1] ?? match?.[2] ?? match?.[3];
429
+ if (body === undefined) return command;
430
+ const quote = match?.[1] !== undefined ? '"' : match?.[2] !== undefined ? "'" : "`";
431
+ return decodeEmbeddedCommand(body, quote);
432
+ }
433
+
434
+ const FILE_READ_COMMAND_START = /^(?:(?:cat|head|tail|less|more|bat|wc|stat|file|readlink)\b|sed\s+-n\b)/iu;
435
+
436
+ function commandContainsOnlyFileReads(command: string): boolean {
437
+ let foundRead = false;
438
+ for (const rawSegment of command.split(/&&|\|\||[;|\n]/u)) {
439
+ let segment = rawSegment.trim();
440
+ if (!segment) continue;
441
+ segment = segment.replace(/^(?:(?:then|do|else)\s+)+/iu, "");
442
+ segment = segment.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)+/u, "");
443
+ if (!segment || /^(?:fi|done|then|do|else|true|:)$/iu.test(segment)) continue;
444
+ if (!FILE_READ_COMMAND_START.test(segment)) return false;
445
+ foundRead = true;
446
+ }
447
+ return foundRead;
448
+ }
449
+
450
+ function commandPresentation(item: JsonRecord): ToolPresentation {
451
+ // Recovered Codex history can contain the JavaScript orchestration wrapper
452
+ // rather than the inner shell command. Classify and label only its `cmd`
453
+ // value so workdir, timeout, and output-limit metadata never look like files.
454
+ const command = publicCommandText(item.command);
455
+ if (
456
+ /\bcmd\s*:\s*["'`]\s*(?:rg|ripgrep|grep|find|fd)\b/iu.test(command)
457
+ || /(?:^|[;&|]\s*|\b)(?:rg|ripgrep|grep|find|fd)\b/iu.test(command)
458
+ ) {
459
+ return {
460
+ itemType: "codebase-search",
461
+ title: "Searching the codebase",
462
+ requestKind: "command",
463
+ };
464
+ }
465
+ if (
466
+ commandContainsOnlyFileReads(command)
467
+ ) {
468
+ const fileNames = commandReadFileNames(command);
469
+ if (fileNames.length > 0) {
470
+ return {
471
+ itemType: "file-read",
472
+ title: readingFilesTitle(fileNames),
473
+ fileNames,
474
+ requestKind: "file-read",
475
+ };
476
+ }
477
+ }
478
+ return { itemType: "command_execution", title: "Command Execution", requestKind: "command" };
479
+ }
480
+
481
+ function publicToolTitle(kind: string, item: JsonRecord): string {
482
+ if (kind === "commandExecution") return commandPresentation(item).title;
483
+ if (kind === "fileChange") return "File Change";
484
+ if (kind === "collabAgentToolCall") return collaborationLabel(item.tool, item);
485
+ if (kind === "mcpToolCall") {
486
+ return mcpToolTitle(item.server ?? item.namespace, item.tool);
487
+ }
488
+ if (kind === "dynamicToolCall") {
489
+ const presentation = dynamicToolPresentation(item);
490
+ if (presentation) return presentation.title;
491
+ const action = humanizeToolToken(item.tool);
492
+ return action ? `Using ${action}` : "Using tool";
493
+ }
494
+ if (kind === "webSearch") {
495
+ return withConciseDetail("Web Search", concisePublicText(item.query, 56));
496
+ }
497
+ if (kind === "imageView") {
498
+ return withConciseDetail("Viewing Image", concisePublicBasename(item.path));
499
+ }
500
+ if (kind === "subAgentActivity") return "Sub-agent activity";
501
+ if (kind === "imageGeneration") return "Image generation";
502
+ if (kind === "reviewMarker") return item.state === "entered" ? "Review started" : "Review completed";
503
+ return "Tool Activity";
504
+ }
505
+
506
+ function findPublicNotificationText(value: unknown, depth = 0): string | null {
507
+ if (depth > 5) return null;
508
+ if (typeof value === "string") {
509
+ const normalized = value.trim();
510
+ return normalized && !normalized.startsWith("{") ? normalized : null;
511
+ }
512
+ if (Array.isArray(value)) {
513
+ for (const entry of value) {
514
+ const found = findPublicNotificationText(entry, depth + 1);
515
+ if (found) return found;
516
+ }
517
+ return null;
518
+ }
519
+ const valueRecord = record(value);
520
+ if (!valueRecord) return null;
521
+ for (const key of ["completed", "final_answer", "finalAnswer", "message", "result", "output", "text"]) {
522
+ if (!(key in valueRecord)) continue;
523
+ const found = findPublicNotificationText(valueRecord[key], depth + 1);
524
+ if (found) return found;
525
+ }
526
+ for (const entry of Object.values(valueRecord)) {
527
+ if (typeof entry !== "object" || entry === null) continue;
528
+ const found = findPublicNotificationText(entry, depth + 1);
529
+ if (found) return found;
530
+ }
531
+ return null;
532
+ }
533
+
534
+ function sanitizeAgentMessage(value: unknown): string {
535
+ const source = text(value);
536
+ return source.replace(
537
+ /<subagent_notification>\s*([\s\S]*?)\s*<\/subagent_notification>/giu,
538
+ (_match, body: string) => {
539
+ let parsed: unknown = null;
540
+ try {
541
+ parsed = JSON.parse(body);
542
+ } catch {
543
+ parsed = null;
544
+ }
545
+ const publicText = findPublicNotificationText(parsed)
546
+ ?? body.replace(/<\/?[^>]+>/gu, " ").replace(/\s+/gu, " ").trim();
547
+ return publicText || "An agent finished its task.";
548
+ },
549
+ );
550
+ }
551
+
552
+ interface ExtractedProposedPlan {
553
+ messageText: string;
554
+ planMarkdown: string;
555
+ }
556
+
557
+ /** One Codex turn can expose its final plan through both a tagged assistant
558
+ * answer and a native `plan` item. Android must treat those as one artifact. */
559
+ export function codexProposedPlanId(turnId: string | null, itemId: string): string {
560
+ return turnId ? `${turnId}:proposed-plan` : `${itemId}:proposed-plan`;
561
+ }
562
+
563
+ /**
564
+ * Newer Codex Desktop builds can persist the official Plan-mode result as a
565
+ * final agentMessage instead of a dedicated `plan` item. Keep the wire tags
566
+ * private and recover the same first-class plan shape used by older builds.
567
+ */
568
+ function extractTaggedProposedPlan(value: string): ExtractedProposedPlan | null {
569
+ const match = /(?:^|\r?\n)[ \t]*<proposed_plan>[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]*<\/proposed_plan>[ \t]*(?=\r?\n|$)/u.exec(value);
570
+ const planMarkdown = match?.[1]?.trim();
571
+ if (!match || !planMarkdown) return null;
572
+ return {
573
+ messageText: `${value.slice(0, match.index)}${value.slice(match.index + match[0].length)}`.trim(),
574
+ planMarkdown,
575
+ };
576
+ }
577
+
578
+ function collaborationModeOf(value: unknown): string {
579
+ const mode = record(value);
580
+ return text(mode?.mode).trim().toLowerCase();
581
+ }
582
+
583
+ function turnIsPlanMode(turn: JsonRecord): boolean {
584
+ const params = record(turn.params);
585
+ return collaborationModeOf(params?.collaborationMode) === "plan"
586
+ || collaborationModeOf(turn.collaborationMode) === "plan";
587
+ }
588
+
589
+ function isPrivateSyntheticUserMessage(value: string): boolean {
590
+ const trimmed = value.trim();
591
+ if (/^<codex_delegation>[\s\S]*<\/codex_delegation>$/u.test(trimmed)) return true;
592
+ if (/^<subagent_notification>[\s\S]*<\/subagent_notification>$/u.test(trimmed)) return true;
593
+ const withoutBootstrapContext = trimmed
594
+ .replace(/<recommended_plugins>[\s\S]*?<\/recommended_plugins>/gu, "")
595
+ .replace(/<environment_context>[\s\S]*?<\/environment_context>/gu, "")
596
+ .trim();
597
+ return withoutBootstrapContext.length === 0 && withoutBootstrapContext !== trimmed;
598
+ }
599
+
600
+ function isoFromSeconds(value: unknown, fallback = Date.now()): string {
601
+ const seconds = finite(value);
602
+ return new Date(seconds === null ? fallback : seconds * 1000).toISOString();
603
+ }
604
+
605
+ function stableId(prefix: string, value: string): string {
606
+ return `${prefix}-${createHash("sha256").update(value).digest("hex").slice(0, 20)}`;
607
+ }
608
+
609
+ function cwdOf(thread: JsonRecord): string {
610
+ return text(thread.cwd, "Unknown workspace");
611
+ }
612
+
613
+ function projectIdOf(thread: JsonRecord, cwd: string): string {
614
+ return text(thread.androidRemoteProjectId).trim()
615
+ || stableId("codex-project", cwd.toLowerCase());
616
+ }
617
+
618
+ function projectForThread(
619
+ thread: JsonRecord,
620
+ cwd: string,
621
+ createdAt: string,
622
+ updatedAt: string,
623
+ ): JsonRecord {
624
+ const title = basename(cwd.replace(/[\\/]+$/, "")) || cwd;
625
+ const workspaceRoot = text(thread.androidRemoteProjectWorkspaceRoot).trim() || cwd;
626
+ return {
627
+ id: projectIdOf(thread, cwd),
628
+ title: text(thread.androidRemoteProjectTitle).trim() || title,
629
+ workspaceRoot,
630
+ repositoryIdentity: null,
631
+ defaultModelSelection: null,
632
+ scripts: [],
633
+ createdAt,
634
+ updatedAt,
635
+ };
636
+ }
637
+
638
+ function providerInstanceId(thread: JsonRecord): string {
639
+ return text(thread.androidRemoteProviderInstanceId).trim()
640
+ || text(thread.modelProvider, "openai");
641
+ }
642
+
643
+ function modelSelection(thread: JsonRecord): JsonRecord {
644
+ const revision = finite(thread.androidRemoteModelSelectionRevision);
645
+ return {
646
+ instanceId: providerInstanceId(thread),
647
+ model: text(thread.androidRemoteModel).trim() || text(thread.model, "gpt-5.6-sol"),
648
+ ...(thread.androidRemoteModelOptions ? { options: thread.androidRemoteModelOptions } : {}),
649
+ ...(revision !== null ? { revision: Math.max(0, Math.floor(revision)) } : {}),
650
+ ...(text(thread.androidRemoteModelSelectionUpdatedAt).trim()
651
+ ? { updatedAt: text(thread.androidRemoteModelSelectionUpdatedAt).trim() }
652
+ : {}),
653
+ };
654
+ }
655
+
656
+ function titleOf(thread: JsonRecord): string {
657
+ return text(thread.androidRemoteTitle).trim()
658
+ || text(thread.name).trim()
659
+ || text(thread.preview).trim().slice(0, 256)
660
+ || "Codex task";
661
+ }
662
+
663
+ function turnRows(thread: JsonRecord): JsonRecord[] {
664
+ return Array.isArray(thread.turns)
665
+ ? thread.turns.flatMap(value => record(value) ? [record(value)!] : [])
666
+ : [];
667
+ }
668
+
669
+ function turnState(turn: JsonRecord): "running" | "interrupted" | "completed" | "error" {
670
+ const status = turn.status ?? turn.state ?? turn.turnStatus ?? turn.turn_status;
671
+ const normalized = normalizedCanonicalTurnStatus(status);
672
+ if (canonicalTurnStatusIsActive(status) || !canonicalTurnStatusIsTerminal(status)) {
673
+ return "running";
674
+ }
675
+ if (normalized.includes("failed") || normalized.includes("error")) return "error";
676
+ if (
677
+ normalized.includes("interrupt")
678
+ || normalized.includes("cancel")
679
+ || normalized.includes("stopped")
680
+ || normalized.includes("aborted")
681
+ ) {
682
+ return "interrupted";
683
+ }
684
+ return "completed";
685
+ }
686
+
687
+ function latestTurn(thread: JsonRecord): JsonRecord | null {
688
+ const projectedState = text(thread.androidRemoteLatestTurnState);
689
+ const hasProjectedLifecycle =
690
+ projectedState === "running" ||
691
+ projectedState === "completed" ||
692
+ projectedState === "interrupted" ||
693
+ projectedState === "error";
694
+ const turns = turnRows(thread);
695
+ if (
696
+ hasProjectedLifecycle
697
+ && projectedTurnLifecycleWins(turns, {
698
+ state: projectedState,
699
+ turnId: thread.androidRemoteLatestTurnId,
700
+ occurredAt: thread.androidRemoteLatestTurnAt,
701
+ })
702
+ ) {
703
+ const occurredAt = text(thread.androidRemoteLatestTurnAt)
704
+ || isoFromSeconds(thread.updatedAt);
705
+ return {
706
+ turnId: text(
707
+ thread.androidRemoteLatestTurnId,
708
+ stableId("turn", `${text(thread.id)}:${occurredAt}`),
709
+ ),
710
+ state: projectedState,
711
+ requestedAt: occurredAt,
712
+ startedAt: projectedState === "running" ? occurredAt : null,
713
+ completedAt: projectedState === "running" ? null : occurredAt,
714
+ assistantMessageId: null,
715
+ };
716
+ }
717
+ const turn = canonicalTurnsNewestFirst(turns)[0];
718
+ if (!turn) return null;
719
+ const state = turnState(turn);
720
+ return {
721
+ turnId: text(turn.id, stableId("turn", `${text(thread.id)}:${turns.length}`)),
722
+ state,
723
+ requestedAt: isoFromSeconds(turn.startedAt, Date.parse(isoFromSeconds(thread.updatedAt))),
724
+ startedAt: finite(turn.startedAt) === null ? null : isoFromSeconds(turn.startedAt),
725
+ completedAt: finite(turn.completedAt) === null ? null : isoFromSeconds(turn.completedAt),
726
+ assistantMessageId: lastAssistantMessageId(turn),
727
+ };
728
+ }
729
+
730
+ function lastAssistantMessageId(turn: JsonRecord): string | null {
731
+ if (!Array.isArray(turn.items)) return null;
732
+ for (let index = turn.items.length - 1; index >= 0; index -= 1) {
733
+ const item = record(turn.items[index]);
734
+ if (item?.type === "agentMessage") return text(item.id) || null;
735
+ }
736
+ return null;
737
+ }
738
+
739
+ function sessionOf(thread: JsonRecord, updatedAt: string): JsonRecord {
740
+ const status = record(thread.status);
741
+ const type = text(status?.type);
742
+ const turns = turnRows(thread);
743
+ const activity = canonicalTurnActivitySnapshot(turns);
744
+ const failed = [...turns].reverse().find(turn => turn.status === "failed");
745
+ const error = failed ? record(failed.error) : null;
746
+ const canonicalErrorMessage = boundedTurnErrorMessage(error);
747
+ const projectedState = text(thread.androidRemoteLatestTurnState);
748
+ const projectedTurnId = text(thread.androidRemoteLatestTurnId) || null;
749
+ const projectedLifecycleWins = projectedTurnLifecycleWins(turns, {
750
+ state: projectedState,
751
+ turnId: projectedTurnId,
752
+ occurredAt: thread.androidRemoteLatestTurnAt,
753
+ });
754
+ const projectedRunning = projectedLifecycleWins && projectedState === "running";
755
+ const projectedError = projectedLifecycleWins && projectedState === "error";
756
+ const projectedErrorMessage = projectedError
757
+ ? boundedTurnErrorMessage(thread.androidRemoteLatestTurnError)
758
+ : "";
759
+ const canonicalRunning = !projectedLifecycleWins && activity.active;
760
+ return {
761
+ threadId: text(thread.id),
762
+ // Codex also reports an open/mounted Desktop conversation as `active`
763
+ // after its turn has completed. Only an explicit running task marker or
764
+ // an in-progress canonical turn proves that Android should keep showing
765
+ // Thinking and the Stop action after a reconnect.
766
+ status: projectedRunning || canonicalRunning
767
+ ? "running"
768
+ : projectedError || type === "systemError"
769
+ ? "error"
770
+ : "idle",
771
+ // Remodex uses providerName as the driver-kind lock and
772
+ // providerInstanceId as the exact routing key. Every Android Remote task
773
+ // is driven by the Codex bridge even when Remodex routes its selected
774
+ // model to Anthropic, Gemini, a custom provider, or a combo.
775
+ providerName: "codex",
776
+ providerInstanceId: providerInstanceId(thread),
777
+ runtimeMode: "full-access",
778
+ activeTurnId: projectedRunning
779
+ ? projectedTurnId
780
+ : canonicalRunning
781
+ ? activity.activeTurnId || null
782
+ : null,
783
+ lastError: projectedError
784
+ ? projectedErrorMessage || canonicalErrorMessage || null
785
+ : canonicalErrorMessage || null,
786
+ updatedAt,
787
+ };
788
+ }
789
+
790
+ function userInputText(content: unknown): string {
791
+ if (!Array.isArray(content)) return "";
792
+ const parts: string[] = [];
793
+ const contextReferences: string[] = [];
794
+ for (const raw of content) {
795
+ const input = record(raw);
796
+ if (!input) continue;
797
+ if (input.type === "text") parts.push(publicUserMessageText(input.text));
798
+ else if (input.type === "localImage") parts.push(`[Image: ${text(input.path, "attachment")}]`);
799
+ else if (input.type === "image") parts.push(`[Image: ${text(input.url, "attachment")}]`);
800
+ else if (input.type === "localAudio" || input.type === "audio") parts.push("[Audio attachment]");
801
+ else if (input.type === "mention" || input.type === "skill") {
802
+ contextReferences.push(`$${text(input.name, "reference")}`);
803
+ }
804
+ }
805
+ const visibleParts = parts.filter(Boolean).join("\n");
806
+ const visibleContext = contextReferences.filter(Boolean).join(" ");
807
+ if (visibleContext && visibleParts) return `${visibleContext}\n\n${visibleParts}`;
808
+ return visibleContext || visibleParts;
809
+ }
810
+
811
+ type CollapsibleAndroidActivityType = "codebase-search" | "file-read";
812
+
813
+ function collapsibleAndroidActivityType(activity: JsonRecord): CollapsibleAndroidActivityType | null {
814
+ const itemType = text(record(activity.payload)?.itemType);
815
+ return itemType === "codebase-search" || itemType === "file-read" ? itemType : null;
816
+ }
817
+
818
+ function activityFileNames(activity: JsonRecord): string[] {
819
+ const fileNames = record(activity.payload)?.fileNames;
820
+ if (!Array.isArray(fileNames)) return [];
821
+ return fileNames.flatMap(value => typeof value === "string" && value.trim() ? [value.trim()] : []);
822
+ }
823
+
824
+ function mergeConsecutiveAndroidActivities(
825
+ previous: JsonRecord,
826
+ next: JsonRecord,
827
+ activityType: CollapsibleAndroidActivityType,
828
+ ): JsonRecord {
829
+ const previousPayload = record(previous.payload) ?? {};
830
+ const nextPayload = record(next.payload) ?? {};
831
+ const previousData = record(previousPayload.data) ?? {};
832
+ const nextData = record(nextPayload.data) ?? {};
833
+ const fileNames = activityType === "file-read"
834
+ ? [...new Set([...activityFileNames(previous), ...activityFileNames(next)])]
835
+ : [];
836
+ const title = activityType === "file-read"
837
+ ? readingFilesTitle(fileNames)
838
+ : "Searching the codebase";
839
+ const previousStatus = text(previousPayload.status);
840
+ const nextStatus = text(nextPayload.status);
841
+ const stillRunning = nextStatus === "inProgress" || nextStatus === "in_progress" || nextStatus === "running";
842
+ const failed = previous.tone === "error"
843
+ || next.tone === "error"
844
+ || previousStatus === "failed"
845
+ || nextStatus === "failed";
846
+ const status = stillRunning ? nextStatus : failed ? "failed" : nextStatus || previousStatus;
847
+
848
+ return {
849
+ ...previous,
850
+ ...next,
851
+ // The first call owns the visual identity and timeline position. Later
852
+ // calls update that same row while retaining their concise public details.
853
+ id: previous.id,
854
+ kind: previous.kind,
855
+ summary: title,
856
+ tone: failed && !stillRunning ? "error" : next.tone,
857
+ turnId: previous.turnId,
858
+ sequence: previous.sequence,
859
+ createdAt: previous.createdAt,
860
+ payload: {
861
+ ...previousPayload,
862
+ ...nextPayload,
863
+ itemId: previousPayload.itemId ?? previous.id,
864
+ itemType: activityType,
865
+ title,
866
+ ...(status ? { status } : {}),
867
+ ...(fileNames.length > 0 ? { fileNames } : {}),
868
+ data: {
869
+ ...previousData,
870
+ ...nextData,
871
+ toolCallId: previousData.toolCallId ?? previous.id,
872
+ },
873
+ },
874
+ };
875
+ }
876
+
877
+ function reasoningActivitySignature(activity: JsonRecord): string | null {
878
+ if (text(activity.kind) !== "task.progress") return null;
879
+ const payload = record(activity.payload) ?? {};
880
+ return JSON.stringify({
881
+ turnId: text(activity.turnId) || null,
882
+ summary: text(activity.summary),
883
+ summaryParts: Array.isArray(payload.summaryParts)
884
+ ? payload.summaryParts.flatMap(value => typeof value === "string" ? [value] : [])
885
+ : [],
886
+ summaryAvailable: payload.summaryAvailable === true,
887
+ });
888
+ }
889
+
890
+ function mergeConsecutiveReasoningActivities(
891
+ previous: JsonRecord,
892
+ next: JsonRecord,
893
+ ): JsonRecord {
894
+ const previousPayload = record(previous.payload) ?? {};
895
+ const nextPayload = record(next.payload) ?? {};
896
+ return {
897
+ ...previous,
898
+ ...next,
899
+ id: previous.id,
900
+ turnId: previous.turnId,
901
+ sequence: previous.sequence,
902
+ createdAt: previous.createdAt,
903
+ payload: {
904
+ ...previousPayload,
905
+ ...nextPayload,
906
+ itemId: previousPayload.itemId ?? previous.id,
907
+ },
908
+ };
909
+ }
910
+
911
+ function messagesAndActivity(
912
+ thread: JsonRecord,
913
+ options: { includeCompletedCommandActivities?: boolean } = {},
914
+ ): {
915
+ messages: JsonRecord[];
916
+ activities: JsonRecord[];
917
+ proposedPlans: JsonRecord[];
918
+ } {
919
+ const messages: JsonRecord[] = [];
920
+ const activities: JsonRecord[] = [];
921
+ const proposedPlans: JsonRecord[] = [];
922
+ let previousVisibleActivityIndex: number | null = null;
923
+ let previousVisibleSemanticActivity: {
924
+ index: number;
925
+ itemType: CollapsibleAndroidActivityType;
926
+ turnId: string | null;
927
+ } | null = null;
928
+ const resetSemanticActivityRun = (): void => {
929
+ previousVisibleActivityIndex = null;
930
+ previousVisibleSemanticActivity = null;
931
+ };
932
+ const appendActivity = (activity: JsonRecord): void => {
933
+ const itemType = collapsibleAndroidActivityType(activity);
934
+ const turnId = text(activity.turnId) || null;
935
+ if (
936
+ itemType !== null
937
+ && previousVisibleSemanticActivity?.itemType === itemType
938
+ && previousVisibleSemanticActivity.turnId === turnId
939
+ && turnId !== null
940
+ ) {
941
+ const index = previousVisibleSemanticActivity.index;
942
+ activities[index] = mergeConsecutiveAndroidActivities(activities[index]!, activity, itemType);
943
+ previousVisibleActivityIndex = index;
944
+ return;
945
+ }
946
+ if (previousVisibleActivityIndex !== null) {
947
+ const previous = activities[previousVisibleActivityIndex];
948
+ const previousSignature = previous ? reasoningActivitySignature(previous) : null;
949
+ if (
950
+ previousSignature !== null
951
+ && previousSignature === reasoningActivitySignature(activity)
952
+ ) {
953
+ activities[previousVisibleActivityIndex] =
954
+ mergeConsecutiveReasoningActivities(previous!, activity);
955
+ previousVisibleSemanticActivity = null;
956
+ return;
957
+ }
958
+ }
959
+ activities.push(activity);
960
+ previousVisibleActivityIndex = activities.length - 1;
961
+ previousVisibleSemanticActivity = itemType === null
962
+ ? null
963
+ : { index: activities.length - 1, itemType, turnId };
964
+ };
965
+ const upsertProposedPlan = (plan: JsonRecord): void => {
966
+ resetSemanticActivityRun();
967
+ const id = text(plan.id);
968
+ const index = proposedPlans.findIndex(candidate => text(candidate.id) === id);
969
+ if (index < 0) {
970
+ proposedPlans.push(plan);
971
+ return;
972
+ }
973
+ const existing = proposedPlans[index]!;
974
+ const existingMarkdown = text(existing.planMarkdown);
975
+ const nextMarkdown = text(plan.planMarkdown);
976
+ const existingSequence = finite(existing.sequence);
977
+ const nextSequence = finite(plan.sequence);
978
+ proposedPlans[index] = {
979
+ ...existing,
980
+ ...plan,
981
+ sequence: existingSequence === null
982
+ ? nextSequence
983
+ : nextSequence === null
984
+ ? existingSequence
985
+ : Math.min(existingSequence, nextSequence),
986
+ planMarkdown: nextMarkdown.length >= existingMarkdown.length ? nextMarkdown : existingMarkdown,
987
+ createdAt: text(existing.createdAt) || text(plan.createdAt),
988
+ };
989
+ };
990
+ const markLatestProposedPlanImplemented = (implementedAt: string): void => {
991
+ for (let index = proposedPlans.length - 1; index >= 0; index -= 1) {
992
+ const plan = proposedPlans[index]!;
993
+ if (plan.implementedAt !== null) continue;
994
+ proposedPlans[index] = {
995
+ ...plan,
996
+ implementedAt,
997
+ implementationThreadId: text(thread.id) || null,
998
+ updatedAt: implementedAt,
999
+ };
1000
+ return;
1001
+ }
1002
+ };
1003
+ let sequence = 0;
1004
+ const turns = turnRows(thread);
1005
+ const hasCanonicalPlanInThread = turns.some((candidate) => {
1006
+ const candidateItems = Array.isArray(candidate.items) ? candidate.items : [];
1007
+ return candidateItems.some((raw) => {
1008
+ const item = record(raw);
1009
+ if (!item) return false;
1010
+ return item.type === "plan"
1011
+ || item.type === "agentMessage"
1012
+ && extractTaggedProposedPlan(sanitizeAgentMessage(item.text)) !== null;
1013
+ });
1014
+ });
1015
+ const hasImplementationRequestInThread = turns.some((candidate) => {
1016
+ const candidateItems = Array.isArray(candidate.items) ? candidate.items : [];
1017
+ return candidateItems.some((raw) => {
1018
+ const item = record(raw);
1019
+ return item?.type === "userMessage"
1020
+ && userInputText(item.content).trim() === "Implement this plan.";
1021
+ });
1022
+ });
1023
+ for (const [turnIndex, turn] of turns.entries()) {
1024
+ const turnId = text(turn.id) || null;
1025
+ const createdAt = isoFromSeconds(turn.startedAt, Date.parse(isoFromSeconds(thread.createdAt)));
1026
+ const updatedAt = isoFromSeconds(turn.completedAt, Date.parse(createdAt));
1027
+ const streaming = turn.status === "inProgress";
1028
+ const items = Array.isArray(turn.items) ? turn.items : [];
1029
+ const attachmentMessageIndexes = new Map<string, number>();
1030
+ const hasCanonicalPlanItem = items.some((raw) => {
1031
+ const item = record(raw);
1032
+ if (!item) return false;
1033
+ return item.type === "plan"
1034
+ || item.type === "agentMessage"
1035
+ && extractTaggedProposedPlan(sanitizeAgentMessage(item.text)) !== null;
1036
+ });
1037
+ // App-server currently omits per-turn collaboration metadata from some
1038
+ // thread/read responses. A thread-level Plan marker is only a last-resort
1039
+ // recovery for a first, malformed plan turn. Once a canonical plan or an
1040
+ // implementation request exists, later normal answers must stay messages.
1041
+ const planMode = turnIsPlanMode(turn)
1042
+ || turnIndex === turns.length - 1
1043
+ && text(thread.androidRemoteInteractionMode) === "plan"
1044
+ && !hasCanonicalPlanInThread
1045
+ && !hasImplementationRequestInThread;
1046
+ for (const raw of items) {
1047
+ const item = record(raw);
1048
+ if (!item) continue;
1049
+ const itemSequence = sequence++;
1050
+ const id = text(item.id, stableId("item", `${text(thread.id)}:${itemSequence}`));
1051
+ // Command rows are transient progress feedback. Completed snapshots keep
1052
+ // the lasting reasoning/file/tool structure but omit terminal history,
1053
+ // matching the compact completed-task presentation on Android.
1054
+ if (
1055
+ item.type === "commandExecution" &&
1056
+ !streaming &&
1057
+ commandPresentation(item).itemType === "command_execution" &&
1058
+ options.includeCompletedCommandActivities !== true
1059
+ ) {
1060
+ continue;
1061
+ }
1062
+ if (item.type === "userMessage") {
1063
+ const messageText = userInputText(item.content);
1064
+ if (!messageText || isPrivateSyntheticUserMessage(messageText)) continue;
1065
+ // The Android plan action intentionally writes this exact visible
1066
+ // prompt into Codex history. Recovering the relationship from that
1067
+ // durable transcript keeps the action card dismissed across Remodex
1068
+ // restarts, even though app-server does not persist source plan ids.
1069
+ if (messageText.trim() === "Implement this plan.") {
1070
+ markLatestProposedPlanImplemented(createdAt);
1071
+ }
1072
+ resetSemanticActivityRun();
1073
+ const messageId = text(item.clientId).trim() || id;
1074
+ const nextMessage: JsonRecord = {
1075
+ id: messageId,
1076
+ role: "user",
1077
+ text: messageText,
1078
+ attachments: [],
1079
+ turnId,
1080
+ sequence: itemSequence,
1081
+ streaming: false,
1082
+ createdAt,
1083
+ updatedAt: createdAt,
1084
+ };
1085
+ const attachmentIdentity = attachmentUserMessageIdentityKey(messageText);
1086
+ const duplicateIndex = attachmentIdentity === null
1087
+ ? undefined
1088
+ : attachmentMessageIndexes.get(attachmentIdentity);
1089
+ if (duplicateIndex === undefined) {
1090
+ messages.push(nextMessage);
1091
+ if (attachmentIdentity !== null) {
1092
+ attachmentMessageIndexes.set(attachmentIdentity, messages.length - 1);
1093
+ }
1094
+ } else {
1095
+ const existing = messages[duplicateIndex]!;
1096
+ const existingText = text(existing.text);
1097
+ // Prefer the public app-server form because Android can open its
1098
+ // path. Preserve the first/client id and timeline position.
1099
+ messages[duplicateIndex] = {
1100
+ ...nextMessage,
1101
+ id: text(existing.id) || messageId,
1102
+ text: messageText.includes("[Image:") ? messageText : existingText,
1103
+ sequence: existing.sequence,
1104
+ createdAt: existing.createdAt,
1105
+ };
1106
+ }
1107
+ continue;
1108
+ }
1109
+ if (item.type === "agentMessage") {
1110
+ // Local compaction writes its generated handoff as a final-looking
1111
+ // assistant item immediately before the private `compacted` record.
1112
+ // It is context for Codex, not conversation content for the phone.
1113
+ const messageText = sanitizeAgentMessage(item.text);
1114
+ if (text(item.phase) === "final_answer" && /^#\s*Handoff Summary\b/iu.test(messageText)) {
1115
+ continue;
1116
+ }
1117
+ const taggedPlan = streaming ? null : extractTaggedProposedPlan(messageText);
1118
+ const fallbackPlan = !streaming
1119
+ && !taggedPlan
1120
+ && !hasCanonicalPlanItem
1121
+ && text(item.phase) === "final_answer"
1122
+ && messageText.trim()
1123
+ && (planMode || item.androidRemotePlanMode === true)
1124
+ ? messageText.trim()
1125
+ : null;
1126
+ if (taggedPlan || fallbackPlan) {
1127
+ upsertProposedPlan({
1128
+ id: codexProposedPlanId(turnId, id),
1129
+ turnId,
1130
+ sequence: itemSequence,
1131
+ planMarkdown: taggedPlan?.planMarkdown ?? fallbackPlan,
1132
+ implementedAt: null,
1133
+ implementationThreadId: null,
1134
+ createdAt,
1135
+ updatedAt,
1136
+ });
1137
+ }
1138
+ if (!fallbackPlan && (!taggedPlan || taggedPlan.messageText)) {
1139
+ resetSemanticActivityRun();
1140
+ messages.push({
1141
+ id,
1142
+ role: "assistant",
1143
+ text: taggedPlan?.messageText ?? messageText,
1144
+ // Codex marks intermediate commentary separately from the real
1145
+ // final answer. Android needs this boundary so it never presents a
1146
+ // still-running response as completed between tool calls.
1147
+ phase: text(item.phase) || null,
1148
+ attachments: [],
1149
+ turnId,
1150
+ sequence: itemSequence,
1151
+ streaming,
1152
+ createdAt,
1153
+ updatedAt,
1154
+ });
1155
+ }
1156
+ continue;
1157
+ }
1158
+ if (item.type === "plan") {
1159
+ const planMarkdown = text(item.text).trim();
1160
+ if (planMarkdown) {
1161
+ if (streaming) {
1162
+ resetSemanticActivityRun();
1163
+ messages.push({
1164
+ id,
1165
+ role: "assistant",
1166
+ text: planMarkdown,
1167
+ phase: "commentary",
1168
+ attachments: [],
1169
+ turnId,
1170
+ sequence: itemSequence,
1171
+ streaming: true,
1172
+ createdAt,
1173
+ updatedAt,
1174
+ });
1175
+ } else {
1176
+ upsertProposedPlan({
1177
+ id: codexProposedPlanId(turnId, id),
1178
+ turnId,
1179
+ sequence: itemSequence,
1180
+ planMarkdown,
1181
+ implementedAt: null,
1182
+ implementationThreadId: null,
1183
+ createdAt,
1184
+ updatedAt,
1185
+ });
1186
+ }
1187
+ }
1188
+ continue;
1189
+ }
1190
+ const activity = activityFromItem(item, turn, id, turnId, createdAt, itemSequence);
1191
+ if (activity) appendActivity(activity);
1192
+ }
1193
+ }
1194
+ return { messages, activities, proposedPlans };
1195
+ }
1196
+
1197
+ function activityFromItem(
1198
+ item: JsonRecord,
1199
+ turn: JsonRecord,
1200
+ id: string,
1201
+ turnId: string | null,
1202
+ createdAt: string,
1203
+ sequence: number,
1204
+ ): JsonRecord | null {
1205
+ const kind = text(item.type);
1206
+ const contextCompaction = kind === "contextCompaction";
1207
+ const reviewMarker = kind === "reviewMarker";
1208
+ const subAgentActivity = kind === "subAgentActivity";
1209
+ const imageGeneration = kind === "imageGeneration";
1210
+ const planUpdate = kind === "planUpdate";
1211
+ const userInputRequest = kind === "userInputRequest";
1212
+ const dynamicPresentation = kind === "dynamicToolCall" ? dynamicToolPresentation(item) : null;
1213
+ const commandToolPresentation = kind === "commandExecution" ? commandPresentation(item) : null;
1214
+ if (kind === "dynamicToolCall" && hiddenDynamicTool(item)) return null;
1215
+ const itemType = kind === "commandExecution"
1216
+ ? commandToolPresentation?.itemType ?? "command_execution"
1217
+ : kind === "fileChange"
1218
+ ? "file_change"
1219
+ : kind === "mcpToolCall"
1220
+ ? "mcp_tool_call"
1221
+ : kind === "dynamicToolCall"
1222
+ ? dynamicPresentation?.itemType ?? "dynamic_tool_call"
1223
+ : kind === "collabAgentToolCall"
1224
+ ? "collab_agent_tool_call"
1225
+ : kind === "webSearch"
1226
+ ? "web_search"
1227
+ : kind === "imageView"
1228
+ ? "image_view"
1229
+ : subAgentActivity
1230
+ ? "sub-agent-activity"
1231
+ : imageGeneration
1232
+ ? "image-generation"
1233
+ : planUpdate
1234
+ ? "plan-update"
1235
+ : reviewMarker
1236
+ ? "review-marker"
1237
+ : contextCompaction
1238
+ ? "context-compaction"
1239
+ : null;
1240
+ let tone: "info" | "tool" | "approval" | "error" = "tool";
1241
+ let summary = "";
1242
+ if (kind === "reasoning") {
1243
+ const rows = Array.isArray(item.summary)
1244
+ ? item.summary.flatMap(row => typeof row === "string" && row.trim() ? [row.trim()] : [])
1245
+ : [];
1246
+ const publicSummary = rows.at(-1)?.slice(0, 240);
1247
+ const turnState = text(turn.status).replace(/[^a-z0-9]/giu, "").toLowerCase();
1248
+ const reasoningInProgress = item.status === "inProgress"
1249
+ || turnState === "inprogress"
1250
+ || turnState === "running";
1251
+ // A reasoning item with no public summary still proves that reasoning was used. Surface that
1252
+ // state without reading or relabelling the item's private/encrypted `content` field.
1253
+ summary = publicSummary
1254
+ || (reasoningInProgress ? "Reasoning" : "Reasoning used · summary unavailable");
1255
+ tone = "info";
1256
+ } else if (contextCompaction) {
1257
+ summary = item.status === "inProgress"
1258
+ ? "Context automatically compacting"
1259
+ : "Context compacted";
1260
+ tone = "info";
1261
+ } else if (kind === "commandExecution") {
1262
+ summary = commandToolPresentation?.title ?? "Command Execution";
1263
+ tone = item.status === "failed" ? "error" : "tool";
1264
+ } else if (kind === "fileChange") {
1265
+ summary = "File Change";
1266
+ tone = item.status === "failed" ? "error" : "tool";
1267
+ } else if (kind === "mcpToolCall") {
1268
+ summary = publicToolTitle(kind, item);
1269
+ tone = item.status === "failed" ? "error" : "tool";
1270
+ } else if (kind === "dynamicToolCall") {
1271
+ // `exec` is Codex's internal JavaScript tool router, not a user-facing
1272
+ // activity name. Nested real tools are projected under their own type;
1273
+ // exposing this wrapper produced the misleading "Using Exec" row.
1274
+ if (text(item.tool).trim().toLowerCase() === "exec") return null;
1275
+ summary = publicToolTitle(kind, item);
1276
+ tone = item.status === "failed" ? "error" : "tool";
1277
+ } else if (kind === "collabAgentToolCall") {
1278
+ summary = publicToolTitle(kind, item);
1279
+ tone = item.status === "failed" ? "error" : "tool";
1280
+ } else if (kind === "webSearch") {
1281
+ summary = publicToolTitle(kind, item);
1282
+ tone = item.status === "failed" ? "error" : "tool";
1283
+ } else if (kind === "imageView") {
1284
+ summary = publicToolTitle(kind, item);
1285
+ tone = item.status === "failed" ? "error" : "tool";
1286
+ } else if (subAgentActivity) {
1287
+ summary = text(item.activity).trim() || "Sub-agent activity";
1288
+ tone = "info";
1289
+ } else if (imageGeneration) {
1290
+ summary = text(item.status).trim() || "Image generation";
1291
+ tone = item.status === "failed" ? "error" : "tool";
1292
+ } else if (planUpdate) {
1293
+ summary = "Plan updated";
1294
+ tone = "info";
1295
+ } else if (userInputRequest) {
1296
+ summary = item.status === "inProgress" ? "User input requested" : "User input submitted";
1297
+ tone = item.status === "failed" ? "error" : "info";
1298
+ } else if (reviewMarker) {
1299
+ summary = item.state === "entered" ? "Review started" : "Review completed";
1300
+ tone = "info";
1301
+ } else {
1302
+ return null;
1303
+ }
1304
+ const status = text(item.status).trim() || undefined;
1305
+ const requestKind = commandToolPresentation?.requestKind ?? dynamicPresentation?.requestKind;
1306
+ const fileNames = commandToolPresentation?.fileNames ?? dynamicPresentation?.fileNames ?? [];
1307
+ const fileChanges = kind === "fileChange" ? publicFileChanges(item) : [];
1308
+ const turnDurationMs = finite(turn.durationMs);
1309
+ const turnTiming = turnDurationMs === null ? {} : { turnDurationMs };
1310
+ const title = kind === "reasoning" || contextCompaction || planUpdate || userInputRequest
1311
+ ? summary
1312
+ : publicToolTitle(kind, item);
1313
+ const detail = undefined;
1314
+ const payload: JsonRecord = kind === "reasoning"
1315
+ ? {
1316
+ itemId: id,
1317
+ title: summary,
1318
+ summaryParts: Array.isArray(item.summary)
1319
+ ? item.summary.flatMap(row => typeof row === "string" && row.trim() ? [row.trim()] : [])
1320
+ : [],
1321
+ summaryAvailable: Array.isArray(item.summary)
1322
+ && item.summary.some(row => typeof row === "string" && row.trim().length > 0),
1323
+ ...turnTiming,
1324
+ }
1325
+ : contextCompaction
1326
+ ? {
1327
+ itemId: id,
1328
+ title: summary,
1329
+ ...turnTiming,
1330
+ }
1331
+ : userInputRequest
1332
+ ? {
1333
+ itemId: id,
1334
+ title: summary,
1335
+ requestId: id,
1336
+ ...(status ? { status } : {}),
1337
+ questions: Array.isArray(item.questions) ? item.questions.slice(0, 3) : [],
1338
+ ...turnTiming,
1339
+ }
1340
+ : itemType
1341
+ ? {
1342
+ itemId: id,
1343
+ itemType,
1344
+ ...(status ? { status } : {}),
1345
+ ...(requestKind ? { requestKind } : {}),
1346
+ title,
1347
+ ...(fileNames.length > 0 ? { fileNames } : {}),
1348
+ ...(detail ? { detail } : {}),
1349
+ ...(fileChanges.length > 0 ? { fileChanges } : {}),
1350
+ ...(planUpdate
1351
+ ? {
1352
+ plan: Array.isArray(item.plan) ? item.plan.slice(0, 64) : [],
1353
+ ...(text(item.explanation).trim()
1354
+ ? { explanation: boundedText(item.explanation, 8 * 1024).value }
1355
+ : {}),
1356
+ }
1357
+ : {}),
1358
+ data: {
1359
+ toolCallId: id,
1360
+ ...(reviewMarker ? { type: "reviewMarker", state: text(item.state) } : {}),
1361
+ ...(subAgentActivity ? { type: "subAgentActivity", activity: text(item.activity) } : {}),
1362
+ ...(imageGeneration ? { type: "imageGeneration", status: text(item.status) } : {}),
1363
+ },
1364
+ ...turnTiming,
1365
+ }
1366
+ : { ...turnTiming };
1367
+ return {
1368
+ id,
1369
+ tone,
1370
+ kind: kind === "reasoning"
1371
+ ? "task.progress"
1372
+ : contextCompaction
1373
+ ? "context-compaction"
1374
+ : planUpdate
1375
+ ? "turn.plan.updated"
1376
+ : userInputRequest
1377
+ ? item.status === "inProgress" ? "user-input.requested" : "user-input.resolved"
1378
+ : kind,
1379
+ summary,
1380
+ payload,
1381
+ turnId,
1382
+ sequence,
1383
+ createdAt,
1384
+ };
1385
+ }
1386
+
1387
+ /**
1388
+ * Reduce one projected activity to a fixed notification label. This consumes
1389
+ * the same projected row sent to the Android timeline, but never copies its
1390
+ * summary, command, path, tool arguments, or provider payload into a system
1391
+ * notification.
1392
+ */
1393
+ export function projectCodexNotificationActivityLabel(activityValue: unknown): string | null {
1394
+ const activity = record(activityValue);
1395
+ if (!activity) return null;
1396
+ const kind = text(activity.kind);
1397
+ if (kind === "approval.requested" || kind === "user-input.requested") {
1398
+ return "Requires your input";
1399
+ }
1400
+ if (kind === "approval.resolved" || kind === "user-input.resolved") return "Reasoning";
1401
+ if (kind === "task.progress") return "Reasoning";
1402
+ if (kind === "turn.plan.updated") return "Plan";
1403
+ if (kind === "context-compaction") return "Compacting context";
1404
+ if (kind === "runtime.error" || kind === "tool.denied") return "Failed";
1405
+
1406
+ const payload = record(activity.payload);
1407
+ switch (text(payload?.itemType)) {
1408
+ case "command_execution":
1409
+ return "Command";
1410
+ case "file_change":
1411
+ return "File change";
1412
+ case "mcp_tool_call":
1413
+ return "MCP tool";
1414
+ case "dynamic_tool_call":
1415
+ return "Dynamic tool";
1416
+ case "collab_agent_tool_call":
1417
+ return "Collaboration";
1418
+ case "web_search":
1419
+ return "Web search";
1420
+ case "image_view":
1421
+ return "Image view";
1422
+ case "sub-agent-activity":
1423
+ return "Sub-agent";
1424
+ case "image-generation":
1425
+ return "Image generation";
1426
+ case "file-read":
1427
+ return "File read";
1428
+ case "codebase-search":
1429
+ return "Codebase search";
1430
+ case "plan-update":
1431
+ case "plan":
1432
+ return "Plan";
1433
+ case "review-marker":
1434
+ return "Review";
1435
+ case "context-compaction":
1436
+ return "Compacting context";
1437
+ case "reasoning":
1438
+ return "Reasoning";
1439
+ default:
1440
+ return null;
1441
+ }
1442
+ }
1443
+
1444
+ /**
1445
+ * Derive a notification label through the ordinary Android item projection so
1446
+ * the notification and in-app activity cannot classify the same tool
1447
+ * differently.
1448
+ */
1449
+ export function projectCodexNotificationItemLabel(itemValue: unknown): string | null {
1450
+ const item = record(itemValue);
1451
+ if (!item) return null;
1452
+ const id = text(item.id).trim() || "notification-activity";
1453
+ const projected = projectCodexLiveTurnItem({
1454
+ threadId: "notification-thread",
1455
+ turnId: "notification-turn",
1456
+ item: { ...item, id },
1457
+ sequence: 0,
1458
+ createdAtMs: 0,
1459
+ completed: false,
1460
+ });
1461
+ return projectCodexNotificationActivityLabel(projected.activity);
1462
+ }
1463
+
1464
+ function shellThread(thread: JsonRecord): JsonRecord {
1465
+ const createdAt = isoFromSeconds(thread.createdAt);
1466
+ const updatedAt = isoFromSeconds(thread.updatedAt, Date.parse(createdAt));
1467
+ const cwd = cwdOf(thread);
1468
+ const latest = latestTurn(thread);
1469
+ const session = sessionOf(thread, updatedAt);
1470
+ const recencyAt = finite(thread.recencyAt) === null
1471
+ ? latest?.requestedAt ?? null
1472
+ : isoFromSeconds(thread.recencyAt, Date.parse(updatedAt));
1473
+ return {
1474
+ id: text(thread.id),
1475
+ projectId: projectIdOf(thread, cwd),
1476
+ title: titleOf(thread),
1477
+ modelSelection: modelSelection(thread),
1478
+ runtimeMode: text(thread.androidRemoteRuntimeMode, "full-access"),
1479
+ interactionMode: text(thread.androidRemoteInteractionMode, "default"),
1480
+ branch: null,
1481
+ worktreePath: cwd,
1482
+ latestTurn: latest,
1483
+ createdAt,
1484
+ updatedAt,
1485
+ archivedAt: text(thread.androidRemoteArchivedAt).trim() || null,
1486
+ session,
1487
+ latestUserMessageAt: recencyAt,
1488
+ hasPendingApprovals: false,
1489
+ hasPendingUserInput: thread.androidRemoteWaitingOnUserInput === true,
1490
+ hasActionableProposedPlan: false,
1491
+ source: { kind: "remodex-projected" },
1492
+ };
1493
+ }
1494
+
1495
+ export function projectCodexShellSnapshot(threads: unknown[], sequence: number): JsonRecord {
1496
+ const rows = threads.flatMap(value => record(value) ? [record(value)!] : []);
1497
+ const projectMap = new Map<string, JsonRecord>();
1498
+ for (const thread of rows) {
1499
+ const createdAt = isoFromSeconds(thread.createdAt);
1500
+ const updatedAt = isoFromSeconds(thread.updatedAt, Date.parse(createdAt));
1501
+ const cwd = cwdOf(thread);
1502
+ const project = projectForThread(thread, cwd, createdAt, updatedAt);
1503
+ const existing = projectMap.get(text(project.id));
1504
+ if (!existing || text(existing.updatedAt) < updatedAt) projectMap.set(text(project.id), project);
1505
+ }
1506
+ return {
1507
+ snapshotSequence: sequence,
1508
+ projects: [...projectMap.values()],
1509
+ threads: rows.map(shellThread),
1510
+ nativeProjects: [],
1511
+ nativeThreads: [],
1512
+ updatedAt: new Date().toISOString(),
1513
+ };
1514
+ }
1515
+
1516
+ export function projectCodexReadModel(threads: unknown[], sequence: number): JsonRecord {
1517
+ const shell = projectCodexShellSnapshot(threads, sequence);
1518
+ return {
1519
+ snapshotSequence: sequence,
1520
+ projects: shell.projects,
1521
+ threads: (shell.threads as JsonRecord[]).map(row => ({
1522
+ ...row,
1523
+ deletedAt: null,
1524
+ messages: [],
1525
+ proposedPlans: [],
1526
+ activities: [],
1527
+ checkpoints: [],
1528
+ })),
1529
+ updatedAt: shell.updatedAt,
1530
+ };
1531
+ }
1532
+
1533
+ export function projectCodexContextWindowActivity(input: {
1534
+ threadId: string;
1535
+ tokenUsage: unknown;
1536
+ createdAt: string;
1537
+ turnId?: string | null;
1538
+ }): JsonRecord | null {
1539
+ const root = record(input.tokenUsage);
1540
+ const usage = record(root?.tokenUsage)
1541
+ ?? record(root?.tokenUsageInfo)
1542
+ ?? record(root?.usage)
1543
+ ?? root;
1544
+ const last = record(usage?.last)
1545
+ ?? record(usage?.lastUsage)
1546
+ ?? record(usage?.lastTokenUsage)
1547
+ ?? record(usage?.last_token_usage);
1548
+ const total = record(usage?.total)
1549
+ ?? record(usage?.totalUsage)
1550
+ ?? record(usage?.total_token_usage);
1551
+ const usedTokens = nonNegativeInteger(
1552
+ last?.totalTokens
1553
+ ?? last?.total_tokens
1554
+ ?? usage?.usedTokens
1555
+ ?? usage?.used_tokens
1556
+ ?? usage?.totalTokens,
1557
+ );
1558
+ const maxTokens = nonNegativeInteger(
1559
+ usage?.modelContextWindow
1560
+ ?? usage?.model_context_window
1561
+ ?? usage?.contextWindow
1562
+ ?? root?.modelContextWindow,
1563
+ );
1564
+ if (usedTokens === null || usedTokens <= 0 || maxTokens === null || maxTokens <= 0) return null;
1565
+
1566
+ const totalProcessedTokens = nonNegativeInteger(total?.totalTokens ?? total?.total_tokens);
1567
+ const inputTokens = nonNegativeInteger(last?.inputTokens ?? last?.input_tokens);
1568
+ const cachedInputTokens = nonNegativeInteger(last?.cachedInputTokens ?? last?.cached_input_tokens);
1569
+ const outputTokens = nonNegativeInteger(last?.outputTokens ?? last?.output_tokens);
1570
+ const reasoningOutputTokens = nonNegativeInteger(
1571
+ last?.reasoningOutputTokens ?? last?.reasoning_output_tokens,
1572
+ );
1573
+ return {
1574
+ id: `context-window-${input.threadId}`,
1575
+ tone: "info",
1576
+ kind: "context-window.updated",
1577
+ summary: "Context window updated",
1578
+ payload: {
1579
+ usedTokens,
1580
+ maxTokens,
1581
+ ...(totalProcessedTokens !== null && totalProcessedTokens > usedTokens
1582
+ ? { totalProcessedTokens }
1583
+ : {}),
1584
+ ...(inputTokens !== null ? { inputTokens, lastInputTokens: inputTokens } : {}),
1585
+ ...(cachedInputTokens !== null
1586
+ ? { cachedInputTokens, lastCachedInputTokens: cachedInputTokens }
1587
+ : {}),
1588
+ ...(outputTokens !== null ? { outputTokens, lastOutputTokens: outputTokens } : {}),
1589
+ ...(reasoningOutputTokens !== null
1590
+ ? { reasoningOutputTokens, lastReasoningOutputTokens: reasoningOutputTokens }
1591
+ : {}),
1592
+ lastUsedTokens: usedTokens,
1593
+ compactsAutomatically: true,
1594
+ },
1595
+ turnId: input.turnId ?? null,
1596
+ createdAt: input.createdAt,
1597
+ };
1598
+ }
1599
+
1600
+ function boundedStatusLabel(value: unknown, fallback: string, maximum = 96): string {
1601
+ const normalized = text(value).trim().replace(/\s+/gu, " ").slice(0, maximum);
1602
+ return normalized || fallback;
1603
+ }
1604
+
1605
+ function boundedPercent(value: unknown): number | null {
1606
+ const number = finite(value);
1607
+ return number === null ? null : Math.max(0, Math.min(100, number));
1608
+ }
1609
+
1610
+ function optionalTimestamp(value: unknown): number | null {
1611
+ const number = finite(value);
1612
+ return number !== null && number > 0 ? number : null;
1613
+ }
1614
+
1615
+ /**
1616
+ * Project only presentation-safe provider quota data for Android. Raw provider
1617
+ * responses, credentials, account ids, plan metadata, and quota sources never
1618
+ * cross the remote boundary.
1619
+ */
1620
+ export function projectProviderUsageActivity(input: {
1621
+ threadId: string;
1622
+ report: unknown;
1623
+ fallbackCreatedAt: string;
1624
+ }): JsonRecord | null {
1625
+ const report = record(input.report);
1626
+ const quota = record(report?.quota);
1627
+ if (!report || !quota) return null;
1628
+
1629
+ const windows: JsonRecord[] = [];
1630
+ const pushWindow = (label: string, percentValue: unknown, resetValue: unknown): void => {
1631
+ const usedPercent = boundedPercent(percentValue);
1632
+ if (usedPercent === null || windows.length >= 12) return;
1633
+ const resetAt = optionalTimestamp(resetValue);
1634
+ windows.push({
1635
+ label,
1636
+ usedPercent,
1637
+ remainingPercent: Math.max(0, Math.min(100, Math.round(100 - usedPercent))),
1638
+ ...(resetAt !== null ? { resetAt } : {}),
1639
+ });
1640
+ };
1641
+ pushWindow("5-hour", quota.fiveHourPercent, quota.fiveHourResetAt);
1642
+ pushWindow("Weekly", quota.weeklyPercent, quota.weeklyResetAt);
1643
+ pushWindow("Monthly", quota.monthlyPercent, quota.monthlyResetAt);
1644
+ if (Array.isArray(quota.customWindows)) {
1645
+ for (const value of quota.customWindows) {
1646
+ const window = record(value);
1647
+ if (!window) continue;
1648
+ pushWindow(
1649
+ boundedStatusLabel(window.label, "Limit", 64),
1650
+ window.percent,
1651
+ window.resetAt,
1652
+ );
1653
+ }
1654
+ }
1655
+
1656
+ const rawCredits = record(quota.creditsUsd);
1657
+ const credits = rawCredits
1658
+ ? (() => {
1659
+ const unlimited = rawCredits.unlimited === true;
1660
+ const used = finite(rawCredits.used);
1661
+ const limit = finite(rawCredits.limit);
1662
+ const remaining = finite(rawCredits.remaining);
1663
+ const usedPercent = boundedPercent(rawCredits.percent);
1664
+ if (!unlimited && used === null && limit === null && remaining === null && usedPercent === null) {
1665
+ return null;
1666
+ }
1667
+ const expiresAt = optionalTimestamp(rawCredits.expiresAt);
1668
+ return {
1669
+ unlimited,
1670
+ ...(used !== null && used >= 0 ? { used } : {}),
1671
+ ...(limit !== null && limit >= 0 ? { limit } : {}),
1672
+ ...(remaining !== null && remaining >= 0 ? { remaining } : {}),
1673
+ ...(usedPercent !== null
1674
+ ? {
1675
+ usedPercent,
1676
+ remainingPercent: Math.max(0, Math.min(100, Math.round(100 - usedPercent))),
1677
+ }
1678
+ : {}),
1679
+ ...(expiresAt !== null ? { expiresAt } : {}),
1680
+ };
1681
+ })()
1682
+ : null;
1683
+ if (windows.length === 0 && credits === null) return null;
1684
+
1685
+ const updatedAtValue = optionalTimestamp(report.updatedAt) ?? optionalTimestamp(quota.updatedAt);
1686
+ const updatedAtMs = updatedAtValue === null
1687
+ ? Date.parse(input.fallbackCreatedAt)
1688
+ : updatedAtValue < 10_000_000_000
1689
+ ? updatedAtValue * 1_000
1690
+ : updatedAtValue;
1691
+ const createdAt = Number.isFinite(updatedAtMs)
1692
+ ? new Date(updatedAtMs).toISOString()
1693
+ : input.fallbackCreatedAt;
1694
+ return {
1695
+ id: `provider-usage-${input.threadId}`,
1696
+ tone: "info",
1697
+ kind: "provider.usage.updated",
1698
+ summary: "Provider usage updated",
1699
+ payload: {
1700
+ providerId: boundedStatusLabel(report.provider, "provider", 64),
1701
+ providerLabel: boundedStatusLabel(report.label, "Provider"),
1702
+ windows,
1703
+ ...(credits ? { credits } : {}),
1704
+ },
1705
+ turnId: null,
1706
+ createdAt,
1707
+ };
1708
+ }
1709
+
1710
+ export function projectCodexThreadDetail(threadValue: unknown, sequence: number): JsonRecord {
1711
+ const thread = record(threadValue);
1712
+ if (!thread) throw new TypeError("Codex returned an invalid task");
1713
+ const shell = shellThread(thread);
1714
+ const detail = messagesAndActivity(thread);
1715
+ const statusUpdatedAt = text(shell.updatedAt);
1716
+ const contextWindow = projectCodexContextWindowActivity({
1717
+ threadId: text(thread.id),
1718
+ tokenUsage: thread.latestTokenUsageInfo,
1719
+ createdAt: statusUpdatedAt,
1720
+ turnId: text(latestTurn(thread)?.id).trim() || null,
1721
+ });
1722
+ return {
1723
+ snapshotSequence: sequence,
1724
+ thread: {
1725
+ ...shell,
1726
+ deletedAt: null,
1727
+ messages: detail.messages,
1728
+ proposedPlans: detail.proposedPlans,
1729
+ activities: [
1730
+ ...detail.activities,
1731
+ ...(contextWindow ? [contextWindow] : []),
1732
+ ],
1733
+ checkpoints: [],
1734
+ hasActionableProposedPlan: detail.proposedPlans.some(plan => plan.implementedAt === null),
1735
+ },
1736
+ };
1737
+ }
1738
+
1739
+ /** Project one live Codex item without rereading and rebuilding the whole task. */
1740
+ export function projectCodexLiveTurnItem(input: {
1741
+ threadId: string;
1742
+ turnId: string;
1743
+ item: JsonRecord;
1744
+ sequence: number;
1745
+ createdAtMs: number;
1746
+ completed: boolean;
1747
+ }): {
1748
+ message: JsonRecord | null;
1749
+ activity: JsonRecord | null;
1750
+ proposedPlan: JsonRecord | null;
1751
+ } {
1752
+ const seconds = input.createdAtMs / 1_000;
1753
+ const detail = messagesAndActivity({
1754
+ id: input.threadId,
1755
+ createdAt: seconds,
1756
+ updatedAt: seconds,
1757
+ turns: [{
1758
+ id: input.turnId,
1759
+ status: input.completed ? "completed" : "inProgress",
1760
+ startedAt: seconds,
1761
+ ...(input.completed ? { completedAt: seconds } : {}),
1762
+ items: [input.item],
1763
+ }],
1764
+ }, { includeCompletedCommandActivities: true });
1765
+ const withSequence = (value: JsonRecord | undefined): JsonRecord | null =>
1766
+ value ? { ...value, sequence: input.sequence } : null;
1767
+ return {
1768
+ message: withSequence(detail.messages[0]),
1769
+ activity: withSequence(detail.activities[0]),
1770
+ proposedPlan: withSequence(detail.proposedPlans[0]),
1771
+ };
1772
+ }
1773
+
1774
+ export function codexThreadIdFromMessage(message: JsonRecord): string | null {
1775
+ const params = record(message.params);
1776
+ const direct = text(params?.threadId).trim();
1777
+ if (direct) return direct;
1778
+ const thread = record(params?.thread);
1779
+ return text(thread?.id).trim() || null;
1780
+ }