@qwen-code/qwen-code 0.22.2-preview.0 → 0.22.2
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/bundled/goal-draft/SKILL.md +109 -0
- package/bundled/qc-helper/docs/configuration/settings.md +12 -12
- package/bundled/qc-helper/docs/features/_meta.ts +1 -0
- package/bundled/qc-helper/docs/features/commands.md +2 -1
- package/bundled/qc-helper/docs/features/goals.md +58 -0
- package/bundled/qc-helper/docs/features/headless.md +13 -11
- package/bundled/qc-helper/docs/features/status-line.md +3 -3
- package/chunks/MaxSizedBox-X5FZSNAJ.js +112 -0
- package/chunks/{StandaloneSessionPicker-KFMJILVY.js → StandaloneSessionPicker-AVLI6VJP.js} +76 -76
- package/chunks/{acpAgent-KNFTTDWL.js → acpAgent-MXEAN54C.js} +994 -277
- package/chunks/{agent-6V6MZOA7.js → agent-PJIPDWHB.js} +41 -41
- package/chunks/agent-headless-B4JVD7UK.js +87 -0
- package/chunks/{anthropicContentGenerator-U76GTVB6.js → anthropicContentGenerator-HGMBACVE.js} +7 -7
- package/chunks/{artifact-tool-XDJCZZCF.js → artifact-tool-5YT4QF7Y.js} +2 -2
- package/chunks/{askUserQuestion-ZXXERLEV.js → askUserQuestion-4SWJY65V.js} +2 -2
- package/chunks/bridge-FKZTVIS3.js +121 -0
- package/chunks/{channel-management-service-HXXS3HOZ.js → channel-management-service-X4AZ6IXX.js} +1 -1
- package/chunks/channel-settings-store-4UFCLBBG.js +117 -0
- package/chunks/{channel-worker-group-EVDC2P4K.js → channel-worker-group-27HMGD5S.js} +7 -6
- package/chunks/{channel-worker-manager-IJJZHGEI.js → channel-worker-manager-QEGX646Z.js} +9 -7
- package/chunks/{channel-worker-supervisor-YSKB5GVB.js → channel-worker-supervisor-UEW2WWCC.js} +5 -4
- package/chunks/{chunk-7QGEJI7M.js → chunk-23RFD54N.js} +1 -1
- package/chunks/{chunk-K5PNJ6VL.js → chunk-2K6ORIHJ.js} +13 -13
- package/chunks/{chunk-OGZAQWJP.js → chunk-2NYCN6F2.js} +11 -11
- package/chunks/chunk-2TNL5KPM.js +49 -0
- package/chunks/{chunk-ZEADBZLO.js → chunk-3AFMQUTI.js} +3 -3
- package/chunks/{chunk-LRLMDV5X.js → chunk-3DXFGHAJ.js} +185 -174
- package/chunks/{chunk-73WFGVIZ.js → chunk-3FDFA4MZ.js} +3 -3
- package/chunks/{chunk-WBT3STGU.js → chunk-3I6UTTDX.js} +1 -1
- package/chunks/{chunk-FVDQ5E67.js → chunk-3JGZSIDA.js} +3 -3
- package/chunks/{chunk-JJJD6QCC.js → chunk-3SGB2UHX.js} +15 -10
- package/chunks/{chunk-77TOMRBG.js → chunk-43GGFFLY.js} +3 -3
- package/chunks/{chunk-ZJG323IA.js → chunk-43UYNAWX.js} +6 -4
- package/chunks/{chunk-T2HMXHYA.js → chunk-4CDSK5AZ.js} +6 -6
- package/chunks/chunk-4CMJKUND.js +95 -0
- package/chunks/{chunk-EB4VDXNI.js → chunk-4FTKQNWJ.js} +1 -1
- package/chunks/{chunk-PAJGKZAA.js → chunk-4HV6HH33.js} +10 -8
- package/chunks/{chunk-BQBUZDDH.js → chunk-4J657AJR.js} +3 -3
- package/chunks/{chunk-BU7OUDQB.js → chunk-4JWNNLPT.js} +4 -4
- package/chunks/{chunk-4N6UAPD3.js → chunk-4MQEPP3Y.js} +9 -84
- package/chunks/{chunk-I6HWGOWD.js → chunk-5O72BNDD.js} +93 -14
- package/chunks/{chunk-IKN3IUJP.js → chunk-5XBFCMWD.js} +2 -2
- package/chunks/{chunk-2YDYDDE2.js → chunk-66KDY3LV.js} +3 -3
- package/chunks/{chunk-UVDOONRU.js → chunk-6CGL7E7G.js} +2 -2
- package/chunks/{chunk-SLEWBQWM.js → chunk-6DIGWMGT.js} +2 -2
- package/chunks/{chunk-SKNVVYIJ.js → chunk-6IUNAPLR.js} +3 -3
- package/chunks/{chunk-VUZY6MEQ.js → chunk-6LFJEACX.js} +4 -4
- package/chunks/{chunk-H2MDBYLL.js → chunk-6LFTG244.js} +8 -8
- package/chunks/{chunk-PVBT5QPD.js → chunk-6PJOTWAN.js} +1 -1
- package/chunks/{chunk-4IDJQOCO.js → chunk-77RG5SFQ.js} +1 -1
- package/chunks/{chunk-SDVF6A42.js → chunk-7DJCPZE3.js} +3 -3
- package/chunks/{chunk-ELY4QVIB.js → chunk-7EQQR46F.js} +1 -1
- package/chunks/{chunk-H4JK2A2V.js → chunk-7UYV53BC.js} +1 -1
- package/chunks/{chunk-6HOU52QX.js → chunk-A7YP63XN.js} +4 -4
- package/chunks/{chunk-E4I5MVBR.js → chunk-ABS25QER.js} +1 -1
- package/chunks/{chunk-XO4NGYRP.js → chunk-AGR5UROZ.js} +5 -5
- package/chunks/{chunk-5T4LWWNX.js → chunk-B7CDU2SL.js} +2 -2
- package/chunks/{chunk-GQAIZL3H.js → chunk-BAJKAAOY.js} +68 -64
- package/chunks/{chunk-DWRFOH3G.js → chunk-BBWV7ONL.js} +3 -3
- package/chunks/{chunk-4TPAG3O7.js → chunk-BFPESSTR.js} +1 -1
- package/chunks/{chunk-RUJTCU46.js → chunk-BNMF3K5X.js} +20 -19
- package/chunks/{chunk-IPQA4SR5.js → chunk-BWORX6FA.js} +1 -1
- package/chunks/{chunk-BKDAF7ES.js → chunk-BXDAPVQU.js} +2 -2
- package/chunks/{chunk-FY62MBPS.js → chunk-C6K43ZDE.js} +4 -4
- package/chunks/{chunk-JUIJ3FXC.js → chunk-CAJTKR6W.js} +5 -5
- package/chunks/{chunk-EADEZDPO.js → chunk-CCBZDVUA.js} +30 -25
- package/chunks/{chunk-DP7LABIB.js → chunk-CMHFCLBU.js} +1 -1
- package/chunks/{chunk-5AFPOEY3.js → chunk-CPGJJJLU.js} +1 -1
- package/chunks/{chunk-LRKMQPMD.js → chunk-CQH5KTKC.js} +1 -1
- package/chunks/{chunk-EWBY3PFB.js → chunk-CSMI4FP5.js} +1 -1
- package/chunks/{chunk-JECO75XD.js → chunk-D5LUXLUH.js} +2 -2
- package/chunks/{chunk-TQYZG5DU.js → chunk-DZWPESIF.js} +3 -3
- package/chunks/{chunk-5Y2JCJAH.js → chunk-EFUM7RVY.js} +15 -2
- package/chunks/{chunk-3UZCLCXR.js → chunk-EKSCLBBF.js} +3 -3
- package/chunks/{chunk-LJP2BBMK.js → chunk-EOGELB3H.js} +4 -4
- package/chunks/{chunk-624NHDGB.js → chunk-ERFDKH32.js} +11 -0
- package/chunks/{chunk-RQAMNZ4Y.js → chunk-F5SZP77N.js} +310 -78
- package/chunks/{chunk-SQPOJ4ZI.js → chunk-F6FV3C5K.js} +12 -7
- package/chunks/{chunk-C2BAEV64.js → chunk-F7TNAPZ4.js} +9 -7
- package/chunks/{chunk-OAAPGZVH.js → chunk-FBSSIAAQ.js} +6 -6
- package/chunks/{chunk-2FN2HNTI.js → chunk-FDY5RMGH.js} +2 -2
- package/chunks/{chunk-U66FXEH2.js → chunk-FY76G3G5.js} +5 -5
- package/chunks/{chunk-LKOSBI2M.js → chunk-G34IR3D6.js} +9 -9
- package/chunks/{chunk-ZWKGZB2K.js → chunk-H2WDGQ6D.js} +3 -3
- package/chunks/chunk-H72BQNM3.js +645 -0
- package/chunks/{chunk-ILPXSKGQ.js → chunk-HVEYF6VT.js} +2 -2
- package/chunks/{chunk-EAV3TENS.js → chunk-HVQ3B7FF.js} +2 -2
- package/chunks/{chunk-MI7CMXPU.js → chunk-IJKTMLBE.js} +1 -1
- package/chunks/{chunk-IN66KO3N.js → chunk-IK5BW3CS.js} +2 -2
- package/chunks/{chunk-5NV2AN56.js → chunk-IOPCFOTF.js} +3 -3
- package/chunks/{chunk-O3BUXIPM.js → chunk-IZIVM7LZ.js} +3 -3
- package/chunks/{chunk-I6VVOZDK.js → chunk-J2OSJFP3.js} +2 -2
- package/chunks/{chunk-AYVIGUUK.js → chunk-JB4JIVTJ.js} +1 -1
- package/chunks/{chunk-76WGY6LW.js → chunk-JCQD3INC.js} +3 -3
- package/chunks/{chunk-CITGRGCM.js → chunk-JHS74YAB.js} +4 -1
- package/chunks/{chunk-GXWY24QS.js → chunk-JSC3H3TD.js} +33 -10
- package/chunks/{chunk-NUHCXGAD.js → chunk-JSHHYW6T.js} +4 -4
- package/chunks/{chunk-5CW26JIE.js → chunk-JU4EEXC7.js} +2 -2
- package/chunks/{chunk-T6OODF2U.js → chunk-JWALNCLT.js} +1 -1
- package/chunks/{chunk-DX5FZGHZ.js → chunk-K2NQPH5D.js} +3 -3
- package/chunks/{chunk-VLY6XOBY.js → chunk-KEKZFOL6.js} +1 -1
- package/chunks/{chunk-RUQBJS34.js → chunk-KUX5D6DK.js} +1200 -339
- package/chunks/{chunk-UX3IFLJM.js → chunk-L2M46FP5.js} +149 -22
- package/chunks/{chunk-6US7BF3G.js → chunk-L6BZRIUL.js} +2 -2
- package/chunks/{chunk-BGWT7VBV.js → chunk-L6YT5H34.js} +4 -4
- package/chunks/{chunk-W2WRNJKX.js → chunk-LDP5OD6N.js} +5 -5
- package/chunks/{chunk-QB5DD75E.js → chunk-LXIBMVRN.js} +7 -7
- package/chunks/{chunk-GNMJNDZE.js → chunk-M6FOJXW3.js} +1 -1
- package/chunks/{chunk-OX44MOAN.js → chunk-MIIVI25Q.js} +3 -3
- package/chunks/{chunk-K3LPAED5.js → chunk-MQM5PJML.js} +24 -4
- package/chunks/{chunk-27ONXAFY.js → chunk-NIFYWDYN.js} +1 -1
- package/chunks/{chunk-IJVDHPKD.js → chunk-NU7RKEZR.js} +2 -2
- package/chunks/{chunk-MB4OCRQ2.js → chunk-O2UN57NX.js} +2 -2
- package/chunks/{chunk-TWUA5EIL.js → chunk-OIP7C4FE.js} +5 -5
- package/chunks/{chunk-UCSV2IZW.js → chunk-OIVXBW3W.js} +3 -3
- package/chunks/{chunk-TOF4UZHH.js → chunk-P2SU6ZTI.js} +3 -3
- package/chunks/{chunk-N4B7VPXK.js → chunk-P5GGM4KS.js} +5 -5
- package/chunks/{chunk-ZKAZP2ZG.js → chunk-PDMJ3KGS.js} +2 -2
- package/chunks/{chunk-RRKNX553.js → chunk-PDQGMSZK.js} +1 -1
- package/chunks/{chunk-3UXJFOB3.js → chunk-PFCQ62V4.js} +5 -5
- package/chunks/{chunk-JLGFGPZJ.js → chunk-PKAYJDB3.js} +2 -2
- package/chunks/{chunk-GTUKW3I6.js → chunk-PPKZ7JOE.js} +1 -1
- package/chunks/{chunk-HOANA6GC.js → chunk-Q26AYA62.js} +4 -4
- package/chunks/{chunk-WEGXPP5E.js → chunk-QFCDF5PL.js} +1 -1
- package/chunks/{chunk-4PLAO7FS.js → chunk-QP4C6WCE.js} +3 -3
- package/chunks/{chunk-3B5DCAQU.js → chunk-R4PMVSR5.js} +27 -25
- package/chunks/{chunk-7JJUFI36.js → chunk-RIEFGNTP.js} +579 -3419
- package/chunks/{chunk-7XKW4CVK.js → chunk-RJC5EIKY.js} +4 -4
- package/chunks/chunk-RKFGE7JH.js +93 -0
- package/chunks/{chunk-CPPYZHBN.js → chunk-RU7BK6B3.js} +1 -1
- package/chunks/{chunk-GZJUCMH4.js → chunk-RUGNCYNO.js} +11 -633
- package/chunks/{chunk-ZLTEQYNC.js → chunk-SFPGAQUL.js} +2 -2
- package/chunks/{chunk-K6FURWIX.js → chunk-SJ3ZUWFK.js} +26 -4
- package/chunks/{chunk-DEYPMJPD.js → chunk-SMPR7SPO.js} +1 -1
- package/chunks/{chunk-7CWEZQ6T.js → chunk-STUJ7IMZ.js} +0 -77
- package/chunks/{chunk-O5YGAEQS.js → chunk-T32W2JHI.js} +6 -6
- package/chunks/chunk-T3WXYEZ4.js +304 -0
- package/chunks/{chunk-D6XO27Z5.js → chunk-TGWAQ5RB.js} +750 -296
- package/chunks/{chunk-EC5BONC3.js → chunk-TPI7O7HR.js} +1 -1
- package/chunks/{chunk-OI2DR6QB.js → chunk-TRZTNIQ6.js} +2 -2
- package/chunks/{chunk-T2CRBIDJ.js → chunk-TTX2JUE6.js} +1 -1
- package/chunks/{chunk-2NNTE2ED.js → chunk-UHQFIS7N.js} +15 -0
- package/chunks/{chunk-KOJ6MHD5.js → chunk-UTLCH2FK.js} +1 -1
- package/chunks/{chunk-7DPKPBOQ.js → chunk-V2NDP3I4.js} +2 -2
- package/chunks/{chunk-RQ5ASDFQ.js → chunk-V4QXXQJ2.js} +3 -3
- package/chunks/{chunk-BVE5EMIG.js → chunk-V5J4J5TP.js} +2 -2
- package/chunks/{chunk-X2UOOG5V.js → chunk-VQUGEVGR.js} +1 -1
- package/chunks/{chunk-XJS54K4A.js → chunk-VTHREBQM.js} +2 -2
- package/chunks/{chunk-V3GINY6K.js → chunk-VVW4ZNFY.js} +3 -3
- package/chunks/{chunk-YT5XN3HV.js → chunk-W5MJJ2V7.js} +1 -1
- package/chunks/{chunk-FPH6TXD3.js → chunk-WAOIHTJS.js} +1 -1
- package/chunks/{chunk-FN5C6C7G.js → chunk-WKK5BQNP.js} +151 -60
- package/chunks/chunk-XDFHY4R3.js +1584 -0
- package/chunks/{chunk-L436XFLJ.js → chunk-XDLEZ5HX.js} +1 -1
- package/chunks/{chunk-VL4YX3FH.js → chunk-XF63PKEN.js} +1 -1
- package/chunks/{chunk-3FSBMAYD.js → chunk-XQFT3QUF.js} +1 -1
- package/chunks/{chunk-2MHGD7EE.js → chunk-XRSOI3DL.js} +1 -1
- package/chunks/{chunk-FJWICLG7.js → chunk-Y3QL45LS.js} +1 -1
- package/chunks/{chunk-RCDXSEJ5.js → chunk-Y6LSMB6J.js} +1 -1
- package/chunks/{chunk-2SERYG4T.js → chunk-YTDVNSUQ.js} +1 -1
- package/chunks/{chunk-4X5GYZFO.js → chunk-Z7FLBQPR.js} +7 -7
- package/chunks/{chunk-3BV7TZNQ.js → chunk-Z7TGYN5F.js} +3 -3
- package/chunks/{chunk-4Z3PSHN4.js → chunk-ZFKOCECU.js} +2 -2
- package/chunks/chunk-ZGXYO7DY.js +3416 -0
- package/chunks/{chunk-E6FGMDFQ.js → chunk-ZU4UDIWX.js} +19 -1
- package/chunks/config-utils-BBOCOGZW.js +113 -0
- package/chunks/contextCommand-EET67MQ2.js +108 -0
- package/chunks/{core-runtime-6TBMEULI.js → core-runtime-677KFX6Q.js} +53 -53
- package/chunks/{create-sub-session-RVHSWVAP.js → create-sub-session-23ITIELI.js} +3 -3
- package/chunks/{create-sub-session-SAOMTP62.js → create-sub-session-JKZAYZX7.js} +160 -79
- package/chunks/{cron-create-IJGVR5R2.js → cron-create-KJ4IY6PP.js} +57 -10
- package/chunks/{cron-delete-4N5BI6GL.js → cron-delete-MBLQCAYY.js} +4 -4
- package/chunks/{cron-list-2CATIPUX.js → cron-list-DWH2PQ47.js} +4 -4
- package/chunks/{daemon-NAYK4XXU.js → daemon-MJQUO7RA.js} +28 -1
- package/chunks/{daemon-git-worktree-guard-TYLESM6G.js → daemon-git-worktree-guard-6MVAAZZZ.js} +53 -53
- package/chunks/daemon-status-provider-RFKZOK3S.js +119 -0
- package/chunks/daemon-trust-policy-DRZPFB4Q.js +114 -0
- package/chunks/{daemon-trust-policy-monitor-WWX7I7P6.js → daemon-trust-policy-monitor-B5U6TQX5.js} +59 -59
- package/chunks/deferred-core-runtime-BBVIOP24.js +116 -0
- package/chunks/{display-image-DR55LL6M.js → display-image-BE7I6ZB6.js} +5 -5
- package/chunks/{dist-FVJZSIO4.js → dist-ILDBUWDB.js} +71 -19
- package/chunks/{dist-6H6RZ5H6.js → dist-QODVOVWI.js} +58 -35
- package/chunks/earlyInputCapture-2JOMMNQ4.js +109 -0
- package/chunks/{edit-GBDKI5DZ.js → edit-BSV6YRVW.js} +42 -42
- package/chunks/{en-6MDBWMTG.js → en-YLXBXM4Z.js} +2 -1
- package/chunks/{enter-worktree-UOZ6ZTP7.js → enter-worktree-MQXFFPKM.js} +7 -7
- package/chunks/{enterPlanMode-2KTIPPM3.js → enterPlanMode-PKNF35FE.js} +41 -41
- package/chunks/{environment-XJ75LJ6N.js → environment-MOZMPUHR.js} +55 -55
- package/chunks/errors-DMPQVYJL.js +115 -0
- package/chunks/{exit-worktree-G2QATUCV.js → exit-worktree-L344UY7K.js} +7 -7
- package/chunks/{exitPlanMode-AUMYTFIQ.js → exitPlanMode-UE4FGCY4.js} +41 -41
- package/chunks/{fast-path-TU7U2ETU.js → fast-path-773PV7PU.js} +5 -5
- package/chunks/{gemini-FKMYDP64.js → gemini-IP4FJAP5.js} +101 -100
- package/chunks/{geminiContentGenerator-WSHP7CSR.js → geminiContentGenerator-PUNEBWJW.js} +8 -8
- package/chunks/{glob-EOM6XK7H.js → glob-CZHOWUEA.js} +41 -41
- package/chunks/{goal-tools-CNVXMSZD.js → goal-tools-YZQJXTGH.js} +9 -8
- package/chunks/{grep-3NLSS32D.js → grep-5GPICQBU.js} +6 -6
- package/chunks/handleAutoUpdate-SGXKTQM6.js +111 -0
- package/chunks/{i18n-XX7U2Q3X.js → i18n-B42HKHKH.js} +54 -54
- package/chunks/{image-gen-63NN2H5N.js → image-gen-HAYOEQ4O.js} +9 -9
- package/chunks/initializer-FB66WYR3.js +115 -0
- package/chunks/installationInfo-Y7AULNMX.js +109 -0
- package/chunks/{keychain-token-storage-3MGFRYGM.js → keychain-token-storage-37XPR34V.js} +2 -2
- package/chunks/list-2HWGSVRA.js +118 -0
- package/chunks/{list-agents-EXRSHIDJ.js → list-agents-DI3YFTDT.js} +2 -2
- package/chunks/loadedSettingsAdapter-7Y7IEI6T.js +112 -0
- package/chunks/{loggingContentGenerator-TUDGCVUG.js → loggingContentGenerator-2GGZNGLG.js} +35 -22
- package/chunks/{loop-wakeup-XBPB2BRO.js → loop-wakeup-XJFONSQ7.js} +5 -5
- package/chunks/{ls-WFONAND2.js → ls-3VAT2J7A.js} +4 -4
- package/chunks/{lsp-DUKPDUXN.js → lsp-5W7IRUBH.js} +2 -2
- package/chunks/{managed-npm-update-5K4FWFX2.js → managed-npm-update-5TEV2E3J.js} +54 -54
- package/chunks/mcp-QGG5NMJJ.js +112 -0
- package/chunks/{monitor-UUHIAGT7.js → monitor-FI7PY36Z.js} +41 -41
- package/chunks/nonInteractiveCli-4LO2TL72.js +186 -0
- package/chunks/{notebook-edit-GZPB3Q57.js → notebook-edit-TPGMGBST.js} +42 -42
- package/chunks/{openaiContentGenerator-RPP6YBS3.js → openaiContentGenerator-SUCYANUZ.js} +24 -24
- package/chunks/pidfile-JMF2CLMY.js +113 -0
- package/chunks/{processUtils-ENAY2GSA.js → processUtils-IQZBYWSB.js} +2 -2
- package/chunks/prompt-terminal-ledger-DCZO6S7C.js +107 -0
- package/chunks/{qwenContentGenerator-GLUZVJAU.js → qwenContentGenerator-J4TXFGJT.js} +46 -46
- package/chunks/{qwenOAuth2-JIYAWXTW.js → qwenOAuth2-RF33DNZW.js} +8 -8
- package/chunks/{read-file-75R5LUWQ.js → read-file-QE2B5FTX.js} +12 -12
- package/chunks/{read-mcp-resource-7A7NFSFL.js → read-mcp-resource-XD257PGJ.js} +2 -2
- package/chunks/{record-artifact-CPJ4IJSD.js → record-artifact-RZGZBENU.js} +4 -4
- package/chunks/{report-findings-VFM5DENC.js → report-findings-Q3SYKKW5.js} +5 -5
- package/chunks/{request-shutdown-AXVOFN5C.js → request-shutdown-BCLA3JV2.js} +7 -7
- package/chunks/resumeHistoryUtils-CZ4JRDVW.js +120 -0
- package/chunks/{ripGrep-FNLXQYBS.js → ripGrep-R6B6HWPX.js} +17 -17
- package/chunks/{run-qwen-serve-FJD4VMI2.js → run-qwen-serve-TPHZMSXJ.js} +148 -319
- package/chunks/{runtime-INDSJB4K.js → runtime-TTIGSBM4.js} +62 -62
- package/chunks/scheduled-tasks-QB4ZVUQS.js +129 -0
- package/chunks/{scheduler-Z52TCSUG.js → scheduler-YRUG22M7.js} +55 -55
- package/chunks/{sdk-exporters-http-XXSKBD2C.js → sdk-exporters-http-KCJ4ZFG7.js} +2 -2
- package/chunks/{sdk-impl-VPNBLV7I.js → sdk-impl-HLSEDAEP.js} +4 -4
- package/chunks/{send-message-A2GXDX3B.js → send-message-Y5I6NYAM.js} +7 -7
- package/chunks/serve-MXXCDAQN.js +120 -0
- package/chunks/{server-PSN5OOSX.js → server-DFUSSSHI.js} +19760 -19444
- package/chunks/{session-JPE34X6Q.js → session-3TEHNENZ.js} +100 -99
- package/chunks/{settings-IVGC4UYK.js → settings-VOZYKLTT.js} +58 -58
- package/chunks/{shell-A5PZGPBR.js → shell-6RTKZVMI.js} +41 -41
- package/chunks/{skill-UUXQPDEH.js → skill-FEOKFX26.js} +22 -21
- package/chunks/{skill-settings-WVRXLTK4.js → skill-settings-VA4XVYVL.js} +58 -58
- package/chunks/spawnChannel-F6WVP723.js +114 -0
- package/chunks/{standalone-update-IRFRNGAA.js → standalone-update-RGILUQVK.js} +55 -55
- package/chunks/{startInteractiveUI-MC4GLL6G.js → startInteractiveUI-WHJPFOGH.js} +2757 -2223
- package/chunks/{syntheticOutput-5AJ444QT.js → syntheticOutput-ML43GEFN.js} +3 -3
- package/chunks/{task-create-CDJSOEQD.js → task-create-TMSV7RD5.js} +11 -11
- package/chunks/{task-list-BELMPA75.js → task-list-W55RBFRX.js} +5 -5
- package/chunks/{task-stop-U3S3KZG2.js → task-stop-CXEH5L2D.js} +2 -2
- package/chunks/{task-update-77VCFDDZ.js → task-update-N536I4GQ.js} +11 -11
- package/chunks/{team-create-WO5ZBCFW.js → team-create-QYSWCYCL.js} +41 -41
- package/chunks/{team-delete-YWFXX77L.js → team-delete-FJRFX33M.js} +5 -5
- package/chunks/{team-plan-approval-KAVKRN7U.js → team-plan-approval-P7DAD644.js} +41 -41
- package/chunks/{terminal-image-renderer-SMRRNI6Z.js → terminal-image-renderer-SMQE46UJ.js} +55 -55
- package/chunks/theme-manager-RFO5WWFF.js +104 -0
- package/chunks/{todoWrite-435EO2CS.js → todoWrite-WDJNQEMW.js} +43 -25
- package/chunks/{tool-search-FBN6OC6U.js → tool-search-F2EMIAPV.js} +17 -17
- package/chunks/total-session-admission-HTVTP4BS.js +116 -0
- package/chunks/{trustedFolders-B4LPJBFK.js → trustedFolders-7OCNMUQE.js} +54 -54
- package/chunks/{update-relaunch-X5TJVKVG.js → update-relaunch-4GF3OXMB.js} +5 -5
- package/chunks/{updateCheck-JCLP5JFT.js → updateCheck-7QBDXZRT.js} +56 -56
- package/chunks/useAutoAcceptIndicator-UI4SNL2O.js +122 -0
- package/chunks/{validateNonInterActiveAuth-W7HYH5ZN.js → validateNonInterActiveAuth-BPPA563H.js} +96 -95
- package/chunks/{version-JG4J76GL.js → version-ANP4TS6O.js} +1 -1
- package/chunks/{web-fetch-SPLMEDWF.js → web-fetch-QJADSAH3.js} +16 -16
- package/chunks/{web-search-PNW6OEH6.js → web-search-7TTQKEKM.js} +9 -9
- package/chunks/{workflow-QHOIIXJO.js → workflow-5XN6XSOT.js} +105 -60
- package/chunks/workspace-providers-status-PDFZWL5Q.js +116 -0
- package/chunks/{workspace-registration-store-H375IYR2.js → workspace-registration-store-XKVW2YVH.js} +1 -1
- package/chunks/{workspace-registry-XTLCD7Z2.js → workspace-registry-23R7XRB5.js} +61 -59
- package/chunks/workspace-service-MUYO2QIC.js +131 -0
- package/chunks/workspace-skills-status-IKECTOOQ.js +115 -0
- package/chunks/{workspace-trust-reconciler-6SC6RKQE.js → workspace-trust-reconciler-KLGOB53D.js} +67 -65
- package/chunks/write-file-GGRBB5TZ.js +90 -0
- package/chunks/{zh-TW-MV4FWFB6.js → zh-TW-RPTMRTHG.js} +2 -1
- package/chunks/{zh-X3WGU6SI.js → zh-UNY63FR3.js} +2 -1
- package/chunks/{zoom-image-WGNPZ572.js → zoom-image-6ASG6J4C.js} +12 -12
- package/cli.js +14 -14
- package/locales/en.js +2 -0
- package/locales/zh-TW.js +2 -0
- package/locales/zh.js +2 -0
- package/package.json +3 -3
- package/web-shell/assets/{abnfDiagram-VCTEODGH-yq7z7Anb.js → abnfDiagram-VCTEODGH-uS-vtfs3.js} +1 -1
- package/web-shell/assets/{arc-CbJO0xnT.js → arc-BSTnk5pu.js} +1 -1
- package/web-shell/assets/{architectureDiagram-5GKGNRK7-CRh89421.js → architectureDiagram-5GKGNRK7-DCMyiqzy.js} +1 -1
- package/web-shell/assets/{blockDiagram-NRAW4CY4-C75KAhKv.js → blockDiagram-NRAW4CY4-C7_gt0r1.js} +1 -1
- package/web-shell/assets/{c4Diagram-UCG6FXSJ-BzlMveFB.js → c4Diagram-UCG6FXSJ-BeQg2L8B.js} +1 -1
- package/web-shell/assets/channel-CENAhErJ.js +1 -0
- package/web-shell/assets/{chunk-2Q5K7J3B-DA8QDX3a.js → chunk-2Q5K7J3B-DnV9bKBe.js} +1 -1
- package/web-shell/assets/{chunk-5VM5RSS4-Bgu7gQ7Y.js → chunk-5VM5RSS4-rT1GAcT7.js} +1 -1
- package/web-shell/assets/{chunk-F27PBJKO-BOew7pIK.js → chunk-F27PBJKO-BcgSxqGA.js} +1 -1
- package/web-shell/assets/{chunk-G27WJ6UU-DHx1I-hO.js → chunk-G27WJ6UU-Bw1ZhyZq.js} +1 -1
- package/web-shell/assets/{chunk-JWPE2WC7-SarFW0WU.js → chunk-JWPE2WC7-D4iDw-96.js} +1 -1
- package/web-shell/assets/{chunk-LCL6LL3I-BLzP083Z.js → chunk-LCL6LL3I-D3MB3GTZ.js} +1 -1
- package/web-shell/assets/{chunk-POPQ4Y6H-Bmkv_iKm.js → chunk-POPQ4Y6H-Cep2ndrS.js} +1 -1
- package/web-shell/assets/{chunk-SVP7TREG-MPx9r1N2.js → chunk-SVP7TREG-BuahMHJc.js} +1 -1
- package/web-shell/assets/{chunk-XXDRQBXY-Ck-09rKG.js → chunk-XXDRQBXY-BdujIvJY.js} +1 -1
- package/web-shell/assets/classDiagram-DTDB5LWJ-CM6FArGH.js +1 -0
- package/web-shell/assets/classDiagram-v2-JRS7N3AN-CM6FArGH.js +1 -0
- package/web-shell/assets/{cose-bilkent-JH36ORCC-DWm6lfOG.js → cose-bilkent-JH36ORCC-CGLLUbF8.js} +1 -1
- package/web-shell/assets/{cynefin-OW5HDTMX-DcBUyr5R.js → cynefin-OW5HDTMX-D6kDsRSh.js} +1 -1
- package/web-shell/assets/{cynefinDiagram-5FMLGOSQ-Df8qmd_p.js → cynefinDiagram-5FMLGOSQ-CCy78qZx.js} +1 -1
- package/web-shell/assets/{dagre-3AP2YEHR-DDmGspD7.js → dagre-3AP2YEHR-D5gJuJsf.js} +1 -1
- package/web-shell/assets/{diagram-S7CK7UJ4-BK_qFvbi.js → diagram-S7CK7UJ4-ZwEZ5W__.js} +1 -1
- package/web-shell/assets/{diagram-UQ7AKVKN-Uw3AP_bi.js → diagram-UQ7AKVKN-B036APjv.js} +1 -1
- package/web-shell/assets/{diagram-VSXAHHWV-CJOQKxY7.js → diagram-VSXAHHWV-BfDJ57Fq.js} +1 -1
- package/web-shell/assets/{diagram-VX7I27RA-DhdvZna5.js → diagram-VX7I27RA-Wha0p5rT.js} +1 -1
- package/web-shell/assets/{diagram-Z3DM3KII-edQU0nlP.js → diagram-Z3DM3KII-L20bz2K1.js} +1 -1
- package/web-shell/assets/{ebnfDiagram-PWID7BFC-4kugBEsO.js → ebnfDiagram-PWID7BFC-D1ZM0Cp7.js} +1 -1
- package/web-shell/assets/{erDiagram-SSCWMZ5O-BMkFivWO.js → erDiagram-SSCWMZ5O-CMtA19v8.js} +1 -1
- package/web-shell/assets/{flowDiagram-A5DVABFB-BM1ea8g-.js → flowDiagram-A5DVABFB-D96m326S.js} +1 -1
- package/web-shell/assets/{ganttDiagram-EL5Y4UJY-CXj_cKRf.js → ganttDiagram-EL5Y4UJY-BuHiYG2o.js} +1 -1
- package/web-shell/assets/{gitGraphDiagram-WWUBYQGX-D0nEqwqu.js → gitGraphDiagram-WWUBYQGX-Ck3NmkSw.js} +1 -1
- package/web-shell/assets/index-8hXIpvo4.js +1865 -0
- package/web-shell/assets/{index-B6JN54Pd.js → index-CBGz4Qjl.js} +1 -1
- package/web-shell/assets/{index-C-XlQn24.css → index-DclEv91i.css} +1 -1
- package/web-shell/assets/{infoDiagram-RXCK75RN-DF7aGGbQ.js → infoDiagram-RXCK75RN-B6DAOyXe.js} +1 -1
- package/web-shell/assets/{ishikawaDiagram-5VMMS53U-CZYNX1lL.js → ishikawaDiagram-5VMMS53U-Bpg-joFo.js} +1 -1
- package/web-shell/assets/{journeyDiagram-EYS64GPL-CvrqMUz1.js → journeyDiagram-EYS64GPL-BclrHzGa.js} +1 -1
- package/web-shell/assets/{kanban-definition-3QL26DDD-BZNJQZBj.js → kanban-definition-3QL26DDD-5gJzNTYG.js} +1 -1
- package/web-shell/assets/{layout-Bf83x7D6.js → layout-CEz_1L8z.js} +1 -1
- package/web-shell/assets/{linear-Bkc3ES2f.js → linear-CBjTq9Hc.js} +1 -1
- package/web-shell/assets/{mermaid.core-C6xuovwz.js → mermaid.core-Dzhbxx_I.js} +6 -6
- package/web-shell/assets/{mindmap-definition-FBJOCRG2-DzH6gVRg.js → mindmap-definition-FBJOCRG2-DQdVqlBF.js} +1 -1
- package/web-shell/assets/{pegDiagram-XKGWAZYB-DCvLh2fa.js → pegDiagram-XKGWAZYB-D1JZlYXL.js} +1 -1
- package/web-shell/assets/{pieDiagram-E7YTZNPT-BtjVlx_6.js → pieDiagram-E7YTZNPT-BmkpYL0w.js} +1 -1
- package/web-shell/assets/{quadrantDiagram-AXDQQJYC-IbroqxFe.js → quadrantDiagram-AXDQQJYC-DRvD6co5.js} +1 -1
- package/web-shell/assets/{railroadDiagram-O6MQD6OU-BvMykiWW.js → railroadDiagram-O6MQD6OU-CEdjDuoo.js} +1 -1
- package/web-shell/assets/{requirementDiagram-EFPCY7ZU-D-HHMyrh.js → requirementDiagram-EFPCY7ZU-BCHG-ie8.js} +1 -1
- package/web-shell/assets/{sankeyDiagram-P5KCCOFB-C0XXhB6d.js → sankeyDiagram-P5KCCOFB-C9cZMnye.js} +1 -1
- package/web-shell/assets/{sequenceDiagram-WJ2MYXX4-DseAXj5d.js → sequenceDiagram-WJ2MYXX4-DYHRV4zL.js} +1 -1
- package/web-shell/assets/{sizeCapture-X5ZJPWSS-Cek2EweK.js → sizeCapture-X5ZJPWSS-DlK21aye.js} +1 -1
- package/web-shell/assets/{stateDiagram-HBIQ2CUA-yLv8wafW.js → stateDiagram-HBIQ2CUA-BrmZssvk.js} +1 -1
- package/web-shell/assets/stateDiagram-v2-4QOOHH4V-B2Q7XDaE.js +1 -0
- package/web-shell/assets/{swimlanes-XN3QIQJK-Cwz-opvE.js → swimlanes-XN3QIQJK-D9xfcayR.js} +1 -1
- package/web-shell/assets/swimlanesDiagram-VK2B7HYN-DpZPOSbX.js +8 -0
- package/web-shell/assets/{timeline-definition-24CTP7MA-BJuFD7OG.js → timeline-definition-24CTP7MA-D4gdDL_d.js} +1 -1
- package/web-shell/assets/{vennDiagram-4TSXK5OY-CNAavdoV.js → vennDiagram-4TSXK5OY-DdhIExjI.js} +1 -1
- package/web-shell/assets/{wardleyDiagram-VM6X3IG4-DaIjAidq.js → wardleyDiagram-VM6X3IG4-C9RS_Zof.js} +1 -1
- package/web-shell/assets/{xychartDiagram-S5SC5T6Z-DkcQXjTy.js → xychartDiagram-S5SC5T6Z-orRgmxWk.js} +1 -1
- package/web-shell/index.html +2 -2
- package/chunks/MaxSizedBox-XYQOMOBE.js +0 -112
- package/chunks/agent-headless-3ZC4LX3E.js +0 -87
- package/chunks/bridge-LFFLHLU5.js +0 -119
- package/chunks/channel-settings-store-CAL2HWAH.js +0 -117
- package/chunks/chunk-JS73NNWF.js +0 -30
- package/chunks/chunk-VGHZT6JR.js +0 -51
- package/chunks/config-utils-YMJNA25D.js +0 -113
- package/chunks/contextCommand-WMPSNKPG.js +0 -108
- package/chunks/daemon-status-provider-YBQAZD3G.js +0 -118
- package/chunks/daemon-trust-policy-O75ZSIJN.js +0 -114
- package/chunks/deferred-core-runtime-MGCCR6LA.js +0 -116
- package/chunks/earlyInputCapture-2EJKJET3.js +0 -109
- package/chunks/errors-XEI3SJRV.js +0 -115
- package/chunks/handleAutoUpdate-S4CLP36D.js +0 -111
- package/chunks/initializer-D3CA3272.js +0 -115
- package/chunks/installationInfo-HVDQOSIE.js +0 -109
- package/chunks/list-XUDSTGT3.js +0 -118
- package/chunks/loadedSettingsAdapter-ZOJ4QEID.js +0 -112
- package/chunks/mcp-QJ6AUYE5.js +0 -112
- package/chunks/nonInteractiveCli-PAWO75YO.js +0 -185
- package/chunks/pidfile-RF3WUFKR.js +0 -113
- package/chunks/prompt-terminal-ledger-4CCXZ2LF.js +0 -107
- package/chunks/resumeHistoryUtils-GFPJD2XB.js +0 -120
- package/chunks/serve-GVD2WXVL.js +0 -120
- package/chunks/spawnChannel-KTURLD4B.js +0 -113
- package/chunks/theme-manager-A7CPN7F4.js +0 -104
- package/chunks/total-session-admission-B7U6UWKQ.js +0 -114
- package/chunks/useAutoAcceptIndicator-VBT3NKDM.js +0 -122
- package/chunks/workspace-providers-status-653DTR35.js +0 -116
- package/chunks/workspace-service-KVJWACCS.js +0 -131
- package/chunks/workspace-skills-status-TRJ2VWGU.js +0 -115
- package/chunks/write-file-EAZSJZKR.js +0 -90
- package/web-shell/assets/channel-B3sGR79J.js +0 -1
- package/web-shell/assets/classDiagram-DTDB5LWJ-sTGo0K3v.js +0 -1
- package/web-shell/assets/classDiagram-v2-JRS7N3AN-sTGo0K3v.js +0 -1
- package/web-shell/assets/index-DLjXM0dn.js +0 -1860
- package/web-shell/assets/stateDiagram-v2-4QOOHH4V-DeThXnol.js +0 -1
- package/web-shell/assets/swimlanesDiagram-VK2B7HYN-GW0R1ZSv.js +0 -8
|
@@ -0,0 +1,3416 @@
|
|
|
1
|
+
// Force strict mode and setup for ESM
|
|
2
|
+
"use strict";
|
|
3
|
+
import {
|
|
4
|
+
SERVE_CONTROL_EXT_METHODS
|
|
5
|
+
} from "./chunk-JHS74YAB.js";
|
|
6
|
+
import {
|
|
7
|
+
ACTIVE_WORK_HEARTBEAT_VERSION,
|
|
8
|
+
ACTIVE_WORK_HOLD_CATEGORIES,
|
|
9
|
+
ACTIVE_WORK_MAX_SESSION_HOLDS,
|
|
10
|
+
ACTIVE_WORK_MAX_SNAPSHOT_SESSIONS,
|
|
11
|
+
ACTIVE_WORK_NOTIFICATION_METHOD,
|
|
12
|
+
DAEMON_PERMISSION_CANCEL_REASON_META_KEY,
|
|
13
|
+
MID_TURN_QUEUE_DRAIN_METHOD,
|
|
14
|
+
MID_TURN_RECONCILIATION_RING_SIZE,
|
|
15
|
+
TODO_STOP_GUARD_CONTINUATION_CLAIM_METHOD
|
|
16
|
+
} from "./chunk-OZ6KS6KW.js";
|
|
17
|
+
import {
|
|
18
|
+
CancelSentinelCollisionError,
|
|
19
|
+
InvalidPermissionOptionError
|
|
20
|
+
} from "./chunk-ERFDKH32.js";
|
|
21
|
+
import {
|
|
22
|
+
CHANNEL_DELIVERY_ERROR_CODES,
|
|
23
|
+
LIVE_TASK_TOOL_NAMES,
|
|
24
|
+
MAX_LIVE_SCREEN_CONTEXT_TEXT_CHARS,
|
|
25
|
+
MAX_LIVE_SPEAK_TO_USER_MESSAGE_CHARS,
|
|
26
|
+
MAX_SUB_SESSION_NAME_CHARS
|
|
27
|
+
} from "./chunk-UDG5EZJI.js";
|
|
28
|
+
import {
|
|
29
|
+
isValidExternalToolGuardDenialReason
|
|
30
|
+
} from "./chunk-3VUENPWF.js";
|
|
31
|
+
import {
|
|
32
|
+
RequestError
|
|
33
|
+
} from "./chunk-IW6RQPQB.js";
|
|
34
|
+
import {
|
|
35
|
+
MAX_SUB_SESSION_PROMPT_CHARS
|
|
36
|
+
} from "./chunk-LJZSMWOH.js";
|
|
37
|
+
import {
|
|
38
|
+
APPROVAL_MODES
|
|
39
|
+
} from "./chunk-V5J4J5TP.js";
|
|
40
|
+
import {
|
|
41
|
+
getSpecificMimeType
|
|
42
|
+
} from "./chunk-3I6UTTDX.js";
|
|
43
|
+
import {
|
|
44
|
+
init_esbuild_shims
|
|
45
|
+
} from "./chunk-5O2XNYP6.js";
|
|
46
|
+
import {
|
|
47
|
+
__name
|
|
48
|
+
} from "./chunk-J2S4EL5Y.js";
|
|
49
|
+
|
|
50
|
+
// packages/acp-bridge/src/permissionMediator.ts
|
|
51
|
+
init_esbuild_shims();
|
|
52
|
+
|
|
53
|
+
// packages/acp-bridge/src/permission.ts
|
|
54
|
+
init_esbuild_shims();
|
|
55
|
+
|
|
56
|
+
// packages/acp-bridge/src/permissionMediator.ts
|
|
57
|
+
var CANCEL_VOTE_SENTINEL = "__cancelled__";
|
|
58
|
+
var MAX_RESOLVED_PERMISSION_RECORDS = 512;
|
|
59
|
+
function stringifyError(err) {
|
|
60
|
+
try {
|
|
61
|
+
if (err instanceof Error) return `${err.name}: ${err.message}`;
|
|
62
|
+
return String(err);
|
|
63
|
+
} catch {
|
|
64
|
+
return "[unstringifiable error]";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
__name(stringifyError, "stringifyError");
|
|
68
|
+
function createNoOpPermissionAuditPublisher() {
|
|
69
|
+
return {
|
|
70
|
+
recordRequested() {
|
|
71
|
+
},
|
|
72
|
+
recordVoted() {
|
|
73
|
+
},
|
|
74
|
+
recordForbidden() {
|
|
75
|
+
},
|
|
76
|
+
recordResolved() {
|
|
77
|
+
},
|
|
78
|
+
recordTimeout() {
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
__name(createNoOpPermissionAuditPublisher, "createNoOpPermissionAuditPublisher");
|
|
83
|
+
var MultiClientPermissionMediator = class {
|
|
84
|
+
static {
|
|
85
|
+
__name(this, "MultiClientPermissionMediator");
|
|
86
|
+
}
|
|
87
|
+
policy;
|
|
88
|
+
deps;
|
|
89
|
+
pending = /* @__PURE__ */ new Map();
|
|
90
|
+
resolved = /* @__PURE__ */ new Map();
|
|
91
|
+
resolvedOrder = [];
|
|
92
|
+
/**
|
|
93
|
+
* Dedup flag for the
|
|
94
|
+
* unanimity-required stderr breadcrumb. Without this, every
|
|
95
|
+
* permission request on a 2-client consensus session would emit
|
|
96
|
+
* an identical line (the unanimity condition is the NORMAL
|
|
97
|
+
* operating mode for M=2, not a rare edge); a busy session with
|
|
98
|
+
* many tool calls would produce dozens of duplicate stderr lines
|
|
99
|
+
* within seconds. One emit per mediator (= per bridge/runtime lifetime
|
|
100
|
+
* since each bridge constructs one) is enough to make the
|
|
101
|
+
* configuration visible without spam.
|
|
102
|
+
*/
|
|
103
|
+
unanimityBreadcrumbEmitted = false;
|
|
104
|
+
constructor(policy, deps) {
|
|
105
|
+
this.policy = policy;
|
|
106
|
+
this.deps = deps;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Register a fresh permission request from the agent.
|
|
110
|
+
*
|
|
111
|
+
* **Promise contract — once the Promise is returned, it never
|
|
112
|
+
* rejects.** All runtime failure modes (timeout, session closure,
|
|
113
|
+
* voter cancel, emit/audit publisher exceptions) are encoded as
|
|
114
|
+
* `PermissionResolution { kind:'cancelled', reason:... }`.
|
|
115
|
+
* Consumers can `await` the returned Promise and forward the
|
|
116
|
+
* result without a `.catch()` block.
|
|
117
|
+
*
|
|
118
|
+
* **Synchronous-throw exception**:
|
|
119
|
+
* when the agent's `allowedOptionIds` contains the
|
|
120
|
+
* cancel-vote sentinel string, this method throws
|
|
121
|
+
* `CancelSentinelCollisionError` synchronously BEFORE constructing
|
|
122
|
+
* the Promise. The synchronous shape is intentional — a
|
|
123
|
+
* never-settling Promise alongside a thrown error would be worse
|
|
124
|
+
* than a clean fail-fast — but callers must wrap this method
|
|
125
|
+
* itself in `try/catch` (or call it from an `async` function so
|
|
126
|
+
* the throw bubbles via the function's own Promise machinery).
|
|
127
|
+
* `bridgeClient.ts` currently has its own pre-check at the bridge
|
|
128
|
+
* layer; embedded callers must do the same. See `@throws` below.
|
|
129
|
+
*
|
|
130
|
+
* **Synchronous-register invariant**: pending entry, audit
|
|
131
|
+
* record, and timer setup all happen inside the Promise executor
|
|
132
|
+
* without `await`. The bridge's `publish → mediator.request → await`
|
|
133
|
+
* sequence relies on this — a `forgetSession` between publish and
|
|
134
|
+
* await would otherwise miss the new pending and leak it until
|
|
135
|
+
* timeout.
|
|
136
|
+
*
|
|
137
|
+
* @throws `CancelSentinelCollisionError` SYNCHRONOUSLY (not as a
|
|
138
|
+
* Promise rejection) if `record.allowedOptionIds` contains the
|
|
139
|
+
* cancel-vote sentinel string. This is a contract violation
|
|
140
|
+
* between agent and daemon and fails loudly at issue time
|
|
141
|
+
* rather than silently miscounting votes downstream. Callers
|
|
142
|
+
* inside an `async` function get the thrown error through the
|
|
143
|
+
* function's own Promise; synchronous callers must use
|
|
144
|
+
* `try/catch`.
|
|
145
|
+
*/
|
|
146
|
+
request(record, timeoutMs) {
|
|
147
|
+
if (record.allowedOptionIds.has(CANCEL_VOTE_SENTINEL)) {
|
|
148
|
+
throw new CancelSentinelCollisionError(
|
|
149
|
+
record.requestId,
|
|
150
|
+
CANCEL_VOTE_SENTINEL
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return new Promise((resolve2) => {
|
|
154
|
+
const policy = this.policy;
|
|
155
|
+
const votersAtIssue = this.deps.votersForSession(record.sessionId);
|
|
156
|
+
const pending = {
|
|
157
|
+
requestId: record.requestId,
|
|
158
|
+
sessionId: record.sessionId,
|
|
159
|
+
promptId: record.promptId,
|
|
160
|
+
policy,
|
|
161
|
+
originatorClientId: record.originatorClientId,
|
|
162
|
+
allowedOptionIds: record.allowedOptionIds,
|
|
163
|
+
issuedAtMs: record.issuedAtMs,
|
|
164
|
+
timeoutMs,
|
|
165
|
+
resolve: resolve2,
|
|
166
|
+
tallies: /* @__PURE__ */ new Map(),
|
|
167
|
+
votersAtIssue,
|
|
168
|
+
timer: void 0,
|
|
169
|
+
consensusQuorumCapNoted: false
|
|
170
|
+
};
|
|
171
|
+
this.pending.set(record.requestId, pending);
|
|
172
|
+
this.safeAudit(
|
|
173
|
+
() => this.deps.audit.recordRequested(record, policy, votersAtIssue)
|
|
174
|
+
);
|
|
175
|
+
const unresolvedWithoutDecision = timeoutMs > 0 ? `voter cancellation, session cancellation, or permissionTimeoutMs (${timeoutMs}ms)` : "voter or session cancellation because permissionTimeoutMs is disabled";
|
|
176
|
+
if (policy === "consensus" && votersAtIssue.size === 0) {
|
|
177
|
+
try {
|
|
178
|
+
process.stderr.write(
|
|
179
|
+
`permissionMediator: consensus request ${record.requestId} for session ${record.sessionId} issued with empty votersAtIssue; can only resolve via ${unresolvedWithoutDecision}
|
|
180
|
+
`
|
|
181
|
+
);
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (policy === "consensus" && this.deps.consensusQuorum === void 0 && votersAtIssue.size >= 2 && Math.floor(votersAtIssue.size / 2) + 1 === votersAtIssue.size && !this.unanimityBreadcrumbEmitted) {
|
|
186
|
+
this.unanimityBreadcrumbEmitted = true;
|
|
187
|
+
try {
|
|
188
|
+
process.stderr.write(
|
|
189
|
+
`permissionMediator: consensus request ${record.requestId} for session ${record.sessionId} requires unanimity (votersAtIssue.size=${votersAtIssue.size}, default quorum=floor(M/2)+1=${votersAtIssue.size}); split votes will only resolve via ${unresolvedWithoutDecision}. This breadcrumb fires once per mediator lifetime; subsequent unanimity-required requests are silent.
|
|
190
|
+
`
|
|
191
|
+
);
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (timeoutMs > 0) {
|
|
196
|
+
pending.timer = setTimeout(() => {
|
|
197
|
+
if (this.pending.get(record.requestId) !== pending) return;
|
|
198
|
+
const firedAtMs = this.deps.now();
|
|
199
|
+
try {
|
|
200
|
+
process.stderr.write(
|
|
201
|
+
`qwen serve: permission ${record.requestId} (session ${record.sessionId}) timed out after ${timeoutMs}ms
|
|
202
|
+
`
|
|
203
|
+
);
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
206
|
+
this.safeAudit(() => this.deps.audit.recordTimeout(record));
|
|
207
|
+
this.resolveEntry(
|
|
208
|
+
pending,
|
|
209
|
+
{ kind: "cancelled", reason: "timeout" },
|
|
210
|
+
{
|
|
211
|
+
type: "timeout",
|
|
212
|
+
issuedAtMs: pending.issuedAtMs,
|
|
213
|
+
timeoutMs: pending.timeoutMs,
|
|
214
|
+
firedAtMs
|
|
215
|
+
},
|
|
216
|
+
void 0
|
|
217
|
+
);
|
|
218
|
+
}, timeoutMs);
|
|
219
|
+
const t = pending.timer;
|
|
220
|
+
if (t && typeof t === "object" && "unref" in t) {
|
|
221
|
+
t.unref();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
vote(vote) {
|
|
227
|
+
const pending = this.pending.get(vote.requestId);
|
|
228
|
+
if (!pending) {
|
|
229
|
+
const prior = this.resolved.get(vote.requestId);
|
|
230
|
+
if (prior && prior.sessionId === vote.sessionId) {
|
|
231
|
+
const optionId = prior.resolution.kind === "option" ? prior.resolution.optionId : CANCEL_VOTE_SENTINEL;
|
|
232
|
+
this.safeEmit(prior.sessionId, {
|
|
233
|
+
type: "permission_already_resolved",
|
|
234
|
+
...prior.promptId ? { promptId: prior.promptId } : {},
|
|
235
|
+
data: {
|
|
236
|
+
requestId: prior.requestId,
|
|
237
|
+
sessionId: prior.sessionId,
|
|
238
|
+
outcome: this.toAcpOutcome(prior.resolution)
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
return { kind: "already_resolved", resolvedOptionId: optionId };
|
|
242
|
+
}
|
|
243
|
+
return { kind: "unknown_request" };
|
|
244
|
+
}
|
|
245
|
+
if (pending.sessionId !== vote.sessionId) {
|
|
246
|
+
return { kind: "unknown_request" };
|
|
247
|
+
}
|
|
248
|
+
if (vote.optionId === CANCEL_VOTE_SENTINEL) {
|
|
249
|
+
const outcome = {
|
|
250
|
+
kind: "resolved",
|
|
251
|
+
resolvedOptionId: CANCEL_VOTE_SENTINEL
|
|
252
|
+
};
|
|
253
|
+
this.safeAudit(
|
|
254
|
+
() => this.deps.audit.recordVoted(this.toRecord(pending), vote, outcome)
|
|
255
|
+
);
|
|
256
|
+
this.resolveEntry(
|
|
257
|
+
pending,
|
|
258
|
+
{ kind: "cancelled", reason: "agent_cancelled" },
|
|
259
|
+
{
|
|
260
|
+
type: "voter-cancelled",
|
|
261
|
+
resolverClientId: vote.clientId
|
|
262
|
+
},
|
|
263
|
+
vote.clientId
|
|
264
|
+
);
|
|
265
|
+
return outcome;
|
|
266
|
+
}
|
|
267
|
+
if (!pending.allowedOptionIds.has(vote.optionId)) {
|
|
268
|
+
throw new InvalidPermissionOptionError(vote.requestId, vote.optionId);
|
|
269
|
+
}
|
|
270
|
+
switch (pending.policy) {
|
|
271
|
+
case "first-responder":
|
|
272
|
+
return this.voteFirstResponder(pending, vote);
|
|
273
|
+
case "designated":
|
|
274
|
+
return this.voteDesignated(pending, vote);
|
|
275
|
+
case "consensus":
|
|
276
|
+
return this.voteConsensus(pending, vote);
|
|
277
|
+
case "local-only":
|
|
278
|
+
return this.voteLocalOnly(pending, vote);
|
|
279
|
+
default: {
|
|
280
|
+
const _exhaustive = pending.policy;
|
|
281
|
+
void _exhaustive;
|
|
282
|
+
throw new Error(`Unknown permission policy "${pending.policy}"`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
forgetSession(sessionId) {
|
|
287
|
+
const requestIds = [];
|
|
288
|
+
for (const [id, pending] of this.pending) {
|
|
289
|
+
if (pending.sessionId === sessionId) requestIds.push(id);
|
|
290
|
+
}
|
|
291
|
+
for (const id of requestIds) {
|
|
292
|
+
const pending = this.pending.get(id);
|
|
293
|
+
if (!pending) continue;
|
|
294
|
+
this.resolveEntry(
|
|
295
|
+
pending,
|
|
296
|
+
{ kind: "cancelled", reason: "session_closed" },
|
|
297
|
+
{ type: "session-closed" },
|
|
298
|
+
void 0
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Lookup the sessionId for a given requestId. Used by the legacy
|
|
304
|
+
* `bridge.respondToPermission(requestId, ...)` route which doesn't
|
|
305
|
+
* carry a sessionId in the URL. NOT part of the
|
|
306
|
+
* `PermissionMediator` interface contract — bridge holds the
|
|
307
|
+
* concrete class reference and calls this directly.
|
|
308
|
+
*/
|
|
309
|
+
peekSessionFor(requestId) {
|
|
310
|
+
const pending = this.pending.get(requestId);
|
|
311
|
+
if (pending) return pending.sessionId;
|
|
312
|
+
const prior = this.resolved.get(requestId);
|
|
313
|
+
return prior?.sessionId;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Daemon-wide in-flight pending count for diagnostics. The bridge
|
|
317
|
+
* exposes this through its `pendingPermissionCount` getter so
|
|
318
|
+
* operators can spot stuck FIFOs without reaching into mediator
|
|
319
|
+
* internals. NOT part of the `PermissionMediator` interface
|
|
320
|
+
* contract.
|
|
321
|
+
*/
|
|
322
|
+
get pendingCount() {
|
|
323
|
+
return this.pending.size;
|
|
324
|
+
}
|
|
325
|
+
// ===========================================================
|
|
326
|
+
// Per-policy vote handlers
|
|
327
|
+
// ===========================================================
|
|
328
|
+
voteFirstResponder(pending, vote) {
|
|
329
|
+
return this.resolveWithVote(pending, vote, {
|
|
330
|
+
type: "first-responder",
|
|
331
|
+
resolverClientId: vote.clientId
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
voteDesignated(pending, vote) {
|
|
335
|
+
if (pending.originatorClientId === void 0) {
|
|
336
|
+
return this.voteFirstResponder(pending, vote);
|
|
337
|
+
}
|
|
338
|
+
if (vote.clientId !== pending.originatorClientId) {
|
|
339
|
+
return this.rejectForbidden(
|
|
340
|
+
pending,
|
|
341
|
+
vote,
|
|
342
|
+
"designated_mismatch",
|
|
343
|
+
"designated_mismatch (voter is not the prompt originator)"
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return this.resolveWithVote(pending, vote, {
|
|
347
|
+
type: "designated-originator",
|
|
348
|
+
originatorClientId: pending.originatorClientId
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
voteConsensus(pending, vote) {
|
|
352
|
+
if (vote.clientId === void 0 || !pending.votersAtIssue.has(vote.clientId)) {
|
|
353
|
+
return this.rejectForbidden(
|
|
354
|
+
pending,
|
|
355
|
+
vote,
|
|
356
|
+
"designated_mismatch",
|
|
357
|
+
"designated_mismatch (voter not in consensus votersAtIssue snapshot)"
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
for (const [originalOptionId, set] of pending.tallies.entries()) {
|
|
361
|
+
if (vote.clientId !== void 0 && set.has(vote.clientId)) {
|
|
362
|
+
const outcome2 = {
|
|
363
|
+
kind: "recorded",
|
|
364
|
+
votesNeeded: this.votesNeededFor(pending)
|
|
365
|
+
};
|
|
366
|
+
this.safeAudit(
|
|
367
|
+
() => this.deps.audit.recordVoted(
|
|
368
|
+
this.toRecord(pending),
|
|
369
|
+
{ ...vote, optionId: originalOptionId },
|
|
370
|
+
outcome2
|
|
371
|
+
)
|
|
372
|
+
);
|
|
373
|
+
return outcome2;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
let bucket = pending.tallies.get(vote.optionId);
|
|
377
|
+
if (!bucket) {
|
|
378
|
+
bucket = /* @__PURE__ */ new Set();
|
|
379
|
+
pending.tallies.set(vote.optionId, bucket);
|
|
380
|
+
}
|
|
381
|
+
bucket.add(vote.clientId);
|
|
382
|
+
const quorum = this.consensusQuorumFor(pending);
|
|
383
|
+
if (bucket.size >= quorum) {
|
|
384
|
+
return this.resolveWithVote(pending, vote, {
|
|
385
|
+
type: "consensus-quorum",
|
|
386
|
+
resolvedOptionId: vote.optionId,
|
|
387
|
+
quorum,
|
|
388
|
+
tally: bucket.size
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
const outcome = {
|
|
392
|
+
kind: "recorded",
|
|
393
|
+
votesNeeded: this.votesNeededFor(pending)
|
|
394
|
+
};
|
|
395
|
+
this.safeAudit(
|
|
396
|
+
() => this.deps.audit.recordVoted(this.toRecord(pending), vote, outcome)
|
|
397
|
+
);
|
|
398
|
+
this.safeEmit(pending.sessionId, {
|
|
399
|
+
type: "permission_partial_vote",
|
|
400
|
+
...pending.promptId ? { promptId: pending.promptId } : {},
|
|
401
|
+
data: {
|
|
402
|
+
requestId: pending.requestId,
|
|
403
|
+
sessionId: pending.sessionId,
|
|
404
|
+
votesReceived: this.totalTalliedFor(pending),
|
|
405
|
+
votesNeeded: outcome.votesNeeded,
|
|
406
|
+
quorum,
|
|
407
|
+
optionTallies: this.optionTalliesFor(pending)
|
|
408
|
+
},
|
|
409
|
+
...pending.originatorClientId !== void 0 ? { originatorClientId: pending.originatorClientId } : {}
|
|
410
|
+
});
|
|
411
|
+
return outcome;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Vote dispatch for `local-only` policy: only `fromLoopback: true`
|
|
415
|
+
* voters can resolve a permission.
|
|
416
|
+
*
|
|
417
|
+
* **Cancel-sentinel asymmetry** (cancel-sentinel note).
|
|
418
|
+
* `vote()` recognizes the cancel sentinel BEFORE calling this
|
|
419
|
+
* method (cross-policy escape hatch — see the
|
|
420
|
+
* `CANCEL_VOTE_SENTINEL` JSDoc for the rationale), so a remote
|
|
421
|
+
* voter under `local-only` CAN abort a pending permission via
|
|
422
|
+
* `{outcome:'cancelled'}` even though they cannot RESOLVE one. The
|
|
423
|
+
* settings-side description for `local-only` and the design doc call
|
|
424
|
+
* out this gap explicitly. Operators who want strict-cancel-too
|
|
425
|
+
* semantics must (a) deploy a dedicated daemon process at
|
|
426
|
+
* loopback bind, OR (b) wait for the follow-up PR that lifts
|
|
427
|
+
* cancel into per-policy gating; This version keeps the current
|
|
428
|
+
* cross-policy cancel for consistency with first-responder /
|
|
429
|
+
* designated / consensus.
|
|
430
|
+
*/
|
|
431
|
+
voteLocalOnly(pending, vote) {
|
|
432
|
+
if (!vote.fromLoopback) {
|
|
433
|
+
return this.rejectForbidden(
|
|
434
|
+
pending,
|
|
435
|
+
vote,
|
|
436
|
+
"remote_not_allowed",
|
|
437
|
+
"remote_not_allowed (local-only policy; vote not from loopback)"
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
return this.resolveWithVote(pending, vote, {
|
|
441
|
+
type: "local-only-loopback",
|
|
442
|
+
resolverClientId: vote.clientId
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
// ===========================================================
|
|
446
|
+
// Shared vote-resolution helpers
|
|
447
|
+
// ===========================================================
|
|
448
|
+
resolveWithVote(pending, vote, decisionReason) {
|
|
449
|
+
const outcome = {
|
|
450
|
+
kind: "resolved",
|
|
451
|
+
resolvedOptionId: vote.optionId
|
|
452
|
+
};
|
|
453
|
+
this.safeAudit(
|
|
454
|
+
() => this.deps.audit.recordVoted(this.toRecord(pending), vote, outcome)
|
|
455
|
+
);
|
|
456
|
+
this.resolveEntry(
|
|
457
|
+
pending,
|
|
458
|
+
{
|
|
459
|
+
kind: "option",
|
|
460
|
+
optionId: vote.optionId,
|
|
461
|
+
...vote.metadata ? { metadata: vote.metadata } : {}
|
|
462
|
+
},
|
|
463
|
+
decisionReason,
|
|
464
|
+
vote.clientId
|
|
465
|
+
);
|
|
466
|
+
return outcome;
|
|
467
|
+
}
|
|
468
|
+
rejectForbidden(pending, vote, reason, stderrDetail) {
|
|
469
|
+
this.safeAudit(
|
|
470
|
+
() => this.deps.audit.recordForbidden(this.toRecord(pending), vote, reason)
|
|
471
|
+
);
|
|
472
|
+
this.safeEmit(pending.sessionId, {
|
|
473
|
+
type: "permission_forbidden",
|
|
474
|
+
...pending.promptId ? { promptId: pending.promptId } : {},
|
|
475
|
+
data: {
|
|
476
|
+
requestId: pending.requestId,
|
|
477
|
+
sessionId: pending.sessionId,
|
|
478
|
+
...vote.clientId !== void 0 ? { clientId: vote.clientId } : {},
|
|
479
|
+
reason
|
|
480
|
+
},
|
|
481
|
+
...pending.originatorClientId !== void 0 ? { originatorClientId: pending.originatorClientId } : {}
|
|
482
|
+
});
|
|
483
|
+
this.writeForbiddenStderr(pending, vote, stderrDetail);
|
|
484
|
+
return { kind: "forbidden", reason };
|
|
485
|
+
}
|
|
486
|
+
// ===========================================================
|
|
487
|
+
// Consensus tally helpers
|
|
488
|
+
// ===========================================================
|
|
489
|
+
/**
|
|
490
|
+
* Compute the quorum size for a `consensus` request. Default
|
|
491
|
+
* `floor(M/2) + 1` of `votersAtIssue.size`; overridden by
|
|
492
|
+
* `deps.consensusQuorum` when set, capped to `M` so an operator
|
|
493
|
+
* misconfig (N > M) can't deadlock.
|
|
494
|
+
*
|
|
495
|
+
* When the cap fires, write
|
|
496
|
+
* a one-time stderr breadcrumb per request so operators don't
|
|
497
|
+
* have to diff their `policy.consensusQuorum` against
|
|
498
|
+
* `votersAtIssue.size` manually to understand why a quorum
|
|
499
|
+
* resolved sooner than configured. Tracked on `MediatorPending`
|
|
500
|
+
* so the breadcrumb fires once even though `consensusQuorumFor`
|
|
501
|
+
* may be called multiple times per request (vote tally + final
|
|
502
|
+
* resolution).
|
|
503
|
+
*/
|
|
504
|
+
consensusQuorumFor(pending) {
|
|
505
|
+
const m = pending.votersAtIssue.size;
|
|
506
|
+
const override = this.deps.consensusQuorum;
|
|
507
|
+
if (override !== void 0) {
|
|
508
|
+
const capped = Math.min(override, Math.max(m, 1));
|
|
509
|
+
if (capped < override && !pending.consensusQuorumCapNoted) {
|
|
510
|
+
pending.consensusQuorumCapNoted = true;
|
|
511
|
+
try {
|
|
512
|
+
process.stderr.write(
|
|
513
|
+
`permissionMediator: consensusQuorum override ${override} capped to ${capped} (votersAtIssue.size=${m}) for request ${pending.requestId} session ${pending.sessionId}
|
|
514
|
+
`
|
|
515
|
+
);
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return capped;
|
|
520
|
+
}
|
|
521
|
+
return Math.max(1, Math.floor(m / 2) + 1);
|
|
522
|
+
}
|
|
523
|
+
totalTalliedFor(pending) {
|
|
524
|
+
let total = 0;
|
|
525
|
+
for (const set of pending.tallies.values()) total += set.size;
|
|
526
|
+
return total;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* `votesNeeded` = `quorum - max(tally per option)`. When no
|
|
530
|
+
* option has any votes (degenerate; `permission_partial_vote`
|
|
531
|
+
* is only emitted AFTER the first vote, so this should never
|
|
532
|
+
* appear on the wire), returns `quorum` itself. Always ≥ 1
|
|
533
|
+
* because the resolved-on-quorum path returns before this
|
|
534
|
+
* helper runs.
|
|
535
|
+
*/
|
|
536
|
+
votesNeededFor(pending) {
|
|
537
|
+
const quorum = this.consensusQuorumFor(pending);
|
|
538
|
+
let max = 0;
|
|
539
|
+
for (const set of pending.tallies.values()) {
|
|
540
|
+
if (set.size > max) max = set.size;
|
|
541
|
+
}
|
|
542
|
+
return Math.max(quorum - max, 1);
|
|
543
|
+
}
|
|
544
|
+
optionTalliesFor(pending) {
|
|
545
|
+
const out = {};
|
|
546
|
+
for (const [optionId, set] of pending.tallies) {
|
|
547
|
+
out[optionId] = set.size;
|
|
548
|
+
}
|
|
549
|
+
return out;
|
|
550
|
+
}
|
|
551
|
+
// ===========================================================
|
|
552
|
+
// Resolution + cleanup
|
|
553
|
+
// ===========================================================
|
|
554
|
+
/**
|
|
555
|
+
* Settle a pending entry. Cleanup order is hardened (cleanup-order invariant):
|
|
556
|
+
* 1. clearTimeout (so a timer can never fire on a half-cleaned entry).
|
|
557
|
+
* 2. Delete from `pending` (state-first half — entry no longer
|
|
558
|
+
* reachable for new votes).
|
|
559
|
+
* 3. emit wire `permission_resolved` (best-effort — emit failures
|
|
560
|
+
* do not block the Promise settle). MUST come before step 4
|
|
561
|
+
* so a re-entrant subscriber synchronously casting another
|
|
562
|
+
* vote during emit sees `pending === undefined && resolved
|
|
563
|
+
* === undefined` (silent false), matching the previous ordering.
|
|
564
|
+
*
|
|
565
|
+
* 4. write to `resolved` (the second half of state move — late
|
|
566
|
+
* voters arriving after this see `permission_already_resolved`).
|
|
567
|
+
* 5. audit.recordResolved (best-effort, same).
|
|
568
|
+
* 6. Settle the Promise (LAST — callbacks running re-entrantly
|
|
569
|
+
* see consistent state).
|
|
570
|
+
*
|
|
571
|
+
* Previously the spec bundled
|
|
572
|
+
* "delete pending + write resolved" into step 2 ahead of emit,
|
|
573
|
+
* which contradicted the code. The fix
|
|
574
|
+
* splits the two halves of the state move around the emit so
|
|
575
|
+
* the spec faithfully describes the ordering invariant.
|
|
576
|
+
*
|
|
577
|
+
* @param resolverClientId wire compat: the
|
|
578
|
+
* `permission_resolved` SSE frame stamps this as
|
|
579
|
+
* `originatorClientId`. The previous `resolvePending` in
|
|
580
|
+
* `httpAcpBridge.ts:1518-1523` filled it from the voter's
|
|
581
|
+
* trusted clientId. We preserve byte-for-byte; vote-driven
|
|
582
|
+
* paths pass `vote.clientId` (which may be undefined for
|
|
583
|
+
* loopback no-header voters); timer + session-closed paths
|
|
584
|
+
* pass undefined (no voter).
|
|
585
|
+
*/
|
|
586
|
+
resolveEntry(pending, resolution, decisionReason, resolverClientId) {
|
|
587
|
+
if (this.pending.get(pending.requestId) !== pending) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (pending.timer !== void 0) {
|
|
591
|
+
clearTimeout(pending.timer);
|
|
592
|
+
pending.timer = void 0;
|
|
593
|
+
}
|
|
594
|
+
this.pending.delete(pending.requestId);
|
|
595
|
+
this.safeEmit(pending.sessionId, {
|
|
596
|
+
type: "permission_resolved",
|
|
597
|
+
...pending.promptId ? { promptId: pending.promptId } : {},
|
|
598
|
+
data: {
|
|
599
|
+
requestId: pending.requestId,
|
|
600
|
+
outcome: this.toAcpOutcome(resolution),
|
|
601
|
+
// Note: `voterClientId` is the canonical,
|
|
602
|
+
// unambiguous name for "who cast the resolving vote". The envelope
|
|
603
|
+
// `originatorClientId` below carries the SAME value for wire
|
|
604
|
+
// compat (it is semantically the voter on `permission_resolved`,
|
|
605
|
+
// unlike on `permission_request` where it is the prompt originator).
|
|
606
|
+
// Both are optional and omitted together for no-voter resolutions
|
|
607
|
+
// (timer expiry / session-closed / loopback voter with no clientId).
|
|
608
|
+
...resolverClientId !== void 0 ? { voterClientId: resolverClientId } : {}
|
|
609
|
+
},
|
|
610
|
+
// Preserve pre-extraction behavior: voter's clientId is stamped
|
|
611
|
+
// here (not the prompt originator's). Documented inconsistency
|
|
612
|
+
// with `permission_request.originatorClientId` (which IS the
|
|
613
|
+
// prompt originator); we do not fix the inconsistency to
|
|
614
|
+
// avoid breaking the wire shape. A4 keeps it as a deprecated
|
|
615
|
+
// alias of `data.voterClientId`.
|
|
616
|
+
...resolverClientId !== void 0 ? { originatorClientId: resolverClientId } : {}
|
|
617
|
+
});
|
|
618
|
+
this.rememberResolved({
|
|
619
|
+
requestId: pending.requestId,
|
|
620
|
+
sessionId: pending.sessionId,
|
|
621
|
+
promptId: pending.promptId,
|
|
622
|
+
resolution,
|
|
623
|
+
resolverClientId
|
|
624
|
+
});
|
|
625
|
+
if (decisionReason !== void 0) {
|
|
626
|
+
this.safeAudit(
|
|
627
|
+
() => this.deps.audit.recordResolved(
|
|
628
|
+
this.toRecord(pending),
|
|
629
|
+
resolution,
|
|
630
|
+
decisionReason
|
|
631
|
+
)
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
pending.resolve(resolution);
|
|
635
|
+
}
|
|
636
|
+
rememberResolved(record) {
|
|
637
|
+
if (!this.resolved.has(record.requestId)) {
|
|
638
|
+
this.resolvedOrder.push(record.requestId);
|
|
639
|
+
}
|
|
640
|
+
this.resolved.set(record.requestId, record);
|
|
641
|
+
while (this.resolvedOrder.length > MAX_RESOLVED_PERMISSION_RECORDS) {
|
|
642
|
+
const oldest = this.resolvedOrder.shift();
|
|
643
|
+
if (oldest !== void 0) this.resolved.delete(oldest);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
safeEmit(sessionId, event) {
|
|
647
|
+
try {
|
|
648
|
+
this.deps.emit(sessionId, event);
|
|
649
|
+
} catch (err) {
|
|
650
|
+
try {
|
|
651
|
+
process.stderr.write(
|
|
652
|
+
`permissionMediator: emit failed for session=${JSON.stringify(sessionId)} type=${JSON.stringify(event.type)}: ${stringifyError(err)}
|
|
653
|
+
`
|
|
654
|
+
);
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Emit a stderr breadcrumb
|
|
661
|
+
* for every vote rejection (the three forbidden paths in
|
|
662
|
+
* voteDesignated / voteConsensus / voteLocalOnly). Mirrors the
|
|
663
|
+
* timeout breadcrumb pattern: audit ring + SSE event are
|
|
664
|
+
* transient observability surfaces (no v1 query route, SSE drops
|
|
665
|
+
* on disconnect), so an operator tailing daemon stderr would see
|
|
666
|
+
* zero indication of permission rejections without this.
|
|
667
|
+
*
|
|
668
|
+
* Wrapped in `try/catch` because `process.stderr.write` can
|
|
669
|
+
* synchronously throw on EPIPE during shutdown — a stderr
|
|
670
|
+
* unavailability must not propagate up through `safeEmit` /
|
|
671
|
+
* `safeAudit` and break the resolveEntry cleanup ladder. Mirrors
|
|
672
|
+
* the safeEmit/safeAudit defensive posture (see the
|
|
673
|
+
* matching hang scenario in safeEmit).
|
|
674
|
+
*/
|
|
675
|
+
writeForbiddenStderr(pending, vote, reasonDetail) {
|
|
676
|
+
try {
|
|
677
|
+
const voterDescriptor = vote.clientId === void 0 ? "<anonymous>" : JSON.stringify(vote.clientId);
|
|
678
|
+
process.stderr.write(
|
|
679
|
+
`qwen serve: permission ${pending.requestId} (session ${pending.sessionId}): vote rejected (${reasonDetail}) by client ${voterDescriptor}
|
|
680
|
+
`
|
|
681
|
+
);
|
|
682
|
+
} catch {
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Run an audit-publisher call defensively. The audit ring is
|
|
687
|
+
* best-effort observability — a publisher exception (ring full,
|
|
688
|
+
* host bug, transient I/O) MUST NOT throw out of `request()`,
|
|
689
|
+
* `vote()`, or the timer callback. Without this guard, the
|
|
690
|
+
* Promise the agent is awaiting would be left unsettled and the
|
|
691
|
+
* pending entry would leak.
|
|
692
|
+
*
|
|
693
|
+
* Single helper used at all five audit call sites so the
|
|
694
|
+
* "audit is best-effort" invariant is uniformly enforced (the
|
|
695
|
+
* pre-fix asymmetric `try/catch` at 2 of 5 sites was a real
|
|
696
|
+
* silent-failure hole.
|
|
697
|
+
*
|
|
698
|
+
* doc placement — JSDoc was previously
|
|
699
|
+
* stacked above `writeForbiddenStderr` so IDE hover and API
|
|
700
|
+
* doc generation showed the wrong attribution. Moved adjacent
|
|
701
|
+
* to its actual definition.
|
|
702
|
+
*/
|
|
703
|
+
safeAudit(fn) {
|
|
704
|
+
try {
|
|
705
|
+
fn();
|
|
706
|
+
} catch (err) {
|
|
707
|
+
try {
|
|
708
|
+
process.stderr.write(
|
|
709
|
+
`permissionMediator: audit publisher threw: ${stringifyError(err)}
|
|
710
|
+
`
|
|
711
|
+
);
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
toRecord(pending) {
|
|
717
|
+
return {
|
|
718
|
+
requestId: pending.requestId,
|
|
719
|
+
sessionId: pending.sessionId,
|
|
720
|
+
...pending.promptId ? { promptId: pending.promptId } : {},
|
|
721
|
+
originatorClientId: pending.originatorClientId,
|
|
722
|
+
allowedOptionIds: pending.allowedOptionIds,
|
|
723
|
+
issuedAtMs: pending.issuedAtMs
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
toAcpOutcome(resolution) {
|
|
727
|
+
if (resolution.kind === "option") {
|
|
728
|
+
return { outcome: "selected", optionId: resolution.optionId };
|
|
729
|
+
}
|
|
730
|
+
return { outcome: "cancelled" };
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
// packages/acp-bridge/src/internal/stderrLine.ts
|
|
735
|
+
init_esbuild_shims();
|
|
736
|
+
function writeStderrLine(message) {
|
|
737
|
+
process.stderr.write(message.endsWith("\n") ? message : `${message}
|
|
738
|
+
`);
|
|
739
|
+
}
|
|
740
|
+
__name(writeStderrLine, "writeStderrLine");
|
|
741
|
+
|
|
742
|
+
// packages/acp-bridge/src/sessionAttachments.ts
|
|
743
|
+
init_esbuild_shims();
|
|
744
|
+
import { randomUUID } from "node:crypto";
|
|
745
|
+
import { promises as fs, statSync } from "node:fs";
|
|
746
|
+
import { tmpdir } from "node:os";
|
|
747
|
+
import * as path from "node:path";
|
|
748
|
+
var SESSION_ATTACHMENT_MAX_ITEM_BYTES = 8 * 1024 * 1024;
|
|
749
|
+
var SESSION_ATTACHMENT_MAX_NAME_BYTES = 255;
|
|
750
|
+
var SUPPORTED_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
751
|
+
"image/bmp",
|
|
752
|
+
"image/gif",
|
|
753
|
+
"image/jpeg",
|
|
754
|
+
"image/png",
|
|
755
|
+
"image/webp"
|
|
756
|
+
]);
|
|
757
|
+
var SESSION_ATTACHMENT_UNAVAILABLE_TEXT = "[Attachment is no longer available]";
|
|
758
|
+
var SessionAttachmentReferenceError = class extends Error {
|
|
759
|
+
constructor(message, code) {
|
|
760
|
+
super(message);
|
|
761
|
+
this.code = code;
|
|
762
|
+
this.name = "SessionAttachmentReferenceError";
|
|
763
|
+
}
|
|
764
|
+
static {
|
|
765
|
+
__name(this, "SessionAttachmentReferenceError");
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
function isSessionAttachmentReference(value) {
|
|
769
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
770
|
+
const record = value;
|
|
771
|
+
return (record["type"] === "image" || record["type"] === "resource") && typeof record["attachmentId"] === "string" && record["attachmentId"].length > 0 && typeof record["mimeType"] === "string" && record["mimeType"].length > 0 && (record["type"] !== "image" || record["mimeType"].startsWith("image/")) && typeof record["size"] === "number" && Number.isSafeInteger(record["size"]) && record["size"] >= 0 && (record["type"] !== "image" || record["size"] > 0);
|
|
772
|
+
}
|
|
773
|
+
__name(isSessionAttachmentReference, "isSessionAttachmentReference");
|
|
774
|
+
function safeAttachmentName(name) {
|
|
775
|
+
const safeName = path.basename(name.replaceAll("\\", "/")).trim();
|
|
776
|
+
const isWindowsReserved = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(safeName);
|
|
777
|
+
const hasInvalidCharacter = Array.from(safeName).some((character) => {
|
|
778
|
+
const codePoint = character.codePointAt(0);
|
|
779
|
+
return codePoint !== void 0 && (codePoint <= 31 || codePoint === 127 || codePoint >= 55296 && codePoint <= 57343);
|
|
780
|
+
});
|
|
781
|
+
return !safeName || safeName === "." || safeName === ".." || safeName.endsWith(".") || isWindowsReserved || /[<>:"|?*]/.test(safeName) || hasInvalidCharacter || Buffer.byteLength(safeName) > SESSION_ATTACHMENT_MAX_NAME_BYTES ? void 0 : safeName;
|
|
782
|
+
}
|
|
783
|
+
__name(safeAttachmentName, "safeAttachmentName");
|
|
784
|
+
function truncateUtf8(value, maxBytes) {
|
|
785
|
+
let bytes = 0;
|
|
786
|
+
let result = "";
|
|
787
|
+
for (const character of value) {
|
|
788
|
+
const characterBytes = Buffer.byteLength(character);
|
|
789
|
+
if (bytes + characterBytes > maxBytes) break;
|
|
790
|
+
result += character;
|
|
791
|
+
bytes += characterBytes;
|
|
792
|
+
}
|
|
793
|
+
return result;
|
|
794
|
+
}
|
|
795
|
+
__name(truncateUtf8, "truncateUtf8");
|
|
796
|
+
function deduplicatedName(name, suffix) {
|
|
797
|
+
if (suffix === 0) return name;
|
|
798
|
+
const extension = path.extname(name);
|
|
799
|
+
const suffixText = ` (${suffix})`;
|
|
800
|
+
const stem = name.slice(0, -extension.length || void 0);
|
|
801
|
+
const extensionBudget = SESSION_ATTACHMENT_MAX_NAME_BYTES - Buffer.byteLength(suffixText) - 1;
|
|
802
|
+
const safeExtension = truncateUtf8(extension, extensionBudget).replace(
|
|
803
|
+
/[. ]+$/u,
|
|
804
|
+
""
|
|
805
|
+
);
|
|
806
|
+
const stemBudget = SESSION_ATTACHMENT_MAX_NAME_BYTES - Buffer.byteLength(suffixText) - Buffer.byteLength(safeExtension);
|
|
807
|
+
return `${truncateUtf8(stem, stemBudget)}${suffixText}${safeExtension}`;
|
|
808
|
+
}
|
|
809
|
+
__name(deduplicatedName, "deduplicatedName");
|
|
810
|
+
function imageName(mimeType) {
|
|
811
|
+
const extension = mimeType.slice("image/".length).split(/[;+]/, 1)[0];
|
|
812
|
+
return `image.${extension === "jpg" ? "jpeg" : extension || "img"}`;
|
|
813
|
+
}
|
|
814
|
+
__name(imageName, "imageName");
|
|
815
|
+
function mimeTypeForName(name) {
|
|
816
|
+
if ([".ts", ".mts", ".cts", ".tsx"].includes(path.extname(name).toLowerCase())) {
|
|
817
|
+
return "text/plain";
|
|
818
|
+
}
|
|
819
|
+
return getSpecificMimeType(name) ?? "application/octet-stream";
|
|
820
|
+
}
|
|
821
|
+
__name(mimeTypeForName, "mimeTypeForName");
|
|
822
|
+
function isSupportedImageMimeType(mimeType) {
|
|
823
|
+
return SUPPORTED_IMAGE_MIME_TYPES.has(mimeType);
|
|
824
|
+
}
|
|
825
|
+
__name(isSupportedImageMimeType, "isSupportedImageMimeType");
|
|
826
|
+
function isTextMimeType(mimeType) {
|
|
827
|
+
return mimeType.startsWith("text/") || mimeType === "application/json" || mimeType.endsWith("+json") || mimeType === "application/xml" || mimeType.endsWith("+xml") || mimeType === "application/javascript" || mimeType === "application/typescript" || mimeType === "application/yaml" || mimeType === "application/x-yaml" || mimeType === "application/toml";
|
|
828
|
+
}
|
|
829
|
+
__name(isTextMimeType, "isTextMimeType");
|
|
830
|
+
function isTextAttachment(data, mimeType) {
|
|
831
|
+
if (isTextMimeType(mimeType)) return true;
|
|
832
|
+
if (mimeType.startsWith("image/") || mimeType.startsWith("audio/") || mimeType.startsWith("video/") || mimeType.startsWith("font/") || mimeType === "application/pdf" || data.includes(0)) {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
return Buffer.from(data.toString("utf8"), "utf8").equals(data);
|
|
836
|
+
}
|
|
837
|
+
__name(isTextAttachment, "isTextAttachment");
|
|
838
|
+
function withAttachmentDegradationMarker(blocks) {
|
|
839
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
840
|
+
const block = blocks[i];
|
|
841
|
+
if (block.type === "text") {
|
|
842
|
+
if (block.text.endsWith(SESSION_ATTACHMENT_UNAVAILABLE_TEXT)) {
|
|
843
|
+
return [...blocks];
|
|
844
|
+
}
|
|
845
|
+
const next = [...blocks];
|
|
846
|
+
next[i] = {
|
|
847
|
+
type: "text",
|
|
848
|
+
text: `${block.text}
|
|
849
|
+
${SESSION_ATTACHMENT_UNAVAILABLE_TEXT}`
|
|
850
|
+
};
|
|
851
|
+
return next;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return [
|
|
855
|
+
...blocks,
|
|
856
|
+
{ type: "text", text: SESSION_ATTACHMENT_UNAVAILABLE_TEXT }
|
|
857
|
+
];
|
|
858
|
+
}
|
|
859
|
+
__name(withAttachmentDegradationMarker, "withAttachmentDegradationMarker");
|
|
860
|
+
var SessionAttachmentStore = class {
|
|
861
|
+
constructor(directoryRoot, sessionId) {
|
|
862
|
+
this.directoryRoot = directoryRoot;
|
|
863
|
+
if (!directoryRoot || !sessionId) return;
|
|
864
|
+
this.persistentDirectory = path.join(
|
|
865
|
+
directoryRoot,
|
|
866
|
+
`session-${encodeURIComponent(sessionId)}`
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
static {
|
|
870
|
+
__name(this, "SessionAttachmentStore");
|
|
871
|
+
}
|
|
872
|
+
directoryPromise;
|
|
873
|
+
persistentDirectory;
|
|
874
|
+
activeDirectory;
|
|
875
|
+
pendingItems = 0;
|
|
876
|
+
pendingNames = /* @__PURE__ */ new Map();
|
|
877
|
+
pendingDrainWaiters = [];
|
|
878
|
+
copyDrainWaiters = [];
|
|
879
|
+
copying = false;
|
|
880
|
+
closing = false;
|
|
881
|
+
closed = false;
|
|
882
|
+
async putAttachment(data, mimeType, name) {
|
|
883
|
+
const isImage = isSupportedImageMimeType(mimeType);
|
|
884
|
+
const safeName = safeAttachmentName(
|
|
885
|
+
name ?? (isImage ? imageName(mimeType) : "")
|
|
886
|
+
);
|
|
887
|
+
if (!safeName) {
|
|
888
|
+
throw new TypeError("Session attachment name is invalid");
|
|
889
|
+
}
|
|
890
|
+
const storedMimeType = mimeTypeForName(safeName);
|
|
891
|
+
if (isImage && storedMimeType !== mimeType || isSupportedImageMimeType(storedMimeType) && !isImage) {
|
|
892
|
+
throw new TypeError("Attachment name and Content-Type do not match");
|
|
893
|
+
}
|
|
894
|
+
if (this.closed || this.closing) {
|
|
895
|
+
throw new Error("Session attachment store is closed");
|
|
896
|
+
}
|
|
897
|
+
if (this.copying) throw new Error("Session attachments are being copied");
|
|
898
|
+
if (isImage && data.byteLength === 0 || data.byteLength > SESSION_ATTACHMENT_MAX_ITEM_BYTES) {
|
|
899
|
+
throw new RangeError(
|
|
900
|
+
`Session attachment must be at most ${SESSION_ATTACHMENT_MAX_ITEM_BYTES} bytes and images cannot be empty`
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
let filePath;
|
|
904
|
+
let pendingName = safeName;
|
|
905
|
+
let removeFileOnFailure = false;
|
|
906
|
+
this.pendingItems += 1;
|
|
907
|
+
this.reservePendingName(safeName);
|
|
908
|
+
try {
|
|
909
|
+
const directory = await this.directory();
|
|
910
|
+
let suffix = 0;
|
|
911
|
+
for (; ; ) {
|
|
912
|
+
const candidateName = deduplicatedName(safeName, suffix);
|
|
913
|
+
if (safeAttachmentName(candidateName) !== candidateName) {
|
|
914
|
+
throw new TypeError("Session attachment name is invalid");
|
|
915
|
+
}
|
|
916
|
+
if (pendingName !== candidateName) {
|
|
917
|
+
if (pendingName) this.releasePendingName(pendingName);
|
|
918
|
+
this.reservePendingName(candidateName);
|
|
919
|
+
pendingName = candidateName;
|
|
920
|
+
}
|
|
921
|
+
filePath = path.join(directory, candidateName);
|
|
922
|
+
removeFileOnFailure = true;
|
|
923
|
+
try {
|
|
924
|
+
await fs.writeFile(filePath, data, { flag: "wx" });
|
|
925
|
+
break;
|
|
926
|
+
} catch (error) {
|
|
927
|
+
if (error.code !== "EEXIST") throw error;
|
|
928
|
+
removeFileOnFailure = false;
|
|
929
|
+
this.releasePendingName(candidateName);
|
|
930
|
+
pendingName = void 0;
|
|
931
|
+
filePath = void 0;
|
|
932
|
+
suffix += 1;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (this.closed || this.closing) {
|
|
936
|
+
throw new Error("Session attachment store is closed");
|
|
937
|
+
}
|
|
938
|
+
const name2 = path.basename(filePath);
|
|
939
|
+
const storedMimeType2 = mimeTypeForName(name2);
|
|
940
|
+
const reference = {
|
|
941
|
+
type: isSupportedImageMimeType(storedMimeType2) ? "image" : "resource",
|
|
942
|
+
attachmentId: name2,
|
|
943
|
+
mimeType: storedMimeType2,
|
|
944
|
+
size: data.byteLength
|
|
945
|
+
};
|
|
946
|
+
return reference;
|
|
947
|
+
} catch (error) {
|
|
948
|
+
if (removeFileOnFailure && filePath) {
|
|
949
|
+
await fs.rm(filePath, { force: true }).catch(() => {
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
throw error;
|
|
953
|
+
} finally {
|
|
954
|
+
if (pendingName) this.releasePendingName(pendingName);
|
|
955
|
+
if (!this.closed) {
|
|
956
|
+
this.pendingItems -= 1;
|
|
957
|
+
if (this.pendingItems === 0) {
|
|
958
|
+
this.resolvePendingDrainWaiters();
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
// Validate one block against the store. Ordinary ACP content passes through
|
|
964
|
+
// untouched, matching `assertReferences`.
|
|
965
|
+
assertReference(block) {
|
|
966
|
+
if (!block || typeof block !== "object" || Array.isArray(block) || !("attachmentId" in block)) {
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
if (!isSessionAttachmentReference(block)) {
|
|
970
|
+
throw new SessionAttachmentReferenceError(
|
|
971
|
+
"Invalid session attachment reference",
|
|
972
|
+
"invalid_session_attachment_reference"
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
this.assertStored(block);
|
|
976
|
+
}
|
|
977
|
+
assertReferences(content) {
|
|
978
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
979
|
+
for (const block of content) {
|
|
980
|
+
if (!block || typeof block !== "object" || Array.isArray(block) || !("attachmentId" in block)) {
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
if (!isSessionAttachmentReference(block)) {
|
|
984
|
+
throw new SessionAttachmentReferenceError(
|
|
985
|
+
"Invalid session attachment reference",
|
|
986
|
+
"invalid_session_attachment_reference"
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
const id = block.attachmentId;
|
|
990
|
+
if (seenIds.has(id)) {
|
|
991
|
+
throw new SessionAttachmentReferenceError(
|
|
992
|
+
`Session attachment referenced more than once: ${id}`,
|
|
993
|
+
"invalid_session_attachment_reference"
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
seenIds.add(id);
|
|
997
|
+
this.assertStored(block);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
async resolveContent(content, memo) {
|
|
1001
|
+
const pendingById = memo ?? /* @__PURE__ */ new Map();
|
|
1002
|
+
return await Promise.all(
|
|
1003
|
+
content.map(async (block) => {
|
|
1004
|
+
if (!isSessionAttachmentReference(block)) return block;
|
|
1005
|
+
const id = block.attachmentId;
|
|
1006
|
+
let pending = pendingById.get(id);
|
|
1007
|
+
if (!pending) {
|
|
1008
|
+
const created = this.resolve(block);
|
|
1009
|
+
pendingById.set(id, created);
|
|
1010
|
+
void created.catch(() => {
|
|
1011
|
+
if (pendingById.get(id) === created) {
|
|
1012
|
+
pendingById.delete(id);
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
pending = created;
|
|
1016
|
+
}
|
|
1017
|
+
return await pending;
|
|
1018
|
+
})
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
// Per-block variant of `resolveContent` for degrade paths: one unresolvable
|
|
1022
|
+
// reference drops only itself, keeping the sibling blocks a wholesale
|
|
1023
|
+
// fallback would discard. Other errors still propagate.
|
|
1024
|
+
async resolveContentDegrading(content, memo) {
|
|
1025
|
+
const retainedBlocks = [];
|
|
1026
|
+
const resolvedBlocks = [];
|
|
1027
|
+
let degraded = 0;
|
|
1028
|
+
for (const block of content) {
|
|
1029
|
+
if (!isSessionAttachmentReference(block)) {
|
|
1030
|
+
retainedBlocks.push(block);
|
|
1031
|
+
resolvedBlocks.push(block);
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
try {
|
|
1035
|
+
const [resolved] = await this.resolveContent([block], memo);
|
|
1036
|
+
if (resolved) resolvedBlocks.push(resolved);
|
|
1037
|
+
retainedBlocks.push(block);
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
if (!(error instanceof SessionAttachmentReferenceError)) throw error;
|
|
1040
|
+
degraded += 1;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return { retainedBlocks, resolvedBlocks, degraded };
|
|
1044
|
+
}
|
|
1045
|
+
async read(attachmentId) {
|
|
1046
|
+
const name = safeAttachmentName(attachmentId);
|
|
1047
|
+
if (!name || name !== attachmentId) return void 0;
|
|
1048
|
+
const filePath = path.join(await this.directory(), name);
|
|
1049
|
+
try {
|
|
1050
|
+
return {
|
|
1051
|
+
data: await fs.readFile(filePath),
|
|
1052
|
+
mimeType: mimeTypeForName(name)
|
|
1053
|
+
};
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
if (error.code === "ENOENT") {
|
|
1056
|
+
return void 0;
|
|
1057
|
+
}
|
|
1058
|
+
throw error;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
async copyFrom(source) {
|
|
1062
|
+
if (source === this) return;
|
|
1063
|
+
if (this.closed || this.closing) {
|
|
1064
|
+
throw new Error("Session attachment store is closed");
|
|
1065
|
+
}
|
|
1066
|
+
if (this.copying) throw new Error("Session attachments are being copied");
|
|
1067
|
+
if (source.closed || source.closing) {
|
|
1068
|
+
throw new Error("Session attachment store is closed");
|
|
1069
|
+
}
|
|
1070
|
+
if (source.copying) {
|
|
1071
|
+
throw new Error("Session attachments are being copied");
|
|
1072
|
+
}
|
|
1073
|
+
this.copying = true;
|
|
1074
|
+
source.copying = true;
|
|
1075
|
+
try {
|
|
1076
|
+
if (this.pendingItems > 0) {
|
|
1077
|
+
await new Promise(
|
|
1078
|
+
(resolve2) => this.pendingDrainWaiters.push(resolve2)
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
if (source.pendingItems > 0) {
|
|
1082
|
+
await new Promise(
|
|
1083
|
+
(resolve2) => source.pendingDrainWaiters.push(resolve2)
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
if (this.closed) throw new Error("Session attachment store is closed");
|
|
1087
|
+
const sourceDirectory = source.persistentDirectory ?? source.activeDirectory;
|
|
1088
|
+
if (!sourceDirectory) return;
|
|
1089
|
+
let entries;
|
|
1090
|
+
try {
|
|
1091
|
+
entries = await fs.readdir(sourceDirectory, { withFileTypes: true });
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
if (error.code === "ENOENT") return;
|
|
1094
|
+
throw error;
|
|
1095
|
+
}
|
|
1096
|
+
const targetDirectory = await this.directory();
|
|
1097
|
+
await Promise.all(
|
|
1098
|
+
entries.filter(
|
|
1099
|
+
(entry) => entry.isFile() && !source.pendingNames.has(entry.name)
|
|
1100
|
+
).map(async (entry) => {
|
|
1101
|
+
const sourcePath = path.join(sourceDirectory, entry.name);
|
|
1102
|
+
try {
|
|
1103
|
+
await fs.copyFile(
|
|
1104
|
+
sourcePath,
|
|
1105
|
+
path.join(targetDirectory, entry.name)
|
|
1106
|
+
);
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
if (error.code === "ENOENT") {
|
|
1109
|
+
try {
|
|
1110
|
+
await fs.stat(sourcePath);
|
|
1111
|
+
} catch (sourceError) {
|
|
1112
|
+
if (sourceError.code === "ENOENT") {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
throw error;
|
|
1118
|
+
}
|
|
1119
|
+
})
|
|
1120
|
+
);
|
|
1121
|
+
} finally {
|
|
1122
|
+
source.copying = false;
|
|
1123
|
+
this.copying = false;
|
|
1124
|
+
source.resolveCopyDrainWaiters();
|
|
1125
|
+
this.resolveCopyDrainWaiters();
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
async remove(attachmentId) {
|
|
1129
|
+
const name = safeAttachmentName(attachmentId);
|
|
1130
|
+
if (!name || name !== attachmentId || this.copying || this.pendingNames.has(name)) {
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
const directory = await this.directory();
|
|
1134
|
+
const filePath = path.join(directory, name);
|
|
1135
|
+
try {
|
|
1136
|
+
await fs.unlink(filePath);
|
|
1137
|
+
return true;
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
if (error.code === "ENOENT") {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
throw error;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
async close() {
|
|
1146
|
+
this.closing = true;
|
|
1147
|
+
await this.waitForCopy();
|
|
1148
|
+
if (this.closed) return;
|
|
1149
|
+
this.closed = true;
|
|
1150
|
+
this.pendingItems = 0;
|
|
1151
|
+
this.pendingNames.clear();
|
|
1152
|
+
this.resolvePendingDrainWaiters();
|
|
1153
|
+
if (this.persistentDirectory || !this.directoryPromise) return;
|
|
1154
|
+
const directory = await this.directoryPromise.catch(() => void 0);
|
|
1155
|
+
if (!directory) return;
|
|
1156
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
1157
|
+
}
|
|
1158
|
+
async delete(options = {}) {
|
|
1159
|
+
options.assertCanCommit?.();
|
|
1160
|
+
this.closing = true;
|
|
1161
|
+
await this.waitForCopy();
|
|
1162
|
+
options.assertCanCommit?.();
|
|
1163
|
+
if (!this.closed) {
|
|
1164
|
+
this.closed = true;
|
|
1165
|
+
this.pendingItems = 0;
|
|
1166
|
+
this.pendingNames.clear();
|
|
1167
|
+
this.resolvePendingDrainWaiters();
|
|
1168
|
+
}
|
|
1169
|
+
const directory = this.persistentDirectory ?? await this.directoryPromise?.catch(() => void 0);
|
|
1170
|
+
if (directory) {
|
|
1171
|
+
options.assertCanCommit?.();
|
|
1172
|
+
const tombstone = path.join(
|
|
1173
|
+
path.dirname(directory),
|
|
1174
|
+
`.${path.basename(directory)}.deleting-${randomUUID()}`
|
|
1175
|
+
);
|
|
1176
|
+
try {
|
|
1177
|
+
await fs.rename(directory, tombstone);
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
if (error.code === "ENOENT") return;
|
|
1180
|
+
throw error;
|
|
1181
|
+
}
|
|
1182
|
+
await fs.rm(tombstone, { recursive: true, force: true });
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
assertStored(reference) {
|
|
1186
|
+
const id = reference.attachmentId;
|
|
1187
|
+
const name = safeAttachmentName(id);
|
|
1188
|
+
let size;
|
|
1189
|
+
const directory = this.persistentDirectory ?? this.activeDirectory;
|
|
1190
|
+
if (name && name === id && directory) {
|
|
1191
|
+
try {
|
|
1192
|
+
size = statSync(path.join(directory, name)).size;
|
|
1193
|
+
} catch {
|
|
1194
|
+
size = void 0;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
const storedMimeType = name ? mimeTypeForName(name) : void 0;
|
|
1198
|
+
const storedType = storedMimeType ? isSupportedImageMimeType(storedMimeType) ? "image" : "resource" : void 0;
|
|
1199
|
+
if (size !== reference.size || storedMimeType !== reference.mimeType || storedType !== reference.type) {
|
|
1200
|
+
throw new SessionAttachmentReferenceError(
|
|
1201
|
+
`Unknown or unavailable session attachment: ${id}`,
|
|
1202
|
+
"session_attachment_gone"
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
releasePendingName(name) {
|
|
1207
|
+
const count = this.pendingNames.get(name) ?? 0;
|
|
1208
|
+
if (count <= 1) this.pendingNames.delete(name);
|
|
1209
|
+
else this.pendingNames.set(name, count - 1);
|
|
1210
|
+
}
|
|
1211
|
+
reservePendingName(name) {
|
|
1212
|
+
this.pendingNames.set(name, (this.pendingNames.get(name) ?? 0) + 1);
|
|
1213
|
+
}
|
|
1214
|
+
async waitForCopy() {
|
|
1215
|
+
while (this.copying) {
|
|
1216
|
+
await new Promise((resolve2) => this.copyDrainWaiters.push(resolve2));
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
resolveCopyDrainWaiters() {
|
|
1220
|
+
for (const resolve2 of this.copyDrainWaiters.splice(0)) resolve2();
|
|
1221
|
+
}
|
|
1222
|
+
resolvePendingDrainWaiters() {
|
|
1223
|
+
for (const resolve2 of this.pendingDrainWaiters.splice(0)) resolve2();
|
|
1224
|
+
}
|
|
1225
|
+
async resolve(reference) {
|
|
1226
|
+
const id = reference.attachmentId;
|
|
1227
|
+
const attachment = await this.read(id);
|
|
1228
|
+
if (!attachment) {
|
|
1229
|
+
throw new SessionAttachmentReferenceError(
|
|
1230
|
+
`Unknown or unavailable session attachment: ${id}`,
|
|
1231
|
+
"session_attachment_gone"
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
if (reference.type === "resource") {
|
|
1235
|
+
const resource = {
|
|
1236
|
+
uri: `attachment:///${encodeURIComponent(reference.attachmentId)}`,
|
|
1237
|
+
mimeType: attachment.mimeType,
|
|
1238
|
+
...isTextAttachment(attachment.data, attachment.mimeType) ? { text: attachment.data.toString("utf8") } : { blob: attachment.data.toString("base64") }
|
|
1239
|
+
};
|
|
1240
|
+
return {
|
|
1241
|
+
type: "resource",
|
|
1242
|
+
resource
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
return {
|
|
1246
|
+
type: "image",
|
|
1247
|
+
data: attachment.data.toString("base64"),
|
|
1248
|
+
mimeType: attachment.mimeType
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
async directory() {
|
|
1252
|
+
if (!this.directoryPromise) {
|
|
1253
|
+
const pending = this.persistentDirectory ? fs.mkdir(this.persistentDirectory, {
|
|
1254
|
+
recursive: true,
|
|
1255
|
+
mode: 448
|
|
1256
|
+
}).then(() => this.persistentDirectory) : this.directoryRoot ? fs.mkdir(this.directoryRoot, { recursive: true, mode: 448 }).then(
|
|
1257
|
+
() => fs.mkdtemp(
|
|
1258
|
+
path.join(this.directoryRoot, "session-attachment-")
|
|
1259
|
+
)
|
|
1260
|
+
) : fs.mkdtemp(path.join(tmpdir(), "qwen-session-attachment-"));
|
|
1261
|
+
const directoryPromise = pending.then((directory) => {
|
|
1262
|
+
this.activeDirectory = directory;
|
|
1263
|
+
return directory;
|
|
1264
|
+
});
|
|
1265
|
+
this.directoryPromise = directoryPromise;
|
|
1266
|
+
void directoryPromise.catch(() => {
|
|
1267
|
+
if (this.directoryPromise === directoryPromise)
|
|
1268
|
+
this.directoryPromise = void 0;
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
return await this.directoryPromise;
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
// packages/acp-bridge/src/bridgeClient.ts
|
|
1276
|
+
init_esbuild_shims();
|
|
1277
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1278
|
+
import { promises as fs2 } from "node:fs";
|
|
1279
|
+
import * as path2 from "node:path";
|
|
1280
|
+
|
|
1281
|
+
// packages/acp-bridge/src/daemonEventTypes.ts
|
|
1282
|
+
init_esbuild_shims();
|
|
1283
|
+
var MID_TURN_MESSAGE_INJECTED_EVENT = "mid_turn_message_injected";
|
|
1284
|
+
|
|
1285
|
+
// packages/acp-bridge/src/bridgeClient.ts
|
|
1286
|
+
var MAX_SCHEDULED_TASK_CRON_CHARS = 200;
|
|
1287
|
+
var MAX_SCHEDULED_TASK_PROMPT_CHARS = 1e5;
|
|
1288
|
+
function parseActiveWorkSnapshot(params) {
|
|
1289
|
+
const seq = params["seq"];
|
|
1290
|
+
const sessions = params["sessions"];
|
|
1291
|
+
if (params["v"] !== ACTIVE_WORK_HEARTBEAT_VERSION || typeof seq !== "number" || !Number.isSafeInteger(seq) || seq <= 0 || !Array.isArray(sessions) || sessions.length > ACTIVE_WORK_MAX_SNAPSHOT_SESSIONS) {
|
|
1292
|
+
return void 0;
|
|
1293
|
+
}
|
|
1294
|
+
const parsed = [];
|
|
1295
|
+
for (const raw of sessions) {
|
|
1296
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
1297
|
+
const entry = raw;
|
|
1298
|
+
const sessionId = entry["sessionId"];
|
|
1299
|
+
const holds = entry["holds"];
|
|
1300
|
+
if (typeof sessionId !== "string" || !Array.isArray(holds) || holds.length > ACTIVE_WORK_MAX_SESSION_HOLDS) {
|
|
1301
|
+
return void 0;
|
|
1302
|
+
}
|
|
1303
|
+
const parsedHolds = [];
|
|
1304
|
+
for (const rawHold of holds) {
|
|
1305
|
+
if (typeof rawHold !== "object" || rawHold === null) return void 0;
|
|
1306
|
+
const hold = rawHold;
|
|
1307
|
+
const category = hold["category"];
|
|
1308
|
+
const id = hold["id"];
|
|
1309
|
+
if (typeof id !== "string" || typeof category !== "string" || !ACTIVE_WORK_HOLD_CATEGORIES.includes(
|
|
1310
|
+
category
|
|
1311
|
+
)) {
|
|
1312
|
+
return void 0;
|
|
1313
|
+
}
|
|
1314
|
+
parsedHolds.push({
|
|
1315
|
+
category,
|
|
1316
|
+
id
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
parsed.push({ sessionId, holds: parsedHolds });
|
|
1320
|
+
}
|
|
1321
|
+
return { v: ACTIVE_WORK_HEARTBEAT_VERSION, seq, sessions: parsed };
|
|
1322
|
+
}
|
|
1323
|
+
__name(parseActiveWorkSnapshot, "parseActiveWorkSnapshot");
|
|
1324
|
+
var PUBLISH_ARTIFACT_TOOL_NAME = "artifact";
|
|
1325
|
+
var MAX_CHANNEL_DELIVERY_TEXT_CHARS = 1e5;
|
|
1326
|
+
var MAX_CHANNEL_DELIVERY_FIELD_CHARS = 2048;
|
|
1327
|
+
var MAX_CHANNEL_DELIVERY_ERROR_CHARS = 500;
|
|
1328
|
+
function isFsErrorShape(err) {
|
|
1329
|
+
return err instanceof Error && err.name === "FsError" && typeof err.kind === "string";
|
|
1330
|
+
}
|
|
1331
|
+
__name(isFsErrorShape, "isFsErrorShape");
|
|
1332
|
+
function isExistingSessionScheduledTaskCreateErrorShape(err) {
|
|
1333
|
+
if (!(err instanceof Error)) return false;
|
|
1334
|
+
const status = err.status;
|
|
1335
|
+
const code = err.code;
|
|
1336
|
+
return err.name === "ExistingSessionScheduledTaskCreateError" && typeof status === "number" && Number.isFinite(status) && typeof code === "string" && code.length > 0;
|
|
1337
|
+
}
|
|
1338
|
+
__name(isExistingSessionScheduledTaskCreateErrorShape, "isExistingSessionScheduledTaskCreateErrorShape");
|
|
1339
|
+
function isRecord(value) {
|
|
1340
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1341
|
+
}
|
|
1342
|
+
__name(isRecord, "isRecord");
|
|
1343
|
+
function normalizeExternalToolGuardResult(value) {
|
|
1344
|
+
if (!isRecord(value)) {
|
|
1345
|
+
throw new Error("External tool guard handler returned an invalid result.");
|
|
1346
|
+
}
|
|
1347
|
+
const keys = Object.keys(value);
|
|
1348
|
+
if (value["allowed"] === true && keys.length === 1 && keys[0] === "allowed") {
|
|
1349
|
+
return { allowed: true };
|
|
1350
|
+
}
|
|
1351
|
+
if (value["allowed"] !== false || keys.some((key) => key !== "allowed" && key !== "reason")) {
|
|
1352
|
+
throw new Error("External tool guard handler returned an invalid result.");
|
|
1353
|
+
}
|
|
1354
|
+
if (!Object.hasOwn(value, "reason")) return { allowed: false };
|
|
1355
|
+
const reason = value["reason"];
|
|
1356
|
+
if (!isValidExternalToolGuardDenialReason(reason)) {
|
|
1357
|
+
throw new Error("External tool guard handler returned an invalid result.");
|
|
1358
|
+
}
|
|
1359
|
+
return { allowed: false, reason };
|
|
1360
|
+
}
|
|
1361
|
+
__name(normalizeExternalToolGuardResult, "normalizeExternalToolGuardResult");
|
|
1362
|
+
function isBoundedChannelDeliveryString(value) {
|
|
1363
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= MAX_CHANNEL_DELIVERY_FIELD_CHARS;
|
|
1364
|
+
}
|
|
1365
|
+
__name(isBoundedChannelDeliveryString, "isBoundedChannelDeliveryString");
|
|
1366
|
+
function isChannelDeliveryTarget(value) {
|
|
1367
|
+
if (!isRecord(value)) return false;
|
|
1368
|
+
return isBoundedChannelDeliveryString(value["channelName"]) && (value["type"] === "user" || value["type"] === "chat") && isBoundedChannelDeliveryString(value["id"]) && Object.keys(value).every(
|
|
1369
|
+
(key) => key === "channelName" || key === "type" || key === "id"
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
__name(isChannelDeliveryTarget, "isChannelDeliveryTarget");
|
|
1373
|
+
function normalizeChannelDeliveryHostResult(value) {
|
|
1374
|
+
if (value.status === "delivered") return { status: "delivered" };
|
|
1375
|
+
if (value.status === "skipped") return { status: "skipped" };
|
|
1376
|
+
const code = CHANNEL_DELIVERY_ERROR_CODES.has(value.code) ? value.code : "channel_delivery_failed";
|
|
1377
|
+
const error = typeof value.error === "string" && value.error.length > 0 ? value.error.slice(0, MAX_CHANNEL_DELIVERY_ERROR_CHARS) : "Channel delivery failed.";
|
|
1378
|
+
return { status: "failed", code, error };
|
|
1379
|
+
}
|
|
1380
|
+
__name(normalizeChannelDeliveryHostResult, "normalizeChannelDeliveryHostResult");
|
|
1381
|
+
function pendingInteractionOptions(options) {
|
|
1382
|
+
return options.map((option) => ({
|
|
1383
|
+
optionId: String(option.optionId ?? ""),
|
|
1384
|
+
...typeof option.name === "string" ? { label: option.name } : {},
|
|
1385
|
+
...typeof option.kind === "string" ? { kind: option.kind } : {}
|
|
1386
|
+
}));
|
|
1387
|
+
}
|
|
1388
|
+
__name(pendingInteractionOptions, "pendingInteractionOptions");
|
|
1389
|
+
function pendingInteractionFromRequest(requestId, params) {
|
|
1390
|
+
const toolCall = params.toolCall;
|
|
1391
|
+
const meta = isRecord(toolCall["_meta"]) ? toolCall["_meta"] : void 0;
|
|
1392
|
+
const rawInput = toolCall["rawInput"];
|
|
1393
|
+
const options = pendingInteractionOptions(
|
|
1394
|
+
Array.isArray(params.options) ? params.options : []
|
|
1395
|
+
);
|
|
1396
|
+
const isUserQuestion = meta?.["qwenInteractionKind"] === "user_question";
|
|
1397
|
+
if (isUserQuestion) {
|
|
1398
|
+
const rawQuestions = Array.isArray(meta?.["qwenQuestions"]) ? meta["qwenQuestions"] : isRecord(rawInput) && Array.isArray(rawInput["questions"]) ? rawInput["questions"] : [];
|
|
1399
|
+
return {
|
|
1400
|
+
requestId,
|
|
1401
|
+
kind: "user_question",
|
|
1402
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1403
|
+
...typeof toolCall["title"] === "string" ? { title: toolCall["title"] } : {},
|
|
1404
|
+
questions: rawQuestions.flatMap(
|
|
1405
|
+
(question, index) => isRecord(question) ? [{ ...question, answerKey: String(index) }] : []
|
|
1406
|
+
),
|
|
1407
|
+
options
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
return {
|
|
1411
|
+
requestId,
|
|
1412
|
+
kind: "permission",
|
|
1413
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1414
|
+
action: {
|
|
1415
|
+
...typeof toolCall["kind"] === "string" ? { type: toolCall["kind"] } : {},
|
|
1416
|
+
...typeof toolCall["title"] === "string" ? { title: toolCall["title"] } : {},
|
|
1417
|
+
...toolCall["content"] !== void 0 ? { content: toolCall["content"] } : {},
|
|
1418
|
+
...toolCall["locations"] !== void 0 ? { locations: toolCall["locations"] } : {},
|
|
1419
|
+
...rawInput !== void 0 ? { input: rawInput } : {}
|
|
1420
|
+
},
|
|
1421
|
+
options
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
__name(pendingInteractionFromRequest, "pendingInteractionFromRequest");
|
|
1425
|
+
function fallbackPendingPermissionInteraction(requestId, options) {
|
|
1426
|
+
return {
|
|
1427
|
+
requestId,
|
|
1428
|
+
kind: "permission",
|
|
1429
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1430
|
+
action: {},
|
|
1431
|
+
options: pendingInteractionOptions(options)
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
__name(fallbackPendingPermissionInteraction, "fallbackPendingPermissionInteraction");
|
|
1435
|
+
function artifactPayloadFields(artifact) {
|
|
1436
|
+
return {
|
|
1437
|
+
title: artifact["title"],
|
|
1438
|
+
kind: artifact["kind"],
|
|
1439
|
+
storage: artifact["storage"],
|
|
1440
|
+
description: artifact["description"],
|
|
1441
|
+
workspacePath: artifact["workspacePath"],
|
|
1442
|
+
managedId: artifact["managedId"],
|
|
1443
|
+
url: artifact["url"],
|
|
1444
|
+
mimeType: artifact["mimeType"],
|
|
1445
|
+
sizeBytes: artifact["sizeBytes"],
|
|
1446
|
+
metadata: artifact["metadata"],
|
|
1447
|
+
retention: artifact["retention"]
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
__name(artifactPayloadFields, "artifactPayloadFields");
|
|
1451
|
+
function extractCappedArtifactInputs(rawArtifacts, limit, sessionId, source, toInput) {
|
|
1452
|
+
const artifacts = [];
|
|
1453
|
+
for (let index = 0; index < rawArtifacts.length; index++) {
|
|
1454
|
+
const artifact = rawArtifacts[index];
|
|
1455
|
+
if (!isRecord(artifact)) {
|
|
1456
|
+
writeStderrLine(
|
|
1457
|
+
`[artifacts] session=${sessionId} action=dropped reason=malformed source=${source} index=${index}`
|
|
1458
|
+
);
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
if (artifacts.length >= limit) {
|
|
1462
|
+
writeStderrLine(
|
|
1463
|
+
`[artifacts] session=${sessionId} action=dropped reason="artifact batch limit exceeded" source=${source} dropped=${rawArtifacts.length - index}`
|
|
1464
|
+
);
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
artifacts.push(toInput(artifact));
|
|
1468
|
+
}
|
|
1469
|
+
return artifacts;
|
|
1470
|
+
}
|
|
1471
|
+
__name(extractCappedArtifactInputs, "extractCappedArtifactInputs");
|
|
1472
|
+
function artifactIngestionErrorReason(error) {
|
|
1473
|
+
if (!(error instanceof Error)) {
|
|
1474
|
+
return String(error);
|
|
1475
|
+
}
|
|
1476
|
+
return {
|
|
1477
|
+
name: error.name,
|
|
1478
|
+
message: error.message,
|
|
1479
|
+
stack: error.stack?.split("\n").slice(0, 4).join("\n")
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
__name(artifactIngestionErrorReason, "artifactIngestionErrorReason");
|
|
1483
|
+
function extractSessionUpdateArtifacts(params, updateMeta, limit, sessionId) {
|
|
1484
|
+
const rawArtifacts = updateMeta?.["artifacts"];
|
|
1485
|
+
if (!Array.isArray(rawArtifacts)) {
|
|
1486
|
+
return [];
|
|
1487
|
+
}
|
|
1488
|
+
const update = params.update;
|
|
1489
|
+
if (update.sessionUpdate !== "tool_call_update" || update.status !== "completed" && update.status !== "failed" && update.status !== "cancelled") {
|
|
1490
|
+
return [];
|
|
1491
|
+
}
|
|
1492
|
+
const toolCallId = typeof update.toolCallId === "string" ? update.toolCallId : void 0;
|
|
1493
|
+
const toolName = typeof updateMeta?.["toolName"] === "string" ? updateMeta["toolName"] : void 0;
|
|
1494
|
+
return extractCappedArtifactInputs(
|
|
1495
|
+
rawArtifacts,
|
|
1496
|
+
limit,
|
|
1497
|
+
sessionId,
|
|
1498
|
+
"tool",
|
|
1499
|
+
(artifact) => ({
|
|
1500
|
+
...artifactPayloadFields(artifact),
|
|
1501
|
+
source: "tool",
|
|
1502
|
+
toolCallId,
|
|
1503
|
+
toolName
|
|
1504
|
+
})
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
__name(extractSessionUpdateArtifacts, "extractSessionUpdateArtifacts");
|
|
1508
|
+
function sanitizeSessionUpdateArtifacts(params, updateMeta) {
|
|
1509
|
+
if (!Array.isArray(updateMeta?.["artifacts"])) {
|
|
1510
|
+
return params;
|
|
1511
|
+
}
|
|
1512
|
+
const sanitizedMeta = { ...updateMeta };
|
|
1513
|
+
delete sanitizedMeta["artifacts"];
|
|
1514
|
+
const update = {
|
|
1515
|
+
...params.update,
|
|
1516
|
+
_meta: sanitizedMeta
|
|
1517
|
+
};
|
|
1518
|
+
return {
|
|
1519
|
+
...params,
|
|
1520
|
+
update
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
__name(sanitizeSessionUpdateArtifacts, "sanitizeSessionUpdateArtifacts");
|
|
1524
|
+
function isTrustedArtifactToolUpdate(params, updateMeta) {
|
|
1525
|
+
const update = params.update;
|
|
1526
|
+
return update.sessionUpdate === "tool_call_update" && update.status === "completed" && updateMeta?.["toolName"] === PUBLISH_ARTIFACT_TOOL_NAME;
|
|
1527
|
+
}
|
|
1528
|
+
__name(isTrustedArtifactToolUpdate, "isTrustedArtifactToolUpdate");
|
|
1529
|
+
function preserveFsErrorOverAcp(err) {
|
|
1530
|
+
if (isFsErrorShape(err)) {
|
|
1531
|
+
const code = err.kind === "parse_error" ? -32602 : -32603;
|
|
1532
|
+
throw new RequestError(code, err.message, {
|
|
1533
|
+
errorKind: err.kind,
|
|
1534
|
+
...err.hint !== void 0 ? { hint: err.hint } : {},
|
|
1535
|
+
...err.status !== void 0 ? { status: err.status } : {}
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
throw err;
|
|
1539
|
+
}
|
|
1540
|
+
__name(preserveFsErrorOverAcp, "preserveFsErrorOverAcp");
|
|
1541
|
+
function preserveScheduledTaskCreateErrorOverAcp(err) {
|
|
1542
|
+
if (isExistingSessionScheduledTaskCreateErrorShape(err)) {
|
|
1543
|
+
throw new RequestError(err.status >= 500 ? -32603 : -32602, err.message, {
|
|
1544
|
+
errorKind: err.code,
|
|
1545
|
+
status: err.status,
|
|
1546
|
+
hint: err.message
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
throw err;
|
|
1550
|
+
}
|
|
1551
|
+
__name(preserveScheduledTaskCreateErrorOverAcp, "preserveScheduledTaskCreateErrorOverAcp");
|
|
1552
|
+
function resolutionToAcpResponse(resolution) {
|
|
1553
|
+
if (resolution.kind === "option") {
|
|
1554
|
+
return {
|
|
1555
|
+
outcome: { outcome: "selected", optionId: resolution.optionId },
|
|
1556
|
+
...resolution.metadata ?? {}
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
return {
|
|
1560
|
+
outcome: { outcome: "cancelled" },
|
|
1561
|
+
_meta: {
|
|
1562
|
+
[DAEMON_PERMISSION_CANCEL_REASON_META_KEY]: resolution.reason
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
__name(resolutionToAcpResponse, "resolutionToAcpResponse");
|
|
1567
|
+
var MAX_EARLY_EVENT_SESSIONS = 64;
|
|
1568
|
+
var MAX_EARLY_EVENTS_PER_SESSION = 32;
|
|
1569
|
+
var MAX_SUGGESTION_LENGTH = 500;
|
|
1570
|
+
var EARLY_EVENT_TTL_MS = 6e4;
|
|
1571
|
+
var KNOWN_APPROVAL_MODES = new Set(
|
|
1572
|
+
APPROVAL_MODES
|
|
1573
|
+
);
|
|
1574
|
+
function describeStatKind(stats) {
|
|
1575
|
+
if (stats.isDirectory()) return "directory";
|
|
1576
|
+
if (stats.isSymbolicLink()) return "symlink";
|
|
1577
|
+
if (stats.isCharacterDevice()) return "character device";
|
|
1578
|
+
if (stats.isBlockDevice()) return "block device";
|
|
1579
|
+
if (stats.isFIFO()) return "named pipe (FIFO)";
|
|
1580
|
+
if (stats.isSocket()) return "socket";
|
|
1581
|
+
return "non-regular file";
|
|
1582
|
+
}
|
|
1583
|
+
__name(describeStatKind, "describeStatKind");
|
|
1584
|
+
function sliceLineRange(content, startLine, endLine) {
|
|
1585
|
+
let offset = 0;
|
|
1586
|
+
for (let i = 0; i < startLine; i++) {
|
|
1587
|
+
const nl = content.indexOf("\n", offset);
|
|
1588
|
+
if (nl === -1) return "";
|
|
1589
|
+
offset = nl + 1;
|
|
1590
|
+
}
|
|
1591
|
+
if (endLine === void 0) return content.slice(offset);
|
|
1592
|
+
let end = offset;
|
|
1593
|
+
const want = endLine - startLine;
|
|
1594
|
+
for (let i = 0; i < want; i++) {
|
|
1595
|
+
const nl = content.indexOf("\n", end);
|
|
1596
|
+
if (nl === -1) return content.slice(offset);
|
|
1597
|
+
end = nl + 1;
|
|
1598
|
+
}
|
|
1599
|
+
return content.slice(offset, end > offset ? end - 1 : end);
|
|
1600
|
+
}
|
|
1601
|
+
__name(sliceLineRange, "sliceLineRange");
|
|
1602
|
+
var BridgeClient = class {
|
|
1603
|
+
constructor(resolveEntry, resolvePendingRestoreEvents, mediator, permissionTimeoutMs, maxPendingPerSession, fileSystem, onModelPromoted, onModePromoted, clientMcpSender, ownsSession = () => true, onTokenUsage, onCreateSubSession, onGenerationEvent, onWorkspaceGenerationEvent, onChannelDelivery, hasSessionSpawnInFlight = () => false, getLiveScreenContextCaptureHandler = () => void 0, getLiveTaskToolRequestHandler = () => void 0, getLiveSpeakToUserHandler = () => void 0, externalToolGuard, onActiveWork, onSessionCatalogChanged, onGoalTurnEnded, onCreateCurrentSessionScheduledTask) {
|
|
1604
|
+
this.resolveEntry = resolveEntry;
|
|
1605
|
+
this.resolvePendingRestoreEvents = resolvePendingRestoreEvents;
|
|
1606
|
+
this.mediator = mediator;
|
|
1607
|
+
this.permissionTimeoutMs = permissionTimeoutMs;
|
|
1608
|
+
this.maxPendingPerSession = maxPendingPerSession;
|
|
1609
|
+
this.fileSystem = fileSystem;
|
|
1610
|
+
this.onModelPromoted = onModelPromoted;
|
|
1611
|
+
this.onModePromoted = onModePromoted;
|
|
1612
|
+
this.clientMcpSender = clientMcpSender;
|
|
1613
|
+
this.ownsSession = ownsSession;
|
|
1614
|
+
this.onTokenUsage = onTokenUsage;
|
|
1615
|
+
this.onCreateSubSession = onCreateSubSession;
|
|
1616
|
+
this.onGenerationEvent = onGenerationEvent;
|
|
1617
|
+
this.onWorkspaceGenerationEvent = onWorkspaceGenerationEvent;
|
|
1618
|
+
this.onChannelDelivery = onChannelDelivery;
|
|
1619
|
+
this.hasSessionSpawnInFlight = hasSessionSpawnInFlight;
|
|
1620
|
+
this.getLiveScreenContextCaptureHandler = getLiveScreenContextCaptureHandler;
|
|
1621
|
+
this.getLiveTaskToolRequestHandler = getLiveTaskToolRequestHandler;
|
|
1622
|
+
this.getLiveSpeakToUserHandler = getLiveSpeakToUserHandler;
|
|
1623
|
+
this.externalToolGuard = externalToolGuard;
|
|
1624
|
+
this.onActiveWork = onActiveWork;
|
|
1625
|
+
this.onSessionCatalogChanged = onSessionCatalogChanged;
|
|
1626
|
+
this.onGoalTurnEnded = onGoalTurnEnded;
|
|
1627
|
+
this.onCreateCurrentSessionScheduledTask = onCreateCurrentSessionScheduledTask;
|
|
1628
|
+
}
|
|
1629
|
+
static {
|
|
1630
|
+
__name(this, "BridgeClient");
|
|
1631
|
+
}
|
|
1632
|
+
async requestPermission(params) {
|
|
1633
|
+
const entry = this.resolveEntry(params.sessionId);
|
|
1634
|
+
if (!entry) return { outcome: { outcome: "cancelled" } };
|
|
1635
|
+
if (entry.pendingPermissionIds.size >= this.maxPendingPerSession) {
|
|
1636
|
+
writeStderrLine(
|
|
1637
|
+
`qwen serve: session ${entry.sessionId} exceeded maxPendingPermissionsPerSession (${this.maxPendingPerSession}) \u2014 resolving new permission as cancelled.`
|
|
1638
|
+
);
|
|
1639
|
+
return { outcome: { outcome: "cancelled" } };
|
|
1640
|
+
}
|
|
1641
|
+
const options = Array.isArray(params.options) ? params.options : [];
|
|
1642
|
+
const allowedOptionIds = new Set(
|
|
1643
|
+
options.map((o) => String(o.optionId ?? ""))
|
|
1644
|
+
);
|
|
1645
|
+
allowedOptionIds.delete("");
|
|
1646
|
+
const requestId = randomUUID2();
|
|
1647
|
+
if (allowedOptionIds.has(CANCEL_VOTE_SENTINEL)) {
|
|
1648
|
+
throw new CancelSentinelCollisionError(requestId, CANCEL_VOTE_SENTINEL);
|
|
1649
|
+
}
|
|
1650
|
+
const published = entry.events.publish({
|
|
1651
|
+
type: "permission_request",
|
|
1652
|
+
...entry.activePromptId ? { promptId: entry.activePromptId } : {},
|
|
1653
|
+
data: {
|
|
1654
|
+
requestId,
|
|
1655
|
+
sessionId: entry.sessionId,
|
|
1656
|
+
toolCall: params.toolCall,
|
|
1657
|
+
options
|
|
1658
|
+
},
|
|
1659
|
+
...entry.activePromptOriginatorClientId ? { originatorClientId: entry.activePromptOriginatorClientId } : {}
|
|
1660
|
+
});
|
|
1661
|
+
if (!published) return { outcome: { outcome: "cancelled" } };
|
|
1662
|
+
let interaction;
|
|
1663
|
+
try {
|
|
1664
|
+
interaction = pendingInteractionFromRequest(requestId, {
|
|
1665
|
+
...params,
|
|
1666
|
+
options
|
|
1667
|
+
});
|
|
1668
|
+
} catch (error) {
|
|
1669
|
+
writeStderrLine(
|
|
1670
|
+
`qwen serve: failed to snapshot pending interaction ${requestId}: ${error instanceof Error ? error.message : String(error)}`
|
|
1671
|
+
);
|
|
1672
|
+
interaction = fallbackPendingPermissionInteraction(requestId, options);
|
|
1673
|
+
}
|
|
1674
|
+
entry.pendingPermissionIds.add(requestId);
|
|
1675
|
+
if (interaction) entry.pendingInteractions.set(requestId, interaction);
|
|
1676
|
+
try {
|
|
1677
|
+
const record = {
|
|
1678
|
+
requestId,
|
|
1679
|
+
sessionId: entry.sessionId,
|
|
1680
|
+
promptId: entry.activePromptId,
|
|
1681
|
+
originatorClientId: entry.activePromptOriginatorClientId,
|
|
1682
|
+
allowedOptionIds,
|
|
1683
|
+
issuedAtMs: Date.now()
|
|
1684
|
+
};
|
|
1685
|
+
const resolution = await this.mediator.request(
|
|
1686
|
+
record,
|
|
1687
|
+
this.permissionTimeoutMs
|
|
1688
|
+
);
|
|
1689
|
+
return resolutionToAcpResponse(resolution);
|
|
1690
|
+
} finally {
|
|
1691
|
+
entry.pendingPermissionIds.delete(requestId);
|
|
1692
|
+
entry.pendingInteractions.delete(requestId);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
async sessionUpdate(params) {
|
|
1696
|
+
if (this.abandonedRestoreIds.has(params.sessionId)) {
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
if (!this.ownsSession(params.sessionId) && !this.inFlightRestoreIds.has(params.sessionId)) {
|
|
1700
|
+
writeStderrLine(
|
|
1701
|
+
`[demux] session=${params.sessionId} type=session_update action=dropped reason=session_not_owned`
|
|
1702
|
+
);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
const entry = this.resolveEntry(params.sessionId);
|
|
1706
|
+
const events = entry?.events ?? this.resolvePendingRestoreEvents(params.sessionId);
|
|
1707
|
+
if (!events) return;
|
|
1708
|
+
const prepared = this.prepareSessionUpdateFrames(params, entry);
|
|
1709
|
+
for (const frame of prepared.frames) {
|
|
1710
|
+
events.publish(frame);
|
|
1711
|
+
}
|
|
1712
|
+
try {
|
|
1713
|
+
this.recordLiveTokenUsage(params, entry);
|
|
1714
|
+
} catch {
|
|
1715
|
+
}
|
|
1716
|
+
if (entry && prepared.artifacts.length > 0) {
|
|
1717
|
+
await this.upsertAndPublishArtifacts(
|
|
1718
|
+
entry,
|
|
1719
|
+
prepared.artifacts,
|
|
1720
|
+
{
|
|
1721
|
+
trustedPublisher: prepared.trustedPublisher
|
|
1722
|
+
},
|
|
1723
|
+
prepared.turn
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
prepareSessionUpdateFrames(params, entry) {
|
|
1728
|
+
const turn = {
|
|
1729
|
+
...entry?.activePromptId ? { promptId: entry.activePromptId } : {},
|
|
1730
|
+
...entry?.activePromptOriginatorClientId ? { originatorClientId: entry.activePromptOriginatorClientId } : {}
|
|
1731
|
+
};
|
|
1732
|
+
const frames = [];
|
|
1733
|
+
const a2ui = extractA2uiToolUpdate(params);
|
|
1734
|
+
if (a2ui) {
|
|
1735
|
+
for (const surface of a2ui.surfaces) {
|
|
1736
|
+
frames.push({
|
|
1737
|
+
type: "session_update",
|
|
1738
|
+
data: {
|
|
1739
|
+
sessionId: params.sessionId,
|
|
1740
|
+
update: {
|
|
1741
|
+
sessionUpdate: "a2ui",
|
|
1742
|
+
a2ui: {
|
|
1743
|
+
surfaceId: surface.surfaceId,
|
|
1744
|
+
callId: a2ui.callId,
|
|
1745
|
+
commands: surface.commands
|
|
1746
|
+
},
|
|
1747
|
+
_meta: { serverTimestamp: Date.now(), source: "a2ui-bridge" }
|
|
1748
|
+
}
|
|
1749
|
+
},
|
|
1750
|
+
...turn
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
params = a2ui.sanitizedParams;
|
|
1754
|
+
}
|
|
1755
|
+
const updateMeta = params.update._meta;
|
|
1756
|
+
const originalTs = updateMeta?.["serverTimestamp"] ?? updateMeta?.["timestamp"];
|
|
1757
|
+
const serverTimestamp = typeof originalTs === "number" && Number.isFinite(originalTs) ? originalTs : void 0;
|
|
1758
|
+
const artifacts = entry?.artifacts ? extractSessionUpdateArtifacts(
|
|
1759
|
+
params,
|
|
1760
|
+
updateMeta,
|
|
1761
|
+
entry.artifacts.inputBatchLimit(),
|
|
1762
|
+
entry.sessionId
|
|
1763
|
+
) : [];
|
|
1764
|
+
const publishParams = sanitizeSessionUpdateArtifacts(params, updateMeta);
|
|
1765
|
+
frames.push({
|
|
1766
|
+
type: "session_update",
|
|
1767
|
+
data: publishParams,
|
|
1768
|
+
...turn,
|
|
1769
|
+
...serverTimestamp !== void 0 ? { _meta: { serverTimestamp } } : {}
|
|
1770
|
+
});
|
|
1771
|
+
return {
|
|
1772
|
+
frames,
|
|
1773
|
+
artifacts,
|
|
1774
|
+
trustedPublisher: isTrustedArtifactToolUpdate(params, updateMeta),
|
|
1775
|
+
turn
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
async seedSessionUpdates(entry, updates, options = {}) {
|
|
1779
|
+
const frames = [];
|
|
1780
|
+
const artifactBatches = [];
|
|
1781
|
+
for (const update of updates) {
|
|
1782
|
+
const prepared = this.prepareSessionUpdateFrames(
|
|
1783
|
+
{ sessionId: entry.sessionId, update },
|
|
1784
|
+
entry
|
|
1785
|
+
);
|
|
1786
|
+
frames.push(...prepared.frames);
|
|
1787
|
+
if (options.ingestArtifacts !== false && prepared.artifacts.length > 0) {
|
|
1788
|
+
artifactBatches.push({
|
|
1789
|
+
artifacts: prepared.artifacts,
|
|
1790
|
+
trustedPublisher: prepared.trustedPublisher,
|
|
1791
|
+
turn: prepared.turn
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
entry.events.seedReplayEvents(frames);
|
|
1796
|
+
for (const batch of artifactBatches) {
|
|
1797
|
+
await this.upsertAndPublishArtifacts(
|
|
1798
|
+
entry,
|
|
1799
|
+
batch.artifacts,
|
|
1800
|
+
{
|
|
1801
|
+
trustedPublisher: batch.trustedPublisher
|
|
1802
|
+
},
|
|
1803
|
+
batch.turn
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
async ingestSessionUpdateArtifactsReady(entry, updates) {
|
|
1808
|
+
for (const update of updates) {
|
|
1809
|
+
const prepared = this.prepareSessionUpdateFrames(
|
|
1810
|
+
{ sessionId: entry.sessionId, update },
|
|
1811
|
+
entry
|
|
1812
|
+
);
|
|
1813
|
+
if (prepared.artifacts.length === 0) continue;
|
|
1814
|
+
const ingested = await this.upsertAndPublishArtifactsReady(
|
|
1815
|
+
entry,
|
|
1816
|
+
prepared.artifacts,
|
|
1817
|
+
{ trustedPublisher: prepared.trustedPublisher },
|
|
1818
|
+
prepared.turn
|
|
1819
|
+
);
|
|
1820
|
+
if (!ingested) throw new Error("Artifact ingestion failed.");
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
async drainDeferredSessionArtifacts(entry) {
|
|
1824
|
+
while (entry.deferredArtifactBatches.length > 0) {
|
|
1825
|
+
const batch = entry.deferredArtifactBatches[0];
|
|
1826
|
+
const ingested = await this.upsertAndPublishArtifactsReady(
|
|
1827
|
+
entry,
|
|
1828
|
+
batch.artifacts,
|
|
1829
|
+
batch.options,
|
|
1830
|
+
batch.turn
|
|
1831
|
+
);
|
|
1832
|
+
if (!ingested) throw new Error("Deferred artifact ingestion failed.");
|
|
1833
|
+
entry.deferredArtifactBatches.shift();
|
|
1834
|
+
entry.deferredArtifactInputCount -= batch.artifacts.length;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Daemon token-burn accounting for LIVE model turns. Called only from
|
|
1839
|
+
* `sessionUpdate` (the live session/update fan-in), never from
|
|
1840
|
+
* `seedSessionUpdates` — so batch load-replay never lands historical usage in
|
|
1841
|
+
* the current metrics window. Additionally guarded on a live `entry`: a stray
|
|
1842
|
+
* pending-restore frame (entry not yet registered) is skipped too, so replayed
|
|
1843
|
+
* history can't post a phantom burn spike with no model call.
|
|
1844
|
+
*
|
|
1845
|
+
* Usage rides an otherwise-empty `agent_message_chunk` as `update._meta.usage`
|
|
1846
|
+
* with per-round camelCase increments; subagent frames carry their own usage
|
|
1847
|
+
* (tagged `parentToolCallId`) and are independent turns, so counting each
|
|
1848
|
+
* frame once is the correct total. `_meta`/`usage` are optional and untyped.
|
|
1849
|
+
*/
|
|
1850
|
+
recordLiveTokenUsage(params, entry) {
|
|
1851
|
+
if (!this.onTokenUsage || !entry) return;
|
|
1852
|
+
const updateMeta = params.update._meta;
|
|
1853
|
+
const usage = updateMeta?.["usage"];
|
|
1854
|
+
if (usage === null || typeof usage !== "object") return;
|
|
1855
|
+
const inputTokens = usage.inputTokens;
|
|
1856
|
+
const outputTokens = usage.outputTokens;
|
|
1857
|
+
if (typeof inputTokens !== "number" && typeof outputTokens !== "number") {
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
const durationMs = updateMeta?.["durationMs"];
|
|
1861
|
+
const apiErrors = updateMeta?.["apiErrors"];
|
|
1862
|
+
const apiRetries = updateMeta?.["apiRetries"];
|
|
1863
|
+
this.onTokenUsage(
|
|
1864
|
+
typeof inputTokens === "number" ? inputTokens : 0,
|
|
1865
|
+
typeof outputTokens === "number" ? outputTokens : 0,
|
|
1866
|
+
typeof durationMs === "number" ? durationMs : void 0,
|
|
1867
|
+
typeof apiErrors === "number" ? apiErrors : 0,
|
|
1868
|
+
typeof apiRetries === "number" ? apiRetries : 0
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Bounded early-event buffer. The map is scoped to this BridgeClient and
|
|
1873
|
+
* therefore to one channel; a stale channel cannot seed a fresh channel's
|
|
1874
|
+
* future session. Frames are keyed by sessionId; each entry tracks its
|
|
1875
|
+
* `expiresAt` for lazy TTL-based eviction in `bufferEarlyEvent`. Drained by
|
|
1876
|
+
* `drainEarlyEvents` whenever the bridge registers a session with a matching
|
|
1877
|
+
* id. See MAX_EARLY_EVENT_* constants for capacity bounds.
|
|
1878
|
+
*/
|
|
1879
|
+
earlyEvents = /* @__PURE__ */ new Map();
|
|
1880
|
+
/**
|
|
1881
|
+
* Tombstone for closed/killed session ids. Prevents late
|
|
1882
|
+
* `extNotification` from a dying child from leaking into the
|
|
1883
|
+
* early-event buffer and being replayed onto a future session
|
|
1884
|
+
* that reuses the same id via `session/load` or `session/resume`.
|
|
1885
|
+
*
|
|
1886
|
+
* Tombstone semantics:
|
|
1887
|
+
* - Marked when the bridge removes a sessionId from `byId` (kill
|
|
1888
|
+
* path, channel.exited handler, closeSession).
|
|
1889
|
+
* - Concurrently purges any in-flight `earlyEvents[id]`.
|
|
1890
|
+
* - `bufferEarlyEvent` rejects tombstoned ids.
|
|
1891
|
+
* - `drainEarlyEvents` clears the tombstone — a fresh
|
|
1892
|
+
* `createSessionEntry` for the same id is a legitimate
|
|
1893
|
+
* "load/resume of a persisted session id" case.
|
|
1894
|
+
* - TTL = `EARLY_EVENT_TTL_MS` (60s) — same as the early-event
|
|
1895
|
+
* buffer, so by the time a tombstone expires there can be no
|
|
1896
|
+
* stale frame for that id anywhere in the system.
|
|
1897
|
+
*/
|
|
1898
|
+
tombstonedSessionIds = /* @__PURE__ */ new Map();
|
|
1899
|
+
/** Restore ownership for `sessionUpdate` and artifact demultiplexing. */
|
|
1900
|
+
inFlightRestoreIds = /* @__PURE__ */ new Set();
|
|
1901
|
+
/**
|
|
1902
|
+
* Registrations allowed to buffer early extended notifications through an
|
|
1903
|
+
* ordinary close tombstone. This includes restores and caller-supplied-id
|
|
1904
|
+
* spawns, and lasts only until their ACP registration attempt settles.
|
|
1905
|
+
*/
|
|
1906
|
+
inFlightSessionRegistrationIds = /* @__PURE__ */ new Set();
|
|
1907
|
+
/**
|
|
1908
|
+
* Restore ids whose caller timed out while the non-cancellable ACP request
|
|
1909
|
+
* continues.
|
|
1910
|
+
*/
|
|
1911
|
+
abandonedRestoreIds = /* @__PURE__ */ new Set();
|
|
1912
|
+
/**
|
|
1913
|
+
* Handle child->bridge ACP `extMethod` requests (calls that expect a
|
|
1914
|
+
* response, unlike `extNotification`). Served methods:
|
|
1915
|
+
* `qwen/control/client_mcp/message` (reverse tool channel),
|
|
1916
|
+
* `qwen/control/create-sub-session` (the `create_sub_session` tool → daemon
|
|
1917
|
+
* spawns a sub-session and, for `'first-turn'`, returns its first-turn
|
|
1918
|
+
* result), and `craft/drainMidTurnQueue`: the ACP child calls the last one
|
|
1919
|
+
* between tool batches to pull any messages the browser queued mid-turn. We splice the per-session
|
|
1920
|
+
* queue, return them to the child as the response, and — when non-empty —
|
|
1921
|
+
* publish a `mid_turn_message_injected` SSE frame so the browser can move
|
|
1922
|
+
* those messages out of its pending queue and render the immediate echo.
|
|
1923
|
+
* Unknown methods reject with ACP `methodNotFound` (-32601), matching
|
|
1924
|
+
* the SDK's
|
|
1925
|
+
* default for an unimplemented client surface; the child's drain caller
|
|
1926
|
+
* treats that as "drain unsupported" and stops asking.
|
|
1927
|
+
*/
|
|
1928
|
+
async extMethod(method, params) {
|
|
1929
|
+
if (method === SERVE_CONTROL_EXT_METHODS.clientMcpMessage) {
|
|
1930
|
+
return this.handleClientMcpMessage(params);
|
|
1931
|
+
}
|
|
1932
|
+
if (method === SERVE_CONTROL_EXT_METHODS.createSubSession) {
|
|
1933
|
+
return this.handleCreateSubSession(params);
|
|
1934
|
+
}
|
|
1935
|
+
if (method === SERVE_CONTROL_EXT_METHODS.createCurrentSessionScheduledTask) {
|
|
1936
|
+
return this.handleCreateCurrentSessionScheduledTask(params);
|
|
1937
|
+
}
|
|
1938
|
+
if (method === SERVE_CONTROL_EXT_METHODS.liveCaptureScreenContext) {
|
|
1939
|
+
return this.handleLiveScreenContextCapture(params);
|
|
1940
|
+
}
|
|
1941
|
+
if (method === SERVE_CONTROL_EXT_METHODS.liveTaskTool) {
|
|
1942
|
+
return this.handleLiveTaskTool(params);
|
|
1943
|
+
}
|
|
1944
|
+
if (method === SERVE_CONTROL_EXT_METHODS.liveSpeakToUser) {
|
|
1945
|
+
return this.handleLiveSpeakToUser(params);
|
|
1946
|
+
}
|
|
1947
|
+
if (method === SERVE_CONTROL_EXT_METHODS.channelDelivery) {
|
|
1948
|
+
return this.handleChannelDelivery(params);
|
|
1949
|
+
}
|
|
1950
|
+
if (method === SERVE_CONTROL_EXT_METHODS.externalToolGuardPrepare) {
|
|
1951
|
+
return this.handleExternalToolGuardPrepare(params);
|
|
1952
|
+
}
|
|
1953
|
+
if (method === TODO_STOP_GUARD_CONTINUATION_CLAIM_METHOD) {
|
|
1954
|
+
return this.handleTodoStopGuardContinuationClaim(params);
|
|
1955
|
+
}
|
|
1956
|
+
if (method !== MID_TURN_QUEUE_DRAIN_METHOD) {
|
|
1957
|
+
throw RequestError.methodNotFound(method);
|
|
1958
|
+
}
|
|
1959
|
+
const sessionId = typeof params["sessionId"] === "string" ? params["sessionId"] : void 0;
|
|
1960
|
+
if (!sessionId) return { messages: [], items: [], hasQueuedPrompt: false };
|
|
1961
|
+
if (!this.ownsSession(sessionId)) {
|
|
1962
|
+
return { messages: [], items: [], hasQueuedPrompt: false };
|
|
1963
|
+
}
|
|
1964
|
+
const entry = this.resolveEntry(sessionId);
|
|
1965
|
+
if (!entry) return { messages: [], items: [], hasQueuedPrompt: false };
|
|
1966
|
+
const drained = entry.midTurnMessageQueue.splice(0);
|
|
1967
|
+
if (drained.length > 0) {
|
|
1968
|
+
for (const item of drained) {
|
|
1969
|
+
entry.settledMidTurnMessageIds.push(item.messageId);
|
|
1970
|
+
}
|
|
1971
|
+
if (entry.settledMidTurnMessageIds.length > MID_TURN_RECONCILIATION_RING_SIZE) {
|
|
1972
|
+
entry.settledMidTurnMessageIds.splice(
|
|
1973
|
+
0,
|
|
1974
|
+
entry.settledMidTurnMessageIds.length - MID_TURN_RECONCILIATION_RING_SIZE
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
const attachmentMemo = /* @__PURE__ */ new Map();
|
|
1979
|
+
const serializedAttachmentIds = /* @__PURE__ */ new Set();
|
|
1980
|
+
const items = [];
|
|
1981
|
+
try {
|
|
1982
|
+
for (const item of drained) {
|
|
1983
|
+
let degraded = 0;
|
|
1984
|
+
const planned = (item.content ?? []).filter((block) => {
|
|
1985
|
+
if (!isSessionAttachmentReference(block)) return true;
|
|
1986
|
+
if (serializedAttachmentIds.has(block.attachmentId)) {
|
|
1987
|
+
degraded += 1;
|
|
1988
|
+
return false;
|
|
1989
|
+
}
|
|
1990
|
+
serializedAttachmentIds.add(block.attachmentId);
|
|
1991
|
+
return true;
|
|
1992
|
+
});
|
|
1993
|
+
let resolvedBlocks;
|
|
1994
|
+
let attachmentReferences;
|
|
1995
|
+
try {
|
|
1996
|
+
resolvedBlocks = await entry.attachments.resolveContent(
|
|
1997
|
+
planned,
|
|
1998
|
+
attachmentMemo
|
|
1999
|
+
);
|
|
2000
|
+
attachmentReferences = planned.filter(isSessionAttachmentReference);
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
if (!(error instanceof SessionAttachmentReferenceError)) throw error;
|
|
2003
|
+
writeStderrLine(
|
|
2004
|
+
`[mid-turn] session=${JSON.stringify(entry.sessionId)} degraded attachment for message ${JSON.stringify(item.messageId)}: ${JSON.stringify(error instanceof Error ? error.message : String(error))}`
|
|
2005
|
+
);
|
|
2006
|
+
const perBlock = await entry.attachments.resolveContentDegrading(
|
|
2007
|
+
planned,
|
|
2008
|
+
attachmentMemo
|
|
2009
|
+
);
|
|
2010
|
+
resolvedBlocks = perBlock.resolvedBlocks;
|
|
2011
|
+
attachmentReferences = perBlock.retainedBlocks.filter(
|
|
2012
|
+
isSessionAttachmentReference
|
|
2013
|
+
);
|
|
2014
|
+
degraded += perBlock.degraded;
|
|
2015
|
+
}
|
|
2016
|
+
let content = [
|
|
2017
|
+
...item.text ? [{ type: "text", text: item.text }] : [],
|
|
2018
|
+
...resolvedBlocks
|
|
2019
|
+
];
|
|
2020
|
+
if (degraded > 0) content = withAttachmentDegradationMarker(content);
|
|
2021
|
+
items.push({
|
|
2022
|
+
messageId: item.messageId,
|
|
2023
|
+
displayText: item.text,
|
|
2024
|
+
content,
|
|
2025
|
+
...attachmentReferences.length > 0 ? { attachmentReferences } : {}
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
} catch (error) {
|
|
2029
|
+
const requeued = new Set(drained.map((queued) => queued.messageId));
|
|
2030
|
+
const ring = entry.settledMidTurnMessageIds;
|
|
2031
|
+
const kept = ring.filter((id) => !requeued.has(id));
|
|
2032
|
+
ring.splice(0, ring.length, ...kept);
|
|
2033
|
+
entry.midTurnMessageQueue.unshift(...drained);
|
|
2034
|
+
writeStderrLine(
|
|
2035
|
+
`[mid-turn] session=${JSON.stringify(entry.sessionId)} drain failed, requeued ${drained.length} message(s): ${JSON.stringify(error instanceof Error ? error.message : String(error))}`
|
|
2036
|
+
);
|
|
2037
|
+
throw error;
|
|
2038
|
+
}
|
|
2039
|
+
const echoed = drained.filter((item) => !item.queueOnly);
|
|
2040
|
+
const messages = drained.map((item) => item.text);
|
|
2041
|
+
const hasQueuedPrompt = entry.pendingPromptList.some(
|
|
2042
|
+
(prompt) => prompt.state === "queued" && !prompt.abortController.signal.aborted
|
|
2043
|
+
);
|
|
2044
|
+
if (echoed.length > 0) {
|
|
2045
|
+
const published = entry.events.publish({
|
|
2046
|
+
type: MID_TURN_MESSAGE_INJECTED_EVENT,
|
|
2047
|
+
...entry.activePromptId ? { promptId: entry.activePromptId } : {},
|
|
2048
|
+
data: {
|
|
2049
|
+
sessionId: entry.sessionId,
|
|
2050
|
+
messages: echoed.map((item) => item.text),
|
|
2051
|
+
messageIds: echoed.map((item) => item.messageId),
|
|
2052
|
+
// Carry the structured `items` twin (content blocks per message) so
|
|
2053
|
+
// the browser-side echo renderer can show attached images alongside
|
|
2054
|
+
// the message text. Older consumers that don't read this field keep
|
|
2055
|
+
// working unchanged.
|
|
2056
|
+
items: echoed.map((item) => ({
|
|
2057
|
+
...item.content && item.content.length > 0 ? { content: item.content } : {}
|
|
2058
|
+
}))
|
|
2059
|
+
}
|
|
2060
|
+
});
|
|
2061
|
+
writeStderrLine(
|
|
2062
|
+
published ? `[mid-turn] session=${entry.sessionId} drained=${messages.length} echoed=${echoed.length} injected into running turn` : `[mid-turn] session=${entry.sessionId} drained=${messages.length} echoed=${echoed.length} echo frame dropped (bus closed); reconciliation required`
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
return { messages, items, hasQueuedPrompt };
|
|
2066
|
+
}
|
|
2067
|
+
async handleExternalToolGuardPrepare(params) {
|
|
2068
|
+
if (!this.externalToolGuard) {
|
|
2069
|
+
throw RequestError.methodNotFound(
|
|
2070
|
+
SERVE_CONTROL_EXT_METHODS.externalToolGuardPrepare
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
const sessionId = params["sessionId"];
|
|
2074
|
+
const promptId = params["promptId"];
|
|
2075
|
+
const toolCallId = params["toolCallId"];
|
|
2076
|
+
const toolName = params["toolName"];
|
|
2077
|
+
const args = params["arguments"];
|
|
2078
|
+
if (typeof sessionId !== "string" || sessionId.length === 0 || promptId !== void 0 && (typeof promptId !== "string" || promptId.length === 0) || typeof toolCallId !== "string" || toolCallId.length === 0 || typeof toolName !== "string" || toolName.length === 0 || !isRecord(args)) {
|
|
2079
|
+
throw RequestError.invalidParams(
|
|
2080
|
+
void 0,
|
|
2081
|
+
"Invalid external tool guard request"
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
const promptScoped = promptId !== void 0;
|
|
2085
|
+
if (!this.ownsSession(sessionId)) {
|
|
2086
|
+
throw RequestError.invalidParams(
|
|
2087
|
+
void 0,
|
|
2088
|
+
"External tool guard session is not owned by this connection"
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
const entry = this.resolveEntry(sessionId);
|
|
2092
|
+
if (!entry || promptScoped && (!entry.promptActive || entry.activePromptId !== promptId)) {
|
|
2093
|
+
throw RequestError.invalidParams(
|
|
2094
|
+
void 0,
|
|
2095
|
+
"External tool guard prompt is not the active prompt"
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
const invocationCwd = params["invocationCwd"];
|
|
2099
|
+
const decision = await this.externalToolGuard({
|
|
2100
|
+
sessionId: entry.sessionId,
|
|
2101
|
+
...promptScoped ? { promptId } : {},
|
|
2102
|
+
toolCallId,
|
|
2103
|
+
toolName,
|
|
2104
|
+
arguments: args,
|
|
2105
|
+
effectiveCwd: entry.effectiveCwd,
|
|
2106
|
+
// Forwarded verbatim and explicitly untrusted: the host policy decides
|
|
2107
|
+
// whether it can establish this scope from state it owns.
|
|
2108
|
+
...typeof invocationCwd === "string" && invocationCwd.length > 0 ? { invocationCwd } : {}
|
|
2109
|
+
});
|
|
2110
|
+
const currentEntry = this.resolveEntry(sessionId);
|
|
2111
|
+
if (!this.ownsSession(sessionId) || currentEntry !== entry || promptScoped && (!currentEntry.promptActive || currentEntry.activePromptId !== promptId)) {
|
|
2112
|
+
throw RequestError.invalidParams(
|
|
2113
|
+
void 0,
|
|
2114
|
+
"External tool guard prompt is no longer active"
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
return normalizeExternalToolGuardResult(decision);
|
|
2118
|
+
}
|
|
2119
|
+
handleTodoStopGuardContinuationClaim(params) {
|
|
2120
|
+
const sessionId = typeof params["sessionId"] === "string" && params["sessionId"].length > 0 ? params["sessionId"] : void 0;
|
|
2121
|
+
if (!sessionId) return { claimed: false, hasQueuedPrompt: false };
|
|
2122
|
+
if (!this.ownsSession(sessionId)) {
|
|
2123
|
+
return { claimed: false, hasQueuedPrompt: false };
|
|
2124
|
+
}
|
|
2125
|
+
const entry = this.resolveEntry(sessionId);
|
|
2126
|
+
if (!entry) return { claimed: false, hasQueuedPrompt: false };
|
|
2127
|
+
const livePrompts = entry.pendingPromptList.filter(
|
|
2128
|
+
(prompt) => !prompt.abortController.signal.aborted
|
|
2129
|
+
);
|
|
2130
|
+
const promptId = typeof params["promptId"] === "string" && params["promptId"].length > 0 ? params["promptId"] : void 0;
|
|
2131
|
+
if (promptId) {
|
|
2132
|
+
const ownsRunningPrompt = entry.activePromptId === promptId && livePrompts.some(
|
|
2133
|
+
(prompt) => prompt.promptId === promptId && prompt.state === "running"
|
|
2134
|
+
);
|
|
2135
|
+
const hasCompetingRunningPrompt = livePrompts.some(
|
|
2136
|
+
(prompt) => prompt.promptId !== promptId && prompt.state === "running"
|
|
2137
|
+
);
|
|
2138
|
+
if (!ownsRunningPrompt || hasCompetingRunningPrompt) {
|
|
2139
|
+
return { claimed: false, hasQueuedPrompt: false };
|
|
2140
|
+
}
|
|
2141
|
+
const hasQueuedPrompt = livePrompts.some(
|
|
2142
|
+
(prompt) => prompt.state === "queued"
|
|
2143
|
+
);
|
|
2144
|
+
if (hasQueuedPrompt) {
|
|
2145
|
+
entry.todoStopGuardAwaitingQueuedPromptOwnerPromptId = promptId;
|
|
2146
|
+
return { claimed: false, hasQueuedPrompt: true };
|
|
2147
|
+
}
|
|
2148
|
+
if (entry.todoStopGuardAwaitingQueuedPromptOwnerPromptId === promptId) {
|
|
2149
|
+
delete entry.todoStopGuardAwaitingQueuedPromptOwnerPromptId;
|
|
2150
|
+
}
|
|
2151
|
+
return { claimed: true, hasQueuedPrompt: false };
|
|
2152
|
+
}
|
|
2153
|
+
if (entry.promptActive || livePrompts.length > 0) {
|
|
2154
|
+
return { claimed: false, hasQueuedPrompt: false };
|
|
2155
|
+
}
|
|
2156
|
+
return { claimed: true, hasQueuedPrompt: false };
|
|
2157
|
+
}
|
|
2158
|
+
async handleChannelDelivery(params) {
|
|
2159
|
+
if (!this.onChannelDelivery) {
|
|
2160
|
+
throw RequestError.methodNotFound(
|
|
2161
|
+
SERVE_CONTROL_EXT_METHODS.channelDelivery
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
const sessionId = params["sessionId"];
|
|
2165
|
+
if (typeof sessionId !== "string" || sessionId.length === 0 || !this.ownsSession(sessionId)) {
|
|
2166
|
+
throw RequestError.invalidParams(
|
|
2167
|
+
void 0,
|
|
2168
|
+
"`sessionId` must name a session owned by this connection"
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
const entry = this.resolveEntry(sessionId);
|
|
2172
|
+
if (!entry) {
|
|
2173
|
+
throw RequestError.invalidParams(void 0, "Unknown `sessionId`");
|
|
2174
|
+
}
|
|
2175
|
+
const deliveryId = params["deliveryId"];
|
|
2176
|
+
const source = params["source"];
|
|
2177
|
+
const text = params["text"];
|
|
2178
|
+
const rawTarget = params["target"];
|
|
2179
|
+
if (!isBoundedChannelDeliveryString(deliveryId) || source !== "prompt" && source !== "scheduled" || typeof text !== "string" || text.length > MAX_CHANNEL_DELIVERY_TEXT_CHARS || !isChannelDeliveryTarget(rawTarget)) {
|
|
2180
|
+
throw RequestError.invalidParams(
|
|
2181
|
+
void 0,
|
|
2182
|
+
"Invalid channel delivery request"
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
const promptId = params["promptId"];
|
|
2186
|
+
const taskId = params["taskId"];
|
|
2187
|
+
const firedAt = params["firedAt"];
|
|
2188
|
+
const allowedKeys = source === "prompt" ? /* @__PURE__ */ new Set([
|
|
2189
|
+
"sessionId",
|
|
2190
|
+
"deliveryId",
|
|
2191
|
+
"source",
|
|
2192
|
+
"target",
|
|
2193
|
+
"text",
|
|
2194
|
+
"promptId"
|
|
2195
|
+
]) : /* @__PURE__ */ new Set([
|
|
2196
|
+
"sessionId",
|
|
2197
|
+
"deliveryId",
|
|
2198
|
+
"source",
|
|
2199
|
+
"target",
|
|
2200
|
+
"text",
|
|
2201
|
+
"taskId",
|
|
2202
|
+
"firedAt"
|
|
2203
|
+
]);
|
|
2204
|
+
const correlationValid = source === "prompt" ? isBoundedChannelDeliveryString(promptId) && promptId === deliveryId : isBoundedChannelDeliveryString(taskId) && typeof firedAt === "number" && Number.isFinite(firedAt) && deliveryId === `${taskId}:${firedAt}`;
|
|
2205
|
+
if (!correlationValid || !Object.keys(params).every((key) => allowedKeys.has(key))) {
|
|
2206
|
+
throw RequestError.invalidParams(
|
|
2207
|
+
void 0,
|
|
2208
|
+
"Invalid channel delivery correlation"
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
const info = {
|
|
2212
|
+
sessionId,
|
|
2213
|
+
deliveryId,
|
|
2214
|
+
source,
|
|
2215
|
+
target: rawTarget,
|
|
2216
|
+
text,
|
|
2217
|
+
...source === "prompt" ? { promptId } : {},
|
|
2218
|
+
...source === "scheduled" ? { taskId, firedAt } : {}
|
|
2219
|
+
};
|
|
2220
|
+
let result;
|
|
2221
|
+
try {
|
|
2222
|
+
result = normalizeChannelDeliveryHostResult(
|
|
2223
|
+
await this.onChannelDelivery(info)
|
|
2224
|
+
);
|
|
2225
|
+
} catch {
|
|
2226
|
+
result = {
|
|
2227
|
+
status: "failed",
|
|
2228
|
+
code: "channel_delivery_failed",
|
|
2229
|
+
error: "Channel delivery failed."
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
try {
|
|
2233
|
+
entry.events.publish({
|
|
2234
|
+
type: "channel_delivery_result",
|
|
2235
|
+
...source === "prompt" ? { promptId } : {},
|
|
2236
|
+
data: {
|
|
2237
|
+
sessionId,
|
|
2238
|
+
deliveryId,
|
|
2239
|
+
source,
|
|
2240
|
+
status: result.status,
|
|
2241
|
+
...source === "prompt" ? { promptId } : {},
|
|
2242
|
+
...source === "scheduled" ? { taskId, firedAt } : {},
|
|
2243
|
+
...result.status === "failed" ? { code: result.code, error: result.error } : {}
|
|
2244
|
+
}
|
|
2245
|
+
});
|
|
2246
|
+
} catch {
|
|
2247
|
+
}
|
|
2248
|
+
return result;
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Reverse tool channel (issue #5626, Phase 2) — answer the child's
|
|
2252
|
+
* `qwen/control/client_mcp/message` ext-method. The child's session
|
|
2253
|
+
* `McpClientManager` calls this when its agent drives a client-hosted
|
|
2254
|
+
* (extension) MCP server: `params` carries the advertised `server` name and
|
|
2255
|
+
* the JSON-RPC `payload` (initialize / tools/list / tools/call / a
|
|
2256
|
+
* notification). We resolve the per-WS-connection sender via the injected
|
|
2257
|
+
* `clientMcpSender` lookup, deliver the payload over the daemon WS, and
|
|
2258
|
+
* return the correlated response as `{ payload }`.
|
|
2259
|
+
*
|
|
2260
|
+
* Rejects with ACP `methodNotFound` when no `clientMcpSender` is wired (Mode
|
|
2261
|
+
* A / tests can't host a client MCP server), and `invalidParams` when the
|
|
2262
|
+
* frame is malformed or the named server is no longer hosted (e.g. the
|
|
2263
|
+
* extension disconnected mid-turn) — the agent's `SdkControlClientTransport`
|
|
2264
|
+
* surfaces that as a transport error rather than hanging.
|
|
2265
|
+
*/
|
|
2266
|
+
async handleClientMcpMessage(params) {
|
|
2267
|
+
if (!this.clientMcpSender) {
|
|
2268
|
+
throw RequestError.methodNotFound(
|
|
2269
|
+
SERVE_CONTROL_EXT_METHODS.clientMcpMessage
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
const server = params["server"];
|
|
2273
|
+
if (typeof server !== "string" || server.length === 0) {
|
|
2274
|
+
throw RequestError.invalidParams(
|
|
2275
|
+
void 0,
|
|
2276
|
+
"`server` must be a non-empty string"
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
const payload = params["payload"];
|
|
2280
|
+
if (payload === null || typeof payload !== "object") {
|
|
2281
|
+
throw RequestError.invalidParams(
|
|
2282
|
+
void 0,
|
|
2283
|
+
"`payload` must be a JSON-RPC message object"
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
const send = this.clientMcpSender(server);
|
|
2287
|
+
if (!send) {
|
|
2288
|
+
throw RequestError.invalidParams(
|
|
2289
|
+
void 0,
|
|
2290
|
+
`client-hosted MCP server '${server}' is not currently connected`
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2293
|
+
const sessionId = params["sessionId"];
|
|
2294
|
+
if (sessionId !== void 0 && (typeof sessionId !== "string" || sessionId.length === 0)) {
|
|
2295
|
+
throw RequestError.invalidParams(
|
|
2296
|
+
void 0,
|
|
2297
|
+
"`sessionId` must be a non-empty string when provided"
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
const ownsSession = typeof sessionId === "string" && this.ownsSession(sessionId);
|
|
2301
|
+
if (typeof sessionId === "string" && !ownsSession && !this.hasSessionSpawnInFlight()) {
|
|
2302
|
+
throw RequestError.invalidParams(
|
|
2303
|
+
void 0,
|
|
2304
|
+
`Session not owned by this channel: ${sessionId}`
|
|
2305
|
+
);
|
|
2306
|
+
}
|
|
2307
|
+
if (typeof sessionId === "string" && !ownsSession) {
|
|
2308
|
+
writeStderrLine(
|
|
2309
|
+
`[demux] session=${sessionId} type=client_mcp_message action=forwarded_without_session reason=session_registration_pending`
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
const response = await send(
|
|
2313
|
+
payload,
|
|
2314
|
+
typeof sessionId === "string" && ownsSession ? { sessionId } : void 0
|
|
2315
|
+
);
|
|
2316
|
+
return { payload: response };
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Handle the `create_sub_session` tool's request: validate, then forward to
|
|
2320
|
+
* the daemon-host `onCreateSubSession` callback (which spawns a fresh
|
|
2321
|
+
* top-level sub-session and, for `'first-turn'`, waits for its first turn and
|
|
2322
|
+
* returns the result). No host wired → `methodNotFound`, which the tool
|
|
2323
|
+
* surfaces as "daemon-only".
|
|
2324
|
+
*/
|
|
2325
|
+
async handleCreateSubSession(params) {
|
|
2326
|
+
if (!this.onCreateSubSession) {
|
|
2327
|
+
throw RequestError.methodNotFound(
|
|
2328
|
+
SERVE_CONTROL_EXT_METHODS.createSubSession
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
const prompt = params["prompt"];
|
|
2332
|
+
if (typeof prompt !== "string" || prompt.length === 0) {
|
|
2333
|
+
throw RequestError.invalidParams(
|
|
2334
|
+
void 0,
|
|
2335
|
+
"`prompt` must be a non-empty string"
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
2338
|
+
if (prompt.length > MAX_SUB_SESSION_PROMPT_CHARS) {
|
|
2339
|
+
throw RequestError.invalidParams(
|
|
2340
|
+
void 0,
|
|
2341
|
+
`\`prompt\` exceeds the ${MAX_SUB_SESSION_PROMPT_CHARS}-character limit`
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
const completion = params["completion"];
|
|
2345
|
+
if (completion !== "sent" && completion !== "first-turn") {
|
|
2346
|
+
throw RequestError.invalidParams(
|
|
2347
|
+
void 0,
|
|
2348
|
+
"`completion` must be 'sent' or 'first-turn'"
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
const name = params["name"];
|
|
2352
|
+
if (typeof name === "string" && name.length > MAX_SUB_SESSION_NAME_CHARS) {
|
|
2353
|
+
throw RequestError.invalidParams(
|
|
2354
|
+
void 0,
|
|
2355
|
+
`\`name\` exceeds the ${MAX_SUB_SESSION_NAME_CHARS}-character limit`
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
const callerSessionId = params["callerSessionId"];
|
|
2359
|
+
if (typeof callerSessionId !== "string" || callerSessionId.length === 0 || !this.ownsSession(callerSessionId)) {
|
|
2360
|
+
throw RequestError.invalidParams(
|
|
2361
|
+
void 0,
|
|
2362
|
+
"`callerSessionId` is required and must name a session owned by this connection"
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
const model = params["model"];
|
|
2366
|
+
const result = await this.onCreateSubSession({
|
|
2367
|
+
prompt,
|
|
2368
|
+
completion,
|
|
2369
|
+
...typeof model === "string" && model.length > 0 && model.length <= 128 ? { model } : {},
|
|
2370
|
+
...typeof name === "string" && name.length > 0 ? { name } : {},
|
|
2371
|
+
callerSessionId
|
|
2372
|
+
});
|
|
2373
|
+
return {
|
|
2374
|
+
sessionId: result.sessionId,
|
|
2375
|
+
...result.result !== void 0 ? { result: result.result } : {},
|
|
2376
|
+
...result.stopReason !== void 0 ? { stopReason: result.stopReason } : {},
|
|
2377
|
+
...result.parentSessionPersisted !== void 0 ? { parentSessionPersisted: result.parentSessionPersisted } : {}
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
async handleCreateCurrentSessionScheduledTask(params) {
|
|
2381
|
+
if (!this.onCreateCurrentSessionScheduledTask) {
|
|
2382
|
+
throw RequestError.methodNotFound(
|
|
2383
|
+
SERVE_CONTROL_EXT_METHODS.createCurrentSessionScheduledTask
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
const callerSessionId = params["callerSessionId"];
|
|
2387
|
+
const promptId = params["promptId"];
|
|
2388
|
+
const cron = params["cron"];
|
|
2389
|
+
const prompt = params["prompt"];
|
|
2390
|
+
const recurring = params["recurring"];
|
|
2391
|
+
if (typeof callerSessionId !== "string" || callerSessionId.length === 0 || !this.ownsSession(callerSessionId)) {
|
|
2392
|
+
throw RequestError.invalidParams(
|
|
2393
|
+
void 0,
|
|
2394
|
+
"`callerSessionId` must name a session owned by this connection"
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
if (typeof promptId !== "string" || promptId.length === 0) {
|
|
2398
|
+
throw RequestError.invalidParams(
|
|
2399
|
+
void 0,
|
|
2400
|
+
"`promptId` must be a non-empty string"
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
if (typeof cron !== "string" || cron.length === 0 || cron.length > MAX_SCHEDULED_TASK_CRON_CHARS) {
|
|
2404
|
+
throw RequestError.invalidParams(
|
|
2405
|
+
void 0,
|
|
2406
|
+
`\`cron\` must be a non-empty string within the ${MAX_SCHEDULED_TASK_CRON_CHARS}-character limit`
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2409
|
+
if (typeof prompt !== "string" || prompt.length === 0 || prompt.length > MAX_SCHEDULED_TASK_PROMPT_CHARS) {
|
|
2410
|
+
throw RequestError.invalidParams(
|
|
2411
|
+
void 0,
|
|
2412
|
+
`\`prompt\` must be non-empty and within the ${MAX_SCHEDULED_TASK_PROMPT_CHARS}-character limit`
|
|
2413
|
+
);
|
|
2414
|
+
}
|
|
2415
|
+
if (typeof recurring !== "boolean") {
|
|
2416
|
+
throw RequestError.invalidParams(
|
|
2417
|
+
void 0,
|
|
2418
|
+
"`recurring` must be a boolean"
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
const entry = this.resolveEntry(callerSessionId);
|
|
2422
|
+
if (!entry || entry.sessionId !== callerSessionId || entry.promptActive !== true || entry.activePromptId !== promptId) {
|
|
2423
|
+
throw RequestError.invalidParams(
|
|
2424
|
+
void 0,
|
|
2425
|
+
"The caller session does not own the active prompt"
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
if (entry.parentSessionId !== void 0 || entry.sourceId !== void 0 || entry.sourceType !== void 0 && entry.sourceType !== "default") {
|
|
2429
|
+
throw RequestError.invalidParams(
|
|
2430
|
+
void 0,
|
|
2431
|
+
"The caller session source cannot own a scheduled task"
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
const result = await this.onCreateCurrentSessionScheduledTask({
|
|
2435
|
+
callerSessionId,
|
|
2436
|
+
promptId,
|
|
2437
|
+
cron,
|
|
2438
|
+
prompt,
|
|
2439
|
+
recurring,
|
|
2440
|
+
assertCallerPromptActive: /* @__PURE__ */ __name(() => {
|
|
2441
|
+
const currentEntry = this.resolveEntry(callerSessionId);
|
|
2442
|
+
if (currentEntry !== entry || currentEntry.promptActive !== true || currentEntry.activePromptId !== promptId) {
|
|
2443
|
+
throw RequestError.invalidParams(
|
|
2444
|
+
void 0,
|
|
2445
|
+
"The caller session no longer owns the active prompt"
|
|
2446
|
+
);
|
|
2447
|
+
}
|
|
2448
|
+
}, "assertCallerPromptActive")
|
|
2449
|
+
}).catch(
|
|
2450
|
+
(error) => preserveScheduledTaskCreateErrorOverAcp(error)
|
|
2451
|
+
);
|
|
2452
|
+
if (typeof result.id !== "string" || result.id.length === 0 || typeof result.cron !== "string" || result.cron.length === 0) {
|
|
2453
|
+
throw RequestError.internalError(
|
|
2454
|
+
void 0,
|
|
2455
|
+
"Scheduled-task host returned an invalid result"
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
return { id: result.id, cron: result.cron };
|
|
2459
|
+
}
|
|
2460
|
+
async handleLiveScreenContextCapture(params) {
|
|
2461
|
+
const handler = this.getLiveScreenContextCaptureHandler();
|
|
2462
|
+
if (!handler) {
|
|
2463
|
+
throw RequestError.methodNotFound(
|
|
2464
|
+
SERVE_CONTROL_EXT_METHODS.liveCaptureScreenContext
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
const callerSessionId = params["callerSessionId"];
|
|
2468
|
+
if (typeof callerSessionId !== "string" || callerSessionId.length === 0 || !this.ownsSession(callerSessionId)) {
|
|
2469
|
+
throw RequestError.invalidParams(
|
|
2470
|
+
void 0,
|
|
2471
|
+
"`callerSessionId` is required and must name a session owned by this connection"
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
const result = await handler({ callerSessionId });
|
|
2475
|
+
if (result.appName.length === 0 || result.appName.length > 512 || result.windowTitle !== void 0 && result.windowTitle.length > 2048 || result.accessibilityText.length > MAX_LIVE_SCREEN_CONTEXT_TEXT_CHARS || result.screenshotPath.length === 0 || result.screenshotPath.length > 4096) {
|
|
2476
|
+
throw RequestError.internalError(void 0, "Invalid Appshot result.");
|
|
2477
|
+
}
|
|
2478
|
+
return {
|
|
2479
|
+
appName: result.appName,
|
|
2480
|
+
...result.windowTitle ? { windowTitle: result.windowTitle } : {},
|
|
2481
|
+
accessibilityText: result.accessibilityText,
|
|
2482
|
+
screenshotPath: result.screenshotPath
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
async handleLiveTaskTool(params) {
|
|
2486
|
+
const handler = this.getLiveTaskToolRequestHandler();
|
|
2487
|
+
if (!handler) {
|
|
2488
|
+
throw RequestError.methodNotFound(SERVE_CONTROL_EXT_METHODS.liveTaskTool);
|
|
2489
|
+
}
|
|
2490
|
+
const callerSessionId = params["callerSessionId"];
|
|
2491
|
+
const name = params["name"];
|
|
2492
|
+
const args = params["arguments"];
|
|
2493
|
+
if (typeof callerSessionId !== "string" || callerSessionId.length === 0 || !this.ownsSession(callerSessionId) || typeof name !== "string" || !LIVE_TASK_TOOL_NAMES.includes(
|
|
2494
|
+
name
|
|
2495
|
+
) || typeof args !== "object" || args === null || Array.isArray(args)) {
|
|
2496
|
+
throw RequestError.invalidParams(
|
|
2497
|
+
void 0,
|
|
2498
|
+
"Invalid Live task-tool request."
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
return handler({
|
|
2502
|
+
callerSessionId,
|
|
2503
|
+
name,
|
|
2504
|
+
arguments: args
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
async handleLiveSpeakToUser(params) {
|
|
2508
|
+
const handler = this.getLiveSpeakToUserHandler();
|
|
2509
|
+
if (!handler) {
|
|
2510
|
+
throw RequestError.methodNotFound(
|
|
2511
|
+
SERVE_CONTROL_EXT_METHODS.liveSpeakToUser
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2514
|
+
const callerSessionId = params["callerSessionId"];
|
|
2515
|
+
const message = params["message"];
|
|
2516
|
+
if (typeof callerSessionId !== "string" || callerSessionId.length === 0 || !this.ownsSession(callerSessionId) || typeof message !== "string" || message.trim().length === 0 || message.length > MAX_LIVE_SPEAK_TO_USER_MESSAGE_CHARS) {
|
|
2517
|
+
throw RequestError.invalidParams(
|
|
2518
|
+
void 0,
|
|
2519
|
+
"Invalid Live speak-to-user request."
|
|
2520
|
+
);
|
|
2521
|
+
}
|
|
2522
|
+
await handler({ callerSessionId, message });
|
|
2523
|
+
return { accepted: true };
|
|
2524
|
+
}
|
|
2525
|
+
/**
|
|
2526
|
+
* Handle child->bridge ACP `extNotification` calls. Recognized methods are
|
|
2527
|
+
* `qwen/notify/session/model-update`,
|
|
2528
|
+
* `qwen/notify/session/mode-update`,
|
|
2529
|
+
* `qwen/notify/session/title-update` (auto/in-process session titles),
|
|
2530
|
+
* `qwen/notify/session/recording-degraded`,
|
|
2531
|
+
* `qwen/notify/session/prompt-suggestion` (followup assist),
|
|
2532
|
+
* `qwen/notify/session/artifact-event` (hook artifacts),
|
|
2533
|
+
* `qwen/notify/session/terminal-sequence`, and
|
|
2534
|
+
* `_qwencode/end_turn` (background-notification and goal turns), and
|
|
2535
|
+
* `qwen/notify/session/mcp-budget-event` — each translated into a
|
|
2536
|
+
* session-scoped SSE frame. Unknown methods are dropped silently for
|
|
2537
|
+
* forward-compat.
|
|
2538
|
+
*/
|
|
2539
|
+
async extNotification(method, params) {
|
|
2540
|
+
const notificationSessionId = params["sessionId"];
|
|
2541
|
+
if (typeof notificationSessionId === "string" && this.abandonedRestoreIds.has(notificationSessionId)) {
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
if (method === ACTIVE_WORK_NOTIFICATION_METHOD) {
|
|
2545
|
+
const snapshot = parseActiveWorkSnapshot(params);
|
|
2546
|
+
if (snapshot) {
|
|
2547
|
+
this.onActiveWork?.({
|
|
2548
|
+
v: ACTIVE_WORK_HEARTBEAT_VERSION,
|
|
2549
|
+
seq: snapshot.seq,
|
|
2550
|
+
sessions: snapshot.sessions.filter(
|
|
2551
|
+
(session) => this.ownsSession(session.sessionId)
|
|
2552
|
+
)
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
if (method === "_qwencode/start_turn") {
|
|
2558
|
+
const sessionId2 = params["sessionId"];
|
|
2559
|
+
if (typeof sessionId2 !== "string" || sessionId2.length === 0 || params["source"] !== "goal") {
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
const entry = this.resolveEntry(sessionId2);
|
|
2563
|
+
if (!entry || !this.ownsSession(sessionId2)) return;
|
|
2564
|
+
entry.goalTurnActive = true;
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
if (method === "_qwencode/end_turn") {
|
|
2568
|
+
const sessionId2 = params["sessionId"];
|
|
2569
|
+
const reason = params["reason"];
|
|
2570
|
+
const source = params["source"];
|
|
2571
|
+
if (typeof sessionId2 !== "string" || sessionId2.length === 0 || typeof reason !== "string" || reason.length === 0 || reason.length > 128 || source !== "background_notification" && source !== "goal") {
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
const entry = this.resolveEntry(sessionId2);
|
|
2575
|
+
if (!entry || !this.ownsSession(sessionId2)) return;
|
|
2576
|
+
if (source === "goal") {
|
|
2577
|
+
entry.goalTurnActive = false;
|
|
2578
|
+
this.onGoalTurnEnded?.(sessionId2);
|
|
2579
|
+
const promptId = params["promptId"];
|
|
2580
|
+
if (typeof promptId !== "string" || promptId.length === 0 || promptId.length > 256) {
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
entry.events.publish({
|
|
2584
|
+
type: "turn_complete",
|
|
2585
|
+
promptId,
|
|
2586
|
+
data: { sessionId: sessionId2, stopReason: reason, promptId }
|
|
2587
|
+
});
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
entry.events.publish({
|
|
2591
|
+
type: "background_notification_turn_complete",
|
|
2592
|
+
data: { sessionId: sessionId2, reason }
|
|
2593
|
+
});
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
if (method === "qwen/notify/session/generation/event") {
|
|
2597
|
+
const sessionId2 = params["sessionId"];
|
|
2598
|
+
const requestId = params["requestId"];
|
|
2599
|
+
const event = params["event"];
|
|
2600
|
+
if (params["v"] !== 1 || typeof sessionId2 !== "string" || typeof requestId !== "string" || !event || typeof event !== "object" || Array.isArray(event)) {
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
const record = event;
|
|
2604
|
+
if (record["type"] === "started") {
|
|
2605
|
+
const model = record["model"];
|
|
2606
|
+
const modelSource = record["modelSource"];
|
|
2607
|
+
if (typeof model !== "string" || modelSource !== "fast" && modelSource !== "main") {
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
this.onGenerationEvent?.(sessionId2, {
|
|
2611
|
+
type: "started",
|
|
2612
|
+
requestId,
|
|
2613
|
+
model,
|
|
2614
|
+
modelSource
|
|
2615
|
+
});
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
if (record["type"] === "thinking") {
|
|
2619
|
+
this.onGenerationEvent?.(sessionId2, {
|
|
2620
|
+
type: "thinking",
|
|
2621
|
+
requestId
|
|
2622
|
+
});
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
if (record["type"] === "delta") {
|
|
2626
|
+
const seq = record["seq"];
|
|
2627
|
+
const text = record["text"];
|
|
2628
|
+
if (typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 0 || typeof text !== "string" || text.length === 0) {
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
this.onGenerationEvent?.(sessionId2, {
|
|
2632
|
+
type: "delta",
|
|
2633
|
+
requestId,
|
|
2634
|
+
seq,
|
|
2635
|
+
text
|
|
2636
|
+
});
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
if (method === "qwen/notify/workspace/generation/event") {
|
|
2642
|
+
const requestId = params["requestId"];
|
|
2643
|
+
const event = params["event"];
|
|
2644
|
+
if (params["v"] !== 1 || typeof requestId !== "string" || !event || typeof event !== "object" || Array.isArray(event)) {
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
const record = event;
|
|
2648
|
+
if (record["type"] === "started") {
|
|
2649
|
+
const model = record["model"];
|
|
2650
|
+
const modelSource = record["modelSource"];
|
|
2651
|
+
if (typeof model !== "string" || modelSource !== "fast" && modelSource !== "main") {
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
this.onWorkspaceGenerationEvent?.({
|
|
2655
|
+
type: "started",
|
|
2656
|
+
requestId,
|
|
2657
|
+
model,
|
|
2658
|
+
modelSource
|
|
2659
|
+
});
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
if (record["type"] === "thinking") {
|
|
2663
|
+
this.onWorkspaceGenerationEvent?.({ type: "thinking", requestId });
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
if (record["type"] === "delta") {
|
|
2667
|
+
const seq = record["seq"];
|
|
2668
|
+
const text = record["text"];
|
|
2669
|
+
if (typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 0 || typeof text !== "string" || text.length === 0) {
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
this.onWorkspaceGenerationEvent?.({
|
|
2673
|
+
type: "delta",
|
|
2674
|
+
requestId,
|
|
2675
|
+
seq,
|
|
2676
|
+
text
|
|
2677
|
+
});
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
if (method === "qwen/notify/session/model-update") {
|
|
2683
|
+
this.handleInSessionModelUpdate(params);
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
if (method === "qwen/notify/session/mode-update") {
|
|
2687
|
+
this.handleInSessionModeUpdate(params);
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
if (method === "qwen/notify/session/title-update") {
|
|
2691
|
+
const sessionId2 = params["sessionId"];
|
|
2692
|
+
const title = params["title"];
|
|
2693
|
+
if (typeof sessionId2 !== "string" || typeof title !== "string" || !title)
|
|
2694
|
+
return;
|
|
2695
|
+
const entry = this.resolveEntry(sessionId2);
|
|
2696
|
+
if (!entry) return;
|
|
2697
|
+
this.onSessionCatalogChanged?.();
|
|
2698
|
+
try {
|
|
2699
|
+
entry.events.publish({
|
|
2700
|
+
type: "session_metadata_updated",
|
|
2701
|
+
data: {
|
|
2702
|
+
sessionId: sessionId2,
|
|
2703
|
+
displayName: title,
|
|
2704
|
+
...typeof params["titleSource"] === "string" ? { titleSource: params["titleSource"] } : {}
|
|
2705
|
+
}
|
|
2706
|
+
});
|
|
2707
|
+
} catch {
|
|
2708
|
+
}
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
if (method === "qwen/notify/session/recording-degraded") {
|
|
2712
|
+
const sessionId2 = params["sessionId"];
|
|
2713
|
+
if (params["v"] !== 1 || typeof sessionId2 !== "string" || sessionId2.length === 0 || params["reason"] !== "write_failed") {
|
|
2714
|
+
writeStderrLine(
|
|
2715
|
+
`[demux] session=${typeof sessionId2 === "string" ? sessionId2 : "<missing>"} type=session_recording_degraded action=dropped reason=malformed`
|
|
2716
|
+
);
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
const entry = this.resolveEntry(sessionId2);
|
|
2720
|
+
if (entry && !this.ownsSession(sessionId2)) {
|
|
2721
|
+
writeStderrLine(
|
|
2722
|
+
`[demux] session=${sessionId2} type=session_recording_degraded action=dropped reason=session_not_owned`
|
|
2723
|
+
);
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
if (entry) entry.recordingDegraded = true;
|
|
2727
|
+
this.publishExtNotification(sessionId2, "session_recording_degraded", {
|
|
2728
|
+
sessionId: sessionId2,
|
|
2729
|
+
reason: "write_failed"
|
|
2730
|
+
});
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
if (method === "qwen/notify/session/prompt-suggestion") {
|
|
2734
|
+
const sessionId2 = params["sessionId"];
|
|
2735
|
+
const suggestion = params["suggestion"];
|
|
2736
|
+
const promptId = params["promptId"];
|
|
2737
|
+
if (typeof sessionId2 !== "string" || typeof suggestion !== "string" || suggestion.length === 0 || suggestion.length > MAX_SUGGESTION_LENGTH || typeof promptId !== "string") {
|
|
2738
|
+
writeStderrLine(
|
|
2739
|
+
`[demux] session=${typeof sessionId2 === "string" ? sessionId2 : "<missing>"} type=prompt_suggestion action=dropped reason=malformed`
|
|
2740
|
+
);
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
const entry = this.resolveEntry(sessionId2);
|
|
2744
|
+
if (!entry) return;
|
|
2745
|
+
entry.events.publish({
|
|
2746
|
+
type: "followup_suggestion",
|
|
2747
|
+
data: { sessionId: sessionId2, suggestion, promptId }
|
|
2748
|
+
});
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2751
|
+
if (method === "qwen/notify/session/terminal-sequence") {
|
|
2752
|
+
const sessionId2 = params["sessionId"];
|
|
2753
|
+
if (typeof sessionId2 !== "string") return;
|
|
2754
|
+
const { v: _v2, sessionId: _sid2, ...rest2 } = params;
|
|
2755
|
+
void _v2;
|
|
2756
|
+
void _sid2;
|
|
2757
|
+
this.publishExtNotification(sessionId2, "terminal_sequence", rest2, true);
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
if (method === "qwen/notify/session/artifact-event") {
|
|
2761
|
+
await this.handleArtifactEvent(params);
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
if (method !== "qwen/notify/session/mcp-budget-event") return;
|
|
2765
|
+
const sessionId = params["sessionId"];
|
|
2766
|
+
if (typeof sessionId !== "string") return;
|
|
2767
|
+
const kind = params["kind"];
|
|
2768
|
+
let type;
|
|
2769
|
+
if (kind === "budget_warning") {
|
|
2770
|
+
type = "mcp_budget_warning";
|
|
2771
|
+
} else if (kind === "refused_batch") {
|
|
2772
|
+
type = "mcp_child_refused_batch";
|
|
2773
|
+
} else {
|
|
2774
|
+
return;
|
|
2775
|
+
}
|
|
2776
|
+
const { v: _v, sessionId: _sid, kind: _kind, ...rest } = params;
|
|
2777
|
+
void _v;
|
|
2778
|
+
void _sid;
|
|
2779
|
+
void _kind;
|
|
2780
|
+
this.publishExtNotification(sessionId, type, rest);
|
|
2781
|
+
}
|
|
2782
|
+
async handleArtifactEvent(params) {
|
|
2783
|
+
const sessionId = params["sessionId"];
|
|
2784
|
+
const rawArtifacts = params["artifacts"];
|
|
2785
|
+
if (typeof sessionId !== "string" || !Array.isArray(rawArtifacts)) {
|
|
2786
|
+
writeStderrLine(
|
|
2787
|
+
`[demux] session=${typeof sessionId === "string" ? sessionId : "<missing>"} type=artifact_event action=dropped reason=malformed`
|
|
2788
|
+
);
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
if (!this.ownsSession(sessionId) && !this.inFlightRestoreIds.has(sessionId)) {
|
|
2792
|
+
writeStderrLine(
|
|
2793
|
+
`[demux] session=${sessionId} type=artifact_event action=dropped reason=session_not_owned`
|
|
2794
|
+
);
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
const entry = this.resolveEntry(sessionId);
|
|
2798
|
+
if (!entry) {
|
|
2799
|
+
writeStderrLine(
|
|
2800
|
+
`[demux] session=${sessionId} type=artifact_event action=dropped reason=session_not_found`
|
|
2801
|
+
);
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
const hookEventName = typeof params["hookEventName"] === "string" ? params["hookEventName"] : void 0;
|
|
2805
|
+
const toolName = typeof params["toolName"] === "string" ? params["toolName"] : void 0;
|
|
2806
|
+
const toolCallId = typeof params["toolCallId"] === "string" ? params["toolCallId"] : void 0;
|
|
2807
|
+
const artifacts = extractCappedArtifactInputs(
|
|
2808
|
+
rawArtifacts,
|
|
2809
|
+
entry.artifacts.inputBatchLimit(),
|
|
2810
|
+
entry.sessionId,
|
|
2811
|
+
"hook",
|
|
2812
|
+
(artifact) => ({
|
|
2813
|
+
...artifactPayloadFields(artifact),
|
|
2814
|
+
source: "hook",
|
|
2815
|
+
hookEventName,
|
|
2816
|
+
toolName,
|
|
2817
|
+
toolCallId
|
|
2818
|
+
})
|
|
2819
|
+
);
|
|
2820
|
+
const turn = {
|
|
2821
|
+
...entry.activePromptId ? { promptId: entry.activePromptId } : {},
|
|
2822
|
+
...entry.activePromptOriginatorClientId ? { originatorClientId: entry.activePromptOriginatorClientId } : {}
|
|
2823
|
+
};
|
|
2824
|
+
await this.upsertAndPublishArtifacts(entry, artifacts, void 0, turn);
|
|
2825
|
+
}
|
|
2826
|
+
async upsertAndPublishArtifacts(entry, artifacts, options, turn = {}) {
|
|
2827
|
+
if (!entry.artifactWorkspaceReady) {
|
|
2828
|
+
this.deferArtifactBatch(entry, artifacts, options, turn);
|
|
2829
|
+
writeStderrLine(
|
|
2830
|
+
`[artifacts] session=${entry.sessionId} action=deferred reason=workspace_not_bound`
|
|
2831
|
+
);
|
|
2832
|
+
return;
|
|
2833
|
+
}
|
|
2834
|
+
try {
|
|
2835
|
+
await entry.prepareArtifactWorkspace?.();
|
|
2836
|
+
} catch {
|
|
2837
|
+
this.deferArtifactBatch(entry, artifacts, options, turn);
|
|
2838
|
+
writeStderrLine(
|
|
2839
|
+
`[artifacts] session=${entry.sessionId} action=deferred reason=workspace_prepare_failed`
|
|
2840
|
+
);
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
await this.upsertAndPublishArtifactsReady(entry, artifacts, options, turn);
|
|
2844
|
+
}
|
|
2845
|
+
deferArtifactBatch(entry, artifacts, options, turn) {
|
|
2846
|
+
if (artifacts.length === 0) return;
|
|
2847
|
+
entry.deferredArtifactBatches.push({ artifacts, options, turn });
|
|
2848
|
+
entry.deferredArtifactInputCount += artifacts.length;
|
|
2849
|
+
const limit = entry.artifacts.inputBatchLimit();
|
|
2850
|
+
let truncated = false;
|
|
2851
|
+
while (entry.deferredArtifactInputCount > limit) {
|
|
2852
|
+
const first = entry.deferredArtifactBatches[0];
|
|
2853
|
+
if (!first) break;
|
|
2854
|
+
const overflow = entry.deferredArtifactInputCount - limit;
|
|
2855
|
+
if (first.artifacts.length <= overflow) {
|
|
2856
|
+
entry.deferredArtifactBatches.shift();
|
|
2857
|
+
entry.deferredArtifactInputCount -= first.artifacts.length;
|
|
2858
|
+
} else {
|
|
2859
|
+
first.artifacts = first.artifacts.slice(overflow);
|
|
2860
|
+
entry.deferredArtifactInputCount -= overflow;
|
|
2861
|
+
}
|
|
2862
|
+
truncated = true;
|
|
2863
|
+
}
|
|
2864
|
+
if (truncated) {
|
|
2865
|
+
writeStderrLine(
|
|
2866
|
+
`[artifacts] session=${entry.sessionId} action=deferred_truncated reason=workspace_not_bound`
|
|
2867
|
+
);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
async upsertAndPublishArtifactsReady(entry, artifacts, options, turn = {}) {
|
|
2871
|
+
try {
|
|
2872
|
+
const result = await entry.artifacts.upsertMany(artifacts, options);
|
|
2873
|
+
for (const warning of result.warnings ?? []) {
|
|
2874
|
+
writeStderrLine(
|
|
2875
|
+
`[artifacts] session=${entry.sessionId} action=warning reason=${JSON.stringify(
|
|
2876
|
+
warning
|
|
2877
|
+
)}`
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2880
|
+
this.publishArtifactChanges(entry, result.changes, turn);
|
|
2881
|
+
return true;
|
|
2882
|
+
} catch (error) {
|
|
2883
|
+
writeStderrLine(
|
|
2884
|
+
`[artifacts] session=${entry.sessionId} action=dropped reason=${JSON.stringify(
|
|
2885
|
+
artifactIngestionErrorReason(error)
|
|
2886
|
+
)}`
|
|
2887
|
+
);
|
|
2888
|
+
return false;
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
publishArtifactChanges(entry, changes, turn) {
|
|
2892
|
+
for (const change of changes) {
|
|
2893
|
+
entry.events.publish({
|
|
2894
|
+
type: "artifact_changed",
|
|
2895
|
+
data: { sessionId: entry.sessionId, change },
|
|
2896
|
+
...turn
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
publishExtNotification(sessionId, type, data, turnScoped = false) {
|
|
2901
|
+
if (this.abandonedRestoreIds.has(sessionId)) {
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
const entry = this.resolveEntry(sessionId);
|
|
2905
|
+
const frame = {
|
|
2906
|
+
type,
|
|
2907
|
+
data,
|
|
2908
|
+
...turnScoped && entry?.activePromptId ? { promptId: entry.activePromptId } : {},
|
|
2909
|
+
...entry?.activePromptOriginatorClientId ? { originatorClientId: entry.activePromptOriginatorClientId } : {}
|
|
2910
|
+
};
|
|
2911
|
+
if (entry) {
|
|
2912
|
+
entry.events.publish(frame);
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
this.bufferEarlyEvent(sessionId, frame);
|
|
2916
|
+
}
|
|
2917
|
+
/**
|
|
2918
|
+
* Promote an in-session `current_model_update` extNotification to a
|
|
2919
|
+
* `model_switched` bus event. Suppressed while the bridge is driving
|
|
2920
|
+
* its own model roundtrip (`entry.modelRoundtripInFlight`) — there the
|
|
2921
|
+
* bridge publishes the authoritative `model_switched`, so promoting
|
|
2922
|
+
* here too would double-publish. A structured log records the decision
|
|
2923
|
+
* so the `dropped` case is observable.
|
|
2924
|
+
*/
|
|
2925
|
+
handleInSessionModelUpdate(params) {
|
|
2926
|
+
const sessionId = params["sessionId"];
|
|
2927
|
+
const currentModelId = params["currentModelId"];
|
|
2928
|
+
if (typeof sessionId !== "string" || typeof currentModelId !== "string") {
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
const entry = this.resolveEntry(sessionId);
|
|
2932
|
+
if (!entry) {
|
|
2933
|
+
writeStderrLine(
|
|
2934
|
+
`[demux] session=${sessionId} type=current_model_update action=dropped reason=no_entry`
|
|
2935
|
+
);
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
if (entry.modelRoundtripInFlight) {
|
|
2939
|
+
writeStderrLine(
|
|
2940
|
+
`[demux] session=${sessionId} type=current_model_update action=suppressed reason=bridge_roundtrip_in_flight`
|
|
2941
|
+
);
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
if (this.onModelPromoted) {
|
|
2945
|
+
this.onModelPromoted(
|
|
2946
|
+
entry,
|
|
2947
|
+
currentModelId,
|
|
2948
|
+
entry.activePromptOriginatorClientId
|
|
2949
|
+
);
|
|
2950
|
+
} else {
|
|
2951
|
+
entry.events.publish({
|
|
2952
|
+
type: "model_switched",
|
|
2953
|
+
...entry.activePromptId ? { promptId: entry.activePromptId } : {},
|
|
2954
|
+
data: { sessionId, modelId: currentModelId },
|
|
2955
|
+
...entry.activePromptOriginatorClientId ? { originatorClientId: entry.activePromptOriginatorClientId } : {}
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
writeStderrLine(
|
|
2959
|
+
`[demux] session=${sessionId} type=current_model_update action=promoted model=${currentModelId}`
|
|
2960
|
+
);
|
|
2961
|
+
}
|
|
2962
|
+
/**
|
|
2963
|
+
* A2: promote an in-session `current_mode_update` extNotification to
|
|
2964
|
+
* `approval_mode_changed`. Uses the same suppression pattern as
|
|
2965
|
+
* `handleInSessionModelUpdate` — suppressed while the bridge is driving
|
|
2966
|
+
* its own approval-mode roundtrip (`entry.approvalModeRoundtripInFlight`)
|
|
2967
|
+
* — but diverges with two additions the model handler lacks: enum
|
|
2968
|
+
* validation against `KNOWN_APPROVAL_MODES`, and a legacy
|
|
2969
|
+
* `session_update{current_mode_update}` dual-emit for IDE companion
|
|
2970
|
+
* compat (transition — see §6 of the design doc), itself deduped via the
|
|
2971
|
+
* `legacyFrameSent` flag.
|
|
2972
|
+
*/
|
|
2973
|
+
handleInSessionModeUpdate(params) {
|
|
2974
|
+
const sessionId = params["sessionId"];
|
|
2975
|
+
const currentModeId = params["currentModeId"];
|
|
2976
|
+
if (typeof sessionId !== "string" || typeof currentModeId !== "string") {
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
if (!KNOWN_APPROVAL_MODES.has(currentModeId)) {
|
|
2980
|
+
writeStderrLine(
|
|
2981
|
+
`[demux] session=${sessionId} type=current_mode_update action=dropped reason=unknown_mode mode=${currentModeId}`
|
|
2982
|
+
);
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
const entry = this.resolveEntry(sessionId);
|
|
2986
|
+
if (!entry) {
|
|
2987
|
+
writeStderrLine(
|
|
2988
|
+
`[demux] session=${sessionId} type=current_mode_update action=dropped reason=no_entry`
|
|
2989
|
+
);
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
if (entry.approvalModeRoundtripInFlight) {
|
|
2993
|
+
writeStderrLine(
|
|
2994
|
+
`[demux] session=${sessionId} type=current_mode_update action=suppressed reason=bridge_roundtrip_in_flight`
|
|
2995
|
+
);
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
if (this.onModePromoted) {
|
|
2999
|
+
this.onModePromoted(
|
|
3000
|
+
entry,
|
|
3001
|
+
currentModeId,
|
|
3002
|
+
entry.activePromptOriginatorClientId
|
|
3003
|
+
);
|
|
3004
|
+
} else {
|
|
3005
|
+
entry.events.publish({
|
|
3006
|
+
type: "approval_mode_changed",
|
|
3007
|
+
...entry.activePromptId ? { promptId: entry.activePromptId } : {},
|
|
3008
|
+
data: {
|
|
3009
|
+
sessionId,
|
|
3010
|
+
previous: "default",
|
|
3011
|
+
next: currentModeId,
|
|
3012
|
+
persisted: false
|
|
3013
|
+
},
|
|
3014
|
+
...entry.activePromptOriginatorClientId ? { originatorClientId: entry.activePromptOriginatorClientId } : {}
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
if (params["legacyFrameSent"] === true) {
|
|
3018
|
+
writeStderrLine(
|
|
3019
|
+
`[demux] session=${sessionId} type=current_mode_update action=promoted mode=${currentModeId} legacy_frame=skipped`
|
|
3020
|
+
);
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
entry.events.publish({
|
|
3024
|
+
type: "session_update",
|
|
3025
|
+
...entry.activePromptId ? { promptId: entry.activePromptId } : {},
|
|
3026
|
+
data: {
|
|
3027
|
+
sessionId,
|
|
3028
|
+
update: {
|
|
3029
|
+
sessionUpdate: "current_mode_update",
|
|
3030
|
+
currentModeId
|
|
3031
|
+
}
|
|
3032
|
+
},
|
|
3033
|
+
...entry.activePromptOriginatorClientId ? { originatorClientId: entry.activePromptOriginatorClientId } : {}
|
|
3034
|
+
});
|
|
3035
|
+
writeStderrLine(
|
|
3036
|
+
`[demux] session=${sessionId} type=current_mode_update action=promoted mode=${currentModeId}`
|
|
3037
|
+
);
|
|
3038
|
+
}
|
|
3039
|
+
/**
|
|
3040
|
+
* Enqueue `frame` for `sessionId`. Lazy TTL sweep runs first so
|
|
3041
|
+
* caller doesn't pay for stale entries before deciding whether
|
|
3042
|
+
* the session-cap is reached. New sessionIds past
|
|
3043
|
+
* `MAX_EARLY_EVENT_SESSIONS` are dropped (defense against a
|
|
3044
|
+
* malicious / buggy child fanning out fake sessionIds); same-
|
|
3045
|
+
* sessionId frames past `MAX_EARLY_EVENTS_PER_SESSION` are
|
|
3046
|
+
* dropped to bound per-session memory.
|
|
3047
|
+
*/
|
|
3048
|
+
bufferEarlyEvent(sessionId, frame) {
|
|
3049
|
+
if (this.abandonedRestoreIds.has(sessionId)) {
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
const now = Date.now();
|
|
3053
|
+
this.sweepExpiredTombstones(now);
|
|
3054
|
+
if (this.tombstonedSessionIds.has(sessionId) && !this.inFlightSessionRegistrationIds.has(sessionId)) {
|
|
3055
|
+
writeStderrLine(
|
|
3056
|
+
`qwen serve: dropping early extNotification for tombstoned session ${JSON.stringify(sessionId)} (post-close stale event)`
|
|
3057
|
+
);
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
this.sweepExpiredEarlyEvents(now);
|
|
3061
|
+
let buf = this.earlyEvents.get(sessionId);
|
|
3062
|
+
if (!buf) {
|
|
3063
|
+
if (this.earlyEvents.size >= MAX_EARLY_EVENT_SESSIONS) {
|
|
3064
|
+
writeStderrLine(
|
|
3065
|
+
`qwen serve: dropping early extNotification \u2014 early-event buffer at MAX_EARLY_EVENT_SESSIONS (${MAX_EARLY_EVENT_SESSIONS}); possible session-id fanout abuse`
|
|
3066
|
+
);
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
buf = { frames: [], expiresAt: now + EARLY_EVENT_TTL_MS };
|
|
3070
|
+
this.earlyEvents.set(sessionId, buf);
|
|
3071
|
+
}
|
|
3072
|
+
if (buf.frames.length >= MAX_EARLY_EVENTS_PER_SESSION) {
|
|
3073
|
+
writeStderrLine(
|
|
3074
|
+
`qwen serve: dropping early extNotification for session ${JSON.stringify(sessionId)} \u2014 per-session cap (${MAX_EARLY_EVENTS_PER_SESSION}) reached`
|
|
3075
|
+
);
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3078
|
+
buf.frames.push(frame);
|
|
3079
|
+
}
|
|
3080
|
+
sweepExpiredEarlyEvents(now) {
|
|
3081
|
+
for (const [sid, buf] of this.earlyEvents) {
|
|
3082
|
+
if (buf.expiresAt <= now) this.earlyEvents.delete(sid);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
sweepExpiredTombstones(now) {
|
|
3086
|
+
for (const [sid, expiresAt] of this.tombstonedSessionIds) {
|
|
3087
|
+
if (expiresAt <= now) this.tombstonedSessionIds.delete(sid);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
/**
|
|
3091
|
+
* Mark a sessionId as closed so a late `extNotification` from the
|
|
3092
|
+
* dying child can't leak into the early-event buffer. Bridge factory
|
|
3093
|
+
* calls this from every `byId.delete(sid)` site (kill path,
|
|
3094
|
+
* channel.exited handler, closeSession). Idempotent on already-
|
|
3095
|
+
* tombstoned ids — refreshes the TTL so a recently-killed id stays
|
|
3096
|
+
* dead long enough for any in-flight stale frames to expire.
|
|
3097
|
+
*/
|
|
3098
|
+
markSessionClosed(sessionId) {
|
|
3099
|
+
const now = Date.now();
|
|
3100
|
+
this.sweepExpiredTombstones(now);
|
|
3101
|
+
this.tombstonedSessionIds.set(sessionId, now + EARLY_EVENT_TTL_MS);
|
|
3102
|
+
this.earlyEvents.delete(sessionId);
|
|
3103
|
+
}
|
|
3104
|
+
/**
|
|
3105
|
+
* Mark a sessionId as currently being restored via `session/load` /
|
|
3106
|
+
* `session/resume`. While in this set, `bufferEarlyEvent` accepts
|
|
3107
|
+
* frames for the id even if it's tombstoned — so restore-time
|
|
3108
|
+
* early events from the freshly-restored child reach
|
|
3109
|
+
* `drainEarlyEvents` instead of being rejected by the tombstone.
|
|
3110
|
+
*
|
|
3111
|
+
* Bridge factory calls this BEFORE awaiting the ACP restore call.
|
|
3112
|
+
* `clearRestoreInFlight` is paired in the matching `finally` so a
|
|
3113
|
+
* failed restore doesn't leave a dangling allow-list entry.
|
|
3114
|
+
*/
|
|
3115
|
+
markRestoreInFlight(sessionId) {
|
|
3116
|
+
this.markSessionRegistrationInFlight(sessionId);
|
|
3117
|
+
this.inFlightRestoreIds.add(sessionId);
|
|
3118
|
+
}
|
|
3119
|
+
/**
|
|
3120
|
+
* Transfer an id from closed/abandoned ownership to a new registration
|
|
3121
|
+
* attempt before its ACP call starts. The in-flight allow-list is what lets
|
|
3122
|
+
* legitimate early notifications bypass the ordinary close tombstone until
|
|
3123
|
+
* `createSessionEntry` can drain them.
|
|
3124
|
+
*/
|
|
3125
|
+
markSessionRegistrationInFlight(sessionId) {
|
|
3126
|
+
this.clearAbandonedRestoreFence(sessionId);
|
|
3127
|
+
this.inFlightSessionRegistrationIds.add(sessionId);
|
|
3128
|
+
}
|
|
3129
|
+
/**
|
|
3130
|
+
* Drop the abandoned-restore fence for `sessionId`.
|
|
3131
|
+
*
|
|
3132
|
+
* The fence has no TTL and suppresses session updates, guardrail events,
|
|
3133
|
+
* and child notifications, so it must not outlive the abandoned attempt it
|
|
3134
|
+
* was raised for. The bridge clears it whenever a legitimate owner takes
|
|
3135
|
+
* the id — a new restore, or `createSessionEntry` registering a session
|
|
3136
|
+
* from any other route.
|
|
3137
|
+
*/
|
|
3138
|
+
clearAbandonedRestoreFence(sessionId) {
|
|
3139
|
+
this.abandonedRestoreIds.delete(sessionId);
|
|
3140
|
+
}
|
|
3141
|
+
/**
|
|
3142
|
+
* Companion to `markRestoreInFlight`. Bridge factory calls this when
|
|
3143
|
+
* the restore IIFE settles — after `createSessionEntry` runs
|
|
3144
|
+
* (success) or after the ACP restore call fails (error). Cleared to
|
|
3145
|
+
* prevent the Set from growing forever under high restore churn.
|
|
3146
|
+
*/
|
|
3147
|
+
clearRestoreInFlight(sessionId) {
|
|
3148
|
+
this.inFlightRestoreIds.delete(sessionId);
|
|
3149
|
+
this.clearSessionRegistrationInFlight(sessionId);
|
|
3150
|
+
}
|
|
3151
|
+
clearSessionRegistrationInFlight(sessionId) {
|
|
3152
|
+
this.inFlightSessionRegistrationIds.delete(sessionId);
|
|
3153
|
+
}
|
|
3154
|
+
markRestoreAbandoned(sessionId) {
|
|
3155
|
+
this.inFlightRestoreIds.delete(sessionId);
|
|
3156
|
+
this.inFlightSessionRegistrationIds.delete(sessionId);
|
|
3157
|
+
this.abandonedRestoreIds.add(sessionId);
|
|
3158
|
+
this.earlyEvents.delete(sessionId);
|
|
3159
|
+
}
|
|
3160
|
+
/**
|
|
3161
|
+
* Drain any frames buffered for `sessionId` onto `entry.events`.
|
|
3162
|
+
* Bridge calls this immediately after `byId.set(sessionId, entry)`
|
|
3163
|
+
* in `createSessionEntry`. The frames were captured before the
|
|
3164
|
+
* entry existed (e.g. MCP discovery during the child's `newSession`
|
|
3165
|
+
* handler), so draining them now lands them in the replay ring as
|
|
3166
|
+
* the FIRST events of this session.
|
|
3167
|
+
*
|
|
3168
|
+
* Public so the bridge factory can call it directly. Idempotent on
|
|
3169
|
+
* unknown sessionIds.
|
|
3170
|
+
*/
|
|
3171
|
+
drainEarlyEvents(sessionId, entry) {
|
|
3172
|
+
this.tombstonedSessionIds.delete(sessionId);
|
|
3173
|
+
const buf = this.earlyEvents.get(sessionId);
|
|
3174
|
+
if (!buf) return;
|
|
3175
|
+
for (const frame of buf.frames) {
|
|
3176
|
+
if (frame.type === "session_recording_degraded") {
|
|
3177
|
+
entry.recordingDegraded = true;
|
|
3178
|
+
}
|
|
3179
|
+
entry.events.publish(frame);
|
|
3180
|
+
}
|
|
3181
|
+
this.earlyEvents.delete(sessionId);
|
|
3182
|
+
}
|
|
3183
|
+
async writeTextFile(params) {
|
|
3184
|
+
if (this.fileSystem) {
|
|
3185
|
+
try {
|
|
3186
|
+
return await this.fileSystem.writeText(params);
|
|
3187
|
+
} catch (err) {
|
|
3188
|
+
preserveFsErrorOverAcp(err);
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
let realTarget = params.path;
|
|
3192
|
+
try {
|
|
3193
|
+
realTarget = await fs2.realpath(params.path);
|
|
3194
|
+
} catch (err) {
|
|
3195
|
+
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
3196
|
+
if (code !== "ENOENT") throw err;
|
|
3197
|
+
try {
|
|
3198
|
+
const linkTarget = await fs2.readlink(params.path);
|
|
3199
|
+
realTarget = path2.resolve(path2.dirname(params.path), linkTarget);
|
|
3200
|
+
} catch {
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
const tmp = `${realTarget}.${process.pid}.${Date.now()}.${randomUUID2()}.tmp`;
|
|
3204
|
+
let preserveMode;
|
|
3205
|
+
try {
|
|
3206
|
+
const targetStat = await fs2.stat(realTarget);
|
|
3207
|
+
preserveMode = {
|
|
3208
|
+
mode: targetStat.mode & 4095,
|
|
3209
|
+
uid: targetStat.uid,
|
|
3210
|
+
gid: targetStat.gid
|
|
3211
|
+
};
|
|
3212
|
+
} catch (err) {
|
|
3213
|
+
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
3214
|
+
if (code !== "ENOENT") throw err;
|
|
3215
|
+
}
|
|
3216
|
+
try {
|
|
3217
|
+
await fs2.writeFile(tmp, params.content, {
|
|
3218
|
+
encoding: "utf8",
|
|
3219
|
+
flag: "wx",
|
|
3220
|
+
mode: preserveMode?.mode ?? 384
|
|
3221
|
+
});
|
|
3222
|
+
if (preserveMode) {
|
|
3223
|
+
await fs2.chmod(tmp, preserveMode.mode).catch(() => {
|
|
3224
|
+
});
|
|
3225
|
+
await fs2.chown(tmp, preserveMode.uid, preserveMode.gid).catch(() => {
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
await fs2.rename(tmp, realTarget);
|
|
3229
|
+
} catch (err) {
|
|
3230
|
+
await fs2.unlink(tmp).catch(() => {
|
|
3231
|
+
});
|
|
3232
|
+
throw err;
|
|
3233
|
+
}
|
|
3234
|
+
return {};
|
|
3235
|
+
}
|
|
3236
|
+
async readTextFile(params) {
|
|
3237
|
+
if (this.fileSystem) {
|
|
3238
|
+
try {
|
|
3239
|
+
return await this.fileSystem.readText(params);
|
|
3240
|
+
} catch (err) {
|
|
3241
|
+
preserveFsErrorOverAcp(err);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
if (typeof params.limit === "number" && params.limit <= 0) {
|
|
3245
|
+
return { content: "" };
|
|
3246
|
+
}
|
|
3247
|
+
if (typeof params.limit === "number" && params.limit > 0 && !Number.isSafeInteger(params.limit)) {
|
|
3248
|
+
throw RequestError.invalidParams(
|
|
3249
|
+
void 0,
|
|
3250
|
+
`\`limit\` must be a positive integer, got ${params.limit}`
|
|
3251
|
+
);
|
|
3252
|
+
}
|
|
3253
|
+
if (typeof params.line === "number" && params.line > 0 && !Number.isSafeInteger(params.line)) {
|
|
3254
|
+
throw RequestError.invalidParams(
|
|
3255
|
+
void 0,
|
|
3256
|
+
`\`line\` must be a positive integer, got ${params.line}`
|
|
3257
|
+
);
|
|
3258
|
+
}
|
|
3259
|
+
const READ_FILE_SIZE_CAP = 100 * 1024 * 1024;
|
|
3260
|
+
const stats = await fs2.stat(params.path);
|
|
3261
|
+
if (!stats.isFile()) {
|
|
3262
|
+
throw new Error(
|
|
3263
|
+
`readTextFile: ${params.path} is not a regular file (reported as ${describeStatKind(stats)}). Pipe / device / proc-like inputs can produce unbounded data and aren't supported by the bridge fs proxy.`
|
|
3264
|
+
);
|
|
3265
|
+
}
|
|
3266
|
+
if (stats.size > READ_FILE_SIZE_CAP) {
|
|
3267
|
+
throw new Error(
|
|
3268
|
+
`readTextFile: ${params.path} is ${stats.size} bytes, exceeds the ${READ_FILE_SIZE_CAP}-byte daemon cap. Tail/grep externally and feed the relevant slice instead.`
|
|
3269
|
+
);
|
|
3270
|
+
}
|
|
3271
|
+
const content = await fs2.readFile(params.path, "utf8");
|
|
3272
|
+
if (typeof params.line === "number" || typeof params.limit === "number") {
|
|
3273
|
+
const startLine = params.line ?? 1;
|
|
3274
|
+
const start = startLine > 0 ? startLine - 1 : 0;
|
|
3275
|
+
const end = params.limit != null ? start + params.limit : void 0;
|
|
3276
|
+
return { content: sliceLineRange(content, start, end) };
|
|
3277
|
+
}
|
|
3278
|
+
return { content };
|
|
3279
|
+
}
|
|
3280
|
+
};
|
|
3281
|
+
var A2UI_TOOL_RE = /(^|__)(present_ui|present_choices|a2ui_action)$/;
|
|
3282
|
+
function isA2uiToolMeta(meta) {
|
|
3283
|
+
if (!meta) return false;
|
|
3284
|
+
if (typeof meta.serverId === "string" && meta.serverId.toLowerCase().includes("a2ui"))
|
|
3285
|
+
return true;
|
|
3286
|
+
return typeof meta.toolName === "string" && A2UI_TOOL_RE.test(meta.toolName);
|
|
3287
|
+
}
|
|
3288
|
+
__name(isA2uiToolMeta, "isA2uiToolMeta");
|
|
3289
|
+
function splitA2uiText(raw) {
|
|
3290
|
+
const s = raw.replace(/^\s+/, "");
|
|
3291
|
+
if (s[0] !== "[") return null;
|
|
3292
|
+
let depth = 0;
|
|
3293
|
+
let inStr = false;
|
|
3294
|
+
let esc = false;
|
|
3295
|
+
let end = -1;
|
|
3296
|
+
for (let i = 0; i < s.length; i++) {
|
|
3297
|
+
const c = s[i];
|
|
3298
|
+
if (inStr) {
|
|
3299
|
+
if (esc) esc = false;
|
|
3300
|
+
else if (c === "\\") esc = true;
|
|
3301
|
+
else if (c === '"') inStr = false;
|
|
3302
|
+
continue;
|
|
3303
|
+
}
|
|
3304
|
+
if (c === '"') inStr = true;
|
|
3305
|
+
else if (c === "[") depth++;
|
|
3306
|
+
else if (c === "]") {
|
|
3307
|
+
depth--;
|
|
3308
|
+
if (depth === 0) {
|
|
3309
|
+
end = i + 1;
|
|
3310
|
+
break;
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
if (end < 0) return null;
|
|
3315
|
+
try {
|
|
3316
|
+
const arr = JSON.parse(s.slice(0, end));
|
|
3317
|
+
if (!Array.isArray(arr) || arr.length === 0) return null;
|
|
3318
|
+
return [arr, s.slice(end).trim()];
|
|
3319
|
+
} catch {
|
|
3320
|
+
return null;
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
__name(splitA2uiText, "splitA2uiText");
|
|
3324
|
+
function surfaceIdOf(c) {
|
|
3325
|
+
const cmd = c;
|
|
3326
|
+
return cmd?.createSurface?.surfaceId ?? cmd?.updateComponents?.surfaceId ?? cmd?.updateDataModel?.surfaceId ?? cmd?.deleteSurface?.surfaceId;
|
|
3327
|
+
}
|
|
3328
|
+
__name(surfaceIdOf, "surfaceIdOf");
|
|
3329
|
+
function extractA2uiToolUpdate(params) {
|
|
3330
|
+
const update = params.update;
|
|
3331
|
+
if (!update || update["sessionUpdate"] !== "tool_call_update") return null;
|
|
3332
|
+
const meta = update["_meta"];
|
|
3333
|
+
if (!isA2uiToolMeta(meta)) return null;
|
|
3334
|
+
const content = update["content"];
|
|
3335
|
+
if (!Array.isArray(content)) return null;
|
|
3336
|
+
let split = null;
|
|
3337
|
+
let hitIndex = -1;
|
|
3338
|
+
for (let i = 0; i < content.length; i++) {
|
|
3339
|
+
const inner = content[i]?.content;
|
|
3340
|
+
if (typeof inner?.text === "string") {
|
|
3341
|
+
split = splitA2uiText(inner.text);
|
|
3342
|
+
if (split) {
|
|
3343
|
+
hitIndex = i;
|
|
3344
|
+
break;
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
if (!split) return null;
|
|
3349
|
+
const [commands, fallback] = split;
|
|
3350
|
+
const order = [];
|
|
3351
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3352
|
+
for (const c of commands) {
|
|
3353
|
+
const sid = surfaceIdOf(c);
|
|
3354
|
+
if (!sid) {
|
|
3355
|
+
const shape = c && typeof c === "object" ? Object.keys(c).join(",") || "empty object" : typeof c;
|
|
3356
|
+
writeStderrLine(
|
|
3357
|
+
`a2ui: dropping command with unrecognized shape (${shape})`
|
|
3358
|
+
);
|
|
3359
|
+
continue;
|
|
3360
|
+
}
|
|
3361
|
+
if (!grouped.has(sid)) {
|
|
3362
|
+
grouped.set(sid, []);
|
|
3363
|
+
order.push(sid);
|
|
3364
|
+
}
|
|
3365
|
+
grouped.get(sid).push(c);
|
|
3366
|
+
}
|
|
3367
|
+
const surfaces = order.map((sid) => ({
|
|
3368
|
+
surfaceId: sid,
|
|
3369
|
+
commands: grouped.get(sid)
|
|
3370
|
+
}));
|
|
3371
|
+
const sanitizedText = fallback || "[A2UI surface rendered]";
|
|
3372
|
+
const sanitizedContent = content.map(
|
|
3373
|
+
(block, i) => i === hitIndex ? {
|
|
3374
|
+
...block,
|
|
3375
|
+
content: {
|
|
3376
|
+
...block.content ?? {},
|
|
3377
|
+
text: sanitizedText
|
|
3378
|
+
}
|
|
3379
|
+
} : block
|
|
3380
|
+
);
|
|
3381
|
+
const sanitizedUpdate = {
|
|
3382
|
+
...update,
|
|
3383
|
+
content: sanitizedContent
|
|
3384
|
+
};
|
|
3385
|
+
if (typeof update["rawOutput"] === "string") {
|
|
3386
|
+
sanitizedUpdate["rawOutput"] = sanitizedText;
|
|
3387
|
+
}
|
|
3388
|
+
return {
|
|
3389
|
+
surfaces,
|
|
3390
|
+
callId: typeof update["toolCallId"] === "string" ? update["toolCallId"] : void 0,
|
|
3391
|
+
sanitizedParams: {
|
|
3392
|
+
...params,
|
|
3393
|
+
update: sanitizedUpdate
|
|
3394
|
+
}
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
__name(extractA2uiToolUpdate, "extractA2uiToolUpdate");
|
|
3398
|
+
|
|
3399
|
+
export {
|
|
3400
|
+
CANCEL_VOTE_SENTINEL,
|
|
3401
|
+
createNoOpPermissionAuditPublisher,
|
|
3402
|
+
MultiClientPermissionMediator,
|
|
3403
|
+
writeStderrLine,
|
|
3404
|
+
SessionAttachmentReferenceError,
|
|
3405
|
+
isSessionAttachmentReference,
|
|
3406
|
+
withAttachmentDegradationMarker,
|
|
3407
|
+
SessionAttachmentStore,
|
|
3408
|
+
KNOWN_APPROVAL_MODES,
|
|
3409
|
+
BridgeClient,
|
|
3410
|
+
isA2uiToolMeta
|
|
3411
|
+
};
|
|
3412
|
+
/**
|
|
3413
|
+
* @license
|
|
3414
|
+
* Copyright 2025 Qwen Team
|
|
3415
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
3416
|
+
*/
|