@shawnstack/quickforge 1.7.3 → 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.
Files changed (210) hide show
  1. package/README.md +1 -1
  2. package/bin/quickforge.mjs +0 -21
  3. package/dist/assets/AgentProfilesPage-B84w-r9S.js +1 -0
  4. package/dist/assets/{ChatPanelHost-wfGCaCzD.js → ChatPanelHost-CyO8fEoj.js} +91 -91
  5. package/dist/assets/{PluginsPage-NAGroBm3.js → PluginsPage-D2862wul.js} +1 -1
  6. package/dist/assets/{ScheduledTasksPage-Bp0MNfQD.js → ScheduledTasksPage-GqbiJDPX.js} +2 -2
  7. package/dist/assets/SettingsWorkspacePage-B53_eDf2.js +1628 -0
  8. package/dist/assets/SharedConversationPage-Cw2QFnbC.js +1 -0
  9. package/dist/assets/TerminalDock-Dgg-_ZmU.js +2 -0
  10. package/dist/assets/WorkspaceInspector-3uLiu6Bv.js +13 -0
  11. package/dist/assets/{abnfDiagram-VRR7QNED-Dl_Fvlqg.js → abnfDiagram-VRR7QNED-Dy7veffk.js} +1 -1
  12. package/dist/assets/{arc-Bhg4c0ht.js → arc-Bo3p2lGy.js} +1 -1
  13. package/dist/assets/architecture-TIHT7OUA-7FQo5GbP.js +1 -0
  14. package/dist/assets/{architectureDiagram-ZJ3FMSHR-BuojN27B.js → architectureDiagram-ZJ3FMSHR-Cwx8qOvu.js} +1 -1
  15. package/dist/assets/{blockDiagram-677ZJIJ3-alDLbrR1.js → blockDiagram-677ZJIJ3-BiuaAOFa.js} +1 -1
  16. package/dist/assets/{c4Diagram-LMCZKHZV-D1lKa4VT.js → c4Diagram-LMCZKHZV-BTcGfhK2.js} +1 -1
  17. package/dist/assets/channel-DToA8d-4.js +1 -0
  18. package/dist/assets/chunk-2Q5K7J3B-D_Wxpk-T.js +1 -0
  19. package/dist/assets/{chunk-32BRIVSS-CwWLNjMZ.js → chunk-32BRIVSS-B9PuF_RF.js} +1 -1
  20. package/dist/assets/{chunk-52WLFC77-Be569m7d.js → chunk-52WLFC77-3zGItfIM.js} +1 -1
  21. package/dist/assets/{chunk-5FCAYU7R-BSvoiWV1.js → chunk-5FCAYU7R-DQ66qlmt.js} +1 -1
  22. package/dist/assets/{chunk-5HE753X5-CRgsy8WS.js → chunk-5HE753X5-CLqTO-VF.js} +1 -1
  23. package/dist/assets/{chunk-5JV3BV7I-CdW477UR.js → chunk-5JV3BV7I-Csxv1jr6.js} +1 -1
  24. package/dist/assets/{chunk-5TONJI2A-CdtKzSmp.js → chunk-5TONJI2A-BCQttpBr.js} +1 -1
  25. package/dist/assets/{chunk-5VM5RSS4-BKjjssLZ.js → chunk-5VM5RSS4-DEWJGSTb.js} +1 -1
  26. package/dist/assets/{chunk-7BUUIJ7U-ByAXiXdn.js → chunk-7BUUIJ7U-DNorm9MM.js} +1 -1
  27. package/dist/assets/{chunk-BIQX33UG-DeG8SCT1.js → chunk-BIQX33UG-DgvY_BOT.js} +1 -1
  28. package/dist/assets/{chunk-C7G6YPKG-D_zHT68Q.js → chunk-C7G6YPKG-DMvn9IPs.js} +1 -1
  29. package/dist/assets/{chunk-CQNSW5MT-CHpHoUSu.js → chunk-CQNSW5MT-Dq2duUgr.js} +1 -1
  30. package/dist/assets/{chunk-CYSBUYHQ-CRP50Vov.js → chunk-CYSBUYHQ-BovYyc4Y.js} +1 -1
  31. package/dist/assets/{chunk-EMLP6XTP-DX6uQQ2v.js → chunk-EMLP6XTP-BMDOpuYN.js} +1 -1
  32. package/dist/assets/{chunk-EX3LRPZG-DAJRNkHR.js → chunk-EX3LRPZG-6eP2UPOf.js} +1 -1
  33. package/dist/assets/{chunk-FWX5IMBZ-XbM5yomC.js → chunk-FWX5IMBZ-D4q-wTSy.js} +2 -2
  34. package/dist/assets/{chunk-HOUHSVGY-Ci0ljnb1.js → chunk-HOUHSVGY-XjIcuzH7.js} +1 -1
  35. package/dist/assets/{chunk-ICXQ74PX-Dy2WJ71z.js → chunk-ICXQ74PX-YBXbanq9.js} +1 -1
  36. package/dist/assets/{chunk-JG7HCLWE-2PrkzHkt.js → chunk-JG7HCLWE-CcN3uNXF.js} +1 -1
  37. package/dist/assets/{chunk-JWPE2WC7-D32uacSM.js → chunk-JWPE2WC7-Cwg6AUAt.js} +1 -1
  38. package/dist/assets/{chunk-MOJQB5TN-DIAFLtiS.js → chunk-MOJQB5TN-NNspcGIU.js} +1 -1
  39. package/dist/assets/{chunk-MOZMSUNE-BbscOaEz.js → chunk-MOZMSUNE-pMJ_jMos.js} +1 -1
  40. package/dist/assets/{chunk-OGEWGWER-ivBVxzKe.js → chunk-OGEWGWER-CJy-3Ldv.js} +1 -1
  41. package/dist/assets/{chunk-OSBZ3O6U-frD0hkdx.js → chunk-OSBZ3O6U-D7cLHHOA.js} +1 -1
  42. package/dist/assets/{chunk-PUDLZKDR-B-37Rxae.js → chunk-PUDLZKDR-D5YBi1Pi.js} +1 -1
  43. package/dist/assets/{chunk-Q4XR5HBZ-Y90m2RTe.js → chunk-Q4XR5HBZ-DYmqQdIf.js} +1 -1
  44. package/dist/assets/{chunk-QBLGF6JB-DixBqslB.js → chunk-QBLGF6JB-Bfcx00X0.js} +1 -1
  45. package/dist/assets/{chunk-R7FJI6CG-DXS1UXn_.js → chunk-R7FJI6CG-oXzadpZY.js} +1 -1
  46. package/dist/assets/{chunk-RYQCIY6F-n4JxhNd0.js → chunk-RYQCIY6F-Dt3-8yUh.js} +1 -1
  47. package/dist/assets/{chunk-U6XO7XAA-yHlgxMmn.js → chunk-U6XO7XAA-B55eSvpx.js} +1 -1
  48. package/dist/assets/{chunk-V7JOEXUC-Z6kuZfbl.js → chunk-V7JOEXUC-Btk4JwCt.js} +1 -1
  49. package/dist/assets/chunk-VAUOI2AC-CIZseNLi.js +1 -0
  50. package/dist/assets/{chunk-VR4S4FIN-Cs_SIQma.js → chunk-VR4S4FIN-DgaSiVOY.js} +1 -1
  51. package/dist/assets/{chunk-WYO6CB5R-CGExYbV-.js → chunk-WYO6CB5R-rRDZ_Tta.js} +3 -3
  52. package/dist/assets/chunk-XXDRQBXY-Do-XYxat.js +1 -0
  53. package/dist/assets/{chunk-YOTPTUD7-DopPXWSl.js → chunk-YOTPTUD7-BnFxj5o4.js} +1 -1
  54. package/dist/assets/{chunk-ZGVPDNZ5-CJHOeqMb.js → chunk-ZGVPDNZ5-CrnlPlmJ.js} +1 -1
  55. package/dist/assets/{chunk-ZIRB5QZD-BBVc5mf6.js → chunk-ZIRB5QZD-BYTiATIH.js} +1 -1
  56. package/dist/assets/classDiagram-OUVF2IWQ-Bu24Sy-k.js +1 -0
  57. package/dist/assets/classDiagram-v2-EOCWNBFH-DKhh5HSW.js +1 -0
  58. package/dist/assets/{cose-bilkent-JH36ORCC-DLjV-dTm.js → cose-bilkent-JH36ORCC-BORKJRwF.js} +1 -1
  59. package/dist/assets/cynefin-VYW2F7L2-oyiAgYC4.js +1 -0
  60. package/dist/assets/{cynefinDiagram-TSTJHNR4-BfH3zVS-.js → cynefinDiagram-TSTJHNR4-BXKeaSl8.js} +1 -1
  61. package/dist/assets/{dagre-CY3D84Cl.js → dagre-CrA0XVwJ.js} +1 -1
  62. package/dist/assets/{dagre-VKFMJZFB-BIhwZCIZ.js → dagre-VKFMJZFB-ouq_lA1a.js} +1 -1
  63. package/dist/assets/{diagram-FQU43EPY-BHnzNM2w.js → diagram-FQU43EPY-BxR5kUiQ.js} +1 -1
  64. package/dist/assets/{diagram-G47NLZAW-C7-ZlmFT.js → diagram-G47NLZAW-DWonk1sL.js} +1 -1
  65. package/dist/assets/{diagram-NH7WQ7WH-C-P9Y7IR.js → diagram-NH7WQ7WH-fmV_wV5e.js} +1 -1
  66. package/dist/assets/{diagram-OA4YK3LP-Ci5lZ_T5.js → diagram-OA4YK3LP-DvOSRRiV.js} +1 -1
  67. package/dist/assets/{diagram-WEI45ONY-C1xuK2bs.js → diagram-WEI45ONY-Bp-xb3uS.js} +1 -1
  68. package/dist/assets/dist-DLKC_odd.js +1 -0
  69. package/dist/assets/{ebnfDiagram-CCIWWBDH-DKQ5NWMD.js → ebnfDiagram-CCIWWBDH-BTlnwneA.js} +1 -1
  70. package/dist/assets/{erDiagram-Q63AITRT-HAWySadP.js → erDiagram-Q63AITRT-C3fFWGCm.js} +1 -1
  71. package/dist/assets/esm-oa69b5u4.js +2 -0
  72. package/dist/assets/eventmodeling-45OFAUF4-YB_0MefX.js +1 -0
  73. package/dist/assets/flowDiagram-23GEKE2U-Bc73xdBd.js +1 -0
  74. package/dist/assets/{ganttDiagram-NO4QXBWP-C1hwIcOA.js → ganttDiagram-NO4QXBWP-DzAiojNi.js} +1 -1
  75. package/dist/assets/gitGraph-TEB2WS4Q-Bvp27L0n.js +1 -0
  76. package/dist/assets/{gitGraphDiagram-IHSO6WYX-DYUYxs1R.js → gitGraphDiagram-IHSO6WYX-Cv57oNEA.js} +1 -1
  77. package/dist/assets/icons-cTkqD_Gi.js +1 -0
  78. package/dist/assets/index-B48prgLL.js +65 -0
  79. package/dist/assets/index-DQhwHHer.css +3 -0
  80. package/dist/assets/info-DKCQHKI2-BsLOivgq.js +1 -0
  81. package/dist/assets/{infoDiagram-FWYZ7A6U-CnIL6tv4.js → infoDiagram-FWYZ7A6U-B5Z_O3Ro.js} +1 -1
  82. package/dist/assets/{ishikawaDiagram-FXEZZL3T-B7WXNycQ.js → ishikawaDiagram-FXEZZL3T-CKTK_TKV.js} +1 -1
  83. package/dist/assets/{journeyDiagram-5HDEW3XC-D4HvioyY.js → journeyDiagram-5HDEW3XC-Cm9k4ywW.js} +1 -1
  84. package/dist/assets/{kanban-definition-HUTT4EX6-CDs763eS.js → kanban-definition-HUTT4EX6-DzNteLwx.js} +1 -1
  85. package/dist/assets/{line-ikKkQiFp.js → line-BG78c_WJ.js} +1 -1
  86. package/dist/assets/{linear-GqPyxQkN.js → linear-1X20_EGu.js} +1 -1
  87. package/dist/assets/{map-CJwjCvwr.js → map-CCI8WZkS.js} +1 -1
  88. package/dist/assets/{mcp-servers-dialog-BQbFywVL.js → mcp-servers-dialog-GXXD9K0U.js} +2 -2
  89. package/dist/assets/{mermaid-parser.core-CoE8cccr.js → mermaid-parser.core-Bb8HI_OS.js} +2 -2
  90. package/dist/assets/{mermaid-renderer-BmbSAmpG.js → mermaid-renderer-PIaNiXAz.js} +2 -2
  91. package/dist/assets/{mermaid.core-DlFSL90j.js → mermaid.core-DUHOIwL5.js} +3 -3
  92. package/dist/assets/{mindmap-definition-LN4V7U3C-DLdq0K9C.js → mindmap-definition-LN4V7U3C-Tnbszo7b.js} +1 -1
  93. package/dist/assets/{monaco-BTsVCDWS.js → monaco-BVtCspYZ.js} +1 -1
  94. package/dist/assets/{ordinal-CibZU3PM.js → ordinal-v4prQcuq.js} +1 -1
  95. package/dist/assets/{otel-BNGM50C-.js → otel-D1LswXpM.js} +1 -1
  96. package/dist/assets/packet-7NZHBO7P-BKQkXRtk.js +1 -0
  97. package/dist/assets/{pegDiagram-2B236MQR-Cm1kPRC2.js → pegDiagram-2B236MQR-CX824Kd0.js} +1 -1
  98. package/dist/assets/{pi-ai-Cr3rpCXT.js → pi-ai-DAJlO9ZB.js} +2 -2
  99. package/dist/assets/{pi-web-ui-DoRGtREc.js → pi-web-ui-B-RJSxjR.js} +1 -1
  100. package/dist/assets/pie-RZYD4A2V-BTiTN5Ge.js +1 -0
  101. package/dist/assets/{pieDiagram-ENE6RG2P-Df05wjjo.js → pieDiagram-ENE6RG2P-BT04VWPQ.js} +1 -1
  102. package/dist/assets/{quadrantDiagram-ABIIQ3AL-BjCwGxJC.js → quadrantDiagram-ABIIQ3AL-BvOZxkBr.js} +1 -1
  103. package/dist/assets/radar-I7S5WNFK-BU-oVjKM.js +1 -0
  104. package/dist/assets/railroad-3IZDKUUU-7H2hxNbL.js +1 -0
  105. package/dist/assets/railroad-abnf-AHOZXSZD-Mx6wf6XQ.js +1 -0
  106. package/dist/assets/railroad-ebnf-EBAXGLYW-BgXSKhYy.js +1 -0
  107. package/dist/assets/railroad-peg-LSFZ7HO6-DTEl2rAC.js +1 -0
  108. package/dist/assets/{railroadDiagram-RFXS5EU6-CTUvpKkv.js → railroadDiagram-RFXS5EU6-DnTU--BV.js} +1 -1
  109. package/dist/assets/{react-vendor-Dr5xvL-e.js → react-vendor-eWH0QhNU.js} +1 -1
  110. package/dist/assets/{requirementDiagram-TGXJPOKE-BU1kvEku.js → requirementDiagram-TGXJPOKE--MZX_yEv.js} +1 -1
  111. package/dist/assets/{sankeyDiagram-HTMAVEWB-BHmvxVqH.js → sankeyDiagram-HTMAVEWB-SEZkHhYn.js} +1 -1
  112. package/dist/assets/{sequenceDiagram-DBY2YBRQ-CTpwICgg.js → sequenceDiagram-DBY2YBRQ-BhC2OHiz.js} +1 -1
  113. package/dist/assets/{sizeCapture-X5ZJPWSS-BbLhGaa4.js → sizeCapture-X5ZJPWSS-Ci5LeToV.js} +1 -1
  114. package/dist/assets/{skills-dialog-REBeTFSH.js → skills-dialog-DDh1h8eH.js} +3 -1
  115. package/dist/assets/{src-B4Tvg3gT.js → src-CovulcRC.js} +1 -1
  116. package/dist/assets/{stateDiagram-2N3HPSRC-BxuDwIgT.js → stateDiagram-2N3HPSRC-DKK5tFT4.js} +1 -1
  117. package/dist/assets/stateDiagram-v2-6OUMAXLB-D8x3mkni.js +1 -0
  118. package/dist/assets/{swimlanes-5IMT3BWC-EkZsiylV.js → swimlanes-5IMT3BWC-DxxGCk67.js} +2 -2
  119. package/dist/assets/swimlanesDiagram-G3AALYLV-C4nOBNp8.js +8 -0
  120. package/dist/assets/{timeline-definition-FHXFAJF6-C9ewOCTa.js → timeline-definition-FHXFAJF6-BPXB3Sue.js} +1 -1
  121. package/dist/assets/treeView-QDETBFTQ-DOE6NSdf.js +1 -0
  122. package/dist/assets/treemap-6X3UGDF4-Ca7QD_b4.js +1 -0
  123. package/dist/assets/{vennDiagram-L72KCM5P-CR49Y4Lp.js → vennDiagram-L72KCM5P-CxcBCKye.js} +1 -1
  124. package/dist/assets/wardley-OPB4EBWU-D9O4RKRm.js +1 -0
  125. package/dist/assets/{wardleyDiagram-EHGQE667-Cn0GgwWi.js → wardleyDiagram-EHGQE667-DEL6Yes5.js} +1 -1
  126. package/dist/assets/web-RgX7H-OH.js +1 -0
  127. package/dist/assets/{xychartDiagram-FW5EYKEG-DlmPNAvd.js → xychartDiagram-FW5EYKEG-N8cPbcjC.js} +1 -1
  128. package/dist/index.html +12 -11
  129. package/dist/mobile-error.html +24 -0
  130. package/package.json +4 -1
  131. package/server/acp/server.mjs +14 -9
  132. package/server/agent-manager.mjs +215 -20
  133. package/server/ai-http-logger.mjs +68 -11
  134. package/server/ai-provider-options.mjs +3 -0
  135. package/server/channels/channel-logs.mjs +13 -0
  136. package/server/channels/event-relay.mjs +33 -0
  137. package/server/channels/process-channel.mjs +29 -0
  138. package/server/channels/providers/wechat.mjs +9 -3
  139. package/server/channels/registry.mjs +5 -1
  140. package/server/index.mjs +36 -23
  141. package/server/lan-access-store.mjs +93 -3
  142. package/server/network-proxy.mjs +68 -3
  143. package/server/plugins/registry.mjs +64 -4
  144. package/server/public-api.mjs +6 -2
  145. package/server/routes/agent-profiles.mjs +2 -2
  146. package/server/routes/agent.mjs +28 -0
  147. package/server/routes/channels.mjs +47 -0
  148. package/server/routes/lan-access.mjs +246 -21
  149. package/server/routes/models.mjs +6 -3
  150. package/server/routes/project.mjs +19 -1
  151. package/server/routes/scheduled-tasks.mjs +2 -2
  152. package/server/routes/static.mjs +26 -1
  153. package/server/routes/system.mjs +11 -0
  154. package/server/routes/terminal.mjs +7 -2
  155. package/server/routes/workspace.mjs +60 -9
  156. package/server/session-utils.mjs +2 -2
  157. package/server/subagents.mjs +1 -1
  158. package/server/terminal/terminal-manager.mjs +2 -4
  159. package/server/tools/definitions.mjs +2 -2
  160. package/server/tools/index.mjs +5 -3
  161. package/server/utils/package-update.mjs +70 -47
  162. package/server/utils/process-env.mjs +7 -0
  163. package/server/utils/response.mjs +2 -2
  164. package/dist/assets/AgentProfilesPage-D7pbg8nm.js +0 -1
  165. package/dist/assets/SettingsWorkspacePage-BtJZMIsZ.js +0 -1576
  166. package/dist/assets/SharedConversationPage-Ce3KoVZt.js +0 -1
  167. package/dist/assets/TerminalDock-4eUuXP7X.js +0 -2
  168. package/dist/assets/WorkspaceInspector-kM3BRPjY.js +0 -13
  169. package/dist/assets/architecture-TIHT7OUA-BgP-IMeo.js +0 -1
  170. package/dist/assets/channel-Rz20dKTK.js +0 -1
  171. package/dist/assets/chunk-2Q5K7J3B-Cb14ZF_k.js +0 -1
  172. package/dist/assets/chunk-VAUOI2AC-BOZvd2Sn.js +0 -1
  173. package/dist/assets/chunk-XXDRQBXY-CD_EGCtT.js +0 -1
  174. package/dist/assets/classDiagram-OUVF2IWQ-C_ozTKCt.js +0 -1
  175. package/dist/assets/classDiagram-v2-EOCWNBFH-FOfFbbZF.js +0 -1
  176. package/dist/assets/cynefin-VYW2F7L2-BzPhLtd5.js +0 -1
  177. package/dist/assets/eventmodeling-45OFAUF4-BcgGbVJ-.js +0 -1
  178. package/dist/assets/flowDiagram-23GEKE2U-dtkzcC8E.js +0 -1
  179. package/dist/assets/gitGraph-TEB2WS4Q-VByWboBU.js +0 -1
  180. package/dist/assets/icons-BP8YOS-Z.js +0 -1
  181. package/dist/assets/index-C6k5taeb.js +0 -66
  182. package/dist/assets/index-CzN8NSKC.css +0 -3
  183. package/dist/assets/info-DKCQHKI2-CZeDLZxx.js +0 -1
  184. package/dist/assets/packet-7NZHBO7P-decrrxyX.js +0 -1
  185. package/dist/assets/pie-RZYD4A2V-J1CasQZl.js +0 -1
  186. package/dist/assets/radar-I7S5WNFK-B-sZytUA.js +0 -1
  187. package/dist/assets/railroad-3IZDKUUU-BSaqNPm8.js +0 -1
  188. package/dist/assets/railroad-abnf-AHOZXSZD-lzZR7PxT.js +0 -1
  189. package/dist/assets/railroad-ebnf-EBAXGLYW-DQtz1Fh9.js +0 -1
  190. package/dist/assets/railroad-peg-LSFZ7HO6-CpMsuGY3.js +0 -1
  191. package/dist/assets/stateDiagram-v2-6OUMAXLB-BGLshxcY.js +0 -1
  192. package/dist/assets/swimlanesDiagram-G3AALYLV-CqdqxA-b.js +0 -8
  193. package/dist/assets/treeView-QDETBFTQ-Byb71heW.js +0 -1
  194. package/dist/assets/treemap-6X3UGDF4-CMV1SJsq.js +0 -1
  195. package/dist/assets/wardley-OPB4EBWU-DtCHtr2V.js +0 -1
  196. /package/dist/assets/{array-CYkMkqnU.js → array-DqLCdDFv.js} +0 -0
  197. /package/dist/assets/{chunk-KEIR6QF5-77hsebd7.js → chunk-KEIR6QF5-s7Qq-R3i.js} +0 -0
  198. /package/dist/assets/{chunk-Y2CYZVJY-CQltPwYk.js → chunk-Y2CYZVJY-BgISNSiw.js} +0 -0
  199. /package/dist/assets/{css-utils-rkE68RDy.js → css-utils-31qzCXsk.js} +0 -0
  200. /package/dist/assets/{cytoscape.esm-DwdZnGOm.js → cytoscape.esm-iekjSZ2n.js} +0 -0
  201. /package/dist/assets/{defaultLocale-Beh6XjaL.js → defaultLocale-CrJzLgRD.js} +0 -0
  202. /package/dist/assets/{dist-DelZBZpK.js → dist-zzfqk5hx.js} +0 -0
  203. /package/dist/assets/{graphlib-CAISHZh4.js → graphlib-CIz_CfxW.js} +0 -0
  204. /package/dist/assets/{init-Rr1s_RiX.js → init-C0r9Gk5G.js} +0 -0
  205. /package/dist/assets/{lit-vendor-Dr3cpBGF.js → lit-vendor-DvaQX_UQ.js} +0 -0
  206. /package/dist/assets/{logger-B65Akg8A.js → logger-8REuLPGK.js} +0 -0
  207. /package/dist/assets/{path-BAQ3hXlG.js → path-DIKpVbHL.js} +0 -0
  208. /package/dist/assets/{plugin-api-BbUkYm3Y.js → plugin-api-C8hMT5W6.js} +0 -0
  209. /package/dist/assets/{rough.esm-Djo4Abte.js → rough.esm-BMOtDaaS.js} +0 -0
  210. /package/dist/assets/{xterm-CGiQzDiR.js → xterm-BFzZ1FH4.js} +0 -0
