@cloudcli-ai/cloudcli 1.37.2 → 1.37.4

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 (453) hide show
  1. package/README.md +269 -0
  2. package/dist/assets/{abnfDiagram-N423BO3Z-C4UAoHJb.js → abnfDiagram-N423BO3Z-Dmkwx38H.js} +1 -1
  3. package/dist/assets/{arc-B8jF-ja9.js → arc-DLe9AYuO.js} +1 -1
  4. package/dist/assets/{architectureDiagram-T3A2C74G-C_Wur_CU.js → architectureDiagram-T3A2C74G-CBbbbNUW.js} +1 -1
  5. package/dist/assets/{blockDiagram-VBNYF7ZC-Cq4P1i8t.js → blockDiagram-VBNYF7ZC-CaMEaWu4.js} +1 -1
  6. package/dist/assets/{c4Diagram-5PPSVZJV-C1wG4eUj.js → c4Diagram-5PPSVZJV-DhjMgNnR.js} +1 -1
  7. package/dist/assets/channel-CWrmwsir.js +1 -0
  8. package/dist/assets/{chunk-2GRJ4B5K-B8fnBG6E.js → chunk-2GRJ4B5K-yjl3-F-i.js} +1 -1
  9. package/dist/assets/{chunk-2Q5K7J3B-WdfPooPJ.js → chunk-2Q5K7J3B-DsAYaWEP.js} +1 -1
  10. package/dist/assets/{chunk-5RXB4S5H-DhZJuG2E.js → chunk-5RXB4S5H-Bt5qmqOX.js} +1 -1
  11. package/dist/assets/{chunk-5VM5RSS4-B0rjslal.js → chunk-5VM5RSS4-yWrWVvUt.js} +1 -1
  12. package/dist/assets/{chunk-6Q2QTUOP-CG2Oj4jL.js → chunk-6Q2QTUOP-Dt66K5xb.js} +1 -1
  13. package/dist/assets/{chunk-GF5L2VYU-rrl2lRxY.js → chunk-GF5L2VYU-DA0vzaj4.js} +1 -1
  14. package/dist/assets/{chunk-JWPE2WC7-CjVbrVMA.js → chunk-JWPE2WC7-CUkbx96V.js} +1 -1
  15. package/dist/assets/{chunk-KBJHAD2P-CRzKgU8S.js → chunk-KBJHAD2P-DWKxx1WH.js} +1 -1
  16. package/dist/assets/{chunk-RYQCIY6F-DBz4GTZF.js → chunk-RYQCIY6F-BqluplN0.js} +1 -1
  17. package/dist/assets/{chunk-XXDRQBXY-wHxalsXk.js → chunk-XXDRQBXY-DelKg3Xf.js} +1 -1
  18. package/dist/assets/classDiagram-JCYQIIEL-COAGCC6w.js +1 -0
  19. package/dist/assets/classDiagram-v2-OCEON4UE-COAGCC6w.js +1 -0
  20. package/dist/assets/{cose-bilkent-JH36ORCC-s1hygY8e.js → cose-bilkent-JH36ORCC-DHV93-Em.js} +1 -1
  21. package/dist/assets/{cynefin-VYW2F7L2-C4LGV15i.js → cynefin-VYW2F7L2-B9HZgoGX.js} +1 -1
  22. package/dist/assets/{cynefinDiagram-MW4NZA55-C5uhu7B3.js → cynefinDiagram-MW4NZA55-hFjjEQg8.js} +1 -1
  23. package/dist/assets/{dagre-VZM6K2ZE-B-OBTewm.js → dagre-VZM6K2ZE-CpeVOC7V.js} +1 -1
  24. package/dist/assets/{diagram-7IWD3JNH-BjGKQxih.js → diagram-7IWD3JNH-D0H1UoRA.js} +1 -1
  25. package/dist/assets/{diagram-B4RE2ZJO-BMKz1X2L.js → diagram-B4RE2ZJO-6Y7BQUup.js} +1 -1
  26. package/dist/assets/{diagram-LBJQPF4R-DNh-GhKC.js → diagram-LBJQPF4R-C0nNOwht.js} +1 -1
  27. package/dist/assets/{diagram-Q27KOJAE-BaTG9jPT.js → diagram-Q27KOJAE-En6QlOVX.js} +1 -1
  28. package/dist/assets/{diagram-UB23O5K3-BUwo2OvJ.js → diagram-UB23O5K3-CmPiNeGD.js} +1 -1
  29. package/dist/assets/{ebnfDiagram-BXEA7PRR-DAZgfzX2.js → ebnfDiagram-BXEA7PRR-Bygzp1Qt.js} +1 -1
  30. package/dist/assets/{erDiagram-JOGREHBK-1w1CcGEb.js → erDiagram-JOGREHBK-CwScRfzl.js} +1 -1
  31. package/dist/assets/{flowDiagram-UKHOOZJN-D4EII8Jj.js → flowDiagram-UKHOOZJN-bKX-0xP6.js} +1 -1
  32. package/dist/assets/{ganttDiagram-PKOTCBZU-ChvJZAgB.js → ganttDiagram-PKOTCBZU-DtZPKXGd.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-DS77QQ5N-DtOn41S8.js → gitGraphDiagram-DS77QQ5N-CUOtdL1o.js} +1 -1
  34. package/dist/assets/index-CdjIFxM2.js +5004 -0
  35. package/dist/assets/index-DgafhGTF.css +32 -0
  36. package/dist/assets/{infoDiagram-6WML65LV-DN_PVEJZ.js → infoDiagram-6WML65LV-ciRQTS0h.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-WSZJBQD7-DbkF1kXb.js → ishikawaDiagram-WSZJBQD7-CgjhQZ84.js} +1 -1
  38. package/dist/assets/{journeyDiagram-NVQOT4AX-pJwmhddE.js → journeyDiagram-NVQOT4AX-BsbQSeIW.js} +1 -1
  39. package/dist/assets/{kanban-definition-27J2QSJJ-PPAPgNp5.js → kanban-definition-27J2QSJJ-CrwzY9Jw.js} +1 -1
  40. package/dist/assets/{linear-iPR-J9f7.js → linear-BUaCDr6l.js} +1 -1
  41. package/dist/assets/{mermaid.core-BEUL86Z6.js → mermaid.core-ClgdUU0j.js} +5 -5
  42. package/dist/assets/{mindmap-definition-FAOFIHXS-BvZoBib7.js → mindmap-definition-FAOFIHXS-BictljY1.js} +1 -1
  43. package/dist/assets/{pegDiagram-VL7TDLO6-DhYreaUd.js → pegDiagram-VL7TDLO6-BXdByy_U.js} +1 -1
  44. package/dist/assets/{pieDiagram-7S7Q4E2Y-CrOHlyD0.js → pieDiagram-7S7Q4E2Y-uZsMfkRX.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-CIZ2JOQS-D4g5yH3t.js → quadrantDiagram-CIZ2JOQS-7lCv4QIj.js} +1 -1
  46. package/dist/assets/{railroadDiagram-AXF67PYL-Dj39lHlf.js → railroadDiagram-AXF67PYL-07dxIWbi.js} +1 -1
  47. package/dist/assets/{requirementDiagram-LRYGKXZP-1AS4Zgm_.js → requirementDiagram-LRYGKXZP-Cb31Yv1_.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-W5VNT64P-CKlMn19P.js → sankeyDiagram-W5VNT64P-X0YPSyF9.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-SI44F4Z6-B_j3qyvo.js → sequenceDiagram-SI44F4Z6-BdBxSIlP.js} +1 -1
  50. package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
  51. package/dist/assets/{sizeCapture-X5ZJPWSS-Bp2Op16v.js → sizeCapture-X5ZJPWSS-DQM5xAyD.js} +1 -1
  52. package/dist/assets/{stateDiagram-OKZ733FA-C0cN6DoY.js → stateDiagram-OKZ733FA-DJ3xkLUF.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-UEYNNEHI-vAaE4zNo.js +1 -0
  54. package/dist/assets/{swimlanes-SLNWSIFB-BEvXOV9b.js → swimlanes-SLNWSIFB-rjfYdW0w.js} +2 -2
  55. package/dist/assets/swimlanesDiagram-ULZ7WXOC-TV2IF4Nq.js +8 -0
  56. package/dist/assets/{timeline-definition-Z64GVDOM-zdJQinvZ.js → timeline-definition-Z64GVDOM-DZMra5do.js} +1 -1
  57. package/dist/assets/{vendor-codemirror-OwyKSvPE.js → vendor-codemirror-CYrSt7-U.js} +18 -18
  58. package/dist/assets/{vennDiagram-T6HMQDX7-DuF1uZMY.js → vennDiagram-T6HMQDX7-BevD8uu0.js} +1 -1
  59. package/dist/assets/{wardleyDiagram-T6FBY63Y-B9AlLQup.js → wardleyDiagram-T6FBY63Y-DsIyXB7z.js} +1 -1
  60. package/dist/assets/{xychartDiagram-ELKLHX3M-DTJX48Qg.js → xychartDiagram-ELKLHX3M-G3PlF3ko.js} +1 -1
  61. package/dist/index.html +3 -3
  62. package/dist-server/server/index.js +11 -4
  63. package/dist-server/server/index.js.map +1 -1
  64. package/dist-server/server/modules/agent/agent.module.js +9 -2
  65. package/dist-server/server/modules/agent/agent.module.js.map +1 -1
  66. package/dist-server/server/modules/agent/agent.routes.js +177 -130
  67. package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
  68. package/dist-server/server/modules/agent/tests/agent-run-registration.test.js +306 -0
  69. package/dist-server/server/modules/agent/tests/agent-run-registration.test.js.map +1 -0
  70. package/dist-server/server/modules/agent/tests/agent.routes.test.js +48 -2
  71. package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
  72. package/dist-server/server/modules/browser-use/browser-use.routes.js +39 -33
  73. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -1
  74. package/dist-server/server/modules/database/connection.js +1 -1
  75. package/dist-server/server/modules/database/connection.js.map +1 -1
  76. package/dist-server/server/modules/database/index.js +5 -0
  77. package/dist-server/server/modules/database/index.js.map +1 -1
  78. package/dist-server/server/modules/database/migrations.js +45 -1
  79. package/dist-server/server/modules/database/migrations.js.map +1 -1
  80. package/dist-server/server/modules/database/repositories/provider-models.js +60 -6
  81. package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -1
  82. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js +71 -0
  83. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js.map +1 -0
  84. package/dist-server/server/modules/database/repositories/session-drafts.db.js +105 -0
  85. package/dist-server/server/modules/database/repositories/session-drafts.db.js.map +1 -0
  86. package/dist-server/server/modules/database/repositories/sessions.db.js +144 -5
  87. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  88. package/dist-server/server/modules/database/repositories/user-preferences.db.js +65 -0
  89. package/dist-server/server/modules/database/repositories/user-preferences.db.js.map +1 -0
  90. package/dist-server/server/modules/database/schema.js +99 -0
  91. package/dist-server/server/modules/database/schema.js.map +1 -1
  92. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +99 -0
  93. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -1
  94. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +39 -0
  95. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
  96. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js +105 -0
  97. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js.map +1 -0
  98. package/dist-server/server/modules/file-tree/file-tree.module.js +2 -1
  99. package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
  100. package/dist-server/server/modules/file-tree/file-tree.service.js +44 -7
  101. package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
  102. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +126 -1
  103. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
  104. package/dist-server/server/modules/git/git-branch-diff.service.js +142 -0
  105. package/dist-server/server/modules/git/git-branch-diff.service.js.map +1 -0
  106. package/dist-server/server/modules/git/git-branch.service.js.map +1 -1
  107. package/dist-server/server/modules/git/git.routes.js +92 -5
  108. package/dist-server/server/modules/git/git.routes.js.map +1 -1
  109. package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js +160 -0
  110. package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js.map +1 -0
  111. package/dist-server/server/modules/projects/project-clone.routes.js +108 -0
  112. package/dist-server/server/modules/projects/project-clone.routes.js.map +1 -0
  113. package/dist-server/server/modules/projects/projects.routes.js +13 -66
  114. package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
  115. package/dist-server/server/modules/projects/services/project-clone-request.service.js +36 -0
  116. package/dist-server/server/modules/projects/services/project-clone-request.service.js.map +1 -0
  117. package/dist-server/server/modules/projects/services/project-clone.service.js +86 -32
  118. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
  119. package/dist-server/server/modules/projects/services/project-delete.service.js +12 -1
  120. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
  121. package/dist-server/server/modules/projects/services/project-star.service.js +2 -2
  122. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -1
  123. package/dist-server/server/modules/projects/tests/project-clone.routes.test.js +191 -0
  124. package/dist-server/server/modules/projects/tests/project-clone.routes.test.js.map +1 -0
  125. package/dist-server/server/modules/projects/tests/project-clone.service.test.js +275 -0
  126. package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -1
  127. package/dist-server/server/modules/providers/index.js +3 -0
  128. package/dist-server/server/modules/providers/index.js.map +1 -1
  129. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +62 -9
  130. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  131. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js +46 -0
  132. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js.map +1 -0
  133. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +31 -12
  134. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  135. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +472 -53
  136. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  137. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +38 -11
  138. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  139. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +1007 -60
  140. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  141. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +4 -1
  142. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  143. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  144. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  145. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js +204 -0
  146. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js.map +1 -0
  147. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js +32 -0
  148. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js.map +1 -0
  149. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +47 -0
  150. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  151. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +71 -53
  152. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
  153. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +8 -0
  154. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  155. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +1621 -431
  156. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  157. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  158. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  159. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +9 -2
  160. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
  161. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +16 -8
  162. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
  163. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
  164. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +274 -9
  165. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  166. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
  167. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js.map +1 -1
  168. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +19 -2
  169. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  170. package/dist-server/server/modules/providers/provider.routes.js +134 -2
  171. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  172. package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js +100 -0
  173. package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js.map +1 -0
  174. package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js +97 -0
  175. package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js.map +1 -0
  176. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +23 -1
  177. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  178. package/dist-server/server/modules/providers/services/provider-models.service.js +59 -5
  179. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  180. package/dist-server/server/modules/providers/services/provider-runtime.service.js +8 -0
  181. package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
  182. package/dist-server/server/modules/providers/services/provider-token-usage.service.js +139 -35
  183. package/dist-server/server/modules/providers/services/provider-token-usage.service.js.map +1 -1
  184. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +2 -4
  185. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  186. package/dist-server/server/modules/providers/services/session-history-cache.service.js +91 -0
  187. package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -0
  188. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +105 -32
  189. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  190. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +3 -60
  191. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  192. package/dist-server/server/modules/providers/services/sessions.service.js +252 -12
  193. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  194. package/dist-server/server/modules/providers/tests/claude-auth.test.js +183 -0
  195. package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
  196. package/dist-server/server/modules/providers/tests/claude-background-work.test.js +172 -0
  197. package/dist-server/server/modules/providers/tests/claude-background-work.test.js.map +1 -0
  198. package/dist-server/server/modules/providers/tests/claude-compaction.test.js +84 -0
  199. package/dist-server/server/modules/providers/tests/claude-compaction.test.js.map +1 -0
  200. package/dist-server/server/modules/providers/tests/claude-message-model.test.js +123 -0
  201. package/dist-server/server/modules/providers/tests/claude-message-model.test.js.map +1 -0
  202. package/dist-server/server/modules/providers/tests/claude-models.test.js +39 -0
  203. package/dist-server/server/modules/providers/tests/claude-models.test.js.map +1 -0
  204. package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js +265 -0
  205. package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js.map +1 -0
  206. package/dist-server/server/modules/providers/tests/claude-sessions.test.js +1815 -33
  207. package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
  208. package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js +228 -0
  209. package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js.map +1 -0
  210. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js +16 -0
  211. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js.map +1 -0
  212. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js +85 -0
  213. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js.map +1 -0
  214. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js +338 -0
  215. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js.map +1 -0
  216. package/dist-server/server/modules/providers/tests/codex-models.test.js +25 -0
  217. package/dist-server/server/modules/providers/tests/codex-models.test.js.map +1 -0
  218. package/dist-server/server/modules/providers/tests/codex-runtime.test.js +83 -0
  219. package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -0
  220. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js +54 -0
  221. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js.map +1 -0
  222. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +354 -15
  223. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
  224. package/dist-server/server/modules/providers/tests/cursor-auth.test.js +167 -0
  225. package/dist-server/server/modules/providers/tests/cursor-auth.test.js.map +1 -0
  226. package/dist-server/server/modules/providers/tests/opencode-models.test.js +196 -4
  227. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  228. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +19 -1
  229. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  230. package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js +30 -0
  231. package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js.map +1 -0
  232. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +79 -1
  233. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
  234. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +164 -1
  235. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
  236. package/dist-server/server/modules/providers/tests/provider.routes.test.js +190 -1
  237. package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
  238. package/dist-server/server/modules/providers/tests/session-fork.test.js +113 -0
  239. package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
  240. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +139 -0
  241. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -0
  242. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js +90 -0
  243. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js.map +1 -0
  244. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js +122 -0
  245. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js.map +1 -0
  246. package/dist-server/server/modules/providers/tests/sessions.service.test.js +219 -1
  247. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
  248. package/dist-server/server/modules/providers/tests/skills.test.js +15 -3
  249. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  250. package/dist-server/server/modules/scheduled-messages/index.js +5 -0
  251. package/dist-server/server/modules/scheduled-messages/index.js.map +1 -0
  252. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js +47 -0
  253. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js.map +1 -0
  254. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +175 -0
  255. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -0
  256. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js +95 -0
  257. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js.map +1 -0
  258. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +290 -0
  259. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -0
  260. package/dist-server/server/modules/taskmaster/taskmaster.routes.js +23 -23
  261. package/dist-server/server/modules/taskmaster/taskmaster.routes.js.map +1 -1
  262. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js +48 -0
  263. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js.map +1 -1
  264. package/dist-server/server/modules/user/tests/user.service.test.js +70 -0
  265. package/dist-server/server/modules/user/tests/user.service.test.js.map +1 -1
  266. package/dist-server/server/modules/user/user.module.js +10 -1
  267. package/dist-server/server/modules/user/user.module.js.map +1 -1
  268. package/dist-server/server/modules/user/user.routes.js +44 -0
  269. package/dist-server/server/modules/user/user.routes.js.map +1 -1
  270. package/dist-server/server/modules/user/user.service.js +75 -0
  271. package/dist-server/server/modules/user/user.service.js.map +1 -1
  272. package/dist-server/server/modules/voice/tests/voice.service.test.js +3 -2
  273. package/dist-server/server/modules/voice/tests/voice.service.test.js.map +1 -1
  274. package/dist-server/server/modules/websocket/index.js +7 -0
  275. package/dist-server/server/modules/websocket/index.js.map +1 -1
  276. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +37 -49
  277. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
  278. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +35 -5
  279. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -1
  280. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +204 -10
  281. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  282. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js +94 -0
  283. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js.map +1 -0
  284. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +236 -20
  285. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  286. package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js +137 -0
  287. package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js.map +1 -0
  288. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js +263 -0
  289. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js.map +1 -0
  290. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +29 -3
  291. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
  292. package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js +96 -0
  293. package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js.map +1 -0
  294. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js +106 -0
  295. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js.map +1 -0
  296. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +500 -0
  297. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
  298. package/dist-server/server/shared/claude-cli-path.js +22 -5
  299. package/dist-server/server/shared/claude-cli-path.js.map +1 -1
  300. package/dist-server/server/shared/index.js +3 -0
  301. package/dist-server/server/shared/index.js.map +1 -0
  302. package/dist-server/server/shared/message-unification.js +376 -0
  303. package/dist-server/server/shared/message-unification.js.map +1 -0
  304. package/dist-server/server/shared/tests/claude-cli-path.test.js +45 -1
  305. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -1
  306. package/dist-server/server/shared/tests/message-unification.test.js +174 -0
  307. package/dist-server/server/shared/tests/message-unification.test.js.map +1 -0
  308. package/dist-server/server/shared/tests/read-only-roots.test.js +124 -0
  309. package/dist-server/server/shared/tests/read-only-roots.test.js.map +1 -0
  310. package/dist-server/server/shared/tests/strip-ansi-sequences.test.js +31 -0
  311. package/dist-server/server/shared/tests/strip-ansi-sequences.test.js.map +1 -0
  312. package/dist-server/server/shared/utils.js +160 -0
  313. package/dist-server/server/shared/utils.js.map +1 -1
  314. package/electron/desktopWindow.js +6 -0
  315. package/electron/viewHost.js +25 -2
  316. package/package.json +20 -17
  317. package/server/index.ts +16 -5
  318. package/server/modules/agent/agent.module.ts +11 -1
  319. package/server/modules/agent/agent.routes.ts +200 -142
  320. package/server/modules/agent/tests/agent-run-registration.test.ts +348 -0
  321. package/server/modules/agent/tests/agent.routes.test.ts +51 -2
  322. package/server/modules/browser-use/browser-use.routes.ts +96 -33
  323. package/server/modules/database/connection.ts +2 -1
  324. package/server/modules/database/index.ts +10 -0
  325. package/server/modules/database/migrations.ts +51 -0
  326. package/server/modules/database/repositories/provider-models.ts +79 -6
  327. package/server/modules/database/repositories/scheduled-messages.db.ts +127 -0
  328. package/server/modules/database/repositories/session-drafts.db.ts +171 -0
  329. package/server/modules/database/repositories/sessions.db.ts +204 -3
  330. package/server/modules/database/repositories/user-preferences.db.ts +80 -0
  331. package/server/modules/database/schema.ts +103 -0
  332. package/server/modules/database/tests/provider-models.db.integration.test.ts +113 -0
  333. package/server/modules/database/tests/sessions.db.integration.test.ts +52 -0
  334. package/server/modules/database/tests/user-preferences.db.integration.test.ts +135 -0
  335. package/server/modules/file-tree/file-tree.module.ts +2 -1
  336. package/server/modules/file-tree/file-tree.service.ts +49 -7
  337. package/server/modules/file-tree/tests/file-tree.service.test.ts +163 -1
  338. package/server/modules/git/git-branch-diff.service.ts +204 -0
  339. package/server/modules/git/git-branch.service.ts +2 -12
  340. package/server/modules/git/git.routes.ts +100 -6
  341. package/server/modules/git/tests/git-branch-diff.service.test.ts +210 -0
  342. package/server/modules/projects/project-clone.routes.ts +142 -0
  343. package/server/modules/projects/projects.routes.ts +16 -81
  344. package/server/modules/projects/services/project-clone-request.service.ts +56 -0
  345. package/server/modules/projects/services/project-clone.service.ts +102 -34
  346. package/server/modules/projects/services/project-delete.service.ts +13 -1
  347. package/server/modules/projects/services/project-star.service.ts +2 -2
  348. package/server/modules/projects/tests/project-clone.routes.test.ts +246 -0
  349. package/server/modules/projects/tests/project-clone.service.test.ts +376 -0
  350. package/server/modules/providers/index.ts +4 -0
  351. package/server/modules/providers/list/claude/claude-auth.provider.ts +81 -11
  352. package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
  353. package/server/modules/providers/list/claude/claude-models.provider.ts +37 -17
  354. package/server/modules/providers/list/claude/claude-runtime.provider.js +510 -54
  355. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +40 -14
  356. package/server/modules/providers/list/claude/claude-sessions.provider.ts +1195 -63
  357. package/server/modules/providers/list/claude/claude-skills.provider.ts +4 -1
  358. package/server/modules/providers/list/claude/claude.provider.ts +3 -0
  359. package/server/modules/providers/list/codex/codex-app-server.client.ts +243 -0
  360. package/server/modules/providers/list/codex/codex-fork.provider.ts +40 -0
  361. package/server/modules/providers/list/codex/codex-models.provider.ts +47 -0
  362. package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +105 -70
  363. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
  364. package/server/modules/providers/list/codex/codex-sessions.provider.ts +1847 -455
  365. package/server/modules/providers/list/codex/codex.provider.ts +3 -0
  366. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +10 -2
  367. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +17 -8
  368. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
  369. package/server/modules/providers/list/opencode/opencode-models.provider.ts +297 -9
  370. package/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
  371. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +30 -2
  372. package/server/modules/providers/provider.routes.ts +162 -2
  373. package/server/modules/providers/services/claude-cli-liveness.service.ts +121 -0
  374. package/server/modules/providers/services/claude-settings-permissions.service.ts +127 -0
  375. package/server/modules/providers/services/provider-capabilities.service.ts +40 -1
  376. package/server/modules/providers/services/provider-models.service.ts +71 -4
  377. package/server/modules/providers/services/provider-runtime.service.ts +11 -0
  378. package/server/modules/providers/services/provider-token-usage.service.ts +166 -39
  379. package/server/modules/providers/services/session-conversations-search.service.ts +2 -5
  380. package/server/modules/providers/services/session-history-cache.service.ts +150 -0
  381. package/server/modules/providers/services/session-synchronizer.service.ts +125 -38
  382. package/server/modules/providers/services/sessions-watcher.service.ts +3 -64
  383. package/server/modules/providers/services/sessions.service.ts +309 -17
  384. package/server/modules/providers/tests/claude-auth.test.ts +208 -0
  385. package/server/modules/providers/tests/claude-background-work.test.ts +213 -0
  386. package/server/modules/providers/tests/claude-compaction.test.ts +101 -0
  387. package/server/modules/providers/tests/claude-message-model.test.ts +160 -0
  388. package/server/modules/providers/tests/claude-models.test.ts +87 -0
  389. package/server/modules/providers/tests/claude-runtime-hold.test.ts +309 -0
  390. package/server/modules/providers/tests/claude-sessions.test.ts +2167 -37
  391. package/server/modules/providers/tests/claude-settings-permissions.test.ts +271 -0
  392. package/server/modules/providers/tests/claude-subagent-echo.test.ts +25 -0
  393. package/server/modules/providers/tests/claude-token-budget.test.ts +104 -0
  394. package/server/modules/providers/tests/codex-message-editing.test.ts +419 -0
  395. package/server/modules/providers/tests/codex-models.test.ts +37 -0
  396. package/server/modules/providers/tests/codex-runtime.test.ts +94 -0
  397. package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
  398. package/server/modules/providers/tests/codex-sessions.test.ts +396 -15
  399. package/server/modules/providers/tests/cursor-auth.test.ts +187 -0
  400. package/server/modules/providers/tests/opencode-models.test.ts +266 -8
  401. package/server/modules/providers/tests/opencode-sessions.test.ts +40 -1
  402. package/server/modules/providers/tests/provider-capabilities.service.test.ts +37 -0
  403. package/server/modules/providers/tests/provider-models.service.test.ts +99 -1
  404. package/server/modules/providers/tests/provider-token-usage.service.test.ts +188 -1
  405. package/server/modules/providers/tests/provider.routes.test.ts +230 -1
  406. package/server/modules/providers/tests/session-fork.test.ts +173 -0
  407. package/server/modules/providers/tests/session-history-cache.service.test.ts +165 -0
  408. package/server/modules/providers/tests/session-orphan-prune.test.ts +107 -0
  409. package/server/modules/providers/tests/session-synchronizer-coalescing.test.ts +136 -0
  410. package/server/modules/providers/tests/sessions.service.test.ts +303 -1
  411. package/server/modules/providers/tests/skills.test.ts +21 -5
  412. package/server/modules/scheduled-messages/index.ts +8 -0
  413. package/server/modules/scheduled-messages/scheduled-messages.routes.ts +68 -0
  414. package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +216 -0
  415. package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
  416. package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +354 -0
  417. package/server/modules/taskmaster/taskmaster.routes.ts +23 -33
  418. package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +57 -0
  419. package/server/modules/user/tests/user.service.test.ts +90 -0
  420. package/server/modules/user/user.module.ts +10 -1
  421. package/server/modules/user/user.routes.ts +44 -0
  422. package/server/modules/user/user.service.ts +105 -0
  423. package/server/modules/voice/tests/voice.service.test.ts +3 -2
  424. package/server/modules/websocket/README.md +4 -2
  425. package/server/modules/websocket/index.ts +8 -0
  426. package/server/modules/websocket/services/chat-run-registry.service.ts +45 -54
  427. package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
  428. package/server/modules/websocket/services/chat-websocket.service.ts +302 -16
  429. package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
  430. package/server/modules/websocket/services/shell-websocket.service.ts +276 -22
  431. package/server/modules/websocket/tests/chat-background-subscribe.test.ts +167 -0
  432. package/server/modules/websocket/tests/chat-edit-send.test.ts +309 -0
  433. package/server/modules/websocket/tests/chat-run-registry.test.ts +34 -3
  434. package/server/modules/websocket/tests/chat-stop-task.test.ts +127 -0
  435. package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
  436. package/server/modules/websocket/tests/shell-websocket.service.test.ts +629 -0
  437. package/server/shared/claude-cli-path.ts +24 -6
  438. package/server/shared/index.ts +3 -0
  439. package/server/shared/interfaces.ts +91 -0
  440. package/server/shared/message-unification.ts +449 -0
  441. package/server/shared/tests/claude-cli-path.test.ts +58 -1
  442. package/server/shared/tests/message-unification.test.ts +218 -0
  443. package/server/shared/tests/read-only-roots.test.ts +139 -0
  444. package/server/shared/tests/strip-ansi-sequences.test.ts +42 -0
  445. package/server/shared/types.ts +382 -7
  446. package/server/shared/utils.ts +176 -0
  447. package/dist/assets/channel-_falYbU1.js +0 -1
  448. package/dist/assets/classDiagram-JCYQIIEL-Dn-3CfpR.js +0 -1
  449. package/dist/assets/classDiagram-v2-OCEON4UE-Dn-3CfpR.js +0 -1
  450. package/dist/assets/index-CzPLBvXT.css +0 -32
  451. package/dist/assets/index-DFkBBuSA.js +0 -1549
  452. package/dist/assets/stateDiagram-v2-UEYNNEHI-BvBxUUrZ.js +0 -1
  453. package/dist/assets/swimlanesDiagram-ULZ7WXOC-CsE-x4YK.js +0 -8
@@ -1,9 +1,30 @@
1
1
  import fsSync from 'node:fs';
2
+ import fsp from 'node:fs/promises';
3
+ import path from 'node:path';
2
4
  import readline from 'node:readline';
3
5
  import { sessionsDb } from '../../../../modules/database/index.js';
6
+ import { codexAppServer } from '../../../../modules/providers/list/codex/codex-app-server.client.js';
4
7
  import { parseFilesInputTag, toImageAttachments } from '../../../../shared/image-attachments.js';
5
- import { createNormalizedMessage, generateMessageId, readObjectRecord, sliceTailPage } from '../../../../shared/utils.js';
8
+ import { prepareTranscriptMessages } from '../../../../shared/message-unification.js';
9
+ import { AppError, createNormalizedMessage, generateMessageId, readObjectRecord, sliceTailPage, truncateSubagentActivity, } from '../../../../shared/utils.js';
6
10
  const PROVIDER = 'codex';
11
+ /**
12
+ * How far up the `~/.codex/sessions/<year>/<month>/<day>` tree a subagent
13
+ * rollout is searched for when it is not next to its parent. Spawned agents
14
+ * are written seconds after the parent, so one directory level up (the month)
15
+ * already covers a run that crosses midnight.
16
+ */
17
+ const SUBAGENT_LOOKUP_PARENT_LEVELS = 2;
18
+ /**
19
+ * Upper bound on how much of a subagent's timeline is sent to the client. A
20
+ * long-running agent can record hundreds of tool calls, and the transcript only
21
+ * ever shows them behind a collapsed header, so shipping the whole history on
22
+ * every load costs far more than it shows.
23
+ */
24
+ const MAX_TRANSMITTED_SUBAGENT_ACTIVITIES = 200;
25
+ function readNonEmptyString(value) {
26
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
27
+ }
7
28
  function isVisibleCodexUserMessage(payload) {
8
29
  if (!payload || payload.type !== 'user_message') {
9
30
  return false;
@@ -13,6 +34,91 @@ function isVisibleCodexUserMessage(payload) {
13
34
  }
14
35
  return typeof payload.message === 'string' && payload.message.trim().length > 0;
15
36
  }
37
+ /**
38
+ * Follows which turn a Codex rollout is inside as its rows stream past.
39
+ *
40
+ * Codex writes no per-row id — every line is `{timestamp, type, payload}` and
41
+ * the `user_message` event that carries a prompt has no identity of its own —
42
+ * so a turn id is the only durable address a user message has. Turns are
43
+ * bracketed by rows that do carry `turn_id`: `task_started` opens one,
44
+ * `turn_context` restates it, `task_complete` closes it, and the prompt is
45
+ * written between them. That id is also exactly what `thread/fork` cuts at,
46
+ * which is what makes "edit this message" and "fork from here" addressable
47
+ * for Codex at all.
48
+ *
49
+ * `thread_rolled_back` retires the last N turns in place: the rows stay in the
50
+ * file, but the turns are no longer part of the thread and the fork endpoint
51
+ * refuses to cut at one. They are tracked here so a retired turn is never
52
+ * offered as an anchor, rather than discovered when the fork fails.
53
+ */
54
+ function createCodexTurnTracker() {
55
+ /** Turn ids still part of the conversation, oldest first. */
56
+ const liveTurnIds = [];
57
+ const seenTurnIds = new Set();
58
+ const rolledBackTurnIds = new Set();
59
+ let currentTurnId;
60
+ return {
61
+ /** Feeds one rollout row in. */
62
+ observe(entryType, payload) {
63
+ if (entryType === 'event_msg' && payload.type === 'thread_rolled_back') {
64
+ const retiredTurns = Number(payload.num_turns);
65
+ for (let retired = 0; retired < retiredTurns && liveTurnIds.length > 0; retired++) {
66
+ rolledBackTurnIds.add(liveTurnIds.pop());
67
+ }
68
+ currentTurnId = undefined;
69
+ return;
70
+ }
71
+ const turnId = readNonEmptyString(payload.turn_id);
72
+ if (!turnId) {
73
+ return;
74
+ }
75
+ currentTurnId = turnId;
76
+ if (!seenTurnIds.has(turnId)) {
77
+ seenTurnIds.add(turnId);
78
+ liveTurnIds.push(turnId);
79
+ }
80
+ },
81
+ /** The turn the rows being read belong to, or undefined outside one. */
82
+ getCurrentTurnId() {
83
+ return currentTurnId;
84
+ },
85
+ getLiveTurnIds() {
86
+ return liveTurnIds;
87
+ },
88
+ isRolledBack(turnId) {
89
+ return rolledBackTurnIds.has(turnId);
90
+ },
91
+ };
92
+ }
93
+ /**
94
+ * Reads the turns a Codex rollout still contains, oldest first.
95
+ *
96
+ * A separate pass rather than a by-product of the transcript reader: this runs
97
+ * once when a message is edited, while the reader runs on every history fetch,
98
+ * and both share the one rule for what a live turn is.
99
+ */
100
+ async function readCodexLiveTurnIds(filePath) {
101
+ const turns = createCodexTurnTracker();
102
+ const stream = fsSync.createReadStream(filePath);
103
+ const lines = readline.createInterface({ input: stream, crlfDelay: Infinity });
104
+ for await (const line of lines) {
105
+ if (!line.trim()) {
106
+ continue;
107
+ }
108
+ let entry;
109
+ try {
110
+ entry = JSON.parse(line);
111
+ }
112
+ catch {
113
+ continue;
114
+ }
115
+ const payload = readObjectRecord(entry.payload);
116
+ if (payload) {
117
+ turns.observe(entry.type, payload);
118
+ }
119
+ }
120
+ return turns.getLiveTurnIds();
121
+ }
16
122
  /**
17
123
  * Reads the image attachments Codex records on `user_message` events.
18
124
  * Turns sent with `local_image` input items land in `local_images` as file
@@ -63,6 +169,97 @@ function extractCodexTextContent(content) {
63
169
  .filter(Boolean)
64
170
  .join('\n');
65
171
  }
172
+ /** Reads image inputs from a typed UserMessage item. */
173
+ function extractCodexTypedUserImages(content) {
174
+ if (!Array.isArray(content)) {
175
+ return undefined;
176
+ }
177
+ const attachments = [];
178
+ for (const rawEntry of content) {
179
+ const entry = readObjectRecord(rawEntry);
180
+ if (!entry) {
181
+ continue;
182
+ }
183
+ const value = entry.type === 'local_image'
184
+ ? readNonEmptyString(entry.path)
185
+ : entry.type === 'image'
186
+ ? readNonEmptyString(entry.image_url)
187
+ : undefined;
188
+ if (!value) {
189
+ continue;
190
+ }
191
+ if (value.startsWith('data:')) {
192
+ attachments.push({ data: value });
193
+ }
194
+ else {
195
+ attachments.push(...toImageAttachments([value]));
196
+ }
197
+ }
198
+ return attachments.length > 0 ? attachments : undefined;
199
+ }
200
+ /**
201
+ * Reads the markdown out of Codex's `<proposed_plan>` envelope.
202
+ *
203
+ * Codex delivers a plan as a wrapped assistant message while Claude delivers
204
+ * one as an `ExitPlanMode` call. Recognizing the envelope here lets both end up
205
+ * on the same plan card instead of one provider showing a card and the other
206
+ * showing prose with a stray tag in it. The closing tag is optional because a
207
+ * streamed plan exposes the opening tag first.
208
+ *
209
+ * Exported for tests.
210
+ */
211
+ export function readCodexProposedPlan(text) {
212
+ const openingTag = /^\s*<proposed_plan>[ \t]*(?:\r?\n)?/i;
213
+ if (!openingTag.test(text)) {
214
+ return null;
215
+ }
216
+ const body = text
217
+ .replace(openingTag, '')
218
+ .replace(/(?:\r?\n)?[ \t]*<\/proposed_plan>\s*$/i, '')
219
+ .trim();
220
+ return body || null;
221
+ }
222
+ /**
223
+ * The block Codex appends to a reply that leaned on its memory files. It is
224
+ * always the last thing in the message and is meant for programmatic parsing,
225
+ * so it reads as raw XML when left in the prose.
226
+ */
227
+ const CODEX_MEMORY_CITATION_BLOCK = /<oai-mem-citation>([\s\S]*?)<\/oai-mem-citation>\s*$/i;
228
+ /**
229
+ * Lifts Codex's memory citations out of an assistant reply.
230
+ *
231
+ * The block names the memory files and line ranges the answer drew on, which is
232
+ * worth showing — but as a footnote under the reply, not as markup inside it.
233
+ * The trailing `<rollout_ids>` list is dropped: those are handles to earlier
234
+ * rollouts that the transcript view has no way to open.
235
+ *
236
+ * Exported for tests.
237
+ */
238
+ export function readCodexMemoryCitations(text) {
239
+ const block = CODEX_MEMORY_CITATION_BLOCK.exec(text);
240
+ if (!block) {
241
+ return { text };
242
+ }
243
+ const entries = /<citation_entries>([\s\S]*?)<\/citation_entries>/i.exec(block[1])?.[1] ?? '';
244
+ const memoryCitations = [];
245
+ for (const line of entries.split('\n')) {
246
+ const entry = line.trim();
247
+ if (!entry) {
248
+ continue;
249
+ }
250
+ const noteMarker = entry.indexOf('|note=');
251
+ const source = (noteMarker === -1 ? entry : entry.slice(0, noteMarker)).trim();
252
+ if (!source) {
253
+ continue;
254
+ }
255
+ const note = noteMarker === -1
256
+ ? ''
257
+ : entry.slice(noteMarker + '|note='.length).trim().replace(/^\[/, '').replace(/\]$/, '').trim();
258
+ memoryCitations.push(note ? { source, note } : { source });
259
+ }
260
+ const prose = text.slice(0, block.index).trimEnd();
261
+ return memoryCitations.length > 0 ? { text: prose, memoryCitations } : { text: prose };
262
+ }
66
263
  function extractCodexToolOutput(output) {
67
264
  if (typeof output === 'string') {
68
265
  return output;
@@ -78,17 +275,13 @@ function extractCodexToolOutput(output) {
78
275
  .filter(Boolean)
79
276
  .join('');
80
277
  }
81
- function readRunningExecOutput(output) {
82
- const runningCell = /Script running with cell ID\s+(\S+)/i.exec(output);
83
- if (!runningCell) {
84
- return null;
85
- }
86
- const outputMarker = /\r?\nOutput:\r?\n/i.exec(output);
87
- return {
88
- cellId: runningCell[1],
89
- content: outputMarker ? output.slice((outputMarker.index || 0) + outputMarker[0].length) : '',
90
- };
91
- }
278
+ /**
279
+ * Decodes one JavaScript string literal (single, double or backtick quoted).
280
+ *
281
+ * Template literals with `${}` interpolation are returned with the expression
282
+ * text intact — the transcript has no bindings to evaluate, and showing the
283
+ * raw template is still far more readable than the surrounding program.
284
+ */
92
285
  function decodeJavaScriptStringLiteral(literal) {
93
286
  if (literal.startsWith('"')) {
94
287
  try {
@@ -105,50 +298,413 @@ function decodeJavaScriptStringLiteral(literal) {
105
298
  .replace(/\\t/g, '\t')
106
299
  .replace(/\\([\\'`])/g, '$1');
107
300
  }
108
- function extractNestedCodexCommands(source) {
109
- const commands = [];
110
- const commandPattern = /(?:["']command["']|\bcommand)\s*:\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)/gs;
111
- for (const match of source.matchAll(commandPattern)) {
112
- commands.push(decodeJavaScriptStringLiteral(match[1]));
113
- }
114
- if (commands.length === 0) {
115
- const arrayPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*\[([\s\S]*?)\]\s*;/g;
116
- const stringPattern = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)/g;
117
- for (const arrayMatch of source.matchAll(arrayPattern)) {
118
- const arrayName = arrayMatch[1];
119
- if (!new RegExp(`\\b${arrayName}\\.map\\s*\\(`).test(source)) {
301
+ /**
302
+ * Returns the source of the argument list of the call that opens at
303
+ * `openParenIndex`, skipping over parens that appear inside string literals.
304
+ *
305
+ * A plain regex cannot be used here: shell commands routinely embed `(`/`)`
306
+ * and quotes, so the argument object has to be found by balancing.
307
+ */
308
+ function readCallArguments(source, openParenIndex) {
309
+ let depth = 0;
310
+ let quote = null;
311
+ for (let index = openParenIndex; index < source.length; index += 1) {
312
+ const character = source[index];
313
+ if (quote) {
314
+ if (character === '\\') {
315
+ index += 1;
120
316
  continue;
121
317
  }
122
- for (const stringMatch of arrayMatch[2].matchAll(stringPattern)) {
123
- commands.push(decodeJavaScriptStringLiteral(stringMatch[1]));
318
+ if (character === quote) {
319
+ quote = null;
124
320
  }
321
+ continue;
322
+ }
323
+ if (character === '"' || character === "'" || character === '`') {
324
+ quote = character;
325
+ continue;
326
+ }
327
+ if (character === '(') {
328
+ depth += 1;
329
+ continue;
330
+ }
331
+ if (character === ')') {
332
+ depth -= 1;
333
+ if (depth === 0) {
334
+ return source.slice(openParenIndex + 1, index);
335
+ }
336
+ }
337
+ }
338
+ return null;
339
+ }
340
+ /** Reads `key: "value"` / `"key": "value"` out of an object-literal source. */
341
+ function readStringProperty(objectSource, key) {
342
+ const pattern = new RegExp(`(?:["']${key}["']|\\b${key})\\s*:\\s*("(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|\`(?:\\\\.|[^\`\\\\])*\`)`, 's');
343
+ const match = pattern.exec(objectSource);
344
+ return match ? decodeJavaScriptStringLiteral(match[1]) : undefined;
345
+ }
346
+ /** Reads every `q: "..."` entry out of a `web__run` search-query array. */
347
+ function readSearchQueries(objectSource) {
348
+ const pattern = /(?:["']q["']|\bq)\s*:\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)/gs;
349
+ return Array.from(objectSource.matchAll(pattern), (match) => decodeJavaScriptStringLiteral(match[1]));
350
+ }
351
+ /**
352
+ * Finds the patch body an `apply_patch` call is given.
353
+ *
354
+ * The program almost always assigns it to a local first
355
+ * (`const patch = "*** Begin Patch..."; tools.apply_patch(patch)`), so the
356
+ * argument itself is usually an identifier. Scanning the whole script for the
357
+ * literal that carries the patch header covers both forms.
358
+ */
359
+ function readPatchLiteral(source) {
360
+ const pattern = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)/gs;
361
+ for (const match of source.matchAll(pattern)) {
362
+ const decoded = decodeJavaScriptStringLiteral(match[1]);
363
+ if (decoded.includes('*** Begin Patch')) {
364
+ return decoded;
365
+ }
366
+ }
367
+ return undefined;
368
+ }
369
+ /**
370
+ * Reads the steps of an `update_plan` call made from an exec script.
371
+ *
372
+ * The script writes its argument as a JavaScript object literal with unquoted
373
+ * keys, so it is not JSON — each `{step, status}` entry is matched directly.
374
+ */
375
+ function readPlanSteps(objectSource) {
376
+ const todos = [];
377
+ for (const entry of objectSource.matchAll(/\{([^{}]*)\}/g)) {
378
+ const content = readStringProperty(entry[1], 'step') ?? readStringProperty(entry[1], 'content');
379
+ if (content) {
380
+ todos.push({ content, status: readStringProperty(entry[1], 'status') ?? 'pending' });
381
+ }
382
+ }
383
+ return todos;
384
+ }
385
+ /**
386
+ * Recovers commands a script keeps in a local array and maps over, which is how
387
+ * Codex batches several shell calls:
388
+ * `const cmds = ["a", "b"]; await Promise.all(cmds.map(command => tools.shell_command({ command })))`
389
+ * The call site then passes `{ command }` shorthand, so there is no literal to
390
+ * read at the call itself.
391
+ */
392
+ function readMappedCommandCandidates(source) {
393
+ const commands = [];
394
+ const arrayPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*\[([\s\S]*?)\]\s*;/g;
395
+ const stringPattern = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)/gs;
396
+ for (const arrayMatch of source.matchAll(arrayPattern)) {
397
+ if (!new RegExp(`\\b${arrayMatch[1]}\\.map\\s*\\(`).test(source)) {
398
+ continue;
399
+ }
400
+ for (const stringMatch of arrayMatch[2].matchAll(stringPattern)) {
401
+ commands.push(decodeJavaScriptStringLiteral(stringMatch[1]));
125
402
  }
126
403
  }
127
404
  return commands;
128
405
  }
129
406
  /**
130
- * Newer Codex rollouts persist the orchestration wrapper (`exec`) instead of
131
- * the nested tool name. Recover the useful UI-level operation so history does
132
- * not degrade into rows labelled only "exec / Parameters".
407
+ * Turns one Codex `exec` script into the operations it performs, in source
408
+ * order. An empty result means the script did nothing recognizable and the
409
+ * caller should fall back to showing it as code.
410
+ *
411
+ * Exported for tests.
133
412
  */
134
- function translateCodexExecInput(input) {
135
- const source = typeof input === 'string' ? input : String(input || '');
136
- if (/\btools\.(?:shell_command|exec_command)\s*\(/.test(source)) {
137
- const commands = extractNestedCodexCommands(source);
138
- if (commands.length > 0) {
139
- return {
140
- toolName: 'Bash',
141
- toolInput: JSON.stringify({ command: commands.join('\n') }),
142
- };
413
+ export function parseCodexExecScript(input) {
414
+ const source = typeof input === 'string' ? input : String(input ?? '');
415
+ if (!source.includes('tools.')) {
416
+ return [];
417
+ }
418
+ const operations = [];
419
+ const callPattern = /tools\.([A-Za-z_0-9]+)\s*\(/g;
420
+ /** Filled in lazily, and only once, for shorthand-argument shell calls. */
421
+ let mappedCommands = null;
422
+ for (const match of source.matchAll(callPattern)) {
423
+ const toolName = match[1];
424
+ const openParenIndex = match.index + match[0].length - 1;
425
+ const argumentSource = readCallArguments(source, openParenIndex) ?? '';
426
+ switch (toolName) {
427
+ case 'shell_command':
428
+ case 'exec_command': {
429
+ const command = readStringProperty(argumentSource, 'command')
430
+ ?? readStringProperty(argumentSource, 'cmd');
431
+ if (command) {
432
+ operations.push({
433
+ kind: 'shell',
434
+ command,
435
+ justification: readStringProperty(argumentSource, 'justification'),
436
+ });
437
+ break;
438
+ }
439
+ if (mappedCommands === null) {
440
+ mappedCommands = readMappedCommandCandidates(source);
441
+ for (const mappedCommand of mappedCommands) {
442
+ operations.push({ kind: 'shell', command: mappedCommand });
443
+ }
444
+ }
445
+ break;
446
+ }
447
+ case 'write_stdin': {
448
+ operations.push({ kind: 'stdin', chars: readStringProperty(argumentSource, 'chars') ?? '' });
449
+ break;
450
+ }
451
+ case 'apply_patch': {
452
+ const patch = readPatchLiteral(argumentSource) ?? readPatchLiteral(source);
453
+ if (patch) {
454
+ operations.push({ kind: 'patch', patch });
455
+ }
456
+ break;
457
+ }
458
+ case 'web__run':
459
+ case 'web_search': {
460
+ const queries = readSearchQueries(argumentSource);
461
+ if (queries.length > 0) {
462
+ operations.push({ kind: 'search', queries });
463
+ }
464
+ break;
465
+ }
466
+ case 'update_plan': {
467
+ const todos = readPlanSteps(argumentSource);
468
+ if (todos.length > 0) {
469
+ operations.push({ kind: 'plan', todos });
470
+ }
471
+ break;
472
+ }
473
+ default:
474
+ break;
143
475
  }
144
476
  }
145
- return null;
477
+ return operations;
146
478
  }
147
- function humanizeCodexToolName(toolName) {
148
- return toolName
149
- .replace(/__/g, ' ')
150
- .replace(/_/g, ' ')
151
- .replace(/\b\w/g, (character) => character.toUpperCase());
479
+ /**
480
+ * Strips the two nested envelopes Codex wraps shell output in.
481
+ *
482
+ * The outer one reports on the sandbox *script* (`Script completed`, its wall
483
+ * time, and `Output:`); the inner one reports on the *command* (`Exit code`,
484
+ * its own wall time, an optional line count). Neither is useful in a chat
485
+ * transcript, but the exit code decides whether the row is shown as failed, so
486
+ * it is lifted out before the text is discarded.
487
+ *
488
+ * Exported for tests.
489
+ */
490
+ export function cleanCodexShellOutput(rawOutput) {
491
+ let output = rawOutput;
492
+ let exitCode;
493
+ let runningCellId;
494
+ let truncated = false;
495
+ const runningMatch = /^Script running with cell ID\s+(\S+)[^\n]*\n?/i.exec(output);
496
+ if (runningMatch) {
497
+ runningCellId = runningMatch[1];
498
+ output = output.slice(runningMatch[0].length);
499
+ }
500
+ else {
501
+ output = output.replace(/^Script (?:completed|failed)[^\n]*\n?/i, '');
502
+ }
503
+ output = output.replace(/^Wall time[^\n]*\n?/i, '');
504
+ output = output.replace(/^Output:\s*\n?/i, '');
505
+ output = output.replace(/^Script error:\s*\n?/i, '');
506
+ const truncationMatch = /^Warning: truncated output[^\n]*\n(?:Total output lines:[^\n]*\n)?\n?/i.exec(output);
507
+ if (truncationMatch) {
508
+ truncated = true;
509
+ output = output.slice(truncationMatch[0].length);
510
+ output = output.replace(/^Script error:\s*\n?/i, '');
511
+ }
512
+ const exitCodeMatch = /^Exit code:\s*(-?\d+)[^\n]*\n?/i.exec(output);
513
+ if (exitCodeMatch) {
514
+ exitCode = Number.parseInt(exitCodeMatch[1], 10);
515
+ output = output.slice(exitCodeMatch[0].length);
516
+ output = output.replace(/^Wall time:[^\n]*\n?/i, '');
517
+ output = output.replace(/^Total output lines:[^\n]*\n?/i, '');
518
+ output = output.replace(/^Output:\s*\n?/i, '');
519
+ }
520
+ return { output, exitCode, runningCellId, truncated };
521
+ }
522
+ /**
523
+ * `write_stdin` answers with a JSON envelope rather than plain text. Only its
524
+ * `output` and `exit_code` matter to a reader.
525
+ */
526
+ function readCodexStdinOutput(rawOutput) {
527
+ const trimmed = rawOutput.trim();
528
+ const jsonStart = trimmed.indexOf('{');
529
+ if (jsonStart === -1) {
530
+ return null;
531
+ }
532
+ try {
533
+ const parsed = JSON.parse(trimmed.slice(jsonStart));
534
+ if (typeof parsed.output !== 'string') {
535
+ return null;
536
+ }
537
+ return {
538
+ output: parsed.output,
539
+ exitCode: typeof parsed.exit_code === 'number' ? parsed.exit_code : undefined,
540
+ truncated: false,
541
+ };
542
+ }
543
+ catch {
544
+ return null;
545
+ }
546
+ }
547
+ /**
548
+ * Rebuilds the before/after text of one unified diff.
549
+ *
550
+ * Only the hunks are available — never the whole file — so the reconstruction
551
+ * keeps context lines in both sides and drops hunk headers. Feeding that pair
552
+ * to the frontend's line differ reproduces exactly the additions, removals and
553
+ * surrounding context the patch described, which is what Claude's `Edit` rows
554
+ * already show.
555
+ *
556
+ * Exported for tests.
557
+ */
558
+ export function unifiedDiffToTexts(unifiedDiff) {
559
+ const oldLines = [];
560
+ const newLines = [];
561
+ for (const line of unifiedDiff.split('\n')) {
562
+ if (line.startsWith('@@') || line.startsWith('\\ No newline')) {
563
+ continue;
564
+ }
565
+ if (line.startsWith('+')) {
566
+ newLines.push(line.slice(1));
567
+ continue;
568
+ }
569
+ if (line.startsWith('-')) {
570
+ oldLines.push(line.slice(1));
571
+ continue;
572
+ }
573
+ const contextLine = line.startsWith(' ') ? line.slice(1) : line;
574
+ oldLines.push(contextLine);
575
+ newLines.push(contextLine);
576
+ }
577
+ // A unified diff ends with a newline, which would otherwise reconstruct as a
578
+ // phantom trailing blank line on both sides.
579
+ if (oldLines[oldLines.length - 1] === '' && newLines[newLines.length - 1] === '') {
580
+ oldLines.pop();
581
+ newLines.pop();
582
+ }
583
+ return { oldText: oldLines.join('\n'), newText: newLines.join('\n') };
584
+ }
585
+ /**
586
+ * Splits a `*** Begin Patch` body into one change per file.
587
+ *
588
+ * Exported for tests.
589
+ */
590
+ export function parseCodexPatch(patch) {
591
+ const changes = [];
592
+ const lines = patch.split('\n');
593
+ let current = null;
594
+ const flush = () => {
595
+ if (!current) {
596
+ return;
597
+ }
598
+ const { oldText, newText } = unifiedDiffToTexts(current.body.join('\n'));
599
+ changes.push({
600
+ filePath: current.filePath,
601
+ changeType: current.changeType,
602
+ oldText: current.changeType === 'add' ? '' : oldText,
603
+ newText: current.changeType === 'delete' ? '' : newText,
604
+ });
605
+ current = null;
606
+ };
607
+ for (const line of lines) {
608
+ const fileHeader = /^\*\*\* (Add|Update|Delete) File:\s*(.+)$/.exec(line);
609
+ if (fileHeader) {
610
+ flush();
611
+ const changeType = fileHeader[1].toLowerCase();
612
+ current = { filePath: fileHeader[2].trim(), changeType, body: [] };
613
+ continue;
614
+ }
615
+ if (line.startsWith('*** ')) {
616
+ // `*** Begin Patch`, `*** End Patch`, `*** Move to:` — never diff content.
617
+ continue;
618
+ }
619
+ current?.body.push(line);
620
+ }
621
+ flush();
622
+ return changes;
623
+ }
624
+ /** Converts a `patch_apply_end` change map into the same per-file shape. */
625
+ function readPatchApplyChanges(changes) {
626
+ const record = readObjectRecord(changes);
627
+ if (!record) {
628
+ return [];
629
+ }
630
+ const parsed = [];
631
+ for (const [filePath, rawChange] of Object.entries(record)) {
632
+ const change = readObjectRecord(rawChange);
633
+ if (!change) {
634
+ continue;
635
+ }
636
+ const changeType = change.type === 'add' || change.type === 'delete' ? change.type : 'update';
637
+ const { oldText, newText } = unifiedDiffToTexts(typeof change.unified_diff === 'string' ? change.unified_diff : '');
638
+ parsed.push({
639
+ filePath,
640
+ changeType,
641
+ oldText: changeType === 'add' ? '' : oldText,
642
+ newText: changeType === 'delete' ? '' : newText,
643
+ });
644
+ }
645
+ return parsed;
646
+ }
647
+ /** Maps a file change onto the tool the frontend already renders diffs for. */
648
+ function fileChangeToToolMessage(change, timestamp, toolCallId) {
649
+ const toolName = change.changeType === 'add' ? 'Write' : 'Edit';
650
+ return {
651
+ type: 'tool_use',
652
+ timestamp,
653
+ toolName,
654
+ toolInput: JSON.stringify({
655
+ file_path: change.filePath,
656
+ old_string: change.oldText,
657
+ new_string: change.newText,
658
+ ...(change.changeType === 'delete' ? { deleted: true } : {}),
659
+ }),
660
+ toolCallId,
661
+ };
662
+ }
663
+ /** Reads the `file_path` back out of a rendered file-change row. */
664
+ function readRowFilePath(row) {
665
+ try {
666
+ return String(JSON.parse(String(row.toolInput ?? '{}')).file_path ?? '');
667
+ }
668
+ catch {
669
+ return '';
670
+ }
671
+ }
672
+ /**
673
+ * Rewrites the rows reconstructed from a patch call's input with the
674
+ * authoritative diffs Codex reports once the patch has been applied.
675
+ *
676
+ * Rows are matched by path, never by position: `patch_apply_end` reports the
677
+ * touched files in map order, which does not follow the order they appeared in
678
+ * the patch body. Rows whose file the report does not mention are dropped
679
+ * rather than left behind claiming an edit that never happened.
680
+ */
681
+ function rewritePatchRows(rows, changes, callId, messages) {
682
+ const rowsByPath = new Map();
683
+ for (const row of rows) {
684
+ rowsByPath.set(readRowFilePath(row), row);
685
+ }
686
+ const keptRows = [];
687
+ for (const [index, change] of changes.entries()) {
688
+ const replacement = fileChangeToToolMessage(change, String(rows[0]?.timestamp ?? ''), `${callId}#${index}`);
689
+ const existing = rowsByPath.get(change.filePath);
690
+ if (existing) {
691
+ existing.toolName = replacement.toolName;
692
+ existing.toolInput = replacement.toolInput;
693
+ keptRows.push(existing);
694
+ rowsByPath.delete(change.filePath);
695
+ continue;
696
+ }
697
+ replacement.timestamp = rows[0]?.timestamp ?? replacement.timestamp;
698
+ keptRows.push(replacement);
699
+ messages.push(replacement);
700
+ }
701
+ for (const surplus of rowsByPath.values()) {
702
+ const position = messages.indexOf(surplus);
703
+ if (position !== -1) {
704
+ messages.splice(position, 1);
705
+ }
706
+ }
707
+ rows.splice(0, rows.length, ...keptRows);
152
708
  }
153
709
  function parseCodexSubagentMessage(payload) {
154
710
  const text = extractCodexTextContent(payload.content);
@@ -163,6 +719,250 @@ function parseCodexSubagentMessage(payload) {
163
719
  result: header?.[3]?.trim() || '',
164
720
  };
165
721
  }
722
+ /**
723
+ * Codex names a spawned agent's rollout file `rollout-<timestamp>-<threadId>`
724
+ * and writes it next to the parent's. Look there first, then widen to the
725
+ * enclosing day/month directories so a run that crosses midnight still
726
+ * resolves, and give up rather than walking the whole archive.
727
+ */
728
+ async function findCodexSubagentRollout(parentFilePath, agentThreadId) {
729
+ const suffix = `-${agentThreadId}.jsonl`;
730
+ let directory = path.dirname(parentFilePath);
731
+ for (let level = 0; level <= SUBAGENT_LOOKUP_PARENT_LEVELS; level += 1) {
732
+ const match = await findFileWithSuffix(directory, suffix, level === 0 ? 0 : level);
733
+ if (match) {
734
+ return match;
735
+ }
736
+ const parent = path.dirname(directory);
737
+ if (parent === directory) {
738
+ break;
739
+ }
740
+ directory = parent;
741
+ }
742
+ return null;
743
+ }
744
+ /** Depth-limited search for a file whose name ends with `suffix`. */
745
+ async function findFileWithSuffix(directory, suffix, depth) {
746
+ let entries;
747
+ try {
748
+ entries = await fsp.readdir(directory, { withFileTypes: true });
749
+ }
750
+ catch {
751
+ return null;
752
+ }
753
+ const subdirectories = [];
754
+ for (const entry of entries) {
755
+ if (entry.isDirectory()) {
756
+ subdirectories.push(path.join(directory, entry.name));
757
+ continue;
758
+ }
759
+ if (entry.name.endsWith(suffix)) {
760
+ return path.join(directory, entry.name);
761
+ }
762
+ }
763
+ if (depth <= 0) {
764
+ return null;
765
+ }
766
+ for (const subdirectory of subdirectories) {
767
+ const match = await findFileWithSuffix(subdirectory, suffix, depth - 1);
768
+ if (match) {
769
+ return match;
770
+ }
771
+ }
772
+ return null;
773
+ }
774
+ /**
775
+ * Flattens a spawned agent's own rollout into the shared activity timeline.
776
+ *
777
+ * The subagent's transcript is the same format as the parent's, so its shell
778
+ * and patch calls go through the same translation and end up rendered by the
779
+ * same components as the main thread's.
780
+ */
781
+ async function readCodexSubagentTranscript(filePath) {
782
+ const activity = [];
783
+ const transcript = { activity };
784
+ const pendingResults = new Map();
785
+ let fileStream;
786
+ try {
787
+ fileStream = fsSync.createReadStream(filePath);
788
+ }
789
+ catch {
790
+ return transcript;
791
+ }
792
+ const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
793
+ for await (const line of rl) {
794
+ if (!line.trim()) {
795
+ continue;
796
+ }
797
+ let entry;
798
+ try {
799
+ entry = JSON.parse(line);
800
+ }
801
+ catch {
802
+ continue;
803
+ }
804
+ const payload = readObjectRecord(entry.payload);
805
+ if (!payload) {
806
+ continue;
807
+ }
808
+ if (entry.type === 'session_meta') {
809
+ // A resumed rollout writes a second, sparser session_meta; assigning
810
+ // unconditionally would erase the identity the first one carried.
811
+ transcript.nickname = readNonEmptyString(payload.agent_nickname) ?? transcript.nickname;
812
+ transcript.agentPath = readNonEmptyString(payload.agent_path) ?? transcript.agentPath;
813
+ continue;
814
+ }
815
+ if (entry.type === 'turn_context') {
816
+ transcript.model = readNonEmptyString(payload.model) ?? transcript.model;
817
+ continue;
818
+ }
819
+ if (entry.type !== 'response_item') {
820
+ continue;
821
+ }
822
+ const timestamp = typeof entry.timestamp === 'string' ? entry.timestamp : undefined;
823
+ if (payload.type === 'message' && payload.role === 'assistant') {
824
+ const content = extractCodexTextContent(payload.content).trim();
825
+ if (content) {
826
+ activity.push({ kind: 'text', content, timestamp });
827
+ }
828
+ continue;
829
+ }
830
+ if (payload.type === 'reasoning') {
831
+ const summary = Array.isArray(payload.summary)
832
+ ? payload.summary.map((item) => item?.text).filter(Boolean).join('\n')
833
+ : '';
834
+ if (summary.trim()) {
835
+ activity.push({ kind: 'thinking', content: summary, timestamp });
836
+ }
837
+ continue;
838
+ }
839
+ if (payload.type === 'function_call' || payload.type === 'custom_tool_call') {
840
+ const callId = readNonEmptyString(payload.call_id) ?? generateMessageId('codex-subagent-call');
841
+ for (const child of translateCodexToolCall(payload, callId)) {
842
+ const record = { ...child, timestamp };
843
+ activity.push(record);
844
+ pendingResults.set(callId, record);
845
+ }
846
+ continue;
847
+ }
848
+ if (payload.type === 'function_call_output' || payload.type === 'custom_tool_call_output') {
849
+ const callId = readNonEmptyString(payload.call_id);
850
+ const target = callId ? pendingResults.get(callId) : undefined;
851
+ if (!target) {
852
+ continue;
853
+ }
854
+ const cleaned = cleanCodexShellOutput(extractCodexToolOutput(payload.output));
855
+ target.toolResult = {
856
+ content: cleaned.output,
857
+ isError: cleaned.exitCode !== undefined && cleaned.exitCode !== 0,
858
+ };
859
+ }
860
+ }
861
+ return transcript;
862
+ }
863
+ /**
864
+ * Translates one persisted Codex tool call into the canonical tool rows the
865
+ * transcript renders. Most calls produce exactly one row; a patch produces one
866
+ * per file it touched.
867
+ *
868
+ * Shared by the main thread and by subagent transcripts so a subagent's shell
869
+ * command looks identical to the parent's.
870
+ */
871
+ function translateCodexToolCall(payload, callId) {
872
+ const name = readNonEmptyString(payload.name) ?? 'tool';
873
+ const rawInput = payload.type === 'custom_tool_call' ? payload.input : payload.arguments;
874
+ if (name === 'exec') {
875
+ const operations = parseCodexExecScript(rawInput);
876
+ const rows = [];
877
+ for (const operation of operations) {
878
+ if (operation.kind === 'shell') {
879
+ rows.push({
880
+ kind: 'tool',
881
+ toolId: callId,
882
+ toolName: 'Bash',
883
+ toolInput: { command: operation.command, description: operation.justification },
884
+ });
885
+ }
886
+ else if (operation.kind === 'patch') {
887
+ for (const change of parseCodexPatch(operation.patch)) {
888
+ rows.push({
889
+ kind: 'tool',
890
+ toolId: callId,
891
+ toolName: change.changeType === 'add' ? 'Write' : 'Edit',
892
+ toolInput: {
893
+ file_path: change.filePath,
894
+ old_string: change.oldText,
895
+ new_string: change.newText,
896
+ },
897
+ });
898
+ }
899
+ }
900
+ else if (operation.kind === 'search') {
901
+ rows.push({
902
+ kind: 'tool',
903
+ toolId: callId,
904
+ toolName: 'WebSearch',
905
+ toolInput: { query: operation.queries.join(' | ') },
906
+ });
907
+ }
908
+ }
909
+ if (rows.length > 0) {
910
+ return rows;
911
+ }
912
+ }
913
+ if (name === 'shell_command' || name === 'exec_command') {
914
+ let command = '';
915
+ try {
916
+ const args = JSON.parse(String(rawInput || '{}'));
917
+ command = String(args.command ?? args.cmd ?? '');
918
+ }
919
+ catch {
920
+ command = String(rawInput ?? '');
921
+ }
922
+ return [{ kind: 'tool', toolId: callId, toolName: 'Bash', toolInput: { command } }];
923
+ }
924
+ if (name === 'apply_patch') {
925
+ return parseCodexPatch(String(rawInput ?? '')).map((change) => ({
926
+ kind: 'tool',
927
+ toolId: callId,
928
+ toolName: change.changeType === 'add' ? 'Write' : 'Edit',
929
+ toolInput: {
930
+ file_path: change.filePath,
931
+ old_string: change.oldText,
932
+ new_string: change.newText,
933
+ },
934
+ }));
935
+ }
936
+ if (name === 'update_plan') {
937
+ return [{ kind: 'tool', toolId: callId, toolName: 'TodoWrite', toolInput: readCodexPlanInput(rawInput) }];
938
+ }
939
+ return [{ kind: 'tool', toolId: callId, toolName: humanizeCodexToolName(name), toolInput: rawInput }];
940
+ }
941
+ /**
942
+ * Converts Codex's `update_plan` steps into the todo shape Claude's
943
+ * `TodoWrite` already uses, so one renderer covers both providers.
944
+ */
945
+ function readCodexPlanInput(rawArguments) {
946
+ try {
947
+ const parsed = JSON.parse(String(rawArguments || '{}'));
948
+ const steps = Array.isArray(parsed.plan) ? parsed.plan : [];
949
+ return {
950
+ todos: steps.map((step) => ({
951
+ content: typeof step?.step === 'string' ? step.step : String(step?.content ?? ''),
952
+ status: typeof step?.status === 'string' ? step.status : 'pending',
953
+ })),
954
+ };
955
+ }
956
+ catch {
957
+ return { todos: [] };
958
+ }
959
+ }
960
+ function humanizeCodexToolName(toolName) {
961
+ return toolName
962
+ .replace(/__/g, ' ')
963
+ .replace(/_/g, ' ')
964
+ .replace(/\b\w/g, (character) => character.toUpperCase());
965
+ }
166
966
  const CODEX_COLLABORATION_CONTROL_TOOLS = new Set([
167
967
  'followup_task',
168
968
  'interrupt_agent',
@@ -170,372 +970,696 @@ const CODEX_COLLABORATION_CONTROL_TOOLS = new Set([
170
970
  'send_message',
171
971
  'wait_agent',
172
972
  ]);
173
- function readNonEmptyString(value) {
174
- return typeof value === 'string' && value.length > 0 ? value : undefined;
175
- }
176
- async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
177
- try {
178
- const sessionFilePath = sessionsDb.getSessionById(sessionId)?.jsonl_path;
179
- if (!sessionFilePath) {
180
- console.warn(`Codex session file not found for session ${sessionId}`);
181
- return { messages: [], total: 0, hasMore: false };
182
- }
183
- const messages = [];
184
- let tokenUsage = null;
185
- const ignoredToolCallIds = new Set();
186
- const execToolCallIds = new Set();
187
- const execCallByCellId = new Map();
188
- const waitCallToExecCall = new Map();
189
- const pendingExecOutput = new Map();
190
- const completedExecCalls = new Set();
191
- const subagentsByCallId = new Map();
192
- const subagentsByPath = new Map();
193
- const fileStream = fsSync.createReadStream(sessionFilePath);
194
- const rl = readline.createInterface({
195
- input: fileStream,
196
- crlfDelay: Infinity,
197
- });
198
- for await (const line of rl) {
199
- if (!line.trim()) {
973
+ // ─── Transcript reader ──────────────────────────────────────────────────────
974
+ /**
975
+ * Reads one Codex rollout file and produces the compact per-message records
976
+ * `normalizeHistoryEntry` turns into `NormalizedMessage`s.
977
+ */
978
+ async function getCodexSessionMessages(sessionId) {
979
+ const sessionFilePath = sessionsDb.getSessionById(sessionId)?.jsonl_path;
980
+ if (!sessionFilePath) {
981
+ console.warn(`Codex session file not found for session ${sessionId}`);
982
+ return { messages: [], total: 0, hasMore: false };
983
+ }
984
+ const messages = [];
985
+ let tokenUsage = null;
986
+ /** Call ids whose orchestration wrapper must never reach the transcript. */
987
+ const ignoredToolCallIds = new Set();
988
+ /** Exec calls that own a shell row, so their output can be routed back. */
989
+ const shellCallMessages = new Map();
990
+ /**
991
+ * File rows produced by each patch call, so the authoritative
992
+ * `patch_apply_end` diffs can replace what was reconstructed from the call
993
+ * input, and so every row gets exactly one result.
994
+ */
995
+ const patchGroups = new Map();
996
+ /** Patch call awaiting its out-of-band `patch_apply_end` report. */
997
+ let pendingPatchCallId = null;
998
+ const execCallByCellId = new Map();
999
+ const waitCallToExecCall = new Map();
1000
+ const pendingExecOutput = new Map();
1001
+ const completedExecCalls = new Set();
1002
+ /** Newest still-running exec call, for `write_stdin` polls that carry no cell id. */
1003
+ let runningExecCallId = null;
1004
+ const subagentsByCallId = new Map();
1005
+ const subagentsByPath = new Map();
1006
+ const turns = createCodexTurnTracker();
1007
+ /** Turns whose prompt already carries the anchor, so only the first does. */
1008
+ const anchoredTurnIds = new Set();
1009
+ const fileStream = fsSync.createReadStream(sessionFilePath);
1010
+ const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
1011
+ /** Emits a tool_result row unless the call already produced one. */
1012
+ const pushToolResult = (callId, timestamp, output, isError) => {
1013
+ if (completedExecCalls.has(callId)) {
1014
+ return;
1015
+ }
1016
+ completedExecCalls.add(callId);
1017
+ messages.push({ type: 'tool_result', timestamp, toolCallId: callId, output, isError });
1018
+ };
1019
+ for await (const line of rl) {
1020
+ if (!line.trim()) {
1021
+ continue;
1022
+ }
1023
+ let entry;
1024
+ try {
1025
+ entry = JSON.parse(line);
1026
+ }
1027
+ catch {
1028
+ continue;
1029
+ }
1030
+ const payload = readObjectRecord(entry.payload);
1031
+ if (!payload) {
1032
+ continue;
1033
+ }
1034
+ const timestamp = typeof entry.timestamp === 'string' ? entry.timestamp : new Date().toISOString();
1035
+ // Before the type-specific branches: a turn is opened and closed by rows
1036
+ // that produce no transcript entry of their own, and each of those
1037
+ // branches ends in a `continue`.
1038
+ turns.observe(entry.type, payload);
1039
+ // ── event_msg ──────────────────────────────────────────────────────────
1040
+ if (entry.type === 'event_msg') {
1041
+ if (payload.type === 'token_count' && payload.info) {
1042
+ const info = payload.info;
1043
+ if (info.total_token_usage) {
1044
+ const usage = info.total_token_usage;
1045
+ tokenUsage = { used: usage.total_tokens || 0, total: info.model_context_window || 200000 };
1046
+ }
200
1047
  continue;
201
1048
  }
202
- try {
203
- const entry = JSON.parse(line);
204
- if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
205
- const info = entry.payload.info;
206
- if (info.total_token_usage) {
207
- const usage = info.total_token_usage;
208
- tokenUsage = {
209
- used: usage.total_tokens || 0,
210
- total: info.model_context_window || 200000,
211
- };
212
- }
213
- }
214
- if (entry.type === 'event_msg'
215
- && entry.payload?.type === 'sub_agent_activity'
216
- && entry.payload.kind === 'started') {
217
- const eventId = readNonEmptyString(entry.payload.event_id);
218
- const agentPath = readNonEmptyString(entry.payload.agent_path);
219
- const subagent = eventId ? subagentsByCallId.get(eventId) : undefined;
220
- if (subagent && agentPath) {
1049
+ if (payload.type === 'sub_agent_activity' && payload.kind === 'started') {
1050
+ const eventId = readNonEmptyString(payload.event_id);
1051
+ const agentPath = readNonEmptyString(payload.agent_path);
1052
+ const agentThreadId = readNonEmptyString(payload.agent_thread_id);
1053
+ const subagent = eventId ? subagentsByCallId.get(eventId) : undefined;
1054
+ if (subagent) {
1055
+ subagent.agentThreadId = agentThreadId ?? subagent.agentThreadId;
1056
+ if (agentPath) {
221
1057
  subagent.agentPath = agentPath;
222
1058
  subagentsByPath.set(agentPath, subagent);
223
1059
  }
224
1060
  }
225
- if (entry.type === 'event_msg' && isVisibleCodexUserMessage(entry.payload)) {
226
- messages.push({
227
- type: 'user',
228
- timestamp: entry.timestamp,
229
- message: {
230
- role: 'user',
231
- content: entry.payload.message,
232
- },
233
- images: extractCodexUserImages(entry.payload),
234
- });
235
- }
236
- if (entry.type === 'response_item' &&
237
- entry.payload?.type === 'message' &&
238
- entry.payload.role === 'assistant') {
239
- const textContent = extractCodexTextContent(entry.payload.content);
240
- if (textContent.trim()) {
241
- messages.push({
242
- type: 'assistant',
243
- timestamp: entry.timestamp,
244
- message: {
245
- role: 'assistant',
246
- content: textContent,
247
- },
248
- });
249
- }
1061
+ continue;
1062
+ }
1063
+ // Codex reports patch results out of band. They carry the real unified
1064
+ // diffs, so they replace whatever was reconstructed from the call input.
1065
+ // Codex reports the applied patch out of band, and it is the only place
1066
+ // the real unified diffs appear. It always arrives before the call's own
1067
+ // output, so the rows reconstructed from the call input are rewritten
1068
+ // here rather than duplicated.
1069
+ if (payload.type === 'patch_apply_end') {
1070
+ const callId = readNonEmptyString(payload.call_id);
1071
+ if (!callId) {
1072
+ continue;
250
1073
  }
251
- if (entry.type === 'response_item' && entry.payload?.type === 'reasoning') {
252
- const summaryText = Array.isArray(entry.payload.summary)
253
- ? entry.payload.summary
254
- .map((item) => item?.text)
255
- .filter(Boolean)
256
- .join('\n')
257
- : '';
258
- if (summaryText.trim()) {
259
- messages.push({
260
- type: 'thinking',
261
- timestamp: entry.timestamp,
262
- message: {
263
- role: 'assistant',
264
- content: summaryText,
265
- },
266
- });
267
- }
1074
+ const changes = readPatchApplyChanges(payload.changes);
1075
+ // A patch applied from inside an `exec` script is reported under a
1076
+ // synthetic id that does not match the script's call id, so fall back
1077
+ // to the patch call that is still in flight.
1078
+ const group = patchGroups.get(callId)
1079
+ ?? (pendingPatchCallId ? patchGroups.get(pendingPatchCallId) : undefined);
1080
+ if (group) {
1081
+ rewritePatchRows(group.rows, changes, group.callId, messages);
1082
+ group.success = payload.success !== false;
1083
+ group.failure = String(payload.stderr || payload.stdout || '');
1084
+ pendingPatchCallId = null;
268
1085
  }
269
- if (entry.type === 'response_item' && entry.payload?.type === 'agent_message') {
270
- const agentMessage = parseCodexSubagentMessage(entry.payload);
271
- if (agentMessage && agentMessage.messageType === 'FINAL_ANSWER' && agentMessage.result) {
272
- let subagent = subagentsByPath.get(agentMessage.author);
273
- if (!subagent) {
274
- const fallbackCallId = entry.payload.id || generateMessageId('codex-subagent');
275
- const taskName = agentMessage.author.split('/').filter(Boolean).pop() || 'agent';
276
- const taskMessage = {
277
- uuid: fallbackCallId,
278
- type: 'tool_use',
279
- timestamp: entry.timestamp,
280
- toolName: 'Task',
281
- toolInput: JSON.stringify({
282
- subagent_type: 'Codex',
283
- description: humanizeCodexToolName(taskName),
284
- }),
285
- toolCallId: fallbackCallId,
286
- };
287
- messages.push(taskMessage);
288
- subagent = {
289
- toolCallId: fallbackCallId,
290
- message: taskMessage,
291
- agentPath: agentMessage.author,
292
- isComplete: false,
293
- };
294
- subagentsByCallId.set(fallbackCallId, subagent);
295
- subagentsByPath.set(agentMessage.author, subagent);
296
- }
297
- if (!subagent.isComplete) {
298
- messages.push({
299
- type: 'tool_result',
300
- timestamp: entry.timestamp,
301
- toolCallId: subagent.toolCallId,
302
- output: agentMessage.result,
303
- });
304
- subagent.isComplete = true;
305
- }
306
- }
1086
+ else if (changes.length > 0) {
1087
+ // A resumed thread can replay the result of a patch whose call is
1088
+ // not in this file; the rows still belong in the transcript.
1089
+ const rows = changes.map((change, index) => fileChangeToToolMessage(change, timestamp, `${callId}#${index}`));
1090
+ rows.forEach((row) => messages.push(row));
1091
+ patchGroups.set(callId, {
1092
+ callId,
1093
+ rows,
1094
+ timestamp,
1095
+ success: payload.success !== false,
1096
+ failure: String(payload.stderr || payload.stdout || ''),
1097
+ });
307
1098
  }
308
- if (entry.type === 'response_item' && entry.payload?.type === 'function_call') {
309
- let toolName = entry.payload.name;
310
- let toolInput = entry.payload.arguments;
311
- if (toolName === 'spawn_agent') {
312
- let taskName = 'agent';
313
- try {
314
- const args = JSON.parse(String(entry.payload.arguments || '{}'));
315
- taskName = readNonEmptyString(args.task_name) || taskName;
316
- }
317
- catch {
318
- // The activity event can still provide the canonical agent path.
1099
+ continue;
1100
+ }
1101
+ if (payload.type === 'turn_aborted') {
1102
+ messages.push({
1103
+ type: 'status_note',
1104
+ timestamp,
1105
+ content: payload.reason === 'interrupted' ? 'Turn interrupted' : `Turn aborted (${String(payload.reason ?? 'unknown')})`,
1106
+ });
1107
+ continue;
1108
+ }
1109
+ if (payload.type === 'context_compacted') {
1110
+ messages.push({ type: 'status_note', timestamp, content: 'Context compacted' });
1111
+ continue;
1112
+ }
1113
+ // Paginated rollouts carry user prompts as `item_completed` /
1114
+ // `UserMessage`; wire `response_item` user rows are internal, not prompts.
1115
+ if (payload.type === 'item_completed') {
1116
+ const item = readObjectRecord(payload.item);
1117
+ if (item?.type === 'UserMessage') {
1118
+ const content = extractCodexTextContent(item.content);
1119
+ const images = extractCodexTypedUserImages(item.content);
1120
+ if (content.trim() || images?.length) {
1121
+ // Only the first prompt of a turn is anchored, matching the legacy
1122
+ // `user_message` branch below: a turn can hold several prompts and
1123
+ // the edit cut is per turn.
1124
+ const turnId = readNonEmptyString(payload.turn_id)
1125
+ ?? turns.getCurrentTurnId();
1126
+ const isFirstPromptOfTurn = Boolean(turnId) && !anchoredTurnIds.has(turnId);
1127
+ if (isFirstPromptOfTurn) {
1128
+ anchoredTurnIds.add(turnId);
319
1129
  }
320
- const taskMessage = {
321
- uuid: entry.payload.call_id,
322
- type: 'tool_use',
323
- timestamp: entry.timestamp,
324
- toolName: 'Task',
325
- toolInput: JSON.stringify({
326
- subagent_type: 'Codex',
327
- description: humanizeCodexToolName(taskName),
328
- }),
329
- toolCallId: entry.payload.call_id,
1130
+ const row = {
1131
+ type: 'user',
1132
+ timestamp,
1133
+ message: { role: 'user', content },
1134
+ ...(images ? { images } : {}),
1135
+ ...(isFirstPromptOfTurn ? { turnId } : {}),
330
1136
  };
331
- messages.push(taskMessage);
332
- subagentsByCallId.set(entry.payload.call_id, {
333
- toolCallId: entry.payload.call_id,
334
- message: taskMessage,
335
- isComplete: false,
336
- });
337
- ignoredToolCallIds.add(entry.payload.call_id);
338
- continue;
1137
+ messages.push(row);
339
1138
  }
340
- if (toolName === 'wait') {
341
- try {
342
- const args = JSON.parse(String(entry.payload.arguments || '{}'));
343
- const cellId = String(args.cell_id || '');
344
- const execCallId = execCallByCellId.get(cellId);
345
- if (execCallId) {
346
- waitCallToExecCall.set(entry.payload.call_id, execCallId);
347
- }
348
- }
349
- catch {
350
- // Suppress the orchestration wait even when its payload is malformed.
351
- }
352
- ignoredToolCallIds.add(entry.payload.call_id);
353
- continue;
1139
+ }
1140
+ continue;
1141
+ }
1142
+ if (isVisibleCodexUserMessage(payload)) {
1143
+ // Only the first prompt of a turn is anchored. A turn can hold more
1144
+ // than one — a follow-up queued while the turn was running is written
1145
+ // into it — and the cut is per turn, so anchoring the second would
1146
+ // quietly take the first with it when the user edited only the second.
1147
+ const turnId = turns.getCurrentTurnId();
1148
+ const isFirstPromptOfTurn = Boolean(turnId) && !anchoredTurnIds.has(turnId);
1149
+ if (isFirstPromptOfTurn) {
1150
+ anchoredTurnIds.add(turnId);
1151
+ }
1152
+ messages.push({
1153
+ type: 'user',
1154
+ timestamp,
1155
+ message: { role: 'user', content: payload.message },
1156
+ images: extractCodexUserImages(payload),
1157
+ ...(isFirstPromptOfTurn ? { turnId } : {}),
1158
+ });
1159
+ }
1160
+ continue;
1161
+ }
1162
+ if (entry.type !== 'response_item') {
1163
+ continue;
1164
+ }
1165
+ // ── response_item ──────────────────────────────────────────────────────
1166
+ if (payload.type === 'message' && payload.role === 'assistant') {
1167
+ const cited = readCodexMemoryCitations(extractCodexTextContent(payload.content));
1168
+ const textContent = cited.text;
1169
+ if (!textContent.trim()) {
1170
+ continue;
1171
+ }
1172
+ const proposedPlan = readCodexProposedPlan(textContent);
1173
+ if (proposedPlan) {
1174
+ const planCallId = readNonEmptyString(payload.id) ?? generateMessageId('codex-plan');
1175
+ messages.push({
1176
+ type: 'tool_use',
1177
+ timestamp,
1178
+ toolName: 'ExitPlanMode',
1179
+ toolInput: JSON.stringify({ plan: proposedPlan }),
1180
+ toolCallId: planCallId,
1181
+ memoryCitations: cited.memoryCitations,
1182
+ });
1183
+ messages.push({ type: 'tool_result', timestamp, toolCallId: planCallId, output: '' });
1184
+ continue;
1185
+ }
1186
+ messages.push({
1187
+ type: 'assistant',
1188
+ timestamp,
1189
+ message: { role: 'assistant', content: textContent },
1190
+ memoryCitations: cited.memoryCitations,
1191
+ });
1192
+ continue;
1193
+ }
1194
+ if (payload.type === 'reasoning') {
1195
+ const summaryText = Array.isArray(payload.summary)
1196
+ ? payload.summary.map((item) => item?.text).filter(Boolean).join('\n')
1197
+ : '';
1198
+ if (summaryText.trim()) {
1199
+ messages.push({
1200
+ type: 'thinking',
1201
+ timestamp,
1202
+ message: { role: 'assistant', content: summaryText },
1203
+ });
1204
+ }
1205
+ continue;
1206
+ }
1207
+ if (payload.type === 'web_search_call') {
1208
+ const action = readObjectRecord(payload.action);
1209
+ const query = readNonEmptyString(action?.query);
1210
+ if (query) {
1211
+ const callId = readNonEmptyString(payload.id) ?? generateMessageId('codex-search');
1212
+ messages.push({
1213
+ type: 'tool_use',
1214
+ timestamp,
1215
+ toolName: 'WebSearch',
1216
+ toolInput: JSON.stringify({ query }),
1217
+ toolCallId: callId,
1218
+ });
1219
+ }
1220
+ continue;
1221
+ }
1222
+ if (payload.type === 'agent_message') {
1223
+ const agentMessage = parseCodexSubagentMessage(payload);
1224
+ if (!agentMessage || agentMessage.messageType !== 'FINAL_ANSWER' || !agentMessage.result) {
1225
+ continue;
1226
+ }
1227
+ let subagent = subagentsByPath.get(agentMessage.author);
1228
+ if (!subagent) {
1229
+ // A resumed thread can replay an answer whose spawn call is not in this
1230
+ // file. Synthesize the container so the answer still has a home.
1231
+ const fallbackCallId = readNonEmptyString(payload.id) ?? generateMessageId('codex-subagent');
1232
+ const taskName = agentMessage.author.split('/').filter(Boolean).pop() || 'agent';
1233
+ const taskMessage = {
1234
+ uuid: fallbackCallId,
1235
+ type: 'tool_use',
1236
+ timestamp,
1237
+ toolName: 'Task',
1238
+ toolInput: JSON.stringify({ description: humanizeCodexToolName(taskName) }),
1239
+ toolCallId: fallbackCallId,
1240
+ };
1241
+ messages.push(taskMessage);
1242
+ subagent = { toolCallId: fallbackCallId, message: taskMessage, agentPath: agentMessage.author, isComplete: false };
1243
+ subagentsByCallId.set(fallbackCallId, subagent);
1244
+ subagentsByPath.set(agentMessage.author, subagent);
1245
+ }
1246
+ if (!subagent.isComplete) {
1247
+ messages.push({
1248
+ type: 'tool_result',
1249
+ timestamp,
1250
+ toolCallId: subagent.toolCallId,
1251
+ output: agentMessage.result,
1252
+ });
1253
+ subagent.isComplete = true;
1254
+ }
1255
+ continue;
1256
+ }
1257
+ if (payload.type === 'function_call' || payload.type === 'custom_tool_call') {
1258
+ const callId = readNonEmptyString(payload.call_id) ?? generateMessageId('codex-call');
1259
+ const name = readNonEmptyString(payload.name) ?? 'tool';
1260
+ const rawInput = payload.type === 'custom_tool_call' ? payload.input : payload.arguments;
1261
+ if (name === 'spawn_agent') {
1262
+ let taskName = 'agent';
1263
+ let prompt;
1264
+ try {
1265
+ const args = JSON.parse(String(rawInput || '{}'));
1266
+ taskName = readNonEmptyString(args.task_name) || taskName;
1267
+ // `message` is an encrypted transport blob on collaboration spawns —
1268
+ // only surface it when it is readable prompt text.
1269
+ const message = readNonEmptyString(args.message);
1270
+ prompt = message && !/^gAAAAA/.test(message) ? message : undefined;
1271
+ }
1272
+ catch {
1273
+ // The activity event still supplies the canonical agent path.
1274
+ }
1275
+ const taskMessage = {
1276
+ uuid: callId,
1277
+ type: 'tool_use',
1278
+ timestamp,
1279
+ toolName: 'Task',
1280
+ // No `subagent_type`: Codex has no agent presets, and naming the
1281
+ // provider there would render as "Codex / Codex" in the transcript.
1282
+ toolInput: JSON.stringify({
1283
+ description: humanizeCodexToolName(taskName),
1284
+ ...(prompt ? { prompt } : {}),
1285
+ }),
1286
+ toolCallId: callId,
1287
+ };
1288
+ messages.push(taskMessage);
1289
+ subagentsByCallId.set(callId, { toolCallId: callId, message: taskMessage, isComplete: false });
1290
+ ignoredToolCallIds.add(callId);
1291
+ continue;
1292
+ }
1293
+ if (name === 'wait') {
1294
+ try {
1295
+ const args = JSON.parse(String(rawInput || '{}'));
1296
+ const execCallId = execCallByCellId.get(String(args.cell_id || ''));
1297
+ if (execCallId) {
1298
+ waitCallToExecCall.set(callId, execCallId);
354
1299
  }
355
- if (CODEX_COLLABORATION_CONTROL_TOOLS.has(toolName)) {
356
- ignoredToolCallIds.add(entry.payload.call_id);
357
- continue;
1300
+ }
1301
+ catch {
1302
+ // Suppress the orchestration wait even when its payload is malformed.
1303
+ }
1304
+ ignoredToolCallIds.add(callId);
1305
+ continue;
1306
+ }
1307
+ if (CODEX_COLLABORATION_CONTROL_TOOLS.has(name)) {
1308
+ ignoredToolCallIds.add(callId);
1309
+ continue;
1310
+ }
1311
+ if (name === 'update_plan') {
1312
+ messages.push({
1313
+ type: 'tool_use',
1314
+ timestamp,
1315
+ toolName: 'TodoWrite',
1316
+ toolInput: JSON.stringify(readCodexPlanInput(rawInput)),
1317
+ toolCallId: callId,
1318
+ });
1319
+ continue;
1320
+ }
1321
+ if (name === 'exec') {
1322
+ const operations = parseCodexExecScript(rawInput);
1323
+ const shellCommands = operations.filter((operation) => operation.kind === 'shell');
1324
+ const stdinWrites = operations.filter((operation) => operation.kind === 'stdin');
1325
+ const patches = operations.filter((operation) => operation.kind === 'patch');
1326
+ const searches = operations.filter((operation) => operation.kind === 'search');
1327
+ const plans = operations.filter((operation) => operation.kind === 'plan');
1328
+ // A bare stdin poll continues whatever command is still running; it is
1329
+ // a continuation, not an action of its own.
1330
+ if (operations.length > 0 && shellCommands.length === 0 && stdinWrites.length > 0) {
1331
+ ignoredToolCallIds.add(callId);
1332
+ if (runningExecCallId) {
1333
+ waitCallToExecCall.set(callId, runningExecCallId);
358
1334
  }
359
- if (toolName === 'shell_command') {
360
- toolName = 'Bash';
361
- try {
362
- const args = JSON.parse(entry.payload.arguments);
363
- toolInput = JSON.stringify({ command: args.command });
364
- }
365
- catch {
366
- // Keep original arguments when parsing fails.
367
- }
1335
+ continue;
1336
+ }
1337
+ if (operations.length === 0) {
1338
+ // An unrecognized wrapper is shown as-is rather than mislabelled as a
1339
+ // shell command, so a new Codex tool degrades to a readable code
1340
+ // block instead of a wrong one.
1341
+ const fallbackMessage = {
1342
+ type: 'tool_use',
1343
+ timestamp,
1344
+ toolName: 'exec',
1345
+ toolInput: rawInput,
1346
+ toolCallId: callId,
1347
+ };
1348
+ messages.push(fallbackMessage);
1349
+ continue;
1350
+ }
1351
+ // The call has exactly one output, so whichever row represents the work
1352
+ // it did keeps the call id and receives that output. Extra rows get
1353
+ // suffixed ids and are resolved by their own bookkeeping.
1354
+ let outputOwnerAssigned = false;
1355
+ const nextRowId = () => {
1356
+ if (!outputOwnerAssigned) {
1357
+ outputOwnerAssigned = true;
1358
+ return callId;
368
1359
  }
1360
+ return `${callId}~${messages.length}`;
1361
+ };
1362
+ if (shellCommands.length > 0) {
1363
+ const shellMessage = {
1364
+ type: 'tool_use',
1365
+ timestamp,
1366
+ toolName: 'Bash',
1367
+ toolInput: JSON.stringify({
1368
+ command: shellCommands.map((operation) => operation.command).join('\n'),
1369
+ description: shellCommands.find((operation) => operation.justification)?.justification,
1370
+ }),
1371
+ toolCallId: nextRowId(),
1372
+ };
1373
+ messages.push(shellMessage);
1374
+ shellCallMessages.set(callId, shellMessage);
1375
+ }
1376
+ for (const search of searches) {
369
1377
  messages.push({
370
1378
  type: 'tool_use',
371
- timestamp: entry.timestamp,
372
- toolName,
373
- toolInput,
374
- toolCallId: entry.payload.call_id,
1379
+ timestamp,
1380
+ toolName: 'WebSearch',
1381
+ toolInput: JSON.stringify({ query: search.queries.join(' | ') }),
1382
+ toolCallId: nextRowId(),
375
1383
  });
376
1384
  }
377
- if (entry.type === 'response_item' && entry.payload?.type === 'function_call_output') {
378
- const waitExecCallId = waitCallToExecCall.get(entry.payload.call_id);
379
- if (waitExecCallId) {
380
- const output = extractCodexToolOutput(entry.payload.output);
381
- const runningOutput = readRunningExecOutput(output);
382
- const accumulatedOutput = `${pendingExecOutput.get(waitExecCallId) || ''}${runningOutput?.content ?? output}`;
383
- pendingExecOutput.set(waitExecCallId, accumulatedOutput);
384
- if (!runningOutput && !completedExecCalls.has(waitExecCallId)) {
385
- messages.push({
386
- type: 'tool_result',
387
- timestamp: entry.timestamp,
388
- toolCallId: waitExecCallId,
389
- output: accumulatedOutput,
390
- });
391
- completedExecCalls.add(waitExecCallId);
392
- }
393
- continue;
394
- }
395
- const subagent = subagentsByCallId.get(entry.payload.call_id);
396
- if (subagent) {
397
- const output = extractCodexToolOutput(entry.payload.output);
398
- try {
399
- const taskPath = readNonEmptyString(JSON.parse(output).task_name);
400
- if (taskPath) {
401
- subagent.agentPath = taskPath;
402
- subagentsByPath.set(taskPath, subagent);
403
- }
404
- }
405
- catch {
406
- // The sub_agent_activity event normally supplies the path.
407
- }
408
- continue;
409
- }
410
- if (ignoredToolCallIds.has(entry.payload.call_id)) {
411
- continue;
412
- }
1385
+ for (const plan of plans) {
1386
+ // The script's own result covers whatever else it did, so the plan
1387
+ // row is closed here — without one it would read as still running.
1388
+ const planCallId = nextRowId();
413
1389
  messages.push({
414
- type: 'tool_result',
415
- timestamp: entry.timestamp,
416
- toolCallId: entry.payload.call_id,
417
- output: extractCodexToolOutput(entry.payload.output),
1390
+ type: 'tool_use',
1391
+ timestamp,
1392
+ toolName: 'TodoWrite',
1393
+ toolInput: JSON.stringify({ todos: plan.todos }),
1394
+ toolCallId: planCallId,
418
1395
  });
1396
+ messages.push({ type: 'tool_result', timestamp, toolCallId: planCallId, output: '' });
419
1397
  }
420
- if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call') {
421
- let toolName = entry.payload.name || 'custom_tool';
422
- const input = entry.payload.input || '';
423
- let toolInput = input;
424
- if (toolName === 'exec') {
425
- const translated = translateCodexExecInput(input);
426
- if (translated) {
427
- toolName = translated.toolName;
428
- toolInput = translated.toolInput;
429
- }
430
- messages.push({
431
- type: 'tool_use',
432
- timestamp: entry.timestamp,
433
- toolName,
434
- toolInput,
435
- toolCallId: entry.payload.call_id,
436
- });
437
- execToolCallIds.add(entry.payload.call_id);
438
- continue;
439
- }
440
- if (toolName === 'apply_patch') {
441
- const fileMatch = String(input).match(/\*\*\* Update File: (.+)/);
442
- const filePath = fileMatch ? fileMatch[1].trim() : 'unknown';
443
- const lines = String(input).split('\n');
444
- const oldLines = [];
445
- const newLines = [];
446
- for (const lineContent of lines) {
447
- if (lineContent.startsWith('-') && !lineContent.startsWith('---')) {
448
- oldLines.push(lineContent.slice(1));
449
- }
450
- else if (lineContent.startsWith('+') && !lineContent.startsWith('+++')) {
451
- newLines.push(lineContent.slice(1));
452
- }
453
- }
454
- messages.push({
455
- type: 'tool_use',
456
- timestamp: entry.timestamp,
457
- toolName: 'Edit',
458
- toolInput: JSON.stringify({
459
- file_path: filePath,
460
- old_string: oldLines.join('\n'),
461
- new_string: newLines.join('\n'),
462
- }),
463
- toolCallId: entry.payload.call_id,
464
- });
465
- }
466
- else {
467
- messages.push({
468
- type: 'tool_use',
469
- timestamp: entry.timestamp,
470
- toolName,
471
- toolInput: input,
472
- toolCallId: entry.payload.call_id,
473
- });
474
- }
1398
+ if (patches.length > 0) {
1399
+ const changes = patches.flatMap((patch) => parseCodexPatch(patch.patch));
1400
+ const rows = changes.map((change, index) => fileChangeToToolMessage(change, timestamp, index === 0 ? nextRowId() : `${callId}#${index}`));
1401
+ rows.forEach((row) => messages.push(row));
1402
+ patchGroups.set(callId, { callId, rows, timestamp });
1403
+ pendingPatchCallId = callId;
1404
+ }
1405
+ if (!outputOwnerAssigned) {
1406
+ // Every recognized operation produced no row (an empty patch, say).
1407
+ // Nothing owns the output, so drop it rather than leaving it loose.
1408
+ ignoredToolCallIds.add(callId);
1409
+ }
1410
+ continue;
1411
+ }
1412
+ if (name === 'apply_patch') {
1413
+ const rows = parseCodexPatch(String(rawInput ?? ''))
1414
+ .map((change, index) => fileChangeToToolMessage(change, timestamp, `${callId}#${index}`));
1415
+ rows.forEach((row) => messages.push(row));
1416
+ patchGroups.set(callId, { callId, rows, timestamp });
1417
+ pendingPatchCallId = callId;
1418
+ // The call's own output is a bare `{}`; the per-file rows carry the result.
1419
+ ignoredToolCallIds.add(callId);
1420
+ continue;
1421
+ }
1422
+ if (name === 'shell_command' || name === 'exec_command') {
1423
+ let command = '';
1424
+ try {
1425
+ const args = JSON.parse(String(rawInput || '{}'));
1426
+ command = String(args.command ?? args.cmd ?? '');
1427
+ }
1428
+ catch {
1429
+ command = String(rawInput ?? '');
475
1430
  }
476
- if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call_output') {
477
- if (ignoredToolCallIds.has(entry.payload.call_id)) {
478
- continue;
1431
+ const shellMessage = {
1432
+ type: 'tool_use',
1433
+ timestamp,
1434
+ toolName: 'Bash',
1435
+ toolInput: JSON.stringify({ command }),
1436
+ toolCallId: callId,
1437
+ };
1438
+ messages.push(shellMessage);
1439
+ shellCallMessages.set(callId, shellMessage);
1440
+ continue;
1441
+ }
1442
+ messages.push({
1443
+ type: 'tool_use',
1444
+ timestamp,
1445
+ toolName: name,
1446
+ toolInput: rawInput,
1447
+ toolCallId: callId,
1448
+ });
1449
+ continue;
1450
+ }
1451
+ if (payload.type === 'function_call_output' || payload.type === 'custom_tool_call_output') {
1452
+ const callId = readNonEmptyString(payload.call_id) ?? '';
1453
+ const rawOutput = extractCodexToolOutput(payload.output);
1454
+ // A wait / stdin poll delivers the tail of an earlier command's output.
1455
+ const continuedCallId = waitCallToExecCall.get(callId);
1456
+ if (continuedCallId) {
1457
+ const cleaned = readCodexStdinOutput(rawOutput) ?? cleanCodexShellOutput(rawOutput);
1458
+ const accumulated = `${pendingExecOutput.get(continuedCallId) || ''}${cleaned.output}`;
1459
+ pendingExecOutput.set(continuedCallId, accumulated);
1460
+ if (!cleaned.runningCellId) {
1461
+ if (runningExecCallId === continuedCallId) {
1462
+ runningExecCallId = null;
479
1463
  }
480
- const output = extractCodexToolOutput(entry.payload.output);
481
- if (execToolCallIds.has(entry.payload.call_id)) {
482
- const runningOutput = readRunningExecOutput(output);
483
- if (runningOutput) {
484
- execCallByCellId.set(runningOutput.cellId, entry.payload.call_id);
485
- pendingExecOutput.set(entry.payload.call_id, runningOutput.content);
486
- continue;
487
- }
488
- completedExecCalls.add(entry.payload.call_id);
1464
+ pushToolResult(continuedCallId, timestamp, accumulated, cleaned.exitCode !== undefined && cleaned.exitCode !== 0);
1465
+ }
1466
+ continue;
1467
+ }
1468
+ const subagent = subagentsByCallId.get(callId);
1469
+ if (subagent) {
1470
+ try {
1471
+ const taskPath = readNonEmptyString(JSON.parse(rawOutput).task_name);
1472
+ if (taskPath) {
1473
+ subagent.agentPath = taskPath;
1474
+ subagentsByPath.set(taskPath, subagent);
489
1475
  }
490
- messages.push({
491
- type: 'tool_result',
492
- timestamp: entry.timestamp,
493
- toolCallId: entry.payload.call_id,
494
- output,
495
- });
496
1476
  }
1477
+ catch {
1478
+ // The sub_agent_activity event normally supplies the path.
1479
+ }
1480
+ continue;
497
1481
  }
498
- catch {
499
- // Skip malformed lines.
500
- }
501
- }
502
- messages.sort((a, b) => new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime());
503
- const total = messages.length;
504
- if (limit !== null) {
505
- const startIndex = Math.max(0, total - offset - limit);
506
- const endIndex = total - offset;
507
- const paginatedMessages = messages.slice(startIndex, endIndex);
508
- const hasMore = startIndex > 0;
509
- return {
510
- messages: paginatedMessages,
511
- total,
512
- hasMore,
513
- offset,
514
- limit,
515
- tokenUsage,
516
- };
1482
+ if (ignoredToolCallIds.has(callId)) {
1483
+ continue;
1484
+ }
1485
+ if (shellCallMessages.has(callId)) {
1486
+ const cleaned = cleanCodexShellOutput(rawOutput);
1487
+ if (cleaned.runningCellId) {
1488
+ execCallByCellId.set(cleaned.runningCellId, callId);
1489
+ pendingExecOutput.set(callId, cleaned.output);
1490
+ runningExecCallId = callId;
1491
+ continue;
1492
+ }
1493
+ const accumulated = `${pendingExecOutput.get(callId) || ''}${cleaned.output}`;
1494
+ pushToolResult(callId, timestamp, accumulated, cleaned.exitCode !== undefined && cleaned.exitCode !== 0);
1495
+ continue;
1496
+ }
1497
+ pushToolResult(callId, timestamp, rawOutput, false);
517
1498
  }
518
- return { messages, tokenUsage };
519
1499
  }
520
- catch (error) {
521
- console.error(`Error reading Codex session messages for ${sessionId}:`, error);
522
- return { messages: [], total: 0, hasMore: false };
1500
+ // Every file row needs a result or the UI shows it as still running. The
1501
+ // patch outcome is known only after `patch_apply_end` (which may never
1502
+ // arrive), so results are emitted once the whole file has been read.
1503
+ for (const group of patchGroups.values()) {
1504
+ const failed = group.success === false;
1505
+ for (const row of group.rows) {
1506
+ messages.push({
1507
+ type: 'tool_result',
1508
+ timestamp: group.timestamp,
1509
+ toolCallId: row.toolCallId,
1510
+ output: failed ? (group.failure || 'Patch failed') : '',
1511
+ isError: failed,
1512
+ });
1513
+ }
1514
+ }
1515
+ await attachCodexSubagentTranscripts(sessionFilePath, subagentsByCallId);
1516
+ // A rollback is recorded after the turns it retires, so a prompt can be
1517
+ // anchored and then retired later in the same file. Its rows still render —
1518
+ // they are what the conversation looked like — but the turn is gone from the
1519
+ // thread, so the anchor goes with it and the message loses its pencil.
1520
+ for (const message of messages) {
1521
+ if (typeof message.turnId === 'string' && turns.isRolledBack(message.turnId)) {
1522
+ delete message.turnId;
1523
+ }
1524
+ }
1525
+ messages.sort((a, b) => new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime());
1526
+ return { messages, tokenUsage: tokenUsage ?? undefined };
1527
+ }
1528
+ /**
1529
+ * Loads each spawned agent's own rollout and hangs its timeline off the
1530
+ * `Task` row that started it.
1531
+ */
1532
+ async function attachCodexSubagentTranscripts(parentFilePath, subagentsByCallId) {
1533
+ for (const record of subagentsByCallId.values()) {
1534
+ let parsedInput = {};
1535
+ try {
1536
+ parsedInput = JSON.parse(String(record.message.toolInput || '{}'));
1537
+ }
1538
+ catch {
1539
+ parsedInput = {};
1540
+ }
1541
+ const agentName = record.agentPath?.split('/').filter(Boolean).pop();
1542
+ // Codex has no agent-type concept the way Claude does, so `type` is left
1543
+ // unset rather than filled with the provider's own name; the panel falls
1544
+ // back to a neutral label.
1545
+ const subagent = {
1546
+ id: record.agentThreadId ?? record.toolCallId,
1547
+ description: readNonEmptyString(parsedInput.description) ?? agentName,
1548
+ status: record.isComplete ? 'completed' : 'running',
1549
+ };
1550
+ if (record.agentThreadId) {
1551
+ const rolloutPath = await findCodexSubagentRollout(parentFilePath, record.agentThreadId);
1552
+ if (rolloutPath) {
1553
+ const transcript = await readCodexSubagentTranscript(rolloutPath);
1554
+ if (transcript.activity.length > 0) {
1555
+ record.message.subagentTools = transcript.activity
1556
+ .slice(0, MAX_TRANSMITTED_SUBAGENT_ACTIVITIES)
1557
+ .map(truncateSubagentActivity);
1558
+ subagent.activityCount = transcript.activity.length;
1559
+ }
1560
+ subagent.name = transcript.nickname ?? agentName;
1561
+ subagent.model = transcript.model;
1562
+ }
1563
+ }
1564
+ subagent.name = subagent.name ?? agentName;
1565
+ record.message.subagent = subagent;
523
1566
  }
524
1567
  }
525
1568
  export class CodexSessionsProvider {
1569
+ /**
1570
+ * Resolves the last turn to keep when the turn `anchorId` names is replaced.
1571
+ *
1572
+ * `thread/fork`'s `lastTurnId` is inclusive of the turn it names, which is
1573
+ * the same thing this contract's `resumeThroughId` means, so the answer is
1574
+ * simply the turn before the edited one. Editing the first prompt leaves
1575
+ * nothing to keep, and `null` is how that is reported.
1576
+ */
1577
+ async resolveEditAnchor(sessionId, anchorId) {
1578
+ const session = sessionsDb.getSessionById(sessionId);
1579
+ const jsonlPath = session?.jsonl_path;
1580
+ if (!jsonlPath || !session?.provider_session_id) {
1581
+ return { found: false, resumeThroughId: null };
1582
+ }
1583
+ const liveTurnIds = await readCodexLiveTurnIds(jsonlPath);
1584
+ const anchorIndex = liveTurnIds.indexOf(anchorId);
1585
+ if (anchorIndex < 0) {
1586
+ return { found: false, resumeThroughId: null };
1587
+ }
1588
+ return {
1589
+ found: true,
1590
+ resumeThroughId: anchorIndex === 0 ? null : liveTurnIds[anchorIndex - 1],
1591
+ };
1592
+ }
1593
+ /**
1594
+ * Branches the conversation at `keepThroughId` and moves the session onto
1595
+ * the branch.
1596
+ *
1597
+ * Codex threads are append-only — the SDK resumes one at its tip and nothing
1598
+ * shortens it — so rewinding means copying the part being kept into a new
1599
+ * thread. The pre-edit thread stays on disk in full and is recorded as
1600
+ * superseded so the session indexer does not hand it back later.
1601
+ */
1602
+ async rewindSession(sessionId, keepThroughId) {
1603
+ const session = sessionsDb.getSessionById(sessionId);
1604
+ const supersededThreadId = session?.provider_session_id;
1605
+ if (!session || !supersededThreadId) {
1606
+ throw new AppError('This session has not produced a transcript yet.', {
1607
+ code: 'EDIT_SOURCE_NOT_READY',
1608
+ statusCode: 409,
1609
+ });
1610
+ }
1611
+ // Nothing of the conversation survives an edit to its first prompt, and
1612
+ // there is no such thing as a fork of no turns, so the session is simply
1613
+ // detached and the next run opens a new thread.
1614
+ if (keepThroughId === null) {
1615
+ sessionsDb.markProviderSessionSuperseded({
1616
+ providerSessionId: supersededThreadId,
1617
+ provider: PROVIDER,
1618
+ sessionId,
1619
+ jsonlPath: session.jsonl_path ?? null,
1620
+ });
1621
+ sessionsDb.detachProviderSession(sessionId);
1622
+ return;
1623
+ }
1624
+ const fork = await codexAppServer.forkThread({
1625
+ threadId: supersededThreadId,
1626
+ lastTurnId: keepThroughId,
1627
+ cwd: session.project_path ?? '',
1628
+ });
1629
+ sessionsDb.markProviderSessionSuperseded({
1630
+ providerSessionId: supersededThreadId,
1631
+ provider: PROVIDER,
1632
+ sessionId,
1633
+ jsonlPath: session.jsonl_path ?? null,
1634
+ });
1635
+ sessionsDb.repointSessionToProviderSession(sessionId, {
1636
+ providerSessionId: fork.threadId,
1637
+ jsonlPath: fork.path,
1638
+ });
1639
+ }
526
1640
  /**
527
1641
  * Normalizes a persisted Codex JSONL entry.
528
1642
  *
529
1643
  * Live Codex SDK events are transformed before they reach normalizeMessage(),
530
- * while history entries already use a compact message/tool shape from projects.js.
1644
+ * while history entries already use the compact message/tool shape produced
1645
+ * by getCodexSessionMessages().
531
1646
  */
532
1647
  normalizeHistoryEntry(raw, sessionId) {
533
1648
  const ts = raw.timestamp || new Date().toISOString();
534
1649
  const baseId = raw.uuid || generateMessageId('codex');
1650
+ if (raw.type === 'status_note' && typeof raw.content === 'string') {
1651
+ return [createNormalizedMessage({
1652
+ id: baseId,
1653
+ sessionId,
1654
+ timestamp: ts,
1655
+ provider: PROVIDER,
1656
+ kind: 'task_notification',
1657
+ summary: raw.content,
1658
+ status: 'info',
1659
+ })];
1660
+ }
535
1661
  if (raw.type === 'thinking' || raw.isReasoning) {
536
- const thinkingContent = typeof raw.message?.content === 'string'
537
- ? raw.message.content
538
- : '';
1662
+ const thinkingContent = typeof raw.message?.content === 'string' ? raw.message.content : '';
539
1663
  if (!thinkingContent.trim()) {
540
1664
  return [];
541
1665
  }
@@ -573,6 +1697,10 @@ export class CodexSessionsProvider {
573
1697
  content: parsedFiles.text,
574
1698
  images: rawImages,
575
1699
  files,
1700
+ // The enclosing turn, when the reader could name one. `baseId` is not
1701
+ // usable for this: Codex rows carry no id, so it is synthesized per
1702
+ // read and would address a different message every time.
1703
+ transcriptAnchorId: readNonEmptyString(raw.turnId),
576
1704
  })];
577
1705
  }
578
1706
  if (raw.message?.role === 'assistant') {
@@ -595,6 +1723,7 @@ export class CodexSessionsProvider {
595
1723
  kind: 'text',
596
1724
  role: 'assistant',
597
1725
  content,
1726
+ memoryCitations: raw.memoryCitations,
598
1727
  })];
599
1728
  }
600
1729
  if (raw.type === 'tool_use' || raw.toolName) {
@@ -607,6 +1736,9 @@ export class CodexSessionsProvider {
607
1736
  toolName: raw.toolName || 'Unknown',
608
1737
  toolInput: raw.toolInput,
609
1738
  toolId: raw.toolCallId || baseId,
1739
+ subagentTools: raw.subagentTools,
1740
+ subagent: raw.subagent,
1741
+ memoryCitations: raw.memoryCitations,
610
1742
  })];
611
1743
  }
612
1744
  if (raw.type === 'tool_result') {
@@ -637,92 +1769,156 @@ export class CodexSessionsProvider {
637
1769
  const ts = raw.timestamp || new Date().toISOString();
638
1770
  const baseId = raw.uuid || generateMessageId('codex');
639
1771
  if (raw.type === 'item') {
1772
+ // Live items carry a stable SDK id, so an in-progress row and its later
1773
+ // completion collapse onto the same transcript entry instead of stacking.
1774
+ const itemId = readNonEmptyString(raw.itemId) ?? baseId;
640
1775
  switch (raw.itemType) {
641
- case 'agent_message':
1776
+ case 'agent_message': {
1777
+ const cited = readCodexMemoryCitations(String(raw.message?.content || ''));
1778
+ const text = cited.text;
1779
+ const proposedPlan = readCodexProposedPlan(text);
1780
+ if (proposedPlan) {
1781
+ // Same unification as history: a proposed plan is a plan card, not
1782
+ // an assistant paragraph that happens to start with a tag.
1783
+ return [createNormalizedMessage({
1784
+ id: itemId,
1785
+ sessionId,
1786
+ timestamp: ts,
1787
+ provider: PROVIDER,
1788
+ kind: 'tool_use',
1789
+ toolName: 'ExitPlanMode',
1790
+ toolInput: { plan: proposedPlan },
1791
+ toolId: itemId,
1792
+ memoryCitations: cited.memoryCitations,
1793
+ })];
1794
+ }
642
1795
  return [createNormalizedMessage({
643
- id: baseId,
1796
+ id: itemId,
644
1797
  sessionId,
645
1798
  timestamp: ts,
646
1799
  provider: PROVIDER,
647
1800
  kind: 'text',
648
1801
  role: 'assistant',
649
- content: raw.message?.content || '',
1802
+ content: text,
1803
+ memoryCitations: cited.memoryCitations,
650
1804
  })];
1805
+ }
651
1806
  case 'reasoning':
652
1807
  return [createNormalizedMessage({
653
- id: baseId,
1808
+ id: itemId,
654
1809
  sessionId,
655
1810
  timestamp: ts,
656
1811
  provider: PROVIDER,
657
1812
  kind: 'thinking',
658
1813
  content: raw.message?.content || '',
659
1814
  })];
660
- case 'command_execution':
661
- return [createNormalizedMessage({
662
- id: baseId,
663
- sessionId,
664
- timestamp: ts,
665
- provider: PROVIDER,
666
- kind: 'tool_use',
667
- toolName: 'Bash',
668
- toolInput: { command: raw.command },
669
- toolId: baseId,
670
- output: raw.output,
671
- exitCode: raw.exitCode,
672
- status: raw.status,
673
- })];
674
- case 'file_change':
675
- return [createNormalizedMessage({
676
- id: baseId,
1815
+ case 'command_execution': {
1816
+ const toolUse = createNormalizedMessage({
1817
+ id: itemId,
1818
+ sessionId,
1819
+ timestamp: ts,
1820
+ provider: PROVIDER,
1821
+ kind: 'tool_use',
1822
+ toolName: 'Bash',
1823
+ toolInput: { command: raw.command },
1824
+ toolId: itemId,
1825
+ status: raw.status,
1826
+ });
1827
+ const partialOutput = typeof raw.output === 'string' ? raw.output : '';
1828
+ if (raw.status === 'in_progress' && !partialOutput) {
1829
+ return [toolUse];
1830
+ }
1831
+ // The SDK reports a command's output on the item itself; the UI reads
1832
+ // tool output from a paired result, so one is emitted here. Emitting
1833
+ // it while the command is still running is what lets its output
1834
+ // stream into the row instead of appearing only at the end.
1835
+ return [toolUse, createNormalizedMessage({
1836
+ id: `${itemId}_result`,
677
1837
  sessionId,
678
1838
  timestamp: ts,
679
1839
  provider: PROVIDER,
680
- kind: 'tool_use',
681
- toolName: 'FileChanges',
682
- toolInput: raw.changes,
683
- toolId: baseId,
684
- status: raw.status,
1840
+ kind: 'tool_result',
1841
+ toolId: itemId,
1842
+ content: partialOutput,
1843
+ isError: raw.status === 'failed' || (typeof raw.exitCode === 'number' && raw.exitCode !== 0),
685
1844
  })];
686
- case 'mcp_tool_call':
687
- return [createNormalizedMessage({
688
- id: baseId,
1845
+ }
1846
+ case 'file_change': {
1847
+ // One row per file so each change gets the same diff view as an Edit.
1848
+ const changes = Array.isArray(raw.changes) ? raw.changes : [];
1849
+ return changes.map((change, index) => createNormalizedMessage({
1850
+ id: `${itemId}_${index}`,
1851
+ sessionId,
1852
+ timestamp: ts,
1853
+ provider: PROVIDER,
1854
+ kind: 'tool_use',
1855
+ toolName: change?.kind === 'add' ? 'Write' : 'Edit',
1856
+ toolInput: { file_path: change?.path, old_string: '', new_string: '' },
1857
+ toolId: `${itemId}_${index}`,
1858
+ status: raw.status,
1859
+ }));
1860
+ }
1861
+ case 'mcp_tool_call': {
1862
+ const toolName = raw.server ? `mcp__${String(raw.server)}__${String(raw.tool ?? 'tool')}` : String(raw.tool || 'MCP');
1863
+ const toolUse = createNormalizedMessage({
1864
+ id: itemId,
1865
+ sessionId,
1866
+ timestamp: ts,
1867
+ provider: PROVIDER,
1868
+ kind: 'tool_use',
1869
+ toolName,
1870
+ toolInput: raw.arguments,
1871
+ toolId: itemId,
1872
+ status: raw.status,
1873
+ });
1874
+ if (raw.status === 'in_progress') {
1875
+ return [toolUse];
1876
+ }
1877
+ return [toolUse, createNormalizedMessage({
1878
+ id: `${itemId}_result`,
689
1879
  sessionId,
690
1880
  timestamp: ts,
691
1881
  provider: PROVIDER,
692
- kind: 'tool_use',
693
- toolName: raw.tool || 'MCP',
694
- toolInput: raw.arguments,
695
- toolId: baseId,
696
- server: raw.server,
697
- result: raw.result,
698
- error: raw.error,
699
- status: raw.status,
1882
+ kind: 'tool_result',
1883
+ toolId: itemId,
1884
+ content: raw.error
1885
+ ? String(raw.error?.message ?? raw.error)
1886
+ : JSON.stringify(raw.result ?? ''),
1887
+ isError: Boolean(raw.error) || raw.status === 'failed',
700
1888
  })];
1889
+ }
701
1890
  case 'web_search':
702
1891
  return [createNormalizedMessage({
703
- id: baseId,
1892
+ id: itemId,
704
1893
  sessionId,
705
1894
  timestamp: ts,
706
1895
  provider: PROVIDER,
707
1896
  kind: 'tool_use',
708
1897
  toolName: 'WebSearch',
709
1898
  toolInput: { query: raw.query },
710
- toolId: baseId,
1899
+ toolId: itemId,
711
1900
  })];
712
1901
  case 'todo_list':
1902
+ // Codex's live plan uses `{text, completed}`; Claude's TodoWrite uses
1903
+ // `{content, status}`. Normalizing here means one renderer, not two.
713
1904
  return [createNormalizedMessage({
714
- id: baseId,
1905
+ id: itemId,
715
1906
  sessionId,
716
1907
  timestamp: ts,
717
1908
  provider: PROVIDER,
718
1909
  kind: 'tool_use',
719
- toolName: 'TodoList',
720
- toolInput: { items: raw.items },
721
- toolId: baseId,
1910
+ toolName: 'TodoWrite',
1911
+ toolInput: {
1912
+ todos: (Array.isArray(raw.items) ? raw.items : []).map((item) => ({
1913
+ content: String(item?.text ?? ''),
1914
+ status: item?.completed ? 'completed' : 'pending',
1915
+ })),
1916
+ },
1917
+ toolId: itemId,
722
1918
  })];
723
1919
  case 'error':
724
1920
  return [createNormalizedMessage({
725
- id: baseId,
1921
+ id: itemId,
726
1922
  sessionId,
727
1923
  timestamp: ts,
728
1924
  provider: PROVIDER,
@@ -731,14 +1927,14 @@ export class CodexSessionsProvider {
731
1927
  })];
732
1928
  default:
733
1929
  return [createNormalizedMessage({
734
- id: baseId,
1930
+ id: itemId,
735
1931
  sessionId,
736
1932
  timestamp: ts,
737
1933
  provider: PROVIDER,
738
1934
  kind: 'tool_use',
739
1935
  toolName: raw.itemType || 'Unknown',
740
1936
  toolInput: raw.item || raw,
741
- toolId: baseId,
1937
+ toolId: itemId,
742
1938
  })];
743
1939
  }
744
1940
  }
@@ -764,26 +1960,22 @@ export class CodexSessionsProvider {
764
1960
  return [];
765
1961
  }
766
1962
  /**
767
- * Loads Codex JSONL history and keeps token usage metadata when projects.js
768
- * provides it.
1963
+ * Loads Codex JSONL history and keeps token usage metadata when the
1964
+ * transcript reported it.
769
1965
  */
770
1966
  async fetchHistory(sessionId, options = {}) {
771
1967
  const { limit = null, offset = 0 } = options;
772
1968
  let result;
773
1969
  try {
774
- // Load full history first so `total` reflects frontend-normalized messages,
775
- // not raw JSONL records.
776
- result = await getCodexSessionMessages(sessionId, null, 0);
1970
+ result = await getCodexSessionMessages(sessionId);
777
1971
  }
778
1972
  catch (error) {
779
1973
  const message = error instanceof Error ? error.message : String(error);
780
1974
  console.warn(`[CodexProvider] Failed to load session ${sessionId}:`, message);
781
1975
  return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
782
1976
  }
783
- const rawMessages = Array.isArray(result) ? result : (result.messages || []);
784
- const tokenUsage = Array.isArray(result) ? undefined : result.tokenUsage;
785
1977
  const normalized = [];
786
- for (const raw of rawMessages) {
1978
+ for (const raw of result.messages) {
787
1979
  normalized.push(...this.normalizeHistoryEntry(raw, sessionId));
788
1980
  }
789
1981
  const toolResultMap = new Map();
@@ -800,22 +1992,20 @@ export class CodexSessionsProvider {
800
1992
  }
801
1993
  }
802
1994
  }
803
- let total = 0;
804
- for (const msg of normalized) {
805
- if (msg.kind !== 'tool_result') {
806
- total += 1;
807
- }
808
- }
1995
+ // Everything the transcript draws, and nothing else — so a page of N rows
1996
+ // is N rows the user sees, and `total` counts the same thing.
1997
+ const transcript = prepareTranscriptMessages(normalized);
1998
+ const total = transcript.length;
809
1999
  const normalizedOffset = Math.max(0, offset);
810
2000
  const normalizedLimit = limit === null ? null : Math.max(0, limit);
811
- const { page, hasMore } = sliceTailPage(normalized, normalizedLimit, normalizedOffset);
2001
+ const { page, hasMore } = sliceTailPage(transcript, normalizedLimit, normalizedOffset);
812
2002
  return {
813
2003
  messages: page,
814
2004
  total,
815
2005
  hasMore,
816
2006
  offset: normalizedOffset,
817
2007
  limit: normalizedLimit,
818
- tokenUsage,
2008
+ tokenUsage: result.tokenUsage,
819
2009
  };
820
2010
  }
821
2011
  }