termdock 1.4.296 → 1.4.297

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 (364) hide show
  1. package/.desktop-dist/main.js +35 -2
  2. package/README.en.md +257 -0
  3. package/README.md +284 -133
  4. package/dist/client/assets/App-BbqtNY2x.js +177 -0
  5. package/dist/client/assets/App-BbqtNY2x.js.br +0 -0
  6. package/dist/client/assets/App-BbqtNY2x.js.gz +0 -0
  7. package/dist/client/assets/{ChangeWalkthroughPanel-DXprT-7m.js → ChangeWalkthroughPanel-CvQJIp_C.js} +1 -1
  8. package/dist/client/assets/ChangeWalkthroughPanel-CvQJIp_C.js.br +0 -0
  9. package/dist/client/assets/ChangeWalkthroughPanel-CvQJIp_C.js.gz +0 -0
  10. package/dist/client/assets/CollaborationMainWorkspace-B0OgRtyR.js +1 -0
  11. package/dist/client/assets/CollaborationMainWorkspace-B0OgRtyR.js.br +0 -0
  12. package/dist/client/assets/CollaborationMainWorkspace-B0OgRtyR.js.gz +0 -0
  13. package/dist/client/assets/CollaborationTaskContent-C01GXOEq.js +2 -0
  14. package/dist/client/assets/ComputerControlView-CW1uA133.js +4 -0
  15. package/dist/client/assets/ComputerControlView-CW1uA133.js.br +0 -0
  16. package/dist/client/assets/ComputerControlView-CW1uA133.js.gz +0 -0
  17. package/dist/client/assets/{DagPlayground-DZcUunT5.js → DagPlayground-CdE3tQsl.js} +1 -1
  18. package/dist/client/assets/DagPlayground-CdE3tQsl.js.br +0 -0
  19. package/dist/client/assets/DagPlayground-CdE3tQsl.js.gz +0 -0
  20. package/dist/client/assets/{DiffLab-Brv4DCTb.js → DiffLab-BLGNnazd.js} +1 -1
  21. package/dist/client/assets/DiffLab-BLGNnazd.js.br +0 -0
  22. package/dist/client/assets/DiffLab-BLGNnazd.js.gz +0 -0
  23. package/dist/client/assets/{DiffReview-BuLu1qCi.js → DiffReview-CYw3ASI6.js} +1 -1
  24. package/dist/client/assets/DiffReview-CYw3ASI6.js.br +0 -0
  25. package/dist/client/assets/DiffReview-CYw3ASI6.js.gz +0 -0
  26. package/dist/client/assets/{DiffReviewLab-DSGamuAD.js → DiffReviewLab-CySIuWq_.js} +1 -1
  27. package/dist/client/assets/DiffReviewLab-CySIuWq_.js.br +0 -0
  28. package/dist/client/assets/DiffReviewLab-CySIuWq_.js.gz +0 -0
  29. package/dist/client/assets/{DiffViewer-BUCYxg27.js → DiffViewer-WsYvN75z.js} +13 -13
  30. package/dist/client/assets/DiffViewer-WsYvN75z.js.br +0 -0
  31. package/dist/client/assets/DiffViewer-WsYvN75z.js.gz +0 -0
  32. package/dist/client/assets/{ModelPreview-Bnlo-oVE.js → ModelPreview-Dnlx9ByX.js} +1 -1
  33. package/dist/client/assets/ModelPreview-Dnlx9ByX.js.br +0 -0
  34. package/dist/client/assets/ModelPreview-Dnlx9ByX.js.gz +0 -0
  35. package/dist/client/assets/RightSidebar-Cp5b_y0u.js +1678 -0
  36. package/dist/client/assets/RightSidebar-Cp5b_y0u.js.br +0 -0
  37. package/dist/client/assets/RightSidebar-Cp5b_y0u.js.gz +0 -0
  38. package/dist/client/assets/{abnfDiagram-VCTEODGH-K6Ppkrbt.js → abnfDiagram-VCTEODGH-CK1mGFRQ.js} +1 -1
  39. package/dist/client/assets/abnfDiagram-VCTEODGH-CK1mGFRQ.js.br +0 -0
  40. package/dist/client/assets/abnfDiagram-VCTEODGH-CK1mGFRQ.js.gz +0 -0
  41. package/dist/client/assets/app-DfACMixk.js +35 -0
  42. package/dist/client/assets/app-DfACMixk.js.br +0 -0
  43. package/dist/client/assets/app-DfACMixk.js.gz +0 -0
  44. package/dist/client/assets/{arc-Co2OQqtH.js → arc-yZE1gqDl.js} +1 -1
  45. package/dist/client/assets/arc-yZE1gqDl.js.br +0 -0
  46. package/dist/client/assets/arc-yZE1gqDl.js.gz +0 -0
  47. package/dist/client/assets/{architectureDiagram-5GKGNRK7-BQnl7tVF.js → architectureDiagram-5GKGNRK7-U5XSC7cY.js} +1 -1
  48. package/dist/client/assets/architectureDiagram-5GKGNRK7-U5XSC7cY.js.br +0 -0
  49. package/dist/client/assets/architectureDiagram-5GKGNRK7-U5XSC7cY.js.gz +0 -0
  50. package/dist/client/assets/{blockDiagram-I7D4REHJ-Bm5D5-qq.js → blockDiagram-I7D4REHJ-pVJnntSG.js} +1 -1
  51. package/dist/client/assets/blockDiagram-I7D4REHJ-pVJnntSG.js.br +0 -0
  52. package/dist/client/assets/blockDiagram-I7D4REHJ-pVJnntSG.js.gz +0 -0
  53. package/dist/client/assets/{c4Diagram-7LVT6UL2-B9oPpey8.js → c4Diagram-7LVT6UL2-Bwz0AJJV.js} +1 -1
  54. package/dist/client/assets/c4Diagram-7LVT6UL2-Bwz0AJJV.js.br +0 -0
  55. package/dist/client/assets/c4Diagram-7LVT6UL2-Bwz0AJJV.js.gz +0 -0
  56. package/dist/client/assets/channel-qqjDJtcZ.js +1 -0
  57. package/dist/client/assets/{chunk-2Q5K7J3B-TMaWHH0U.js → chunk-2Q5K7J3B-CXJ1kP4X.js} +1 -1
  58. package/dist/client/assets/{chunk-5VM5RSS4-CH7XpknV.js → chunk-5VM5RSS4-Dpdum_2z.js} +1 -1
  59. package/dist/client/assets/{chunk-F27PBJKO-CJ1rotBe.js → chunk-F27PBJKO-BCG5Zi_-.js} +1 -1
  60. package/dist/client/assets/chunk-F27PBJKO-BCG5Zi_-.js.br +0 -0
  61. package/dist/client/assets/chunk-F27PBJKO-BCG5Zi_-.js.gz +0 -0
  62. package/dist/client/assets/{chunk-IMKFNOWR-UYKZ5uOG.js → chunk-IMKFNOWR-BM-GcNcv.js} +1 -1
  63. package/dist/client/assets/chunk-IMKFNOWR-BM-GcNcv.js.br +0 -0
  64. package/dist/client/assets/chunk-IMKFNOWR-BM-GcNcv.js.gz +0 -0
  65. package/dist/client/assets/{chunk-JWPE2WC7-BYL8aXjQ.js → chunk-JWPE2WC7-BWiXpQT9.js} +1 -1
  66. package/dist/client/assets/{chunk-POPQ4Y6H-C3E8yfU2.js → chunk-POPQ4Y6H-vQSslMNv.js} +1 -1
  67. package/dist/client/assets/{chunk-SVP7TREG-DCTDYa1E.js → chunk-SVP7TREG-Aqfz6nPA.js} +1 -1
  68. package/dist/client/assets/chunk-SVP7TREG-Aqfz6nPA.js.br +0 -0
  69. package/dist/client/assets/chunk-SVP7TREG-Aqfz6nPA.js.gz +0 -0
  70. package/dist/client/assets/{chunk-TICWLB2K-wHiaOsBh.js → chunk-TICWLB2K-DDjekMd0.js} +1 -1
  71. package/dist/client/assets/chunk-TICWLB2K-DDjekMd0.js.br +0 -0
  72. package/dist/client/assets/chunk-TICWLB2K-DDjekMd0.js.gz +0 -0
  73. package/dist/client/assets/{chunk-XXDRQBXY-D_d479hK.js → chunk-XXDRQBXY-BegIBcFg.js} +1 -1
  74. package/dist/client/assets/classDiagram-ZZMXUADV-BDuBrfNz.js +1 -0
  75. package/dist/client/assets/classDiagram-v2-VYDZK3BY-BDuBrfNz.js +1 -0
  76. package/dist/client/assets/{cose-bilkent-JH36ORCC-DnhlPBLr.js → cose-bilkent-JH36ORCC-B2izFsBY.js} +1 -1
  77. package/dist/client/assets/cose-bilkent-JH36ORCC-B2izFsBY.js.br +0 -0
  78. package/dist/client/assets/cose-bilkent-JH36ORCC-B2izFsBY.js.gz +0 -0
  79. package/dist/client/assets/{cynefin-OW5HDTMX-BsjzNqJZ.js → cynefin-OW5HDTMX-C7HA99Zc.js} +1 -1
  80. package/dist/client/assets/cynefin-OW5HDTMX-C7HA99Zc.js.br +0 -0
  81. package/dist/client/assets/cynefin-OW5HDTMX-C7HA99Zc.js.gz +0 -0
  82. package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-sVUb0zY5.js → cynefinDiagram-5FMLGOSQ-vNIzcCYl.js} +1 -1
  83. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-vNIzcCYl.js.br +0 -0
  84. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-vNIzcCYl.js.gz +0 -0
  85. package/dist/client/assets/{dagre-GXQ25YYZ-YErk8S-c.js → dagre-GXQ25YYZ-Bv9WHNT9.js} +1 -1
  86. package/dist/client/assets/dagre-GXQ25YYZ-Bv9WHNT9.js.br +0 -0
  87. package/dist/client/assets/dagre-GXQ25YYZ-Bv9WHNT9.js.gz +0 -0
  88. package/dist/client/assets/{diagram-S7CK7UJ4-BaHbV5ho.js → diagram-S7CK7UJ4-DFg5UNic.js} +1 -1
  89. package/dist/client/assets/diagram-S7CK7UJ4-DFg5UNic.js.br +0 -0
  90. package/dist/client/assets/diagram-S7CK7UJ4-DFg5UNic.js.gz +0 -0
  91. package/dist/client/assets/{diagram-UQ7AKVKN-BocCYx3V.js → diagram-UQ7AKVKN-BFQsCAs7.js} +1 -1
  92. package/dist/client/assets/diagram-UQ7AKVKN-BFQsCAs7.js.br +0 -0
  93. package/dist/client/assets/diagram-UQ7AKVKN-BFQsCAs7.js.gz +0 -0
  94. package/dist/client/assets/{diagram-VSXAHHWV-ZMTYwWOQ.js → diagram-VSXAHHWV-C4R8TKEv.js} +1 -1
  95. package/dist/client/assets/diagram-VSXAHHWV-C4R8TKEv.js.br +0 -0
  96. package/dist/client/assets/diagram-VSXAHHWV-C4R8TKEv.js.gz +0 -0
  97. package/dist/client/assets/{diagram-VX7I27RA-xmk_EY1h.js → diagram-VX7I27RA-CL75HIf0.js} +1 -1
  98. package/dist/client/assets/diagram-VX7I27RA-CL75HIf0.js.br +0 -0
  99. package/dist/client/assets/diagram-VX7I27RA-CL75HIf0.js.gz +0 -0
  100. package/dist/client/assets/{diagram-Z3DM3KII-DKDY_cG6.js → diagram-Z3DM3KII-CfJampuO.js} +1 -1
  101. package/dist/client/assets/diagram-Z3DM3KII-CfJampuO.js.br +0 -0
  102. package/dist/client/assets/diagram-Z3DM3KII-CfJampuO.js.gz +0 -0
  103. package/dist/client/assets/{ebnfDiagram-PWID7BFC-C_0z-Jx9.js → ebnfDiagram-PWID7BFC-Ct53jhNd.js} +1 -1
  104. package/dist/client/assets/ebnfDiagram-PWID7BFC-Ct53jhNd.js.br +0 -0
  105. package/dist/client/assets/ebnfDiagram-PWID7BFC-Ct53jhNd.js.gz +0 -0
  106. package/dist/client/assets/{erDiagram-RLTQ6QDP-DHUaG7mt.js → erDiagram-RLTQ6QDP-BgL2xOOa.js} +1 -1
  107. package/dist/client/assets/erDiagram-RLTQ6QDP-BgL2xOOa.js.br +0 -0
  108. package/dist/client/assets/erDiagram-RLTQ6QDP-BgL2xOOa.js.gz +0 -0
  109. package/dist/client/assets/{flowDiagram-HODETNUW-BMNsdMBD.js → flowDiagram-HODETNUW-Cp4Nu5jY.js} +1 -1
  110. package/dist/client/assets/flowDiagram-HODETNUW-Cp4Nu5jY.js.br +0 -0
  111. package/dist/client/assets/flowDiagram-HODETNUW-Cp4Nu5jY.js.gz +0 -0
  112. package/dist/client/assets/{ganttDiagram-EL5Y4UJY-BH1AiKuF.js → ganttDiagram-EL5Y4UJY-DI0rdyPX.js} +1 -1
  113. package/dist/client/assets/ganttDiagram-EL5Y4UJY-DI0rdyPX.js.br +0 -0
  114. package/dist/client/assets/ganttDiagram-EL5Y4UJY-DI0rdyPX.js.gz +0 -0
  115. package/dist/client/assets/{gestureArbiter-DvupkPra.js → gestureArbiter-Belsq4xq.js} +1 -1
  116. package/dist/client/assets/gestureArbiter-Belsq4xq.js.br +0 -0
  117. package/dist/client/assets/gestureArbiter-Belsq4xq.js.gz +0 -0
  118. package/dist/client/assets/{gitGraphDiagram-WWUBYQGX-DAgV0MV-.js → gitGraphDiagram-WWUBYQGX-C0H4loQP.js} +1 -1
  119. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-C0H4loQP.js.br +0 -0
  120. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-C0H4loQP.js.gz +0 -0
  121. package/dist/client/assets/index-CYmI1EsU.css +1 -0
  122. package/dist/client/assets/index-CYmI1EsU.css.br +0 -0
  123. package/dist/client/assets/index-CYmI1EsU.css.gz +0 -0
  124. package/dist/client/assets/{infoDiagram-27XIBGKW-C7iSg0cB.js → infoDiagram-27XIBGKW-BN4RpQTL.js} +1 -1
  125. package/dist/client/assets/{ishikawaDiagram-5VMMS53U-BpZwsQvw.js → ishikawaDiagram-5VMMS53U-BZQ5qZB_.js} +1 -1
  126. package/dist/client/assets/ishikawaDiagram-5VMMS53U-BZQ5qZB_.js.br +0 -0
  127. package/dist/client/assets/ishikawaDiagram-5VMMS53U-BZQ5qZB_.js.gz +0 -0
  128. package/dist/client/assets/{journeyDiagram-3NMN7TZE-DXEeFLvw.js → journeyDiagram-3NMN7TZE-Q8bx0_N4.js} +1 -1
  129. package/dist/client/assets/journeyDiagram-3NMN7TZE-Q8bx0_N4.js.br +0 -0
  130. package/dist/client/assets/journeyDiagram-3NMN7TZE-Q8bx0_N4.js.gz +0 -0
  131. package/dist/client/assets/{kanban-definition-UXKFOSKX-CV9ivL5u.js → kanban-definition-UXKFOSKX-D7JhGtpO.js} +1 -1
  132. package/dist/client/assets/kanban-definition-UXKFOSKX-D7JhGtpO.js.br +0 -0
  133. package/dist/client/assets/kanban-definition-UXKFOSKX-D7JhGtpO.js.gz +0 -0
  134. package/dist/client/assets/{layout-DsKKsJKX.js → layout-DERUg0xU.js} +1 -1
  135. package/dist/client/assets/layout-DERUg0xU.js.br +0 -0
  136. package/dist/client/assets/layout-DERUg0xU.js.gz +0 -0
  137. package/dist/client/assets/{linear-Dl9MDNV3.js → linear-lNTe4x00.js} +1 -1
  138. package/dist/client/assets/linear-lNTe4x00.js.br +0 -0
  139. package/dist/client/assets/linear-lNTe4x00.js.gz +0 -0
  140. package/dist/client/assets/{mermaid.core-CxZFe3as.js → mermaid.core-WHCwLQDQ.js} +5 -5
  141. package/dist/client/assets/mermaid.core-WHCwLQDQ.js.br +0 -0
  142. package/dist/client/assets/mermaid.core-WHCwLQDQ.js.gz +0 -0
  143. package/dist/client/assets/{mindmap-definition-YA3MSWOX-C3ormYdL.js → mindmap-definition-YA3MSWOX-BouapA4v.js} +1 -1
  144. package/dist/client/assets/mindmap-definition-YA3MSWOX-BouapA4v.js.br +0 -0
  145. package/dist/client/assets/mindmap-definition-YA3MSWOX-BouapA4v.js.gz +0 -0
  146. package/dist/client/assets/{passwordBootstrap-a8z6jJNY.js → passwordBootstrap-E0piP814.js} +2 -2
  147. package/dist/client/assets/passwordBootstrap-E0piP814.js.br +0 -0
  148. package/dist/client/assets/passwordBootstrap-E0piP814.js.gz +0 -0
  149. package/dist/client/assets/{pegDiagram-XKGWAZYB-CG0wNylH.js → pegDiagram-XKGWAZYB-Curpo5Rq.js} +1 -1
  150. package/dist/client/assets/pegDiagram-XKGWAZYB-Curpo5Rq.js.br +0 -0
  151. package/dist/client/assets/pegDiagram-XKGWAZYB-Curpo5Rq.js.gz +0 -0
  152. package/dist/client/assets/{pieDiagram-E7YTZNPT-D2-ydqyX.js → pieDiagram-E7YTZNPT-BLQCHaOG.js} +1 -1
  153. package/dist/client/assets/pieDiagram-E7YTZNPT-BLQCHaOG.js.br +0 -0
  154. package/dist/client/assets/pieDiagram-E7YTZNPT-BLQCHaOG.js.gz +0 -0
  155. package/dist/client/assets/{quadrantDiagram-AXDQQJYC-CK3fRC18.js → quadrantDiagram-AXDQQJYC-PViFqGao.js} +1 -1
  156. package/dist/client/assets/quadrantDiagram-AXDQQJYC-PViFqGao.js.br +0 -0
  157. package/dist/client/assets/quadrantDiagram-AXDQQJYC-PViFqGao.js.gz +0 -0
  158. package/dist/client/assets/{railroadDiagram-O6MQD6OU-V4EFlydu.js → railroadDiagram-O6MQD6OU-BtHo1LpT.js} +1 -1
  159. package/dist/client/assets/railroadDiagram-O6MQD6OU-BtHo1LpT.js.br +0 -0
  160. package/dist/client/assets/railroadDiagram-O6MQD6OU-BtHo1LpT.js.gz +0 -0
  161. package/dist/client/assets/rdpSession-O_f9ejOI.js +1 -0
  162. package/dist/client/assets/rdpSession-O_f9ejOI.js.br +0 -0
  163. package/dist/client/assets/rdpSession-O_f9ejOI.js.gz +0 -0
  164. package/dist/client/assets/{referenceLongPress-BjmYd1vB.js → referenceLongPress-BZr7Lqzh.js} +1 -1
  165. package/dist/client/assets/referenceLongPress-BZr7Lqzh.js.br +0 -0
  166. package/dist/client/assets/referenceLongPress-BZr7Lqzh.js.gz +0 -0
  167. package/dist/client/assets/{requirementDiagram-BXWQKSXE-Di1_tyAX.js → requirementDiagram-BXWQKSXE-BUm81HUU.js} +1 -1
  168. package/dist/client/assets/requirementDiagram-BXWQKSXE-BUm81HUU.js.br +0 -0
  169. package/dist/client/assets/requirementDiagram-BXWQKSXE-BUm81HUU.js.gz +0 -0
  170. package/dist/client/assets/rfb-CxsXsMUX.js +5 -0
  171. package/dist/client/assets/rfb-CxsXsMUX.js.br +0 -0
  172. package/dist/client/assets/rfb-CxsXsMUX.js.gz +0 -0
  173. package/dist/client/assets/{sankeyDiagram-P5KCCOFB-D-qpWNza.js → sankeyDiagram-P5KCCOFB-B-_-aZ_y.js} +1 -1
  174. package/dist/client/assets/sankeyDiagram-P5KCCOFB-B-_-aZ_y.js.br +0 -0
  175. package/dist/client/assets/sankeyDiagram-P5KCCOFB-B-_-aZ_y.js.gz +0 -0
  176. package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-DmIxafrB.js → sequenceDiagram-WJ2MYXX4-DT6LDIfx.js} +1 -1
  177. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DT6LDIfx.js.br +0 -0
  178. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DT6LDIfx.js.gz +0 -0
  179. package/dist/client/assets/{sizeCapture-INFHLROL-4MTh3tTt.js → sizeCapture-INFHLROL-CTYerd9j.js} +1 -1
  180. package/dist/client/assets/sizeCapture-INFHLROL-CTYerd9j.js.br +0 -0
  181. package/dist/client/assets/sizeCapture-INFHLROL-CTYerd9j.js.gz +0 -0
  182. package/dist/client/assets/{sparkles-Dk93lHg0.js → sparkles-CgaUBIwH.js} +1 -1
  183. package/dist/client/assets/{stateDiagram-D77RDMKH-CgYgneIv.js → stateDiagram-D77RDMKH-CQbb5x2_.js} +1 -1
  184. package/dist/client/assets/stateDiagram-D77RDMKH-CQbb5x2_.js.br +0 -0
  185. package/dist/client/assets/stateDiagram-D77RDMKH-CQbb5x2_.js.gz +0 -0
  186. package/dist/client/assets/stateDiagram-v2-MP3YSRHH-CJAVXcx1.js +1 -0
  187. package/dist/client/assets/{swimlanes-42K2YHIH-OWA9HFVf.js → swimlanes-42K2YHIH-BHZ4X2Fd.js} +1 -1
  188. package/dist/client/assets/swimlanes-42K2YHIH-BHZ4X2Fd.js.br +0 -0
  189. package/dist/client/assets/swimlanes-42K2YHIH-BHZ4X2Fd.js.gz +0 -0
  190. package/dist/client/assets/swimlanesDiagram-VR7AAH4N-VL2nyI27.js +8 -0
  191. package/dist/client/assets/{timeline-definition-24CTP7MA-BAzx5DyO.js → timeline-definition-24CTP7MA-BrjH6R6w.js} +1 -1
  192. package/dist/client/assets/timeline-definition-24CTP7MA-BrjH6R6w.js.br +0 -0
  193. package/dist/client/assets/timeline-definition-24CTP7MA-BrjH6R6w.js.gz +0 -0
  194. package/dist/client/assets/{triangle-alert-CUIBOwqW.js → triangle-alert-C9IHD_eF.js} +1 -1
  195. package/dist/client/assets/{vennDiagram-4TSXK5OY-CBZ-PhTQ.js → vennDiagram-4TSXK5OY-C9f5jYgk.js} +1 -1
  196. package/dist/client/assets/vennDiagram-4TSXK5OY-C9f5jYgk.js.br +0 -0
  197. package/dist/client/assets/vennDiagram-4TSXK5OY-C9f5jYgk.js.gz +0 -0
  198. package/dist/client/assets/{videoCompression-BCnqCkRK.js → videoCompression-DJaU8exd.js} +1 -1
  199. package/dist/client/assets/videoCompression-DJaU8exd.js.br +0 -0
  200. package/dist/client/assets/videoCompression-DJaU8exd.js.gz +0 -0
  201. package/dist/client/assets/{wardleyDiagram-VM6X3IG4-DbWzC6l5.js → wardleyDiagram-VM6X3IG4-6ljFvtiC.js} +1 -1
  202. package/dist/client/assets/wardleyDiagram-VM6X3IG4-6ljFvtiC.js.br +0 -0
  203. package/dist/client/assets/wardleyDiagram-VM6X3IG4-6ljFvtiC.js.gz +0 -0
  204. package/dist/client/assets/{xychartDiagram-S5SC5T6Z-BmApnTWE.js → xychartDiagram-S5SC5T6Z-BaR2hm3e.js} +1 -1
  205. package/dist/client/assets/xychartDiagram-S5SC5T6Z-BaR2hm3e.js.br +0 -0
  206. package/dist/client/assets/xychartDiagram-S5SC5T6Z-BaR2hm3e.js.gz +0 -0
  207. package/dist/client/index.html +2 -2
  208. package/dist/client/index.html.br +0 -0
  209. package/dist/client/index.html.gz +0 -0
  210. package/dist/client/sw.js +1 -1
  211. package/dist/client/sw.js.br +0 -0
  212. package/dist/client/sw.js.gz +0 -0
  213. package/dist/server/agent/collaborationCli.js +3 -3
  214. package/dist/server/agent/collaborationGroupRoutes.js +0 -2
  215. package/dist/server/agent/collaborationResultPresentation.js +14 -0
  216. package/dist/server/agent/collaborationService.js +3 -1
  217. package/dist/server/agent/collaborationStore.js +12 -21
  218. package/dist/server/agent/collaborationTaskBundles.js +2 -2
  219. package/dist/server/agent/collaborationTaskCli.js +4 -4
  220. package/dist/server/agent/collaborationTaskStore.js +87 -25
  221. package/dist/server/agent/collaborationTaskWorkspace.js +66 -1
  222. package/dist/server/agent/collaborationTeam.js +16 -0
  223. package/dist/server/agent/executionArchives.js +52 -0
  224. package/dist/server/entry.js +6 -1
  225. package/dist/server/federation/runtime.js +24 -6
  226. package/dist/server/routes/computer.js +198 -0
  227. package/dist/server/routes/computerTarget.js +23 -0
  228. package/dist/server/routes/guacamoleProtocol.js +55 -0
  229. package/dist/server/routes/rdp.js +162 -0
  230. package/dist/server/routes/terminal.js +375 -360
  231. package/dist/server/shared/computerPreferences.js +29 -0
  232. package/dist/server/utils/computerCredentials.js +88 -0
  233. package/dist/server/utils/sharedSampling.js +93 -0
  234. package/dist/server/utils/terminalOutputDelivery.js +8 -1
  235. package/dist/server/utils/tmuxMetadataWriter.js +61 -0
  236. package/package.json +4 -1
  237. package/runtime-manifest.json +4 -4
  238. package/dist/client/assets/App-BiLjlrk-.js +0 -171
  239. package/dist/client/assets/App-BiLjlrk-.js.br +0 -0
  240. package/dist/client/assets/App-BiLjlrk-.js.gz +0 -0
  241. package/dist/client/assets/ChangeWalkthroughPanel-DXprT-7m.js.br +0 -0
  242. package/dist/client/assets/ChangeWalkthroughPanel-DXprT-7m.js.gz +0 -0
  243. package/dist/client/assets/DagPlayground-DZcUunT5.js.br +0 -0
  244. package/dist/client/assets/DagPlayground-DZcUunT5.js.gz +0 -0
  245. package/dist/client/assets/DiffLab-Brv4DCTb.js.br +0 -0
  246. package/dist/client/assets/DiffLab-Brv4DCTb.js.gz +0 -0
  247. package/dist/client/assets/DiffReview-BuLu1qCi.js.br +0 -0
  248. package/dist/client/assets/DiffReview-BuLu1qCi.js.gz +0 -0
  249. package/dist/client/assets/DiffReviewLab-DSGamuAD.js.br +0 -0
  250. package/dist/client/assets/DiffReviewLab-DSGamuAD.js.gz +0 -0
  251. package/dist/client/assets/DiffViewer-BUCYxg27.js.br +0 -0
  252. package/dist/client/assets/DiffViewer-BUCYxg27.js.gz +0 -0
  253. package/dist/client/assets/ModelPreview-Bnlo-oVE.js.br +0 -0
  254. package/dist/client/assets/ModelPreview-Bnlo-oVE.js.gz +0 -0
  255. package/dist/client/assets/RightSidebar-r_3Lk5Rp.js +0 -1678
  256. package/dist/client/assets/RightSidebar-r_3Lk5Rp.js.br +0 -0
  257. package/dist/client/assets/RightSidebar-r_3Lk5Rp.js.gz +0 -0
  258. package/dist/client/assets/abnfDiagram-VCTEODGH-K6Ppkrbt.js.br +0 -0
  259. package/dist/client/assets/abnfDiagram-VCTEODGH-K6Ppkrbt.js.gz +0 -0
  260. package/dist/client/assets/app-CATnTUZF.js +0 -35
  261. package/dist/client/assets/app-CATnTUZF.js.br +0 -0
  262. package/dist/client/assets/app-CATnTUZF.js.gz +0 -0
  263. package/dist/client/assets/arc-Co2OQqtH.js.br +0 -0
  264. package/dist/client/assets/arc-Co2OQqtH.js.gz +0 -0
  265. package/dist/client/assets/architectureDiagram-5GKGNRK7-BQnl7tVF.js.br +0 -0
  266. package/dist/client/assets/architectureDiagram-5GKGNRK7-BQnl7tVF.js.gz +0 -0
  267. package/dist/client/assets/blockDiagram-I7D4REHJ-Bm5D5-qq.js.br +0 -0
  268. package/dist/client/assets/blockDiagram-I7D4REHJ-Bm5D5-qq.js.gz +0 -0
  269. package/dist/client/assets/c4Diagram-7LVT6UL2-B9oPpey8.js.br +0 -0
  270. package/dist/client/assets/c4Diagram-7LVT6UL2-B9oPpey8.js.gz +0 -0
  271. package/dist/client/assets/channel-Czv2W4qA.js +0 -1
  272. package/dist/client/assets/chunk-F27PBJKO-CJ1rotBe.js.br +0 -0
  273. package/dist/client/assets/chunk-F27PBJKO-CJ1rotBe.js.gz +0 -0
  274. package/dist/client/assets/chunk-IMKFNOWR-UYKZ5uOG.js.br +0 -0
  275. package/dist/client/assets/chunk-IMKFNOWR-UYKZ5uOG.js.gz +0 -0
  276. package/dist/client/assets/chunk-SVP7TREG-DCTDYa1E.js.br +0 -0
  277. package/dist/client/assets/chunk-SVP7TREG-DCTDYa1E.js.gz +0 -0
  278. package/dist/client/assets/chunk-TICWLB2K-wHiaOsBh.js.br +0 -0
  279. package/dist/client/assets/chunk-TICWLB2K-wHiaOsBh.js.gz +0 -0
  280. package/dist/client/assets/classDiagram-ZZMXUADV-BdvOOL9k.js +0 -1
  281. package/dist/client/assets/classDiagram-v2-VYDZK3BY-BdvOOL9k.js +0 -1
  282. package/dist/client/assets/cose-bilkent-JH36ORCC-DnhlPBLr.js.br +0 -0
  283. package/dist/client/assets/cose-bilkent-JH36ORCC-DnhlPBLr.js.gz +0 -0
  284. package/dist/client/assets/cynefin-OW5HDTMX-BsjzNqJZ.js.br +0 -0
  285. package/dist/client/assets/cynefin-OW5HDTMX-BsjzNqJZ.js.gz +0 -0
  286. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-sVUb0zY5.js.br +0 -0
  287. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-sVUb0zY5.js.gz +0 -0
  288. package/dist/client/assets/dagre-GXQ25YYZ-YErk8S-c.js.br +0 -0
  289. package/dist/client/assets/dagre-GXQ25YYZ-YErk8S-c.js.gz +0 -0
  290. package/dist/client/assets/diagram-S7CK7UJ4-BaHbV5ho.js.br +0 -0
  291. package/dist/client/assets/diagram-S7CK7UJ4-BaHbV5ho.js.gz +0 -0
  292. package/dist/client/assets/diagram-UQ7AKVKN-BocCYx3V.js.br +0 -0
  293. package/dist/client/assets/diagram-UQ7AKVKN-BocCYx3V.js.gz +0 -0
  294. package/dist/client/assets/diagram-VSXAHHWV-ZMTYwWOQ.js.br +0 -0
  295. package/dist/client/assets/diagram-VSXAHHWV-ZMTYwWOQ.js.gz +0 -0
  296. package/dist/client/assets/diagram-VX7I27RA-xmk_EY1h.js.br +0 -0
  297. package/dist/client/assets/diagram-VX7I27RA-xmk_EY1h.js.gz +0 -0
  298. package/dist/client/assets/diagram-Z3DM3KII-DKDY_cG6.js.br +0 -0
  299. package/dist/client/assets/diagram-Z3DM3KII-DKDY_cG6.js.gz +0 -0
  300. package/dist/client/assets/ebnfDiagram-PWID7BFC-C_0z-Jx9.js.br +0 -0
  301. package/dist/client/assets/ebnfDiagram-PWID7BFC-C_0z-Jx9.js.gz +0 -0
  302. package/dist/client/assets/erDiagram-RLTQ6QDP-DHUaG7mt.js.br +0 -0
  303. package/dist/client/assets/erDiagram-RLTQ6QDP-DHUaG7mt.js.gz +0 -0
  304. package/dist/client/assets/flowDiagram-HODETNUW-BMNsdMBD.js.br +0 -0
  305. package/dist/client/assets/flowDiagram-HODETNUW-BMNsdMBD.js.gz +0 -0
  306. package/dist/client/assets/ganttDiagram-EL5Y4UJY-BH1AiKuF.js.br +0 -0
  307. package/dist/client/assets/ganttDiagram-EL5Y4UJY-BH1AiKuF.js.gz +0 -0
  308. package/dist/client/assets/gestureArbiter-DvupkPra.js.br +0 -0
  309. package/dist/client/assets/gestureArbiter-DvupkPra.js.gz +0 -0
  310. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-DAgV0MV-.js.br +0 -0
  311. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-DAgV0MV-.js.gz +0 -0
  312. package/dist/client/assets/index-Bi9A6QUP.css +0 -1
  313. package/dist/client/assets/index-Bi9A6QUP.css.br +0 -0
  314. package/dist/client/assets/index-Bi9A6QUP.css.gz +0 -0
  315. package/dist/client/assets/ishikawaDiagram-5VMMS53U-BpZwsQvw.js.br +0 -0
  316. package/dist/client/assets/ishikawaDiagram-5VMMS53U-BpZwsQvw.js.gz +0 -0
  317. package/dist/client/assets/journeyDiagram-3NMN7TZE-DXEeFLvw.js.br +0 -0
  318. package/dist/client/assets/journeyDiagram-3NMN7TZE-DXEeFLvw.js.gz +0 -0
  319. package/dist/client/assets/kanban-definition-UXKFOSKX-CV9ivL5u.js.br +0 -0
  320. package/dist/client/assets/kanban-definition-UXKFOSKX-CV9ivL5u.js.gz +0 -0
  321. package/dist/client/assets/layout-DsKKsJKX.js.br +0 -0
  322. package/dist/client/assets/layout-DsKKsJKX.js.gz +0 -0
  323. package/dist/client/assets/linear-Dl9MDNV3.js.br +0 -0
  324. package/dist/client/assets/linear-Dl9MDNV3.js.gz +0 -0
  325. package/dist/client/assets/mermaid.core-CxZFe3as.js.br +0 -0
  326. package/dist/client/assets/mermaid.core-CxZFe3as.js.gz +0 -0
  327. package/dist/client/assets/mindmap-definition-YA3MSWOX-C3ormYdL.js.br +0 -0
  328. package/dist/client/assets/mindmap-definition-YA3MSWOX-C3ormYdL.js.gz +0 -0
  329. package/dist/client/assets/passwordBootstrap-a8z6jJNY.js.br +0 -0
  330. package/dist/client/assets/passwordBootstrap-a8z6jJNY.js.gz +0 -0
  331. package/dist/client/assets/pegDiagram-XKGWAZYB-CG0wNylH.js.br +0 -0
  332. package/dist/client/assets/pegDiagram-XKGWAZYB-CG0wNylH.js.gz +0 -0
  333. package/dist/client/assets/pieDiagram-E7YTZNPT-D2-ydqyX.js.br +0 -0
  334. package/dist/client/assets/pieDiagram-E7YTZNPT-D2-ydqyX.js.gz +0 -0
  335. package/dist/client/assets/quadrantDiagram-AXDQQJYC-CK3fRC18.js.br +0 -0
  336. package/dist/client/assets/quadrantDiagram-AXDQQJYC-CK3fRC18.js.gz +0 -0
  337. package/dist/client/assets/railroadDiagram-O6MQD6OU-V4EFlydu.js.br +0 -0
  338. package/dist/client/assets/railroadDiagram-O6MQD6OU-V4EFlydu.js.gz +0 -0
  339. package/dist/client/assets/referenceLongPress-BjmYd1vB.js.br +0 -0
  340. package/dist/client/assets/referenceLongPress-BjmYd1vB.js.gz +0 -0
  341. package/dist/client/assets/requirementDiagram-BXWQKSXE-Di1_tyAX.js.br +0 -0
  342. package/dist/client/assets/requirementDiagram-BXWQKSXE-Di1_tyAX.js.gz +0 -0
  343. package/dist/client/assets/sankeyDiagram-P5KCCOFB-D-qpWNza.js.br +0 -0
  344. package/dist/client/assets/sankeyDiagram-P5KCCOFB-D-qpWNza.js.gz +0 -0
  345. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DmIxafrB.js.br +0 -0
  346. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DmIxafrB.js.gz +0 -0
  347. package/dist/client/assets/sizeCapture-INFHLROL-4MTh3tTt.js.br +0 -0
  348. package/dist/client/assets/sizeCapture-INFHLROL-4MTh3tTt.js.gz +0 -0
  349. package/dist/client/assets/stateDiagram-D77RDMKH-CgYgneIv.js.br +0 -0
  350. package/dist/client/assets/stateDiagram-D77RDMKH-CgYgneIv.js.gz +0 -0
  351. package/dist/client/assets/stateDiagram-v2-MP3YSRHH-CvPf0fxf.js +0 -1
  352. package/dist/client/assets/swimlanes-42K2YHIH-OWA9HFVf.js.br +0 -0
  353. package/dist/client/assets/swimlanes-42K2YHIH-OWA9HFVf.js.gz +0 -0
  354. package/dist/client/assets/swimlanesDiagram-VR7AAH4N-CZ_OauyW.js +0 -8
  355. package/dist/client/assets/timeline-definition-24CTP7MA-BAzx5DyO.js.br +0 -0
  356. package/dist/client/assets/timeline-definition-24CTP7MA-BAzx5DyO.js.gz +0 -0
  357. package/dist/client/assets/vennDiagram-4TSXK5OY-CBZ-PhTQ.js.br +0 -0
  358. package/dist/client/assets/vennDiagram-4TSXK5OY-CBZ-PhTQ.js.gz +0 -0
  359. package/dist/client/assets/videoCompression-BCnqCkRK.js.br +0 -0
  360. package/dist/client/assets/videoCompression-BCnqCkRK.js.gz +0 -0
  361. package/dist/client/assets/wardleyDiagram-VM6X3IG4-DbWzC6l5.js.br +0 -0
  362. package/dist/client/assets/wardleyDiagram-VM6X3IG4-DbWzC6l5.js.gz +0 -0
  363. package/dist/client/assets/xychartDiagram-S5SC5T6Z-BmApnTWE.js.br +0 -0
  364. package/dist/client/assets/xychartDiagram-S5SC5T6Z-BmApnTWE.js.gz +0 -0
