@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,86 @@
1
+ /**
2
+ * Turn a parsed step (Claude's `TimedChatEvent`, Codex's tail-parser output)
3
+ * into the wire `AgentTranscriptEnvelope`, and split a batch of them into
4
+ * pages small enough for one WS frame.
5
+ */
6
+ import type { ChatEvent } from "../../types/chat.ts";
7
+ import { redactTruncate } from "../../providers/codex-app-server/codex-redact.ts";
8
+ import {
9
+ CATCH_UP_MAX_BYTES, CATCH_UP_MAX_EVENTS, TOOL_OUTPUT_MAX_CHARS,
10
+ type AgentTranscriptEnvelope,
11
+ } from "../../shared/agent-transcript-protocol.ts";
12
+
13
+ /**
14
+ * A history view can afford to show a tool's full output; a window is a new
15
+ * long-lived stream kept open for as long as the card is, so both providers'
16
+ * `tool_result` get the same redaction + 20k cap here regardless of what the
17
+ * underlying parser already did for its own output shape.
18
+ */
19
+ export function capToolOutput(ev: ChatEvent): ChatEvent {
20
+ if (ev.type !== "tool_result") return ev;
21
+ const capped = redactTruncate(ev.output, TOOL_OUTPUT_MAX_CHARS);
22
+ return capped === ev.output ? ev : { ...ev, output: capped };
23
+ }
24
+
25
+ /** Assemble one wire envelope for an explicit, already-decided key. */
26
+ export function makeEnvelope(k: string, ev: ChatEvent, ts: number, replace?: true): AgentTranscriptEnvelope {
27
+ return {
28
+ ev: capToolOutput(ev),
29
+ ts,
30
+ k,
31
+ ...(replace ? { replace: true as const } : {}),
32
+ };
33
+ }
34
+
35
+ export function buildEnvelope(
36
+ fileKey: string,
37
+ chunkStartOffset: number,
38
+ index: number,
39
+ ev: ChatEvent,
40
+ ts: number,
41
+ replace?: true,
42
+ ): AgentTranscriptEnvelope {
43
+ return makeEnvelope(`${fileKey}:${chunkStartOffset}:${index}`, ev, ts, replace);
44
+ }
45
+
46
+ /**
47
+ * An envelope tagged with which subscription file it came from and the file
48
+ * offset fully consumed once this specific envelope has been delivered. The
49
+ * hub uses this to attach an exact, per-page cursor — the offset a page's
50
+ * own content corresponds to — rather than the subscription's final offset
51
+ * after the whole tick's read, which a client that only received an earlier
52
+ * page would wrongly treat as "already seen" on reconnect.
53
+ */
54
+ export interface TaggedEnvelope {
55
+ envelope: AgentTranscriptEnvelope;
56
+ fileKey: string;
57
+ consumedThrough: number;
58
+ }
59
+
60
+ /**
61
+ * Split `items` into pages of at most `CATCH_UP_MAX_EVENTS` entries and
62
+ * roughly `CATCH_UP_MAX_BYTES` of serialized JSON — "roughly" because the
63
+ * byte count is measured on each candidate event as it's added rather than
64
+ * re-serializing the whole page, which is cheap and close enough for a page
65
+ * size limit. A single event that alone exceeds the byte budget still gets
66
+ * its own page rather than being dropped or blocking forever.
67
+ */
68
+ export function paginateTagged(items: TaggedEnvelope[]): TaggedEnvelope[][] {
69
+ if (items.length === 0) return [];
70
+ const pages: TaggedEnvelope[][] = [];
71
+ let i = 0;
72
+ while (i < items.length) {
73
+ let count = 0;
74
+ let bytes = 0;
75
+ while (i + count < items.length && count < CATCH_UP_MAX_EVENTS) {
76
+ const size = JSON.stringify(items[i + count]!.envelope).length;
77
+ if (count > 0 && bytes + size > CATCH_UP_MAX_BYTES) break;
78
+ bytes += size;
79
+ count++;
80
+ }
81
+ const take = count > 0 ? count : 1;
82
+ pages.push(items.slice(i, i + take));
83
+ i += take;
84
+ }
85
+ return pages;
86
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Collapse the internal ownership/source error codes down to the four fixed
3
+ * codes the wire protocol allows — nothing past this point ever carries an
4
+ * internal enum name, a path, or an exception message.
5
+ */
6
+ import type { OwnershipErrorCode } from "./session-ownership.ts";
7
+ import type { SourceErrorCode } from "./agent-transcript-sources.ts";
8
+ import type { AgentTranscriptErrorCode } from "../../shared/agent-transcript-protocol.ts";
9
+
10
+ export function mapOwnershipErrorCode(code: OwnershipErrorCode): AgentTranscriptErrorCode {
11
+ switch (code) {
12
+ case "invalid_session_id":
13
+ case "invalid_provider":
14
+ return "bad_request";
15
+ case "invalid_project":
16
+ case "session_not_found":
17
+ return "not_found";
18
+ }
19
+ }
20
+
21
+ export function mapSourceErrorCode(code: SourceErrorCode): AgentTranscriptErrorCode {
22
+ switch (code) {
23
+ case "invalid_card_id":
24
+ case "invalid_team_name":
25
+ case "invalid_member_name":
26
+ return "bad_request";
27
+ case "card_not_found":
28
+ case "member_not_found":
29
+ return "not_found";
30
+ case "not_descendant":
31
+ return "forbidden";
32
+ }
33
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * The read-a-chunk-of-new-bytes operation shared by catch-up (subscribe) and
3
+ * the live tick, over the state shape in `agent-transcript-file-tail-state.ts`.
4
+ *
5
+ * Every line is walked individually (not the whole newly-read chunk at once)
6
+ * so each produced envelope can be tagged with the exact file offset it made
7
+ * complete — the hub needs that to hand a client back a cursor for exactly
8
+ * what it was sent, not for however far the read happened to reach that tick.
9
+ */
10
+ import { buildEnvelope } from "./agent-transcript-envelope.ts";
11
+ import { agentTranscriptFsIo, statMtimeSafe } from "./agent-transcript-fs-io.ts";
12
+ import { codexEnvelope, resetFileTailState, type FileTailState } from "./agent-transcript-file-tail-state.ts";
13
+ import type { AgentTranscriptEnvelope } from "../../shared/agent-transcript-protocol.ts";
14
+
15
+ /** Upper bound on bytes read from one file in one tick, so a multi-MB backlog
16
+ * (a long teammate transcript, a large Codex rollout) cannot block the event
17
+ * loop for the whole gap in one call — the remainder is picked up, as a plain
18
+ * continuation (not another reset), on the next tick. */
19
+ const PER_TICK_READ_BUDGET_BYTES = 512 * 1024;
20
+
21
+ export interface FeedOutcome {
22
+ envelopes: AgentTranscriptEnvelope[];
23
+ /** Same length/order as `envelopes`: the file offset fully consumed once that envelope has been delivered. */
24
+ consumedThrough: number[];
25
+ /** Thread ids a Codex card's tail parser discovered since the last feed (nested spawns). */
26
+ links: string[];
27
+ }
28
+
29
+ /**
30
+ * Feed newly-read bytes through the file's parser, advancing `offset` only
31
+ * up to the last complete line. `growthAt` seeds/updates `lastGrowthAt` from
32
+ * the file's own mtime rather than "now" — reading a large, long-existing
33
+ * backlog for the very first time (a window opened on an already-finished
34
+ * agent) must not read as fresh activity just because THIS subscription is
35
+ * only just now catching up on it.
36
+ */
37
+ function feedChunk(fstate: FileTailState, newChunk: Buffer, growthAt: number): FeedOutcome & { parserReset: boolean } {
38
+ const all = fstate.pendingBytes.length ? Buffer.concat([fstate.pendingBytes, newChunk]) : newChunk;
39
+ const lastNl = all.lastIndexOf(0x0a);
40
+ if (lastNl === -1) {
41
+ fstate.pendingBytes = all;
42
+ return { envelopes: [], consumedThrough: [], links: [], parserReset: false };
43
+ }
44
+ const consumed = all.subarray(0, lastNl + 1);
45
+ fstate.pendingBytes = all.subarray(lastNl + 1);
46
+ const chunkStartOffset = fstate.offset;
47
+ fstate.offset += consumed.length;
48
+ fstate.lastGrowthAt = growthAt;
49
+
50
+ const envelopes: AgentTranscriptEnvelope[] = [];
51
+ const consumedThrough: number[] = [];
52
+ const links: string[] = [];
53
+ let idx = 0;
54
+ let parserReset = false;
55
+ let pos = 0;
56
+ let fileOffset = chunkStartOffset;
57
+
58
+ while (pos < consumed.length) {
59
+ const nl = consumed.indexOf(0x0a, pos);
60
+ if (nl === -1) break; // unreachable: `consumed` always ends at a real newline
61
+ const lineBuf = consumed.subarray(pos, nl);
62
+ fileOffset += nl - pos + 1;
63
+ pos = nl + 1;
64
+ const line = lineBuf.toString("utf8").trim();
65
+ if (!line) continue;
66
+
67
+ if (fstate.ref.provider === "claude") {
68
+ for (const timed of fstate.claudeParser!.feed(line)) {
69
+ envelopes.push(buildEnvelope(fstate.ref.key, chunkStartOffset, idx++, timed.ev, timed.ts));
70
+ consumedThrough.push(fileOffset);
71
+ }
72
+ continue;
73
+ }
74
+
75
+ const fed = fstate.codexParser!.feed(`${line}\n`);
76
+ if (fed.reset) {
77
+ // A compaction/rollback record inside THIS SAME read means everything
78
+ // accumulated from earlier lines of it is no longer authoritative —
79
+ // the parser already dropped its own de-dupe state for this; the
80
+ // per-line loop just does the same to what it had already pushed out.
81
+ envelopes.length = 0;
82
+ consumedThrough.length = 0;
83
+ fstate.toolUseKeyById.clear();
84
+ parserReset = true;
85
+ }
86
+ for (const e of fed.events) {
87
+ const freshKey = `${fstate.ref.key}:${chunkStartOffset}:${idx++}`;
88
+ envelopes.push(codexEnvelope(fstate, freshKey, e));
89
+ consumedThrough.push(fileOffset);
90
+ }
91
+ links.push(...fed.links);
92
+ }
93
+
94
+ return { envelopes, consumedThrough, links, parserReset };
95
+ }
96
+
97
+ export interface ProcessFileResult extends FeedOutcome {
98
+ /** The client must discard prior state for this file: truncation, or a Codex compaction/rollback. */
99
+ reset: boolean;
100
+ /**
101
+ * True when the file still has unread bytes past `PER_TICK_READ_BUDGET_BYTES`
102
+ * after this call. The tick scheduler must keep polling at the live cadence
103
+ * while this is true — a finished-but-large transcript (idle by mtime, so
104
+ * `running: false`) would otherwise drain in slow idle-cadence chunks
105
+ * instead of catching up as fast as the budget allows.
106
+ */
107
+ hasMoreBacklog: boolean;
108
+ }
109
+
110
+ const EMPTY_RESULT: FeedOutcome = { envelopes: [], consumedThrough: [], links: [] };
111
+
112
+ /**
113
+ * Read whatever is new for one file (from `fstate.offset` up to at most
114
+ * `PER_TICK_READ_BUDGET_BYTES` past it) and return the resulting steps.
115
+ * Handles both reset triggers: a file that shrank below the last known
116
+ * offset (rotated/truncated), and a Codex tail parser reporting its own
117
+ * history was rewritten mid-chunk. Never throws — a locked or vanished file,
118
+ * or a raced read past a boundary, degrades to "nothing new this tick"
119
+ * rather than reaching the caller (a single bad file must not take the
120
+ * whole subscription, or the process, down with it).
121
+ */
122
+ export function processFileTail(fstate: FileTailState, now: number): ProcessFileResult {
123
+ let size: number;
124
+ try {
125
+ size = agentTranscriptFsIo.statSize(fstate.ref.path);
126
+ } catch {
127
+ return { ...EMPTY_RESULT, reset: false, hasMoreBacklog: false };
128
+ }
129
+
130
+ let truncated = false;
131
+ if (size < fstate.offset) {
132
+ resetFileTailState(fstate, now);
133
+ truncated = true;
134
+ }
135
+
136
+ let diskPos = fstate.offset + fstate.pendingBytes.length;
137
+ if (size < diskPos) {
138
+ fstate.pendingBytes = Buffer.alloc(0);
139
+ diskPos = fstate.offset;
140
+ }
141
+ if (size <= diskPos) return { ...EMPTY_RESULT, reset: truncated, hasMoreBacklog: false };
142
+
143
+ const growthAt = statMtimeSafe(fstate.ref.path) ?? now;
144
+ const toRead = Math.min(size - diskPos, PER_TICK_READ_BUDGET_BYTES);
145
+ let chunk: Buffer;
146
+ try {
147
+ chunk = agentTranscriptFsIo.readRange(fstate.ref.path, diskPos, toRead);
148
+ } catch {
149
+ return { ...EMPTY_RESULT, reset: truncated, hasMoreBacklog: size > diskPos };
150
+ }
151
+
152
+ const fed = feedChunk(fstate, chunk, growthAt);
153
+ const remainderAfterNormalRead = size > fstate.offset + fstate.pendingBytes.length;
154
+ if (!fed.parserReset) {
155
+ return {
156
+ envelopes: fed.envelopes, consumedThrough: fed.consumedThrough, links: fed.links,
157
+ reset: truncated, hasMoreBacklog: remainderAfterNormalRead,
158
+ };
159
+ }
160
+
161
+ // The parser's own history was rewritten (Codex compaction/rollback): the
162
+ // events already computed from this chunk reflect a mix of pre- and
163
+ // post-rewrite state, so re-derive the authoritative version from byte 0
164
+ // with a fresh parser rather than trying to patch up what we have. Bounded
165
+ // by the same read budget — a huge rewritten file resumes as a plain
166
+ // continuation (not another reset) on the next tick.
167
+ resetFileTailState(fstate, now);
168
+ let full: Buffer;
169
+ try {
170
+ full = agentTranscriptFsIo.readRange(fstate.ref.path, 0, Math.min(size, PER_TICK_READ_BUDGET_BYTES));
171
+ } catch {
172
+ return { ...EMPTY_RESULT, reset: true, hasMoreBacklog: size > 0 };
173
+ }
174
+ const fed2 = feedChunk(fstate, full, growthAt);
175
+ const remainderAfterResync = size > fstate.offset + fstate.pendingBytes.length;
176
+ return {
177
+ envelopes: fed2.envelopes, consumedThrough: fed2.consumedThrough, links: fed2.links,
178
+ reset: true, hasMoreBacklog: remainderAfterResync,
179
+ };
180
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Per-subscription, per-file read state: what a `FileTailState` carries and
3
+ * how it starts out / gets wiped on a reset. The read-a-chunk-of-new-bytes
4
+ * operation itself (shared by catch-up and the live tick) is
5
+ * `agent-transcript-file-tail-feed.ts` — kept apart so this file stays just
6
+ * the state shape or it would not fit the project's file-size guideline.
7
+ *
8
+ * The reported `offset` always sits on a line boundary. Bytes already read
9
+ * from disk but not yet newline-terminated stay in `pendingBytes`, in memory,
10
+ * NOT counted in `offset` — so a client that reconnects with an old cursor
11
+ * and a fresh server-side parser always resumes exactly at a line start. If
12
+ * that were not true, a cursor landing mid-line would either skip the rest of
13
+ * that line forever (offset counted past it) or feed a parser a fragment it
14
+ * cannot make sense of (a fresh parser's own internal buffering, if it had
15
+ * any, would not know a prefix was already spent elsewhere).
16
+ */
17
+ import type { ChatEvent } from "../../types/chat.ts";
18
+ import type { TranscriptFileRef } from "./agent-transcript-sources.ts";
19
+ import { createAgentTranscriptLineParser } from "../subagent-transcript-merger.ts";
20
+ import { createRolloutTailParser } from "../../providers/codex-app-server/codex-rollout-tail-parser.ts";
21
+ import { makeEnvelope } from "./agent-transcript-envelope.ts";
22
+ import { statMtimeSafe } from "./agent-transcript-fs-io.ts";
23
+ import type { AgentTranscriptEnvelope } from "../../shared/agent-transcript-protocol.ts";
24
+
25
+ export interface FileTailState {
26
+ ref: TranscriptFileRef;
27
+ offset: number;
28
+ pendingBytes: Buffer;
29
+ lastGrowthAt: number;
30
+ claudeParser?: ReturnType<typeof createAgentTranscriptLineParser>;
31
+ codexParser?: ReturnType<typeof createRolloutTailParser>;
32
+ /** Codex only: the `k` first issued for a toolUseId, so a later `replace`
33
+ * record for the same id reuses it instead of minting an unmatchable one
34
+ * (see `agent-transcript-file-tail-feed.ts`). */
35
+ toolUseKeyById: Map<string, string>;
36
+ }
37
+
38
+ /** Seed idle/liveness tracking from the file's own mtime, not "now" — a window
39
+ * opened on an already-finished agent must not read as freshly active just
40
+ * because that is when someone happened to subscribe. */
41
+ function initialGrowthAt(path: string, now: number): number {
42
+ return statMtimeSafe(path) ?? now;
43
+ }
44
+
45
+ export function createFileTailState(ref: TranscriptFileRef, startOffset: number, now: number): FileTailState {
46
+ const midFile = startOffset > 0;
47
+ return {
48
+ ref,
49
+ offset: startOffset,
50
+ pendingBytes: Buffer.alloc(0),
51
+ lastGrowthAt: initialGrowthAt(ref.path, now),
52
+ claudeParser: ref.provider === "claude" ? createAgentTranscriptLineParser({ midFile, maxEvents: Infinity }) : undefined,
53
+ codexParser: ref.provider === "codex" ? createRolloutTailParser() : undefined,
54
+ toolUseKeyById: new Map(),
55
+ };
56
+ }
57
+
58
+ /** Wipe a file's tracked state back to "never read": a truncation/rotation or
59
+ * a Codex compaction/rollback means nothing previously derived from it can
60
+ * be trusted, so the next read starts over from byte 0 with fresh parsers. */
61
+ export function resetFileTailState(fstate: FileTailState, now: number): void {
62
+ fstate.offset = 0;
63
+ fstate.pendingBytes = Buffer.alloc(0);
64
+ fstate.lastGrowthAt = now;
65
+ fstate.claudeParser = fstate.ref.provider === "claude" ? createAgentTranscriptLineParser({ midFile: false, maxEvents: Infinity }) : undefined;
66
+ fstate.codexParser = fstate.ref.provider === "codex" ? createRolloutTailParser() : undefined;
67
+ fstate.toolUseKeyById = new Map();
68
+ }
69
+
70
+ function toolUseIdOf(ev: ChatEvent): string | undefined {
71
+ return "toolUseId" in ev ? ev.toolUseId : undefined;
72
+ }
73
+
74
+ /**
75
+ * One Codex event → its envelope, reusing the id's first-issued key on a
76
+ * replace so the client's upsert-by-`k` lands on the entry it is meant to
77
+ * update instead of appending.
78
+ *
79
+ * Keyed by `${type}:${toolUseId}`, NOT by id alone: a single `CommandExecution`
80
+ * item emits a `tool_use` and a `tool_result` sharing the SAME id, and the
81
+ * tail parser flags the second one `replace` because it only tracks "has
82
+ * this id been seen", not "as which type". Keying on id alone made a
83
+ * `tool_result` reuse its own `tool_use`'s key — the client then replaced
84
+ * the tool_use IN PLACE with the result and the step vanished. `replace` is
85
+ * only ever honoured here when a PRIOR envelope of the SAME type for the
86
+ * SAME id exists; otherwise a fresh key is minted (and `replace` dropped —
87
+ * there is nothing for it to refer to).
88
+ */
89
+ export function codexEnvelope(fstate: FileTailState, freshKey: string, e: { ev: ChatEvent; ts: number; replace?: boolean }): AgentTranscriptEnvelope {
90
+ const id = toolUseIdOf(e.ev);
91
+ if (!id) return makeEnvelope(freshKey, e.ev, e.ts, undefined);
92
+
93
+ const mapKey = `${e.ev.type}:${id}`;
94
+ if (e.replace) {
95
+ const original = fstate.toolUseKeyById.get(mapKey);
96
+ if (original) return makeEnvelope(original, e.ev, e.ts, true);
97
+ }
98
+ fstate.toolUseKeyById.set(mapKey, freshKey);
99
+ return makeEnvelope(freshKey, e.ev, e.ts, undefined);
100
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Filesystem seam the hub reads through, so a test can simulate a locked or
3
+ * vanished file without touching real disk I/O, and so the tick path never
4
+ * hides a surprise `fs` call behind a helper that looks pure.
5
+ *
6
+ * Every read is by explicit byte range — the hub never re-reads a file from
7
+ * the start just to find out how much it grew. Each subscription stats and
8
+ * reads its own files independently (one stat per (subscription, file) pair
9
+ * per tick) — several subscriptions watching the same underlying path are
10
+ * not de-duplicated against each other.
11
+ */
12
+ import { closeSync, openSync, readSync, statSync } from "node:fs";
13
+
14
+ export interface AgentTranscriptFsIo {
15
+ /** Current file size in bytes. Throws if the file does not exist. */
16
+ statSize(path: string): number;
17
+ /** File mtime in epoch ms, for seeding "last known activity" without assuming `now`. Throws if missing. */
18
+ statMtimeMs(path: string): number;
19
+ /** Directory mtime in epoch ms, for the "did anything change" index-refresh check. Throws if missing. */
20
+ statDirMtimeMs(path: string): number;
21
+ /** Bytes `[start, start+length)`. `length <= 0` returns an empty buffer without opening the file. */
22
+ readRange(path: string, start: number, length: number): Buffer;
23
+ }
24
+
25
+ function realReadRange(path: string, start: number, length: number): Buffer {
26
+ if (length <= 0) return Buffer.alloc(0);
27
+ const fd = openSync(path, "r");
28
+ try {
29
+ const buf = Buffer.alloc(length);
30
+ const n = readSync(fd, buf, 0, length, start);
31
+ return buf.subarray(0, n);
32
+ } finally {
33
+ closeSync(fd);
34
+ }
35
+ }
36
+
37
+ export const realAgentTranscriptFsIo: AgentTranscriptFsIo = {
38
+ statSize: (path) => statSync(path).size,
39
+ statMtimeMs: (path) => statSync(path).mtimeMs,
40
+ statDirMtimeMs: (path) => statSync(path).mtimeMs,
41
+ readRange: realReadRange,
42
+ };
43
+
44
+ /** Mutable seam — reassign in a test, restore in `afterEach`. */
45
+ export const agentTranscriptFsIo: AgentTranscriptFsIo = { ...realAgentTranscriptFsIo };
46
+
47
+ export function resetAgentTranscriptFsIoForTest(): void {
48
+ agentTranscriptFsIo.statSize = realAgentTranscriptFsIo.statSize;
49
+ agentTranscriptFsIo.statMtimeMs = realAgentTranscriptFsIo.statMtimeMs;
50
+ agentTranscriptFsIo.statDirMtimeMs = realAgentTranscriptFsIo.statDirMtimeMs;
51
+ agentTranscriptFsIo.readRange = realAgentTranscriptFsIo.readRange;
52
+ }
53
+
54
+ /** `statSize`, or `null` for any error (missing file, permission, raced delete). */
55
+ export function statSizeSafe(path: string): number | null {
56
+ try {
57
+ return agentTranscriptFsIo.statSize(path);
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /** `statMtimeMs`, or `null` for any error. */
64
+ export function statMtimeSafe(path: string): number | null {
65
+ try {
66
+ return agentTranscriptFsIo.statMtimeMs(path);
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /** `statDirMtimeMs`, or `null` for any error. */
73
+ export function statDirMtimeSafe(path: string): number | null {
74
+ try {
75
+ return agentTranscriptFsIo.statDirMtimeMs(path);
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Test seams for the hub's two time-driven behaviors: "what time is it" and
3
+ * "call me back in N ms". Mutable singletons (same shape as
4
+ * `designWriteClock` in `design-write-rate-limit.ts`) rather than constructor
5
+ * parameters threaded through every function, so a unit test can swap either
6
+ * one out for the duration of a case and put it back.
7
+ *
8
+ * A test that wants deterministic ticks replaces `setInterval` with a no-op
9
+ * that never actually schedules anything, then drives the hub by calling its
10
+ * internal tick function directly while moving `clock.now` forward by hand —
11
+ * real wall-clock delays never enter the picture.
12
+ */
13
+
14
+ export interface AgentTranscriptClock {
15
+ now(): number;
16
+ }
17
+
18
+ export const agentTranscriptClock: AgentTranscriptClock = {
19
+ now: () => Date.now(),
20
+ };
21
+
22
+ export interface AgentTranscriptTimers {
23
+ setInterval(fn: () => void, ms: number): unknown;
24
+ clearInterval(handle: unknown): void;
25
+ }
26
+
27
+ export const agentTranscriptTimers: AgentTranscriptTimers = {
28
+ setInterval: (fn, ms) => setInterval(fn, ms),
29
+ clearInterval: (handle) => clearInterval(handle as ReturnType<typeof setInterval>),
30
+ };
31
+
32
+ /** Restore both seams to their real implementations; call from `afterEach`. */
33
+ export function resetAgentTranscriptClockForTest(): void {
34
+ agentTranscriptClock.now = () => Date.now();
35
+ agentTranscriptTimers.setInterval = (fn, ms) => setInterval(fn, ms);
36
+ agentTranscriptTimers.clearInterval = (handle) => clearInterval(handle as ReturnType<typeof setInterval>);
37
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Cross-hub bookkeeping for `agent-transcript-hub.ts`: which `SessionHub` a
3
+ * client's `subId` currently lives in (needed because a `subId` carries no
4
+ * session info on `unsubscribe`, and the same client can hold subscriptions
5
+ * on several different sessions at once), the server-wide subscription count,
6
+ * and dropping a client from every hub it touches at once.
7
+ */
8
+ import { configService } from "../config.service.ts";
9
+ import type { OwnedSession } from "./session-ownership.ts";
10
+ import type { AgentTranscriptWsLike } from "./agent-transcript-ws-like.ts";
11
+ import {
12
+ activitySubCount, createSessionHub, hubIsEmpty, transcriptSubCount, type SessionHub,
13
+ } from "./agent-transcript-session-hub-types.ts";
14
+ import { dropAllTranscriptForWs, removeTranscriptSubscription } from "./agent-transcript-session-hub.ts";
15
+ import { dropAllActivityForWs, removeActivitySubscription } from "./agent-transcript-session-hub-activity.ts";
16
+
17
+ const sessionHubs = new Map<string, SessionHub>();
18
+ /** Which session hub a client's transcript/activity `subId` currently lives in. */
19
+ const clientTranscriptSubs = new Map<AgentTranscriptWsLike, Map<string, string>>();
20
+ const clientActivitySubs = new Map<AgentTranscriptWsLike, Map<string, string>>();
21
+
22
+ export function hubKey(providerId: string, sessionId: string): string {
23
+ return `${providerId}\0${sessionId}`;
24
+ }
25
+
26
+ /** `ws.data.token` (snapshotted at upgrade) against the live config — see `server/index.ts` and `ws/global.ts`. */
27
+ export function tokenStillValid(ws: AgentTranscriptWsLike): boolean {
28
+ const auth = configService.get("auth");
29
+ if (!auth.enabled) return true;
30
+ return (ws.data?.token ?? null) === auth.token;
31
+ }
32
+
33
+ export function totalTranscriptSubs(): number {
34
+ let n = 0;
35
+ for (const hub of sessionHubs.values()) n += transcriptSubCount(hub);
36
+ return n;
37
+ }
38
+
39
+ export function totalActivitySubs(): number {
40
+ let n = 0;
41
+ for (const hub of sessionHubs.values()) n += activitySubCount(hub);
42
+ return n;
43
+ }
44
+
45
+ export function projectPathFor(projectName: string): string | null {
46
+ return configService.get("projects").find((p) => p.name === projectName)?.path ?? null;
47
+ }
48
+
49
+ /**
50
+ * `owned` must come from an `assertSessionInProject` call made for THIS
51
+ * request's own claimed project — a hub already cached under the same
52
+ * `(providerId, sessionId)` is only ever reused once the caller has
53
+ * independently proven that pairing resolves to a project it may read,
54
+ * never trusted just because some earlier, different caller created it.
55
+ */
56
+ export function getOrCreateHub(owned: OwnedSession): SessionHub {
57
+ const key = hubKey(owned.providerId, owned.sessionId);
58
+ const existing = sessionHubs.get(key);
59
+ if (existing) return existing;
60
+ const hub = createSessionHub(key, owned, dropClientEverywhere, unregisterTranscriptSub, tokenStillValid);
61
+ sessionHubs.set(key, hub);
62
+ return hub;
63
+ }
64
+
65
+ export function maybeDropHub(key: string): void {
66
+ const hub = sessionHubs.get(key);
67
+ if (hub && hubIsEmpty(hub)) sessionHubs.delete(key);
68
+ }
69
+
70
+ export function registerTranscriptSub(ws: AgentTranscriptWsLike, subId: string, key: string): void {
71
+ let bySubId = clientTranscriptSubs.get(ws);
72
+ if (!bySubId) {
73
+ bySubId = new Map();
74
+ clientTranscriptSubs.set(ws, bySubId);
75
+ }
76
+ bySubId.set(subId, key);
77
+ }
78
+
79
+ export function registerActivitySub(ws: AgentTranscriptWsLike, subId: string, key: string): void {
80
+ let bySubId = clientActivitySubs.get(ws);
81
+ if (!bySubId) {
82
+ bySubId = new Map();
83
+ clientActivitySubs.set(ws, bySubId);
84
+ }
85
+ bySubId.set(subId, key);
86
+ }
87
+
88
+ export function transcriptSubCountForClient(ws: AgentTranscriptWsLike): number {
89
+ return clientTranscriptSubs.get(ws)?.size ?? 0;
90
+ }
91
+
92
+ export function activitySubCountForClient(ws: AgentTranscriptWsLike): number {
93
+ return clientActivitySubs.get(ws)?.size ?? 0;
94
+ }
95
+
96
+ export function unregisterTranscriptSub(ws: AgentTranscriptWsLike, subId: string): void {
97
+ const key = clientTranscriptSubs.get(ws)?.get(subId);
98
+ if (!key) return;
99
+ clientTranscriptSubs.get(ws)!.delete(subId);
100
+ if (clientTranscriptSubs.get(ws)!.size === 0) clientTranscriptSubs.delete(ws);
101
+ const hub = sessionHubs.get(key);
102
+ if (hub) {
103
+ removeTranscriptSubscription(hub, ws, subId);
104
+ maybeDropHub(key);
105
+ }
106
+ }
107
+
108
+ export function unregisterActivitySub(ws: AgentTranscriptWsLike, subId: string): void {
109
+ const key = clientActivitySubs.get(ws)?.get(subId);
110
+ if (!key) return;
111
+ clientActivitySubs.get(ws)!.delete(subId);
112
+ if (clientActivitySubs.get(ws)!.size === 0) clientActivitySubs.delete(ws);
113
+ const hub = sessionHubs.get(key);
114
+ if (hub) {
115
+ removeActivitySubscription(hub, ws, subId);
116
+ maybeDropHub(key);
117
+ }
118
+ }
119
+
120
+ /** Drop everything a socket holds, in every hub it touches. Called on close, a stale token, or a failed send. */
121
+ export function dropClientEverywhere(ws: AgentTranscriptWsLike): void {
122
+ const hubKeys = new Set<string>([
123
+ ...(clientTranscriptSubs.get(ws)?.values() ?? []),
124
+ ...(clientActivitySubs.get(ws)?.values() ?? []),
125
+ ]);
126
+ clientTranscriptSubs.delete(ws);
127
+ clientActivitySubs.delete(ws);
128
+ for (const key of hubKeys) {
129
+ const hub = sessionHubs.get(key);
130
+ if (!hub) continue;
131
+ dropAllTranscriptForWs(hub, ws);
132
+ dropAllActivityForWs(hub, ws);
133
+ maybeDropHub(key);
134
+ }
135
+ }
136
+
137
+ /** Test-only: forget every hub and client mapping between cases. */
138
+ export function _resetAgentTranscriptHubForTest(): void {
139
+ sessionHubs.clear();
140
+ clientTranscriptSubs.clear();
141
+ clientActivitySubs.clear();
142
+ }
143
+
144
+ /** Test-only: peek at a hub's existence/subscription counts without exporting internal state. */
145
+ export function _debugHubSnapshotForTest(providerId: "claude" | "codex", sessionId: string): { exists: boolean; transcriptSubs: number } {
146
+ const hub = sessionHubs.get(hubKey(providerId, sessionId));
147
+ return { exists: !!hub, transcriptSubs: hub ? transcriptSubCount(hub) : 0 };
148
+ }
149
+
150
+ /** Test-only: the raw hub, so a test can drive its tick functions directly instead of waiting on real timers. */
151
+ export function _getSessionHubForTest(providerId: "claude" | "codex", sessionId: string): SessionHub | undefined {
152
+ return sessionHubs.get(hubKey(providerId, sessionId));
153
+ }