@hienlh/ppm 0.23.8 → 0.23.10

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 (448) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CLAUDE.md +4 -0
  3. package/assets/skills/ppm/SKILL.md +1 -1
  4. package/assets/skills/ppm/references/cli-reference.md +4 -4
  5. package/assets/skills/ppm/references/http-api.md +1 -1
  6. package/dist/web/assets/accounts-settings-section-BOBJbG7i.js +1 -0
  7. package/dist/web/assets/accounts-settings-section-BOBJbG7i.js.br +0 -0
  8. package/dist/web/assets/accounts-settings-section-BOBJbG7i.js.gz +0 -0
  9. package/dist/web/assets/agent-session-sheet-CjD4hBZV.js +2 -0
  10. package/dist/web/assets/agent-session-sheet-CjD4hBZV.js.br +0 -0
  11. package/dist/web/assets/agent-session-sheet-CjD4hBZV.js.gz +0 -0
  12. package/dist/web/assets/agent-session-sheet-store-BcQbnaV2.js +1 -0
  13. package/dist/web/assets/agent-session-window-content-Drm1HsSn.js +1 -0
  14. package/dist/web/assets/agent-session-window-content-Drm1HsSn.js.br +0 -0
  15. package/dist/web/assets/agent-session-window-content-Drm1HsSn.js.gz +0 -0
  16. package/dist/web/assets/{ai-resource-editor-DfVRMUHS.js → ai-resource-editor-M1Eax2Hc.js} +1 -1
  17. package/dist/web/assets/ai-resource-editor-M1Eax2Hc.js.br +0 -0
  18. package/dist/web/assets/ai-resource-editor-M1Eax2Hc.js.gz +0 -0
  19. package/dist/web/assets/ai-settings-section-B73t9SXm.js +1 -0
  20. package/dist/web/assets/{ai-settings-section-Ezhw4Fef.js → ai-settings-section-H173dm1k.js} +1 -1
  21. package/dist/web/assets/ai-settings-section-H173dm1k.js.br +0 -0
  22. package/dist/web/assets/ai-settings-section-H173dm1k.js.gz +0 -0
  23. package/dist/web/assets/{api-settings-tYKJ5Fqk.js → api-settings-BA_n8cPt.js} +1 -1
  24. package/dist/web/assets/{appearance-settings-section-CPOAp0fY.js → appearance-settings-section-Dqu20pt8.js} +1 -1
  25. package/dist/web/assets/appearance-settings-section-Dqu20pt8.js.br +0 -0
  26. package/dist/web/assets/appearance-settings-section-Dqu20pt8.js.gz +0 -0
  27. package/dist/web/assets/{apps-panel-Cz7_o-M4.js → apps-panel-mkrzY7K0.js} +1 -1
  28. package/dist/web/assets/apps-panel-mkrzY7K0.js.br +0 -0
  29. package/dist/web/assets/apps-panel-mkrzY7K0.js.gz +0 -0
  30. package/dist/web/assets/branch-review-tab-Dw3bNixU.js +1 -0
  31. package/dist/web/assets/branch-review-tab-Dw3bNixU.js.br +0 -0
  32. package/dist/web/assets/branch-review-tab-Dw3bNixU.js.gz +0 -0
  33. package/dist/web/assets/chat-tab-BAbcVFzD.js +15 -0
  34. package/dist/web/assets/chat-tab-BAbcVFzD.js.br +0 -0
  35. package/dist/web/assets/chat-tab-BAbcVFzD.js.gz +0 -0
  36. package/dist/web/assets/chat-tab-BaXGaBEC.js +1 -0
  37. package/dist/web/assets/chat-tab-BaXGaBEC.js.br +4 -0
  38. package/dist/web/assets/chat-tab-BaXGaBEC.js.gz +0 -0
  39. package/dist/web/assets/code-editor-CXLANXnI.js +7 -0
  40. package/dist/web/assets/code-editor-CXLANXnI.js.br +0 -0
  41. package/dist/web/assets/code-editor-CXLANXnI.js.gz +0 -0
  42. package/dist/web/assets/{codex-reset-credit-button-D2hal4bG.js → codex-reset-credit-button-CB8Dfhki.js} +1 -1
  43. package/dist/web/assets/codex-reset-credit-button-CB8Dfhki.js.br +0 -0
  44. package/dist/web/assets/codex-reset-credit-button-CB8Dfhki.js.gz +0 -0
  45. package/dist/web/assets/{conflict-editor-Cj_gUncw.js → conflict-editor-CKsxIw3v.js} +2 -2
  46. package/dist/web/assets/conflict-editor-CKsxIw3v.js.br +0 -0
  47. package/dist/web/assets/conflict-editor-CKsxIw3v.js.gz +0 -0
  48. package/dist/web/assets/{csv-preview-Be3ODNj0.js → csv-preview-U0v_r_-7.js} +1 -1
  49. package/dist/web/assets/csv-preview-U0v_r_-7.js.br +0 -0
  50. package/dist/web/assets/csv-preview-U0v_r_-7.js.gz +0 -0
  51. package/dist/web/assets/{database-viewer-B1b--4y8.js → database-viewer-xQyUcBoL.js} +1 -1
  52. package/dist/web/assets/database-viewer-xQyUcBoL.js.br +0 -0
  53. package/dist/web/assets/database-viewer-xQyUcBoL.js.gz +0 -0
  54. package/dist/web/assets/{design-tab-8wi0IHss.js → design-tab-aaAt2_QE.js} +4 -4
  55. package/dist/web/assets/design-tab-aaAt2_QE.js.br +0 -0
  56. package/dist/web/assets/design-tab-aaAt2_QE.js.gz +0 -0
  57. package/dist/web/assets/{designs-sidebar-panel-D5-1Q8zQ.js → designs-sidebar-panel-CTzO4Thu.js} +1 -1
  58. package/dist/web/assets/designs-sidebar-panel-CTzO4Thu.js.br +0 -0
  59. package/dist/web/assets/designs-sidebar-panel-CTzO4Thu.js.gz +0 -0
  60. package/dist/web/assets/{diff-viewer-Dcxk6UTG.js → diff-viewer-CSE2-OE7.js} +1 -1
  61. package/dist/web/assets/diff-viewer-CSE2-OE7.js.br +0 -0
  62. package/dist/web/assets/diff-viewer-CSE2-OE7.js.gz +0 -0
  63. package/dist/web/assets/{edit-diff-preview-DxDAtYhi.js → edit-diff-preview-gH6pRk4S.js} +1 -1
  64. package/dist/web/assets/edit-diff-preview-gH6pRk4S.js.br +0 -0
  65. package/dist/web/assets/{edit-diff-preview-DxDAtYhi.js.gz → edit-diff-preview-gH6pRk4S.js.gz} +0 -0
  66. package/dist/web/assets/{editor-language-service-BVJQnTSV.js → editor-language-service-CYl04pUy.js} +1 -1
  67. package/dist/web/assets/editor-language-service-CYl04pUy.js.br +0 -0
  68. package/dist/web/assets/editor-language-service-CYl04pUy.js.gz +0 -0
  69. package/dist/web/assets/explorer-body-BBOcvwmV.js +2 -0
  70. package/dist/web/assets/explorer-body-BBOcvwmV.js.br +0 -0
  71. package/dist/web/assets/explorer-body-BBOcvwmV.js.gz +0 -0
  72. package/dist/web/assets/{explorer-window-content-DH5AD3Ng.js → explorer-window-content-juCGcrkc.js} +1 -1
  73. package/dist/web/assets/explorer-window-content-juCGcrkc.js.br +0 -0
  74. package/dist/web/assets/explorer-window-content-juCGcrkc.js.gz +0 -0
  75. package/dist/web/assets/{extension-manager-section-BJr6ccWw.js → extension-manager-section-poMRmwvg.js} +1 -1
  76. package/dist/web/assets/extension-manager-section-poMRmwvg.js.br +0 -0
  77. package/dist/web/assets/extension-manager-section-poMRmwvg.js.gz +0 -0
  78. package/dist/web/assets/{extension-webview-CYNXHd8D.js → extension-webview-0QfzTI6s.js} +1 -1
  79. package/dist/web/assets/extension-webview-0QfzTI6s.js.br +0 -0
  80. package/dist/web/assets/extension-webview-0QfzTI6s.js.gz +0 -0
  81. package/dist/web/assets/{files-settings-section-JX7o4lex.js → files-settings-section-DJptxRgH.js} +1 -1
  82. package/dist/web/assets/files-settings-section-DJptxRgH.js.br +0 -0
  83. package/dist/web/assets/files-settings-section-DJptxRgH.js.gz +0 -0
  84. package/dist/web/assets/{general-settings-section-kF94Mz5q.js → general-settings-section-BFNbdDNv.js} +1 -1
  85. package/dist/web/assets/general-settings-section-BFNbdDNv.js.br +0 -0
  86. package/dist/web/assets/general-settings-section-BFNbdDNv.js.gz +0 -0
  87. package/dist/web/assets/{git-log-panel-Crlp9ofM.js → git-log-panel-SMMIbjSe.js} +1 -1
  88. package/dist/web/assets/git-log-panel-SMMIbjSe.js.br +0 -0
  89. package/dist/web/assets/git-log-panel-SMMIbjSe.js.gz +0 -0
  90. package/dist/web/assets/{glide-data-grid-O6MO5dKU.js → glide-data-grid-DZVmgu0T.js} +3 -3
  91. package/dist/web/assets/glide-data-grid-DZVmgu0T.js.br +0 -0
  92. package/dist/web/assets/glide-data-grid-DZVmgu0T.js.gz +0 -0
  93. package/dist/web/assets/group-chat-tab-d_aaYuK_.js +1 -0
  94. package/dist/web/assets/group-chat-tab-d_aaYuK_.js.br +0 -0
  95. package/dist/web/assets/group-chat-tab-d_aaYuK_.js.gz +0 -0
  96. package/dist/web/assets/{index-DQCw6d-2.js → index-CIscPw4g.js} +6 -6
  97. package/dist/web/assets/index-CIscPw4g.js.br +0 -0
  98. package/dist/web/assets/index-CIscPw4g.js.gz +0 -0
  99. package/dist/web/assets/{jira-watcher-section-DwM7gqHT.js → jira-watcher-section-CsbhnLLn.js} +1 -1
  100. package/dist/web/assets/keybindings-store-BPs0pM0r.js +1 -0
  101. package/dist/web/assets/{keyboard-shortcuts-section-CewWGwiF.js → keyboard-shortcuts-section-CXqc2OtM.js} +1 -1
  102. package/dist/web/assets/keyboard-shortcuts-section-CXqc2OtM.js.br +0 -0
  103. package/dist/web/assets/keyboard-shortcuts-section-CXqc2OtM.js.gz +0 -0
  104. package/dist/web/assets/{language-servers-section-nricdxke.js → language-servers-section-B4FsBVvZ.js} +2 -2
  105. package/dist/web/assets/language-servers-section-B4FsBVvZ.js.br +0 -0
  106. package/dist/web/assets/language-servers-section-B4FsBVvZ.js.gz +0 -0
  107. package/dist/web/assets/{lsp-client-DRMEWb-z.js → lsp-client-MQ4mjDfT.js} +1 -1
  108. package/dist/web/assets/lsp-client-MQ4mjDfT.js.br +0 -0
  109. package/dist/web/assets/lsp-client-MQ4mjDfT.js.gz +0 -0
  110. package/dist/web/assets/markdown-renderer-BvCZZlue.js +1 -0
  111. package/dist/web/assets/{markdown-renderer-Dab9Us6u.js → markdown-renderer-CjPb5pns.js} +3 -3
  112. package/dist/web/assets/markdown-renderer-CjPb5pns.js.br +0 -0
  113. package/dist/web/assets/markdown-renderer-CjPb5pns.js.gz +0 -0
  114. package/dist/web/assets/{mobile-explorer-sheet-DtgdpvJ9.js → mobile-explorer-sheet-0aGhVQtI.js} +1 -1
  115. package/dist/web/assets/mobile-explorer-sheet-0aGhVQtI.js.br +0 -0
  116. package/dist/web/assets/mobile-explorer-sheet-0aGhVQtI.js.gz +0 -0
  117. package/dist/web/assets/{new-design-dialog-Dd5KDB_p.js → new-design-dialog-2oT2oc_F.js} +1 -1
  118. package/dist/web/assets/new-design-dialog-2oT2oc_F.js.br +0 -0
  119. package/dist/web/assets/new-design-dialog-2oT2oc_F.js.gz +0 -0
  120. package/dist/web/assets/notification-store-Dd25PMPQ.js +1 -0
  121. package/dist/web/assets/{panel-store-DrSH3Nhp.js → panel-store-1X6yM3XZ.js} +1 -1
  122. package/dist/web/assets/panel-store-1X6yM3XZ.js.br +0 -0
  123. package/dist/web/assets/panel-store-1X6yM3XZ.js.gz +0 -0
  124. package/dist/web/assets/performance-panel-Ub96qrEH.js +3 -0
  125. package/dist/web/assets/performance-panel-Ub96qrEH.js.br +0 -0
  126. package/dist/web/assets/performance-panel-Ub96qrEH.js.gz +0 -0
  127. package/dist/web/assets/{ppmbot-settings-section-D67u6gPi.js → ppmbot-settings-section-CPMGP66g.js} +1 -1
  128. package/dist/web/assets/ppmbot-settings-section-CPMGP66g.js.br +0 -0
  129. package/dist/web/assets/ppmbot-settings-section-CPMGP66g.js.gz +0 -0
  130. package/dist/web/assets/{problems-panel-DKgIw4uZ.js → problems-panel-CWwzZIP3.js} +1 -1
  131. package/dist/web/assets/problems-panel-CWwzZIP3.js.br +0 -0
  132. package/dist/web/assets/problems-panel-CWwzZIP3.js.gz +0 -0
  133. package/dist/web/assets/process-table-BwPws3zS.js +1 -0
  134. package/dist/web/assets/process-table-BwPws3zS.js.br +0 -0
  135. package/dist/web/assets/process-table-BwPws3zS.js.gz +0 -0
  136. package/dist/web/assets/{proxy-settings-section-CFiYcTBA.js → proxy-settings-section-nS8VbxI_.js} +1 -1
  137. package/dist/web/assets/proxy-settings-section-nS8VbxI_.js.br +0 -0
  138. package/dist/web/assets/proxy-settings-section-nS8VbxI_.js.gz +0 -0
  139. package/dist/web/assets/{query-audit-section-BybgiPsc.js → query-audit-section-Dv5Pxn-w.js} +1 -1
  140. package/dist/web/assets/query-audit-section-Dv5Pxn-w.js.br +0 -0
  141. package/dist/web/assets/query-audit-section-Dv5Pxn-w.js.gz +0 -0
  142. package/dist/web/assets/{remote-desktop-clipboard-notice-Bs3fZSRw.js → remote-desktop-clipboard-notice-DEbdu0hC.js} +1 -1
  143. package/dist/web/assets/remote-desktop-clipboard-notice-DEbdu0hC.js.br +0 -0
  144. package/dist/web/assets/remote-desktop-clipboard-notice-DEbdu0hC.js.gz +0 -0
  145. package/dist/web/assets/remote-desktop-mobile-sheet-BJm-akVY.js +2 -0
  146. package/dist/web/assets/remote-desktop-mobile-sheet-BJm-akVY.js.br +0 -0
  147. package/dist/web/assets/remote-desktop-mobile-sheet-BJm-akVY.js.gz +0 -0
  148. package/dist/web/assets/{remote-desktop-mobile-view-DgGOfPVY.js → remote-desktop-mobile-view-DPdGFD6C.js} +1 -1
  149. package/dist/web/assets/remote-desktop-mobile-view-DPdGFD6C.js.br +0 -0
  150. package/dist/web/assets/remote-desktop-mobile-view-DPdGFD6C.js.gz +0 -0
  151. package/dist/web/assets/{remote-desktop-readiness-gate-DOWryrQx.js → remote-desktop-readiness-gate-DV7YgSpP.js} +1 -1
  152. package/dist/web/assets/remote-desktop-readiness-gate-DV7YgSpP.js.br +0 -0
  153. package/dist/web/assets/remote-desktop-readiness-gate-DV7YgSpP.js.gz +0 -0
  154. package/dist/web/assets/remote-desktop-window-content-BktcWMjD.js +1 -0
  155. package/dist/web/assets/remote-desktop-window-content-BktcWMjD.js.br +0 -0
  156. package/dist/web/assets/remote-desktop-window-content-BktcWMjD.js.gz +0 -0
  157. package/dist/web/assets/{run-in-terminal-Cdn9PaAl.js → run-in-terminal-B_PCeBqG.js} +1 -1
  158. package/dist/web/assets/{schedules-settings-section-DGhG-b5O.js → schedules-settings-section-B3HYKSVh.js} +1 -1
  159. package/dist/web/assets/schedules-settings-section-B3HYKSVh.js.br +0 -0
  160. package/dist/web/assets/schedules-settings-section-B3HYKSVh.js.gz +0 -0
  161. package/dist/web/assets/{send-to-chat-B09VjsB5.js → send-to-chat-Bjg6c8Ld.js} +1 -1
  162. package/dist/web/assets/send-to-chat-Bjg6c8Ld.js.br +0 -0
  163. package/dist/web/assets/send-to-chat-Bjg6c8Ld.js.gz +0 -0
  164. package/dist/web/assets/{services-panel-0klE3O4g.js → services-panel-D6k6AVS4.js} +1 -1
  165. package/dist/web/assets/services-panel-D6k6AVS4.js.br +0 -0
  166. package/dist/web/assets/services-panel-D6k6AVS4.js.gz +0 -0
  167. package/dist/web/assets/{settings-body-CfqpqFfq.js → settings-body-Cu0mNA5x.js} +2 -2
  168. package/dist/web/assets/settings-body-Cu0mNA5x.js.br +0 -0
  169. package/dist/web/assets/settings-body-Cu0mNA5x.js.gz +0 -0
  170. package/dist/web/assets/{settings-store-3jIBB8gx.js → settings-store-DIV25PM_.js} +2 -2
  171. package/dist/web/assets/settings-store-DIV25PM_.js.br +0 -0
  172. package/dist/web/assets/settings-store-DIV25PM_.js.gz +0 -0
  173. package/dist/web/assets/{settings-tab-CHweH5Nh.js → settings-tab-C-A6RNQ3.js} +1 -1
  174. package/dist/web/assets/{settings-window-content-C6Od3qV7.js → settings-window-content-48e5FJAq.js} +1 -1
  175. package/dist/web/assets/{sql-query-editor-ALQ5_X0j.js → sql-query-editor-CgGWYu9K.js} +1 -1
  176. package/dist/web/assets/sql-query-editor-CgGWYu9K.js.br +0 -0
  177. package/dist/web/assets/sql-query-editor-CgGWYu9K.js.gz +0 -0
  178. package/dist/web/assets/{sqlite-viewer-DKTty_jR.js → sqlite-viewer-Co1Oi3dv.js} +1 -1
  179. package/dist/web/assets/sqlite-viewer-Co1Oi3dv.js.br +0 -0
  180. package/dist/web/assets/sqlite-viewer-Co1Oi3dv.js.gz +0 -0
  181. package/dist/web/assets/{system-monitor-body-D6J6lq7-.js → system-monitor-body-DxPKVa-W.js} +2 -2
  182. package/dist/web/assets/system-monitor-body-DxPKVa-W.js.br +0 -0
  183. package/dist/web/assets/system-monitor-body-DxPKVa-W.js.gz +0 -0
  184. package/dist/web/assets/system-monitor-tab-4miOZuQQ.js +1 -0
  185. package/dist/web/assets/{system-monitor-window-content-D1t30f32.js → system-monitor-window-content-B-A7DdML.js} +1 -1
  186. package/dist/web/assets/{tab-host-window-content-DTcx2D0O.js → tab-host-window-content-CChcpg1v.js} +1 -1
  187. package/dist/web/assets/tab-host-window-content-CChcpg1v.js.br +0 -0
  188. package/dist/web/assets/tab-host-window-content-CChcpg1v.js.gz +0 -0
  189. package/dist/web/assets/{tab-store-CUPgQIQz.js → tab-store-D33icgdA.js} +1 -1
  190. package/dist/web/assets/{terminal-tab-DgTfQKBI.js → terminal-tab-BDqlyBLK.js} +2 -2
  191. package/dist/web/assets/terminal-tab-BDqlyBLK.js.br +0 -0
  192. package/dist/web/assets/terminal-tab-BDqlyBLK.js.gz +0 -0
  193. package/dist/web/assets/tool-cards-iJ8tyrLq.js +9 -0
  194. package/dist/web/assets/tool-cards-iJ8tyrLq.js.br +0 -0
  195. package/dist/web/assets/tool-cards-iJ8tyrLq.js.gz +0 -0
  196. package/dist/web/assets/{use-inline-blame-Dv111BKY.js → use-inline-blame-D9js570a.js} +4 -4
  197. package/dist/web/assets/use-inline-blame-D9js570a.js.br +0 -0
  198. package/dist/web/assets/use-inline-blame-D9js570a.js.gz +0 -0
  199. package/dist/web/assets/{use-lsp-CZTFJIXX.js → use-lsp-BYtylZId.js} +1 -1
  200. package/dist/web/assets/use-lsp-BYtylZId.js.br +0 -0
  201. package/dist/web/assets/use-lsp-BYtylZId.js.gz +0 -0
  202. package/dist/web/assets/{use-monaco-theme-DL2dIG5L.js → use-monaco-theme-DJ8y_7IZ.js} +1 -1
  203. package/dist/web/assets/{use-process-kill-D7D9QvJG.js → use-process-kill-BS84S-kD.js} +1 -1
  204. package/dist/web/assets/use-process-kill-BS84S-kD.js.br +0 -0
  205. package/dist/web/assets/use-process-kill-BS84S-kD.js.gz +0 -0
  206. package/dist/web/assets/{use-websocket-ziY-Gz_B.js → use-websocket-Mqj27umG.js} +1 -1
  207. package/dist/web/assets/{video-preview-DzwY3q9W.js → video-preview-BuzUAIma.js} +1 -1
  208. package/dist/web/assets/video-preview-BuzUAIma.js.br +0 -0
  209. package/dist/web/assets/video-preview-BuzUAIma.js.gz +0 -0
  210. package/dist/web/assets/{voice-settings-section-B7zMHsuz.js → voice-settings-section-DnwQjBbw.js} +1 -1
  211. package/dist/web/assets/voice-settings-section-DnwQjBbw.js.br +0 -0
  212. package/dist/web/assets/voice-settings-section-DnwQjBbw.js.gz +0 -0
  213. package/dist/web/assets/{window-pip-registry-dxrBFfkP.js → window-pip-registry-Bl--fK8-.js} +1 -1
  214. package/dist/web/assets/window-store-vo1N70nK.js +1 -0
  215. package/dist/web/assets/window-store-vo1N70nK.js.br +0 -0
  216. package/dist/web/assets/window-store-vo1N70nK.js.gz +0 -0
  217. package/dist/web/index.html +6 -6
  218. package/dist/web/index.html.br +0 -0
  219. package/dist/web/index.html.gz +0 -0
  220. package/dist/web/sw.js +1 -1
  221. package/dist/web/sw.js.br +0 -0
  222. package/dist/web/sw.js.gz +0 -0
  223. package/docs/architecture/ai-chat-and-providers.md +66 -0
  224. package/docs/architecture/workspace-and-ui.md +15 -3
  225. package/docs/design-guidelines.md +6 -1
  226. package/package.json +1 -1
  227. package/src/providers/codex-app-server/codex-history.ts +4 -4
  228. package/src/providers/codex-app-server/codex-provider.ts +1 -1
  229. package/src/providers/codex-app-server/codex-rollout-tail-parser.ts +203 -0
  230. package/src/providers/codex-app-server/codex-subagent-thread.ts +7 -1
  231. package/src/server/index.ts +7 -2
  232. package/src/server/routes/chat.ts +12 -4
  233. package/src/server/ws/global.ts +19 -3
  234. package/src/services/agent-transcript/agent-transcript-activity.ts +72 -0
  235. package/src/services/agent-transcript/agent-transcript-codex-activity.ts +161 -0
  236. package/src/services/agent-transcript/agent-transcript-codex-tail-cache.ts +85 -0
  237. package/src/services/agent-transcript/agent-transcript-envelope.ts +86 -0
  238. package/src/services/agent-transcript/agent-transcript-error-map.ts +33 -0
  239. package/src/services/agent-transcript/agent-transcript-file-tail-feed.ts +180 -0
  240. package/src/services/agent-transcript/agent-transcript-file-tail-state.ts +100 -0
  241. package/src/services/agent-transcript/agent-transcript-fs-io.ts +79 -0
  242. package/src/services/agent-transcript/agent-transcript-hub-clock.ts +37 -0
  243. package/src/services/agent-transcript/agent-transcript-hub-registry.ts +153 -0
  244. package/src/services/agent-transcript/agent-transcript-hub.ts +103 -0
  245. package/src/services/agent-transcript/agent-transcript-index-cache.ts +85 -0
  246. package/src/services/agent-transcript/agent-transcript-session-hub-activity.ts +103 -0
  247. package/src/services/agent-transcript/agent-transcript-session-hub-read.ts +194 -0
  248. package/src/services/agent-transcript/agent-transcript-session-hub-types.ts +81 -0
  249. package/src/services/agent-transcript/agent-transcript-session-hub.ts +196 -0
  250. package/src/services/agent-transcript/agent-transcript-sources.ts +199 -0
  251. package/src/services/agent-transcript/agent-transcript-ws-like.ts +19 -0
  252. package/src/services/agent-transcript/claude-projects-root.ts +21 -0
  253. package/src/services/agent-transcript/session-ownership.ts +152 -0
  254. package/src/services/nested-subagent-spy.ts +3 -3
  255. package/src/services/subagent-transcript-merger.ts +21 -7
  256. package/src/shared/agent-transcript-protocol.ts +158 -0
  257. package/src/types/chat.ts +27 -3
  258. package/src/web/app.tsx +5 -5
  259. package/src/web/components/chat/agent-card-summary.tsx +73 -0
  260. package/src/web/components/chat/agent-session-context.ts +34 -0
  261. package/src/web/components/chat/agent-session-fallback-store.ts +60 -0
  262. package/src/web/components/chat/agent-session-sheet-store.ts +23 -0
  263. package/src/web/components/chat/{team-member-sheet.tsx → agent-session-sheet.tsx} +12 -12
  264. package/src/web/components/chat/agent-session-window-content.tsx +133 -0
  265. package/src/web/components/chat/chat-history-bar.tsx +1 -0
  266. package/src/web/components/chat/chat-tab.tsx +21 -4
  267. package/src/web/components/chat/codex-usage-panel.tsx +26 -8
  268. package/src/web/components/chat/running-agents-bar.tsx +93 -0
  269. package/src/web/components/chat/team-activity-panel.tsx +19 -5
  270. package/src/web/components/chat/tool-cards.tsx +73 -4
  271. package/src/web/components/chat/use-open-agent-session.ts +83 -0
  272. package/src/web/components/floating-window/window-content-registry.ts +4 -5
  273. package/src/web/components/floating-window/window-geometry.ts +18 -0
  274. package/src/web/components/floating-window/window-persistence.ts +5 -4
  275. package/src/web/components/floating-window/window-store-types.ts +1 -1
  276. package/src/web/components/settings/resolve-open-settings-action.ts +1 -1
  277. package/src/web/components/system/resolve-open-system-monitor-action.ts +1 -1
  278. package/src/web/hooks/use-agent-activity.ts +65 -0
  279. package/src/web/hooks/use-agent-session-stream.ts +134 -0
  280. package/src/web/hooks/use-chat.ts +12 -18
  281. package/src/web/hooks/use-global-events.ts +35 -2
  282. package/src/web/lib/agent-session-stream-merge.ts +48 -0
  283. package/src/web/lib/agent-step-summary.ts +331 -0
  284. package/src/web/lib/global-ws-channel.ts +41 -0
  285. package/src/web/lib/running-agent-rows.ts +115 -0
  286. package/dist/web/assets/accounts-settings-section-BNcPvEq1.js +0 -1
  287. package/dist/web/assets/accounts-settings-section-BNcPvEq1.js.br +0 -0
  288. package/dist/web/assets/accounts-settings-section-BNcPvEq1.js.gz +0 -0
  289. package/dist/web/assets/ai-resource-editor-DfVRMUHS.js.br +0 -0
  290. package/dist/web/assets/ai-resource-editor-DfVRMUHS.js.gz +0 -0
  291. package/dist/web/assets/ai-settings-section-B_0-P8jx.js +0 -1
  292. package/dist/web/assets/ai-settings-section-Ezhw4Fef.js.br +0 -0
  293. package/dist/web/assets/ai-settings-section-Ezhw4Fef.js.gz +0 -0
  294. package/dist/web/assets/appearance-settings-section-CPOAp0fY.js.br +0 -0
  295. package/dist/web/assets/appearance-settings-section-CPOAp0fY.js.gz +0 -0
  296. package/dist/web/assets/apps-panel-Cz7_o-M4.js.br +0 -0
  297. package/dist/web/assets/apps-panel-Cz7_o-M4.js.gz +0 -0
  298. package/dist/web/assets/branch-review-tab-BPx3kHrM.js +0 -1
  299. package/dist/web/assets/branch-review-tab-BPx3kHrM.js.br +0 -0
  300. package/dist/web/assets/branch-review-tab-BPx3kHrM.js.gz +0 -0
  301. package/dist/web/assets/chat-tab-BadDM_j8.js +0 -1
  302. package/dist/web/assets/chat-tab-BadDM_j8.js.br +0 -0
  303. package/dist/web/assets/chat-tab-BadDM_j8.js.gz +0 -0
  304. package/dist/web/assets/chat-tab-IqAY4c7p.js +0 -15
  305. package/dist/web/assets/chat-tab-IqAY4c7p.js.br +0 -0
  306. package/dist/web/assets/chat-tab-IqAY4c7p.js.gz +0 -0
  307. package/dist/web/assets/code-editor-Dmitw9QS.js +0 -7
  308. package/dist/web/assets/code-editor-Dmitw9QS.js.br +0 -0
  309. package/dist/web/assets/code-editor-Dmitw9QS.js.gz +0 -0
  310. package/dist/web/assets/codex-reset-credit-button-D2hal4bG.js.br +0 -0
  311. package/dist/web/assets/codex-reset-credit-button-D2hal4bG.js.gz +0 -0
  312. package/dist/web/assets/conflict-editor-Cj_gUncw.js.br +0 -0
  313. package/dist/web/assets/conflict-editor-Cj_gUncw.js.gz +0 -0
  314. package/dist/web/assets/csv-preview-Be3ODNj0.js.br +0 -0
  315. package/dist/web/assets/csv-preview-Be3ODNj0.js.gz +0 -0
  316. package/dist/web/assets/database-viewer-B1b--4y8.js.br +0 -0
  317. package/dist/web/assets/database-viewer-B1b--4y8.js.gz +0 -0
  318. package/dist/web/assets/design-tab-8wi0IHss.js.br +0 -0
  319. package/dist/web/assets/design-tab-8wi0IHss.js.gz +0 -0
  320. package/dist/web/assets/designs-sidebar-panel-D5-1Q8zQ.js.br +0 -0
  321. package/dist/web/assets/designs-sidebar-panel-D5-1Q8zQ.js.gz +0 -0
  322. package/dist/web/assets/diff-viewer-Dcxk6UTG.js.br +0 -0
  323. package/dist/web/assets/diff-viewer-Dcxk6UTG.js.gz +0 -0
  324. package/dist/web/assets/edit-diff-preview-DxDAtYhi.js.br +0 -0
  325. package/dist/web/assets/editor-language-service-BVJQnTSV.js.br +0 -0
  326. package/dist/web/assets/editor-language-service-BVJQnTSV.js.gz +0 -0
  327. package/dist/web/assets/explorer-body-CTkrlrI3.js +0 -2
  328. package/dist/web/assets/explorer-body-CTkrlrI3.js.br +0 -0
  329. package/dist/web/assets/explorer-body-CTkrlrI3.js.gz +0 -0
  330. package/dist/web/assets/explorer-window-content-DH5AD3Ng.js.br +0 -0
  331. package/dist/web/assets/explorer-window-content-DH5AD3Ng.js.gz +0 -0
  332. package/dist/web/assets/extension-manager-section-BJr6ccWw.js.br +0 -0
  333. package/dist/web/assets/extension-manager-section-BJr6ccWw.js.gz +0 -0
  334. package/dist/web/assets/extension-webview-CYNXHd8D.js.br +0 -0
  335. package/dist/web/assets/extension-webview-CYNXHd8D.js.gz +0 -0
  336. package/dist/web/assets/files-settings-section-JX7o4lex.js.br +0 -0
  337. package/dist/web/assets/files-settings-section-JX7o4lex.js.gz +0 -0
  338. package/dist/web/assets/general-settings-section-kF94Mz5q.js.br +0 -0
  339. package/dist/web/assets/general-settings-section-kF94Mz5q.js.gz +0 -0
  340. package/dist/web/assets/git-log-panel-Crlp9ofM.js.br +0 -0
  341. package/dist/web/assets/git-log-panel-Crlp9ofM.js.gz +0 -0
  342. package/dist/web/assets/glide-data-grid-O6MO5dKU.js.br +0 -0
  343. package/dist/web/assets/glide-data-grid-O6MO5dKU.js.gz +0 -0
  344. package/dist/web/assets/group-chat-tab-DGLSGf37.js +0 -1
  345. package/dist/web/assets/group-chat-tab-DGLSGf37.js.br +0 -0
  346. package/dist/web/assets/group-chat-tab-DGLSGf37.js.gz +0 -0
  347. package/dist/web/assets/index-DQCw6d-2.js.br +0 -0
  348. package/dist/web/assets/index-DQCw6d-2.js.gz +0 -0
  349. package/dist/web/assets/keybindings-store-CJtEHFIQ.js +0 -1
  350. package/dist/web/assets/keyboard-shortcuts-section-CewWGwiF.js.br +0 -0
  351. package/dist/web/assets/keyboard-shortcuts-section-CewWGwiF.js.gz +0 -0
  352. package/dist/web/assets/language-servers-section-nricdxke.js.br +0 -0
  353. package/dist/web/assets/language-servers-section-nricdxke.js.gz +0 -0
  354. package/dist/web/assets/lsp-client-DRMEWb-z.js.br +0 -0
  355. package/dist/web/assets/lsp-client-DRMEWb-z.js.gz +0 -0
  356. package/dist/web/assets/markdown-renderer-D5-xliE9.js +0 -1
  357. package/dist/web/assets/markdown-renderer-Dab9Us6u.js.br +0 -0
  358. package/dist/web/assets/markdown-renderer-Dab9Us6u.js.gz +0 -0
  359. package/dist/web/assets/mobile-explorer-sheet-DtgdpvJ9.js.br +0 -0
  360. package/dist/web/assets/mobile-explorer-sheet-DtgdpvJ9.js.gz +0 -0
  361. package/dist/web/assets/new-design-dialog-Dd5KDB_p.js.br +0 -0
  362. package/dist/web/assets/new-design-dialog-Dd5KDB_p.js.gz +0 -0
  363. package/dist/web/assets/notification-store-D2vTyp_Q.js +0 -1
  364. package/dist/web/assets/panel-store-DrSH3Nhp.js.br +0 -0
  365. package/dist/web/assets/panel-store-DrSH3Nhp.js.gz +0 -0
  366. package/dist/web/assets/performance-panel-LMGefOQ2.js +0 -3
  367. package/dist/web/assets/performance-panel-LMGefOQ2.js.br +0 -0
  368. package/dist/web/assets/performance-panel-LMGefOQ2.js.gz +0 -0
  369. package/dist/web/assets/ppmbot-settings-section-D67u6gPi.js.br +0 -0
  370. package/dist/web/assets/ppmbot-settings-section-D67u6gPi.js.gz +0 -0
  371. package/dist/web/assets/problems-panel-DKgIw4uZ.js.br +0 -0
  372. package/dist/web/assets/problems-panel-DKgIw4uZ.js.gz +0 -0
  373. package/dist/web/assets/process-table-BIAgC_jG.js +0 -1
  374. package/dist/web/assets/process-table-BIAgC_jG.js.br +0 -0
  375. package/dist/web/assets/process-table-BIAgC_jG.js.gz +0 -0
  376. package/dist/web/assets/proxy-settings-section-CFiYcTBA.js.br +0 -0
  377. package/dist/web/assets/proxy-settings-section-CFiYcTBA.js.gz +0 -0
  378. package/dist/web/assets/query-audit-section-BybgiPsc.js.br +0 -0
  379. package/dist/web/assets/query-audit-section-BybgiPsc.js.gz +0 -0
  380. package/dist/web/assets/remote-desktop-clipboard-notice-Bs3fZSRw.js.br +0 -0
  381. package/dist/web/assets/remote-desktop-clipboard-notice-Bs3fZSRw.js.gz +0 -0
  382. package/dist/web/assets/remote-desktop-mobile-sheet-DgxTdWMh.js +0 -2
  383. package/dist/web/assets/remote-desktop-mobile-sheet-DgxTdWMh.js.br +0 -0
  384. package/dist/web/assets/remote-desktop-mobile-sheet-DgxTdWMh.js.gz +0 -0
  385. package/dist/web/assets/remote-desktop-mobile-view-DgGOfPVY.js.br +0 -0
  386. package/dist/web/assets/remote-desktop-mobile-view-DgGOfPVY.js.gz +0 -0
  387. package/dist/web/assets/remote-desktop-readiness-gate-DOWryrQx.js.br +0 -0
  388. package/dist/web/assets/remote-desktop-readiness-gate-DOWryrQx.js.gz +0 -0
  389. package/dist/web/assets/remote-desktop-window-content-7t0wQsHx.js +0 -1
  390. package/dist/web/assets/remote-desktop-window-content-7t0wQsHx.js.br +0 -0
  391. package/dist/web/assets/remote-desktop-window-content-7t0wQsHx.js.gz +0 -0
  392. package/dist/web/assets/schedules-settings-section-DGhG-b5O.js.br +0 -0
  393. package/dist/web/assets/schedules-settings-section-DGhG-b5O.js.gz +0 -0
  394. package/dist/web/assets/send-to-chat-B09VjsB5.js.br +0 -0
  395. package/dist/web/assets/send-to-chat-B09VjsB5.js.gz +0 -0
  396. package/dist/web/assets/services-panel-0klE3O4g.js.br +0 -0
  397. package/dist/web/assets/services-panel-0klE3O4g.js.gz +0 -0
  398. package/dist/web/assets/settings-body-CfqpqFfq.js.br +0 -0
  399. package/dist/web/assets/settings-body-CfqpqFfq.js.gz +0 -0
  400. package/dist/web/assets/settings-store-3jIBB8gx.js.br +0 -0
  401. package/dist/web/assets/settings-store-3jIBB8gx.js.gz +0 -0
  402. package/dist/web/assets/sql-query-editor-ALQ5_X0j.js.br +0 -0
  403. package/dist/web/assets/sql-query-editor-ALQ5_X0j.js.gz +0 -0
  404. package/dist/web/assets/sqlite-viewer-DKTty_jR.js.br +0 -0
  405. package/dist/web/assets/sqlite-viewer-DKTty_jR.js.gz +0 -0
  406. package/dist/web/assets/system-monitor-body-D6J6lq7-.js.br +0 -0
  407. package/dist/web/assets/system-monitor-body-D6J6lq7-.js.gz +0 -0
  408. package/dist/web/assets/system-monitor-tab-Di18zNNS.js +0 -1
  409. package/dist/web/assets/tab-host-window-content-DTcx2D0O.js.br +0 -0
  410. package/dist/web/assets/tab-host-window-content-DTcx2D0O.js.gz +0 -0
  411. package/dist/web/assets/team-member-sheet-CFSbPG6n.js +0 -2
  412. package/dist/web/assets/team-member-sheet-CFSbPG6n.js.br +0 -0
  413. package/dist/web/assets/team-member-sheet-CFSbPG6n.js.gz +0 -0
  414. package/dist/web/assets/team-member-window-content-VeI6DGeM.js +0 -1
  415. package/dist/web/assets/team-member-window-content-VeI6DGeM.js.br +0 -0
  416. package/dist/web/assets/team-member-window-content-VeI6DGeM.js.gz +0 -0
  417. package/dist/web/assets/terminal-tab-DgTfQKBI.js.br +0 -0
  418. package/dist/web/assets/terminal-tab-DgTfQKBI.js.gz +0 -0
  419. package/dist/web/assets/tool-cards-_fpYI1hB.js +0 -9
  420. package/dist/web/assets/tool-cards-_fpYI1hB.js.br +0 -0
  421. package/dist/web/assets/tool-cards-_fpYI1hB.js.gz +0 -0
  422. package/dist/web/assets/use-inline-blame-Dv111BKY.js.br +0 -0
  423. package/dist/web/assets/use-inline-blame-Dv111BKY.js.gz +0 -0
  424. package/dist/web/assets/use-lsp-CZTFJIXX.js.br +0 -0
  425. package/dist/web/assets/use-lsp-CZTFJIXX.js.gz +0 -0
  426. package/dist/web/assets/use-open-team-member-ElN3KmCI.js +0 -1
  427. package/dist/web/assets/use-process-kill-D7D9QvJG.js.br +0 -0
  428. package/dist/web/assets/use-process-kill-D7D9QvJG.js.gz +0 -0
  429. package/dist/web/assets/video-preview-DzwY3q9W.js.br +0 -0
  430. package/dist/web/assets/video-preview-DzwY3q9W.js.gz +0 -0
  431. package/dist/web/assets/voice-settings-section-B7zMHsuz.js.br +0 -0
  432. package/dist/web/assets/voice-settings-section-B7zMHsuz.js.gz +0 -0
  433. package/dist/web/assets/window-store-B4cbZ5rr.js +0 -1
  434. package/dist/web/assets/window-store-B4cbZ5rr.js.br +0 -0
  435. package/dist/web/assets/window-store-B4cbZ5rr.js.gz +0 -0
  436. package/src/web/components/chat/team-member-window-content.tsx +0 -66
  437. package/src/web/components/chat/team-working-bar.tsx +0 -62
  438. package/src/web/components/chat/use-open-team-member.ts +0 -41
  439. package/src/web/hooks/use-member-transcript-tail.ts +0 -95
  440. /package/dist/web/assets/{api-settings-BeINN5Dy.js → api-settings-BWgfXl7o.js} +0 -0
  441. /package/dist/web/assets/{api-settings-BeINN5Dy.js.br → api-settings-BWgfXl7o.js.br} +0 -0
  442. /package/dist/web/assets/{api-settings-BeINN5Dy.js.gz → api-settings-BWgfXl7o.js.gz} +0 -0
  443. /package/dist/web/assets/{libesm-CxeQSM3t.js → libesm-DAF5_F99.js} +0 -0
  444. /package/dist/web/assets/{libesm-CxeQSM3t.js.br → libesm-DAF5_F99.js.br} +0 -0
  445. /package/dist/web/assets/{libesm-CxeQSM3t.js.gz → libesm-DAF5_F99.js.gz} +0 -0
  446. /package/dist/web/assets/{onboarding-entry-DD3QSDiz.js → onboarding-entry-Bh-WE3vq.js} +0 -0
  447. /package/dist/web/assets/{onboarding-entry-DD3QSDiz.js.br → onboarding-entry-Bh-WE3vq.js.br} +0 -0
  448. /package/dist/web/assets/{onboarding-entry-DD3QSDiz.js.gz → onboarding-entry-Bh-WE3vq.js.gz} +0 -0
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Transcript-subscription lifecycle for a session hub: subscribe → catch-up
3
+ * → live tick scheduling. The per-tick read-and-push mechanics (exact per-page
4
+ * cursors, dynamic file-set growth) live in `agent-transcript-session-hub-read.ts`.
5
+ */
6
+ import { CODEX_THREAD_ID_RE, resolveCodexDescendantFile, resolveSources } from "./agent-transcript-sources.ts";
7
+ import { statSizeSafe } from "./agent-transcript-fs-io.ts";
8
+ import { agentTranscriptClock, agentTranscriptTimers } from "./agent-transcript-hub-clock.ts";
9
+ import { createFileTailState } from "./agent-transcript-file-tail-state.ts";
10
+ import { isIdle, safePushUpdate } from "./agent-transcript-session-hub-read.ts";
11
+ import { mapSourceErrorCode } from "./agent-transcript-error-map.ts";
12
+ import { sendWsMessage, type AgentTranscriptWsLike } from "./agent-transcript-ws-like.ts";
13
+ import type { SessionHub, TranscriptSubscription } from "./agent-transcript-session-hub-types.ts";
14
+ import {
15
+ IDLE_TICK_MS, LIVE_TICK_MS,
16
+ type AgentTranscriptCursor, type AgentTranscriptErrorCode,
17
+ type AgentTranscriptErrorMsg, type AgentTranscriptSourceKind,
18
+ } from "../../shared/agent-transcript-protocol.ts";
19
+
20
+ /**
21
+ * A client-supplied cursor offset, sanitized: only a safe non-negative
22
+ * integer is trusted, and the result is clamped to the file's current size
23
+ * either way. A fractional, negative, NaN or absurdly large value degrades
24
+ * to "start from the beginning" rather than ever reaching a raw file read —
25
+ * `fs.readSync`'s position argument throws on anything but a real integer.
26
+ */
27
+ function sanitizeOffset(value: unknown, size: number): number {
28
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return 0;
29
+ return Math.min(value, size);
30
+ }
31
+
32
+ export interface SubscribeResult {
33
+ ok: boolean;
34
+ code?: AgentTranscriptErrorCode;
35
+ /** A push failed mid-catch-up (send returned 0 or threw) — caller drops the whole client. */
36
+ sendFailed?: boolean;
37
+ }
38
+
39
+ function ensureTicker(hub: SessionHub): void {
40
+ if (hub.tickHandle !== null) return;
41
+ hub.tickHandle = agentTranscriptTimers.setInterval(() => tickTranscripts(hub), LIVE_TICK_MS);
42
+ }
43
+
44
+ function stopTickerIfEmpty(hub: SessionHub): void {
45
+ if (hub.subscriptions.size > 0 || hub.tickHandle === null) return;
46
+ agentTranscriptTimers.clearInterval(hub.tickHandle);
47
+ hub.tickHandle = null;
48
+ }
49
+
50
+ /**
51
+ * Codex only: a cursor can name descendant thread ids the server derived for
52
+ * this same card in an EARLIER subscription — added live via a tail parser's
53
+ * own `links` after the initial subscribe — that a fresh `resolveSources`
54
+ * call has no way to rediscover on its own (it only ever returns the card's
55
+ * own thread). Each such key is re-validated through the exact same
56
+ * fail-closed descendant chain a directly-requested card goes through; a
57
+ * forged or unrelated key is simply ignored, never trusted just because the
58
+ * client echoed it back.
59
+ */
60
+ function seedCodexDescendantsFromCursor(
61
+ hub: SessionHub,
62
+ sub: TranscriptSubscription,
63
+ source: AgentTranscriptSourceKind,
64
+ cursor: AgentTranscriptCursor | undefined,
65
+ now: number,
66
+ ): void {
67
+ if (hub.owned.providerId !== "codex" || source.kind !== "card" || !cursor) return;
68
+ for (const key of Object.keys(cursor)) {
69
+ if (sub.files.has(key) || !CODEX_THREAD_ID_RE.test(key)) continue;
70
+ const ref = resolveCodexDescendantFile(hub.owned, key);
71
+ if (!ref) continue;
72
+ const size = statSizeSafe(ref.path) ?? 0;
73
+ const start = sanitizeOffset(cursor[key], size);
74
+ sub.files.set(key, createFileTailState(ref, start, now));
75
+ }
76
+ }
77
+
78
+ /** How long until the next tick: the live cadence while idle-but-still-catching-up
79
+ * on a read-budget-truncated backlog, else the usual idle/live split. */
80
+ function nextCadence(sub: TranscriptSubscription, now: number, hasMoreBacklog: boolean): number {
81
+ return now + (hasMoreBacklog || !isIdle(sub, now) ? LIVE_TICK_MS : IDLE_TICK_MS);
82
+ }
83
+
84
+ export function addTranscriptSubscription(
85
+ hub: SessionHub,
86
+ ws: AgentTranscriptWsLike,
87
+ subId: string,
88
+ source: AgentTranscriptSourceKind,
89
+ cursor: AgentTranscriptCursor | undefined,
90
+ ): SubscribeResult {
91
+ const resolved = resolveSources(hub.owned, source);
92
+ if (!Array.isArray(resolved)) return { ok: false, code: mapSourceErrorCode(resolved.code) };
93
+
94
+ const now = agentTranscriptClock.now();
95
+ const sub: TranscriptSubscription = { subId, source, files: new Map(), nextTickAt: now + LIVE_TICK_MS };
96
+ for (const ref of resolved) {
97
+ const size = statSizeSafe(ref.path) ?? 0;
98
+ const start = sanitizeOffset(cursor?.[ref.key], size);
99
+ sub.files.set(ref.key, createFileTailState(ref, start, now));
100
+ }
101
+ seedCodexDescendantsFromCursor(hub, sub, source, cursor, now);
102
+
103
+ // Registered in the hub BEFORE the first push, so a throw from that push
104
+ // (caught below and treated as a failed send) still leaves the subscription
105
+ // reachable for cleanup — never an entry the registry never learns about
106
+ // and no unsubscribe or disconnect path can then find.
107
+ let bySubId = hub.subscriptions.get(ws);
108
+ if (!bySubId) {
109
+ bySubId = new Map();
110
+ hub.subscriptions.set(ws, bySubId);
111
+ }
112
+ bySubId.set(subId, sub);
113
+ ensureTicker(hub);
114
+
115
+ const result = safePushUpdate(hub, ws, sub, now, true);
116
+ if (result.outcome === "error") {
117
+ // The subscription never reached the top-level registry (that happens
118
+ // only once this function returns `ok: true`), so undoing the hub-local
119
+ // registration here is enough — nothing else has learned about it yet.
120
+ bySubId.delete(subId);
121
+ stopTickerIfEmpty(hub);
122
+ console.warn(`[agent-transcript] hub=${hub.key} subId=${subId} subscribe-time read failed`);
123
+ return { ok: false, code: "bad_request" };
124
+ }
125
+ sub.nextTickAt = nextCadence(sub, now, result.hasMoreBacklog);
126
+ return { ok: true, sendFailed: result.outcome === "send-failed" };
127
+ }
128
+
129
+ /** Remove one subscription. Returns whether it existed. */
130
+ export function removeTranscriptSubscription(hub: SessionHub, ws: AgentTranscriptWsLike, subId: string): boolean {
131
+ const bySubId = hub.subscriptions.get(ws);
132
+ if (!bySubId?.delete(subId)) return false;
133
+ if (bySubId.size === 0) hub.subscriptions.delete(ws);
134
+ stopTickerIfEmpty(hub);
135
+ return true;
136
+ }
137
+
138
+ /** Drop every transcript subscription a socket holds on this hub. Returns how many existed. */
139
+ export function dropAllTranscriptForWs(hub: SessionHub, ws: AgentTranscriptWsLike): number {
140
+ const bySubId = hub.subscriptions.get(ws);
141
+ if (!bySubId) return 0;
142
+ hub.subscriptions.delete(ws);
143
+ stopTickerIfEmpty(hub);
144
+ return bySubId.size;
145
+ }
146
+
147
+ export function transcriptSubIdsForWs(hub: SessionHub, ws: AgentTranscriptWsLike): string[] {
148
+ return [...(hub.subscriptions.get(ws)?.keys() ?? [])];
149
+ }
150
+
151
+ function sendSubscriptionError(ws: AgentTranscriptWsLike, subId: string): void {
152
+ sendWsMessage(ws, { type: "agent-transcript:error", subId, code: "bad_request" } satisfies AgentTranscriptErrorMsg);
153
+ }
154
+
155
+ /**
156
+ * One 250ms wake: push only the subscriptions whose own cadence is due (live
157
+ * or idle-slowed).
158
+ *
159
+ * A thrown read/parse error is scoped to the ONE subscription that threw —
160
+ * logged once, reported to the client as a normal `agent-transcript:error`,
161
+ * and dropped — never treated like a dead socket. `erroredSubs` mirrors
162
+ * `failedClients`'s defer-to-after-the-loop pattern: `onSubscriptionError`
163
+ * mutates `hub.subscriptions` (via the registry's `unregisterTranscriptSub`),
164
+ * which must not happen while this function is still iterating that same map.
165
+ */
166
+ export function tickTranscripts(hub: SessionHub): void {
167
+ const now = agentTranscriptClock.now();
168
+ const failedClients = new Set<AgentTranscriptWsLike>();
169
+ const erroredSubs: { ws: AgentTranscriptWsLike; subId: string }[] = [];
170
+
171
+ for (const [ws, bySubId] of hub.subscriptions) {
172
+ if (failedClients.has(ws)) continue;
173
+ if (!hub.tokenStillValid(ws)) {
174
+ failedClients.add(ws);
175
+ continue;
176
+ }
177
+ for (const [subId, sub] of bySubId) {
178
+ if (now < sub.nextTickAt) continue;
179
+ const result = safePushUpdate(hub, ws, sub, now, false);
180
+ if (result.outcome === "send-failed") {
181
+ failedClients.add(ws);
182
+ break;
183
+ }
184
+ if (result.outcome === "error") {
185
+ console.warn(`[agent-transcript] hub=${hub.key} subId=${subId} tick failed, dropping this subscription`);
186
+ sendSubscriptionError(ws, subId);
187
+ erroredSubs.push({ ws, subId });
188
+ continue;
189
+ }
190
+ sub.nextTickAt = nextCadence(sub, now, result.hasMoreBacklog);
191
+ }
192
+ }
193
+
194
+ for (const ws of failedClients) hub.onSendFailure(ws);
195
+ for (const { ws, subId } of erroredSubs) hub.onSubscriptionError(ws, subId);
196
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Given an already-owned session (see session-ownership.ts) and a requested
3
+ * card or teammate, return the exact transcript file(s) the server may read —
4
+ * never a client-supplied path.
5
+ *
6
+ * Claude card: `groupSubagentsByCard` keyed by the card's `toolUseId`, root
7
+ * agent first then its nested descendants (already depth-ordered).
8
+ * Codex card: the named thread's own rollout, accepted only when its
9
+ * `session_meta.parent_thread_id` chain reaches the owning session — a
10
+ * grandchild discovered later via the tail parser's own `links` is re-checked
11
+ * through the exact same chain walk (`resolveCodexDescendantFile`) before the
12
+ * hub ever starts tailing it.
13
+ * Member: a teammate's newest transcript, `teamName` pinned to the project the
14
+ * same way a Claude session id is (a team IS a session, see
15
+ * `resolveTeamSubagentsDir`).
16
+ *
17
+ * Every returned path is realpath-contained under the Claude root or a Codex
18
+ * sessions dir. Results — hits and misses alike — are cached for 2s per
19
+ * (session, card/member) so a polling subscriber costs one dir scan per tick
20
+ * at most, not one per subscriber.
21
+ */
22
+
23
+ import { join } from "node:path";
24
+ import { getCachedSubagentGroups } from "./agent-transcript-index-cache.ts";
25
+ import { resolveMemberTranscript } from "../team-member-activity/member-activity.service.ts";
26
+ import { findRolloutByThreadId, isCodexRolloutPath, readSessionMeta } from "../../providers/codex-app-server/codex-history.ts";
27
+ import { claudeProjectsRoot } from "./claude-projects-root.ts";
28
+ import { SESSION_ID_RE, pinClaudeSessionDir, realpathContained, type OwnedSession } from "./session-ownership.ts";
29
+
30
+ export type AgentTranscriptSource =
31
+ | { kind: "card"; cardId: string }
32
+ | { kind: "member"; teamName: string; memberName: string };
33
+
34
+ export interface TranscriptFileRef {
35
+ key: string;
36
+ path: string;
37
+ provider: "claude" | "codex";
38
+ }
39
+
40
+ export type SourceErrorCode =
41
+ | "invalid_card_id"
42
+ | "card_not_found"
43
+ | "invalid_team_name"
44
+ | "invalid_member_name"
45
+ | "member_not_found"
46
+ | "not_descendant";
47
+
48
+ export interface SourceError {
49
+ ok: false;
50
+ code: SourceErrorCode;
51
+ }
52
+
53
+ const CLAUDE_CARD_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
54
+ const CODEX_CARD_ID_RE = /^subagent-([0-9a-f-]{36})$/;
55
+ const MEMBER_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
56
+ /** Shape of a raw Codex thread id (a `TranscriptFileRef.key`, not a `subagent-<id>`
57
+ * CARD id) — reused by the hub to recognize a descendant key in a cursor. */
58
+ export const CODEX_THREAD_ID_RE = /^[0-9a-f-]{36}$/i;
59
+
60
+ /** How far up a chain of spawned Codex threads a card id is chased before giving up. */
61
+ const MAX_CHAIN_DEPTH = 8;
62
+
63
+ const CACHE_TTL_MS = 2000;
64
+ /** Bounds a client cycling through novel card ids (subscribe, unsubscribe, repeat)
65
+ * from growing this map forever — expired entries are swept first, and only a
66
+ * still-live entry is evicted (oldest insertion) if that alone isn't enough. */
67
+ const MAX_CACHE_ENTRIES = 500;
68
+ interface CacheEntry {
69
+ expiresAt: number;
70
+ result: TranscriptFileRef[] | SourceError;
71
+ }
72
+ const cache = new Map<string, CacheEntry>();
73
+
74
+ function evictForCapacity(now: number): void {
75
+ if (cache.size < MAX_CACHE_ENTRIES) return;
76
+ for (const [key, entry] of cache) {
77
+ if (entry.expiresAt <= now) cache.delete(key);
78
+ }
79
+ while (cache.size >= MAX_CACHE_ENTRIES) {
80
+ const oldest = cache.keys().next().value;
81
+ if (oldest === undefined) break;
82
+ cache.delete(oldest);
83
+ }
84
+ }
85
+
86
+ function err(code: SourceErrorCode): SourceError {
87
+ return { ok: false, code };
88
+ }
89
+
90
+ function sourceCacheKey(sessionId: string, source: AgentTranscriptSource): string {
91
+ return source.kind === "card"
92
+ ? `${sessionId}\0card\0${source.cardId}`
93
+ : `${sessionId}\0member\0${source.teamName}\0${source.memberName}`;
94
+ }
95
+
96
+ function resolveClaudeCard(owned: OwnedSession, cardId: string): TranscriptFileRef[] | SourceError {
97
+ if (!CLAUDE_CARD_ID_RE.test(cardId)) return err("invalid_card_id");
98
+ const subagentsDir = join(owned.claude!.sessionDir, "subagents");
99
+ const group = getCachedSubagentGroups(subagentsDir).get(cardId);
100
+ if (!group) return err("card_not_found");
101
+ const root = claudeProjectsRoot();
102
+ const files: TranscriptFileRef[] = [];
103
+ for (const entry of group) {
104
+ const real = realpathContained(entry.transcriptPath, root);
105
+ if (real) files.push({ key: entry.agentId, path: real, provider: "claude" });
106
+ }
107
+ return files;
108
+ }
109
+
110
+ /** First rollout matching `threadId`, searched filename-first across every dir the owning session was found in. */
111
+ function findCodexFile(dirs: string[], threadId: string, projectPath: string): string | null {
112
+ for (const dir of dirs) {
113
+ const file = findRolloutByThreadId(dir, threadId, projectPath);
114
+ if (file && isCodexRolloutPath(file)) return file;
115
+ }
116
+ return null;
117
+ }
118
+
119
+ /** Walk `session_meta.parent_thread_id` from `threadId` up to `sessionId`. One header read per hop. */
120
+ export function isCodexDescendant(dirs: string[], threadId: string, sessionId: string, projectPath: string): boolean {
121
+ let cur = threadId;
122
+ const seen = new Set<string>();
123
+ for (let depth = 0; depth < MAX_CHAIN_DEPTH; depth++) {
124
+ if (seen.has(cur)) return false; // cyclic session_meta — corrupt, refuse
125
+ seen.add(cur);
126
+ const file = findCodexFile(dirs, cur, projectPath);
127
+ if (!file) return false;
128
+ const parent = readSessionMeta(file)?.parentThreadId;
129
+ if (!parent) return false;
130
+ if (parent === sessionId) return true;
131
+ cur = parent;
132
+ }
133
+ return false;
134
+ }
135
+
136
+ function resolveCodexCard(owned: OwnedSession, cardId: string): TranscriptFileRef[] | SourceError {
137
+ const m = CODEX_CARD_ID_RE.exec(cardId);
138
+ if (!m) return err("invalid_card_id");
139
+ const threadId = m[1]!;
140
+ const ref = resolveCodexDescendantFile(owned, threadId);
141
+ if (!ref) {
142
+ // Distinguish "no such rollout" from "found but not a descendant" the same
143
+ // way the pre-refactor inline checks did, so the wire error code is unchanged.
144
+ const file = findCodexFile(owned.codex!.dirs, threadId, owned.projectPath);
145
+ return file ? err("not_descendant") : err("card_not_found");
146
+ }
147
+ return [ref];
148
+ }
149
+
150
+ /**
151
+ * A Codex thread id → its transcript file, but only once the same fail-closed
152
+ * chain walk used for a directly-requested card also accepts it. The hub's
153
+ * live-discovery path (a tail parser's own `links`, surfacing a grandchild
154
+ * spawned after a subscription started) reuses this rather than re-deriving
155
+ * the check, so a nested thread can never bypass the descendant guard just
156
+ * because it arrived through a different door.
157
+ */
158
+ export function resolveCodexDescendantFile(owned: OwnedSession, threadId: string): TranscriptFileRef | null {
159
+ const { dirs } = owned.codex!;
160
+ const file = findCodexFile(dirs, threadId, owned.projectPath);
161
+ if (!file) return null;
162
+ if (!isCodexDescendant(dirs, threadId, owned.sessionId, owned.projectPath)) return null;
163
+ return { key: threadId, path: file, provider: "codex" };
164
+ }
165
+
166
+ function resolveMember(owned: OwnedSession, teamName: string, memberName: string): TranscriptFileRef[] | SourceError {
167
+ if (!SESSION_ID_RE.test(teamName)) return err("invalid_team_name");
168
+ if (!MEMBER_NAME_RE.test(memberName)) return err("invalid_member_name");
169
+ // A team is itself a session (resolveTeamSubagentsDir → resolveSessionDir): pin it to the
170
+ // project the same way the primary session id is, before trusting anything it names.
171
+ if (!pinClaudeSessionDir(teamName, owned.projectPath)) return err("member_not_found");
172
+ const path = resolveMemberTranscript(teamName, memberName, owned.projectPath);
173
+ if (!path) return err("member_not_found");
174
+ const real = realpathContained(path, claudeProjectsRoot());
175
+ if (!real) return err("member_not_found");
176
+ return [{ key: memberName, path: real, provider: "claude" }];
177
+ }
178
+
179
+ function computeSources(owned: OwnedSession, source: AgentTranscriptSource): TranscriptFileRef[] | SourceError {
180
+ if (source.kind === "member") return resolveMember(owned, source.teamName, source.memberName);
181
+ return owned.providerId === "claude" ? resolveClaudeCard(owned, source.cardId) : resolveCodexCard(owned, source.cardId);
182
+ }
183
+
184
+ export function resolveSources(owned: OwnedSession, source: AgentTranscriptSource): TranscriptFileRef[] | SourceError {
185
+ const key = sourceCacheKey(owned.sessionId, source);
186
+ const now = Date.now();
187
+ const hit = cache.get(key);
188
+ if (hit && hit.expiresAt > now) return hit.result;
189
+ evictForCapacity(now);
190
+ const result = computeSources(owned, source);
191
+ cache.set(key, { expiresAt: now + CACHE_TTL_MS, result });
192
+ return result;
193
+ }
194
+
195
+ /** Test-only: clear the 2s resolve cache between cases. */
196
+ export function _resetSourcesCache(): void { cache.clear(); }
197
+
198
+ /** Test-only: current cache size, to assert the capacity bound holds. */
199
+ export function _sourcesCacheSizeForTest(): number { return cache.size; }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The slice of a `/ws/global` socket the hub needs. `send` returns a number on
3
+ * a real Bun `ServerWebSocket` (-1 backpressure, 0 dropped, >0 bytes sent) —
4
+ * the hub checks it after every push and drops the socket's subscriptions on
5
+ * 0 or a throw, never on -1 (that just means try again later).
6
+ */
7
+ export interface AgentTranscriptWsLike {
8
+ send(data: string): number;
9
+ data?: { token?: string | null };
10
+ }
11
+
12
+ /** Send one JSON message; false on a dropped send (0) or a throw — never on backpressure (-1). */
13
+ export function sendWsMessage(ws: AgentTranscriptWsLike, payload: unknown): boolean {
14
+ try {
15
+ return ws.send(JSON.stringify(payload)) !== 0;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Where Claude Code writes per-project session transcripts
3
+ * (`<root>/<slug(projectPath)>/<sessionId>.jsonl`).
4
+ *
5
+ * Real home by default; overridable so session-ownership and transcript-source
6
+ * tests never read or write the user's actual `~/.claude/projects`.
7
+ */
8
+
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+
12
+ let override: string | null = null;
13
+
14
+ export function claudeProjectsRoot(): string {
15
+ return override ?? join(homedir(), ".claude", "projects");
16
+ }
17
+
18
+ /** Test-only: point claudeProjectsRoot() at a scratch directory. Pass null to reset. */
19
+ export function _setClaudeProjectsRoot(root: string | null): void {
20
+ override = root;
21
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Prove a session belongs to the requesting project before any transcript file
3
+ * is named. Everything an Agent card window reads is derived from this result
4
+ * — never from a client-supplied path.
5
+ *
6
+ * Claude: the session's own JSONL must sit under `<claudeRoot>/<slug(projectPath)>/`.
7
+ * A session started outside PPM (no DB row) is accepted on that basis alone; the
8
+ * cross-project fallback (DB `project_path`, drive-letter/case drift) is used
9
+ * only when it independently names the SAME project the caller asked for.
10
+ *
11
+ * Codex: the root rollout must be found by `findRolloutByThreadId`, which is
12
+ * itself fail-closed on `cwd` — a rollout whose recorded cwd does not match
13
+ * `projectPath` is never returned. `dirsFn` is injectable so tests never touch
14
+ * the real `~/.codex`.
15
+ */
16
+
17
+ import { existsSync, realpathSync } from "node:fs";
18
+ import { dirname, join, resolve, sep } from "node:path";
19
+ import { getSessionProjectPath } from "../db.service.ts";
20
+ import { codexSessionsDirs as realCodexSessionsDirs } from "../../providers/codex-app-server/codex-provider.ts";
21
+ import { findRolloutByThreadId, isCodexRolloutPath } from "../../providers/codex-app-server/codex-history.ts";
22
+ import { claudeProjectsRoot } from "./claude-projects-root.ts";
23
+
24
+ export const SESSION_ID_RE = /^[A-Za-z0-9_-]{8,64}$/;
25
+
26
+ export type OwnershipErrorCode =
27
+ | "invalid_session_id"
28
+ | "invalid_provider"
29
+ | "invalid_project"
30
+ | "session_not_found";
31
+
32
+ export interface OwnershipError {
33
+ ok: false;
34
+ code: OwnershipErrorCode;
35
+ }
36
+
37
+ export interface OwnedSession {
38
+ ok: true;
39
+ providerId: "claude" | "codex";
40
+ sessionId: string;
41
+ projectPath: string;
42
+ /** Claude only: the session's own directory, `<claudeRoot>/<slug>/<sessionId>`. */
43
+ claude?: { sessionDir: string };
44
+ /** Codex only: where the root rollout was found, plus every dir it was searched in
45
+ * (reused by agent-transcript-sources so a descendant lookup need not re-derive it). */
46
+ codex?: { path: string; sessionsDir: string; dirs: string[] };
47
+ }
48
+
49
+ export interface AssertSessionParams {
50
+ providerId: string;
51
+ sessionId: string;
52
+ projectPath: string;
53
+ /** Test-only: override which directories Codex rollouts are searched under. */
54
+ codexSessionsDirs?: (sessionId?: string) => string[];
55
+ }
56
+
57
+ /**
58
+ * Test-only fallback for callers that cannot pass `codexSessionsDirs` per
59
+ * call (the `/ws/global` hub never accepts a client-supplied directory list,
60
+ * by design) but still need Codex fixtures to live outside the real
61
+ * `~/.codex`. `codex-provider.ts`'s real `codexSessionsDirs` freezes
62
+ * `homedir()` into a module-level constant at import time, so patching
63
+ * `HOME`/`USERPROFILE` in a `beforeEach` has no effect on it once any test in
64
+ * the same process has already imported that module.
65
+ */
66
+ let testCodexSessionsDirsOverride: ((sessionId?: string) => string[]) | null = null;
67
+
68
+ export function _setCodexSessionsDirsForTest(fn: ((sessionId?: string) => string[]) | null): void {
69
+ testCodexSessionsDirsOverride = fn;
70
+ }
71
+
72
+ /** Absolute path normalized for cross-platform / case-insensitive (win32) comparison. */
73
+ function normalizeProjectPath(p: string): string {
74
+ const r = resolve(p);
75
+ return process.platform === "win32" ? r.toLowerCase() : r;
76
+ }
77
+
78
+ /** Same encoding `resolveSessionDir` (subagent-transcript-merger.ts) uses. */
79
+ function encodeClaudeSlug(projectPath: string): string {
80
+ return projectPath.replace(/[/\\:.]/g, "-");
81
+ }
82
+
83
+ function isWithin(child: string, root: string): boolean {
84
+ const c = process.platform === "win32" ? child.toLowerCase() : child;
85
+ const r = process.platform === "win32" ? root.toLowerCase() : root;
86
+ return c === r || c.startsWith(r + sep);
87
+ }
88
+
89
+ /** Realpath `path` and confirm it stays under `root`'s realpath. Null on any failure. */
90
+ export function realpathContained(path: string, root: string): string | null {
91
+ try {
92
+ const real = realpathSync(path);
93
+ const realRoot = realpathSync(root);
94
+ return isWithin(real, realRoot) ? real : null;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Pin a Claude session id to a project: its JSONL must sit directly under
102
+ * `<claudeRoot>/<slug(projectPath)>/`, or — only when the DB independently
103
+ * names the SAME project (drive-letter/case drift) — under the slug the DB
104
+ * recorded. Returns the session's own directory (sibling of the JSONL),
105
+ * realpath-contained under `claudeRoot`. Shared by the primary-session check
106
+ * and the `member` source's `teamName` (a team is itself a session id).
107
+ */
108
+ export function pinClaudeSessionDir(sessionId: string, projectPath: string): string | null {
109
+ const root = claudeProjectsRoot();
110
+ let jsonlFile = join(root, encodeClaudeSlug(projectPath), `${sessionId}.jsonl`);
111
+ if (!existsSync(jsonlFile)) {
112
+ const recorded = getSessionProjectPath(sessionId);
113
+ if (!recorded || normalizeProjectPath(recorded) !== normalizeProjectPath(projectPath)) return null;
114
+ jsonlFile = join(root, encodeClaudeSlug(recorded), `${sessionId}.jsonl`);
115
+ if (!existsSync(jsonlFile)) return null;
116
+ }
117
+ const real = realpathContained(jsonlFile, root);
118
+ if (!real) return null;
119
+ // sessionId is regex-validated (no separators, no "..") so this join cannot escape the slug dir.
120
+ return join(dirname(real), sessionId);
121
+ }
122
+
123
+ function assertClaudeSession(sessionId: string, projectPath: string): OwnedSession | OwnershipError {
124
+ const sessionDir = pinClaudeSessionDir(sessionId, projectPath);
125
+ if (!sessionDir) return { ok: false, code: "session_not_found" };
126
+ return { ok: true, providerId: "claude", sessionId, projectPath, claude: { sessionDir } };
127
+ }
128
+
129
+ function assertCodexSession(
130
+ sessionId: string,
131
+ projectPath: string,
132
+ dirsFn: (sessionId?: string) => string[],
133
+ ): OwnedSession | OwnershipError {
134
+ const dirs = dirsFn(sessionId);
135
+ for (const dir of dirs) {
136
+ const file = findRolloutByThreadId(dir, sessionId, projectPath);
137
+ if (!file || !isCodexRolloutPath(file)) continue;
138
+ return { ok: true, providerId: "codex", sessionId, projectPath, codex: { path: file, sessionsDir: dir, dirs } };
139
+ }
140
+ return { ok: false, code: "session_not_found" };
141
+ }
142
+
143
+ export function assertSessionInProject(params: AssertSessionParams): OwnedSession | OwnershipError {
144
+ const { providerId, sessionId, projectPath } = params;
145
+ if (!SESSION_ID_RE.test(sessionId)) return { ok: false, code: "invalid_session_id" };
146
+ if (!projectPath) return { ok: false, code: "invalid_project" };
147
+ if (providerId === "claude") return assertClaudeSession(sessionId, projectPath);
148
+ if (providerId === "codex") {
149
+ return assertCodexSession(sessionId, projectPath, params.codexSessionsDirs ?? testCodexSessionsDirsOverride ?? realCodexSessionsDirs);
150
+ }
151
+ return { ok: false, code: "invalid_provider" };
152
+ }
@@ -28,7 +28,7 @@ import { closeSync, openSync, readSync, statSync } from "node:fs";
28
28
  import { join } from "node:path";
29
29
  import type { ChatEvent } from "../types/chat.ts";
30
30
  import { createAgentTranscriptLineParser } from "./subagent-transcript-merger.ts";
31
- import { groupSubagentsByCard } from "./team-member-activity/subagent-transcript-index.ts";
31
+ import { getCachedSubagentGroups } from "./agent-transcript/agent-transcript-index-cache.ts";
32
32
 
33
33
  const POLL_INTERVAL_MS = 1000;
34
34
  /** Repeated per-file read failures (locked/replaced file) log once per this window. */
@@ -97,9 +97,9 @@ function warnThrottled(spy: SessionSpy, message: string): void {
97
97
 
98
98
  /** One poll for a session: scan the dir once, then drain each open card's nested tails. */
99
99
  function tick(spy: SessionSpy): void {
100
- let groups: ReturnType<typeof groupSubagentsByCard>;
100
+ let groups: ReturnType<typeof getCachedSubagentGroups>;
101
101
  try {
102
- groups = groupSubagentsByCard(spy.subagentsDir);
102
+ groups = getCachedSubagentGroups(spy.subagentsDir);
103
103
  } catch (e) {
104
104
  warnThrottled(spy, `scan failed: ${(e as Error).message}`);
105
105
  return;