@@ -1,3 +1,4 @@
1
+ import { ensureTeam } from '../agent/collaborationTeam.js';
1
2
  import { androidRecordings } from '../android/recording.js';
2
3
  import { assertPeerRegistrationAuthority } from '../agent/collaborationPeerTransport.js';
3
4
  import { progressRoutes } from '../notifications/progressRoutes.js';
@@ -5,6 +6,8 @@ import { ensureNodePty } from '../utils/ensureNodePty.js';
5
6
  import { PtySpawnBackoff, PtySpawnDeferredError } from '../utils/ptySpawnBackoff.js';
6
7
  import { collaborationGroupRoutes } from '../agent/collaborationGroupRoutes.js';
7
8
  import { TerminalClientAttachment } from '../utils/terminalClientAttachment.js';
9
+ import { SharedSessionSampler, SharedSnapshotCache } from '../utils/sharedSampling.js';
10
+ import { batchTmuxOptions, TmuxMetadataWriter } from '../utils/tmuxMetadataWriter.js';
8
11
  import { redrawTmuxClient } from '../utils/tmuxClientRedraw.js';
9
12
  import { TmuxInitialScreen } from '../utils/tmuxInitialScreen.js';
10
13
  import { TerminalOutputDelivery, resolveTerminalReplayCursor } from '../utils/terminalOutputDelivery.js';
