@deepseek-insight/dsi 0.5.5
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/LICENSE +204 -0
- package/README.md +98 -0
- package/bin/dsi.mjs +147 -0
- package/build/client/_app/immutable/assets/0.DqFt8jIM.css +2 -0
- package/build/client/_app/immutable/assets/2.YlW45hiM.css +1 -0
- package/build/client/_app/immutable/assets/codicon.Brq4_Ui5.ttf +0 -0
- package/build/client/_app/immutable/assets/editor.jjEx9u7D.css +1 -0
- package/build/client/_app/immutable/assets/settings-monaco.C1CMdMvw.css +1 -0
- package/build/client/_app/immutable/chunks/-g7BcI22.js +4 -0
- package/build/client/_app/immutable/chunks/08VXUWAP.js +1 -0
- package/build/client/_app/immutable/chunks/2irkVCIh.js +10 -0
- package/build/client/_app/immutable/chunks/43DmfD33.js +1 -0
- package/build/client/_app/immutable/chunks/6GKQw8m6.js +99 -0
- package/build/client/_app/immutable/chunks/6RTg0lbw.js +1 -0
- package/build/client/_app/immutable/chunks/9TvT6fta.js +120 -0
- package/build/client/_app/immutable/chunks/B1YSaoU7.js +96 -0
- package/build/client/_app/immutable/chunks/B2J2w5EN.js +1 -0
- package/build/client/_app/immutable/chunks/B5-_aFwp.js +2 -0
- package/build/client/_app/immutable/chunks/B8-Q2PIv.js +1 -0
- package/build/client/_app/immutable/chunks/B8J7ngcE.js +1 -0
- package/build/client/_app/immutable/chunks/B9JPFI3C.js +2 -0
- package/build/client/_app/immutable/chunks/BAZKJuQw.js +1 -0
- package/build/client/_app/immutable/chunks/BBKpNcUe.js +1 -0
- package/build/client/_app/immutable/chunks/BEEn4gHE.js +1 -0
- package/build/client/_app/immutable/chunks/BEtHBSE6.js +1 -0
- package/build/client/_app/immutable/chunks/BFoDCU3G.js +1 -0
- package/build/client/_app/immutable/chunks/BHtPwB2y.js +847 -0
- package/build/client/_app/immutable/chunks/BI-MrBd-.js +1 -0
- package/build/client/_app/immutable/chunks/BI4pMaIG.js +1 -0
- package/build/client/_app/immutable/chunks/BJL7RCQE.js +4 -0
- package/build/client/_app/immutable/chunks/BKxtCVv1.js +1 -0
- package/build/client/_app/immutable/chunks/BOfSXrr7.js +1 -0
- package/build/client/_app/immutable/chunks/BOotOW0E.js +1 -0
- package/build/client/_app/immutable/chunks/BTzV9O1M.js +3 -0
- package/build/client/_app/immutable/chunks/BW-FRVGs.js +1 -0
- package/build/client/_app/immutable/chunks/BWPQTe0t.js +1 -0
- package/build/client/_app/immutable/chunks/BYgofb2h.js +1 -0
- package/build/client/_app/immutable/chunks/Ba7i2nA0.js +1 -0
- package/build/client/_app/immutable/chunks/Bb19NAkg.js +1 -0
- package/build/client/_app/immutable/chunks/Bc_sGQ0U.js +1 -0
- package/build/client/_app/immutable/chunks/BeGwcela.js +1 -0
- package/build/client/_app/immutable/chunks/BeZqd5lB.js +1 -0
- package/build/client/_app/immutable/chunks/Beo7Z46k.js +7 -0
- package/build/client/_app/immutable/chunks/BhNEBkk_.js +39 -0
- package/build/client/_app/immutable/chunks/BifhSqXX.js +1 -0
- package/build/client/_app/immutable/chunks/Bjy-W4x2.js +81 -0
- package/build/client/_app/immutable/chunks/BlltJnm0.js +1 -0
- package/build/client/_app/immutable/chunks/Bn9NRi0R.js +168 -0
- package/build/client/_app/immutable/chunks/BnF5Jm-h.js +1 -0
- package/build/client/_app/immutable/chunks/Bp4uuUjI.js +3 -0
- package/build/client/_app/immutable/chunks/BrTsmS-o.js +57 -0
- package/build/client/_app/immutable/chunks/Bri6UV-V.js +1 -0
- package/build/client/_app/immutable/chunks/Btk-1DaX.js +1 -0
- package/build/client/_app/immutable/chunks/BwWrilGY.js +1 -0
- package/build/client/_app/immutable/chunks/Bxkupp3x.js +1 -0
- package/build/client/_app/immutable/chunks/Bz4Y3fRF.js +1 -0
- package/build/client/_app/immutable/chunks/C-OQMol4.js +1 -0
- package/build/client/_app/immutable/chunks/C-y9NEjX.js +1 -0
- package/build/client/_app/immutable/chunks/C1JXY9b5.js +30 -0
- package/build/client/_app/immutable/chunks/C2x7inlt.js +321 -0
- package/build/client/_app/immutable/chunks/C531GsRP.js +2 -0
- package/build/client/_app/immutable/chunks/C5TAQhFO.js +1 -0
- package/build/client/_app/immutable/chunks/C5_-GSwl.js +1 -0
- package/build/client/_app/immutable/chunks/C6lv30UQ.js +1 -0
- package/build/client/_app/immutable/chunks/C7h46wYY.js +1 -0
- package/build/client/_app/immutable/chunks/C8Ef5oNN.js +1 -0
- package/build/client/_app/immutable/chunks/CA_WNCsl.js +41 -0
- package/build/client/_app/immutable/chunks/CC2rA5mh.js +1 -0
- package/build/client/_app/immutable/chunks/CHdTS7qa.js +15 -0
- package/build/client/_app/immutable/chunks/CIPQ_RoX.js +1 -0
- package/build/client/_app/immutable/chunks/CJ_zvTBg.js +72 -0
- package/build/client/_app/immutable/chunks/CPEyWost.js +1 -0
- package/build/client/_app/immutable/chunks/CPgSxomx.js +1 -0
- package/build/client/_app/immutable/chunks/CRfGWb83.js +1 -0
- package/build/client/_app/immutable/chunks/CTgm3pse.js +1 -0
- package/build/client/_app/immutable/chunks/CVaqCwHa.js +1 -0
- package/build/client/_app/immutable/chunks/CWKM3f5q.js +1 -0
- package/build/client/_app/immutable/chunks/CWVpRRWx.js +162 -0
- package/build/client/_app/immutable/chunks/CXMH3UgJ.js +257 -0
- package/build/client/_app/immutable/chunks/CZ-RNlot.js +1 -0
- package/build/client/_app/immutable/chunks/CZXl7qUb.js +70 -0
- package/build/client/_app/immutable/chunks/C_N-eONP.js +1 -0
- package/build/client/_app/immutable/chunks/Cb1pwoev.js +1 -0
- package/build/client/_app/immutable/chunks/CcL1k2On.js +11 -0
- package/build/client/_app/immutable/chunks/CcWV7GYH.js +1 -0
- package/build/client/_app/immutable/chunks/CcwsVR0C.js +1 -0
- package/build/client/_app/immutable/chunks/Cd6K7Y8m.js +1 -0
- package/build/client/_app/immutable/chunks/CfSJPG6W.js +1 -0
- package/build/client/_app/immutable/chunks/CfZ7uSQe.js +38 -0
- package/build/client/_app/immutable/chunks/Cimd5fb3.js +1 -0
- package/build/client/_app/immutable/chunks/Cj-Zm41R.js +36 -0
- package/build/client/_app/immutable/chunks/CjZ_QSbm.js +1 -0
- package/build/client/_app/immutable/chunks/CkrcOjFY.js +1 -0
- package/build/client/_app/immutable/chunks/ClamHrr6.js +1 -0
- package/build/client/_app/immutable/chunks/Cm7r79vr.js +1 -0
- package/build/client/_app/immutable/chunks/CmbEK4CY.js +84 -0
- package/build/client/_app/immutable/chunks/CmvnEPtD.js +7 -0
- package/build/client/_app/immutable/chunks/CopQ2nXW.js +1 -0
- package/build/client/_app/immutable/chunks/CpzslTdj.js +1 -0
- package/build/client/_app/immutable/chunks/CsHVnZCH.js +1 -0
- package/build/client/_app/immutable/chunks/CsQS60Ue.js +1 -0
- package/build/client/_app/immutable/chunks/CuRJVwEq.js +1 -0
- package/build/client/_app/immutable/chunks/D-oSqn_W.js +1 -0
- package/build/client/_app/immutable/chunks/D-ptJ9y-.js +1 -0
- package/build/client/_app/immutable/chunks/D05T4iGw.js +1 -0
- package/build/client/_app/immutable/chunks/D0I62FkS.js +1 -0
- package/build/client/_app/immutable/chunks/D16vkywU.js +1 -0
- package/build/client/_app/immutable/chunks/D1qzYj9m.js +1 -0
- package/build/client/_app/immutable/chunks/D3hlyOfw.js +1 -0
- package/build/client/_app/immutable/chunks/D4gq_dik.js +1 -0
- package/build/client/_app/immutable/chunks/D7fX-3xy.js +1 -0
- package/build/client/_app/immutable/chunks/D8-5qLJg.js +231 -0
- package/build/client/_app/immutable/chunks/D8hlhfM3.js +1 -0
- package/build/client/_app/immutable/chunks/DAS1CLuE.js +24 -0
- package/build/client/_app/immutable/chunks/DGarjVZC.js +1 -0
- package/build/client/_app/immutable/chunks/DGd2pJF5.js +1 -0
- package/build/client/_app/immutable/chunks/DHpiXF5c.js +1 -0
- package/build/client/_app/immutable/chunks/DHwB1DNv.js +161 -0
- package/build/client/_app/immutable/chunks/DIMkf-bt.js +3 -0
- package/build/client/_app/immutable/chunks/DISqw1ZD.js +1 -0
- package/build/client/_app/immutable/chunks/DITEY8D_.js +62 -0
- package/build/client/_app/immutable/chunks/DL0yNkAA.js +1 -0
- package/build/client/_app/immutable/chunks/DL1Hc2zs.js +1 -0
- package/build/client/_app/immutable/chunks/DLOOos_8.js +292 -0
- package/build/client/_app/immutable/chunks/DMUM7wLl.js +1 -0
- package/build/client/_app/immutable/chunks/DPKol_4Z.js +139 -0
- package/build/client/_app/immutable/chunks/DS17s2tU.js +1 -0
- package/build/client/_app/immutable/chunks/DT7zqm-g.js +1 -0
- package/build/client/_app/immutable/chunks/DTj74zXo.js +1 -0
- package/build/client/_app/immutable/chunks/DV3o63ZY.js +1 -0
- package/build/client/_app/immutable/chunks/DVXVVWkX.js +129 -0
- package/build/client/_app/immutable/chunks/DWDfcLfy.js +206 -0
- package/build/client/_app/immutable/chunks/DXeGLmed.js +8 -0
- package/build/client/_app/immutable/chunks/DYqsupvX.js +1 -0
- package/build/client/_app/immutable/chunks/DZ6u0Z-K.js +1 -0
- package/build/client/_app/immutable/chunks/DZFCIeNp.js +1 -0
- package/build/client/_app/immutable/chunks/D_Lu-MrJ.js +1 -0
- package/build/client/_app/immutable/chunks/DauUninz.js +1 -0
- package/build/client/_app/immutable/chunks/DbInun42.js +1 -0
- package/build/client/_app/immutable/chunks/DbcKN35_.js +3 -0
- package/build/client/_app/immutable/chunks/DcZe4Woe.js +1 -0
- package/build/client/_app/immutable/chunks/DdL9SqIa.js +1 -0
- package/build/client/_app/immutable/chunks/De2hgUGE.js +1 -0
- package/build/client/_app/immutable/chunks/DezsRK8O.js +1 -0
- package/build/client/_app/immutable/chunks/Dg_UWGX7.js +24 -0
- package/build/client/_app/immutable/chunks/Dj2t5yNg.js +1 -0
- package/build/client/_app/immutable/chunks/DkqhCAOL.js +1 -0
- package/build/client/_app/immutable/chunks/DlQrSOw4.js +7 -0
- package/build/client/_app/immutable/chunks/DmHpPYUd.js +1 -0
- package/build/client/_app/immutable/chunks/Dmn-tsie.js +1 -0
- package/build/client/_app/immutable/chunks/DpFanUEy.js +298 -0
- package/build/client/_app/immutable/chunks/DqwfDHZH.js +89 -0
- package/build/client/_app/immutable/chunks/Dr0pcX_F.js +1 -0
- package/build/client/_app/immutable/chunks/DrH0SRf2.js +1 -0
- package/build/client/_app/immutable/chunks/DsF7k-Jl.js +1 -0
- package/build/client/_app/immutable/chunks/DtBGId5S.js +2 -0
- package/build/client/_app/immutable/chunks/DuM1hSCa.js +1 -0
- package/build/client/_app/immutable/chunks/Du_ODUAO.js +1 -0
- package/build/client/_app/immutable/chunks/DvCMcJ3P.js +1 -0
- package/build/client/_app/immutable/chunks/Dy-Kn_BL.js +1 -0
- package/build/client/_app/immutable/chunks/DzVMo6mB.js +62 -0
- package/build/client/_app/immutable/chunks/ESmLJICu.js +1 -0
- package/build/client/_app/immutable/chunks/FvmIjYaQ.js +1 -0
- package/build/client/_app/immutable/chunks/GBaNblmo.js +1 -0
- package/build/client/_app/immutable/chunks/HclGiUj8.js +1 -0
- package/build/client/_app/immutable/chunks/Ju5jF6hy.js +1 -0
- package/build/client/_app/immutable/chunks/KythIMN8.js +1 -0
- package/build/client/_app/immutable/chunks/LWre5GKl.js +40 -0
- package/build/client/_app/immutable/chunks/NEE52Syq.js +1 -0
- package/build/client/_app/immutable/chunks/No6lfiAw.js +1 -0
- package/build/client/_app/immutable/chunks/OmE6SGvs.js +106 -0
- package/build/client/_app/immutable/chunks/P_ok0Z_j.js +2 -0
- package/build/client/_app/immutable/chunks/PaZBB01u.js +1 -0
- package/build/client/_app/immutable/chunks/Rh9cy7xg.js +2 -0
- package/build/client/_app/immutable/chunks/SOo6toE5.js +1 -0
- package/build/client/_app/immutable/chunks/TNLUqJaC.js +1 -0
- package/build/client/_app/immutable/chunks/UNxFjlik.js +1 -0
- package/build/client/_app/immutable/chunks/UmdltqoE.js +1 -0
- package/build/client/_app/immutable/chunks/Z5eAx3Zn.js +1 -0
- package/build/client/_app/immutable/chunks/bTuwJoIa.js +1 -0
- package/build/client/_app/immutable/chunks/fmXr3nnJ.js +1 -0
- package/build/client/_app/immutable/chunks/fybaL0A-.js +1 -0
- package/build/client/_app/immutable/chunks/gp-XZpBa.js +3 -0
- package/build/client/_app/immutable/chunks/h-p5ZdY8.js +1 -0
- package/build/client/_app/immutable/chunks/hxYEUgOo.js +1 -0
- package/build/client/_app/immutable/chunks/i0VQdJtv.js +1 -0
- package/build/client/_app/immutable/chunks/i0X4ZdIN.js +1 -0
- package/build/client/_app/immutable/chunks/mVHw7KEX.js +7 -0
- package/build/client/_app/immutable/chunks/nDMCpPkF.js +1 -0
- package/build/client/_app/immutable/chunks/nr791fC2.js +1 -0
- package/build/client/_app/immutable/chunks/onAF5SnQ.js +1 -0
- package/build/client/_app/immutable/chunks/oz_6vUea.js +1 -0
- package/build/client/_app/immutable/chunks/qEGLx2Wc.js +78 -0
- package/build/client/_app/immutable/chunks/raVGrKQy.js +44 -0
- package/build/client/_app/immutable/chunks/sBoK_t0W.js +1 -0
- package/build/client/_app/immutable/chunks/sIEmSkn6.js +34 -0
- package/build/client/_app/immutable/chunks/wFIeY1Cf.js +88 -0
- package/build/client/_app/immutable/chunks/xihTtKlq.js +1 -0
- package/build/client/_app/immutable/chunks/zpcgJjUg.js +127 -0
- package/build/client/_app/immutable/entry/app.B2QxMn0t.js +2 -0
- package/build/client/_app/immutable/entry/start.BR3A4iFi.js +1 -0
- package/build/client/_app/immutable/nodes/0.B_ZEZy7b.js +1 -0
- package/build/client/_app/immutable/nodes/1.By3Gd2cs.js +1 -0
- package/build/client/_app/immutable/nodes/2.1TAwiWw1.js +69 -0
- package/build/client/_app/immutable/workers/css.worker-DRH2OcAG.js +93 -0
- package/build/client/_app/immutable/workers/html.worker-DQLddT9h.js +506 -0
- package/build/client/_app/immutable/workers/json.worker-D_Dqrls5.js +62 -0
- package/build/client/_app/immutable/workers/settings-editor-worker-D9zwrD0f.js +30 -0
- package/build/client/_app/immutable/workers/ts.worker-DkYoA6bF.js +67738 -0
- package/build/client/_app/immutable/workers/yaml.worker-BTGFEDOA.js +449 -0
- package/build/client/_app/version.json +1 -0
- package/build/client/favicon.svg +7 -0
- package/build/env.js +70 -0
- package/build/handler.js +23 -0
- package/build/index.js +146 -0
- package/build/server/chunks/chunks/a2a-protocol.js-C77mVg2Q.js +52 -0
- package/build/server/chunks/chunks/app-config.svelte.js-Co9jacqT.js +52 -0
- package/build/server/chunks/chunks/config.js-BBYogCe7.js +123 -0
- package/build/server/chunks/chunks/db.js-DcmO3JAx.js +120 -0
- package/build/server/chunks/chunks/db2.js-DBltVUGg.js +717 -0
- package/build/server/chunks/chunks/dsh-connection.js-DwihVtk_.js +1957 -0
- package/build/server/chunks/chunks/dsh-events.js-BtDxaV6Q.js +706 -0
- package/build/server/chunks/chunks/env.js-l0sNRNKZ.js +2 -0
- package/build/server/chunks/chunks/exports.js-DMiPee3x.js +263 -0
- package/build/server/chunks/chunks/goal-projection.js-0_34t60K.js +44 -0
- package/build/server/chunks/chunks/image-limits.js-BMPZ55kJ.js +186 -0
- package/build/server/chunks/chunks/insight-config.js-CfMPTA6Z.js +352 -0
- package/build/server/chunks/chunks/internal.js-CQzEjdyk.js +747 -0
- package/build/server/chunks/chunks/internal2.js-D-DAuSVh.js +22 -0
- package/build/server/chunks/chunks/server.js-DoCd0fEo.js +4104 -0
- package/build/server/chunks/chunks/shared.js-C4-Py8s_.js +775 -0
- package/build/server/chunks/chunks/state.js-CpmOS0a2.js +114 -0
- package/build/server/chunks/chunks/uneval.js-BJ9Qq2SF.js +488 -0
- package/build/server/chunks/chunks/utils.js-tOWCe9Wk.js +933 -0
- package/build/server/chunks/chunks/watcher.js-BelxjMOP.js +473 -0
- package/build/server/chunks/entries/endpoints/api/a2a/_id_/_server.ts.js-B4HUNFZv.js +37 -0
- package/build/server/chunks/entries/endpoints/api/a2a/_server.ts.js-JDV_oqbX.js +61 -0
- package/build/server/chunks/entries/endpoints/api/a2a/register/_server.ts.js-Dxwc-7-5.js +99 -0
- package/build/server/chunks/entries/endpoints/api/config/_server.ts.js-D8i35bGH.js +35 -0
- package/build/server/chunks/entries/endpoints/api/dsh/directory/_server.ts.js-CeBU1G85.js +43 -0
- package/build/server/chunks/entries/endpoints/api/dsh/pick-directory/_server.ts.js-CyrVUxv3.js +36 -0
- package/build/server/chunks/entries/endpoints/api/dsh/presets/_server.ts.js-qeO_1htT.js +33 -0
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/attachment/_server.ts.js-DnHffohz.js +44 -0
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/cancel/_server.ts.js-fm6MFyjP.js +33 -0
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/catalog/_server.ts.js-wJ9V_Ue7.js +42 -0
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/command/_server.ts.js-o83L4GUY.js +85 -0
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/events/_server.ts.js-DLCHSC1M.js +212 -0
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/fork/_server.ts.js-Ca6NU9HO.js +58 -0
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/goal/_server.ts.js-RteR85vn.js +138 -0
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/history/_server.ts.js-CEKDkK7v.js +55 -0
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/models/_server.ts.js-D7ziyW3d.js +35 -0
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/permission/_server.ts.js-DbkD4JZb.js +92 -0
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/prompt/_server.ts.js-eOLNlf1-.js +94 -0
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/rename/_server.ts.js-txQRocK9.js +57 -0
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/respond/_server.ts.js-D3WGYzkE.js +69 -0
- package/build/server/chunks/entries/endpoints/api/dsh/session/_sessionId_/select-model/_server.ts.js-g2GtvLTs.js +69 -0
- package/build/server/chunks/entries/endpoints/api/dsh/sessions/_server.ts.js-17yKojU_.js +85 -0
- package/build/server/chunks/entries/endpoints/api/dsh/workspace-file/_server.ts.js-D16pEhvd.js +99 -0
- package/build/server/chunks/entries/endpoints/api/dsh/workspace-file/save/_server.ts.js-BXpZNLK8.js +110 -0
- package/build/server/chunks/entries/endpoints/api/dsh/workspace-file-bytes/_server.ts.js-hBlyjq-x.js +113 -0
- package/build/server/chunks/entries/endpoints/api/dsh/workspace-tree/_server.ts.js-KWRnsAkm.js +59 -0
- package/build/server/chunks/entries/endpoints/api/dsh/workspaces/_server.ts.js-DzNvvCxK.js +55 -0
- package/build/server/chunks/entries/endpoints/api/dsh/workspaces/_workspaceId_/delete/_server.ts.js-MK5925CY.js +45 -0
- package/build/server/chunks/entries/endpoints/api/dsh/workspaces/_workspaceId_/rename/_server.ts.js-w3Mo0nf7.js +63 -0
- package/build/server/chunks/entries/endpoints/api/prompts/_id_/_server.ts.js-CInfjM-i.js +64 -0
- package/build/server/chunks/entries/endpoints/api/prompts/_server.ts.js-C1lqP1OV.js +87 -0
- package/build/server/chunks/entries/endpoints/api/prompts/use/_server.ts.js-B36YMKMt.js +54 -0
- package/build/server/chunks/entries/endpoints/api/settings/_server.ts.js-CHvHNzeB.js +101 -0
- package/build/server/chunks/entries/pages/_error.svelte.js-BE3-BWDd.js +41 -0
- package/build/server/chunks/entries/pages/_layout.svelte.js-EFiihTdF.js +18 -0
- package/build/server/chunks/entries/pages/_layout.ts.js-CNSo-Z1U.js +22 -0
- package/build/server/chunks/entries/pages/_page.server.ts.js-Bk8UuUXA.js +85 -0
- package/build/server/chunks/entries/pages/_page.svelte.js-CfmKQYLK.js +13883 -0
- package/build/server/chunks/handler-BshnD4Y4.js +1735 -0
- package/build/server/chunks/index.js-I_pMeIwQ.js +3864 -0
- package/build/server/chunks/manifest.js-DwhpAk_y.js +273 -0
- package/build/server/chunks/nodes/0.js-DCyDo8-C.js +12 -0
- package/build/server/chunks/nodes/1.js-Dx7z_VWN.js +9 -0
- package/build/server/chunks/nodes/2.js-CLw9pxho.js +24 -0
- package/build/shims.js +33 -0
- package/package.json +65 -0
- package/template/.dsi/a2a.sqlite +0 -0
- package/template/.dsi/config.json +42 -0
- package/template/.dsi/prompts.sqlite +0 -0
- package/template/.dsi/settings.yaml +33 -0
|
@@ -0,0 +1,1957 @@
|
|
|
1
|
+
import { p as dshBaseUrl } from './config.js-BBYogCe7.js';
|
|
2
|
+
import { r as readDshPresetEnglishConfig, a as readDshAuthConfig, b as readServerConfig } from './insight-config.js-CfMPTA6Z.js';
|
|
3
|
+
|
|
4
|
+
//#region src/lib/server/dsh-rpc.ts
|
|
5
|
+
/**
|
|
6
|
+
* dsh-rpc — the single choke point for DSH wire bytes (BC-6).
|
|
7
|
+
*
|
|
8
|
+
* Every RPC fact below is pinned by the gateway guide
|
|
9
|
+
* (dev/gateways/dsh-gateway-live-streaming.md) and the harness sources it
|
|
10
|
+
* cites (packages/client/connection/src/client/rpc.ts — envelope;
|
|
11
|
+
* packages/host/apiproxy/src/api/rpc-map.ts — POST /api/{method} paths;
|
|
12
|
+
* api/rpc.ts — RpcResult ok/false error union; api/sessions.ts — payloads).
|
|
13
|
+
*
|
|
14
|
+
* Live-verified 2026-08-21: session.list and session.history round-trip,
|
|
15
|
+
* events.mux open → session/subscribed baseline with lastSeq.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Wire endpoints this app calls (0.1.2-alpha.1 wire: slash paths, args
|
|
19
|
+
* envelopes). The closed set grows only by spec change (BC-6). Arg names are
|
|
20
|
+
* each endpoint's declared parameter name — exact-match validated by the
|
|
21
|
+
* host descriptor (extra/missing keys → arguments-invalid).
|
|
22
|
+
*
|
|
23
|
+
* Live-verified 2026-08-29 against dsh-0.1.2-alpha.1 (cd5ef81481):
|
|
24
|
+
* session/list, session/create, session/prompt, session/rename,
|
|
25
|
+
* session/selectModel, session/modelCatalog, session/page, session/cancel,
|
|
26
|
+
* session/attachment, agentPresets/list, workspace/create, commands/list
|
|
27
|
+
* (verified on the wire, never called by DSI — no DSH_METHODS entry),
|
|
28
|
+
* commands/execute, directoryPicker/pick. DSH_METHODS below is the canonical
|
|
29
|
+
* set this app calls.
|
|
30
|
+
*
|
|
31
|
+
* Grown by spec 2026-08-30 (BC-6): the Slash Menu ADR
|
|
32
|
+
* (dev/architectural-decission/2026-08-30 - The Slash Menu — The Composer
|
|
33
|
+
* Reads the Host's Own Directory.md) adds commandsList + skillsList — DSI
|
|
34
|
+
* becomes a reader of the host's command/skill catalogs. Both endpoints
|
|
35
|
+
* live-pinned 2026-08-30 against dsh web 0.1.2 (see tests/unit/dsh-rpc.test.ts
|
|
36
|
+
* — the recorded round-trip: envelope payload:{args}, skills 10 rows,
|
|
37
|
+
* commands 5 rows).
|
|
38
|
+
*/
|
|
39
|
+
var DSH_METHODS = {
|
|
40
|
+
/** args `{}` — `_request` is reserved-empty (acceptsUndefined). */
|
|
41
|
+
list: "session/list",
|
|
42
|
+
/** args `{request:{address,throughSeq,beforeSeq?,maxMessages?}}` → `{records,hasMore}`. */
|
|
43
|
+
page: "session/page",
|
|
44
|
+
prompt: "session/prompt",
|
|
45
|
+
cancel: "session/cancel",
|
|
46
|
+
create: "session/create",
|
|
47
|
+
/**
|
|
48
|
+
* args `{request:{sessionId,atSeq?}}` → `{sessionId}` (the fork child).
|
|
49
|
+
* Host contract (harness packages/api/session-controller/src/types.ts
|
|
50
|
+
* SessionForkRequest/Value): `atSeq` omitted cuts at the source's last
|
|
51
|
+
* completed turn; a given `atSeq` anchors to the first `turn/end` at or
|
|
52
|
+
* after it — an anchor inside an open turn refuses `session/fork-unavailable`,
|
|
53
|
+
* never clips. Refusals: `session/not-found`, `session/fork-unavailable`,
|
|
54
|
+
* `session/workspace-attach-failed`. Live-pinned 2026-09-01 against
|
|
55
|
+
* dsh-0.1.2-alpha.3: the args descriptor exact-matches `{request:{sessionId}}`
|
|
56
|
+
* (a bare sessionId → gateway/arguments-invalid: missing "request") and a
|
|
57
|
+
* turn-less source refuses fork-unavailable ("has no completed turn to
|
|
58
|
+
* fork from"). The success path (child id) is pinned by the harness
|
|
59
|
+
* client fixture (connection/src/client/fixture.ts case 'session/fork').
|
|
60
|
+
*/
|
|
61
|
+
fork: "session/fork",
|
|
62
|
+
presets: "agentPresets/list",
|
|
63
|
+
rename: "session/rename",
|
|
64
|
+
/** args `{}` → ModelCatalog {default,routableProviders,groups,failures}. */
|
|
65
|
+
modelCatalog: "session/modelCatalog",
|
|
66
|
+
selectModel: "session/selectModel",
|
|
67
|
+
workspaceCreate: "workspace/create",
|
|
68
|
+
/**
|
|
69
|
+
* args `{request:{workspaceId,title}}` → Workspace view. Refusals:
|
|
70
|
+
* `workspace/not-found`, `workspace/name-conflict` (details carry the
|
|
71
|
+
* conflicting name). A no-op title (trimmed-equal) succeeds silently and
|
|
72
|
+
* emits NO follow frame — semantics pinned against the harness client
|
|
73
|
+
* fixture (connection fixture.client.spec.ts:1187-1216). Chip Menu ADR D3.
|
|
74
|
+
*/
|
|
75
|
+
workspaceRename: "workspace/rename",
|
|
76
|
+
/**
|
|
77
|
+
* args `{request:{workspaceId}}` → removed workspaceId. Removes ONLY the
|
|
78
|
+
* registry row (sessions survive, ungrouped) and emits a remove frame on
|
|
79
|
+
* workspace/follow — pinned at connection fixture.client.spec.ts:1292-1303.
|
|
80
|
+
* Chip Menu ADR D3.
|
|
81
|
+
*/
|
|
82
|
+
workspaceDelete: "workspace/delete",
|
|
83
|
+
listDirectory: "directoryPicker/list",
|
|
84
|
+
pickDirectory: "directoryPicker/pick",
|
|
85
|
+
/**
|
|
86
|
+
* args `{agentId,line,submittedAttachments}` → CommandExecution | null
|
|
87
|
+
* (agentId = sessionId). 0.1.3-alpha.1 renamed the wire arg — the Typert
|
|
88
|
+
* descriptor binds the implementation's parameter name, and
|
|
89
|
+
* CommandRuntime.execute's third parameter became `submittedAttachments`
|
|
90
|
+
* (mixed image/file submissions); the old `images` envelope now rejects
|
|
91
|
+
* `gateway/arguments-invalid` (probed live 2026-09-05).
|
|
92
|
+
*/
|
|
93
|
+
commandExecute: "commands/execute",
|
|
94
|
+
/** args `{agentId}` → DshCommandRow[] name-sorted (Slash Menu ADR §2; live-pinned 2026-08-30). */
|
|
95
|
+
commandsList: "commands/list",
|
|
96
|
+
/** args `{request:{sessionId}}` → DshSkillList (user-invocable only; Slash Menu ADR §2; live-pinned 2026-08-30). */
|
|
97
|
+
skillsList: "skills/list",
|
|
98
|
+
attachment: "session/attachment",
|
|
99
|
+
/**
|
|
100
|
+
* args `{workspaceFileScopeId, path, range?}` - WorkspaceFileText
|
|
101
|
+
* {offset,text,lines,eof,absolutePath,version,bytes?} (Workspace Explorer
|
|
102
|
+
* W2 task 2.1). The scope wire key was `agentId` until the host's
|
|
103
|
+
* 2026-09-09 session-roots fix (7214a97723, first shipped 0.1.5-alpha.2)
|
|
104
|
+
* registered the `workspaceFileScope` lookup with
|
|
105
|
+
* `wire: 'workspaceFileScopeId'` (packages/api/workspace-files/src/index.ts:204
|
|
106
|
+
* at 0.1.5-rc.1) — sending `agentId` now rejects gateway/arguments-invalid
|
|
107
|
+
* (observed live 2026-09-10). Refusals (RemoteErrorDetailsMap, types.ts):
|
|
108
|
+
* workspace-file/not-found, outside-workspace, too-large, not-text,
|
|
109
|
+
* not-regular-file.
|
|
110
|
+
*/
|
|
111
|
+
workspaceFileRead: "workspaceFiles/read",
|
|
112
|
+
/**
|
|
113
|
+
* args `{workspaceFileScopeId, path, range}` → WorkspaceFileBytes —
|
|
114
|
+
* readBytes(workspaceFileScope, path, range) on the same descriptor: raw
|
|
115
|
+
* bytes (base64 on the wire),
|
|
116
|
+
* no text decoding and no binary refusal. Binary previews (images)
|
|
117
|
+
* read through this (2026-09-10): `read` refuses non-text with
|
|
118
|
+
* workspace-file/not-text. Same refusals as read.
|
|
119
|
+
*/
|
|
120
|
+
workspaceFileReadBytes: "workspaceFiles/readBytes",
|
|
121
|
+
workspaceFileList: "workspaceFiles/list"
|
|
122
|
+
};
|
|
123
|
+
/** Args builders — one per endpoint, pinned to the live-probed shapes. */
|
|
124
|
+
var DSH_ARGS = {
|
|
125
|
+
list: () => ({ _request: {} }),
|
|
126
|
+
page: (request) => ({ request }),
|
|
127
|
+
prompt: (request) => ({ request }),
|
|
128
|
+
cancel: (request) => ({ request }),
|
|
129
|
+
create: (request) => ({ request }),
|
|
130
|
+
fork: (request) => ({ request }),
|
|
131
|
+
presets: () => ({}),
|
|
132
|
+
rename: (request) => ({ request }),
|
|
133
|
+
modelCatalog: () => ({}),
|
|
134
|
+
selectModel: (request) => ({ request }),
|
|
135
|
+
workspaceCreate: (request) => ({ request }),
|
|
136
|
+
workspaceRename: (workspaceId, title) => ({ request: {
|
|
137
|
+
workspaceId,
|
|
138
|
+
title
|
|
139
|
+
} }),
|
|
140
|
+
workspaceDelete: (workspaceId) => ({ request: { workspaceId } }),
|
|
141
|
+
listDirectory: (path) => path === void 0 ? {} : { path },
|
|
142
|
+
pickDirectory: () => ({}),
|
|
143
|
+
commandExecute: (agentId, line, submittedAttachments) => ({
|
|
144
|
+
agentId,
|
|
145
|
+
line,
|
|
146
|
+
submittedAttachments
|
|
147
|
+
}),
|
|
148
|
+
commandsList: (agentId) => ({ agentId }),
|
|
149
|
+
skillsList: (sessionId) => ({ request: { sessionId } }),
|
|
150
|
+
attachment: (request) => ({ request }),
|
|
151
|
+
/** Flat args — the descriptor has NO request wrapper: scope wire
|
|
152
|
+
* workspaceFileScopeId (2026-09-09 rename, was agentId), json path/range. */
|
|
153
|
+
workspaceFileRead: (sessionId, path, range) => ({
|
|
154
|
+
workspaceFileScopeId: sessionId,
|
|
155
|
+
path,
|
|
156
|
+
range: range ?? {}
|
|
157
|
+
}),
|
|
158
|
+
/** Flat args like the read — the descriptor scopes workspaceFileScopeId on the wire. */
|
|
159
|
+
workspaceFileList: (sessionId, path) => ({
|
|
160
|
+
workspaceFileScopeId: sessionId,
|
|
161
|
+
path
|
|
162
|
+
}),
|
|
163
|
+
/** Flat args like the read — WorkspaceByteRange {offset, length}, 0-based
|
|
164
|
+
* bytes; `{}` = the host's window defaults. */
|
|
165
|
+
workspaceFileReadBytes: (sessionId, path, range) => ({
|
|
166
|
+
workspaceFileScopeId: sessionId,
|
|
167
|
+
path,
|
|
168
|
+
range: range ?? {}
|
|
169
|
+
})
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* The ONE stream carrier (0.1.2): `/api/remote.mux` WebSocket multiplexing
|
|
173
|
+
* logical streams — `{type:'open',streamId,endpoint,payload}` opens one,
|
|
174
|
+
* `{type:'item'|'end'|'error',streamId,…}` frames carry it (stream-protocol.ts).
|
|
175
|
+
* Cookie-authenticated like every /api surface. The old events.mux/events.host
|
|
176
|
+
* pair is gone (404).
|
|
177
|
+
*/
|
|
178
|
+
var DSH_REMOTE_MUX_WS_PATH = "/api/remote.mux";
|
|
179
|
+
/** Forwarded-event logical stream endpoint ($events — approvals/questions ride it). */
|
|
180
|
+
var REMOTE_EVENT_STREAM_ENDPOINT = "$events";
|
|
181
|
+
/** Empty standard payload that opens the $events stream (stream-protocol.ts). */
|
|
182
|
+
var REMOTE_EVENT_STREAM_PAYLOAD = { args: {} };
|
|
183
|
+
/** Waterfall-result unary endpoint answering a forwarded event ($events/result). */
|
|
184
|
+
var REMOTE_EVENT_RESULT_ENDPOINT = "$events/result";
|
|
185
|
+
/** Error carrying the DSH RpcError (ok:false result). */
|
|
186
|
+
var DshRpcError = class extends Error {
|
|
187
|
+
code;
|
|
188
|
+
details;
|
|
189
|
+
constructor(code, message, details) {
|
|
190
|
+
super(`DSH RPC ${code}: ${message}`);
|
|
191
|
+
this.name = "DshRpcError";
|
|
192
|
+
this.code = code;
|
|
193
|
+
this.details = details;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* api-dsh error mapping (Module Map: dsh-rpc owns error mapping; routes stay DRY
|
|
198
|
+
* with no cross-route imports). DshRpcError → 502 with the host's code; anything
|
|
199
|
+
* else (transport down) → 503 host-unreachable.
|
|
200
|
+
*/
|
|
201
|
+
function statusFor(err) {
|
|
202
|
+
return err instanceof DshRpcError ? 502 : 503;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Host codes for the subagent-ownership family (session-controller
|
|
206
|
+
* validateAddress + apiSessionSubagentOwnershipError): the request reached the
|
|
207
|
+
* host and was REJECTED by session policy — the host is fine, the address form
|
|
208
|
+
* or ownership is the problem. Never surfaces as "cannot reach the host".
|
|
209
|
+
* 0.1.2-alpha.2+ wire vocabulary: `<domain>/<reason>` codes (the 2026-08-28
|
|
210
|
+
* RemoteError note in the harness repo renamed them wholesale).
|
|
211
|
+
*/
|
|
212
|
+
var SUBAGENT_REJECTION_CODES = /* @__PURE__ */ new Set([
|
|
213
|
+
"session/agent-busy",
|
|
214
|
+
"subagent/unauthorized",
|
|
215
|
+
"subagent/not-found",
|
|
216
|
+
"subagent/catalog-diagnostic"
|
|
217
|
+
]);
|
|
218
|
+
/** True when the error is a host subagent-ownership rejection (see the code set). */
|
|
219
|
+
function isSubagentRejection(err) {
|
|
220
|
+
return err instanceof DshRpcError && SUBAGENT_REJECTION_CODES.has(err.code);
|
|
221
|
+
}
|
|
222
|
+
/** Error body for api-dsh routes: machine code + user-facing message.
|
|
223
|
+
* DshRpcError details ride along when the host sent them — the native
|
|
224
|
+
* picker fallback branches on details.capability. */
|
|
225
|
+
function mapRpcFailure(err) {
|
|
226
|
+
if (err instanceof DshRpcError) return {
|
|
227
|
+
ok: false,
|
|
228
|
+
error: {
|
|
229
|
+
code: err.code,
|
|
230
|
+
message: err.message,
|
|
231
|
+
...err.details !== void 0 ? { details: err.details } : {}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
error: {
|
|
237
|
+
code: "host-unreachable",
|
|
238
|
+
message: err instanceof Error ? err.message : String(err)
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Wave 2 (task 2.1) — the typed prompt-image surface. Raster media types
|
|
244
|
+
* the version-one wire accepts, pinned to the host schema
|
|
245
|
+
* (imageMediaTypeSchema); the server lane owns its own whitelist — client
|
|
246
|
+
* admission (attachment-service) is a different trust boundary and is
|
|
247
|
+
* never imported here.
|
|
248
|
+
*/
|
|
249
|
+
var PROMPT_IMAGE_MEDIA_TYPES = [
|
|
250
|
+
"image/png",
|
|
251
|
+
"image/jpeg",
|
|
252
|
+
"image/webp",
|
|
253
|
+
"image/gif"
|
|
254
|
+
];
|
|
255
|
+
/** Type guard for the wire's raster whitelist (route validation entry). */
|
|
256
|
+
function isPromptImageMediaType(value) {
|
|
257
|
+
return PROMPT_IMAGE_MEDIA_TYPES.includes(value);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Build ordered session.prompt content — images first, text last (the
|
|
261
|
+
* host composer's own order); the text part is omitted when empty, so an
|
|
262
|
+
* attachments-only send is exactly the image parts (DSH sendSession: the
|
|
263
|
+
* text part appears only for non-empty text).
|
|
264
|
+
*
|
|
265
|
+
* @param text - prompt text, sent verbatim as one text block when non-empty.
|
|
266
|
+
* @param images - validated image parts, in draft order.
|
|
267
|
+
* @returns the content array for the session.prompt payload.
|
|
268
|
+
*/
|
|
269
|
+
function buildPromptContent(text, images) {
|
|
270
|
+
return [...images, ...text === "" ? [] : [{
|
|
271
|
+
type: "text",
|
|
272
|
+
text
|
|
273
|
+
}]];
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Parse + validate a server-response envelope against the expected rpcId.
|
|
277
|
+
* Throws: DshRpcError when the result is ok:false; Error on envelope/rpcId mismatch.
|
|
278
|
+
*/
|
|
279
|
+
function parseResponse(raw, expectedRpcId) {
|
|
280
|
+
let parsed;
|
|
281
|
+
try {
|
|
282
|
+
parsed = JSON.parse(raw);
|
|
283
|
+
} catch (cause) {
|
|
284
|
+
throw new Error(`dsh-rpc: response is not JSON: ${String(cause)}`);
|
|
285
|
+
}
|
|
286
|
+
if (parsed?.type !== "server-response") throw new Error(`dsh-rpc: expected server-response, got ${JSON.stringify(parsed?.type)}`);
|
|
287
|
+
if (parsed.rpcId !== expectedRpcId) throw new Error(`dsh-rpc: rpcId mismatch — sent ${expectedRpcId}, got ${parsed.rpcId}`);
|
|
288
|
+
const result = parsed.result;
|
|
289
|
+
if (!result || typeof result !== "object" || !("ok" in result)) throw new Error("dsh-rpc: response result is not an RpcResult");
|
|
290
|
+
if (result.ok) return result.value;
|
|
291
|
+
const err = result.error;
|
|
292
|
+
throw new DshRpcError(err.code, err.message, err.details);
|
|
293
|
+
}
|
|
294
|
+
/** Build the RPC endpoint URL: POST {base}/api/{method} (rpc-map.ts key = wire path). */
|
|
295
|
+
function rpcUrl(base, method) {
|
|
296
|
+
return `${(base ?? dshBaseUrl()).replace(/\/+$/, "")}/api/${method}`;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* DshAuth — the 0.1.2 cookie carrier (server-side singleton state).
|
|
300
|
+
*
|
|
301
|
+
* The upgraded host requires a signed session cookie (30d, HttpOnly,
|
|
302
|
+
* SameSite=Strict, authority-bound) on every /api call and stream; the cookie
|
|
303
|
+
* is minted by ONE GET / with the launch token (the `?token=*** URL `dsh web`
|
|
304
|
+
* prints). The token is per-process; the minted cookie survives host
|
|
305
|
+
* restarts, so the exchange runs once and again only on a 401 (fresh host
|
|
306
|
+
* process). Single-flight: concurrent 401s share one mint.
|
|
307
|
+
*/
|
|
308
|
+
var DshAuth = class {
|
|
309
|
+
cookie = null;
|
|
310
|
+
minting;
|
|
311
|
+
baseUrl;
|
|
312
|
+
fetchFn;
|
|
313
|
+
log;
|
|
314
|
+
constructor(opts = {}) {
|
|
315
|
+
this.baseUrl = opts.baseUrl ?? dshBaseUrl();
|
|
316
|
+
this.fetchFn = opts.fetchFn ?? fetch;
|
|
317
|
+
this.log = opts.log ?? (() => {});
|
|
318
|
+
}
|
|
319
|
+
/** Current cookie pair, or null before the first successful mint. */
|
|
320
|
+
current() {
|
|
321
|
+
return this.cookie;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Ensure a cookie exists: reuse the minted one, else exchange the operator
|
|
325
|
+
* token from readDshAuthConfig. No token configured → null (hosts without
|
|
326
|
+
* the 0.1.2 fence never send 401 and never reach here).
|
|
327
|
+
*/
|
|
328
|
+
async ensureCookie() {
|
|
329
|
+
if (this.cookie !== null) return this.cookie;
|
|
330
|
+
this.minting ??= this.mint();
|
|
331
|
+
try {
|
|
332
|
+
return await this.minting;
|
|
333
|
+
} finally {
|
|
334
|
+
this.minting = void 0;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/** Forget the cookie (next call re-mints — a 401 means a new host process). */
|
|
338
|
+
invalidate() {
|
|
339
|
+
this.cookie = null;
|
|
340
|
+
}
|
|
341
|
+
/** Cookie header value for fetch/WS calls; undefined before any mint. */
|
|
342
|
+
cookieHeader() {
|
|
343
|
+
return this.cookie ?? void 0;
|
|
344
|
+
}
|
|
345
|
+
async mint() {
|
|
346
|
+
const { authToken } = readDshAuthConfig();
|
|
347
|
+
if (authToken === null) {
|
|
348
|
+
this.log("dsh-auth: no token configured — calling the host unauthenticated");
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const url = new URL(this.baseUrl);
|
|
352
|
+
url.pathname = "/";
|
|
353
|
+
url.search = "";
|
|
354
|
+
const exchange = `${url.origin}${url.pathname}?token=${encodeURIComponent(authToken)}`;
|
|
355
|
+
try {
|
|
356
|
+
const response = await this.fetchFn(exchange, { redirect: "manual" });
|
|
357
|
+
const setCookie = response.headers.get("set-cookie");
|
|
358
|
+
if (response.status === 303 && setCookie !== null) {
|
|
359
|
+
this.cookie = setCookie.split(";")[0] ?? null;
|
|
360
|
+
this.log("dsh-auth: cookie minted");
|
|
361
|
+
return this.cookie;
|
|
362
|
+
}
|
|
363
|
+
this.log(`dsh-auth: exchange failed (HTTP ${String(response.status)}) — stale token?`);
|
|
364
|
+
return null;
|
|
365
|
+
} catch (err) {
|
|
366
|
+
this.log("dsh-auth: exchange transport failure", err);
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
//#endregion
|
|
372
|
+
//#region src/lib/server/dsh-connection.ts
|
|
373
|
+
/**
|
|
374
|
+
* dsh-connection — the transport singleton (BC-5) owning every wire byte to
|
|
375
|
+
* the DSH Host (BC-6: it speaks only through dsh-rpc helpers and the two
|
|
376
|
+
* downlink WebSocket paths).
|
|
377
|
+
*
|
|
378
|
+
* State model (PRD §3.5):
|
|
379
|
+
* mux frames (session/subscribed, session/event) → per-session ring buffer
|
|
380
|
+
* host frames (host/session-status) → per-session running map
|
|
381
|
+
* running map re-baselines from session.list at every generation open
|
|
382
|
+
* (2026-08-26): the host pushes status only on TRANSITIONS and sends no
|
|
383
|
+
* initial snapshot, so a downlink rebuilt mid-turn would otherwise never
|
|
384
|
+
* learn the session was already running.
|
|
385
|
+
* ring buffer = freshness only; the ledger (session.history) is the truth —
|
|
386
|
+
* cold loads and every resync re-read the ledger (BC-4).
|
|
387
|
+
*
|
|
388
|
+
* Generation semantics (guide Part 1): the two downlinks form one generation;
|
|
389
|
+
* if either dies, both are rebuilt after backoff.
|
|
390
|
+
*/
|
|
391
|
+
/** Live capacity from ~/.dsi/settings.yaml (no cache — runtime edits
|
|
392
|
+
* apply, same contract as every other config section). */
|
|
393
|
+
function ringCapacity() {
|
|
394
|
+
return readServerConfig().ringCapacity;
|
|
395
|
+
}
|
|
396
|
+
var singleton;
|
|
397
|
+
/** The process-wide transport (BC-5). Creates lazily on first use. */
|
|
398
|
+
function getDshConnection(opts = {}) {
|
|
399
|
+
singleton ??= new DshConnection(opts);
|
|
400
|
+
return singleton;
|
|
401
|
+
}
|
|
402
|
+
var DshConnection = class {
|
|
403
|
+
baseUrl;
|
|
404
|
+
fetchFn;
|
|
405
|
+
wsFactory;
|
|
406
|
+
auth;
|
|
407
|
+
rpcIdFactory;
|
|
408
|
+
backoff;
|
|
409
|
+
log;
|
|
410
|
+
buffers = /* @__PURE__ */ new Map();
|
|
411
|
+
running = /* @__PURE__ */ new Map();
|
|
412
|
+
/**
|
|
413
|
+
* Live assistant-stream tails (0.1.3-alpha.1): the one in-flight model
|
|
414
|
+
* attempt per session, folded from the follow's seq-less assistant-stream
|
|
415
|
+
* frames. Presentation state only — never part of the durable cursor
|
|
416
|
+
* algebra (lastSeq/gap/coverage), because the frames carry no seq.
|
|
417
|
+
*/
|
|
418
|
+
liveTails = /* @__PURE__ */ new Map();
|
|
419
|
+
/**
|
|
420
|
+
* Session-ids that received a host/session-status push since the running
|
|
421
|
+
* re-baseline RPC was ISSUED (2026-08-26). A push is a strictly newer
|
|
422
|
+
* transition than an in-flight snapshot — the seed must never overwrite
|
|
423
|
+
* one. Swapped for a fresh set every time a seed is issued; read at
|
|
424
|
+
* apply time so pushes landing mid-flight are counted.
|
|
425
|
+
*/
|
|
426
|
+
runningSeedRaces = /* @__PURE__ */ new Set();
|
|
427
|
+
wsMux;
|
|
428
|
+
generation = 0;
|
|
429
|
+
/** Logical streams by streamId → owning socket (0.1.2 remote.mux). */
|
|
430
|
+
streamSockets = /* @__PURE__ */ new Map();
|
|
431
|
+
/** Generation-scoped counter keeping stream ids unique across rebuilds. */
|
|
432
|
+
streamSeq = 0;
|
|
433
|
+
/** $events generation clientId (RemoteEventReadyFrame; binds $events/result). */
|
|
434
|
+
eventClientId = null;
|
|
435
|
+
/** True once the carrier's OPEN event fired (sends are legal). */
|
|
436
|
+
opened = false;
|
|
437
|
+
/** Per-session projection baselines (follow snapshot + control frames; W2). */
|
|
438
|
+
projections = /* @__PURE__ */ new Map();
|
|
439
|
+
/** Workspace registry cache from workspace/follow (W2; authority = host). */
|
|
440
|
+
workspaces = [];
|
|
441
|
+
/** Waiters resolved by the first workspace/follow baseline (one-shot). */
|
|
442
|
+
workspaceBaselineWaiters = /* @__PURE__ */ new Set();
|
|
443
|
+
/** Subscribes parked while the carrier is CONNECTING (flushed on open). */
|
|
444
|
+
pendingSubscribes = /* @__PURE__ */ new Set();
|
|
445
|
+
/**
|
|
446
|
+
* Parent-address hints for subagent-origin sessions (SubagentAddressHint),
|
|
447
|
+
* folded from session.list rows by rawListSessions.
|
|
448
|
+
*/
|
|
449
|
+
subagentHints = /* @__PURE__ */ new Map();
|
|
450
|
+
/** Session-ids whose hint absence was already probed with a list pass
|
|
451
|
+
* (one probe per session lifetime — ordinary sessions never re-probe). */
|
|
452
|
+
hintProbed = /* @__PURE__ */ new Set();
|
|
453
|
+
reconnectTimer;
|
|
454
|
+
disposed = false;
|
|
455
|
+
/** Wave 4.2 — consecutive failed generations; drives the backoff ladder. */
|
|
456
|
+
reconnectAttempt = 0;
|
|
457
|
+
/** Reap grace (see DshConnectionOptions.answerReapGraceMs). */
|
|
458
|
+
answerReapGraceMs;
|
|
459
|
+
answerReapTimer;
|
|
460
|
+
/** Stale-generation retry deadline (see DshConnectionOptions.staleGenerationRetryMs). */
|
|
461
|
+
staleGenerationRetryMs;
|
|
462
|
+
constructor(opts = {}) {
|
|
463
|
+
this.baseUrl = opts.baseUrl ?? dshBaseUrl();
|
|
464
|
+
this.fetchFn = opts.fetchFn ?? fetch;
|
|
465
|
+
this.wsFactory = opts.wsFactory ?? ((url, headers) => new WebSocket(url, headers === void 0 ? void 0 : { headers }));
|
|
466
|
+
this.auth = opts.auth ?? new DshAuth({
|
|
467
|
+
baseUrl: this.baseUrl,
|
|
468
|
+
log: opts.log
|
|
469
|
+
});
|
|
470
|
+
this.rpcIdFactory = opts.rpcIdFactory ?? (() => crypto.randomUUID());
|
|
471
|
+
this.backoff = opts.backoffScheduleMs ?? [
|
|
472
|
+
1e3,
|
|
473
|
+
2e3,
|
|
474
|
+
5e3,
|
|
475
|
+
1e4,
|
|
476
|
+
3e4
|
|
477
|
+
];
|
|
478
|
+
this.answerReapGraceMs = opts.answerReapGraceMs ?? 3e3;
|
|
479
|
+
this.staleGenerationRetryMs = opts.staleGenerationRetryMs ?? 5e3;
|
|
480
|
+
this.log = opts.log ?? (() => {});
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Open the stream generation (idempotent): ONE cookie-authenticated
|
|
484
|
+
* remote.mux socket (0.1.2 — events.mux/events.host are gone), with every
|
|
485
|
+
* subscribed session's `session/follow` reopened as a logical stream plus
|
|
486
|
+
* the $events forwarded-event stream (approvals/questions).
|
|
487
|
+
*/
|
|
488
|
+
ensureDownlinks() {
|
|
489
|
+
if (this.disposed || this.wsMux) return;
|
|
490
|
+
++this.generation;
|
|
491
|
+
this.log(`dsh-connection: opening remote.mux generation ${this.generation}`);
|
|
492
|
+
this.wsMux = this.openDownlink((socket) => this.onMuxOpen(socket));
|
|
493
|
+
this.seedRunning();
|
|
494
|
+
}
|
|
495
|
+
openDownlink(attach) {
|
|
496
|
+
const url = this.baseUrl.replace(/^http/, "ws") + DSH_REMOTE_MUX_WS_PATH;
|
|
497
|
+
const headers = this.auth.cookieHeader();
|
|
498
|
+
const socket = this.wsFactory(url, headers === void 0 ? void 0 : { cookie: headers });
|
|
499
|
+
this.auth.ensureCookie().catch(() => null);
|
|
500
|
+
socket.addEventListener("message", (e) => {
|
|
501
|
+
try {
|
|
502
|
+
this.onStreamFrame(JSON.parse(String(e.data)));
|
|
503
|
+
} catch (err) {
|
|
504
|
+
this.log("dsh-connection: dropping malformed frame", err);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
socket.addEventListener("open", () => {
|
|
508
|
+
this.opened = true;
|
|
509
|
+
attach(socket);
|
|
510
|
+
});
|
|
511
|
+
socket.addEventListener("close", () => this.onDownlinkClosed());
|
|
512
|
+
socket.addEventListener("error", () => this.onDownlinkClosed());
|
|
513
|
+
return socket;
|
|
514
|
+
}
|
|
515
|
+
/** mux open: (re)open every subscribed session's follow stream + $events. */
|
|
516
|
+
onMuxOpen(socket) {
|
|
517
|
+
this.streamSeq += 1;
|
|
518
|
+
this.pendingSubscribes.clear();
|
|
519
|
+
for (const sessionId of this.buffers.keys()) this.openFollowWithProbe(socket, sessionId);
|
|
520
|
+
this.openEventStream(socket);
|
|
521
|
+
this.openStateStream(socket, `workspaces-${String(this.streamSeq)}`, "workspace/follow");
|
|
522
|
+
this.openStateStream(socket, `control-${String(this.streamSeq)}`, "session/control");
|
|
523
|
+
}
|
|
524
|
+
/** Open one state stream (workspace/follow | session/control) with empty args. */
|
|
525
|
+
openStateStream(socket, streamId, endpoint) {
|
|
526
|
+
this.sendStreamOpen(socket, streamId, endpoint, { args: {} });
|
|
527
|
+
}
|
|
528
|
+
openFollowStream(socket, sessionId) {
|
|
529
|
+
this.sendStreamOpen(socket, followStreamId(sessionId, this.streamSeq), "session/follow", { args: { request: {
|
|
530
|
+
address: this.addressFor(sessionId),
|
|
531
|
+
maxMessages: 200,
|
|
532
|
+
assistantStream: true
|
|
533
|
+
} } });
|
|
534
|
+
}
|
|
535
|
+
openEventStream(socket) {
|
|
536
|
+
this.sendStreamOpen(socket, `events-${String(this.streamSeq)}`, REMOTE_EVENT_STREAM_ENDPOINT, REMOTE_EVENT_STREAM_PAYLOAD);
|
|
537
|
+
}
|
|
538
|
+
sendStreamOpen(socket, streamId, endpoint, payload) {
|
|
539
|
+
const send = (msg) => socket.send?.(msg);
|
|
540
|
+
this.streamSockets.set(streamId, socket);
|
|
541
|
+
send(JSON.stringify({
|
|
542
|
+
type: "open",
|
|
543
|
+
streamId,
|
|
544
|
+
endpoint,
|
|
545
|
+
payload
|
|
546
|
+
}));
|
|
547
|
+
}
|
|
548
|
+
/** Either socket dying rebuilds the whole generation (guide Part 1). */
|
|
549
|
+
onDownlinkClosed() {
|
|
550
|
+
if (this.disposed) return;
|
|
551
|
+
if (!this.wsMux) return;
|
|
552
|
+
this.log("dsh-connection: remote.mux died — rebuilding generation");
|
|
553
|
+
this.fireCatalogInvalidated("all");
|
|
554
|
+
this.teardownSockets();
|
|
555
|
+
this.scheduleReconnect();
|
|
556
|
+
}
|
|
557
|
+
scheduleReconnect() {
|
|
558
|
+
if (this.disposed) return;
|
|
559
|
+
const rung = Math.min(this.reconnectAttempt, this.backoff.length - 1);
|
|
560
|
+
const delay = this.backoff[rung];
|
|
561
|
+
if (this.reconnectTimer !== void 0) clearTimeout(this.reconnectTimer);
|
|
562
|
+
this.reconnectTimer = setTimeout(() => {
|
|
563
|
+
if (this.disposed) return;
|
|
564
|
+
this.reconnectAttempt += 1;
|
|
565
|
+
this.ensureDownlinks();
|
|
566
|
+
}, delay);
|
|
567
|
+
}
|
|
568
|
+
teardownSockets() {
|
|
569
|
+
const sockets = [this.wsMux];
|
|
570
|
+
this.wsMux = void 0;
|
|
571
|
+
this.opened = false;
|
|
572
|
+
this.streamSockets.clear();
|
|
573
|
+
this.projections.clear();
|
|
574
|
+
this.workspaces = [];
|
|
575
|
+
for (const waiter of this.workspaceBaselineWaiters) waiter.resolve();
|
|
576
|
+
this.workspaceBaselineWaiters.clear();
|
|
577
|
+
if (this.reconnectTimer !== void 0) {
|
|
578
|
+
clearTimeout(this.reconnectTimer);
|
|
579
|
+
this.reconnectTimer = void 0;
|
|
580
|
+
}
|
|
581
|
+
for (const socket of sockets) socket?.close();
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* 0.1.2 remote.mux demux: `{item,end,error,streamId}` frames. Follow
|
|
585
|
+
* streams deliver a `snapshot` opening (seeds the baseline) then event
|
|
586
|
+
* entries (ring buffer appends); the $events stream delivers forwarded
|
|
587
|
+
* Cordis events (approval/question waterfalls → answerer registry).
|
|
588
|
+
*/
|
|
589
|
+
onStreamFrame(payload) {
|
|
590
|
+
const frame = payload;
|
|
591
|
+
if (typeof frame?.streamId !== "string") return;
|
|
592
|
+
if (frame.type === "item" && frame.value !== void 0) {
|
|
593
|
+
this.reconnectAttempt = 0;
|
|
594
|
+
const value = frame.value;
|
|
595
|
+
if (frame.streamId.startsWith("follow-")) this.onFollowItem(frame.streamId, value);
|
|
596
|
+
else if (frame.streamId.startsWith("events-")) this.onEventItem(value);
|
|
597
|
+
else if (frame.streamId.startsWith("workspaces-")) this.onWorkspaceItem(value);
|
|
598
|
+
else if (frame.streamId.startsWith("control-")) this.onControlItem(value);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (frame.type === "error") this.log(`dsh-connection: stream ${frame.streamId} failed`, frame.error?.code, frame.error?.message);
|
|
602
|
+
this.streamSockets.delete(frame.streamId);
|
|
603
|
+
}
|
|
604
|
+
/** session/follow item: snapshot seeds the baseline; events append; the
|
|
605
|
+
* opted-in assistant-stream frames update the live tail (seq-less —
|
|
606
|
+
* they never touch the durable cursor bookkeeping below). */
|
|
607
|
+
onFollowItem(streamId, value) {
|
|
608
|
+
if (value.type === "snapshot") {
|
|
609
|
+
const sessionId = this.sessionIdOfStream(streamId);
|
|
610
|
+
if (sessionId === null) return;
|
|
611
|
+
const snapshot = value;
|
|
612
|
+
const events = expandRecords(snapshot.records ?? []);
|
|
613
|
+
for (const event of events) this.appendEvent(sessionId, event);
|
|
614
|
+
this.seedSnapshotCursor(sessionId, snapshot.cursor);
|
|
615
|
+
if (snapshot.projections !== null && typeof snapshot.projections === "object") this.projections.set(sessionId, snapshot.projections);
|
|
616
|
+
this.seedLiveTailFromBaseline(sessionId, snapshot.assistantStream);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (value.type === "assistant-stream") {
|
|
620
|
+
const sessionId = this.sessionIdOfStream(streamId);
|
|
621
|
+
const frame = value.frame;
|
|
622
|
+
if (sessionId !== null && frame !== void 0) this.onAssistantStreamFrame(sessionId, frame);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (value.type === "event") {
|
|
626
|
+
const sessionId = this.sessionIdOfStream(streamId);
|
|
627
|
+
const entry = value;
|
|
628
|
+
if (sessionId !== null && entry.event !== void 0) for (const event of expandEntry(entry.event)) this.appendEvent(sessionId, event);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/** $events item: forwarded Cordis event frames (waterfall = answerable). */
|
|
632
|
+
onEventItem(value) {
|
|
633
|
+
if (value.type === "ready") {
|
|
634
|
+
if (typeof value.clientId === "string") this.eventClientId = value.clientId;
|
|
635
|
+
this.log("dsh-connection: $events stream ready");
|
|
636
|
+
this.armAnswerReap();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (value.type === "waterfall") {
|
|
640
|
+
const event = typeof value.event === "string" ? value.event : "";
|
|
641
|
+
const eventId = typeof value.eventId === "string" ? value.eventId : void 0;
|
|
642
|
+
const sessionId = typeof value.agentId === "string" ? value.agentId : void 0;
|
|
643
|
+
const request = value.request !== null && typeof value.request === "object" ? value.request : {};
|
|
644
|
+
if (eventId === void 0) return;
|
|
645
|
+
if (event === "approval/request") {
|
|
646
|
+
this.registerPending({
|
|
647
|
+
type: "approval/requested",
|
|
648
|
+
sessionId: sessionId ?? "",
|
|
649
|
+
...request
|
|
650
|
+
}, eventId);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (event === "user-questions/request") {
|
|
654
|
+
this.registerPending({
|
|
655
|
+
type: "question/requested",
|
|
656
|
+
sessionId: sessionId ?? "",
|
|
657
|
+
...request
|
|
658
|
+
}, eventId);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
this.log("dsh-connection: forwarded waterfall ignored", event);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (value.type === "cancel") {
|
|
665
|
+
const eventId = typeof value.eventId === "string" ? value.eventId : void 0;
|
|
666
|
+
if (eventId !== void 0) this.withdrawPending(eventId);
|
|
667
|
+
this.log("dsh-connection: forwarded waterfall cancelled");
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (value.type === "settle") {
|
|
671
|
+
const eventId = typeof value.eventId === "string" ? value.eventId : void 0;
|
|
672
|
+
const outcome = typeof value.outcome === "string" ? value.outcome : "unknown";
|
|
673
|
+
if (eventId !== void 0) this.withdrawPending(eventId, outcome);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (value.type === "emit") {
|
|
677
|
+
if (value.event === "api-session/status" && Array.isArray(value.args)) {
|
|
678
|
+
const [sessionId, running] = value.args;
|
|
679
|
+
if (typeof sessionId === "string") this.running.set(sessionId, running === true);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if (value.event === "commands/change") {
|
|
683
|
+
this.fireCatalogInvalidated("all");
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
if (value.event === "agent-preset/selected" && typeof value.agentId === "string") {
|
|
687
|
+
this.fireCatalogInvalidated(value.agentId);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
this.log("dsh-connection: forwarded emit ignored", value.event);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* workspace/follow item (W2): baseline replaces the cache; increments
|
|
696
|
+
* apply in order (upsert/remove/order/archived — archived sessions are
|
|
697
|
+
* hidden from DSI's surfaces the same way the 0.1.1 registry did).
|
|
698
|
+
*/
|
|
699
|
+
onWorkspaceItem(value) {
|
|
700
|
+
if (value.type === "baseline") {
|
|
701
|
+
const baseline = value.value ?? {};
|
|
702
|
+
const rows = Array.isArray(baseline.items) ? baseline.items : [];
|
|
703
|
+
this.workspaces = rows.flatMap((row) => {
|
|
704
|
+
const w = row;
|
|
705
|
+
if (typeof w?.workspaceId !== "string" || typeof w?.path !== "string") return [];
|
|
706
|
+
return [{
|
|
707
|
+
workspaceId: w.workspaceId,
|
|
708
|
+
title: typeof w.title === "string" && w.title.length > 0 ? w.title : w.path,
|
|
709
|
+
path: w.path,
|
|
710
|
+
sessionIds: Array.isArray(w.sessionIds) ? w.sessionIds.filter((id) => typeof id === "string") : []
|
|
711
|
+
}];
|
|
712
|
+
});
|
|
713
|
+
for (const waiter of this.workspaceBaselineWaiters) waiter.resolve();
|
|
714
|
+
this.workspaceBaselineWaiters.clear();
|
|
715
|
+
this.log(`dsh-connection: workspace baseline (${this.workspaces.length} rows)`);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (value.type === "upsert") {
|
|
719
|
+
const w = value.workspace;
|
|
720
|
+
if (typeof w?.workspaceId !== "string" || typeof w?.path !== "string") return;
|
|
721
|
+
const row = {
|
|
722
|
+
workspaceId: w.workspaceId,
|
|
723
|
+
title: typeof w.title === "string" && w.title.length > 0 ? w.title : w.path,
|
|
724
|
+
path: w.path,
|
|
725
|
+
sessionIds: Array.isArray(w.sessionIds) ? w.sessionIds.filter((id) => typeof id === "string") : []
|
|
726
|
+
};
|
|
727
|
+
const idx = this.workspaces.findIndex((x) => x.workspaceId === row.workspaceId);
|
|
728
|
+
if (idx >= 0) this.workspaces.splice(idx, 1, row);
|
|
729
|
+
else this.workspaces.push(row);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (value.type === "remove" && typeof value.workspaceId === "string") {
|
|
733
|
+
this.workspaces = this.workspaces.filter((x) => x.workspaceId !== value.workspaceId);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* session/control item (W2): the baseline's per-session projections seed
|
|
739
|
+
* the store (permissions for the access chip); later projection frames
|
|
740
|
+
* update single keys. queue/job frames are not DSI state (ignored).
|
|
741
|
+
*/
|
|
742
|
+
onControlItem(value) {
|
|
743
|
+
if (value.type === "baseline") {
|
|
744
|
+
const baseline = value.value ?? {};
|
|
745
|
+
for (const [sessionId, block] of Object.entries(baseline.projections ?? {})) if (block !== null && typeof block === "object") this.projections.set(sessionId, block);
|
|
746
|
+
this.log(`dsh-connection: control baseline (${this.projections.size} sessions)`);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (value.type === "projection" && typeof value.sessionId === "string" && typeof value.key === "string") {
|
|
750
|
+
const block = this.projections.get(value.sessionId) ?? {
|
|
751
|
+
asOfSeq: 0,
|
|
752
|
+
values: {}
|
|
753
|
+
};
|
|
754
|
+
this.projections.set(value.sessionId, {
|
|
755
|
+
asOfSeq: typeof value.seq === "number" ? value.seq : block.asOfSeq,
|
|
756
|
+
values: {
|
|
757
|
+
...block.values,
|
|
758
|
+
[value.key]: value.value
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
/** Extract the sessionId from a follow-<sessionId>-<seq> stream id. */
|
|
764
|
+
sessionIdOfStream(streamId) {
|
|
765
|
+
const rest = streamId.slice(7);
|
|
766
|
+
const cut = rest.lastIndexOf("-");
|
|
767
|
+
return cut > 0 ? rest.slice(0, cut) : null;
|
|
768
|
+
}
|
|
769
|
+
pendingAnswers = /* @__PURE__ */ new Map();
|
|
770
|
+
/** Settlement ring: last outcome per rpcId, delivered-then-pruned. */
|
|
771
|
+
settledAnswers = /* @__PURE__ */ new Map();
|
|
772
|
+
/** Catalog-invalidation registry (Slash Menu W1) — listeners see every arm. */
|
|
773
|
+
catalogInvalidated = /* @__PURE__ */ new Set();
|
|
774
|
+
/** Subscribe to catalog invalidations ($events arms + socket reset). */
|
|
775
|
+
onCatalogInvalidated(listener) {
|
|
776
|
+
this.catalogInvalidated.add(listener);
|
|
777
|
+
return () => this.catalogInvalidated.delete(listener);
|
|
778
|
+
}
|
|
779
|
+
fireCatalogInvalidated(scope) {
|
|
780
|
+
for (const listener of this.catalogInvalidated) listener(scope);
|
|
781
|
+
}
|
|
782
|
+
onMuxFrame(payload, rpcId) {
|
|
783
|
+
this.reconnectAttempt = 0;
|
|
784
|
+
const frame = payload;
|
|
785
|
+
switch (frame?.type) {
|
|
786
|
+
case "session/subscribed":
|
|
787
|
+
if (typeof frame.sessionId === "string") this.seedBaseline(frame.sessionId, frame.lastSeq ?? -1);
|
|
788
|
+
return;
|
|
789
|
+
case "session/event":
|
|
790
|
+
if (frame.sessionId && frame.event) this.appendEvent(frame.sessionId, frame.event);
|
|
791
|
+
return;
|
|
792
|
+
case "approval/requested":
|
|
793
|
+
case "question/requested":
|
|
794
|
+
this.registerPending(frame, rpcId);
|
|
795
|
+
return;
|
|
796
|
+
case "approval/resolved":
|
|
797
|
+
case "question/resolved":
|
|
798
|
+
this.registerSettlement(frame);
|
|
799
|
+
return;
|
|
800
|
+
default: this.log("dsh-connection: mux frame ignored", frame?.type);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
onHostFrame(payload) {
|
|
804
|
+
const frame = payload;
|
|
805
|
+
if (frame?.type === "host/session-status" && typeof frame.sessionId === "string") {
|
|
806
|
+
this.runningSeedRaces.add(frame.sessionId);
|
|
807
|
+
this.running.set(frame.sessionId, Boolean(frame.running));
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* 2026-08-26 running re-baseline: DSH pushes host/session-status only on
|
|
812
|
+
* transitions and the host stream sends no initial snapshot on (re)open —
|
|
813
|
+
* a downlink rebuilt mid-turn never sees the running:true flip that
|
|
814
|
+
* happened before it existed (the live bug: events kept flowing on the
|
|
815
|
+
* mux while the streaming indicator said idle). session.list is the host's
|
|
816
|
+
* documented reconnect baseline, so every generation opens with ONE list
|
|
817
|
+
* RPC seeding the whole map — which also prunes entries for sessions that
|
|
818
|
+
* died while disconnected. Pushes that land while the RPC is in flight win
|
|
819
|
+
* over the snapshot (they are strictly newer transitions); a seed from a
|
|
820
|
+
* superseded generation is dropped entirely.
|
|
821
|
+
*/
|
|
822
|
+
async seedRunning() {
|
|
823
|
+
const generation = this.generation;
|
|
824
|
+
this.runningSeedRaces = /* @__PURE__ */ new Set();
|
|
825
|
+
try {
|
|
826
|
+
const { items } = await this.listSessions();
|
|
827
|
+
if (this.disposed || generation !== this.generation) return;
|
|
828
|
+
const races = this.runningSeedRaces;
|
|
829
|
+
const next = /* @__PURE__ */ new Map();
|
|
830
|
+
for (const [id, running] of this.running) if (races.has(id)) next.set(id, running);
|
|
831
|
+
for (const row of items) if (!races.has(row.sessionId)) next.set(row.sessionId, row.running);
|
|
832
|
+
this.running = next;
|
|
833
|
+
this.log(`dsh-connection: running map re-baselined (${items.length} sessions)`);
|
|
834
|
+
} catch (err) {
|
|
835
|
+
this.log("dsh-connection: running re-baseline failed — pushes stay authoritative", err);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
bufferFor(sessionId) {
|
|
839
|
+
let buf = this.buffers.get(sessionId);
|
|
840
|
+
if (!buf) {
|
|
841
|
+
buf = {
|
|
842
|
+
entries: [],
|
|
843
|
+
lastSeq: -1,
|
|
844
|
+
coverageFloor: -1
|
|
845
|
+
};
|
|
846
|
+
this.buffers.set(sessionId, buf);
|
|
847
|
+
}
|
|
848
|
+
return buf;
|
|
849
|
+
}
|
|
850
|
+
seedBaseline(sessionId, lastSeq) {
|
|
851
|
+
const buf = this.bufferFor(sessionId);
|
|
852
|
+
if (lastSeq > buf.lastSeq && buf.entries.length === 0) {
|
|
853
|
+
buf.lastSeq = lastSeq;
|
|
854
|
+
buf.coverageFloor = lastSeq;
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (buf.lastSeq >= 0 && lastSeq < buf.lastSeq) {
|
|
858
|
+
buf.entries.length = 0;
|
|
859
|
+
buf.lastSeq = lastSeq;
|
|
860
|
+
buf.coverageFloor = lastSeq;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Snapshot cursor seed (0.1.2). A snapshot BELOW the buffered high-water
|
|
865
|
+
* mark means the upstream was rebuilt (stream restart): stale entries
|
|
866
|
+
* belong to the dead instance — reset the buffer to the snapshot truth
|
|
867
|
+
* (Wave 2 parity with the 0.1.1 rewound-baseline semantics).
|
|
868
|
+
*/
|
|
869
|
+
seedSnapshotCursor(sessionId, cursor) {
|
|
870
|
+
const buf = this.bufferFor(sessionId);
|
|
871
|
+
if (cursor > buf.lastSeq) {
|
|
872
|
+
buf.lastSeq = cursor;
|
|
873
|
+
buf.coverageFloor = cursor;
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (buf.lastSeq >= 0 && cursor < buf.lastSeq) {
|
|
877
|
+
buf.entries.length = 0;
|
|
878
|
+
buf.lastSeq = cursor;
|
|
879
|
+
buf.coverageFloor = cursor;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
appendEvent(sessionId, event) {
|
|
883
|
+
const buf = this.bufferFor(sessionId);
|
|
884
|
+
if (event.seq > buf.lastSeq) {
|
|
885
|
+
if (buf.lastSeq >= 0 && event.seq > buf.lastSeq + 1) {
|
|
886
|
+
buf.coverageFloor = Math.max(buf.coverageFloor, buf.lastSeq);
|
|
887
|
+
this.log(`dsh-connection: seq gap for ${sessionId} (${buf.lastSeq} → ${event.seq}) — resync required below ${event.seq}`);
|
|
888
|
+
}
|
|
889
|
+
buf.lastSeq = event.seq;
|
|
890
|
+
buf.entries.push(event);
|
|
891
|
+
const cap = ringCapacity();
|
|
892
|
+
if (buf.entries.length > cap) buf.entries.splice(0, buf.entries.length - cap);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Fold one assistant-stream frame into the session's live tail. Seq-less
|
|
897
|
+
* presentation state: never touches lastSeq/coverage — the durable
|
|
898
|
+
* cursor algebra stays event-entry-only. The host keeps ONE active
|
|
899
|
+
* attempt per session and drops its own accumulator on any continuity
|
|
900
|
+
* break (foreign attemptId, dense-index gap), so mirrors do the same.
|
|
901
|
+
*/
|
|
902
|
+
onAssistantStreamFrame(sessionId, frame) {
|
|
903
|
+
if (frame.type === "start") {
|
|
904
|
+
this.liveTails.set(sessionId, {
|
|
905
|
+
attemptId: frame.attemptId,
|
|
906
|
+
turn: frame.turn,
|
|
907
|
+
step: frame.step,
|
|
908
|
+
startedAfterSeq: frame.startedAfterSeq,
|
|
909
|
+
startedAt: Date.now(),
|
|
910
|
+
lastTime: Date.now(),
|
|
911
|
+
text: "",
|
|
912
|
+
reasoning: "",
|
|
913
|
+
nextIndex: 0
|
|
914
|
+
});
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const tail = this.liveTails.get(sessionId);
|
|
918
|
+
if (tail === void 0) return;
|
|
919
|
+
if (frame.type === "chunk") {
|
|
920
|
+
if (frame.attemptId !== tail.attemptId || frame.index !== tail.nextIndex) {
|
|
921
|
+
this.liveTails.delete(sessionId);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const kind = frame.chunk?.type;
|
|
925
|
+
if (kind === "text-delta" && typeof frame.chunk?.text === "string") tail.text += frame.chunk.text;
|
|
926
|
+
else if (kind === "reasoning-delta" && typeof frame.chunk?.text === "string") tail.reasoning += frame.chunk.text;
|
|
927
|
+
tail.nextIndex += 1;
|
|
928
|
+
if (typeof frame.time === "number") tail.lastTime = frame.time;
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
this.liveTails.delete(sessionId);
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Seed the live tail from a follow-opening snapshot's assistantStream
|
|
935
|
+
* baseline (mux-drop reopen mid-attempt): expand the compact stream
|
|
936
|
+
* records' text/reasoning runs into the tail. No activeAttempt → clear
|
|
937
|
+
* any remembered tail — the fresh opening is authoritative for what is
|
|
938
|
+
* still in flight.
|
|
939
|
+
*/
|
|
940
|
+
seedLiveTailFromBaseline(sessionId, baseline) {
|
|
941
|
+
const active = baseline?.activeAttempt;
|
|
942
|
+
if (active === void 0) {
|
|
943
|
+
this.liveTails.delete(sessionId);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
const tail = {
|
|
947
|
+
attemptId: active.attemptId,
|
|
948
|
+
turn: active.turn,
|
|
949
|
+
step: active.step,
|
|
950
|
+
startedAfterSeq: active.startedAfterSeq,
|
|
951
|
+
startedAt: Date.now(),
|
|
952
|
+
lastTime: Date.now(),
|
|
953
|
+
text: "",
|
|
954
|
+
reasoning: "",
|
|
955
|
+
nextIndex: active.nextIndex
|
|
956
|
+
};
|
|
957
|
+
for (const record of active.stream ?? []) {
|
|
958
|
+
if (record.type === "text-chunks" || record.type === "reasoning-chunks") {
|
|
959
|
+
const texts = record.texts;
|
|
960
|
+
const joined = Array.isArray(texts) ? texts.filter((t) => typeof t === "string").join("") : "";
|
|
961
|
+
if (record.type === "text-chunks") tail.text += joined;
|
|
962
|
+
else tail.reasoning += joined;
|
|
963
|
+
}
|
|
964
|
+
const times = compactRecordTailTime(record);
|
|
965
|
+
if (times !== void 0) tail.lastTime = times;
|
|
966
|
+
}
|
|
967
|
+
this.liveTails.set(sessionId, tail);
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* The session's live tail for the events poll, or null when no attempt
|
|
971
|
+
* is in flight. Served with the FULL accumulated text each poll — the
|
|
972
|
+
* client replaces its streaming bubble wholesale (no cross-poll fragment
|
|
973
|
+
* bookkeeping, so reconnects/retries converge from the served truth).
|
|
974
|
+
*/
|
|
975
|
+
liveAssistantStream(sessionId) {
|
|
976
|
+
const tail = this.liveTails.get(sessionId);
|
|
977
|
+
if (tail === void 0) return null;
|
|
978
|
+
return {
|
|
979
|
+
id: `a:${String(tail.turn)}:${String(tail.step)}`,
|
|
980
|
+
turn: tail.turn,
|
|
981
|
+
step: tail.step,
|
|
982
|
+
seq: tail.startedAfterSeq,
|
|
983
|
+
time: tail.lastTime,
|
|
984
|
+
text: tail.text,
|
|
985
|
+
reasoning: tail.reasoning
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Delta slice: events with seq > n, plus current lastSeq/running.
|
|
990
|
+
* Wave 4.2: `gap` is true when the caller's `since` mark fell into a hole
|
|
991
|
+
* the buffer no longer covers (mux drop). The response then carries the
|
|
992
|
+
* resync flag so the client re-reads from the ledger instead of trusting
|
|
993
|
+
* a partial delta (BC-4).
|
|
994
|
+
*/
|
|
995
|
+
eventsSince(sessionId, n) {
|
|
996
|
+
const buf = this.bufferFor(sessionId);
|
|
997
|
+
const events = buf.entries.filter((e) => e.seq > n);
|
|
998
|
+
const firstDelta = events[0]?.seq;
|
|
999
|
+
const gap = firstDelta !== void 0 && firstDelta > n + 1;
|
|
1000
|
+
return {
|
|
1001
|
+
events,
|
|
1002
|
+
lastSeq: buf.lastSeq,
|
|
1003
|
+
running: this.running.get(sessionId) ?? false,
|
|
1004
|
+
gap
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Subscribe one session (0.1.2): create its buffer and open a
|
|
1009
|
+
* session/follow logical stream on the live carrier (or mark it for the
|
|
1010
|
+
* next generation). Idempotent; the snapshot frame seeds the baseline.
|
|
1011
|
+
* A repeat call while THIS generation already holds the session's follow
|
|
1012
|
+
* stream is a no-op — the api-dsh events route calls subscribe on every
|
|
1013
|
+
* poll, and reopening would replace the stream (same streamId → fresh
|
|
1014
|
+
* snapshot) on every tick. A stream 'end'/'error' frame drops the
|
|
1015
|
+
* tracking entry so the next poll reopens and self-heals.
|
|
1016
|
+
*/
|
|
1017
|
+
subscribe(sessionId) {
|
|
1018
|
+
this.bufferFor(sessionId);
|
|
1019
|
+
const socket = this.wsMux;
|
|
1020
|
+
if (socket === void 0) return;
|
|
1021
|
+
if (this.opened === false) {
|
|
1022
|
+
this.pendingSubscribes.add(sessionId);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (this.streamSockets.has(followStreamId(sessionId, this.streamSeq))) return;
|
|
1026
|
+
this.openFollowWithProbe(socket, sessionId);
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Open the follow stream with the best-known address NOW (sync, the
|
|
1030
|
+
* historical contract), then — when this turns out to be a subagent child
|
|
1031
|
+
* nobody has folded yet — probe and REOPEN with the parent address (the
|
|
1032
|
+
* mux replaces a stream id; the fresh snapshot reseeds the baseline).
|
|
1033
|
+
*/
|
|
1034
|
+
openFollowWithProbe(socket, sessionId) {
|
|
1035
|
+
this.openFollowStream(socket, sessionId);
|
|
1036
|
+
if (!this.subagentHints.has(sessionId) && !this.hintProbed.has(sessionId)) {
|
|
1037
|
+
this.hintProbed.add(sessionId);
|
|
1038
|
+
this.probeHintThenSubscribe(sessionId, socket);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
/** Resolve one session's address hint after a plain-form follow went out;
|
|
1042
|
+
* on a folded hint REOPEN the follow with the parent address (a plain
|
|
1043
|
+
* form on a subagent child is rejected by the host and the stream would
|
|
1044
|
+
* stay dead for the whole generation). Skipped when the carrier moved on
|
|
1045
|
+
* (generation rebuilt / disposed) during the list RPC — onMuxOpen
|
|
1046
|
+
* reopens every buffer with the then-current addresses. */
|
|
1047
|
+
async probeHintThenSubscribe(sessionId, socket) {
|
|
1048
|
+
try {
|
|
1049
|
+
if (!this.subagentHints.has(sessionId)) await this.rawListSessions();
|
|
1050
|
+
} catch {}
|
|
1051
|
+
if (this.disposed || this.wsMux !== socket || this.opened === false) return;
|
|
1052
|
+
if (this.subagentHints.has(sessionId)) this.openFollowStream(socket, sessionId);
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Per-session projection value (W2): follow-snapshot + control-baseline
|
|
1056
|
+
* + control projection frames, newest wins. permissions (access chip),
|
|
1057
|
+
* modelSelection, todos, plan … — null when never seen.
|
|
1058
|
+
*/
|
|
1059
|
+
projectionValue(sessionId, key) {
|
|
1060
|
+
return this.projections.get(sessionId)?.values?.[key];
|
|
1061
|
+
}
|
|
1062
|
+
/** The whole stored block for one session (null when absent). */
|
|
1063
|
+
projectionBlock(sessionId) {
|
|
1064
|
+
return this.projections.get(sessionId) ?? null;
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Bounded wait for the session's FIRST projection baseline (0.1.2 cold
|
|
1068
|
+
* loads seed the access mode + admission numbers from the follow/control
|
|
1069
|
+
* store — session/page carries no projections block). Resolves
|
|
1070
|
+
* immediately when a block exists; null after timeoutMs (an unfenced or
|
|
1071
|
+
* slow host degrades to the documented defaults, never a hang).
|
|
1072
|
+
*/
|
|
1073
|
+
async waitForProjections(sessionId, timeoutMs = 2500) {
|
|
1074
|
+
this.ensureDownlinks();
|
|
1075
|
+
this.subscribe(sessionId);
|
|
1076
|
+
const deadline = Date.now() + timeoutMs;
|
|
1077
|
+
for (;;) {
|
|
1078
|
+
const block = this.projectionBlock(sessionId);
|
|
1079
|
+
if (block !== null) return block;
|
|
1080
|
+
if (Date.now() >= deadline) return null;
|
|
1081
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* True once the workspace/follow baseline landed. Registers the waiter
|
|
1086
|
+
* even while the carrier is CONNECTING (the first RPC often races the
|
|
1087
|
+
* handshake); bounded by 3s — an honest empty after that, never a hang.
|
|
1088
|
+
*/
|
|
1089
|
+
async awaitWorkspaceBaseline() {
|
|
1090
|
+
if (this.workspaces.length > 0) return true;
|
|
1091
|
+
this.ensureDownlinks();
|
|
1092
|
+
if (this.wsMux === void 0) return false;
|
|
1093
|
+
const waiter = Promise.withResolvers();
|
|
1094
|
+
this.workspaceBaselineWaiters.add(waiter);
|
|
1095
|
+
await Promise.race([waiter.promise, new Promise((r) => setTimeout(r, 3e3))]);
|
|
1096
|
+
this.workspaceBaselineWaiters.delete(waiter);
|
|
1097
|
+
return this.workspaces.length > 0;
|
|
1098
|
+
}
|
|
1099
|
+
/** True when the session has a live baseline (subscribed at least once). */
|
|
1100
|
+
hasBaseline(sessionId) {
|
|
1101
|
+
return this.buffers.has(sessionId) && this.bufferFor(sessionId).lastSeq >= 0;
|
|
1102
|
+
}
|
|
1103
|
+
isRunning(sessionId) {
|
|
1104
|
+
return this.running.get(sessionId) ?? false;
|
|
1105
|
+
}
|
|
1106
|
+
/** approval/requested | question/requested → registry (idempotent by rpcId). */
|
|
1107
|
+
registerPending(frame, rpcId) {
|
|
1108
|
+
const sessionId = typeof frame.sessionId === "string" ? frame.sessionId : "";
|
|
1109
|
+
if (!sessionId) {
|
|
1110
|
+
this.log("dsh-connection: answerable frame without sessionId — ignored", frame?.type);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (typeof rpcId !== "string" || rpcId.length === 0) {
|
|
1114
|
+
this.log("dsh-connection: answerable frame without rpcId — ignored", frame?.type);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const kind = frame.type === "approval/requested" ? "approval" : "question";
|
|
1118
|
+
this.pendingAnswers.set(rpcId, {
|
|
1119
|
+
rpcId,
|
|
1120
|
+
sessionId,
|
|
1121
|
+
kind,
|
|
1122
|
+
body: frame,
|
|
1123
|
+
receivedAt: Date.now(),
|
|
1124
|
+
deliveredGen: this.generation
|
|
1125
|
+
});
|
|
1126
|
+
this.log(`dsh-connection: pending ${kind} registered (${rpcId}) for ${sessionId}`);
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Withdraw one pending answer (0.1.2 $events settle/cancel): remove the
|
|
1130
|
+
* registry entry and record the outcome in the settlement ring. Unknown
|
|
1131
|
+
* ids settle nothing (answered before we subscribed — honest no-op).
|
|
1132
|
+
*/
|
|
1133
|
+
withdrawPending(eventId, outcome) {
|
|
1134
|
+
const pending = this.pendingAnswers.get(eventId);
|
|
1135
|
+
if (pending === void 0) {
|
|
1136
|
+
this.log(`dsh-connection: settlement with no matching pending (${eventId}) — ignored`);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
this.pendingAnswers.delete(eventId);
|
|
1140
|
+
this.settledAnswers.set(eventId, {
|
|
1141
|
+
rpcId: eventId,
|
|
1142
|
+
sessionId: pending.sessionId,
|
|
1143
|
+
kind: pending.kind,
|
|
1144
|
+
outcome: outcome ?? "cancelled",
|
|
1145
|
+
settledAt: Date.now()
|
|
1146
|
+
});
|
|
1147
|
+
this.log(`dsh-connection: ${pending.kind} settled (${eventId}) outcome=${outcome ?? "cancelled"}`);
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Schedule the still-pending replay reap for this generation (ready
|
|
1151
|
+
* frame just arrived). The host replays every still-pending waterfall
|
|
1152
|
+
* into the new generation, but the ready frame can hit the wire before
|
|
1153
|
+
* the replayed frames drain, so the check runs after a short grace —
|
|
1154
|
+
* entries an older generation registered and the replay did NOT
|
|
1155
|
+
* re-confirm finished while we were disconnected (answered, cancelled,
|
|
1156
|
+
* or aborted); keeping them pending resurrects zombie answer cards the
|
|
1157
|
+
* user can answer forever with no effect.
|
|
1158
|
+
*/
|
|
1159
|
+
armAnswerReap() {
|
|
1160
|
+
clearTimeout(this.answerReapTimer);
|
|
1161
|
+
this.answerReapTimer = setTimeout(() => this.reapUnconfirmedPending(), this.answerReapGraceMs);
|
|
1162
|
+
}
|
|
1163
|
+
/** Withdraw every pending answer not re-delivered by the current generation. */
|
|
1164
|
+
reapUnconfirmedPending() {
|
|
1165
|
+
for (const [eventId, pending] of [...this.pendingAnswers]) {
|
|
1166
|
+
if (pending.deliveredGen >= this.generation) continue;
|
|
1167
|
+
this.log(`dsh-connection: reap unconfirmed pending (${eventId}) from generation ${pending.deliveredGen} — not in generation ${this.generation} replay`);
|
|
1168
|
+
this.withdrawPending(eventId);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* After OUR answer was accepted the question is done — withdraw the
|
|
1173
|
+
* registry entry with the outcome we sent instead of waiting for the
|
|
1174
|
+
* host's settle broadcast, which races generation rebuilds and can be
|
|
1175
|
+
* lost entirely (the answered card would resurrect on the next page
|
|
1176
|
+
* load from a stale pendingAnswers).
|
|
1177
|
+
*/
|
|
1178
|
+
forgetAnswered(rpcId, value) {
|
|
1179
|
+
this.withdrawPending(rpcId, typeof value === "string" ? value : "answered");
|
|
1180
|
+
}
|
|
1181
|
+
/** approval/resolved | question/resolved → pending removed, settlement recorded. */
|
|
1182
|
+
registerSettlement(frame) {
|
|
1183
|
+
const sessionId = typeof frame.sessionId === "string" ? frame.sessionId : "";
|
|
1184
|
+
if (!sessionId) {
|
|
1185
|
+
this.log("dsh-connection: settlement frame without sessionId — ignored", frame?.type);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const kind = frame.type === "approval/resolved" ? "approval" : "question";
|
|
1189
|
+
const outcome = typeof frame.outcome === "string" ? frame.outcome : "unknown";
|
|
1190
|
+
let rpcId;
|
|
1191
|
+
if (kind === "approval" && typeof frame.approvalId === "string") rpcId = [...this.pendingAnswers.values()].find((p) => p.sessionId === sessionId && p.body.approvalId === frame.approvalId)?.rpcId;
|
|
1192
|
+
else if (kind === "question" && typeof frame.questionRpcId === "string") rpcId = frame.questionRpcId;
|
|
1193
|
+
if (rpcId) {
|
|
1194
|
+
this.pendingAnswers.delete(rpcId);
|
|
1195
|
+
this.settledAnswers.set(rpcId, {
|
|
1196
|
+
rpcId,
|
|
1197
|
+
sessionId,
|
|
1198
|
+
kind,
|
|
1199
|
+
outcome,
|
|
1200
|
+
settledAt: Date.now()
|
|
1201
|
+
});
|
|
1202
|
+
this.log(`dsh-connection: ${kind} settled (${rpcId}) outcome=${outcome}`);
|
|
1203
|
+
} else this.log(`dsh-connection: settlement with no matching pending (${frame?.type}) — ignored`);
|
|
1204
|
+
}
|
|
1205
|
+
/** Pending answers for a session (for the PollResponse extension). */
|
|
1206
|
+
pendingFor(sessionId) {
|
|
1207
|
+
return [...this.pendingAnswers.values()].filter((p) => p.sessionId === sessionId);
|
|
1208
|
+
}
|
|
1209
|
+
/** Recent settlements for a session (ring: delivered-then-pruned). */
|
|
1210
|
+
settlementsFor(sessionId) {
|
|
1211
|
+
return [...this.settledAnswers.values()].filter((s) => s.sessionId === sessionId);
|
|
1212
|
+
}
|
|
1213
|
+
/** Delivery hook: drop settlements the client has already seen (ring prune). */
|
|
1214
|
+
pruneSettlements(sessionId, seenRpcIds) {
|
|
1215
|
+
for (const id of seenRpcIds) {
|
|
1216
|
+
const s = this.settledAnswers.get(id);
|
|
1217
|
+
if (s && s.sessionId === sessionId) this.settledAnswers.delete(id);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
/** session.list → normalized rows (DsiSessionSummary + raw projections). */
|
|
1221
|
+
async listSessions() {
|
|
1222
|
+
return { items: (await this.rawListSessions()).map(normalizeSessionRow) };
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* The workspace-RPC OWNER for a session (2026-09-10): the host keys
|
|
1226
|
+
* workspaceFiles by the OWNING agent session and refuses sub-agent ids
|
|
1227
|
+
* (session/not-found / agent-busy — a sub-agent session is not an RPC
|
|
1228
|
+
* address). A sub-agent's workspace IS its root ancestor's, so walk the
|
|
1229
|
+
* parentSessionId chain (session.list) to the root; a root session
|
|
1230
|
+
* resolves to itself. Cycles guard themselves.
|
|
1231
|
+
*/
|
|
1232
|
+
async workspaceOwnerSessionId(sessionId) {
|
|
1233
|
+
const { items } = await this.listSessions();
|
|
1234
|
+
const parentOf = new Map(items.map((s) => [s.sessionId, s.parentSessionId ?? null]));
|
|
1235
|
+
let cursor = sessionId;
|
|
1236
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1237
|
+
while (!seen.has(cursor)) {
|
|
1238
|
+
seen.add(cursor);
|
|
1239
|
+
const parent = parentOf.get(cursor);
|
|
1240
|
+
if (!parent || parent === cursor) break;
|
|
1241
|
+
cursor = parent;
|
|
1242
|
+
}
|
|
1243
|
+
return cursor;
|
|
1244
|
+
}
|
|
1245
|
+
/** Raw session/list rows (0.1.2) — projections.asOfSeq drives page calls. */
|
|
1246
|
+
async rawListSessions() {
|
|
1247
|
+
const raw = await this.rpc(DSH_METHODS.list, DSH_ARGS.list());
|
|
1248
|
+
const rows = Array.isArray(raw?.items) ? raw.items : [];
|
|
1249
|
+
for (const row of rows) this.foldSubagentHint(row);
|
|
1250
|
+
return rows;
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Fold one list row's subagent identity into the hint cache. Only rows
|
|
1254
|
+
* the host stamps `origin:'subagent'` with a readable parent and a
|
|
1255
|
+
* descriptor lifecycle mode land here; a half-readable row keeps no hint
|
|
1256
|
+
* so the host's own rejection (surfaced honestly downstream) names the
|
|
1257
|
+
* truth instead of DSI guessing a mode.
|
|
1258
|
+
*/
|
|
1259
|
+
foldSubagentHint(row) {
|
|
1260
|
+
if (row.origin !== "subagent") return;
|
|
1261
|
+
if (typeof row.parentSessionId !== "string" || row.parentSessionId === "") return;
|
|
1262
|
+
const identity = row.projections?.values?.["subagent"];
|
|
1263
|
+
const mode = identity !== null && typeof identity === "object" ? identity.mode : void 0;
|
|
1264
|
+
if (mode !== "one-shot" && mode !== "continuable") return;
|
|
1265
|
+
this.subagentHints.set(row.sessionId, {
|
|
1266
|
+
parentSessionId: row.parentSessionId,
|
|
1267
|
+
mode
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* The session/page + session/follow address for one session id: the
|
|
1272
|
+
* parent-addressed subagent form when a hint is folded, else the plain
|
|
1273
|
+
* session form. Subagent child ids are bare UUIDs (the spawner mints
|
|
1274
|
+
* `SessionId(randomUUID())`) — the id's SHAPE is never a discriminator;
|
|
1275
|
+
* only the host row's origin is.
|
|
1276
|
+
*/
|
|
1277
|
+
addressFor(sessionId) {
|
|
1278
|
+
const hint = this.subagentHints.get(sessionId);
|
|
1279
|
+
if (hint === void 0) return {
|
|
1280
|
+
kind: "session",
|
|
1281
|
+
sessionId
|
|
1282
|
+
};
|
|
1283
|
+
return {
|
|
1284
|
+
kind: "subagent",
|
|
1285
|
+
parentSessionId: hint.parentSessionId,
|
|
1286
|
+
childSessionId: sessionId,
|
|
1287
|
+
mode: hint.mode
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* The tail-page through-cursor (0.1.2 session/page `throughSeq`). The
|
|
1292
|
+
* list projection's watermark (asOfSeq) is the LOWEST served-projection
|
|
1293
|
+
* row seq (session-projection-cache cachedSnapshot) and FREEZES while no
|
|
1294
|
+
* projection updates — mid-turn it sat at the last projection-relevant
|
|
1295
|
+
* event while the follow stream ran dozens of seqs ahead, so a page cut
|
|
1296
|
+
* there ended below the live truth, rewound the client's mark, and
|
|
1297
|
+
* pinned every later poll into a gap→resync loop. The follow buffer's
|
|
1298
|
+
* cursor IS the host's stream truth (seeded by the host's own snapshot
|
|
1299
|
+
* cursor, advanced only by host events), so a subscribed session pages
|
|
1300
|
+
* through it; the watermark stays the fallback for sessions without a
|
|
1301
|
+
* buffer (never subscribed — the pre-2026-09 behavior).
|
|
1302
|
+
*
|
|
1303
|
+
* A session the list does not name at all, and a listed row whose served
|
|
1304
|
+
* projection carries no cursor, both page with `throughSeq: -1` — the
|
|
1305
|
+
* 0.1.3 wire's unconstrained tail-page form (validated ≥ −1; the page is
|
|
1306
|
+
* bounded by its own cursor, never by ours). The HOST still owns every
|
|
1307
|
+
* refusal: a missing session rejects session/not-found before the cursor
|
|
1308
|
+
* is read (the cold load's 404 branch), never a client-invented error.
|
|
1309
|
+
*
|
|
1310
|
+
* 0.1.3-alpha.1: cold fork children serve `asOfSeq: -1` (the host's cold
|
|
1311
|
+
* blank-probe left with the v2 list projection) — a negative watermark is
|
|
1312
|
+
* "no served cursor", not "below the ledger head".
|
|
1313
|
+
*/
|
|
1314
|
+
async pageCursorFor(sessionId) {
|
|
1315
|
+
const live = this.bufferFor(sessionId).lastSeq;
|
|
1316
|
+
if (live >= 0) return live;
|
|
1317
|
+
const throughSeq = (await this.rawListSessions()).find((r) => r.sessionId === sessionId)?.projections?.asOfSeq;
|
|
1318
|
+
if (typeof throughSeq === "number" && throughSeq >= 0) return throughSeq;
|
|
1319
|
+
return -1;
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* session.history successor (0.1.2): the tail page via `session/page`.
|
|
1323
|
+
* throughSeq comes from pageCursorFor: the follow buffer's cursor when
|
|
1324
|
+
* the session has one, else the session/list projections (asOfSeq) of
|
|
1325
|
+
* the same session, else −1 — the 0.1.3 unconstrained tail-page form.
|
|
1326
|
+
*/
|
|
1327
|
+
async history(sessionId) {
|
|
1328
|
+
const throughSeq = await this.pageCursorFor(sessionId);
|
|
1329
|
+
return this.pageAsHistory(await this.rpc(DSH_METHODS.page, DSH_ARGS.page({
|
|
1330
|
+
address: this.addressFor(sessionId),
|
|
1331
|
+
throughSeq
|
|
1332
|
+
})));
|
|
1333
|
+
}
|
|
1334
|
+
/** Workspace registry adoption (2026-08-23, DSH parity): the Add
|
|
1335
|
+
* workspace button's host call. Raw view passthrough — DSI renders
|
|
1336
|
+
* only title/path; ordering and session membership stay host-owned. */
|
|
1337
|
+
async createWorkspace(path) {
|
|
1338
|
+
return await this.rpc(DSH_METHODS.workspaceCreate, DSH_ARGS.workspaceCreate({ path }));
|
|
1339
|
+
}
|
|
1340
|
+
/** Workspace rename (Chip Menu ADR D3): {request:{workspaceId,title}}
|
|
1341
|
+
* → the updated Workspace view. Refusals throw DshRpcError
|
|
1342
|
+
* (workspace/not-found, workspace/name-conflict). No cache mutation —
|
|
1343
|
+
* the workspace/follow upsert frame owns the registry (ADR D6). */
|
|
1344
|
+
async renameWorkspace(workspaceId, title) {
|
|
1345
|
+
return await this.rpc(DSH_METHODS.workspaceRename, DSH_ARGS.workspaceRename(workspaceId, title));
|
|
1346
|
+
}
|
|
1347
|
+
/** Workspace delete (Chip Menu ADR D3): {request:{workspaceId}} → the
|
|
1348
|
+
* removed id. The host removes ONLY the registry row (sessions survive,
|
|
1349
|
+
* ungrouped); the workspace/follow remove frame owns the registry
|
|
1350
|
+
* (ADR D6) — no local mutation here. */
|
|
1351
|
+
async deleteWorkspace(workspaceId) {
|
|
1352
|
+
return await this.rpc(DSH_METHODS.workspaceDelete, DSH_ARGS.workspaceDelete(workspaceId));
|
|
1353
|
+
}
|
|
1354
|
+
/** Goal Bar (2026-09-08, ADR amendment): one goal mutation through the
|
|
1355
|
+
* host's Goal Remotes — goals/pause | goals/resume | goals/clear |
|
|
1356
|
+
* goals/edit with the CAS ref {id, revision} (a stale revision loses
|
|
1357
|
+
* host-side). goals/edit additionally carries the changed-fields
|
|
1358
|
+
* request {objective?, maxGoalRounds?} (Goal Editor, 2026-09-09). The
|
|
1359
|
+
* raw RemoteResult passes through; the route maps refusals. */
|
|
1360
|
+
async goalVerb(verb, sessionId, ref, request) {
|
|
1361
|
+
return this.rpc(`goals/${verb}`, request === void 0 ? {
|
|
1362
|
+
agentId: sessionId,
|
|
1363
|
+
ref
|
|
1364
|
+
} : {
|
|
1365
|
+
agentId: sessionId,
|
|
1366
|
+
ref,
|
|
1367
|
+
request
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* workspaceFiles/read — one page of a UTF-8 text file inside the
|
|
1372
|
+
* session's workspace (Workspace Explorer W2 task 2.1). Flat args
|
|
1373
|
+
* {workspaceFileScopeId, path, range} per the descriptor — NO request
|
|
1374
|
+
* wrapper (scope wire renamed from agentId, host 2026-09-09).
|
|
1375
|
+
* Host refusals (not-found, outside-workspace, too-large, not-text,
|
|
1376
|
+
* not-regular-file) reject as DshRpcError; the route maps the codes.
|
|
1377
|
+
*
|
|
1378
|
+
* @param sessionId - owning session; the host confines the read to its
|
|
1379
|
+
* workspace root by containment, never prefix match.
|
|
1380
|
+
* @param path - workspace path, absolute or root-relative.
|
|
1381
|
+
* @param range - line window; omitted fields take the host's page defaults.
|
|
1382
|
+
*/
|
|
1383
|
+
async readWorkspaceFile(sessionId, path, range) {
|
|
1384
|
+
return await this.rpc(DSH_METHODS.workspaceFileRead, DSH_ARGS.workspaceFileRead(sessionId, path, range));
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* workspaceFiles/readBytes — one byte window of a workspace file, raw
|
|
1388
|
+
* bytes (base64 receipt), no text decoding and no binary refusal
|
|
1389
|
+
* (2026-09-10: binary previews such as images read through this; `read`
|
|
1390
|
+
* refuses them workspace-file/not-text).
|
|
1391
|
+
*
|
|
1392
|
+
* @param sessionId - owning session; the host confines the read to its
|
|
1393
|
+
workspace root by containment.
|
|
1394
|
+
* @param path - workspace path, absolute or root-relative.
|
|
1395
|
+
* @param range - byte window {offset, length}; omitted fields take the
|
|
1396
|
+
host's window defaults.
|
|
1397
|
+
*/
|
|
1398
|
+
async readWorkspaceFileBytes(sessionId, path, range) {
|
|
1399
|
+
return await this.rpc(DSH_METHODS.workspaceFileReadBytes, DSH_ARGS.workspaceFileReadBytes(sessionId, path, range));
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* workspaceFiles/list — one directory level inside the session's
|
|
1403
|
+
* workspace (Workspace Explorer bugfix 2026-09-09: the picker's
|
|
1404
|
+
* directoryPicker/list is native-only on real hosts — the LIVE tree
|
|
1405
|
+
* lists through this session-authorized namespace instead). Flat args
|
|
1406
|
+
* {workspaceFileScopeId, path}; path is workspace-relative, '' = the root.
|
|
1407
|
+
*/
|
|
1408
|
+
async listWorkspaceDirectory(sessionId, path) {
|
|
1409
|
+
return await this.rpc(DSH_METHODS.workspaceFileList, DSH_ARGS.workspaceFileList(sessionId, path));
|
|
1410
|
+
}
|
|
1411
|
+
/** The workspace registry (2026-08-23 registry parity) — the AUTHORITY
|
|
1412
|
+
* for DSI's workspace pills/chips: a deleted workspace disappears, and
|
|
1413
|
+
* sessions outside every registry entry render cue-less (DSH's
|
|
1414
|
+
* Ungrouped). Membership is sessionIds, not cwd matching. */
|
|
1415
|
+
async listWorkspaces() {
|
|
1416
|
+
if (!await this.awaitWorkspaceBaseline()) this.log("dsh-connection: workspace baseline did not land in time — honest empty");
|
|
1417
|
+
return { items: [...this.workspaces] };
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* host.listDirectory — one browsed directory level for the Add-workspace
|
|
1421
|
+
* folder picker (DSH parity: the directory-picker browse capability).
|
|
1422
|
+
* Absent path lists the host home directory. Conservative normalize:
|
|
1423
|
+
* garbage rows drop out, never throw, never invent data.
|
|
1424
|
+
*/
|
|
1425
|
+
async listDirectory(path) {
|
|
1426
|
+
const raw = await this.rpc(DSH_METHODS.listDirectory, DSH_ARGS.listDirectory(path));
|
|
1427
|
+
const entry = (v) => {
|
|
1428
|
+
if (v === null || typeof v !== "object") return null;
|
|
1429
|
+
const e = v;
|
|
1430
|
+
if (typeof e.name !== "string" || typeof e.path !== "string") return null;
|
|
1431
|
+
return {
|
|
1432
|
+
name: e.name,
|
|
1433
|
+
path: e.path,
|
|
1434
|
+
hidden: e.hidden === true
|
|
1435
|
+
};
|
|
1436
|
+
};
|
|
1437
|
+
const rows = (v) => Array.isArray(v) ? v.flatMap((x) => {
|
|
1438
|
+
const e = entry(x);
|
|
1439
|
+
return e ? [e] : [];
|
|
1440
|
+
}) : [];
|
|
1441
|
+
return {
|
|
1442
|
+
path: typeof raw?.path === "string" ? raw.path : "",
|
|
1443
|
+
home: typeof raw?.home === "string" ? raw.home : "",
|
|
1444
|
+
crumbs: rows(raw?.crumbs),
|
|
1445
|
+
entries: rows(raw?.entries),
|
|
1446
|
+
truncated: raw?.truncated === true
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* host.pickDirectory — the NATIVE interaction: the host opens one OS
|
|
1451
|
+
* chooser on its own display and resolves the picked absolute path
|
|
1452
|
+
* (null = operator cancelled). The signal follows the caller — a
|
|
1453
|
+
* disconnected browser closes the host's dialog mid-choice
|
|
1454
|
+
* (api-proxy pickDirectory's carrier contract).
|
|
1455
|
+
*/
|
|
1456
|
+
/**
|
|
1457
|
+
* Wave 3 (task 3.2) — session.attachment: the session-authorized durable
|
|
1458
|
+
* image read. Returns the ref plus canonical base64 data; host refusals
|
|
1459
|
+
* (unknown id, foreign session) reject as DshRpcError → 502 upstream.
|
|
1460
|
+
*
|
|
1461
|
+
* @param sessionId - owning session (authorization scope).
|
|
1462
|
+
* @param attachmentId - durable content-addressed id from an ImageBlock.
|
|
1463
|
+
*/
|
|
1464
|
+
async readAttachment(sessionId, attachmentId) {
|
|
1465
|
+
return await this.rpc(DSH_METHODS.attachment, DSH_ARGS.attachment({
|
|
1466
|
+
sessionId,
|
|
1467
|
+
attachmentId
|
|
1468
|
+
}));
|
|
1469
|
+
}
|
|
1470
|
+
async pickDirectory(signal) {
|
|
1471
|
+
const raw = await this.rpc(DSH_METHODS.pickDirectory, DSH_ARGS.pickDirectory(), signal);
|
|
1472
|
+
return { path: typeof raw?.path === "string" ? raw.path : null };
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* POC-2 W3 — session.history PAGE ending at beforeSeq−1 (ledger truth, BC-4).
|
|
1476
|
+
* Live-probed semantics (PRD §1.2): the page covers events with seq <
|
|
1477
|
+
* beforeSeq; hasMore=false at the ledger head. Drives load-older paging —
|
|
1478
|
+
* the ring buffer is freshness-only and never consulted here.
|
|
1479
|
+
*/
|
|
1480
|
+
async historyPage(sessionId, beforeSeq) {
|
|
1481
|
+
const throughSeq = await this.pageCursorFor(sessionId);
|
|
1482
|
+
return this.pageAsHistory(await this.rpc(DSH_METHODS.page, DSH_ARGS.page({
|
|
1483
|
+
address: this.addressFor(sessionId),
|
|
1484
|
+
throughSeq,
|
|
1485
|
+
beforeSeq
|
|
1486
|
+
})));
|
|
1487
|
+
}
|
|
1488
|
+
/** Adapt a 0.1.2 SessionPage {records,hasMore} to the DshHistoryPage shape. */
|
|
1489
|
+
pageAsHistory(raw) {
|
|
1490
|
+
const page = raw;
|
|
1491
|
+
return {
|
|
1492
|
+
events: expandRecords(Array.isArray(page?.records) ? page.records : []).map((event) => ({ event })),
|
|
1493
|
+
hasMore: page?.hasMore === true
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* POC-2 W3 — session.create {cwd?, agentPreset?} → {sessionId, …}.
|
|
1498
|
+
* Live-probed round-trip 2026-08-21 (probe7 → real session id).
|
|
1499
|
+
*/
|
|
1500
|
+
async createSession(cwd, agentPreset) {
|
|
1501
|
+
return await this.rpc(DSH_METHODS.create, DSH_ARGS.create({
|
|
1502
|
+
...cwd === void 0 ? {} : { cwd },
|
|
1503
|
+
...agentPreset === void 0 ? {} : { agentPreset }
|
|
1504
|
+
}));
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* session/fork {request:{sessionId,atSeq?}} → {sessionId} — the fork child
|
|
1508
|
+
* (The Fork Button ADR, 2026-09-01). An undefined atSeq is OMITTED from the
|
|
1509
|
+
* args, never sent as null: the host then cuts at the source's last
|
|
1510
|
+
* completed turn (host-enforced; see DSH_METHODS.fork for the pinned
|
|
1511
|
+
* contract and refusals).
|
|
1512
|
+
*/
|
|
1513
|
+
async forkSession(sessionId, atSeq) {
|
|
1514
|
+
return await this.rpc(DSH_METHODS.fork, DSH_ARGS.fork({
|
|
1515
|
+
sessionId,
|
|
1516
|
+
...atSeq === void 0 ? {} : { atSeq }
|
|
1517
|
+
}));
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* POC-2 W3 — agentPreset.list → {presets:[…]} (live-probed 2026-08-21,
|
|
1521
|
+
* rpc-map.ts line 54). Presets are the picker's only source of truth.
|
|
1522
|
+
* The shipped-preset English overlay honors ~/.dsi/settings.yaml
|
|
1523
|
+
* `dsh.presetEnglish` (default true; read live per call — no cache, so
|
|
1524
|
+
* an operator edit applies on the next fetch without a restart).
|
|
1525
|
+
*/
|
|
1526
|
+
async listPresets() {
|
|
1527
|
+
const overlayEnglish = readDshPresetEnglishConfig().presetEnglish;
|
|
1528
|
+
const raw = await this.rpc(DSH_METHODS.presets, DSH_ARGS.presets());
|
|
1529
|
+
return { presets: (Array.isArray(raw?.presets) ? raw.presets : []).map((row) => normalizePreset(row, overlayEnglish)) };
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* session.prompt — returns after the receipt only (BC-3 boundary lives
|
|
1533
|
+
* upstream). Wave 2: a string payload still maps to exactly one text
|
|
1534
|
+
* part (byte-identical for every pre-existing caller); a content array
|
|
1535
|
+
* passes through verbatim — the route owns validation and ordering
|
|
1536
|
+
* (buildPromptContent, images first, text last).
|
|
1537
|
+
*/
|
|
1538
|
+
async prompt(sessionId, payload, mode = "queue") {
|
|
1539
|
+
const content = typeof payload === "string" ? [{
|
|
1540
|
+
type: "text",
|
|
1541
|
+
text: payload
|
|
1542
|
+
}] : payload;
|
|
1543
|
+
return await this.rpc(DSH_METHODS.prompt, DSH_ARGS.prompt({
|
|
1544
|
+
requestId: this.rpcIdFactory(),
|
|
1545
|
+
sessionId,
|
|
1546
|
+
mode,
|
|
1547
|
+
content,
|
|
1548
|
+
clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
1549
|
+
}));
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* commands/execute (2026-08-25 fix) — the NATIVE slash-command surface the
|
|
1553
|
+
* host's own web GUI uses (Typert Gateway remote; session.prompt has no
|
|
1554
|
+
* slash interception — the 2026-08-25 RCA). Executes one line on the
|
|
1555
|
+
* session's agent; the host durably logs command/run + command/done (and
|
|
1556
|
+
* the permission command appends its knob events — the poll fold's
|
|
1557
|
+
* confirmation). Returns null on an admission miss (the line named no
|
|
1558
|
+
* registered command): ok:true, the command never ran — the honest
|
|
1559
|
+
* receipt callers must distinguish from a success.
|
|
1560
|
+
*/
|
|
1561
|
+
async executeCommand(sessionId, line) {
|
|
1562
|
+
return await this.rpc(DSH_METHODS.commandExecute, DSH_ARGS.commandExecute(sessionId, line, [])) ?? null;
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* commands/list (Slash Menu W1, task 1.2) — the host's command catalog,
|
|
1566
|
+
* name-sorted rows {name, description, input?{hint, images?}}. Live-pinned
|
|
1567
|
+
* 2026-08-30 (see tests/unit/dsh-rpc.test.ts): agentId IS the sessionId.
|
|
1568
|
+
* May resume a cold session on the host — callers fetch on first menu
|
|
1569
|
+
* open, never on panel mount (ADR §3.3).
|
|
1570
|
+
*/
|
|
1571
|
+
async listCommands(sessionId) {
|
|
1572
|
+
return await this.rpc(DSH_METHODS.commandsList, DSH_ARGS.commandsList(sessionId));
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* skills/list (Slash Menu W1, task 1.2) — the session's user-invocable
|
|
1576
|
+
* skill catalog {skills:[{name, description, whenToUse?, modelInvocable}]}.
|
|
1577
|
+
* Live-pinned 2026-08-30. Never activates a cold session — the menu may
|
|
1578
|
+
* read it freely on first open.
|
|
1579
|
+
*/
|
|
1580
|
+
async listSkills(sessionId) {
|
|
1581
|
+
return await this.rpc(DSH_METHODS.skillsList, DSH_ARGS.skillsList(sessionId));
|
|
1582
|
+
}
|
|
1583
|
+
/** session.cancel */
|
|
1584
|
+
async cancel(sessionId) {
|
|
1585
|
+
return await this.rpc(DSH_METHODS.cancel, DSH_ARGS.cancel({ sessionId }));
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* POC-3 W3 (3.1) — session.rename {sessionId, title}.
|
|
1589
|
+
*
|
|
1590
|
+
* Appends a session/title event with the `user` source — pins the title
|
|
1591
|
+
* against automatic regeneration (sessions.ts rename doc). The host
|
|
1592
|
+
* normalizes; a title that normalizes to empty fails `title-invalid` (an
|
|
1593
|
+
* RpcError → 502 upstream). Session-backed subagents reject `agent-busy`.
|
|
1594
|
+
*/
|
|
1595
|
+
async renameSession(sessionId, title) {
|
|
1596
|
+
return await this.rpc(DSH_METHODS.rename, DSH_ARGS.rename({
|
|
1597
|
+
sessionId,
|
|
1598
|
+
title
|
|
1599
|
+
}));
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* POC-3 W3 (3.2) — session.models {sessionId} → the model directory.
|
|
1603
|
+
*
|
|
1604
|
+
* Provider lookups run independently on the host (sessions.ts): `groups`
|
|
1605
|
+
* are successfully-loaded catalogs, `failures` are provider-local
|
|
1606
|
+
* diagnostics — successful groups remain usable. Garbage-safe normalize
|
|
1607
|
+
* below never throws (task 3.2-T: garbage catalog → empty, not an error).
|
|
1608
|
+
*/
|
|
1609
|
+
async listModels(_sessionId) {
|
|
1610
|
+
return normalizeModelDirectory(await this.rpc(DSH_METHODS.modelCatalog, DSH_ARGS.modelCatalog()));
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* POC-3 W3 (3.2) — session.selectModel {sessionId, provider, model,
|
|
1614
|
+
* reasoningEffort?} → {selected: ModelSelection}. Catalog membership is
|
|
1615
|
+
* advisory on the host — an exact-route selection still validates the
|
|
1616
|
+
* effort. Subagents reject `session/agent-busy` (RpcError → 502 upstream).
|
|
1617
|
+
*/
|
|
1618
|
+
async selectModel(sessionId, provider, model, reasoningEffort) {
|
|
1619
|
+
return normalizeModelSelection((await this.rpc(DSH_METHODS.selectModel, DSH_ARGS.selectModel({
|
|
1620
|
+
sessionId,
|
|
1621
|
+
provider,
|
|
1622
|
+
model,
|
|
1623
|
+
...reasoningEffort !== void 0 ? { reasoningEffort } : {}
|
|
1624
|
+
})))?.selected);
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* POC-3 W1 — answer a pending approval/question via the dedicated
|
|
1628
|
+
* client-response carrier (POST /api/respond). NOT a unary method: no
|
|
1629
|
+
* DSH_METHODS entry, no rpcId minted here — the rpcId is the REQUEST's
|
|
1630
|
+
* stable id (registry key), echoed in the envelope. Receipt is a union,
|
|
1631
|
+
* not ok/error: {accepted:false, reason} is a SUCCESSFUL exchange (BC-B
|
|
1632
|
+
* upstream maps it to 200 + settle-elsewhere). Transport failure still
|
|
1633
|
+
* throws (route maps to 503 — card stays answerable, lock released).
|
|
1634
|
+
*/
|
|
1635
|
+
/**
|
|
1636
|
+
* POC-3 W1 → 0.1.2: an answer is a `$events/result` unary RPC
|
|
1637
|
+
* {clientId, eventId, outcome} returning to the SAME generation whose
|
|
1638
|
+
* $events stream delivered the waterfall (rpcId = the eventId DSI
|
|
1639
|
+
* registered the card under). The outcome's `value` is the waterfall
|
|
1640
|
+
* listener's RETURN VALUE, and the host consumes it verbatim — a
|
|
1641
|
+
* question's value must BE the AskUserQuestionAnswer `{answers:[…]}`
|
|
1642
|
+
* (the live host crashes the tool on any other shape, losing the
|
|
1643
|
+
* answer); an approval's value is the outcome string. The panel's
|
|
1644
|
+
* legacy `{sessionId, answer:{answers}}` wrapper is unwrapped here so
|
|
1645
|
+
* the wire carries the host's contract, not the panel's convenience.
|
|
1646
|
+
*/
|
|
1647
|
+
async respond(rpcId, payload) {
|
|
1648
|
+
if (this.eventClientId === null) throw new Error("dsh-connection: no $events generation to answer through yet");
|
|
1649
|
+
const { sessionId, ...rest } = payload;
|
|
1650
|
+
const value = rest.outcome !== void 0 ? rest.outcome : rest.answer !== void 0 ? rest.answer : rest.answers !== void 0 ? { answers: rest.answers } : rest;
|
|
1651
|
+
try {
|
|
1652
|
+
await this.sendEventResult(rpcId, value);
|
|
1653
|
+
this.forgetAnswered(rpcId, value);
|
|
1654
|
+
} catch (err) {
|
|
1655
|
+
if (err instanceof DshRpcError && err.code === "not-pending") return {
|
|
1656
|
+
accepted: false,
|
|
1657
|
+
reason: "not-pending"
|
|
1658
|
+
};
|
|
1659
|
+
if (err instanceof DshRpcError && err.code === "gateway/internal" && err.message.includes("no active event stream")) {
|
|
1660
|
+
const previous = this.eventClientId;
|
|
1661
|
+
if (previous !== null && await this.refreshEventGeneration(previous)) {
|
|
1662
|
+
await this.sendEventResult(rpcId, value);
|
|
1663
|
+
this.forgetAnswered(rpcId, value);
|
|
1664
|
+
return { accepted: true };
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
throw err;
|
|
1668
|
+
}
|
|
1669
|
+
return { accepted: true };
|
|
1670
|
+
}
|
|
1671
|
+
/** One $events/result answer under the CURRENT generation's clientId. */
|
|
1672
|
+
sendEventResult(rpcId, value) {
|
|
1673
|
+
return this.rpc(REMOTE_EVENT_RESULT_ENDPOINT, {
|
|
1674
|
+
clientId: this.eventClientId,
|
|
1675
|
+
eventId: rpcId,
|
|
1676
|
+
outcome: {
|
|
1677
|
+
kind: "result",
|
|
1678
|
+
value
|
|
1679
|
+
}
|
|
1680
|
+
}).then(() => void 0);
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Rebuild the mux generation immediately (bypassing the reconnect
|
|
1684
|
+
* ladder) and wait until a NEW $events ready frame lands or the deadline
|
|
1685
|
+
* expires. Returns true only when a fresh clientId was observed — the
|
|
1686
|
+
* caller may retry an answer against it.
|
|
1687
|
+
*/
|
|
1688
|
+
async refreshEventGeneration(previousClientId) {
|
|
1689
|
+
this.log("dsh-connection: $events generation stale — rebuilding for the answer retry");
|
|
1690
|
+
if (this.reconnectTimer !== void 0) {
|
|
1691
|
+
clearTimeout(this.reconnectTimer);
|
|
1692
|
+
this.reconnectTimer = void 0;
|
|
1693
|
+
}
|
|
1694
|
+
this.teardownSockets();
|
|
1695
|
+
this.ensureDownlinks();
|
|
1696
|
+
const deadline = Date.now() + this.staleGenerationRetryMs;
|
|
1697
|
+
while (!this.disposed && this.eventClientId === previousClientId && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1698
|
+
return this.eventClientId !== previousClientId && this.eventClientId !== null;
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Resync: replace the ring buffer with the ledger's tail page (BC-4).
|
|
1702
|
+
* Returns the fresh entries so callers can hand them to the store.
|
|
1703
|
+
*/
|
|
1704
|
+
async resyncFromLedger(sessionId) {
|
|
1705
|
+
const events = ((await this.history(sessionId)).events ?? []).map((h) => h.event);
|
|
1706
|
+
const buf = this.bufferFor(sessionId);
|
|
1707
|
+
buf.entries = [...events].slice(-ringCapacity());
|
|
1708
|
+
buf.lastSeq = events.length > 0 ? events[events.length - 1].seq : -1;
|
|
1709
|
+
buf.coverageFloor = buf.entries[0]?.seq ?? buf.lastSeq;
|
|
1710
|
+
return buf.entries;
|
|
1711
|
+
}
|
|
1712
|
+
async rpc(method, payload, signal) {
|
|
1713
|
+
const body = JSON.stringify({
|
|
1714
|
+
type: "client-request",
|
|
1715
|
+
rpcId: this.rpcIdFactory(),
|
|
1716
|
+
method,
|
|
1717
|
+
payload: { args: payload }
|
|
1718
|
+
});
|
|
1719
|
+
const attempt = async () => {
|
|
1720
|
+
const cookie = await this.auth.ensureCookie();
|
|
1721
|
+
return this.fetchFn(rpcUrl(this.baseUrl, method), {
|
|
1722
|
+
method: "POST",
|
|
1723
|
+
headers: {
|
|
1724
|
+
"content-type": "application/json",
|
|
1725
|
+
...cookie === null ? {} : { cookie }
|
|
1726
|
+
},
|
|
1727
|
+
body,
|
|
1728
|
+
...signal !== void 0 ? { signal } : {}
|
|
1729
|
+
});
|
|
1730
|
+
};
|
|
1731
|
+
let response = await attempt();
|
|
1732
|
+
if (response.status === 401) {
|
|
1733
|
+
this.auth.invalidate();
|
|
1734
|
+
const { authToken } = readDshAuthConfig();
|
|
1735
|
+
response = await attempt();
|
|
1736
|
+
}
|
|
1737
|
+
if (!response.ok) throw new Error(`dsh-connection: transport failure for ${method}: HTTP ${response.status}`);
|
|
1738
|
+
const rpcId = JSON.parse(body).rpcId;
|
|
1739
|
+
return parseResponse(await response.text(), rpcId);
|
|
1740
|
+
}
|
|
1741
|
+
dispose() {
|
|
1742
|
+
this.disposed = true;
|
|
1743
|
+
clearTimeout(this.answerReapTimer);
|
|
1744
|
+
this.teardownSockets();
|
|
1745
|
+
this.pendingAnswers.clear();
|
|
1746
|
+
this.settledAnswers.clear();
|
|
1747
|
+
this.buffers.clear();
|
|
1748
|
+
this.liveTails.clear();
|
|
1749
|
+
this.running.clear();
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
/** The follow-<sessionId>-<streamSeq> logical stream id (one per generation). */
|
|
1753
|
+
function followStreamId(sessionId, streamSeq) {
|
|
1754
|
+
return `follow-${sessionId}-${String(streamSeq)}`;
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* Expand ONE follow/page event payload into its ring-buffer form
|
|
1758
|
+
* (single-element list — ledger v2 serves every record as a plain event).
|
|
1759
|
+
*/
|
|
1760
|
+
function expandEntry(e) {
|
|
1761
|
+
return [{
|
|
1762
|
+
type: e.type,
|
|
1763
|
+
seq: typeof e.seq === "number" ? e.seq : -1,
|
|
1764
|
+
time: typeof e.time === "number" ? e.time : 0,
|
|
1765
|
+
data: e.data
|
|
1766
|
+
}];
|
|
1767
|
+
}
|
|
1768
|
+
/** Expand one page/snapshot record ({type:'event', event}) into its member
|
|
1769
|
+
* events; a record without an event payload contributes nothing. */
|
|
1770
|
+
function expandRecord(record) {
|
|
1771
|
+
if (record === void 0 || record.event === void 0) return [];
|
|
1772
|
+
return expandEntry(record.event);
|
|
1773
|
+
}
|
|
1774
|
+
/** Expand a records array into a flat event list. */
|
|
1775
|
+
function expandRecords(records) {
|
|
1776
|
+
return records.flatMap((record) => expandRecord(record));
|
|
1777
|
+
}
|
|
1778
|
+
/** The last member time of one compact stream record, or undefined when
|
|
1779
|
+
* the record carries no readable time (malformed members stay silent —
|
|
1780
|
+
* the baseline is presentation seed data, never re-validated truth). */
|
|
1781
|
+
function compactRecordTailTime(record) {
|
|
1782
|
+
if (record.type === "chunk") return typeof record.time === "number" ? record.time : void 0;
|
|
1783
|
+
if (record.type === "text-chunks" || record.type === "reasoning-chunks" || record.type === "tool-call-chunks") {
|
|
1784
|
+
const time0 = typeof record.time0 === "number" ? record.time0 : void 0;
|
|
1785
|
+
const dt = Array.isArray(record.dt) ? record.dt : [];
|
|
1786
|
+
if (time0 === void 0 || dt.some((g) => typeof g !== "number")) return void 0;
|
|
1787
|
+
return dt.reduce((t, g) => t + g, time0);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
function normalizeModelSelection(v) {
|
|
1791
|
+
if (v === null || typeof v !== "object") return null;
|
|
1792
|
+
const sel = v;
|
|
1793
|
+
if (typeof sel.provider !== "string" || typeof sel.model !== "string") return null;
|
|
1794
|
+
return {
|
|
1795
|
+
provider: sel.provider,
|
|
1796
|
+
model: sel.model,
|
|
1797
|
+
...typeof sel.reasoningEffort === "string" ? { reasoningEffort: sel.reasoningEffort } : {}
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Conservative normalize of a SessionModels value: structural reads only,
|
|
1802
|
+
* never throws, never invents data (task 3.2-T: a garbage catalog → empty
|
|
1803
|
+
* groups + empty failures, honest `routable`/`current` — the selector can
|
|
1804
|
+
* still show the current selection and say nothing is listed). Reasoning
|
|
1805
|
+
* efforts are flattened to opaque ids (the adapter owns their meaning).
|
|
1806
|
+
*/
|
|
1807
|
+
function normalizeModelDirectory(raw) {
|
|
1808
|
+
const empty = {
|
|
1809
|
+
current: null,
|
|
1810
|
+
routable: false,
|
|
1811
|
+
groups: [],
|
|
1812
|
+
failures: []
|
|
1813
|
+
};
|
|
1814
|
+
if (raw === null || typeof raw !== "object") return empty;
|
|
1815
|
+
const dir = raw;
|
|
1816
|
+
const groups = Array.isArray(dir.groups) ? modelGroupsOf(dir.groups) : [];
|
|
1817
|
+
const failures = Array.isArray(dir.failures) ? modelFailuresOf(dir.failures) : [];
|
|
1818
|
+
if (dir.current === void 0 && dir.default !== void 0) return {
|
|
1819
|
+
current: normalizeModelSelection(dir.default),
|
|
1820
|
+
routable: Array.isArray(dir.routableProviders) ? dir.routableProviders.length > 0 || groups.length > 0 : true,
|
|
1821
|
+
groups,
|
|
1822
|
+
failures
|
|
1823
|
+
};
|
|
1824
|
+
return {
|
|
1825
|
+
current: normalizeModelSelection(dir.current),
|
|
1826
|
+
routable: dir.routable === true,
|
|
1827
|
+
groups,
|
|
1828
|
+
failures
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
/** ModelCatalog provider groups → DsiModelGroup rows (conservative). */
|
|
1832
|
+
function modelGroupsOf(groups) {
|
|
1833
|
+
return groups.flatMap((g) => {
|
|
1834
|
+
if (g === null || typeof g !== "object") return [];
|
|
1835
|
+
const grp = g;
|
|
1836
|
+
if (typeof grp.id !== "string" || grp.id.length === 0) return [];
|
|
1837
|
+
const models = Array.isArray(grp.models) ? grp.models.flatMap((m) => {
|
|
1838
|
+
if (m === null || typeof m !== "object") return [];
|
|
1839
|
+
const mod = m;
|
|
1840
|
+
if (typeof mod.id !== "string" || mod.id.length === 0) return [];
|
|
1841
|
+
return [{
|
|
1842
|
+
id: mod.id,
|
|
1843
|
+
name: typeof mod.name === "string" ? mod.name : null,
|
|
1844
|
+
...Array.isArray(mod.reasoning?.efforts) ? { reasoningEfforts: mod.reasoning.efforts.flatMap((e) => typeof e?.id === "string" ? [e.id] : []) } : {}
|
|
1845
|
+
}];
|
|
1846
|
+
}) : [];
|
|
1847
|
+
return [{
|
|
1848
|
+
id: grp.id,
|
|
1849
|
+
name: typeof grp.name === "string" ? grp.name : null,
|
|
1850
|
+
models
|
|
1851
|
+
}];
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
/** ModelCatalog provider failures → diagnostic rows (conservative). */
|
|
1855
|
+
function modelFailuresOf(failures) {
|
|
1856
|
+
return failures.flatMap((f) => {
|
|
1857
|
+
if (f === null || typeof f !== "object") return [];
|
|
1858
|
+
const fail = f;
|
|
1859
|
+
if (typeof fail.id !== "string" || fail.id.length === 0) return [];
|
|
1860
|
+
return [{
|
|
1861
|
+
id: fail.id,
|
|
1862
|
+
name: typeof fail.name === "string" ? fail.name : null,
|
|
1863
|
+
message: typeof fail.message === "string" ? fail.message : null
|
|
1864
|
+
}];
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
function normalizeSessionRow(row) {
|
|
1868
|
+
return {
|
|
1869
|
+
sessionId: row.sessionId,
|
|
1870
|
+
title: row.origin === "subagent" ? readSubagentLabel(row) ?? readTitle(row) : readTitle(row),
|
|
1871
|
+
agentPreset: readAgentPreset(row) ?? row.agentPreset ?? null,
|
|
1872
|
+
running: Boolean(row.running),
|
|
1873
|
+
blank: Boolean(row.blank),
|
|
1874
|
+
updatedAt: row.updatedAt ?? 0,
|
|
1875
|
+
workspace: typeof row.cwd === "string" && row.cwd.length > 0 ? row.cwd : null,
|
|
1876
|
+
turns: readTurns(row),
|
|
1877
|
+
parentSessionId: row.parentSessionId ?? null,
|
|
1878
|
+
origin: row.origin ?? null
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
function readTitle(row) {
|
|
1882
|
+
const title = row.projections?.values?.["title"];
|
|
1883
|
+
return typeof title === "string" && title.length > 0 ? title : null;
|
|
1884
|
+
}
|
|
1885
|
+
/** projections.values.subagent.label (the child descriptor's creation label;
|
|
1886
|
+
* one-shot children may omit it — structural read, null when unreadable). */
|
|
1887
|
+
function readSubagentLabel(row) {
|
|
1888
|
+
const identity = row.projections?.values?.["subagent"];
|
|
1889
|
+
const label = identity !== null && typeof identity === "object" ? identity.label : void 0;
|
|
1890
|
+
return typeof label === "string" && label.length > 0 ? label : null;
|
|
1891
|
+
}
|
|
1892
|
+
/** projections.values.agentPreset (0.1.2 home of the preset id on list rows). */
|
|
1893
|
+
function readAgentPreset(row) {
|
|
1894
|
+
const preset = row.projections?.values?.["agentPreset"];
|
|
1895
|
+
return typeof preset === "string" && preset.length > 0 ? preset : null;
|
|
1896
|
+
}
|
|
1897
|
+
/** `sessionStats.turns` (0.1.2: projections.values.sessionStats) — integer-strict, null on absent/junk. */
|
|
1898
|
+
function readTurns(row) {
|
|
1899
|
+
const stats = row.projections?.values?.["sessionStats"];
|
|
1900
|
+
const turns = stats !== null && typeof stats === "object" ? stats.turns : row.sessionStats?.turns;
|
|
1901
|
+
if (typeof turns !== "number" || !Number.isInteger(turns) || turns < 0) return null;
|
|
1902
|
+
return turns;
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* English display copy for the host's SHIPPED presets, keyed by the stable
|
|
1906
|
+
* preset id. The wire's `name`/`description` are the UNLOCALIZED metadata a
|
|
1907
|
+
* preset publishes (DSH `PresetDisplaySource`), and the four built-ins ship
|
|
1908
|
+
* Simplified Chinese there — DSH localizes them only inside its own web UI
|
|
1909
|
+
* (`packages/preset/agent-presets/src/display.ts` folds trust:'system' + id →
|
|
1910
|
+
* locale dictionary keys). This app has no host locale seat, so the overlay
|
|
1911
|
+
* mirrors that fold in English: trust:'system' + a known id → the English
|
|
1912
|
+
* copy; every other row (user presets, unknown future ids) keeps the host's
|
|
1913
|
+
* name verbatim — user metadata is never translated, and an unknown id falls
|
|
1914
|
+
* back honestly. Operator-tunable off via ~/.dsi/settings.yaml
|
|
1915
|
+
* `dsh.presetEnglish: false` (readDshPresetEnglishConfig — no cache, so the
|
|
1916
|
+
* next listPresets fetch picks an edit up). Keep the strings in step with DSH
|
|
1917
|
+
* `packages/client/ui-agent-preset/src/client/locales.ts` (the `en` bundle).
|
|
1918
|
+
*/
|
|
1919
|
+
var SHIPPED_PRESET_ENGLISH = {
|
|
1920
|
+
standard: {
|
|
1921
|
+
name: "Standard mode",
|
|
1922
|
+
description: "Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows."
|
|
1923
|
+
},
|
|
1924
|
+
ptc: {
|
|
1925
|
+
name: "PTC mode",
|
|
1926
|
+
description: "Full coding agent without the workflow tool; other tools are exposed through the PTC mode SDK so the model can combine multi-step operations in one TypeScript program."
|
|
1927
|
+
},
|
|
1928
|
+
minimal: {
|
|
1929
|
+
name: "Minimal mode",
|
|
1930
|
+
description: "Two-tool coding agent with persistent bash and str_replace_editor."
|
|
1931
|
+
},
|
|
1932
|
+
cordis: {
|
|
1933
|
+
name: "Creator mode",
|
|
1934
|
+
description: "Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance."
|
|
1935
|
+
}
|
|
1936
|
+
};
|
|
1937
|
+
/**
|
|
1938
|
+
* Conservative normalize: unknown shapes keep their id but never invent data.
|
|
1939
|
+
* The one English overlay seat (SHIPPED_PRESET_ENGLISH) applies before the
|
|
1940
|
+
* host fallbacks — a shipped preset's label is locale-resolved here, never
|
|
1941
|
+
* the wire's unlocalized text. Operator-tunable via
|
|
1942
|
+
* ~/.dsi/settings.yaml `dsh.presetEnglish` (read live per listPresets
|
|
1943
|
+
* call, passed through here): false hands the wire name through untouched.
|
|
1944
|
+
*/
|
|
1945
|
+
function normalizePreset(row, overlayEnglish = true) {
|
|
1946
|
+
const id = typeof row.id === "string" ? row.id : String(row.id ?? "");
|
|
1947
|
+
const shipped = overlayEnglish && row.trust === "system" ? SHIPPED_PRESET_ENGLISH[id] : void 0;
|
|
1948
|
+
return {
|
|
1949
|
+
id,
|
|
1950
|
+
name: shipped?.name ?? (typeof row.name === "string" ? row.name : null),
|
|
1951
|
+
description: shipped?.description ?? (typeof row.description === "string" ? row.description : null),
|
|
1952
|
+
isDefault: row.isDefault === true
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
export { DshRpcError as D, isPromptImageMediaType as a, buildPromptContent as b, getDshConnection as g, isSubagentRejection as i, mapRpcFailure as m, statusFor as s };
|
|
1957
|
+
//# sourceMappingURL=dsh-connection.js-DwihVtk_.js.map
|