@shawnstack/quickforge 1.7.2 → 1.7.4
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/README.md +1 -0
- package/bin/quickforge.mjs +2 -21
- package/dist/assets/AgentProfilesPage-B84w-r9S.js +1 -0
- package/dist/assets/{ChatPanelHost-Cld9HcVz.js → ChatPanelHost-CyO8fEoj.js} +98 -98
- package/dist/assets/{PluginsPage-DEMqwhOA.js → PluginsPage-D2862wul.js} +1 -1
- package/dist/assets/{ScheduledTasksPage-DTP_gedp.js → ScheduledTasksPage-GqbiJDPX.js} +2 -2
- package/dist/assets/SettingsWorkspacePage-B53_eDf2.js +1628 -0
- package/dist/assets/SharedConversationPage-Cw2QFnbC.js +1 -0
- package/dist/assets/TerminalDock-Dgg-_ZmU.js +2 -0
- package/dist/assets/WorkspaceInspector-3uLiu6Bv.js +13 -0
- package/dist/assets/{abnfDiagram-VRR7QNED-Dl_Fvlqg.js → abnfDiagram-VRR7QNED-Dy7veffk.js} +1 -1
- package/dist/assets/{arc-Bhg4c0ht.js → arc-Bo3p2lGy.js} +1 -1
- package/dist/assets/architecture-TIHT7OUA-7FQo5GbP.js +1 -0
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-BuojN27B.js → architectureDiagram-ZJ3FMSHR-Cwx8qOvu.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-alDLbrR1.js → blockDiagram-677ZJIJ3-BiuaAOFa.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-D1lKa4VT.js → c4Diagram-LMCZKHZV-BTcGfhK2.js} +1 -1
- package/dist/assets/channel-DToA8d-4.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-D_Wxpk-T.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-CwWLNjMZ.js → chunk-32BRIVSS-B9PuF_RF.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-Be569m7d.js → chunk-52WLFC77-3zGItfIM.js} +1 -1
- package/dist/assets/{chunk-5FCAYU7R-BSvoiWV1.js → chunk-5FCAYU7R-DQ66qlmt.js} +1 -1
- package/dist/assets/{chunk-5HE753X5-CRgsy8WS.js → chunk-5HE753X5-CLqTO-VF.js} +1 -1
- package/dist/assets/{chunk-5JV3BV7I-CdW477UR.js → chunk-5JV3BV7I-Csxv1jr6.js} +1 -1
- package/dist/assets/{chunk-5TONJI2A-CdtKzSmp.js → chunk-5TONJI2A-BCQttpBr.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BKjjssLZ.js → chunk-5VM5RSS4-DEWJGSTb.js} +1 -1
- package/dist/assets/{chunk-7BUUIJ7U-ByAXiXdn.js → chunk-7BUUIJ7U-DNorm9MM.js} +1 -1
- package/dist/assets/{chunk-BIQX33UG-DeG8SCT1.js → chunk-BIQX33UG-DgvY_BOT.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-D_zHT68Q.js → chunk-C7G6YPKG-DMvn9IPs.js} +1 -1
- package/dist/assets/{chunk-CQNSW5MT-CHpHoUSu.js → chunk-CQNSW5MT-Dq2duUgr.js} +1 -1
- package/dist/assets/{chunk-CYSBUYHQ-CRP50Vov.js → chunk-CYSBUYHQ-BovYyc4Y.js} +1 -1
- package/dist/assets/{chunk-EMLP6XTP-DX6uQQ2v.js → chunk-EMLP6XTP-BMDOpuYN.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-DAJRNkHR.js → chunk-EX3LRPZG-6eP2UPOf.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-XbM5yomC.js → chunk-FWX5IMBZ-D4q-wTSy.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-Ci0ljnb1.js → chunk-HOUHSVGY-XjIcuzH7.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-Dy2WJ71z.js → chunk-ICXQ74PX-YBXbanq9.js} +1 -1
- package/dist/assets/{chunk-JG7HCLWE-2PrkzHkt.js → chunk-JG7HCLWE-CcN3uNXF.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-D32uacSM.js → chunk-JWPE2WC7-Cwg6AUAt.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-DIAFLtiS.js → chunk-MOJQB5TN-NNspcGIU.js} +1 -1
- package/dist/assets/{chunk-MOZMSUNE-BbscOaEz.js → chunk-MOZMSUNE-pMJ_jMos.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-ivBVxzKe.js → chunk-OGEWGWER-CJy-3Ldv.js} +1 -1
- package/dist/assets/{chunk-OSBZ3O6U-frD0hkdx.js → chunk-OSBZ3O6U-D7cLHHOA.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-B-37Rxae.js → chunk-PUDLZKDR-D5YBi1Pi.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-Y90m2RTe.js → chunk-Q4XR5HBZ-DYmqQdIf.js} +1 -1
- package/dist/assets/{chunk-QBLGF6JB-DixBqslB.js → chunk-QBLGF6JB-Bfcx00X0.js} +1 -1
- package/dist/assets/{chunk-R7FJI6CG-DXS1UXn_.js → chunk-R7FJI6CG-oXzadpZY.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-n4JxhNd0.js → chunk-RYQCIY6F-Dt3-8yUh.js} +1 -1
- package/dist/assets/{chunk-U6XO7XAA-yHlgxMmn.js → chunk-U6XO7XAA-B55eSvpx.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-Z6kuZfbl.js → chunk-V7JOEXUC-Btk4JwCt.js} +1 -1
- package/dist/assets/chunk-VAUOI2AC-CIZseNLi.js +1 -0
- package/dist/assets/{chunk-VR4S4FIN-Cs_SIQma.js → chunk-VR4S4FIN-DgaSiVOY.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-CGExYbV-.js → chunk-WYO6CB5R-rRDZ_Tta.js} +3 -3
- package/dist/assets/chunk-XXDRQBXY-Do-XYxat.js +1 -0
- package/dist/assets/{chunk-YOTPTUD7-DopPXWSl.js → chunk-YOTPTUD7-BnFxj5o4.js} +1 -1
- package/dist/assets/{chunk-ZGVPDNZ5-CJHOeqMb.js → chunk-ZGVPDNZ5-CrnlPlmJ.js} +1 -1
- package/dist/assets/{chunk-ZIRB5QZD-BBVc5mf6.js → chunk-ZIRB5QZD-BYTiATIH.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-Bu24Sy-k.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-DKhh5HSW.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-DLjV-dTm.js → cose-bilkent-JH36ORCC-BORKJRwF.js} +1 -1
- package/dist/assets/cynefin-VYW2F7L2-oyiAgYC4.js +1 -0
- package/dist/assets/{cynefinDiagram-TSTJHNR4-BfH3zVS-.js → cynefinDiagram-TSTJHNR4-BXKeaSl8.js} +1 -1
- package/dist/assets/{dagre-CY3D84Cl.js → dagre-CrA0XVwJ.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-BIhwZCIZ.js → dagre-VKFMJZFB-ouq_lA1a.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-BHnzNM2w.js → diagram-FQU43EPY-BxR5kUiQ.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-C7-ZlmFT.js → diagram-G47NLZAW-DWonk1sL.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-C-P9Y7IR.js → diagram-NH7WQ7WH-fmV_wV5e.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-Ci5lZ_T5.js → diagram-OA4YK3LP-DvOSRRiV.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-C1xuK2bs.js → diagram-WEI45ONY-Bp-xb3uS.js} +1 -1
- package/dist/assets/dist-DLKC_odd.js +1 -0
- package/dist/assets/{ebnfDiagram-CCIWWBDH-DKQ5NWMD.js → ebnfDiagram-CCIWWBDH-BTlnwneA.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-HAWySadP.js → erDiagram-Q63AITRT-C3fFWGCm.js} +1 -1
- package/dist/assets/esm-oa69b5u4.js +2 -0
- package/dist/assets/eventmodeling-45OFAUF4-YB_0MefX.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-Bc73xdBd.js +1 -0
- package/dist/assets/{ganttDiagram-NO4QXBWP-C1hwIcOA.js → ganttDiagram-NO4QXBWP-DzAiojNi.js} +1 -1
- package/dist/assets/gitGraph-TEB2WS4Q-Bvp27L0n.js +1 -0
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-DYUYxs1R.js → gitGraphDiagram-IHSO6WYX-Cv57oNEA.js} +1 -1
- package/dist/assets/icons-cTkqD_Gi.js +1 -0
- package/dist/assets/index-B48prgLL.js +65 -0
- package/dist/assets/index-DQhwHHer.css +3 -0
- package/dist/assets/info-DKCQHKI2-BsLOivgq.js +1 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-CnIL6tv4.js → infoDiagram-FWYZ7A6U-B5Z_O3Ro.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-B7WXNycQ.js → ishikawaDiagram-FXEZZL3T-CKTK_TKV.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-D4HvioyY.js → journeyDiagram-5HDEW3XC-Cm9k4ywW.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-CDs763eS.js → kanban-definition-HUTT4EX6-DzNteLwx.js} +1 -1
- package/dist/assets/{line-ikKkQiFp.js → line-BG78c_WJ.js} +1 -1
- package/dist/assets/{linear-GqPyxQkN.js → linear-1X20_EGu.js} +1 -1
- package/dist/assets/{map-CJwjCvwr.js → map-CCI8WZkS.js} +1 -1
- package/dist/assets/{mcp-servers-dialog-Bp6kbIup.js → mcp-servers-dialog-GXXD9K0U.js} +2 -2
- package/dist/assets/{mermaid-parser.core-CoE8cccr.js → mermaid-parser.core-Bb8HI_OS.js} +2 -2
- package/dist/assets/{mermaid-renderer-BmbSAmpG.js → mermaid-renderer-PIaNiXAz.js} +2 -2
- package/dist/assets/{mermaid.core-DlFSL90j.js → mermaid.core-DUHOIwL5.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-DLdq0K9C.js → mindmap-definition-LN4V7U3C-Tnbszo7b.js} +1 -1
- package/dist/assets/{monaco-BTsVCDWS.js → monaco-BVtCspYZ.js} +1 -1
- package/dist/assets/{ordinal-CibZU3PM.js → ordinal-v4prQcuq.js} +1 -1
- package/dist/assets/{otel-BNGM50C-.js → otel-D1LswXpM.js} +1 -1
- package/dist/assets/packet-7NZHBO7P-BKQkXRtk.js +1 -0
- package/dist/assets/{pegDiagram-2B236MQR-Cm1kPRC2.js → pegDiagram-2B236MQR-CX824Kd0.js} +1 -1
- package/dist/assets/{pi-ai-Cr3rpCXT.js → pi-ai-DAJlO9ZB.js} +2 -2
- package/dist/assets/{pi-web-ui-DoRGtREc.js → pi-web-ui-B-RJSxjR.js} +1 -1
- package/dist/assets/pie-RZYD4A2V-BTiTN5Ge.js +1 -0
- package/dist/assets/{pieDiagram-ENE6RG2P-Df05wjjo.js → pieDiagram-ENE6RG2P-BT04VWPQ.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-BjCwGxJC.js → quadrantDiagram-ABIIQ3AL-BvOZxkBr.js} +1 -1
- package/dist/assets/radar-I7S5WNFK-BU-oVjKM.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-7H2hxNbL.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-Mx6wf6XQ.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-BgXSKhYy.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6-DTEl2rAC.js +1 -0
- package/dist/assets/{railroadDiagram-RFXS5EU6-CTUvpKkv.js → railroadDiagram-RFXS5EU6-DnTU--BV.js} +1 -1
- package/dist/assets/{react-vendor-Dr5xvL-e.js → react-vendor-eWH0QhNU.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-BU1kvEku.js → requirementDiagram-TGXJPOKE--MZX_yEv.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-BHmvxVqH.js → sankeyDiagram-HTMAVEWB-SEZkHhYn.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-CTpwICgg.js → sequenceDiagram-DBY2YBRQ-BhC2OHiz.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-BbLhGaa4.js → sizeCapture-X5ZJPWSS-Ci5LeToV.js} +1 -1
- package/dist/assets/{skills-dialog-DiJAUfvW.js → skills-dialog-DDh1h8eH.js} +3 -1
- package/dist/assets/{src-B4Tvg3gT.js → src-CovulcRC.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-BxuDwIgT.js → stateDiagram-2N3HPSRC-DKK5tFT4.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-D8x3mkni.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-EkZsiylV.js → swimlanes-5IMT3BWC-DxxGCk67.js} +2 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-C4nOBNp8.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-C9ewOCTa.js → timeline-definition-FHXFAJF6-BPXB3Sue.js} +1 -1
- package/dist/assets/treeView-QDETBFTQ-DOE6NSdf.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-Ca7QD_b4.js +1 -0
- package/dist/assets/{vennDiagram-L72KCM5P-CR49Y4Lp.js → vennDiagram-L72KCM5P-CxcBCKye.js} +1 -1
- package/dist/assets/wardley-OPB4EBWU-D9O4RKRm.js +1 -0
- package/dist/assets/{wardleyDiagram-EHGQE667-Cn0GgwWi.js → wardleyDiagram-EHGQE667-DEL6Yes5.js} +1 -1
- package/dist/assets/web-RgX7H-OH.js +1 -0
- package/dist/assets/{xychartDiagram-FW5EYKEG-DlmPNAvd.js → xychartDiagram-FW5EYKEG-N8cPbcjC.js} +1 -1
- package/dist/index.html +12 -11
- package/dist/mobile-error.html +24 -0
- package/package.json +7 -1
- package/server/acp/server.mjs +14 -9
- package/server/agent-manager.mjs +215 -20
- package/server/ai-http-logger.mjs +69 -12
- package/server/ai-provider-options.mjs +11 -0
- package/server/channels/channel-logs.mjs +13 -0
- package/server/channels/event-relay.mjs +33 -0
- package/server/channels/process-channel.mjs +29 -0
- package/server/channels/providers/wechat.mjs +9 -3
- package/server/channels/registry.mjs +5 -1
- package/server/index.mjs +43 -25
- package/server/lan-access-store.mjs +93 -3
- package/server/network-proxy.mjs +449 -0
- package/server/plugins/registry.mjs +64 -4
- package/server/public-api.mjs +10 -2
- package/server/routes/agent-profiles.mjs +4 -2
- package/server/routes/agent.mjs +28 -0
- package/server/routes/channels.mjs +47 -0
- package/server/routes/lan-access.mjs +246 -21
- package/server/routes/models.mjs +7 -3
- package/server/routes/project.mjs +19 -1
- package/server/routes/scheduled-tasks.mjs +4 -2
- package/server/routes/static.mjs +26 -1
- package/server/routes/system.mjs +37 -0
- package/server/routes/terminal.mjs +7 -2
- package/server/routes/workspace.mjs +62 -9
- package/server/session-utils.mjs +4 -2
- package/server/subagents.mjs +1 -1
- package/server/terminal/terminal-manager.mjs +2 -4
- package/server/tools/definitions.mjs +2 -2
- package/server/tools/index.mjs +5 -3
- package/server/utils/package-update.mjs +70 -47
- package/server/utils/process-env.mjs +7 -0
- package/server/utils/response.mjs +2 -2
- package/dist/assets/AgentProfilesPage-BFgyFa5e.js +0 -1
- package/dist/assets/SettingsWorkspacePage-Dr9zT5Bs.js +0 -1495
- package/dist/assets/SharedConversationPage-op3DRwcw.js +0 -1
- package/dist/assets/TerminalDock-BL_UovwU.js +0 -2
- package/dist/assets/WorkspaceInspector-73dhAime.js +0 -13
- package/dist/assets/architecture-TIHT7OUA-BgP-IMeo.js +0 -1
- package/dist/assets/channel-Rz20dKTK.js +0 -1
- package/dist/assets/chunk-2Q5K7J3B-Cb14ZF_k.js +0 -1
- package/dist/assets/chunk-VAUOI2AC-BOZvd2Sn.js +0 -1
- package/dist/assets/chunk-XXDRQBXY-CD_EGCtT.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-C_ozTKCt.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-FOfFbbZF.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-BzPhLtd5.js +0 -1
- package/dist/assets/eventmodeling-45OFAUF4-BcgGbVJ-.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-dtkzcC8E.js +0 -1
- package/dist/assets/gitGraph-TEB2WS4Q-VByWboBU.js +0 -1
- package/dist/assets/icons-BP8YOS-Z.js +0 -1
- package/dist/assets/index-Cu2bBHLv.css +0 -3
- package/dist/assets/index-yV1wtqTr.js +0 -66
- package/dist/assets/info-DKCQHKI2-CZeDLZxx.js +0 -1
- package/dist/assets/packet-7NZHBO7P-decrrxyX.js +0 -1
- package/dist/assets/pie-RZYD4A2V-J1CasQZl.js +0 -1
- package/dist/assets/radar-I7S5WNFK-B-sZytUA.js +0 -1
- package/dist/assets/railroad-3IZDKUUU-BSaqNPm8.js +0 -1
- package/dist/assets/railroad-abnf-AHOZXSZD-lzZR7PxT.js +0 -1
- package/dist/assets/railroad-ebnf-EBAXGLYW-DQtz1Fh9.js +0 -1
- package/dist/assets/railroad-peg-LSFZ7HO6-CpMsuGY3.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-BGLshxcY.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-CqdqxA-b.js +0 -8
- package/dist/assets/treeView-QDETBFTQ-Byb71heW.js +0 -1
- package/dist/assets/treemap-6X3UGDF4-CMV1SJsq.js +0 -1
- package/dist/assets/wardley-OPB4EBWU-DtCHtr2V.js +0 -1
- /package/dist/assets/{array-CYkMkqnU.js → array-DqLCdDFv.js} +0 -0
- /package/dist/assets/{chunk-KEIR6QF5-77hsebd7.js → chunk-KEIR6QF5-s7Qq-R3i.js} +0 -0
- /package/dist/assets/{chunk-Y2CYZVJY-CQltPwYk.js → chunk-Y2CYZVJY-BgISNSiw.js} +0 -0
- /package/dist/assets/{css-utils-rkE68RDy.js → css-utils-31qzCXsk.js} +0 -0
- /package/dist/assets/{cytoscape.esm-DwdZnGOm.js → cytoscape.esm-iekjSZ2n.js} +0 -0
- /package/dist/assets/{defaultLocale-Beh6XjaL.js → defaultLocale-CrJzLgRD.js} +0 -0
- /package/dist/assets/{dist-DelZBZpK.js → dist-zzfqk5hx.js} +0 -0
- /package/dist/assets/{graphlib-CAISHZh4.js → graphlib-CIz_CfxW.js} +0 -0
- /package/dist/assets/{init-Rr1s_RiX.js → init-C0r9Gk5G.js} +0 -0
- /package/dist/assets/{lit-vendor-Dr3cpBGF.js → lit-vendor-DvaQX_UQ.js} +0 -0
- /package/dist/assets/{logger-B65Akg8A.js → logger-8REuLPGK.js} +0 -0
- /package/dist/assets/{path-BAQ3hXlG.js → path-DIKpVbHL.js} +0 -0
- /package/dist/assets/{plugin-api-BbUkYm3Y.js → plugin-api-C8hMT5W6.js} +0 -0
- /package/dist/assets/{rough.esm-Djo4Abte.js → rough.esm-BMOtDaaS.js} +0 -0
- /package/dist/assets/{xterm-CGiQzDiR.js → xterm-BFzZ1FH4.js} +0 -0
package/server/agent-manager.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { projectContextFromId, defaultGlobalWorkspaceContext, readProjectConfig } from './project-config.mjs'
|
|
29
29
|
import { ensureStorage, readStore, atomicUpdate, atomicSessionMetadataUpdate, readSessionValue, writeSessionValue, deleteSessionValue, tempAgentsDir } from './storage.mjs'
|
|
30
30
|
import { logger } from './utils/logger.mjs'
|
|
31
|
+
import { publishChannelSessionChanged } from './channels/event-relay.mjs'
|
|
31
32
|
import { withSessionPersistenceLock } from './session-persistence-lock.mjs'
|
|
32
33
|
import { getGlobalMemoryRevision, isGlobalMemoryEnabled } from './global-memory.mjs'
|
|
33
34
|
import { buildSystemPrompt, generateAiTitle, generateTitle } from './session-utils.mjs'
|
|
@@ -198,6 +199,7 @@ function hasFullAccess(session) {
|
|
|
198
199
|
/** @typedef {{ agent: Agent, projectContext: object|null, projectId: string|null, accessMode: string, yoloMode: boolean, model: object, thinkingLevel: string, scope: string, title: string, createdAt: string, status: string, startedAt: string|null, finishedAt: string|null, listeners: Set<function>, idleTimer: NodeJS.Timeout|null, eventBus: EventEmitter }} AgentSession */
|
|
199
200
|
|
|
200
201
|
const IDLE_TIMEOUT_MS = 30 * 60 * 1000 // 30 minutes
|
|
202
|
+
const ABORT_IDLE_WAIT_TIMEOUT_MS = 3000
|
|
201
203
|
const SUBAGENT_DEFAULT_TIMEOUT_MS = 30 * 60 * 1000
|
|
202
204
|
const SUBAGENT_TRACE_THROTTLE_MS = 150
|
|
203
205
|
|
|
@@ -429,6 +431,71 @@ function addToolTimingToEvent(session, event) {
|
|
|
429
431
|
return event
|
|
430
432
|
}
|
|
431
433
|
|
|
434
|
+
function isRecord(value) {
|
|
435
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function updateRuntimeToolExecution(session, event) {
|
|
439
|
+
if (!event?.toolCallId) return
|
|
440
|
+
if (event.type !== 'tool_execution_start' && event.type !== 'tool_execution_update' && event.type !== 'tool_execution_end') return
|
|
441
|
+
|
|
442
|
+
const existing = session.runtimeToolExecutions?.get(event.toolCallId)
|
|
443
|
+
const toolName = event.toolName ?? existing?.toolName
|
|
444
|
+
if (!toolName) return
|
|
445
|
+
const result = event.type === 'tool_execution_end' ? event.result : event.partialResult
|
|
446
|
+
const existingDetails = isRecord(existing?.details) ? existing.details : {}
|
|
447
|
+
const resultDetails = isRecord(result?.details) ? result.details : {}
|
|
448
|
+
const quickforgeTiming = event.quickforgeTiming || resultDetails.quickforgeTiming || existingDetails.quickforgeTiming
|
|
449
|
+
const details = {
|
|
450
|
+
...existingDetails,
|
|
451
|
+
...resultDetails,
|
|
452
|
+
...(quickforgeTiming ? { quickforgeTiming } : {}),
|
|
453
|
+
sessionId: resultDetails.sessionId ?? existingDetails.sessionId ?? session.sessionId,
|
|
454
|
+
toolCallId: resultDetails.toolCallId ?? existingDetails.toolCallId ?? event.toolCallId,
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
session.runtimeToolExecutions?.set(event.toolCallId, {
|
|
458
|
+
role: 'toolResult',
|
|
459
|
+
toolCallId: event.toolCallId,
|
|
460
|
+
toolName,
|
|
461
|
+
content: result?.content ?? existing?.content ?? [],
|
|
462
|
+
details,
|
|
463
|
+
isError: event.type === 'tool_execution_end' ? Boolean(event.isError) : false,
|
|
464
|
+
timestamp: existing?.timestamp ?? Date.now(),
|
|
465
|
+
pending: event.type !== 'tool_execution_end',
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function messagesWithRuntimeToolExecutions(session) {
|
|
470
|
+
const messages = session.agent.state.messages || []
|
|
471
|
+
if (!session.runtimeToolExecutions?.size) return messages
|
|
472
|
+
|
|
473
|
+
const authoritativeToolCallIds = new Set(
|
|
474
|
+
messages
|
|
475
|
+
.filter((message) => message?.role === 'toolResult' && typeof message.toolCallId === 'string')
|
|
476
|
+
.map((message) => message.toolCallId),
|
|
477
|
+
)
|
|
478
|
+
const runtimeMessages = []
|
|
479
|
+
for (const [toolCallId, snapshot] of session.runtimeToolExecutions) {
|
|
480
|
+
if (authoritativeToolCallIds.has(toolCallId)) {
|
|
481
|
+
session.runtimeToolExecutions.delete(toolCallId)
|
|
482
|
+
} else {
|
|
483
|
+
const { pending: _pending, ...message } = snapshot
|
|
484
|
+
runtimeMessages.push(message)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return runtimeMessages.length > 0 ? [...messages, ...runtimeMessages] : messages
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function runtimePendingToolCalls(session) {
|
|
491
|
+
const pending = new Set(session.agent.state.pendingToolCalls || [])
|
|
492
|
+
for (const [toolCallId, snapshot] of session.runtimeToolExecutions || []) {
|
|
493
|
+
if (snapshot.pending) pending.add(toolCallId)
|
|
494
|
+
else pending.delete(toolCallId)
|
|
495
|
+
}
|
|
496
|
+
return Array.from(pending)
|
|
497
|
+
}
|
|
498
|
+
|
|
432
499
|
export function markLatestAssistantProcessFinished(messages, finishedAt = Date.now()) {
|
|
433
500
|
if (!Array.isArray(messages)) return false
|
|
434
501
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
@@ -1319,6 +1386,12 @@ function applyActiveCapabilityPrompt(messages, capabilityPrompt) {
|
|
|
1319
1386
|
return messages
|
|
1320
1387
|
}
|
|
1321
1388
|
|
|
1389
|
+
function activeTurnContextPrompt(session) {
|
|
1390
|
+
return [session?.activeTransientContextPrompt, session?.activeCapabilityPrompt]
|
|
1391
|
+
.filter((prompt) => typeof prompt === 'string' && prompt.trim())
|
|
1392
|
+
.join('\n\n') || null
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1322
1395
|
function compactSummaryIndex(messages) {
|
|
1323
1396
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
1324
1397
|
if (isCompactSummaryMessage(messages[index])) return index
|
|
@@ -1352,7 +1425,7 @@ async function transformSessionContext(session, messages, signal) {
|
|
|
1352
1425
|
session.lastTransformedContextMessages = transformedMessages
|
|
1353
1426
|
return applyActiveCapabilityPrompt(
|
|
1354
1427
|
applyActiveCommandPrompt(compactedContextMessages(transformedMessages), session?.activeCommandPrompt),
|
|
1355
|
-
session
|
|
1428
|
+
activeTurnContextPrompt(session),
|
|
1356
1429
|
)
|
|
1357
1430
|
}
|
|
1358
1431
|
|
|
@@ -1432,6 +1505,9 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1432
1505
|
const {
|
|
1433
1506
|
scope = 'global',
|
|
1434
1507
|
projectId = null,
|
|
1508
|
+
source = null,
|
|
1509
|
+
channelId = null,
|
|
1510
|
+
channelName = null,
|
|
1435
1511
|
accessMode: rawAccessMode,
|
|
1436
1512
|
yoloMode = false,
|
|
1437
1513
|
model = null,
|
|
@@ -1600,6 +1676,9 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1600
1676
|
agent,
|
|
1601
1677
|
projectContext,
|
|
1602
1678
|
projectId,
|
|
1679
|
+
source: typeof source === 'string' && source.trim() ? source.trim() : null,
|
|
1680
|
+
channelId: typeof channelId === 'string' && channelId.trim() ? channelId.trim() : null,
|
|
1681
|
+
channelName: typeof channelName === 'string' && channelName.trim() ? channelName.trim() : null,
|
|
1603
1682
|
accessMode,
|
|
1604
1683
|
yoloMode: resolvedYoloMode,
|
|
1605
1684
|
model: resolvedModel,
|
|
@@ -1617,6 +1696,8 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1617
1696
|
activeCommandName: null,
|
|
1618
1697
|
activeCommandPermissions: null,
|
|
1619
1698
|
activeCommandPrompt: null,
|
|
1699
|
+
activeCapabilityPrompt: null,
|
|
1700
|
+
activeTransientContextPrompt: null,
|
|
1620
1701
|
eventBus,
|
|
1621
1702
|
idleTimer: null,
|
|
1622
1703
|
persistTimer: null,
|
|
@@ -1625,6 +1706,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1625
1706
|
titleGenerationPromiseId: null,
|
|
1626
1707
|
sessionCreatedEmitted: messages.length > 0,
|
|
1627
1708
|
toolTimings: new Map(),
|
|
1709
|
+
runtimeToolExecutions: new Map(),
|
|
1628
1710
|
getApiKey,
|
|
1629
1711
|
contextCompaction,
|
|
1630
1712
|
agentProfile: resolvedAgentProfile ? agentProfileSnapshot(resolvedAgentProfile) : null,
|
|
@@ -1640,6 +1722,8 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1640
1722
|
/** Track active SSE connections. Only one SSE stream allowed per session to prevent
|
|
1641
1723
|
* connection-pool exhaustion when two browser tabs load the same session. */
|
|
1642
1724
|
sseConnected: false,
|
|
1725
|
+
abortPending: false,
|
|
1726
|
+
abortEndEmitted: false,
|
|
1643
1727
|
}
|
|
1644
1728
|
|
|
1645
1729
|
// Subscribe to agent lifecycle events and forward to eventBus
|
|
@@ -1649,6 +1733,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1649
1733
|
// complete session history. Replace with the authoritative full state
|
|
1650
1734
|
// before forwarding to clients.
|
|
1651
1735
|
const timedEvent = addToolTimingToEvent(session, event)
|
|
1736
|
+
updateRuntimeToolExecution(session, timedEvent)
|
|
1652
1737
|
const eventEndStatus = event.type === 'agent_end'
|
|
1653
1738
|
? session.agent.signal?.aborted
|
|
1654
1739
|
? 'aborted'
|
|
@@ -1668,11 +1753,14 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1668
1753
|
}
|
|
1669
1754
|
: timedEvent
|
|
1670
1755
|
|
|
1671
|
-
|
|
1672
|
-
|
|
1756
|
+
const shouldForwardEvent = !(timedEvent.type === 'agent_end' && session.abortEndEmitted)
|
|
1757
|
+
if (shouldForwardEvent) {
|
|
1758
|
+
emitSessionEvent(session, forwardEvent)
|
|
1759
|
+
}
|
|
1673
1760
|
|
|
1674
1761
|
// Track status
|
|
1675
1762
|
if (event.type === 'agent_start') {
|
|
1763
|
+
session.abortEndEmitted = false
|
|
1676
1764
|
session.status = 'running'
|
|
1677
1765
|
session.startedAt = session.startedAt ?? new Date().toISOString()
|
|
1678
1766
|
session.finishedAt = null
|
|
@@ -1687,6 +1775,8 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1687
1775
|
}
|
|
1688
1776
|
|
|
1689
1777
|
if (event.type === 'agent_end') {
|
|
1778
|
+
session.abortPending = false
|
|
1779
|
+
session.abortEndEmitted = false
|
|
1690
1780
|
session.status = eventEndStatus || (session.agent.state.errorMessage ? 'error' : 'idle')
|
|
1691
1781
|
session.finishedAt = new Date().toISOString()
|
|
1692
1782
|
session.toolTimings?.clear()
|
|
@@ -1758,7 +1848,7 @@ function sessionLastModifiedFromMessages(messages, fallback) {
|
|
|
1758
1848
|
* Persist session data to storage.
|
|
1759
1849
|
*/
|
|
1760
1850
|
async function persistSessionUnlocked(session) {
|
|
1761
|
-
const { sessionId, agent, scope, projectId, title, titleSource, createdAt, lastModified: storedLastModified, status, startedAt, finishedAt, model, thinkingLevel, accessMode, yoloMode, contextCompaction } = session
|
|
1851
|
+
const { sessionId, agent, scope, projectId, source, channelId, channelName, title, titleSource, createdAt, lastModified: storedLastModified, status, startedAt, finishedAt, model, thinkingLevel, accessMode, yoloMode, contextCompaction } = session
|
|
1762
1852
|
const messages = agent.state.messages
|
|
1763
1853
|
|
|
1764
1854
|
if (messages.length === 0) {
|
|
@@ -1789,6 +1879,9 @@ async function persistSessionUnlocked(session) {
|
|
|
1789
1879
|
lastModified,
|
|
1790
1880
|
scope,
|
|
1791
1881
|
projectId: scope === 'project' ? projectId : undefined,
|
|
1882
|
+
source: source || undefined,
|
|
1883
|
+
channelId: channelId || undefined,
|
|
1884
|
+
channelName: channelName || undefined,
|
|
1792
1885
|
taskStatus: status,
|
|
1793
1886
|
taskStartedAt: startedAt,
|
|
1794
1887
|
taskFinishedAt: finishedAt,
|
|
@@ -1842,6 +1935,9 @@ async function persistSessionUnlocked(session) {
|
|
|
1842
1935
|
preview,
|
|
1843
1936
|
scope,
|
|
1844
1937
|
projectId: scope === 'project' ? projectId : undefined,
|
|
1938
|
+
source: source || undefined,
|
|
1939
|
+
channelId: channelId || undefined,
|
|
1940
|
+
channelName: channelName || undefined,
|
|
1845
1941
|
taskStatus: status,
|
|
1846
1942
|
taskStartedAt: startedAt,
|
|
1847
1943
|
taskFinishedAt: finishedAt,
|
|
@@ -1856,30 +1952,45 @@ async function persistSessionUnlocked(session) {
|
|
|
1856
1952
|
}
|
|
1857
1953
|
|
|
1858
1954
|
// Write to storage atomically (read-modify-write within queue)
|
|
1955
|
+
let persistedMetadata
|
|
1859
1956
|
try {
|
|
1860
1957
|
await writeSessionValue(sessionId, sessionData)
|
|
1861
|
-
let archivedAt
|
|
1862
1958
|
await atomicSessionMetadataUpdate(scope, projectId, (data) => {
|
|
1863
1959
|
const existingMetadata = data[sessionId]
|
|
1864
|
-
archivedAt = existingMetadata?.archivedAt
|
|
1960
|
+
const archivedAt = existingMetadata?.archivedAt
|
|
1865
1961
|
// Merge onto the existing record so storage-only fields the in-memory
|
|
1866
1962
|
// session does not own (pinnedAt, archivedAt, …) survive the rebuild.
|
|
1867
|
-
|
|
1963
|
+
persistedMetadata = {
|
|
1868
1964
|
...existingMetadata,
|
|
1869
1965
|
...metadata,
|
|
1870
1966
|
...(archivedAt ? { archivedAt } : {}),
|
|
1871
1967
|
}
|
|
1968
|
+
data[sessionId] = persistedMetadata
|
|
1872
1969
|
return data
|
|
1873
1970
|
})
|
|
1874
|
-
if (archivedAt) {
|
|
1875
|
-
await writeSessionValue(sessionId, { ...sessionData, archivedAt })
|
|
1971
|
+
if (persistedMetadata?.archivedAt) {
|
|
1972
|
+
await writeSessionValue(sessionId, { ...sessionData, archivedAt: persistedMetadata.archivedAt })
|
|
1876
1973
|
}
|
|
1877
1974
|
} catch (err) {
|
|
1878
1975
|
logger.error(`Failed to persist session ${sessionId}:`, err, { sessionId })
|
|
1879
1976
|
return null
|
|
1880
1977
|
}
|
|
1881
1978
|
|
|
1882
|
-
|
|
1979
|
+
if (source === 'acp' && channelId && persistedMetadata) {
|
|
1980
|
+
void publishChannelSessionChanged({
|
|
1981
|
+
channelId,
|
|
1982
|
+
channelName: channelName || undefined,
|
|
1983
|
+
sessionId,
|
|
1984
|
+
projectId: scope === 'project' ? projectId : null,
|
|
1985
|
+
workspace: scope === 'project'
|
|
1986
|
+
? { id: projectId, kind: 'project' }
|
|
1987
|
+
: { id: 'default', kind: 'default' },
|
|
1988
|
+
change: 'upsert',
|
|
1989
|
+
metadata: persistedMetadata,
|
|
1990
|
+
})
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
return persistedMetadata || metadata
|
|
1883
1994
|
}
|
|
1884
1995
|
|
|
1885
1996
|
async function persistSession(session) {
|
|
@@ -2020,16 +2131,13 @@ function scheduleSessionTitleGeneration(session, userMessage) {
|
|
|
2020
2131
|
* Send a user message to the agent and start the agent loop.
|
|
2021
2132
|
* Returns immediately; events are streamed via the event bus.
|
|
2022
2133
|
*/
|
|
2023
|
-
export async function runPrompt(sessionId, message, selectedCapabilities = [], promptCommand = null) {
|
|
2024
|
-
let session =
|
|
2025
|
-
if (!session) {
|
|
2026
|
-
session = await restoreAgent(sessionId)
|
|
2027
|
-
}
|
|
2134
|
+
export async function runPrompt(sessionId, message, selectedCapabilities = [], promptCommand = null, transientContextPrompt = null) {
|
|
2135
|
+
let session = await syncSessionFromStorage(sessionId)
|
|
2028
2136
|
if (!session) {
|
|
2029
2137
|
throw Object.assign(new Error('Session not found'), { statusCode: 404 })
|
|
2030
2138
|
}
|
|
2031
2139
|
|
|
2032
|
-
if (session.agent.state.isStreaming) {
|
|
2140
|
+
if (session.agent.state.isStreaming || session.abortPending) {
|
|
2033
2141
|
throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes.'), { statusCode: 409 })
|
|
2034
2142
|
}
|
|
2035
2143
|
|
|
@@ -2082,6 +2190,9 @@ export async function runPrompt(sessionId, message, selectedCapabilities = [], p
|
|
|
2082
2190
|
session.activeCommandPermissions = commandState.permissions ?? null
|
|
2083
2191
|
session.activeCommandPrompt = commandState.commandPrompt ?? null
|
|
2084
2192
|
session.activeCapabilityPrompt = selectedCapabilityPrompt(selectedCapabilities)
|
|
2193
|
+
session.activeTransientContextPrompt = typeof transientContextPrompt === 'string' && transientContextPrompt.trim()
|
|
2194
|
+
? transientContextPrompt
|
|
2195
|
+
: null
|
|
2085
2196
|
|
|
2086
2197
|
// Fire and forget — events come through eventBus
|
|
2087
2198
|
session.agent.prompt(userMessage).catch((err) => {
|
|
@@ -2096,6 +2207,7 @@ export async function runPrompt(sessionId, message, selectedCapabilities = [], p
|
|
|
2096
2207
|
session.activeCommandPermissions = null
|
|
2097
2208
|
session.activeCommandPrompt = null
|
|
2098
2209
|
session.activeCapabilityPrompt = null
|
|
2210
|
+
session.activeTransientContextPrompt = null
|
|
2099
2211
|
})
|
|
2100
2212
|
|
|
2101
2213
|
return { sessionId, status: session.status }
|
|
@@ -2181,7 +2293,19 @@ export async function abortRun(sessionId) {
|
|
|
2181
2293
|
}
|
|
2182
2294
|
|
|
2183
2295
|
session.agent.abort()
|
|
2184
|
-
|
|
2296
|
+
let idleWaitTimer
|
|
2297
|
+
const becameIdle = await Promise.race([
|
|
2298
|
+
session.agent.waitForIdle().then(() => true),
|
|
2299
|
+
new Promise((resolve) => {
|
|
2300
|
+
idleWaitTimer = setTimeout(() => resolve(false), ABORT_IDLE_WAIT_TIMEOUT_MS)
|
|
2301
|
+
idleWaitTimer.unref?.()
|
|
2302
|
+
}),
|
|
2303
|
+
])
|
|
2304
|
+
clearTimeout(idleWaitTimer)
|
|
2305
|
+
if (!becameIdle) {
|
|
2306
|
+
session.abortPending = true
|
|
2307
|
+
logger.warn(`Agent ${sessionId} did not become idle within ${ABORT_IDLE_WAIT_TIMEOUT_MS}ms after abort`, { sessionId })
|
|
2308
|
+
}
|
|
2185
2309
|
|
|
2186
2310
|
if (session.status === 'running') {
|
|
2187
2311
|
session.status = 'aborted'
|
|
@@ -2189,6 +2313,7 @@ export async function abortRun(sessionId) {
|
|
|
2189
2313
|
persistSession(session).catch((err) =>
|
|
2190
2314
|
logger.error(`Failed to persist aborted session ${sessionId}:`, err, { sessionId }),
|
|
2191
2315
|
)
|
|
2316
|
+
session.abortEndEmitted = true
|
|
2192
2317
|
const event = {
|
|
2193
2318
|
type: 'agent_end',
|
|
2194
2319
|
status: 'aborted',
|
|
@@ -2243,6 +2368,62 @@ function getSessionContextUsage(session) {
|
|
|
2243
2368
|
}
|
|
2244
2369
|
}
|
|
2245
2370
|
|
|
2371
|
+
function storedMessagesExtendLocalHistory(localMessages, storedMessages) {
|
|
2372
|
+
if (!Array.isArray(localMessages) || !Array.isArray(storedMessages) || storedMessages.length <= localMessages.length) return false
|
|
2373
|
+
return localMessages.every((message, index) => {
|
|
2374
|
+
const local = { ...message }
|
|
2375
|
+
const stored = { ...storedMessages[index] }
|
|
2376
|
+
delete local.processFinishedAt
|
|
2377
|
+
delete stored.processFinishedAt
|
|
2378
|
+
return JSON.stringify(local) === JSON.stringify(stored)
|
|
2379
|
+
})
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
export async function syncSessionFromStorage(sessionId) {
|
|
2383
|
+
let session = agentSessions.get(sessionId)
|
|
2384
|
+
if (!session) return restoreAgent(sessionId)
|
|
2385
|
+
if (session.agent.state.isStreaming) return session
|
|
2386
|
+
|
|
2387
|
+
try {
|
|
2388
|
+
const stored = await readSessionValue(sessionId)
|
|
2389
|
+
if (!stored || !Array.isArray(stored.messages)) return session
|
|
2390
|
+
|
|
2391
|
+
const localMessages = Array.isArray(session.agent.state.messages) ? session.agent.state.messages : []
|
|
2392
|
+
const storedStateVersion = Number.isFinite(stored.stateVersion) ? stored.stateVersion : 0
|
|
2393
|
+
const localStateVersion = Number.isFinite(session.stateVersion) ? session.stateVersion : 0
|
|
2394
|
+
const storedLastModified = Date.parse(stored.lastModified || '')
|
|
2395
|
+
const localLastModified = Date.parse(session.lastModified || '')
|
|
2396
|
+
const hasNewerVersion = storedStateVersion > localStateVersion
|
|
2397
|
+
const storedExtendsLocalHistory = storedMessagesExtendLocalHistory(localMessages, stored.messages)
|
|
2398
|
+
const hasMoreMessages = storedExtendsLocalHistory
|
|
2399
|
+
const hasNewerActivityAtSameVersion = storedStateVersion === localStateVersion
|
|
2400
|
+
&& stored.messages.length === localMessages.length
|
|
2401
|
+
&& Number.isFinite(storedLastModified)
|
|
2402
|
+
&& (!Number.isFinite(localLastModified) || storedLastModified > localLastModified)
|
|
2403
|
+
|
|
2404
|
+
if (!hasNewerVersion && !hasMoreMessages && !hasNewerActivityAtSameVersion) return session
|
|
2405
|
+
|
|
2406
|
+
session.agent.state.messages = stored.messages
|
|
2407
|
+
session.title = stored.title || session.title
|
|
2408
|
+
session.titleSource = stored.titleSource || session.titleSource
|
|
2409
|
+
session.source = stored.source || session.source
|
|
2410
|
+
session.channelId = stored.channelId || session.channelId
|
|
2411
|
+
session.channelName = stored.channelName || session.channelName
|
|
2412
|
+
session.lastModified = stored.lastModified || session.lastModified
|
|
2413
|
+
session.status = stored.taskStatus || 'idle'
|
|
2414
|
+
session.startedAt = stored.taskStartedAt || null
|
|
2415
|
+
session.finishedAt = stored.taskFinishedAt || null
|
|
2416
|
+
session.contextCompaction = stored.contextCompaction || null
|
|
2417
|
+
session.stateVersion = Math.max(localStateVersion, storedStateVersion)
|
|
2418
|
+
session.runtimeToolExecutions?.clear()
|
|
2419
|
+
resetIdleTimer(session)
|
|
2420
|
+
return session
|
|
2421
|
+
} catch (error) {
|
|
2422
|
+
logger.warn(`Failed to sync session ${sessionId} from storage:`, error?.message || error, { sessionId })
|
|
2423
|
+
return session
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2246
2427
|
/**
|
|
2247
2428
|
* Get the current state of a session (for page refresh recovery).
|
|
2248
2429
|
*/
|
|
@@ -2250,10 +2431,14 @@ export function getSessionState(sessionId) {
|
|
|
2250
2431
|
const session = agentSessions.get(sessionId)
|
|
2251
2432
|
if (!session) return null
|
|
2252
2433
|
|
|
2434
|
+
const messages = messagesWithRuntimeToolExecutions(session)
|
|
2253
2435
|
return {
|
|
2254
2436
|
sessionId: session.sessionId,
|
|
2255
2437
|
scope: session.scope,
|
|
2256
2438
|
projectId: session.projectId,
|
|
2439
|
+
source: session.source || undefined,
|
|
2440
|
+
channelId: session.channelId || undefined,
|
|
2441
|
+
channelName: session.channelName || undefined,
|
|
2257
2442
|
accessMode: session.accessMode,
|
|
2258
2443
|
yoloMode: session.yoloMode,
|
|
2259
2444
|
systemPrompt: session.agent.state.systemPrompt,
|
|
@@ -2268,12 +2453,13 @@ export function getSessionState(sessionId) {
|
|
|
2268
2453
|
startedAt: session.startedAt,
|
|
2269
2454
|
finishedAt: session.finishedAt,
|
|
2270
2455
|
tools: session.agent.state.tools,
|
|
2271
|
-
messages
|
|
2456
|
+
messages,
|
|
2457
|
+
pendingToolCalls: runtimePendingToolCalls(session),
|
|
2272
2458
|
contextCompaction: session.contextCompaction,
|
|
2273
2459
|
contextUsage: getSessionContextUsage(session),
|
|
2274
2460
|
pendingToolApproval: getPendingApprovalForSession(session.sessionId),
|
|
2275
2461
|
pendingAutoCompactApproval: getPendingAutoCompactApprovalForSession(session.sessionId),
|
|
2276
|
-
isStreaming: session.agent.state.isStreaming,
|
|
2462
|
+
isStreaming: session.abortPending ? false : session.agent.state.isStreaming,
|
|
2277
2463
|
errorMessage: session.agent.state.errorMessage,
|
|
2278
2464
|
}
|
|
2279
2465
|
}
|
|
@@ -2291,6 +2477,9 @@ export function getSessionStatus(sessionId) {
|
|
|
2291
2477
|
sessionId: session.sessionId,
|
|
2292
2478
|
scope: session.scope,
|
|
2293
2479
|
projectId: session.projectId,
|
|
2480
|
+
source: session.source || undefined,
|
|
2481
|
+
channelId: session.channelId || undefined,
|
|
2482
|
+
channelName: session.channelName || undefined,
|
|
2294
2483
|
title: session.title,
|
|
2295
2484
|
createdAt: session.createdAt,
|
|
2296
2485
|
lastModified: session.lastModified,
|
|
@@ -2298,7 +2487,7 @@ export function getSessionStatus(sessionId) {
|
|
|
2298
2487
|
status: session.status,
|
|
2299
2488
|
startedAt: session.startedAt,
|
|
2300
2489
|
finishedAt: session.finishedAt,
|
|
2301
|
-
isStreaming: session.agent.state.isStreaming,
|
|
2490
|
+
isStreaming: session.abortPending ? false : session.agent.state.isStreaming,
|
|
2302
2491
|
errorMessage: session.agent.state.errorMessage,
|
|
2303
2492
|
messageCount: messages.length,
|
|
2304
2493
|
lastMessageTimestamp: lastMessage?.timestamp ?? null,
|
|
@@ -2402,6 +2591,9 @@ export async function restoreAgent(sessionId) {
|
|
|
2402
2591
|
return await createAgent(sessionId, {
|
|
2403
2592
|
scope: sessionData.scope || 'global',
|
|
2404
2593
|
projectId: sessionData.projectId || null,
|
|
2594
|
+
source: sessionData.source || null,
|
|
2595
|
+
channelId: sessionData.channelId || null,
|
|
2596
|
+
channelName: sessionData.channelName || null,
|
|
2405
2597
|
accessMode: normalizeAccessMode(sessionData.accessMode, sessionData.yoloMode),
|
|
2406
2598
|
yoloMode: sessionData.yoloMode || false,
|
|
2407
2599
|
model: sessionData.model,
|
|
@@ -2486,6 +2678,9 @@ export function listSessions() {
|
|
|
2486
2678
|
scope: session.scope,
|
|
2487
2679
|
status: session.status,
|
|
2488
2680
|
title: session.title,
|
|
2681
|
+
source: session.source || undefined,
|
|
2682
|
+
channelId: session.channelId || undefined,
|
|
2683
|
+
channelName: session.channelName || undefined,
|
|
2489
2684
|
idleRetention: session.idleRetention || undefined,
|
|
2490
2685
|
})
|
|
2491
2686
|
}
|
|
@@ -3,6 +3,10 @@ import path from 'node:path'
|
|
|
3
3
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
4
4
|
import { randomUUID } from 'node:crypto'
|
|
5
5
|
import { streamSimple } from '@earendil-works/pi-ai/compat'
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_AI_STREAM_DEADLINE_MS,
|
|
8
|
+
withDefaultAiProviderOptions,
|
|
9
|
+
} from './ai-provider-options.mjs'
|
|
6
10
|
import { logsDir } from './storage.mjs'
|
|
7
11
|
|
|
8
12
|
const PATCH_MARKER = Symbol.for('quickforge.aiHttpLogger.fetchPatched')
|
|
@@ -205,19 +209,72 @@ export function installAiHttpLogger() {
|
|
|
205
209
|
globalThis[PATCH_MARKER] = true
|
|
206
210
|
}
|
|
207
211
|
|
|
208
|
-
|
|
209
|
-
if (!
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
function combineAbortSignals(parentSignal, deadlineSignal) {
|
|
213
|
+
if (!parentSignal) return deadlineSignal
|
|
214
|
+
if (typeof AbortSignal.any === 'function') return AbortSignal.any([parentSignal, deadlineSignal])
|
|
215
|
+
|
|
216
|
+
const controller = new AbortController()
|
|
217
|
+
const abort = () => controller.abort()
|
|
218
|
+
if (parentSignal.aborted || deadlineSignal.aborted) {
|
|
219
|
+
controller.abort()
|
|
220
|
+
} else {
|
|
221
|
+
parentSignal.addEventListener('abort', abort, { once: true })
|
|
222
|
+
deadlineSignal.addEventListener('abort', abort, { once: true })
|
|
218
223
|
}
|
|
224
|
+
return controller.signal
|
|
225
|
+
}
|
|
219
226
|
|
|
220
|
-
|
|
227
|
+
function wrapStreamWithDeadline(stream, deadlineController, deadlineMs) {
|
|
228
|
+
let timer
|
|
229
|
+
const deadlinePromise = new Promise((_, reject) => {
|
|
230
|
+
timer = setTimeout(() => {
|
|
231
|
+
deadlineController.abort()
|
|
232
|
+
reject(new Error(`AI stream timed out after ${deadlineMs}ms`))
|
|
233
|
+
}, deadlineMs)
|
|
234
|
+
timer.unref?.()
|
|
235
|
+
})
|
|
236
|
+
const resultPromise = Promise.race([stream.result(), deadlinePromise])
|
|
237
|
+
.finally(() => clearTimeout(timer))
|
|
238
|
+
resultPromise.catch(() => {})
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
result: () => resultPromise,
|
|
242
|
+
[Symbol.asyncIterator]() {
|
|
243
|
+
const iterator = stream[Symbol.asyncIterator]()
|
|
244
|
+
return {
|
|
245
|
+
next: () => Promise.race([iterator.next(), deadlinePromise]),
|
|
246
|
+
return: async () => {
|
|
247
|
+
clearTimeout(timer)
|
|
248
|
+
if (typeof iterator.return === 'function') return iterator.return()
|
|
249
|
+
return { value: undefined, done: true }
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
}
|
|
221
254
|
}
|
|
222
255
|
|
|
223
|
-
|
|
256
|
+
export function streamSimpleWithAiHttpLogging(model, context, options = {}) {
|
|
257
|
+
const providerOptions = withDefaultAiProviderOptions(options)
|
|
258
|
+
const deadlineMs = Number.isFinite(providerOptions.deadlineMs)
|
|
259
|
+
? Math.max(1, providerOptions.deadlineMs)
|
|
260
|
+
: DEFAULT_AI_STREAM_DEADLINE_MS
|
|
261
|
+
const deadlineController = new AbortController()
|
|
262
|
+
const effectiveOptions = {
|
|
263
|
+
...providerOptions,
|
|
264
|
+
signal: combineAbortSignals(providerOptions.signal, deadlineController.signal),
|
|
265
|
+
}
|
|
266
|
+
delete effectiveOptions.deadlineMs
|
|
267
|
+
|
|
268
|
+
const stream = aiHttpLogEnabled
|
|
269
|
+
? aiHttpContext.run({
|
|
270
|
+
traceId: randomUUID(),
|
|
271
|
+
sessionId: effectiveOptions.sessionId,
|
|
272
|
+
purpose: effectiveOptions.metadata?.quickforgePurpose || 'chat',
|
|
273
|
+
provider: model?.provider,
|
|
274
|
+
api: model?.api,
|
|
275
|
+
model: model?.id,
|
|
276
|
+
}, () => streamSimple(model, context, effectiveOptions))
|
|
277
|
+
: streamSimple(model, context, effectiveOptions)
|
|
278
|
+
|
|
279
|
+
return wrapStreamWithDeadline(stream, deadlineController, deadlineMs)
|
|
280
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const DEFAULT_AI_MAX_RETRIES = 3
|
|
2
|
+
export const DEFAULT_AI_HTTP_TIMEOUT_MS = 2 * 60 * 1000
|
|
3
|
+
export const DEFAULT_AI_STREAM_DEADLINE_MS = 5 * 60 * 1000
|
|
4
|
+
|
|
5
|
+
export function withDefaultAiProviderOptions(options = {}) {
|
|
6
|
+
return {
|
|
7
|
+
maxRetries: DEFAULT_AI_MAX_RETRIES,
|
|
8
|
+
timeoutMs: DEFAULT_AI_HTTP_TIMEOUT_MS,
|
|
9
|
+
...options,
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
function safeChannelId(channelId) {
|
|
4
|
+
const safeId = String(channelId || '')
|
|
5
|
+
.trim()
|
|
6
|
+
.replace(/[^a-zA-Z0-9_-]+/g, '-')
|
|
7
|
+
.replace(/^-+|-+$/g, '')
|
|
8
|
+
return safeId || 'channel'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function channelLogsDirectory(logsDir, channelId) {
|
|
12
|
+
return path.join(path.resolve(logsDir), 'channels', safeChannelId(channelId))
|
|
13
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const CHANNEL_EVENTS_URL_ENV = 'QUICKFORGE_CHANNEL_EVENTS_URL'
|
|
2
|
+
const CHANNEL_EVENT_ACTION = 'channel-event'
|
|
3
|
+
const RELAY_TIMEOUT_MS = 3000
|
|
4
|
+
|
|
5
|
+
export async function publishChannelSessionChanged(event) {
|
|
6
|
+
const url = process.env[CHANNEL_EVENTS_URL_ENV]
|
|
7
|
+
if (!url) return false
|
|
8
|
+
|
|
9
|
+
const controller = new AbortController()
|
|
10
|
+
const timer = setTimeout(() => controller.abort(), RELAY_TIMEOUT_MS)
|
|
11
|
+
timer.unref?.()
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const response = await fetch(url, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: {
|
|
17
|
+
'content-type': 'application/json',
|
|
18
|
+
'x-quickforge-action': CHANNEL_EVENT_ACTION,
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify({
|
|
21
|
+
...event,
|
|
22
|
+
type: 'sessions-changed',
|
|
23
|
+
timestamp: event.timestamp || new Date().toISOString(),
|
|
24
|
+
}),
|
|
25
|
+
signal: controller.signal,
|
|
26
|
+
})
|
|
27
|
+
return response.ok
|
|
28
|
+
} catch {
|
|
29
|
+
return false
|
|
30
|
+
} finally {
|
|
31
|
+
clearTimeout(timer)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/* eslint-disable no-control-regex -- ANSI escape matching intentionally includes ESC. */
|
|
2
2
|
import { spawn } from 'node:child_process'
|
|
3
|
+
import { promises as fs } from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
3
5
|
import { EventEmitter } from 'node:events'
|
|
4
6
|
import { setTimeout as delay } from 'node:timers/promises'
|
|
7
|
+
import { channelLogsDirectory } from './channel-logs.mjs'
|
|
8
|
+
import { logger } from '../utils/logger.mjs'
|
|
5
9
|
|
|
6
10
|
const DEFAULT_LOG_LIMIT = 300
|
|
7
11
|
const STOP_TIMEOUT_MS = 5000
|
|
@@ -55,6 +59,9 @@ export class ProcessChannelProvider extends EventEmitter {
|
|
|
55
59
|
super()
|
|
56
60
|
this.definition = definition
|
|
57
61
|
this.logLimit = options.logLimit || DEFAULT_LOG_LIMIT
|
|
62
|
+
this.logsDir = options.logsDir
|
|
63
|
+
this.logWriteErrorReported = false
|
|
64
|
+
this.logWritePromise = Promise.resolve()
|
|
58
65
|
this.status = 'stopped'
|
|
59
66
|
this.process = null
|
|
60
67
|
this.pid = null
|
|
@@ -118,6 +125,27 @@ export class ProcessChannelProvider extends EventEmitter {
|
|
|
118
125
|
this.emitEvent('status', { status, snapshot: this.snapshot() })
|
|
119
126
|
}
|
|
120
127
|
|
|
128
|
+
persistLog(entry) {
|
|
129
|
+
if (!this.logsDir) return
|
|
130
|
+
const directory = channelLogsDirectory(this.logsDir, this.definition.id)
|
|
131
|
+
const filePath = path.join(directory, `channel-${entry.time.slice(0, 10)}.log`)
|
|
132
|
+
const line = `${JSON.stringify({ time: entry.time, stream: entry.stream, text: entry.text })}\n`
|
|
133
|
+
this.logWritePromise = this.logWritePromise
|
|
134
|
+
.then(() => fs.mkdir(directory, { recursive: true }))
|
|
135
|
+
.then(() => fs.appendFile(filePath, line, 'utf8'))
|
|
136
|
+
.then(() => {
|
|
137
|
+
this.logWriteErrorReported = false
|
|
138
|
+
})
|
|
139
|
+
.catch((error) => {
|
|
140
|
+
if (this.logWriteErrorReported) return
|
|
141
|
+
this.logWriteErrorReported = true
|
|
142
|
+
logger.warn('Failed to persist channel log.', {
|
|
143
|
+
channelId: this.definition.id,
|
|
144
|
+
error: error instanceof Error ? error.message : String(error),
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
121
149
|
addLog(stream, text) {
|
|
122
150
|
if (!text) return
|
|
123
151
|
const entry = {
|
|
@@ -131,6 +159,7 @@ export class ProcessChannelProvider extends EventEmitter {
|
|
|
131
159
|
this.logs.splice(0, this.logs.length - this.logLimit)
|
|
132
160
|
}
|
|
133
161
|
this.emitEvent('log', { log: entry })
|
|
162
|
+
this.persistLog(entry)
|
|
134
163
|
}
|
|
135
164
|
|
|
136
165
|
addProcessText(stream, text) {
|