@@ -30,11 +33,13 @@ import { getOnboardingServerUrl } from '../onboardingServer.js';
30
33
  import { inspectLinuxInotifyUsage, isWatchResourceExhaustion } from '../utils/fileWatchPolicy.js';
31
34
  import { writeResponseChunk } from '../utils/httpResponse.js';
32
35
  import { getFocusSequence, removeClientFocusState, scanFocusTrackingMode, setClientFocusState, } from '../utils/tmuxFocus.js';
33
- import { extractProgramLabelFromArgs, normalizeProgramName, normalizeTmuxMetadataProgram, selectTmuxForegroundProgram, tmuxMetadataChanged, } from '../utils/tmuxProgramDetection.js';
36
+ import { extractProgramLabelFromArgs, normalizeProgramName, normalizeTmuxMetadataProgram, selectTmuxForegroundProgram, } from '../utils/tmuxProgramDetection.js';
34
37
  import { agentBySlug, buildResumeCommand, detectAgentFromCommand, inferResumeSessionId, listAgents, } from '../agent/registry.js';
35
38
  import { applyAgentEvent, agentEventMatchesCurrentAgent, defaultAgentSessionState, parseAgentEvent, } from '../agent/session.js';
36
39
  import { canRestoreDeadAgentShell, isConfirmedAgentResumeProcess, normalizePersistedAgentResumeInfo, } from '../agent/resumePersistence.js';
