termdock 1.4.307 → 1.4.309
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.
- package/dist/client/assets/{App-cagV-flM.js → App-ggIA_xqR.js} +6 -6
- package/dist/client/assets/App-ggIA_xqR.js.br +0 -0
- package/dist/client/assets/App-ggIA_xqR.js.gz +0 -0
- package/dist/client/assets/{ArchitecturePanel-TdKPja7L.js → ArchitecturePanel-CoBspW_C.js} +1 -1
- package/dist/client/assets/ArchitecturePanel-CoBspW_C.js.br +0 -0
- package/dist/client/assets/ArchitecturePanel-CoBspW_C.js.gz +0 -0
- package/dist/client/assets/{ChangeWalkthroughPanel-DOj40Srm.js → ChangeWalkthroughPanel-D1Puc7B3.js} +1 -1
- package/dist/client/assets/ChangeWalkthroughPanel-D1Puc7B3.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-D1Puc7B3.js.gz +0 -0
- package/dist/client/assets/{CollaborationMainWorkspace-C5Tcos5l.js → CollaborationMainWorkspace-BPyK5Xs8.js} +1 -1
- package/dist/client/assets/CollaborationMainWorkspace-BPyK5Xs8.js.br +0 -0
- package/dist/client/assets/CollaborationMainWorkspace-BPyK5Xs8.js.gz +0 -0
- package/dist/client/assets/CollaborationTaskContent-CTSJJqze.js +2 -0
- package/dist/client/assets/{ComputerControlView-C2sOFyuA.js → ComputerControlView-D5Lw9sNA.js} +2 -2
- package/dist/client/assets/ComputerControlView-D5Lw9sNA.js.br +0 -0
- package/dist/client/assets/ComputerControlView-D5Lw9sNA.js.gz +0 -0
- package/dist/client/assets/{DagPlayground-DFfVJW00.js → DagPlayground-DYJrGfiG.js} +1 -1
- package/dist/client/assets/DagPlayground-DYJrGfiG.js.br +0 -0
- package/dist/client/assets/DagPlayground-DYJrGfiG.js.gz +0 -0
- package/dist/client/assets/{DiffLab-hoj8gwUi.js → DiffLab-5zdcFW3z.js} +1 -1
- package/dist/client/assets/DiffLab-5zdcFW3z.js.br +0 -0
- package/dist/client/assets/DiffLab-5zdcFW3z.js.gz +0 -0
- package/dist/client/assets/{DiffReview-Cn_PmN-9.js → DiffReview-BBBC_NjV.js} +1 -1
- package/dist/client/assets/DiffReview-BBBC_NjV.js.br +0 -0
- package/dist/client/assets/DiffReview-BBBC_NjV.js.gz +0 -0
- package/dist/client/assets/{DiffReviewLab-DycgD40E.js → DiffReviewLab-CbhosFSB.js} +1 -1
- package/dist/client/assets/DiffReviewLab-CbhosFSB.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-CbhosFSB.js.gz +0 -0
- package/dist/client/assets/{DiffViewer-DRnuph7U.js → DiffViewer-BuQwFQ4V.js} +1 -1
- package/dist/client/assets/DiffViewer-BuQwFQ4V.js.br +0 -0
- package/dist/client/assets/DiffViewer-BuQwFQ4V.js.gz +0 -0
- package/dist/client/assets/{ModelPreview-Dvw-UtQQ.js → ModelPreview-nO2VmqdP.js} +1 -1
- package/dist/client/assets/ModelPreview-nO2VmqdP.js.br +0 -0
- package/dist/client/assets/ModelPreview-nO2VmqdP.js.gz +0 -0
- package/dist/client/assets/{RightSidebar-CnnU7Ffp.js → RightSidebar-BoNzpS2K.js} +3 -3
- package/dist/client/assets/RightSidebar-BoNzpS2K.js.br +0 -0
- package/dist/client/assets/{RightSidebar-CnnU7Ffp.js.gz → RightSidebar-BoNzpS2K.js.gz} +0 -0
- package/dist/client/assets/{abnfDiagram-VCTEODGH-BQVIT5gD.js → abnfDiagram-VCTEODGH-yi0K8GDG.js} +1 -1
- package/dist/client/assets/abnfDiagram-VCTEODGH-yi0K8GDG.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-yi0K8GDG.js.gz +0 -0
- package/dist/client/assets/{app-CXvK8WZg.js → app-B6FdOF6K.js} +3 -3
- package/dist/client/assets/app-B6FdOF6K.js.br +0 -0
- package/dist/client/assets/app-B6FdOF6K.js.gz +0 -0
- package/dist/client/assets/{arc-DZG1NzUc.js → arc-_nLI_-XZ.js} +1 -1
- package/dist/client/assets/arc-_nLI_-XZ.js.br +0 -0
- package/dist/client/assets/arc-_nLI_-XZ.js.gz +0 -0
- package/dist/client/assets/{architectureDiagram-5GKGNRK7-Bv15lJGE.js → architectureDiagram-5GKGNRK7-CikrA78O.js} +1 -1
- package/dist/client/assets/architectureDiagram-5GKGNRK7-CikrA78O.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-CikrA78O.js.gz +0 -0
- package/dist/client/assets/{blockDiagram-I7D4REHJ-DShw-lH7.js → blockDiagram-I7D4REHJ-CWJq6qJ7.js} +1 -1
- package/dist/client/assets/blockDiagram-I7D4REHJ-CWJq6qJ7.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-CWJq6qJ7.js.gz +0 -0
- package/dist/client/assets/{c4Diagram-7LVT6UL2-BN6SsyOM.js → c4Diagram-7LVT6UL2-gzPzLGvs.js} +1 -1
- package/dist/client/assets/c4Diagram-7LVT6UL2-gzPzLGvs.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-gzPzLGvs.js.gz +0 -0
- package/dist/client/assets/channel-BYsFmOmF.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-BkBkNjv0.js → chunk-2Q5K7J3B-CfX9l18E.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-LrpNvv_o.js → chunk-5VM5RSS4-JSV1yrXM.js} +1 -1
- package/dist/client/assets/{chunk-F27PBJKO-BcpMMU4P.js → chunk-F27PBJKO-CQ0LXeap.js} +1 -1
- package/dist/client/assets/chunk-F27PBJKO-CQ0LXeap.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-CQ0LXeap.js.gz +0 -0
- package/dist/client/assets/{chunk-IMKFNOWR-cWvd5gJk.js → chunk-IMKFNOWR-CJOEPkK9.js} +1 -1
- package/dist/client/assets/chunk-IMKFNOWR-CJOEPkK9.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-CJOEPkK9.js.gz +0 -0
- package/dist/client/assets/{chunk-JWPE2WC7-Bf4ORht5.js → chunk-JWPE2WC7-Dpp7Xu_E.js} +1 -1
- package/dist/client/assets/{chunk-POPQ4Y6H-M5MSHOUM.js → chunk-POPQ4Y6H-9hDa1b8z.js} +1 -1
- package/dist/client/assets/{chunk-SVP7TREG-Bj8gUFc2.js → chunk-SVP7TREG-BskkG9gg.js} +1 -1
- package/dist/client/assets/chunk-SVP7TREG-BskkG9gg.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-BskkG9gg.js.gz +0 -0
- package/dist/client/assets/{chunk-TICWLB2K-W1cGkC4s.js → chunk-TICWLB2K-DyzuP-9I.js} +1 -1
- package/dist/client/assets/chunk-TICWLB2K-DyzuP-9I.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-DyzuP-9I.js.gz +0 -0
- package/dist/client/assets/{chunk-XXDRQBXY-mXLNbLH8.js → chunk-XXDRQBXY-C31_1i6z.js} +1 -1
- package/dist/client/assets/classDiagram-ZZMXUADV-BnmZv6Ns.js +1 -0
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-BnmZv6Ns.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-CK0EaMQY.js → cose-bilkent-JH36ORCC-Bm-ESDOc.js} +1 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-Bm-ESDOc.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-Bm-ESDOc.js.gz +0 -0
- package/dist/client/assets/{cynefin-OW5HDTMX-C-7tsMpt.js → cynefin-OW5HDTMX-DbFWAPvX.js} +1 -1
- package/dist/client/assets/cynefin-OW5HDTMX-DbFWAPvX.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-DbFWAPvX.js.gz +0 -0
- package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-CijqM5Xa.js → cynefinDiagram-5FMLGOSQ-Ca0IKRdH.js} +1 -1
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-Ca0IKRdH.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-Ca0IKRdH.js.gz +0 -0
- package/dist/client/assets/{dagre-GXQ25YYZ-DCLINHGD.js → dagre-GXQ25YYZ-D_5uEMjJ.js} +1 -1
- package/dist/client/assets/dagre-GXQ25YYZ-D_5uEMjJ.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-D_5uEMjJ.js.gz +0 -0
- package/dist/client/assets/{diagram-S7CK7UJ4-D-JvTWtL.js → diagram-S7CK7UJ4-DWVpndRP.js} +1 -1
- package/dist/client/assets/diagram-S7CK7UJ4-DWVpndRP.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-DWVpndRP.js.gz +0 -0
- package/dist/client/assets/{diagram-UQ7AKVKN-BT01Oee8.js → diagram-UQ7AKVKN-DmYTya3w.js} +1 -1
- package/dist/client/assets/diagram-UQ7AKVKN-DmYTya3w.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-DmYTya3w.js.gz +0 -0
- package/dist/client/assets/{diagram-VSXAHHWV-C_j4_Ldm.js → diagram-VSXAHHWV-BEA6kqNj.js} +1 -1
- package/dist/client/assets/diagram-VSXAHHWV-BEA6kqNj.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-BEA6kqNj.js.gz +0 -0
- package/dist/client/assets/{diagram-VX7I27RA-DY62ObO9.js → diagram-VX7I27RA-CU9vP0P0.js} +1 -1
- package/dist/client/assets/diagram-VX7I27RA-CU9vP0P0.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-CU9vP0P0.js.gz +0 -0
- package/dist/client/assets/{diagram-Z3DM3KII-TnINooIy.js → diagram-Z3DM3KII-B_GvE2g-.js} +1 -1
- package/dist/client/assets/diagram-Z3DM3KII-B_GvE2g-.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-B_GvE2g-.js.gz +0 -0
- package/dist/client/assets/{ebnfDiagram-PWID7BFC-BahaUYTh.js → ebnfDiagram-PWID7BFC-BLrPQuLE.js} +1 -1
- package/dist/client/assets/ebnfDiagram-PWID7BFC-BLrPQuLE.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-BLrPQuLE.js.gz +0 -0
- package/dist/client/assets/{erDiagram-RLTQ6QDP-DNiKhK5j.js → erDiagram-RLTQ6QDP-DlXgoyZ-.js} +1 -1
- package/dist/client/assets/erDiagram-RLTQ6QDP-DlXgoyZ-.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-DlXgoyZ-.js.gz +0 -0
- package/dist/client/assets/{flowDiagram-HODETNUW-x9c9CGXD.js → flowDiagram-HODETNUW-fPHWxEDZ.js} +1 -1
- package/dist/client/assets/flowDiagram-HODETNUW-fPHWxEDZ.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-fPHWxEDZ.js.gz +0 -0
- package/dist/client/assets/{ganttDiagram-EL5Y4UJY-BofL5Lxs.js → ganttDiagram-EL5Y4UJY-B8MOv6ug.js} +1 -1
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-B8MOv6ug.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-B8MOv6ug.js.gz +0 -0
- package/dist/client/assets/{gestureArbiter-Cy5v_ZUL.js → gestureArbiter-JW8vcVRm.js} +1 -1
- package/dist/client/assets/gestureArbiter-JW8vcVRm.js.br +0 -0
- package/dist/client/assets/gestureArbiter-JW8vcVRm.js.gz +0 -0
- package/dist/client/assets/{gitGraphDiagram-WWUBYQGX-CHYuiGg1.js → gitGraphDiagram-WWUBYQGX-lH9gSoFx.js} +1 -1
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-lH9gSoFx.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-lH9gSoFx.js.gz +0 -0
- package/dist/client/assets/{infoDiagram-27XIBGKW-mfFgE6OE.js → infoDiagram-27XIBGKW-CSG29Ty_.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-CiCmFa95.js → ishikawaDiagram-5VMMS53U-CcwSE5qV.js} +1 -1
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-CcwSE5qV.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-CcwSE5qV.js.gz +0 -0
- package/dist/client/assets/{journeyDiagram-3NMN7TZE-CVnBzo7n.js → journeyDiagram-3NMN7TZE-CXFUeP2c.js} +1 -1
- package/dist/client/assets/journeyDiagram-3NMN7TZE-CXFUeP2c.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-CXFUeP2c.js.gz +0 -0
- package/dist/client/assets/{kanban-definition-UXKFOSKX-3-zdGxI0.js → kanban-definition-UXKFOSKX-KEHwljd2.js} +1 -1
- package/dist/client/assets/kanban-definition-UXKFOSKX-KEHwljd2.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-KEHwljd2.js.gz +0 -0
- package/dist/client/assets/{layout-DnB4RHWA.js → layout-DqYx0MnO.js} +1 -1
- package/dist/client/assets/layout-DqYx0MnO.js.br +0 -0
- package/dist/client/assets/layout-DqYx0MnO.js.gz +0 -0
- package/dist/client/assets/{linear-B_lli4BL.js → linear-BnG3XaJW.js} +1 -1
- package/dist/client/assets/linear-BnG3XaJW.js.br +0 -0
- package/dist/client/assets/linear-BnG3XaJW.js.gz +0 -0
- package/dist/client/assets/{mermaid.core-ClemZ-kc.js → mermaid.core-Ce00rdM6.js} +5 -5
- package/dist/client/assets/mermaid.core-Ce00rdM6.js.br +0 -0
- package/dist/client/assets/mermaid.core-Ce00rdM6.js.gz +0 -0
- package/dist/client/assets/{mindmap-definition-YA3MSWOX-hXDufapN.js → mindmap-definition-YA3MSWOX-D0hOW4SY.js} +1 -1
- package/dist/client/assets/mindmap-definition-YA3MSWOX-D0hOW4SY.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-D0hOW4SY.js.gz +0 -0
- package/dist/client/assets/{passwordBootstrap-c-hmAGCQ.js → passwordBootstrap-Bncb8qF_.js} +1 -1
- package/dist/client/assets/passwordBootstrap-Bncb8qF_.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-Bncb8qF_.js.gz +0 -0
- package/dist/client/assets/{pegDiagram-XKGWAZYB-yb1lAfHR.js → pegDiagram-XKGWAZYB-UKp9t91x.js} +1 -1
- package/dist/client/assets/pegDiagram-XKGWAZYB-UKp9t91x.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-UKp9t91x.js.gz +0 -0
- package/dist/client/assets/{pieDiagram-E7YTZNPT-lVC2DWY_.js → pieDiagram-E7YTZNPT-Dif97wbH.js} +1 -1
- package/dist/client/assets/pieDiagram-E7YTZNPT-Dif97wbH.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-Dif97wbH.js.gz +0 -0
- package/dist/client/assets/{quadrantDiagram-AXDQQJYC-CeqeNP5C.js → quadrantDiagram-AXDQQJYC-BPyHtH91.js} +1 -1
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-BPyHtH91.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-BPyHtH91.js.gz +0 -0
- package/dist/client/assets/{railroadDiagram-O6MQD6OU-BMEBmjF2.js → railroadDiagram-O6MQD6OU-Wleq9LmS.js} +1 -1
- package/dist/client/assets/railroadDiagram-O6MQD6OU-Wleq9LmS.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-Wleq9LmS.js.gz +0 -0
- package/dist/client/assets/{rdpSession-D77Fs4-L.js → rdpSession-CobFMSUs.js} +1 -1
- package/dist/client/assets/rdpSession-CobFMSUs.js.br +0 -0
- package/dist/client/assets/rdpSession-CobFMSUs.js.gz +0 -0
- package/dist/client/assets/{referenceLongPress-xNXQRqYn.js → referenceLongPress-bZ0xp-v2.js} +1 -1
- package/dist/client/assets/referenceLongPress-bZ0xp-v2.js.br +0 -0
- package/dist/client/assets/referenceLongPress-bZ0xp-v2.js.gz +0 -0
- package/dist/client/assets/{requirementDiagram-BXWQKSXE-CRhmIdDq.js → requirementDiagram-BXWQKSXE-BL4pp0Ta.js} +1 -1
- package/dist/client/assets/requirementDiagram-BXWQKSXE-BL4pp0Ta.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-BL4pp0Ta.js.gz +0 -0
- package/dist/client/assets/{sankeyDiagram-P5KCCOFB-CZZPxGRu.js → sankeyDiagram-P5KCCOFB-CV1cKx37.js} +1 -1
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-CV1cKx37.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-CV1cKx37.js.gz +0 -0
- package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-CvKYTCaL.js → sequenceDiagram-WJ2MYXX4-BF2InLwV.js} +1 -1
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-BF2InLwV.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-BF2InLwV.js.gz +0 -0
- package/dist/client/assets/{sizeCapture-INFHLROL-DpMFgVd8.js → sizeCapture-INFHLROL-CldUyStR.js} +1 -1
- package/dist/client/assets/sizeCapture-INFHLROL-CldUyStR.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-CldUyStR.js.gz +0 -0
- package/dist/client/assets/{sparkles-BBYDWqCu.js → sparkles-Cg-yPdE2.js} +1 -1
- package/dist/client/assets/{stateDiagram-D77RDMKH-BUCKWh69.js → stateDiagram-D77RDMKH-CByehbEd.js} +1 -1
- package/dist/client/assets/stateDiagram-D77RDMKH-CByehbEd.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-CByehbEd.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-CTcrJ-w0.js +1 -0
- package/dist/client/assets/{swimlanes-42K2YHIH-Du84aO1l.js → swimlanes-42K2YHIH--1_7YTL4.js} +1 -1
- package/dist/client/assets/swimlanes-42K2YHIH--1_7YTL4.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH--1_7YTL4.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-RSMV9AhO.js +8 -0
- package/dist/client/assets/{timeline-definition-24CTP7MA-94wGyYVM.js → timeline-definition-24CTP7MA-CXfXJFK6.js} +1 -1
- package/dist/client/assets/timeline-definition-24CTP7MA-CXfXJFK6.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-CXfXJFK6.js.gz +0 -0
- package/dist/client/assets/{triangle-alert-BFCgLlxS.js → triangle-alert-CbMRJ6sx.js} +1 -1
- package/dist/client/assets/{vennDiagram-4TSXK5OY-C74llwIj.js → vennDiagram-4TSXK5OY-byr6UQpL.js} +1 -1
- package/dist/client/assets/vennDiagram-4TSXK5OY-byr6UQpL.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-byr6UQpL.js.gz +0 -0
- package/dist/client/assets/{videoCompression-b96RKMFE.js → videoCompression-ivJAy00d.js} +1 -1
- package/dist/client/assets/videoCompression-ivJAy00d.js.br +0 -0
- package/dist/client/assets/videoCompression-ivJAy00d.js.gz +0 -0
- package/dist/client/assets/{wardleyDiagram-VM6X3IG4-DVdgFaBs.js → wardleyDiagram-VM6X3IG4-FJduk-7s.js} +1 -1
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-FJduk-7s.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-FJduk-7s.js.gz +0 -0
- package/dist/client/assets/{xychartDiagram-S5SC5T6Z-BmUxPbqk.js → xychartDiagram-S5SC5T6Z-Ct1fBPFS.js} +1 -1
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-Ct1fBPFS.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-Ct1fBPFS.js.gz +0 -0
- package/dist/client/index.html +1 -1
- package/dist/client/index.html.br +0 -0
- package/dist/client/index.html.gz +0 -0
- package/dist/client/sw.js +1 -1
- package/dist/client/sw.js.br +0 -0
- package/dist/client/sw.js.gz +0 -0
- package/dist/server/agent/collaborationCli.js +77 -17
- package/dist/server/agent/collaborationPrompt.js +3 -1
- package/dist/server/agent/collaborationProtocol.js +1 -1
- package/dist/server/agent/collaborationRoutes.js +7 -1
- package/dist/server/agent/collaborationStore.js +86 -11
- package/dist/server/agent/collaborationTaskService.js +2 -0
- package/dist/server/agent/integrationCli.js +52 -6
- package/dist/server/agent/integrationServer.js +99 -9
- package/dist/server/agent/integrationSessions.js +226 -18
- package/dist/server/agent/integrationStore.js +1 -1
- package/dist/server/agent/sessionLifecycle.js +81 -0
- package/dist/server/agent/sessionResumeConfiguration.js +32 -0
- package/dist/server/cli.js +1 -1
- package/dist/server/routes/terminal.js +75 -1
- package/docs/integrations.md +226 -6
- package/package.json +1 -1
- package/runtime-manifest.json +3 -3
- package/dist/client/assets/App-cagV-flM.js.br +0 -0
- package/dist/client/assets/App-cagV-flM.js.gz +0 -0
- package/dist/client/assets/ArchitecturePanel-TdKPja7L.js.br +0 -0
- package/dist/client/assets/ArchitecturePanel-TdKPja7L.js.gz +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-DOj40Srm.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-DOj40Srm.js.gz +0 -0
- package/dist/client/assets/CollaborationMainWorkspace-C5Tcos5l.js.br +0 -0
- package/dist/client/assets/CollaborationMainWorkspace-C5Tcos5l.js.gz +0 -0
- package/dist/client/assets/CollaborationTaskContent-DQpH2HIj.js +0 -2
- package/dist/client/assets/ComputerControlView-C2sOFyuA.js.br +0 -0
- package/dist/client/assets/ComputerControlView-C2sOFyuA.js.gz +0 -0
- package/dist/client/assets/DagPlayground-DFfVJW00.js.br +0 -0
- package/dist/client/assets/DagPlayground-DFfVJW00.js.gz +0 -0
- package/dist/client/assets/DiffLab-hoj8gwUi.js.br +0 -0
- package/dist/client/assets/DiffLab-hoj8gwUi.js.gz +0 -0
- package/dist/client/assets/DiffReview-Cn_PmN-9.js.br +0 -0
- package/dist/client/assets/DiffReview-Cn_PmN-9.js.gz +0 -0
- package/dist/client/assets/DiffReviewLab-DycgD40E.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-DycgD40E.js.gz +0 -0
- package/dist/client/assets/DiffViewer-DRnuph7U.js.br +0 -0
- package/dist/client/assets/DiffViewer-DRnuph7U.js.gz +0 -0
- package/dist/client/assets/ModelPreview-Dvw-UtQQ.js.br +0 -0
- package/dist/client/assets/ModelPreview-Dvw-UtQQ.js.gz +0 -0
- package/dist/client/assets/RightSidebar-CnnU7Ffp.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-BQVIT5gD.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-BQVIT5gD.js.gz +0 -0
- package/dist/client/assets/app-CXvK8WZg.js.br +0 -0
- package/dist/client/assets/app-CXvK8WZg.js.gz +0 -0
- package/dist/client/assets/arc-DZG1NzUc.js.br +0 -0
- package/dist/client/assets/arc-DZG1NzUc.js.gz +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-Bv15lJGE.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-Bv15lJGE.js.gz +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-DShw-lH7.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-DShw-lH7.js.gz +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-BN6SsyOM.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-BN6SsyOM.js.gz +0 -0
- package/dist/client/assets/channel-YDx-Ycsw.js +0 -1
- package/dist/client/assets/chunk-F27PBJKO-BcpMMU4P.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-BcpMMU4P.js.gz +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-cWvd5gJk.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-cWvd5gJk.js.gz +0 -0
- package/dist/client/assets/chunk-SVP7TREG-Bj8gUFc2.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-Bj8gUFc2.js.gz +0 -0
- package/dist/client/assets/chunk-TICWLB2K-W1cGkC4s.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-W1cGkC4s.js.gz +0 -0
- package/dist/client/assets/classDiagram-ZZMXUADV-qeb16N1g.js +0 -1
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-qeb16N1g.js +0 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-CK0EaMQY.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-CK0EaMQY.js.gz +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-C-7tsMpt.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-C-7tsMpt.js.gz +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-CijqM5Xa.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-CijqM5Xa.js.gz +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-DCLINHGD.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-DCLINHGD.js.gz +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-D-JvTWtL.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-D-JvTWtL.js.gz +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-BT01Oee8.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-BT01Oee8.js.gz +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-C_j4_Ldm.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-C_j4_Ldm.js.gz +0 -0
- package/dist/client/assets/diagram-VX7I27RA-DY62ObO9.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-DY62ObO9.js.gz +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-TnINooIy.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-TnINooIy.js.gz +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-BahaUYTh.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-BahaUYTh.js.gz +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-DNiKhK5j.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-DNiKhK5j.js.gz +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-x9c9CGXD.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-x9c9CGXD.js.gz +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-BofL5Lxs.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-BofL5Lxs.js.gz +0 -0
- package/dist/client/assets/gestureArbiter-Cy5v_ZUL.js.br +0 -0
- package/dist/client/assets/gestureArbiter-Cy5v_ZUL.js.gz +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-CHYuiGg1.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-CHYuiGg1.js.gz +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-CiCmFa95.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-CiCmFa95.js.gz +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-CVnBzo7n.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-CVnBzo7n.js.gz +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-3-zdGxI0.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-3-zdGxI0.js.gz +0 -0
- package/dist/client/assets/layout-DnB4RHWA.js.br +0 -0
- package/dist/client/assets/layout-DnB4RHWA.js.gz +0 -0
- package/dist/client/assets/linear-B_lli4BL.js.br +0 -0
- package/dist/client/assets/linear-B_lli4BL.js.gz +0 -0
- package/dist/client/assets/mermaid.core-ClemZ-kc.js.br +0 -0
- package/dist/client/assets/mermaid.core-ClemZ-kc.js.gz +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-hXDufapN.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-hXDufapN.js.gz +0 -0
- package/dist/client/assets/passwordBootstrap-c-hmAGCQ.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-c-hmAGCQ.js.gz +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-yb1lAfHR.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-yb1lAfHR.js.gz +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-lVC2DWY_.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-lVC2DWY_.js.gz +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-CeqeNP5C.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-CeqeNP5C.js.gz +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-BMEBmjF2.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-BMEBmjF2.js.gz +0 -0
- package/dist/client/assets/rdpSession-D77Fs4-L.js.br +0 -0
- package/dist/client/assets/rdpSession-D77Fs4-L.js.gz +0 -0
- package/dist/client/assets/referenceLongPress-xNXQRqYn.js.br +0 -0
- package/dist/client/assets/referenceLongPress-xNXQRqYn.js.gz +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-CRhmIdDq.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-CRhmIdDq.js.gz +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-CZZPxGRu.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-CZZPxGRu.js.gz +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-CvKYTCaL.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-CvKYTCaL.js.gz +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-DpMFgVd8.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-DpMFgVd8.js.gz +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-BUCKWh69.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-BUCKWh69.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-CrhJVriY.js +0 -1
- package/dist/client/assets/swimlanes-42K2YHIH-Du84aO1l.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-Du84aO1l.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-bsc39ya3.js +0 -8
- package/dist/client/assets/timeline-definition-24CTP7MA-94wGyYVM.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-94wGyYVM.js.gz +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-C74llwIj.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-C74llwIj.js.gz +0 -0
- package/dist/client/assets/videoCompression-b96RKMFE.js.br +0 -0
- package/dist/client/assets/videoCompression-b96RKMFE.js.gz +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-DVdgFaBs.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-DVdgFaBs.js.gz +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-BmUxPbqk.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-BmUxPbqk.js.gz +0 -0
|
@@ -4,12 +4,20 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { timingSafeEqual } from 'node:crypto';
|
|
7
|
-
import { CollaborationError } from './collaborationProtocol.js';
|
|
7
|
+
import { CollaborationError, resolveIdPrefix } from './collaborationProtocol.js';
|
|
8
8
|
import { terminalMessage } from './collaborationStore.js';
|
|
9
9
|
import { IntegrationStore, INTEGRATION_PROTOCOL, INTEGRATION_OPERATIONS, integrationError } from './integrationStore.js';
|
|
10
10
|
import { IntegrationSessions, publicIntegrationSession } from './integrationSessions.js';
|
|
11
11
|
import { getTermdockVersion } from '../utils/version.js';
|
|
12
12
|
export function integrationSocketPath(port) { return path.join(homedir(), '.termdock', `integration-${port}.sock`); }
|
|
13
|
+
function scopedId(input, ids, type) {
|
|
14
|
+
if (typeof input !== 'string')
|
|
15
|
+
integrationError(`INVALID_${type}_ID`, 'A message or member id is required');
|
|
16
|
+
const result = resolveIdPrefix([...new Set(ids)], input);
|
|
17
|
+
if (result.status !== 'ok')
|
|
18
|
+
integrationError(`${type}_${result.status === 'ambiguous' ? 'ID_AMBIGUOUS' : 'NOT_FOUND'}`, `No unique ${type.toLowerCase()} in this integration group`, result.status === 'ambiguous' ? 409 : 404);
|
|
19
|
+
return result.id;
|
|
20
|
+
}
|
|
13
21
|
/** A local authenticated API and push stream. It is never mounted on the
|
|
14
22
|
* public HTTP application and does not reuse an integration's admin token. */
|
|
15
23
|
export class IntegrationRuntime {
|
|
@@ -26,6 +34,7 @@ export class IntegrationRuntime {
|
|
|
26
34
|
this.options = options;
|
|
27
35
|
this.store = new IntegrationStore(path.join(options.directory, 'integrations.json'));
|
|
28
36
|
this.sessions = new IntegrationSessions(path.join(options.directory, 'integration-sessions.json'), options.sessions);
|
|
37
|
+
this.stops.push(options.messages.registerRuntimeGuard(id => this.sessions.assertRuntimeMessage(id)));
|
|
29
38
|
const changed = () => { try {
|
|
30
39
|
this.sync();
|
|
31
40
|
}
|
|
@@ -35,7 +44,7 @@ export class IntegrationRuntime {
|
|
|
35
44
|
} };
|
|
36
45
|
this.stops.push(options.messages.subscribe(changed), options.taskStore.subscribe(changed), this.sessions.subscribe(changed));
|
|
37
46
|
const app = express();
|
|
38
|
-
app.use(express.json({ limit: '
|
|
47
|
+
app.use(express.json({ limit: '2mb' }));
|
|
39
48
|
app.use((req, res, next) => {
|
|
40
49
|
if (req.headers['x-termdock-integration-protocol'] !== String(INTEGRATION_PROTOCOL)) {
|
|
41
50
|
res.status(409).json({ ...this.error(new CollaborationError('INCOMPATIBLE_INTEGRATION_PROTOCOL', 'Unsupported integration protocol', 409)), protocol: INTEGRATION_PROTOCOL });
|
|
@@ -62,6 +71,8 @@ export class IntegrationRuntime {
|
|
|
62
71
|
app.get('/admin/groups', run(req => { admin(req); return { groups: options.messages.list().filter(g => !g.federated).map(g => ({ id: g.id, name: g.name })) }; }));
|
|
63
72
|
app.get('/admin/capabilities', run(req => { admin(req); return this.capabilities(); }));
|
|
64
73
|
app.get('/admin/sessions/:id/restore-diagnostics', run(req => { admin(req); return this.sessions.restoreDiagnostics(req.params.id); }));
|
|
74
|
+
app.get('/admin/sessions/:id/resume-configuration', run(async (req) => { admin(req); return this.sessions.resumeConfiguration(req.params.id); }));
|
|
75
|
+
app.post('/admin/sessions/:id/resume-configuration', run(async (req) => { admin(req); return { session: publicIntegrationSession(await this.sessions.confirmResumeConfiguration(req.params.id, req.body?.generation, req.body?.fingerprint)) }; }));
|
|
65
76
|
app.get('/admin/principals', run(req => { admin(req); return { integrations: this.store.policies() }; }));
|
|
66
77
|
app.post('/admin/principals', run(req => { admin(req); this.group(req.body?.groupId); const provisioned = this.store.provision(req.body); try {
|
|
67
78
|
this.sync();
|
|
@@ -103,11 +114,70 @@ export class IntegrationRuntime {
|
|
|
103
114
|
const event = options.taskStore.integrationRequestEvent(p.id, input.idempotencyKey, task.id);
|
|
104
115
|
return { task, event_id: event?.id ?? null, attempt_id: event?.attemptId ?? null, delivery_id: event?.deliveryId ?? null };
|
|
105
116
|
}));
|
|
117
|
+
app.post('/send', run(req => {
|
|
118
|
+
const p = principal(req);
|
|
119
|
+
this.authorize(p, 'message.send', req.body?.group_id);
|
|
120
|
+
const group = this.group(p.groupId), raw = req.body ?? {};
|
|
121
|
+
if (raw.kind && raw.kind !== 'message')
|
|
122
|
+
integrationError('INTEGRATION_OPERATION_DENIED', 'Integration send uses ordinary messages only', 403);
|
|
123
|
+
const targets = Array.isArray(raw.toSessionIds) ? raw.toSessionIds : [raw.targetSessionId];
|
|
124
|
+
if (!targets.length || targets.some((id) => typeof id !== 'string'))
|
|
125
|
+
integrationError('INVALID_MESSAGE_TARGET', 'Specify a local group member');
|
|
126
|
+
const descriptors = this.sessions.snapshot().filter(s => s.group_id === p.groupId);
|
|
127
|
+
const recipients = targets.map((id) => scopedId(id, [...group.sessionIds, ...descriptors.map(s => s.session_id)], 'SESSION'));
|
|
128
|
+
for (const id of recipients) {
|
|
129
|
+
this.sessions.assertMessageTarget(p, id);
|
|
130
|
+
if (!group.sessionIds.includes(id))
|
|
131
|
+
integrationError('SESSION_NOT_FOUND', 'Restore the original runtime and membership before sending', 404);
|
|
132
|
+
}
|
|
133
|
+
let thread;
|
|
134
|
+
if (raw.thread_id)
|
|
135
|
+
thread = scopedId(raw.thread_id, options.messages.snapshotMessages().filter(m => m.groupId === p.groupId).map(m => m.threadId), 'THREAD');
|
|
136
|
+
const messages = options.messages.send({ ...this.messageExtras(raw), groupId: p.groupId, fromSessionId: null,
|
|
137
|
+
integrationOrigin: this.origin(p, raw.origin), toSessionIds: recipients, kind: 'message', content: raw.message, threadId: thread });
|
|
138
|
+
this.sync();
|
|
139
|
+
return { ok: true, message_ids: messages.map(m => m.id), receipts: messages.map(m => options.messages.receipt(m.id)), ...options.messages.receipt(messages[0].id) };
|
|
140
|
+
}));
|
|
141
|
+
app.post('/reply', run(req => {
|
|
142
|
+
const p = principal(req);
|
|
143
|
+
this.authorize(p, 'message.send', req.body?.group_id);
|
|
144
|
+
const raw = req.body ?? {}, original = this.message(p, raw.messageId);
|
|
145
|
+
const target = original.toPrincipalId === p.id ? original.fromSessionId
|
|
146
|
+
: original.integrationOrigin?.integrationId === p.id ? original.toSessionId : null;
|
|
147
|
+
if (target)
|
|
148
|
+
this.sessions.assertMessageTarget(p, target);
|
|
149
|
+
if (!target || !this.group(p.groupId).sessionIds.includes(target))
|
|
150
|
+
integrationError('NO_REPLY_TARGET', 'Reply requires your own message and its original live group member', 409);
|
|
151
|
+
const messages = options.messages.send({ ...this.messageExtras(raw), groupId: p.groupId, fromSessionId: null,
|
|
152
|
+
integrationOrigin: this.origin(p, raw.origin), toSessionIds: [target], kind: 'reply', content: raw.content, threadId: original.threadId, replyTo: original.id });
|
|
153
|
+
this.sync();
|
|
154
|
+
return { ok: true, ...options.messages.receipt(messages[0].id) };
|
|
155
|
+
}));
|
|
156
|
+
app.get('/message', run(req => {
|
|
157
|
+
const p = principal(req);
|
|
158
|
+
this.authorize(p, 'message.read', req.query.group);
|
|
159
|
+
const messages = options.messages.snapshotMessages().filter(m => m.groupId === p.groupId).sort((a, b) => a.sequence - b.sequence);
|
|
160
|
+
const after = req.query['after-id'] ? this.message(p, String(req.query['after-id'])) : null;
|
|
161
|
+
const limit = req.query.limit === undefined ? 50 : Number(req.query.limit);
|
|
162
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200)
|
|
163
|
+
integrationError('INVALID_MESSAGE_LIMIT', 'limit must be 1..200');
|
|
164
|
+
const thread = req.query.thread ? scopedId(String(req.query.thread), messages.map(m => m.threadId), 'THREAD') : null;
|
|
165
|
+
const filtered = messages.filter(m => (!after || m.sequence > after.sequence) && (!thread || m.threadId === thread));
|
|
166
|
+
const page = [];
|
|
167
|
+
let bytes = 0;
|
|
168
|
+
for (const message of filtered.slice(0, limit)) {
|
|
169
|
+
const size = Buffer.byteLength(JSON.stringify(message));
|
|
170
|
+
if (page.length && bytes + size > 2 * 1024 * 1024)
|
|
171
|
+
break;
|
|
172
|
+
page.push(message);
|
|
173
|
+
bytes += size;
|
|
174
|
+
}
|
|
175
|
+
return { messages: page.map(m => terminalMessage(m)), next_after_id: page.at(-1)?.id ?? after?.id ?? null, has_more: filtered.length > page.length, retention: 'retained_messages_only' };
|
|
176
|
+
}));
|
|
106
177
|
app.get('/message/:id', run(req => {
|
|
107
|
-
const p = principal(req)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
this.authorize(p, 'task.read', message.groupId);
|
|
178
|
+
const p = principal(req);
|
|
179
|
+
this.authorize(p, p.permissions.includes('message.read') ? 'message.read' : 'task.read');
|
|
180
|
+
const message = this.message(p, req.params.id);
|
|
111
181
|
const receipt = options.messages.receipt(message.id);
|
|
112
182
|
const { snapshot: _, ...diagnostic } = receipt;
|
|
113
183
|
return { ok: true, ...diagnostic, ...(req.query.receipt_only === 'true' ? {} : { message: terminalMessage(message) }) };
|
|
@@ -121,6 +191,7 @@ export class IntegrationRuntime {
|
|
|
121
191
|
}));
|
|
122
192
|
app.get('/sessions/:id', run(async (req) => { const p = principal(req); this.authorize(p, 'session.read'); return { session: publicIntegrationSession(await this.sessions.get(p, req.params.id)) }; }));
|
|
123
193
|
app.post('/sessions/:id/restore', run(async (req) => { const p = principal(req); this.authorize(p, 'session.restore'); const session = await this.sessions.restore(p, req.params.id, req.body?.idempotency_key); this.sync(); return { session: publicIntegrationSession(session) }; }));
|
|
194
|
+
app.post('/sessions/:id/release', run(async (req) => { const p = principal(req); this.authorize(p, 'session.release'); const session = await this.sessions.release(p, req.params.id, req.body?.generation, req.body?.idempotency_key); this.sync(); return { session: publicIntegrationSession(session) }; }));
|
|
124
195
|
app.get('/events', run((req, res) => {
|
|
125
196
|
const p = principal(req);
|
|
126
197
|
this.authorize(p, 'events.read', req.query.group);
|
|
@@ -144,11 +215,13 @@ export class IntegrationRuntime {
|
|
|
144
215
|
integration_principal: true, group_scoped_permissions: true, durable_event_push: true,
|
|
145
216
|
event_ack_semantics: 'durable_received', correlated_task_responses: true, accepted_task_comments: true,
|
|
146
217
|
idempotent_session_create: true, exact_session_restore: true, external_validation: false,
|
|
147
|
-
startup_input_conditions: true,
|
|
218
|
+
startup_input_conditions: true, session_runtime_release: true, stable_session_identity: true, session_lifecycle_generation: true,
|
|
219
|
+
session_resume_configuration: true, session_lifecycle_min_version: '1.4.309',
|
|
148
220
|
integration_policy_update: true,
|
|
149
221
|
session_restore_diagnostics: true,
|
|
150
222
|
native_identity_linux_flock_owner: process.platform === 'linux',
|
|
151
223
|
background_task_records: true, task_purpose_configuration: true, explicit_execution_state: true,
|
|
224
|
+
integration_message_send: true, integration_message_reply: true, integration_message_history: true,
|
|
152
225
|
permissions: INTEGRATION_OPERATIONS };
|
|
153
226
|
}
|
|
154
227
|
error(error) {
|
|
@@ -170,6 +243,18 @@ export class IntegrationRuntime {
|
|
|
170
243
|
integrationError('TASK_NOT_FOUND', 'Task does not exist within this integration grant', 404);
|
|
171
244
|
this.authorize(p, operation, task.groupId);
|
|
172
245
|
}
|
|
246
|
+
message(p, id) {
|
|
247
|
+
const scoped = this.options.messages.snapshotMessages().filter(m => m.groupId === p.groupId);
|
|
248
|
+
const resolved = scopedId(id, scoped.map(m => m.id), 'MESSAGE');
|
|
249
|
+
return this.options.messages.getMessage(resolved);
|
|
250
|
+
}
|
|
251
|
+
messageExtras(raw) {
|
|
252
|
+
if (typeof raw.idempotency_key !== 'string' || !raw.idempotency_key.trim())
|
|
253
|
+
integrationError('IDEMPOTENCY_KEY_REQUIRED', 'Integration message mutations require an explicit idempotency key');
|
|
254
|
+
if (raw.task || raw.task_envelope || raw.metadata?.termdockTask)
|
|
255
|
+
integrationError('INTEGRATION_OPERATION_DENIED', 'Ordinary messages cannot forge task envelopes', 403);
|
|
256
|
+
return { idempotencyKey: raw.idempotency_key, responseKind: raw.response_kind, metadata: raw.metadata, expiresAt: raw.expires_at };
|
|
257
|
+
}
|
|
173
258
|
origin(p, input) {
|
|
174
259
|
const value = input && typeof input === 'object' ? input : {};
|
|
175
260
|
if (Buffer.byteLength(JSON.stringify(value)) > 16384)
|
|
@@ -215,11 +300,16 @@ export class IntegrationRuntime {
|
|
|
215
300
|
if (groups.has(message.groupId)) {
|
|
216
301
|
const link = message.metadata?.termdockTask;
|
|
217
302
|
const context = { group_id: message.groupId, task_id: link?.taskId ?? null, attempt_id: link?.attemptId ?? null,
|
|
218
|
-
reply_to_event_id: link?.replyToEventId ?? null, message_id: message.id, source: message.fromSessionId ? 'member' : 'system'
|
|
303
|
+
reply_to_event_id: link?.replyToEventId ?? null, message_id: message.id, source: message.integrationOrigin ? 'integration' : message.fromSessionId ? 'member' : 'system',
|
|
304
|
+
actor: message.integrationOrigin ? { integration_id: message.integrationOrigin.integrationId } : message.fromSessionId ? { session_id: message.fromSessionId } : null,
|
|
305
|
+
external_actor: message.integrationOrigin?.externalActor ?? null };
|
|
219
306
|
updates.push({ sourceKey: `message:${message.id}`, sourceVersion: '1', event: { ...context,
|
|
220
307
|
kind: message.kind === 'reply' ? 'message.reply' : 'message.queued', created_at: message.createdAt,
|
|
221
308
|
payload: { message_id: message.id, thread_id: message.threadId, reply_to: message.replyTo ?? null,
|
|
222
|
-
from_session_id: message.fromSessionId, to_session_id: message.
|
|
309
|
+
from_session_id: message.fromSessionId, to_session_id: message.toPrincipalId ? null : message.toSessionId,
|
|
310
|
+
to_principal_id: message.toPrincipalId ?? null, integration_origin: message.integrationOrigin ?? null,
|
|
311
|
+
response_kind: message.responseKind ?? null, stored_at: message.storedAt ?? null,
|
|
312
|
+
content: message.content, metadata: message.metadata ?? null } } });
|
|
223
313
|
const { snapshot: _, ...receipt } = this.options.messages.receipt(message.id);
|
|
224
314
|
updates.push({ sourceKey: `message-status:${message.id}`, sourceVersion: JSON.stringify(receipt), event: {
|
|
225
315
|
...context, kind: 'message.delivery', created_at: message.deliveredAt ?? message.createdAt, payload: { receipt }
|
|
@@ -2,6 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
4
4
|
import { atomicJson, integrationError } from './integrationStore.js';
|
|
5
|
+
import { SessionLifecycleCoordinator, assertMessageRuntime, assertReleaseFacts, assertReleaseGeneration } from './sessionLifecycle.js';
|
|
6
|
+
import { resumeConfigurationFingerprint } from './sessionResumeConfiguration.js';
|
|
5
7
|
import { NativeResumeOwnerError } from './nativeResumeOwner.js';
|
|
6
8
|
/** Quote each configured argument as literal shell data, including paths. */
|
|
7
9
|
export function integrationLaunchCommand(record, restore) {
|
|
@@ -50,7 +52,8 @@ export class IntegrationSessions {
|
|
|
50
52
|
doc = { version: 1, sessions: [], requests: {} };
|
|
51
53
|
pending = new Map();
|
|
52
54
|
listeners = new Set();
|
|
53
|
-
|
|
55
|
+
lifecycle = new SessionLifecycleCoordinator();
|
|
56
|
+
mutations = new Map();
|
|
54
57
|
timers = new Set();
|
|
55
58
|
stopObservation;
|
|
56
59
|
stopDeliveryGuard;
|
|
@@ -70,8 +73,12 @@ export class IntegrationSessions {
|
|
|
70
73
|
if (error.code !== 'ENOENT')
|
|
71
74
|
throw error;
|
|
72
75
|
}
|
|
76
|
+
for (const record of this.doc.sessions) {
|
|
77
|
+
record.generation ??= 1;
|
|
78
|
+
record.runtime_present ??= null;
|
|
79
|
+
}
|
|
73
80
|
for (const record of this.doc.sessions)
|
|
74
|
-
if (['starting', 'restoring', 'binding_pending'].includes(record.state) || record.startup_input?.state === 'pending')
|
|
81
|
+
if (['starting', 'restoring', 'binding_pending'].includes(record.state) || record.state === 'releasing' || record.startup_input?.state === 'pending')
|
|
75
82
|
this.scheduleRefresh(record);
|
|
76
83
|
// A pre-restart partial match cannot prove continuity of the live screen.
|
|
77
84
|
for (const record of this.doc.sessions)
|
|
@@ -89,12 +96,17 @@ export class IntegrationSessions {
|
|
|
89
96
|
}
|
|
90
97
|
subscribe(listener) { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; }
|
|
91
98
|
snapshot() { return structuredClone(this.doc.sessions); }
|
|
92
|
-
save() {
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
save() {
|
|
100
|
+
if (Buffer.byteLength(JSON.stringify(this.doc)) > 64 * 1024 * 1024)
|
|
101
|
+
integrationError('SESSION_STORAGE_FULL', 'Recovery descriptor journal is full; no new operation was committed', 503);
|
|
102
|
+
atomicJson(this.file, this.doc);
|
|
103
|
+
for (const listener of this.listeners) {
|
|
104
|
+
try {
|
|
105
|
+
listener();
|
|
106
|
+
}
|
|
107
|
+
catch { /* Durable session state is reconciled on restart. */ }
|
|
95
108
|
}
|
|
96
|
-
|
|
97
|
-
} }
|
|
109
|
+
}
|
|
98
110
|
saveRecord(record) {
|
|
99
111
|
const before = this.doc.sessions;
|
|
100
112
|
this.doc.sessions = [...before.filter(s => s.session_id !== record.session_id), structuredClone(record)];
|
|
@@ -139,7 +151,7 @@ export class IntegrationSessions {
|
|
|
139
151
|
integrationError('INTEGRATION_SESSION_LIMIT', 'Integration session storage is full', 409);
|
|
140
152
|
const now = Date.now(), record = { operation_id: randomUUID(), session_id: randomBytes(6).toString('hex'),
|
|
141
153
|
group_id: principal.groupId, principal_id: principal.id, agent_slug: profile.agentSlug, agent_native_session_id: null,
|
|
142
|
-
state: 'starting', error_code: null, cwd, launch_profile: profile.id, profile: structuredClone(profile),
|
|
154
|
+
state: 'starting', generation: 1, runtime_present: null, error_code: null, cwd, launch_profile: profile.id, profile: structuredClone(profile),
|
|
143
155
|
created_at: now, updated_at: now, deadline: now + this.bindingTimeout, launch_submitted: false };
|
|
144
156
|
record.startup_input = startupInput(record, now);
|
|
145
157
|
this.doc.requests[request.scoped] = { hash: request.hash, sessionId: record.session_id };
|
|
@@ -156,6 +168,21 @@ export class IntegrationSessions {
|
|
|
156
168
|
const promise = (async () => {
|
|
157
169
|
try {
|
|
158
170
|
// Store intent before launching. A failed persistence cannot execute.
|
|
171
|
+
if (this.adapter.resumeConfiguration) {
|
|
172
|
+
try {
|
|
173
|
+
const fingerprint = await resumeConfigurationFingerprint(record, this.adapter);
|
|
174
|
+
if (record.resume_configuration_fingerprint && record.resume_configuration_fingerprint !== fingerprint)
|
|
175
|
+
integrationError('RESUME_CONFIGURATION_CHANGED', 'Recovery launcher or plugin semantics changed', 409);
|
|
176
|
+
if (!restoring)
|
|
177
|
+
record.resume_configuration_fingerprint = fingerprint;
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
// Existing creation supports plugins without an exact resume path.
|
|
181
|
+
// They remain launchable, but cannot gain a releasable descriptor.
|
|
182
|
+
if (restoring || error.code !== 'EXACT_RESUME_UNSUPPORTED')
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
159
186
|
record.launch_submitted = true;
|
|
160
187
|
this.saveRecord(record);
|
|
161
188
|
const prepared = (binding) => { record.terminal_binding = binding; this.saveRecord(record); };
|
|
@@ -187,27 +214,74 @@ export class IntegrationSessions {
|
|
|
187
214
|
return structuredClone({ session_id: record.session_id, operation_id: record.operation_id,
|
|
188
215
|
state: record.state, error_code: record.error_code, diagnostics: record.restore_diagnostics ?? null });
|
|
189
216
|
}
|
|
190
|
-
|
|
217
|
+
assertRuntimeMessage(id) { assertMessageRuntime(this.doc.sessions.find(s => s.session_id === id)); }
|
|
218
|
+
assertMessageTarget(principal, id) {
|
|
219
|
+
const record = this.doc.sessions.find(s => s.session_id === id && s.group_id === principal.groupId);
|
|
220
|
+
assertMessageRuntime(record);
|
|
221
|
+
}
|
|
222
|
+
async mutate(principal, key, input, action) {
|
|
223
|
+
const request = this.request(principal, key, input);
|
|
224
|
+
if (request.previous?.result) {
|
|
225
|
+
this.lookup(principal, request.previous.sessionId);
|
|
226
|
+
return structuredClone(request.previous.result);
|
|
227
|
+
}
|
|
228
|
+
const active = this.mutations.get(request.scoped);
|
|
229
|
+
if (active) {
|
|
230
|
+
if (active.hash !== request.hash)
|
|
231
|
+
integrationError('IDEMPOTENCY_CONFLICT', 'Operation key parameters changed', 409);
|
|
232
|
+
return active.promise;
|
|
233
|
+
}
|
|
234
|
+
const promise = action().then(result => {
|
|
235
|
+
if (this.doc.requests[request.scoped]) {
|
|
236
|
+
const before = this.doc.requests[request.scoped];
|
|
237
|
+
this.doc.requests[request.scoped] = { ...before, result: structuredClone(result) };
|
|
238
|
+
try {
|
|
239
|
+
this.save();
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
this.doc.requests[request.scoped] = before;
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}).finally(() => this.mutations.delete(request.scoped));
|
|
248
|
+
this.mutations.set(request.scoped, { hash: request.hash, promise });
|
|
249
|
+
return promise;
|
|
250
|
+
}
|
|
251
|
+
async replayRequest(principal, id, request) {
|
|
252
|
+
const record = await (this.pending.get(id) ?? this.get(principal, id));
|
|
253
|
+
if (request.operationId && (record.operation_id !== request.operationId || record.generation !== request.generation))
|
|
254
|
+
integrationError('OPERATION_OUTCOME_UNCONFIRMED', 'Original operation result was not committed and a later generation exists; read session get, do not reuse this key', 409);
|
|
255
|
+
return record;
|
|
256
|
+
}
|
|
257
|
+
async restore(principal, id, key) {
|
|
258
|
+
return this.mutate(principal, key, ['restore', id], () => this.lifecycle.run(id, () => this.restoreOnce(principal, id, key)));
|
|
259
|
+
}
|
|
260
|
+
async restoreOnce(principal, id, idempotencyKey) {
|
|
191
261
|
const request = this.request(principal, idempotencyKey, ['restore', id]);
|
|
192
262
|
if (request.previous)
|
|
193
|
-
return this.
|
|
194
|
-
if (this.pending.has(id)
|
|
263
|
+
return this.replayRequest(principal, id, request.previous);
|
|
264
|
+
if (this.pending.has(id))
|
|
195
265
|
integrationError('SESSION_OPERATION_IN_PROGRESS', 'Another session operation is in progress', 409);
|
|
196
|
-
|
|
266
|
+
let releaseNative;
|
|
197
267
|
try {
|
|
198
268
|
const record = await this.get(principal, id);
|
|
199
269
|
if (!record.agent_native_session_id)
|
|
200
270
|
integrationError('NATIVE_SESSION_ID_MISSING', 'An exact native session binding is required', 409);
|
|
201
271
|
// Recheck the current grant and actual directories; restore uses the
|
|
202
272
|
// original profile snapshot, never changed model or wrapper defaults.
|
|
203
|
-
|
|
273
|
+
const authorizedProfile = principal.launchProfiles.find(p => p.id === record.launch_profile);
|
|
274
|
+
if (!authorizedProfile)
|
|
204
275
|
integrationError('LAUNCH_PROFILE_DENIED', 'Original launch profile is no longer authorized', 403);
|
|
276
|
+
permittedCwd(authorizedProfile, record.cwd);
|
|
205
277
|
permittedCwd(record.profile, record.cwd);
|
|
278
|
+
if (record.operation_uncertain || record.state === 'releasing')
|
|
279
|
+
integrationError('OPERATION_OUTCOME_UNCONFIRMED', 'Reconcile prior release using session get before restore', 409);
|
|
206
280
|
const observed = await this.adapter.inspect(record);
|
|
207
281
|
if (observed.running) {
|
|
208
282
|
if (observed.agentSlug !== record.agent_slug || observed.nativeId !== record.agent_native_session_id)
|
|
209
283
|
integrationError('SESSION_IDENTITY_MISMATCH', 'Existing process does not prove the exact native binding', 409);
|
|
210
|
-
this.doc.requests[request.scoped] = { hash: request.hash, sessionId: id };
|
|
284
|
+
this.doc.requests[request.scoped] = { hash: request.hash, sessionId: id, operationId: record.operation_id, generation: record.generation };
|
|
211
285
|
try {
|
|
212
286
|
this.save();
|
|
213
287
|
}
|
|
@@ -221,6 +295,13 @@ export class IntegrationSessions {
|
|
|
221
295
|
integrationError('SESSION_OPERATION_IN_PROGRESS', 'Prior launch outcome is not yet resolved; inspect the original session', 409);
|
|
222
296
|
if (observed.exists && !observed.shell)
|
|
223
297
|
integrationError('SESSION_TARGET_NOT_SHELL', 'Original pane is not a verified shell', 409);
|
|
298
|
+
if (record.state === 'released' && !record.resume_configuration_fingerprint)
|
|
299
|
+
integrationError('RESUME_CONFIGURATION_UNCONFIRMED', 'Administrator must explicitly confirm the current recovery configuration', 409);
|
|
300
|
+
releaseNative = this.lifecycle.reserveNative(record, this.doc.sessions);
|
|
301
|
+
if (record.resume_configuration_fingerprint && record.resume_configuration_fingerprint !== await resumeConfigurationFingerprint(record, this.adapter))
|
|
302
|
+
integrationError('RESUME_CONFIGURATION_CHANGED', 'Recovery launcher or plugin semantics changed', 409);
|
|
303
|
+
record.generation = (record.generation ?? 1) + 1;
|
|
304
|
+
record.runtime_present = observed.exists;
|
|
224
305
|
record.state = 'restoring';
|
|
225
306
|
record.error_code = null;
|
|
226
307
|
record.operation_id = randomUUID();
|
|
@@ -230,7 +311,7 @@ export class IntegrationSessions {
|
|
|
230
311
|
if (!record.profile.startupInput && !record.startup_condition)
|
|
231
312
|
record.startup_condition = structuredClone(principal.launchProfiles.find(profile => profile.id === record.launch_profile)?.startupInput);
|
|
232
313
|
record.startup_input = startupInput(record, Date.now());
|
|
233
|
-
this.doc.requests[request.scoped] = { hash: request.hash, sessionId: id };
|
|
314
|
+
this.doc.requests[request.scoped] = { hash: request.hash, sessionId: id, operationId: record.operation_id, generation: record.generation };
|
|
234
315
|
try {
|
|
235
316
|
this.saveRecord(record);
|
|
236
317
|
}
|
|
@@ -241,13 +322,135 @@ export class IntegrationSessions {
|
|
|
241
322
|
return await this.launch(record, true);
|
|
242
323
|
}
|
|
243
324
|
finally {
|
|
244
|
-
|
|
325
|
+
releaseNative?.();
|
|
245
326
|
}
|
|
246
327
|
}
|
|
328
|
+
async resumeConfiguration(id) {
|
|
329
|
+
const record = this.doc.sessions.find(s => s.session_id === id);
|
|
330
|
+
if (!record)
|
|
331
|
+
return integrationError('INTEGRATION_SESSION_NOT_FOUND', 'Recovery record does not exist', 404);
|
|
332
|
+
return { session_id: id, generation: record.generation ?? 1, recorded_fingerprint: record.resume_configuration_fingerprint ?? null,
|
|
333
|
+
current_fingerprint: await resumeConfigurationFingerprint(record, this.adapter), confirmation_required: !record.resume_configuration_fingerprint };
|
|
334
|
+
}
|
|
335
|
+
async confirmResumeConfiguration(id, generation, fingerprint) {
|
|
336
|
+
return this.lifecycle.run(id, async () => {
|
|
337
|
+
const record = structuredClone(this.doc.sessions.find(s => s.session_id === id));
|
|
338
|
+
if (!record)
|
|
339
|
+
return integrationError('INTEGRATION_SESSION_NOT_FOUND', 'Recovery record does not exist', 404);
|
|
340
|
+
assertReleaseGeneration(record, generation);
|
|
341
|
+
if (this.pending.has(id) || ['starting', 'restoring', 'binding_pending', 'releasing'].includes(record.state))
|
|
342
|
+
integrationError('SESSION_OPERATION_IN_PROGRESS', 'Resolve the current operation before confirming compatibility', 409);
|
|
343
|
+
if (!/^[a-f0-9]{64}$/.test(fingerprint) || fingerprint !== await resumeConfigurationFingerprint(record, this.adapter))
|
|
344
|
+
integrationError('RESUME_CONFIGURATION_CHANGED', 'Current fingerprint no longer matches the administrator confirmation', 409);
|
|
345
|
+
record.resume_configuration_fingerprint = fingerprint;
|
|
346
|
+
record.generation = generation + 1;
|
|
347
|
+
record.updated_at = Date.now();
|
|
348
|
+
this.saveRecord(record);
|
|
349
|
+
return record;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
async release(principal, id, generation, key) {
|
|
353
|
+
return this.mutate(principal, key, ['release', id, generation], () => this.lifecycle.run(id, async () => {
|
|
354
|
+
const request = this.request(principal, key, ['release', id, generation]);
|
|
355
|
+
if (request.previous)
|
|
356
|
+
return this.replayRequest(principal, id, request.previous);
|
|
357
|
+
if (!this.adapter.release || !this.adapter.runtimePresent)
|
|
358
|
+
integrationError('SESSION_RELEASE_UNSUPPORTED', 'Runtime adapter does not support release', 409);
|
|
359
|
+
if (this.pending.has(id))
|
|
360
|
+
integrationError('SESSION_OPERATION_IN_PROGRESS', 'Launch is in progress', 409);
|
|
361
|
+
const record = await this.get(principal, id);
|
|
362
|
+
assertReleaseGeneration(record, generation);
|
|
363
|
+
if (record.state !== 'released') {
|
|
364
|
+
assertReleaseFacts(record, await this.adapter.inspect(record));
|
|
365
|
+
if (this.adapter.pendingMessages?.(id))
|
|
366
|
+
integrationError('SESSION_PENDING_MESSAGES', 'Pending messages must be resolved before release', 409);
|
|
367
|
+
if (!record.resume_configuration_fingerprint)
|
|
368
|
+
integrationError('RESUME_CONFIGURATION_UNCONFIRMED', 'Administrator must inspect and confirm the recovery configuration before release', 409);
|
|
369
|
+
if (record.resume_configuration_fingerprint !== await resumeConfigurationFingerprint(record, this.adapter))
|
|
370
|
+
integrationError('RESUME_CONFIGURATION_CHANGED', 'Recovery launcher or plugin semantics changed', 409);
|
|
371
|
+
}
|
|
372
|
+
if (record.state === 'released') {
|
|
373
|
+
this.doc.requests[request.scoped] = { hash: request.hash, sessionId: id, operationId: record.operation_id, generation: record.generation };
|
|
374
|
+
try {
|
|
375
|
+
this.save();
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
delete this.doc.requests[request.scoped];
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
return record;
|
|
382
|
+
}
|
|
383
|
+
if (this.adapter.prepareRelease)
|
|
384
|
+
record.group_role = (await this.adapter.prepareRelease(record)).group_role;
|
|
385
|
+
record.state = 'releasing';
|
|
386
|
+
record.operation_id = randomUUID();
|
|
387
|
+
record.generation = generation + 1;
|
|
388
|
+
record.operation_uncertain = false;
|
|
389
|
+
record.error_code = null;
|
|
390
|
+
record.updated_at = Date.now();
|
|
391
|
+
this.doc.requests[request.scoped] = { hash: request.hash, sessionId: id, operationId: record.operation_id, generation: record.generation };
|
|
392
|
+
try {
|
|
393
|
+
this.saveRecord(record);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
delete this.doc.requests[request.scoped];
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
await this.adapter.release(record, false);
|
|
401
|
+
if (await this.adapter.runtimePresent(record))
|
|
402
|
+
integrationError('OPERATION_OUTCOME_UNCONFIRMED', 'Runtime removal could not be confirmed', 409);
|
|
403
|
+
record.state = 'released';
|
|
404
|
+
record.runtime_present = false;
|
|
405
|
+
record.terminal_binding = null;
|
|
406
|
+
record.startup_input = undefined;
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
record.state = 'failed';
|
|
410
|
+
record.operation_uncertain = true;
|
|
411
|
+
record.runtime_present = null;
|
|
412
|
+
record.error_code = typeof error.code === 'string' ? error.code : 'SESSION_RELEASE_FAILED';
|
|
413
|
+
}
|
|
414
|
+
record.updated_at = Date.now();
|
|
415
|
+
this.saveRecord(record);
|
|
416
|
+
return record;
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
247
419
|
async refresh(id) {
|
|
248
420
|
const record = structuredClone(this.doc.sessions.find(s => s.session_id === id));
|
|
249
421
|
if (!record)
|
|
250
422
|
return;
|
|
423
|
+
if (record.state === 'released' || record.state === 'releasing' || record.operation_uncertain) {
|
|
424
|
+
if (this.lifecycle.active(id))
|
|
425
|
+
return;
|
|
426
|
+
try {
|
|
427
|
+
if (record.state !== 'released')
|
|
428
|
+
await this.adapter.release?.(record, true);
|
|
429
|
+
const present = await this.adapter.runtimePresent?.(record);
|
|
430
|
+
if (present === undefined)
|
|
431
|
+
integrationError('SESSION_RELEASE_UNSUPPORTED', 'Runtime presence is unavailable', 409);
|
|
432
|
+
if (record.state !== 'released') {
|
|
433
|
+
if (present)
|
|
434
|
+
integrationError('OPERATION_OUTCOME_UNCONFIRMED', 'Prior release still has a runtime; explicit release retry is required', 409);
|
|
435
|
+
// Absent runtime metadata has been reconciled without killing a live runtime.
|
|
436
|
+
}
|
|
437
|
+
else if (present)
|
|
438
|
+
integrationError('OPERATION_OUTCOME_UNCONFIRMED', 'A runtime exists for a released descriptor', 409);
|
|
439
|
+
const next = { ...record, state: 'released', runtime_present: false, terminal_binding: null, startup_input: undefined, operation_uncertain: false, error_code: null };
|
|
440
|
+
if (JSON.stringify(this.doc.sessions.find(s => s.session_id === id)) !== JSON.stringify(record))
|
|
441
|
+
return;
|
|
442
|
+
if (JSON.stringify(next) !== JSON.stringify(record))
|
|
443
|
+
this.saveRecord({ ...next, updated_at: Date.now() });
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
if (JSON.stringify(this.doc.sessions.find(s => s.session_id === id)) !== JSON.stringify(record))
|
|
447
|
+
return;
|
|
448
|
+
const next = { ...record, state: 'failed', runtime_present: null, operation_uncertain: true, error_code: typeof error.code === 'string' ? error.code : 'OPERATION_OUTCOME_UNCONFIRMED' };
|
|
449
|
+
if (JSON.stringify(next) !== JSON.stringify(record))
|
|
450
|
+
this.saveRecord({ ...next, updated_at: Date.now() });
|
|
451
|
+
}
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
251
454
|
let observed;
|
|
252
455
|
try {
|
|
253
456
|
observed = await this.adapter.inspect(record);
|
|
@@ -261,7 +464,7 @@ export class IntegrationSessions {
|
|
|
261
464
|
this.saveRecord({ ...record, state: 'failed', error_code: typeof code === 'string' ? code : 'SESSION_OBSERVATION_FAILED', updated_at: Date.now() });
|
|
262
465
|
return;
|
|
263
466
|
}
|
|
264
|
-
const next = { ...record };
|
|
467
|
+
const next = { ...record, runtime_present: this.adapter.runtimePresent ? await this.adapter.runtimePresent(record) : observed.exists };
|
|
265
468
|
if (observed.nativeId && (typeof observed.nativeId !== 'string' || observed.nativeId.length > 256 || /[\x00-\x1f\x7f]/.test(observed.nativeId))) {
|
|
266
469
|
next.state = 'failed';
|
|
267
470
|
next.error_code = 'NATIVE_SESSION_ID_INVALID';
|
|
@@ -325,7 +528,12 @@ export class IntegrationSessions {
|
|
|
325
528
|
}
|
|
326
529
|
/** Only gates configured launches. Native UUID and Agent turn state are not input prerequisites. */
|
|
327
530
|
async deliveryReadiness(id) {
|
|
328
|
-
|
|
531
|
+
const existing = this.doc.sessions.find(record => record.session_id === id);
|
|
532
|
+
if (!existing)
|
|
533
|
+
return null;
|
|
534
|
+
if (existing.state === 'released' || existing.state === 'releasing' || existing.operation_uncertain)
|
|
535
|
+
return { allowed: false, reason: existing.state === 'released' ? 'SESSION_RELEASED' : 'SESSION_OPERATION_IN_PROGRESS' };
|
|
536
|
+
if (!existing.startup_input)
|
|
329
537
|
return null;
|
|
330
538
|
await this.refresh(id);
|
|
331
539
|
const record = this.doc.sessions.find(record => record.session_id === id);
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
4
4
|
import { CollaborationError } from './collaborationProtocol.js';
|
|
5
5
|
export const INTEGRATION_PROTOCOL = 1;
|
|
6
|
-
export const INTEGRATION_OPERATIONS = ['task.read', 'task.create', 'task.configure', 'task.assign', 'task.comment', 'task.revise', 'task.answer', 'events.read', 'session.create', 'session.read', 'session.restore'];
|
|
6
|
+
export const INTEGRATION_OPERATIONS = ['message.send', 'message.read', 'task.read', 'task.create', 'task.configure', 'task.assign', 'task.comment', 'task.revise', 'task.answer', 'events.read', 'session.create', 'session.read', 'session.restore', 'session.release'];
|
|
7
7
|
export function integrationError(code, message, status = 400) { throw new CollaborationError(code, message, status); }
|
|
8
8
|
export function atomicJson(file, value) {
|
|
9
9
|
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { CollaborationError } from './collaborationProtocol.js';
|
|
2
|
+
const fail = (code, message) => { throw new CollaborationError(code, message, 409); };
|
|
3
|
+
/** One coordinator per service. A reservation covers validation and launch;
|
|
4
|
+
* persisted unresolved intents also block a second identity after restart.
|
|
5
|
+
* This does not promise atomic exclusion of independently launched programs. */
|
|
6
|
+
export class SessionLifecycleCoordinator {
|
|
7
|
+
operations = new Set();
|
|
8
|
+
natives = new Map();
|
|
9
|
+
async run(id, action) {
|
|
10
|
+
if (this.operations.has(id))
|
|
11
|
+
fail('SESSION_OPERATION_IN_PROGRESS', 'Another lifecycle operation is in progress');
|
|
12
|
+
this.operations.add(id);
|
|
13
|
+
try {
|
|
14
|
+
return await action();
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
this.operations.delete(id);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
active(id) { return this.operations.has(id); }
|
|
21
|
+
reserveNative(record, descriptors) {
|
|
22
|
+
const key = JSON.stringify([record.agent_slug, record.agent_native_session_id]);
|
|
23
|
+
const owner = this.natives.get(key);
|
|
24
|
+
if (owner && owner !== record.session_id || descriptors.some(other => other.session_id !== record.session_id
|
|
25
|
+
&& other.agent_slug === record.agent_slug && other.agent_native_session_id === record.agent_native_session_id
|
|
26
|
+
&& (['starting', 'restoring', 'binding_pending'].includes(other.state) || other.operation_uncertain))) {
|
|
27
|
+
fail('SESSION_NATIVE_OPERATION_IN_PROGRESS', 'Another descriptor has an unresolved operation for this native identity');
|
|
28
|
+
}
|
|
29
|
+
this.natives.set(key, record.session_id);
|
|
30
|
+
return () => { if (this.natives.get(key) === record.session_id)
|
|
31
|
+
this.natives.delete(key); };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function assertReleaseGeneration(record, generation) {
|
|
35
|
+
if (!Number.isSafeInteger(generation) || Number(generation) < 1)
|
|
36
|
+
fail('INVALID_SESSION_GENERATION', 'release requires the generation returned by session get');
|
|
37
|
+
if (generation !== (record.generation ?? 1))
|
|
38
|
+
fail('SESSION_GENERATION_MISMATCH', 'Session generation changed; read its current state before releasing');
|
|
39
|
+
}
|
|
40
|
+
export function assertReleaseFacts(record, facts) {
|
|
41
|
+
if (!record.agent_native_session_id)
|
|
42
|
+
fail('NATIVE_SESSION_ID_MISSING', 'Release requires a durable exact native identity');
|
|
43
|
+
if (['starting', 'restoring', 'binding_pending', 'releasing'].includes(record.state))
|
|
44
|
+
fail('SESSION_OPERATION_IN_PROGRESS', 'Prior operation is unresolved');
|
|
45
|
+
if (facts.running)
|
|
46
|
+
fail('SESSION_STILL_RUNNING', 'Agent is still running; explicitly exit it before releasing the runtime');
|
|
47
|
+
if (facts.exists && !facts.shell)
|
|
48
|
+
fail('SESSION_IDENTITY_MISMATCH', 'Runtime is not the verified original empty shell');
|
|
49
|
+
}
|
|
50
|
+
export function assertMessageRuntime(record) {
|
|
51
|
+
if (!record)
|
|
52
|
+
return;
|
|
53
|
+
if (record.state === 'released')
|
|
54
|
+
fail('SESSION_RELEASED', 'Runtime was released; explicitly restore this session before sending');
|
|
55
|
+
if (record.state === 'releasing')
|
|
56
|
+
fail('SESSION_OPERATION_IN_PROGRESS', 'Runtime release is in progress; message was not enqueued');
|
|
57
|
+
if (record.operation_uncertain)
|
|
58
|
+
fail('OPERATION_OUTCOME_UNCONFIRMED', 'Lifecycle outcome needs reconciliation before sending');
|
|
59
|
+
}
|
|
60
|
+
/** Shared release protocol. No Agent idle inference, task result, or UI
|
|
61
|
+
* visibility participates in deciding whether a process may be destroyed. */
|
|
62
|
+
export async function releaseSessionRuntime(adapter, reconcile) {
|
|
63
|
+
await adapter.barrier(async () => {
|
|
64
|
+
if (adapter.pendingMessages())
|
|
65
|
+
fail('SESSION_PENDING_MESSAGES', 'Pending messages appeared before runtime release');
|
|
66
|
+
if (await adapter.exists()) {
|
|
67
|
+
if (reconcile)
|
|
68
|
+
fail('OPERATION_OUTCOME_UNCONFIRMED', 'Original runtime still exists; restart reconciliation will not terminate it');
|
|
69
|
+
await adapter.verifyOwnership();
|
|
70
|
+
const facts = await adapter.inspect();
|
|
71
|
+
if (facts.running)
|
|
72
|
+
fail('SESSION_STILL_RUNNING', 'Agent restarted before release');
|
|
73
|
+
if (!facts.exists || !facts.shell)
|
|
74
|
+
fail('SESSION_IDENTITY_MISMATCH', 'Original runtime is not a verified foreground shell');
|
|
75
|
+
await adapter.destroy();
|
|
76
|
+
}
|
|
77
|
+
if (await adapter.exists())
|
|
78
|
+
fail('OPERATION_OUTCOME_UNCONFIRMED', 'Owned runtime still exists after release');
|
|
79
|
+
await adapter.forgetRuntime();
|
|
80
|
+
});
|
|
81
|
+
}
|