@@ -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?.activeCapabilityPrompt,
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
- // Forward all events to the session event bus and the global bus.
1672
- emitSessionEvent(session, forwardEvent)
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
- data[sessionId] = {
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
- return metadata
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 = agentSessions.get(sessionId)
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
- await session.agent.waitForIdle()
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: session.agent.state.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,7 +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 { withDefaultAiProviderOptions } from './ai-provider-options.mjs'
6
+ import {
7
+ DEFAULT_AI_STREAM_DEADLINE_MS,
8
+ withDefaultAiProviderOptions,
9
+ } from './ai-provider-options.mjs'
7
10
  import { logsDir } from './storage.mjs'
8
11
 
9
12
  const PATCH_MARKER = Symbol.for('quickforge.aiHttpLogger.fetchPatched')
@@ -206,18 +209,72 @@ export function installAiHttpLogger() {
206
209
  globalThis[PATCH_MARKER] = true
207
210
  }
208
211
 
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 })
223
+ }
224
+ return controller.signal
225
+ }
226
+
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
+ }
254
+ }
255
+
209
256
  export function streamSimpleWithAiHttpLogging(model, context, options = {}) {
210
257
  const providerOptions = withDefaultAiProviderOptions(options)
211
- if (!aiHttpLogEnabled) return streamSimple(model, context, providerOptions)
212
-
213
- const traceContext = {
214
- traceId: randomUUID(),
215
- sessionId: providerOptions.sessionId,
216
- purpose: providerOptions.metadata?.quickforgePurpose || 'chat',
217
- provider: model?.provider,
218
- api: model?.api,
219
- model: model?.id,
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),
220
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)
221
278
 
222
- return aiHttpContext.run(traceContext, () => streamSimple(model, context, providerOptions))
279
+ return wrapStreamWithDeadline(stream, deadlineController, deadlineMs)
223
280
  }
@@ -1,8 +1,11 @@
1
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
2
4
 
3
5
  export function withDefaultAiProviderOptions(options = {}) {
4
6
  return {
5
7
  maxRetries: DEFAULT_AI_MAX_RETRIES,
8
+ timeoutMs: DEFAULT_AI_HTTP_TIMEOUT_MS,
6
9
  ...options,
7
10
  }
8
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) {