37
40
  import { AgentResumeHistoryStore } from '../agent/resumeHistory.js';
41
+ import { completedExecution, ExecutionArchiveStore } from '../agent/executionArchives.js';
42
+ import { archiveTaskWorkspace, restoreTaskWorkspace, snapshotTaskWorkspace } from '../agent/collaborationTaskWorkspace.js';
38
43
  import { isTmuxRecoveryCandidate } from '../utils/tmuxRecoveryCandidate.js';
39
44
  import { AutomationStore, normalizeAutomationSchedule } from '../agent/automationStore.js';
40
45
  import { buildBracketedSubmitBytes, canDeliverPromptToAgent } from '../agent/promptDelivery.js';
@@ -181,6 +186,8 @@ function generateFrontendSessionId() {
181
186
  const GLOBAL_SESSION_STATE_FILE = `${TERMDOCK_DIR}/global-session-state.json`;
182
187
  const CLIENT_STATES_FILE = `${TERMDOCK_DIR}/client-states.json`; // 保留用于迁移
183
188
  const agentResumeHistory = new AgentResumeHistoryStore(`${TERMDOCK_DIR}/agent-resume-history.json`);
189
+ const executionArchives = new ExecutionArchiveStore(`${TERMDOCK_DIR}/execution-archives.json`);
190
+ let executionArchiveMutation = Promise.resolve();
184
191
  const TMUX_RECOVERY_STATE_FILE = `${TERMDOCK_DIR}/tmux-recovery-state.json`;
185
192
  const tmuxLifecycle = new TmuxLifecycleCoordinator();
186
193
  const intentionallyDeletingTmuxSessions = new Set();
@@ -194,6 +201,8 @@ let globalSessionStateReloadTimer = null;
194
201
  // reconciliation, etc.). Replaces the 5-second poll on the front-end. ──
195
202
  const controlClients = new Map();
196
203
  let latestSessionInventory = null;
204
+ // Only authoritative observations may remove persisted tmux sessions.
205
+ const inventoryTmuxLiveness = new WeakMap();
197
206
  let latestSessionInventoryAt = 0;
198
207
  let sessionInventoryBuildPromise = null;
199
208
  let broadcastInventorySeq = 0;
@@ -205,13 +214,12 @@ const inventoryOpenLocks = new Map();
205
214
  const CONTROL_BROADCAST_COALESCE_MS = 50;
206
215
  const SESSION_INVENTORY_CACHE_TTL_MS = 1500;
207
216
  async function getSessionInventorySnapshot(options = {}) {
217
+ if (sessionInventoryBuildPromise)
218
+ return sessionInventoryBuildPromise;
208
219
  const now = Date.now();
209
220
  if (!options.refresh && latestSessionInventory && now - latestSessionInventoryAt < SESSION_INVENTORY_CACHE_TTL_MS) {
210
221
  return latestSessionInventory;
211
222
  }
212
- if (!options.refresh && sessionInventoryBuildPromise) {
213
- return sessionInventoryBuildPromise;
214
- }
215
223
  const promise = buildSessionInventory()
216
224
  .then((inventory) => {
217
225
  latestSessionInventory = inventory;
@@ -982,22 +990,6 @@ function buildRuntimeTmuxMetadata(input) {
982
990
  });
983
991
  return { program, cwd, label, rawArgs: input.rawArgs ?? null };
984
992
  }
985
- function maybeRepairTmuxOptions(sessionName, current, next) {
986
- const currentSnapshot = {
987
- program: current.program ?? null,
988
- cwd: current.cwd ?? null,
989
- label: current.label ?? '',
990
- };
991
- if (!tmuxMetadataChanged(currentSnapshot, next)) {
992
- return;
993
- }
994
- void setTmuxOptions(sessionName, {
995
- '@termdock-label': next.label,
996
- '@termdock-program': next.program ?? '',
997
- '@termdock-cwd': next.cwd ?? '',
998
- '@termdock-last-active-at': String(Date.now()),
999
- });
1000
- }
1001
993
  function makeTerminalSessionPayload(backendSessionId, session, cols = 80, rows = 24) {
1002
994
  return {
1003
995
  sessionId: backendSessionId,
@@ -1719,7 +1711,7 @@ async function resolveCollaborationRoute(frontendSessionId) {
1719
1711
  async function refreshCollaborationAgentIdentity(backendSessionId, session) {
1720
1712
  let activeProgram = null;
1721
1713
  if (session.mode === 'shell') {
1722
- activeProgram = await detectShellActiveProgram(session);
1714
+ activeProgram = await getCachedShellActiveProgram(backendSessionId, session);
1723
1715
  }
1724
1716
  else if (session.tmuxSessionName) {
1725
1717
  const layout = await getCachedTmuxLayout(session.tmuxSessionName);
@@ -1736,7 +1728,7 @@ async function refreshCollaborationAgentIdentity(backendSessionId, session) {
1736
1728
  function deliverCollaborationInboxWhenAgentReady(frontendSessionId) {
1737
1729
  collaborationDeliveryWorker.wake(frontendSessionId);
1738
1730
  }
1739
- async function spawnCollaborationAgentSession(req, group, sourceSessionId, input) {
1731
+ async function spawnCollaborationAgentSession(req, group, sourceSessionId, input, bootstrapRole) {
1740
1732
  const agentSlug = typeof input.agentSlug === 'string' ? input.agentSlug.trim().toLowerCase() : '';
1741
1733
  const launchers = await listDetectedAgentLaunchers();
1742
1734
  const launcher = launchers.find((candidate) => candidate.slug === agentSlug);
@@ -1768,11 +1760,26 @@ async function spawnCollaborationAgentSession(req, group, sourceSessionId, input
1768
1760
  const backend = terminalSessions.get(opened.terminalSession.sessionId);
1769
1761
  if (!backend)
1770
1762
  throw new HttpStatusError(500, 'Agent Session 创建失败', 'COLLAB_AGENT_SESSION_FAILED');
1763
+ const latestGroup = collaborationStore.getGroup(group.id);
1764
+ if (!latestGroup || latestGroup.deleted) {
1765
+ const created = globalSessionState.sessions.find(record => record.sessionId === frontendSessionId);
1766
+ if (created?.tmuxSessionName)
1767
+ await destroyTmuxSessionSafely(created.tmuxSessionName);
1768
+ else {
1769
+ cleanupSession(opened.terminalSession.sessionId, { killProcess: true });
1770
+ globalSessionState.sessions = globalSessionState.sessions.filter(record => record.sessionId !== frontendSessionId);
1771
+ }
1772
+ await persistGlobalStateNow();
1773
+ broadcastClientState();
1774
+ throw new HttpStatusError(409, '协作组已删除,未启动 Agent', 'COLLAB_GROUP_CHANGED');
1775
+ }
1771
1776
  const updatedGroup = collaborationStore.save({
1772
- id: group.id,
1773
- name: group.name,
1774
- sessionIds: [...group.sessionIds, frontendSessionId],
1777
+ id: latestGroup.id,
1778
+ name: latestGroup.name,
1779
+ sessionIds: [...latestGroup.sessionIds, frontendSessionId],
1775
1780
  });
1781
+ if (bootstrapRole)
1782
+ collaborationStore.setRole({ groupId: group.id, sessionId: frontendSessionId, role: bootstrapRole });
1776
1783
  writeTerminalInput(backend, `${buildCollaborationSpawnCommand({ slug: launcher.slug, command: launcher.command })}\r`);
1777
1784
  const requestedTask = typeof input.task === 'string' ? input.task.trim().slice(0, 20_000) : '';
1778
1785
  collaborationStore.send({
@@ -2416,9 +2423,11 @@ async function observeTmuxServerGeneration(liveTmuxSessions) {
2416
2423
  async function buildSessionInventory() {
2417
2424
  const tmuxStatus = await getTmuxStatus();
2418
2425
  let liveTmuxSessions = [];
2426
+ let authoritativeTmuxList = false;
2419
2427
  if (tmuxStatus.available) {
2420
2428
  try {
2421
2429
  liveTmuxSessions = await listLiveTmuxInventorySessions();
2430
+ authoritativeTmuxList = true;
2422
2431
  }
2423
2432
  catch (error) {
2424
2433
  const errorMessage = getErrorMessage(error);
@@ -2434,12 +2443,12 @@ async function buildSessionInventory() {
2434
2443
  return tmux;
2435
2444
  }
2436
2445
  try {
2437
- const metadata = await resolveLiveTmuxMetadata(tmux.name);
2446
+ const metadata = await resolveLiveTmuxMetadata(tmux.name, true);
2438
2447
  if (!metadata) {
2439
2448
  return tmux;
2440
2449
  }
2441
2450
  backfillAgentResumeFromTmuxProcess(tmux.name, metadata.rawArgs);
2442
- maybeRepairTmuxOptions(tmux.name, tmux, metadata);
2451
+ syncDynamicTmuxMetadata({ tmuxSessionName: tmux.name, program: metadata.program, cwd: metadata.cwd });
2443
2452
  return {
2444
2453
  ...tmux,
2445
2454
  program: metadata.program,
@@ -2585,13 +2594,19 @@ async function buildSessionInventory() {
2585
2594
  lastActiveAt: tmux.lastActiveAt,
2586
2595
  };
2587
2596
  });
2588
- return {
2597
+ const inventory = {
2589
2598
  clientSessions,
2590
2599
  tmuxSessions,
2591
2600
  tmuxStatus,
2592
2601
  tmuxRecovery: getTmuxRecoveryView(),
2593
2602
  updatedAt: Date.now(),
2594
2603
  };
2604
+ if (authoritativeTmuxList) {
2605
+ const names = new Set(liveTmuxSessions.map(session => session.name));
2606
+ inventoryTmuxLiveness.set(inventory, names);
2607
+ dynamicTmuxMetadata.retain(names);
2608
+ }
2609
+ return inventory;
2595
2610
  }
2596
2611
  // ── end tmux user-option helpers ──
2597
2612
  async function captureTmuxPane(sessionName) {
@@ -3603,6 +3618,8 @@ try {
3603
3618
  catch { /* plugin loading must never block startup */ }
3604
3619
  let processSnapshot = null;
3605
3620
  let processSnapshotPromise = null;
3621
+ const ttyProcessSnapshots = new SharedSnapshotCache(PROCESS_SNAPSHOT_CACHE_TTL_MS);
3622
+ const shellProgramSnapshots = new SharedSnapshotCache(ACTIVE_PROGRAM_POLL_INTERVAL);
3606
3623
  function parseProcessSnapshot(stdout) {
3607
3624
  return stdout
3608
3625
  .split('\n')
@@ -3670,26 +3687,28 @@ async function resolveTmuxPaneProgram(pane) {
3670
3687
  const psArgs = pane.tty
3671
3688
  ? ['-t', pane.tty.replace(/^\/dev\//, ''), '-o', 'pid=,ppid=,pgid=,tpgid=,stat=,comm=,args=']
3672
3689
  : ['-o', 'pid=,ppid=,pgid=,tpgid=,stat=,comm=,args='];
3673
- const { stdout } = await execFileAsync('ps', psArgs, { timeout: 3000, maxBuffer: 512 * 1024 });
3674
- rows = stdout
3675
- .split('\n')
3676
- .map((line) => line.trim())
3677
- .filter(Boolean)
3678
- .map((line) => {
3679
- const match = line.match(/^(\d+)\s+(\d+)\s+(-?\d+)\s+(-?\d+)\s+(\S+)\s+(\S+)\s+(.+)$/);
3680
- if (!match)
3681
- return null;
3682
- return {
3683
- pid: Number.parseInt(match[1] || '0', 10),
3684
- ppid: Number.parseInt(match[2] || '0', 10),
3685
- pgid: Number.parseInt(match[3] || '0', 10),
3686
- tpgid: Number.parseInt(match[4] || '0', 10),
3687
- stat: match[5] || '',
3688
- comm: match[6] || '',
3689
- args: match[7]?.trim() || '',
3690
- };
3691
- })
3692
- .filter((row) => row !== null);
3690
+ rows = await ttyProcessSnapshots.get(pane.tty || '*', async () => {
3691
+ const result = await execFileAsync('ps', psArgs, { timeout: 3000, maxBuffer: 512 * 1024 });
3692
+ return result.stdout
3693
+ .split('\n')
3694
+ .map((line) => line.trim())
3695
+ .filter(Boolean)
3696
+ .map((line) => {
3697
+ const match = line.match(/^(\d+)\s+(\d+)\s+(-?\d+)\s+(-?\d+)\s+(\S+)\s+(\S+)\s+(.+)$/);
3698
+ if (!match)
3699
+ return null;
3700
+ return {
3701
+ pid: Number.parseInt(match[1] || '0', 10),
3702
+ ppid: Number.parseInt(match[2] || '0', 10),
3703
+ pgid: Number.parseInt(match[3] || '0', 10),
3704
+ tpgid: Number.parseInt(match[4] || '0', 10),
3705
+ stat: match[5] || '',
3706
+ comm: match[6] || '',
3707
+ args: match[7]?.trim() || '',
3708
+ };
3709
+ })
3710
+ .filter((row) => row !== null);
3711
+ });
3693
3712
  }
3694
3713
  const selected = selectTmuxForegroundProgram({
3695
3714
  panePid: pane.pid,
@@ -3745,8 +3764,8 @@ function getCwdFromTmuxLayout(layout) {
3745
3764
  const activePane = activeWindow?.panes.find((pane) => pane.id === layout.activePaneId);
3746
3765
  return activePane?.currentPath || null;
3747
3766
  }
3748
- async function resolveLiveTmuxMetadata(tmuxSessionName) {
3749
- const layout = await getTmuxLayout(tmuxSessionName);
3767
+ async function resolveLiveTmuxMetadata(tmuxSessionName, cached = false) {
3768
+ const layout = cached ? await getCachedTmuxLayout(tmuxSessionName) : await getTmuxLayout(tmuxSessionName);
3750
3769
  const activePane = getActivePaneFromLayout(layout);
3751
3770
  if (!activePane) {
3752
3771
  return null;
@@ -3811,31 +3830,87 @@ function findFriendlyNameForTmuxSession(tmuxSessionName) {
3811
3830
  }
3812
3831
  return null;
3813
3832
  }
3814
- // Push the latest dynamic metadata (program / cwd / label / last-active-at)
3815
- // onto the tmux session as user options. Caller passes the previous metadata
3816
- // snapshot and last-active-write timestamp so repeated polls with no change
3817
- // skip the tmux write entirely. Returns the new snapshot + write timestamp.
3818
- const TERMDOCK_LAST_ACTIVE_REFRESH_MS = 30_000;
3833
+ // Metadata belongs to the tmux session, including writes from inventory scans.
3834
+ // Batch options into one tmux process rather than four parallel child processes.
3835
+ const dynamicTmuxMetadata = new TmuxMetadataWriter(async (name, options) => {
3836
+ await runTmux(batchTmuxOptions(name, options));
3837
+ });
3819
3838
  function syncDynamicTmuxMetadata(input) {
3820
- const { tmuxSessionName, program, cwd, previousMetadata, lastActiveWriteAt } = input;
3821
- const metadata = buildRuntimeTmuxMetadata({ tmuxSessionName, program, cwd });
3822
- const now = Date.now();
3823
- if (!tmuxMetadataChanged(previousMetadata, metadata)) {
3824
- // Cheap path: refresh last-active-at at most every 30 s so external
3825
- // tools see the session as alive without flooding tmux every 500 ms.
3826
- if (now - lastActiveWriteAt >= TERMDOCK_LAST_ACTIVE_REFRESH_MS) {
3827
- void setTmuxOption(tmuxSessionName, '@termdock-last-active-at', String(now));
3828
- return { ...metadata, lastActiveWriteAt: now };
3829
- }
3830
- return { ...metadata, lastActiveWriteAt };
3831
- }
3832
- void setTmuxOptions(tmuxSessionName, {
3833
- '@termdock-label': metadata.label,
3834
- '@termdock-program': metadata.program ?? '',
3835
- '@termdock-cwd': metadata.cwd ?? '',
3836
- '@termdock-last-active-at': String(now),
3839
+ const metadata = buildRuntimeTmuxMetadata(input);
3840
+ void dynamicTmuxMetadata.sync(input.tmuxSessionName, {
3841
+ program: metadata.program, cwd: metadata.cwd, label: metadata.label,
3842
+ }).catch(error => {
3843
+ console.warn(`[tmux] metadata write failed for ${input.tmuxSessionName}: ${getErrorMessage(error)}`);
3837
3844
  });
3838
- return { ...metadata, lastActiveWriteAt: now };
3845
+ }
3846
+ function getCachedShellActiveProgram(sessionId, session) {
3847
+ return shellProgramSnapshots.get(`${sessionId}:${session.ptyProcess.pid}`, () => detectShellActiveProgram(session));
3848
+ }
3849
+ const terminalSamplers = new Map();
3850
+ const sampledProgramSessions = new WeakSet();
3851
+ function subscribeTerminalSampling(sessionId, session, observer) {
3852
+ let shared = terminalSamplers.get(sessionId);
3853
+ if (!shared || shared.session !== session) {
3854
+ shared?.sampler.stop();
3855
+ shared = {
3856
+ session,
3857
+ sampler: new SharedSessionSampler(() => sampleTerminalSession(sessionId, session), () => session.mode === 'tmux' ? TMUX_POLL_INTERVAL : ACTIVE_PROGRAM_POLL_INTERVAL, error => console.warn(`[terminal-sampling] ${sessionId}: ${getErrorMessage(error)}`)),
3858
+ };
3859
+ terminalSamplers.set(sessionId, shared);
3860
+ }
3861
+ return shared.sampler.subscribe(observer);
3862
+ }
3863
+ async function sampleTerminalSession(sessionId, session) {
3864
+ const tmuxName = session.tmuxSessionName;
3865
+ const layout = session.mode === 'tmux' && tmuxName ? await getCachedTmuxLayout(tmuxName) : null;
3866
+ const activePane = layout ? getActivePaneFromLayout(layout) : null;
3867
+ const resolved = activePane ? await resolveTmuxPaneProgram(activePane) : null;
3868
+ const activeProgram = layout
3869
+ ? (resolved ? { ...resolved, updatedAt: Date.now() } : getActiveProgramFromTmuxLayout(layout))
3870
+ : await getCachedShellActiveProgram(sessionId, session);
3871
+ // A deletion or task switch during a slow subprocess invalidates its result.
3872
+ if (terminalSessions.get(sessionId) !== session || session.tmuxSessionName !== tmuxName)
3873
+ return [];
3874
+ const programSignature = (ap) => JSON.stringify(ap
3875
+ ? { command: ap.command, source: ap.source, rawArgs: ap.rawArgs } : null);
3876
+ const programChanged = programSignature(activeProgram) !== programSignature(session.activeProgram);
3877
+ session.activeProgram = activeProgram;
3878
+ if (programChanged)
3879
+ persistActiveProgramBinding(sessionId, activeProgram?.command);
3880
+ if (programChanged || !sampledProgramSessions.has(session) || session.agentLeftAt !== null)
3881
+ syncAgentIdentity(sessionId, session);
3882
+ sampledProgramSessions.add(session);
3883
+ const events = [{
3884
+ type: 'active-program',
3885
+ activeProgram: activeProgram?.command ?? null,
3886
+ activeProgramRaw: activeProgram?.rawArgs ?? null,
3887
+ activeProgramSource: activeProgram?.source ?? null,
3888
+ }];
3889
+ if (!layout)
3890
+ return events;
3891
+ const cwd = getCwdFromTmuxLayout(layout);
3892
+ if (cwd && cwd !== session.cwd) {
3893
+ session.cwd = cwd;
3894
+ if (updateGlobalBindingForBackendSession(sessionId, { cwd, lastActivity: session.lastActivity }))
3895
+ schedulePersistGlobalState();
3896
+ refreshGitStatus(sessionId, session, { minIntervalMs: 0 });
3897
+ }
3898
+ if (activePane) {
3899
+ if (activePane.title)
3900
+ session.lastOscTitle = activePane.title;
3901
+ const state = activePane.command && !shellNamesBackend.has(activePane.command) ? 'running' : 'idle';
3902
+ if (session.lastPromptState === 'running' && state === 'idle')
3903
+ refreshGitStatus(sessionId, session);
3904
+ session.lastPromptState = state;
3905
+ }
3906
+ syncDynamicTmuxMetadata({ tmuxSessionName: tmuxName, program: activeProgram?.command ?? null, cwd: session.cwd ?? null });
3907
+ events.push({ type: 'cwd', cwd: session.cwd ?? null });
3908
+ if (session.lastOscTitle)
3909
+ events.push({ type: 'shell-title', title: session.lastOscTitle });
3910
+ if (session.lastPromptState)
3911
+ events.push({ type: 'prompt-state', state: session.lastPromptState });
3912
+ events.push({ type: 'tmux-layout', layout });
3913
+ return events;
3839
3914
  }
3840
3915
  async function detectShellActiveProgram(session) {
3841
3916
  const pid = getPtyProcessPid(session.ptyProcess);
@@ -4420,6 +4495,9 @@ function cleanupSession(sessionId, options) {
4420
4495
  if (!session) {
4421
4496
  return;
4422
4497
  }
4498
+ const sampler = terminalSamplers.get(sessionId);
4499
+ sampler?.sampler.stop();
4500
+ terminalSamplers.delete(sessionId);
4423
4501
  session.dataDisposable?.dispose();
4424
4502
  session.exitDisposable?.dispose();
4425
4503
  cancelLongRunningAutoTitle(session);
@@ -5430,6 +5508,7 @@ router.delete('/tmux/recovery', async (_req, res) => {
5430
5508
  async function destroyTmuxSessionSafely(rawName) {
5431
5509
  return tmuxLifecycle.run(`destroy:${rawName}`, async () => {
5432
5510
  intentionallyDeletingTmuxSessions.add(rawName);
5511
+ dynamicTmuxMetadata.forget(rawName);
5433
5512
  try {
5434
5513
  const affectedSessionIds = [];
5435
5514
  for (const [sessionId, session] of terminalSessions.entries()) {
@@ -5712,6 +5791,53 @@ function resolveGroupParam(id) {
5712
5791
  return { group: resolved.status === 'ok' ? collaborationStore.getGroup(resolved.id) : null, ambiguous: 0 };
5713
5792
  }
5714
5793
  const ambiguousGroupResponse = (res, count) => res.status(409).json({ error: ambiguousIdMessage('协作组', count), code: 'GROUP_ID_AMBIGUOUS' });
5794
+ const preparingTeams = new Map();
5795
+ router.post('/operations/collaboration-groups/:groupId/team', async (req, res) => {
5796
+ try {
5797
+ assertPeerRegistrationAuthority(req);
5798
+ const { group, ambiguous } = resolveGroupParam(req.params.groupId);
5799
+ if (ambiguous)
5800
+ return ambiguousGroupResponse(res, ambiguous);
5801
+ if (!group || group.deleted)
5802
+ return res.status(404).json({ error: '协作组不存在' });
5803
+ if (group.federated)
5804
+ throw new Error('跨服务组请先从成员设置添加 Agent');
5805
+ const agentSlug = typeof req.body?.agentSlug === 'string' ? req.body.agentSlug : '';
5806
+ if (!(await listDetectedAgentLaunchers()).some(agent => agent.slug === agentSlug))
5807
+ throw new Error('所选 Agent 当前不可用');
5808
+ if (typeof req.body?.cwd !== 'string' || !req.body.cwd.trim())
5809
+ throw new Error('请选择工作目录');
5810
+ const cwd = await resolveWorkingDirectory(req, req.body.cwd);
5811
+ const previous = preparingTeams.get(group.id) ?? Promise.resolve();
5812
+ const operation = previous.catch(() => { }).then(() => ensureTeam({ agentSlug, cwd,
5813
+ members: () => {
5814
+ const current = collaborationStore.getGroup(group.id);
5815
+ if (!current || current.deleted)
5816
+ throw new Error('协作组已删除');
5817
+ return globalSessionState.sessions.filter(record => current.sessionIds.includes(record.sessionId))
5818
+ .map(record => ({ id: record.sessionId, cwd: record.cwd, role: current.roles?.[record.sessionId], agentSlug: orchestrationSessionSnapshot(record).agent?.slug }));
5819
+ },
5820
+ spawn: async (role) => {
5821
+ const current = collaborationStore.getGroup(group.id);
5822
+ if (!current || current.deleted)
5823
+ throw new Error('协作组已删除');
5824
+ const result = await spawnCollaborationAgentSession(req, current, null, { agentSlug, cwd, mode: 'tmux', name: role.startsWith('自动协调者') ? `协调 ${group.name}` : `执行与评审 ${group.name}` }, role);
5825
+ return result.session.sessionId;
5826
+ },
5827
+ }));
5828
+ preparingTeams.set(group.id, operation);
5829
+ try {
5830
+ res.json(await operation);
5831
+ }
5832
+ finally {
5833
+ if (preparingTeams.get(group.id) === operation)
5834
+ preparingTeams.delete(group.id);
5835
+ }
5836
+ }
5837
+ catch (error) {
5838
+ res.status(getErrorMessage(error) === 'AUTHORIZATION_DENIED' ? 403 : 409).json({ error: getErrorMessage(error) });
5839
+ }
5840
+ });
5715
5841
  router.post('/operations/collaboration-groups/:groupId/spawn', async (req, res) => {
5716
5842
  const { group, ambiguous } = resolveGroupParam(req.params.groupId);
5717
5843
  if (ambiguous)
@@ -5824,8 +5950,8 @@ router.post('/operations/orchestration/members', (req, res) => {
5824
5950
  const nextSessionIds = action === 'add'
5825
5951
  ? Array.from(new Set([...group.sessionIds, targetSessionId]))
5826
5952
  : group.sessionIds.filter((sessionId) => sessionId !== targetSessionId);
5827
- if (nextSessionIds.length < 2)
5828
- return res.status(400).json({ error: '协作组至少需要两个会话' });
5953
+ if (group.federated && nextSessionIds.length < 2)
5954
+ return res.status(400).json({ error: '跨服务组仍需两个成员;移出操作未保存' });
5829
5955
  const updated = collaborationStore.save({ id: group.id, name: group.name, sessionIds: nextSessionIds });
5830
5956
  res.json({ ok: true, group: updated });
5831
5957
  });
@@ -5895,7 +6021,7 @@ router.post('/operations/orchestration/cleanup', async (req, res) => {
5895
6021
  const remaining = group.sessionIds.filter((member) => !requestedSet.has(member)).length;
5896
6022
  groupEffects.push({
5897
6023
  id: group.id, name: group.name ?? null, sizeBefore: group.sessionIds.length,
5898
- sizeAfter: remaining < 2 ? 0 : remaining, dissolves: remaining < 2,
6024
+ sizeAfter: group.federated ? group.sessionIds.length : remaining, dissolves: false,
5899
6025
  includesCaller: group.sessionIds.includes(sourceSessionId),
5900
6026
  });
5901
6027
  }
@@ -6161,6 +6287,140 @@ router.post('/operations/session-search/prepare', async (req, res) => {
6161
6287
  res.status(400).json({ error: `无法恢复会话:${getErrorMessage(error)}` });
6162
6288
  }
6163
6289
  });
6290
+ router.get('/operations/execution-archives', (req, res) => {
6291
+ try {
6292
+ assertPeerRegistrationAuthority(req);
6293
+ res.json({ entries: executionArchives.list().filter(entry => !globalSessionState.sessions.some(record => record.sessionId === entry.sessionId)).map(entry => entry.restoredAt ? { ...entry, cleanup: { ...entry.cleanup, state: 'retained', reason: '恢复后关闭的会话,执行目录保留' } } : entry) });
6294
+ }
6295
+ catch (error) {
6296
+ res.status(403).json({ error: getErrorMessage(error) });
6297
+ }
6298
+ });
6299
+ function executionArchiveAction(action) {
6300
+ return async (req, res) => {
6301
+ try {
6302
+ assertPeerRegistrationAuthority(req);
6303
+ const operation = executionArchiveMutation.then(() => action(req));
6304
+ executionArchiveMutation = operation.then(() => undefined, () => undefined);
6305
+ res.json(await operation);
6306
+ }
6307
+ catch (error) {
6308
+ res.status(getErrorMessage(error) === 'AUTHORIZATION_DENIED' ? 403 : 409).json({ error: getErrorMessage(error) });
6309
+ }
6310
+ };
6311
+ }
6312
+ router.post('/operations/execution-archives/:sessionId/archive', executionArchiveAction(async (req) => {
6313
+ const sessionId = req.params.sessionId;
6314
+ const record = globalSessionState.sessions.find(record => record.sessionId === sessionId);
6315
+ if (!record) {
6316
+ const entry = executionArchives.get(sessionId);
6317
+ if (entry && !entry.restoredAt)
6318
+ return { entry };
6319
+ throw new Error('执行会话已变化,请刷新列表');
6320
+ }
6321
+ const service = req.app.locals.collaborationService;
6322
+ if (!service)
6323
+ throw new Error('协作服务正在启动');
6324
+ const groups = collaborationStore.groupsForSession(sessionId);
6325
+ const tasks = collaborationTaskStore.list(collaborationStore.list().map(group => group.id));
6326
+ const completed = completedExecution(tasks, service.descriptor().serviceId, sessionId);
6327
+ const workspace = completed.find(task => task.workspace?.cwd === record.cwd)?.workspace;
6328
+ if (!workspace)
6329
+ throw new Error('执行目录与任务记录不符,终端已保留');
6330
+ if (globalSessionState.sessions.some(other => other.sessionId !== sessionId && (other.cwd === workspace.cwd
6331
+ || record.tmuxSessionName && other.tmuxSessionName === record.tmuxSessionName
6332
+ || record.backendSessionId && other.backendSessionId === record.backendSessionId)))
6333
+ throw new Error('此执行目录或终端仍被其他会话使用,不能归档');
6334
+ const observed = orchestrationSessionSnapshot(record);
6335
+ const agent = normalizePersistedAgentResumeInfo({ slug: observed.agent?.slug, sessionId: observed.agentNativeSessionId,
6336
+ launchArgv: record.agentResume && record.agentResume.slug === observed.agent?.slug ? record.agentResume.launchArgv : null, updatedAt: Date.now() });
6337
+ if (!agent)
6338
+ throw new Error('尚未取得 Agent 原生会话 ID,无法保证恢复;终端已保留');
6339
+ const launcher = agentBySlug(agent.slug);
6340
+ if (!launcher || !buildResumeCommand(launcher, agent.sessionId, agent.launchArgv))
6341
+ throw new Error('此 Agent 没有可用的恢复命令,终端已保留');
6342
+ const commit = await snapshotTaskWorkspace(workspace);
6343
+ // Persist the conversation and branch before stopping anything. Pending
6344
+ // cleanup can also reconstruct a removed directory after an interrupted save.
6345
+ const entry = executionArchives.save({ sessionId, title: record.name, cwd: workspace.cwd, agent,
6346
+ groups: groups.map(group => ({ id: group.id, role: group.roles?.[sessionId] })), taskIds: completed.map(task => task.id),
6347
+ workspace, archivedAt: Date.now(), cleanup: { state: 'pending', commit } });
6348
+ if (record.mode === 'tmux' && record.tmuxSessionName)
6349
+ await destroyTmuxSessionSafely(record.tmuxSessionName);
6350
+ else {
6351
+ if (record.backendSessionId)
6352
+ cleanupSession(record.backendSessionId, { killProcess: true });
6353
+ globalSessionState = { sessions: globalSessionState.sessions.filter(item => item.sessionId !== sessionId), updatedAt: Date.now() };
6354
+ }
6355
+ collaborationStore.archiveSession(sessionId);
6356
+ collaborationRouting.remove(sessionId);
6357
+ await persistGlobalStateNow();
6358
+ broadcastClientState();
6359
+ const cleanup = await archiveTaskWorkspace(workspace);
6360
+ return { entry: executionArchives.save({ ...entry, cleanup: { ...cleanup, commit: cleanup.commit ?? commit } }) };
6361
+ }));
6362
+ function restoreArchivedMembership(entry, sessionId) {
6363
+ for (const saved of entry.groups) {
6364
+ const group = collaborationStore.getGroup(saved.id);
6365
+ if (!group || group.deleted)
6366
+ continue;
6367
+ collaborationStore.save({ id: group.id, name: group.name, sessionIds: [...new Set([...group.sessionIds, sessionId])] });
6368
+ if (saved.role)
6369
+ collaborationStore.setRole({ groupId: group.id, sessionId: sessionId, role: saved.role });
6370
+ }
6371
+ }
6372
+ router.post('/operations/execution-archives/:sessionId/restore', executionArchiveAction(async (req) => {
6373
+ const entry = executionArchives.get(req.params.sessionId);
6374
+ if (!entry)
6375
+ throw new Error('执行归档不存在');
6376
+ const existing = globalSessionState.sessions.find(record => record.sessionId === entry.sessionId);
6377
+ if (existing) {
6378
+ if (!entry.restoredAt)
6379
+ throw new Error('原会话仍在运行,请先完成归档');
6380
+ restoreArchivedMembership(entry, existing.sessionId);
6381
+ return { session: orchestrationSessionSnapshot(existing) };
6382
+ }
6383
+ // A newly reopened task may already have another active execution. Resume is
6384
+ // an explicit conversation launch; it never reopens or reassigns the task.
6385
+ const launcher = agentBySlug(entry.agent.slug);
6386
+ const command = launcher ? buildResumeCommand(launcher, entry.agent.sessionId, entry.agent.launchArgv) : null;
6387
+ if (!command)
6388
+ throw new Error('此 Agent 当前没有可用的恢复命令,归档记录已保留');
6389
+ const target = { slug: entry.agent.slug, nativeSessionId: entry.agent.sessionId, command };
6390
+ if (findActiveAgentResumeOwner('', target) || await findExternalCodexWriter(target))
6391
+ throw new Error('此 Agent 会话已在其他终端打开,请切换到已有会话');
6392
+ await restoreTaskWorkspace(entry.workspace, entry.cleanup);
6393
+ await pathValidator.allowSessionCwd(entry.cwd);
6394
+ const opened = await openInventorySession(req, { preferredFrontendSessionId: entry.sessionId, name: entry.title, customName: true, mode: 'tmux', cwd: entry.cwd });
6395
+ const record = globalSessionState.sessions.find(record => record.sessionId === opened.session.sessionId);
6396
+ const backend = terminalSessions.get(opened.terminalSession.sessionId);
6397
+ if (!backend)
6398
+ throw new Error('恢复终端失败,归档记录已保留');
6399
+ upsertGlobalSessionRecord({ ...record, agentResume: entry.agent });
6400
+ // One serialized restore per native conversation. Never type into an existing Agent.
6401
+ try {
6402
+ executionArchives.save({ ...entry, restoredAt: Date.now() });
6403
+ }
6404
+ catch (error) {
6405
+ // The newly created shell has not received a resume command. Roll it back
6406
+ // so the retained archive remains safely retryable after a disk failure.
6407
+ if (record.tmuxSessionName)
6408
+ await destroyTmuxSessionSafely(record.tmuxSessionName);
6409
+ else {
6410
+ cleanupSession(opened.terminalSession.sessionId, { killProcess: true });
6411
+ globalSessionState.sessions = globalSessionState.sessions.filter(item => item.sessionId !== record.sessionId);
6412
+ }
6413
+ collaborationStore.archiveSession(record.sessionId);
6414
+ await persistGlobalStateNow();
6415
+ broadcastClientState();
6416
+ throw error;
6417
+ }
6418
+ writeTerminalInput(backend, `${command}\r`);
6419
+ restoreArchivedMembership(entry, record.sessionId);
6420
+ await persistGlobalStateNow();
6421
+ broadcastClientState();
6422
+ return { session: orchestrationSessionSnapshot(globalSessionState.sessions.find(item => item.sessionId === record.sessionId)) };
6423
+ }));
6164
6424
  router.get('/agent-resume-history', (_req, res) => {
6165
6425
  const entries = agentResumeHistory.list().flatMap((entry) => {
6166
6426
  const agent = agentBySlug(entry.agent.slug);
@@ -7208,7 +7468,7 @@ router.get('/:sessionId/stream', async (req, res) => {
7208
7468
  // 连接时立即检测一次 activeProgram,避免前端首次显示闪烁
7209
7469
  try {
7210
7470
  if (session.mode === 'shell') {
7211
- const ap = await detectShellActiveProgram(session);
7471
+ const ap = await getCachedShellActiveProgram(sessionId, session);
7212
7472
  if (ap)
7213
7473
  session.activeProgram = ap;
7214
7474
  }
@@ -7261,131 +7521,7 @@ router.get('/:sessionId/stream', async (req, res) => {
7261
7521
  if (session.gitStatus) {
7262
7522
  writeSse(res, { type: 'git-status', gitStatus: session.gitStatus });
7263
7523
  }
7264
- let tmuxInterval = null;
7265
- let activeProgramInterval = null;
7266
- let lastTmuxLayoutSnapshot = '';
7267
- let lastActiveProgramSnapshot = JSON.stringify(session.activeProgram ?? null);
7268
- let lastTmuxMetadata = null;
7269
- let lastTmuxMetaWriteAt = 0;
7270
- const maybeWriteActiveProgram = (activeProgram) => {
7271
- const snapshot = JSON.stringify(activeProgram ? { command: activeProgram.command, source: activeProgram.source } : null);
7272
- if (snapshot === lastActiveProgramSnapshot) {
7273
- return;
7274
- }
7275
- lastActiveProgramSnapshot = snapshot;
7276
- session.activeProgram = activeProgram;
7277
- persistActiveProgramBinding(sessionId, activeProgram?.command);
7278
- // Agent status: react to AI tool start/exit
7279
- syncAgentIdentity(sessionId, session);
7280
- console.log(`[active-program][shell-sse] session=${sessionId} client=${clientId} cmd=${activeProgram?.command ?? null} source=${activeProgram?.source ?? null}`);
7281
- writeSse(res, {
7282
- type: 'active-program',
7283
- activeProgram: activeProgram?.command ?? null,
7284
- activeProgramRaw: activeProgram?.rawArgs ?? null,
7285
- activeProgramSource: activeProgram?.source ?? null,
7286
- });
7287
- };
7288
- const sendTmuxLayout = async () => {
7289
- if (session.mode !== 'tmux' || !session.tmuxSessionName) {
7290
- return;
7291
- }
7292
- try {
7293
- const layout = await getCachedTmuxLayout(session.tmuxSessionName);
7294
- // Resolve the active program — try ps-based detection for generic commands
7295
- const activePane = getActivePaneFromLayout(layout);
7296
- if (activePane) {
7297
- const resolved = await resolveTmuxPaneProgram(activePane);
7298
- if (resolved) {
7299
- maybeWriteActiveProgram({
7300
- command: resolved.command,
7301
- source: resolved.source,
7302
- rawArgs: resolved.rawArgs,
7303
- updatedAt: Date.now(),
7304
- });
7305
- }
7306
- else {
7307
- maybeWriteActiveProgram(getActiveProgramFromTmuxLayout(layout));
7308
- }
7309
- }
7310
- else {
7311
- maybeWriteActiveProgram(getActiveProgramFromTmuxLayout(layout));
7312
- }
7313
- const newCwd = getCwdFromTmuxLayout(layout);
7314
- if (newCwd && newCwd !== session.cwd) {
7315
- session.cwd = newCwd;
7316
- console.log(`[tmux-cwd][sse] session=${sessionId} cwd=${newCwd}`);
7317
- if (updateGlobalBindingForBackendSession(sessionId, { cwd: newCwd, lastActivity: session.lastActivity })) {
7318
- schedulePersistGlobalState();
7319
- }
7320
- writeSse(res, { type: 'cwd', cwd: newCwd });
7321
- refreshGitStatus(sessionId, session, { minIntervalMs: 0 });
7322
- }
7323
- // tmux 消费了 inner shell 发的 OSC 2/133,不透传到外层 PTY。
7324
- // 从 tmux layout 提取 active pane 的 title 和 command 来推导。
7325
- if (activePane) {
7326
- const paneTitle = activePane.title || '';
7327
- if (paneTitle && paneTitle !== session.lastOscTitle) {
7328
- session.lastOscTitle = paneTitle;
7329
- writeSse(res, { type: 'shell-title', title: paneTitle });
7330
- }
7331
- const paneCmd = activePane.command || '';
7332
- const inferredState = paneCmd && !shellNamesBackend.has(paneCmd) ? 'running' : 'idle';
7333
- if (inferredState !== session.lastPromptState) {
7334
- const wasRunning = session.lastPromptState === 'running';
7335
- session.lastPromptState = inferredState;
7336
- writeSse(res, { type: 'prompt-state', state: inferredState });
7337
- if (wasRunning && inferredState === 'idle') {
7338
- refreshGitStatus(sessionId, session);
7339
- }
7340
- }
7341
- }
7342
- // Mirror dynamic metadata onto tmux user options (cheap when nothing
7343
- // changed thanks to the full metadata snapshot cache).
7344
- const meta = syncDynamicTmuxMetadata({
7345
- tmuxSessionName: session.tmuxSessionName,
7346
- program: session.activeProgram?.command ?? null,
7347
- cwd: session.cwd ?? null,
7348
- previousMetadata: lastTmuxMetadata,
7349
- lastActiveWriteAt: lastTmuxMetaWriteAt,
7350
- });
7351
- lastTmuxMetadata = { program: meta.program, cwd: meta.cwd, label: meta.label, rawArgs: null };
7352
- lastTmuxMetaWriteAt = meta.lastActiveWriteAt;
7353
- const snapshot = JSON.stringify(layout);
7354
- if (snapshot === lastTmuxLayoutSnapshot) {
7355
- return;
7356
- }
7357
- lastTmuxLayoutSnapshot = snapshot;
7358
- writeSse(res, { type: 'tmux-layout', layout });
7359
- }
7360
- catch (error) {
7361
- const errorMessage = error instanceof Error ? error.message : String(error);
7362
- console.warn(`Failed to fetch tmux layout for ${session.tmuxSessionName}: ${errorMessage}`);
7363
- }
7364
- };
7365
- const sendShellActiveProgram = async () => {
7366
- if (session.mode !== 'shell') {
7367
- return;
7368
- }
7369
- try {
7370
- maybeWriteActiveProgram(await detectShellActiveProgram(session));
7371
- }
7372
- catch (error) {
7373
- const errorMessage = error instanceof Error ? error.message : String(error);
7374
- console.warn(`Failed to detect active shell program for ${sessionId}: ${errorMessage}`);
7375
- }
7376
- };
7377
- if (session.mode === 'tmux' && session.tmuxSessionName) {
7378
- void sendTmuxLayout();
7379
- tmuxInterval = setInterval(() => {
7380
- void sendTmuxLayout();
7381
- }, TMUX_POLL_INTERVAL);
7382
- }
7383
- else {
7384
- void sendShellActiveProgram();
7385
- activeProgramInterval = setInterval(() => {
7386
- void sendShellActiveProgram();
7387
- }, ACTIVE_PROGRAM_POLL_INTERVAL);
7388
- }
7524
+ const stopSampling = subscribeTerminalSampling(sessionId, session, event => writeSse(res, event));
7389
7525
  const heartbeatInterval = setInterval(() => {
7390
7526
  if (!writeResponseChunk(res, ': heartbeat\n\n')) {
7391
7527
  clearInterval(heartbeatInterval);
@@ -7401,12 +7537,7 @@ router.get('/:sessionId/stream', async (req, res) => {
7401
7537
  }
7402
7538
  const cleanup = () => {
7403
7539
  clearInterval(heartbeatInterval);
7404
- if (tmuxInterval) {
7405
- clearInterval(tmuxInterval);
7406
- }
7407
- if (activeProgramInterval) {
7408
- clearInterval(activeProgramInterval);
7409
- }
7540
+ stopSampling();
7410
7541
  closeClient(session, sessionId, clientId);
7411
7542
  console.log(`Client ${clientId} disconnected from terminal session ${sessionId}`);
7412
7543
  };
@@ -7964,7 +8095,7 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
7964
8095
  // 连接时立即检测一次 activeProgram,避免前端首次显示闪烁
7965
8096
  try {
7966
8097
  if (session.mode === 'shell') {
7967
- const ap = await detectShellActiveProgram(session);
8098
+ const ap = await getCachedShellActiveProgram(sessionId, session);
7968
8099
  if (ap)
7969
8100
  session.activeProgram = ap;
7970
8101
  }
@@ -7994,7 +8125,7 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
7994
8125
  if (attachment) {
7995
8126
  if (ws.readyState !== ws.OPEN)
7996
8127
  return;
7997
- delivery.setActive(outputWanted && !clientPaused);
8128
+ delivery.setActive(outputWanted);
7998
8129
  if (delivery.active) {
7999
8130
  const screen = new TmuxInitialScreen();
8000
8131
  initialScreen = screen;
@@ -8114,129 +8245,10 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
8114
8245
  await sendReplay(0);
8115
8246
  });
8116
8247
  void sendReplay();
8117
- // Tmux layout polling (per-client, like the SSE stream does)
8118
- let tmuxInterval = null;
8119
- let activeProgramInterval = null;
8120
- if (session.mode === 'tmux' && session.tmuxSessionName) {
8121
- let lastTmuxLayoutSnapshot = '';
8122
- let lastActiveProgramSnapshot = JSON.stringify(session.activeProgram ?? null);
8123
- let lastTmuxMetadata = null;
8124
- let lastTmuxMetaWriteAt = 0;
8125
- const sendTmuxLayout = async () => {
8126
- if (ws.readyState !== ws.OPEN)
8127
- return;
8128
- try {
8129
- const layout = await getCachedTmuxLayout(session.tmuxSessionName);
8130
- // Update active program — try ps-based detection for generic commands
8131
- const activePane = getActivePaneFromLayout(layout);
8132
- let ap = null;
8133
- if (activePane) {
8134
- const resolved = await resolveTmuxPaneProgram(activePane);
8135
- if (resolved) {
8136
- ap = { command: resolved.command, source: resolved.source, rawArgs: resolved.rawArgs, updatedAt: Date.now() };
8137
- }
8138
- else {
8139
- ap = getActiveProgramFromTmuxLayout(layout);
8140
- }
8141
- }
8142
- else {
8143
- ap = getActiveProgramFromTmuxLayout(layout);
8144
- }
8145
- const apSnapshot = JSON.stringify(ap ? { command: ap.command, source: ap.source } : null);
8146
- if (apSnapshot !== lastActiveProgramSnapshot) {
8147
- lastActiveProgramSnapshot = apSnapshot;
8148
- session.activeProgram = ap;
8149
- persistActiveProgramBinding(sessionId, ap?.command);
8150
- // Agent status: react to AI tool start/exit
8151
- syncAgentIdentity(sessionId, session);
8152
- console.log(`[active-program][ws] session=${sessionId} cmd=${ap?.command ?? null} source=${ap?.source ?? null}`);
8153
- ws.send(JSON.stringify({
8154
- type: 'active-program',
8155
- activeProgram: ap?.command ?? null,
8156
- activeProgramRaw: ap?.rawArgs ?? null,
8157
- activeProgramSource: ap?.source ?? null,
8158
- }));
8159
- }
8160
- // Update cwd from active pane
8161
- const newCwd = getCwdFromTmuxLayout(layout);
8162
- if (newCwd && newCwd !== session.cwd) {
8163
- session.cwd = newCwd;
8164
- console.log(`[tmux-cwd][ws] session=${sessionId} cwd=${newCwd}`);
8165
- if (updateGlobalBindingForBackendSession(sessionId, { cwd: newCwd, lastActivity: session.lastActivity })) {
8166
- schedulePersistGlobalState();
8167
- }
8168
- ws.send(JSON.stringify({ type: 'cwd', cwd: newCwd }));
8169
- refreshGitStatus(sessionId, session, { minIntervalMs: 0 });
8170
- }
8171
- // tmux 消费了 inner shell 发的 OSC 2(存入 pane_title)和 OSC 133,
8172
- // 不透传到外层 PTY。因此从 tmux layout 提取 active pane 的 title
8173
- // 和 command 来推导 shell-title / prompt-state。
8174
- if (activePane) {
8175
- const paneTitle = activePane.title || '';
8176
- if (paneTitle && paneTitle !== session.lastOscTitle) {
8177
- session.lastOscTitle = paneTitle;
8178
- ws.send(JSON.stringify({ type: 'shell-title', title: paneTitle }));
8179
- }
8180
- // prompt-state: command 是 shell 名 → idle;否则 → running
8181
- const paneCmd = activePane.command || '';
8182
- const inferredState = paneCmd && !shellNamesBackend.has(paneCmd) ? 'running' : 'idle';
8183
- if (inferredState !== session.lastPromptState) {
8184
- const wasRunning = session.lastPromptState === 'running';
8185
- session.lastPromptState = inferredState;
8186
- ws.send(JSON.stringify({ type: 'prompt-state', state: inferredState }));
8187
- if (wasRunning && inferredState === 'idle') {
8188
- refreshGitStatus(sessionId, session);
8189
- }
8190
- }
8191
- }
8192
- // Mirror dynamic metadata onto tmux user options.
8193
- const meta = syncDynamicTmuxMetadata({
8194
- tmuxSessionName: session.tmuxSessionName,
8195
- program: session.activeProgram?.command ?? null,
8196
- cwd: session.cwd ?? null,
8197
- previousMetadata: lastTmuxMetadata,
8198
- lastActiveWriteAt: lastTmuxMetaWriteAt,
8199
- });
8200
- lastTmuxMetadata = { program: meta.program, cwd: meta.cwd, label: meta.label, rawArgs: null };
8201
- lastTmuxMetaWriteAt = meta.lastActiveWriteAt;
8202
- const snapshot = JSON.stringify(layout);
8203
- if (snapshot !== lastTmuxLayoutSnapshot) {
8204
- lastTmuxLayoutSnapshot = snapshot;
8205
- ws.send(JSON.stringify({ type: 'tmux-layout', layout }));
8206
- }
8207
- }
8208
- catch { /* ignore polling errors */ }
8209
- };
8210
- sendTmuxLayout();
8211
- tmuxInterval = setInterval(sendTmuxLayout, TMUX_POLL_INTERVAL);
8212
- }
8213
- // Active program polling (shell mode)
8214
- if (session.mode === 'shell') {
8215
- let lastApSnapshot = JSON.stringify(session.activeProgram ? { command: session.activeProgram.command, source: session.activeProgram.source } : null);
8216
- const pollActiveProgram = async () => {
8217
- if (ws.readyState !== ws.OPEN)
8218
- return;
8219
- try {
8220
- const ap = await detectShellActiveProgram(session);
8221
- const snapshot = JSON.stringify(ap ? { command: ap.command, source: ap.source } : null);
8222
- if (snapshot !== lastApSnapshot) {
8223
- lastApSnapshot = snapshot;
8224
- session.activeProgram = ap;
8225
- persistActiveProgramBinding(sessionId, ap?.command);
8226
- // Agent status: react to AI tool start/exit
8227
- syncAgentIdentity(sessionId, session);
8228
- ws.send(JSON.stringify({
8229
- type: 'active-program',
8230
- activeProgram: ap?.command ?? null,
8231
- activeProgramRaw: ap?.rawArgs ?? null,
8232
- activeProgramSource: ap?.source ?? null,
8233
- }));
8234
- }
8235
- }
8236
- catch { /* ignore */ }
8237
- };
8238
- activeProgramInterval = setInterval(pollActiveProgram, ACTIVE_PROGRAM_POLL_INTERVAL);
8239
- }
8248
+ const stopSampling = subscribeTerminalSampling(sessionId, session, event => {
8249
+ if (ws.readyState === ws.OPEN)
8250
+ ws.send(JSON.stringify(event));
8251
+ });
8240
8252
  // Handle client → server messages
8241
8253
  ws.on('message', async (raw) => {
8242
8254
  const messageReceivedAt = performance.now();
@@ -8370,7 +8382,7 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
8370
8382
  if (!options.flowControl || typeof msg.active !== 'boolean')
8371
8383
  break;
8372
8384
  outputWanted = msg.active;
8373
- const resumed = delivery.setActive(outputWanted && !clientPaused);
8385
+ const resumed = delivery.setActive(outputWanted);
8374
8386
  if (!delivery.active)
8375
8387
  attachment?.close();
8376
8388
  if (resumed && delivery.active) {
@@ -8391,12 +8403,9 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
8391
8403
  if (typeof msg.paused === 'boolean') {
8392
8404
  const reason = typeof msg.reason === 'string' ? msg.reason : 'client-flow-control';
8393
8405
  if (options.flowControl) {
8394
- const wasPaused = clientPaused;
8395
8406
  clientPaused = msg.paused;
8396
- delivery.setActive(outputWanted && !clientPaused);
8397
- if (!delivery.active)
8398
- attachment?.close();
8399
- if (wasPaused && delivery.active)
8407
+ delivery.setPaused(clientPaused);
8408
+ if (!clientPaused && delivery.needsReplay && delivery.active)
8400
8409
  void sendReplay(0);
8401
8410
  }
8402
8411
  else
@@ -8437,10 +8446,7 @@ export function handleTerminalWebSocket(ws, sessionId, clientId, options = {}, i
8437
8446
  initialScreen?.cancel();
8438
8447
  initialScreen = null;
8439
8448
  attachment?.close();
8440
- if (tmuxInterval)
8441
- clearInterval(tmuxInterval);
8442
- if (activeProgramInterval)
8443
- clearInterval(activeProgramInterval);
8449
+ stopSampling();
8444
8450
  const clients = wsClients.get(sessionId);
8445
8451
  if (clients) {
8446
8452
  clients.delete(clientId);
@@ -8548,7 +8554,9 @@ export function handleControlWebSocket(ws, clientId) {
8548
8554
  // session/tmux server is no longer alive; broadcast the cleaned list.
8549
8555
  const CLIENT_STATE_RECONCILE_INTERVAL_MS = 30_000;
8550
8556
  const reconcileTimer = setInterval(() => {
8551
- void reconcileClientState();
8557
+ void reconcileClientState().catch(error => {
8558
+ console.warn('[reconcile] failed:', getErrorMessage(error));
8559
+ });
8552
8560
  }, CLIENT_STATE_RECONCILE_INTERVAL_MS);
8553
8561
  // Don't keep the event loop alive for housekeeping.
8554
8562
  reconcileTimer.unref?.();
@@ -8560,17 +8568,30 @@ const automationTimer = setInterval(() => {
8560
8568
  }
8561
8569
  }, 30_000);
8562
8570
  automationTimer.unref?.();
8563
- async function reconcileClientState() {
8571
+ let reconcileInFlight = null;
8572
+ function reconcileClientState() {
8573
+ if (reconcileInFlight)
8574
+ return reconcileInFlight;
8575
+ reconcileInFlight = reconcileClientStateOnce().finally(() => { reconcileInFlight = null; });
8576
+ return reconcileInFlight;
8577
+ }
8578
+ async function reconcileClientStateOnce() {
8564
8579
  if (globalSessionState.sessions.length === 0)
8565
8580
  return;
8581
+ const observedEntries = new Map(globalSessionState.sessions.map(entry => [entry.sessionId, entry]));
8566
8582
  // Refresh first so a shared tmux crash is recorded before orphan cleanup
8567
8583
  // can discard the metadata needed to recover Agent conversations.
8568
- await getSessionInventorySnapshot({ refresh: true }).catch((error) => {
8584
+ const inventory = await getSessionInventorySnapshot({ refresh: true }).catch((error) => {
8569
8585
  console.warn('[tmux-recovery] reconcile observation failed:', getErrorMessage(error));
8586
+ return null;
8570
8587
  });
8588
+ const liveTmuxNames = inventory ? inventoryTmuxLiveness.get(inventory) : undefined;
8571
8589
  const toRemove = [];
8572
8590
  const toDetach = [];
8573
8591
  for (const entry of globalSessionState.sessions) {
8592
+ // Do not delete a session created/rebound while the inventory was loading.
8593
+ if (observedEntries.get(entry.sessionId) !== entry)
8594
+ continue;
8574
8595
  if (entry.mode === 'shell') {
8575
8596
  // Shell wrapper: backendSessionId must map to a live terminal session.
8576
8597
  // A resumable Agent record survives with a detached backend so the tab
@@ -8590,21 +8611,15 @@ async function reconcileClientState() {
8590
8611
  // own this session name. We deliberately do NOT require a live
8591
8612
  // terminal wrapper here — detaching from the wrapper doesn't kill
8592
8613
  // the tmux session.
8593
- try {
8594
- const alive = await tmuxSessionExists(entry.tmuxSessionName);
8595
- if (!alive) {
8596
- if (entry.cwd && entry.agentResume?.sessionId) {
8597
- if (entry.backendSessionId)
8598
- toDetach.push(entry.sessionId);
8599
- }
8600
- else {
8601
- toRemove.push(entry.sessionId);
8602
- }
8614
+ // A failed list is unknown, never proof that every session disappeared.
8615
+ if (liveTmuxNames && !liveTmuxNames.has(entry.tmuxSessionName)) {
8616
+ if (entry.cwd && entry.agentResume?.sessionId) {
8617
+ if (entry.backendSessionId)
8618
+ toDetach.push(entry.sessionId);
8619
+ }
8620
+ else {
8621
+ toRemove.push(entry.sessionId);
8603
8622
  }
8604
- }
8605
- catch {
8606
- // tmux itself is down — leave the entry alone; the next tick (or
8607
- // boot-time prune) will clean it up once tmux is back.
8608
8623
  }
8609
8624
  }
8610
8625
  }