@shawnstack/quickforge 1.7.4 → 1.7.6

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 (258) hide show
  1. package/dist/assets/AgentProfilesPage-Dl5Peqov.js +1 -0
  2. package/dist/assets/ChatPanelHost-D3987TUJ.js +307 -0
  3. package/dist/assets/CloudAccountSettingsPage-BG5Nn3bq.js +1 -0
  4. package/dist/assets/PluginsPage-BDKYfLna.js +1 -0
  5. package/dist/assets/ScheduledTasksPage--GAki3lB.js +2 -0
  6. package/dist/assets/SettingsWorkspacePage-DSy66Tuw.js +1696 -0
  7. package/dist/assets/ShareLinksSettingsPage-BQQMsvJo.js +1 -0
  8. package/dist/assets/SharedConversationPage-B9BUVCri.js +1 -0
  9. package/dist/assets/TerminalDock-CT1nYo7P.js +2 -0
  10. package/dist/assets/WorkspaceInspector-BnfDLf9a.js +36 -0
  11. package/dist/assets/{abnfDiagram-VRR7QNED-Dy7veffk.js → abnfDiagram-VRR7QNED-CMhVoYUh.js} +1 -1
  12. package/dist/assets/{arc-Bo3p2lGy.js → arc-w21O0TLn.js} +1 -1
  13. package/dist/assets/architecture-TIHT7OUA-DMwFMHxq.js +1 -0
  14. package/dist/assets/{architectureDiagram-ZJ3FMSHR-Cwx8qOvu.js → architectureDiagram-ZJ3FMSHR-C52qL6Hg.js} +1 -1
  15. package/dist/assets/{blockDiagram-677ZJIJ3-BiuaAOFa.js → blockDiagram-677ZJIJ3-CM3XbLol.js} +1 -1
  16. package/dist/assets/{c4Diagram-LMCZKHZV-BTcGfhK2.js → c4Diagram-LMCZKHZV-D7Bh1ui1.js} +1 -1
  17. package/dist/assets/channel-Duu1CuH9.js +1 -0
  18. package/dist/assets/chunk-2Q5K7J3B-B6rHWc4S.js +1 -0
  19. package/dist/assets/{chunk-32BRIVSS-B9PuF_RF.js → chunk-32BRIVSS-xgrtbT8e.js} +1 -1
  20. package/dist/assets/{chunk-52WLFC77-3zGItfIM.js → chunk-52WLFC77-CDbaHw80.js} +1 -1
  21. package/dist/assets/{chunk-5FCAYU7R-DQ66qlmt.js → chunk-5FCAYU7R-CcSvVbPc.js} +1 -1
  22. package/dist/assets/{chunk-5HE753X5-CLqTO-VF.js → chunk-5HE753X5-erFpzzgx.js} +1 -1
  23. package/dist/assets/{chunk-5JV3BV7I-Csxv1jr6.js → chunk-5JV3BV7I-rEhN9F6G.js} +1 -1
  24. package/dist/assets/{chunk-5TONJI2A-BCQttpBr.js → chunk-5TONJI2A-D8L-QTwD.js} +1 -1
  25. package/dist/assets/{chunk-5VM5RSS4-DEWJGSTb.js → chunk-5VM5RSS4-CjlfMBbM.js} +1 -1
  26. package/dist/assets/{chunk-7BUUIJ7U-DNorm9MM.js → chunk-7BUUIJ7U-CCcsI9XE.js} +1 -1
  27. package/dist/assets/{chunk-BIQX33UG-DgvY_BOT.js → chunk-BIQX33UG-FSqFbmb3.js} +1 -1
  28. package/dist/assets/{chunk-C7G6YPKG-DMvn9IPs.js → chunk-C7G6YPKG-ZMLUvfLy.js} +1 -1
  29. package/dist/assets/{chunk-CQNSW5MT-Dq2duUgr.js → chunk-CQNSW5MT-DZuBpk25.js} +1 -1
  30. package/dist/assets/{chunk-CYSBUYHQ-BovYyc4Y.js → chunk-CYSBUYHQ-CB26wsG0.js} +1 -1
  31. package/dist/assets/{chunk-EMLP6XTP-BMDOpuYN.js → chunk-EMLP6XTP-BbsM8oRw.js} +1 -1
  32. package/dist/assets/{chunk-EX3LRPZG-6eP2UPOf.js → chunk-EX3LRPZG-BcQhaqt7.js} +1 -1
  33. package/dist/assets/{chunk-FWX5IMBZ-D4q-wTSy.js → chunk-FWX5IMBZ-DrnrdrSu.js} +2 -2
  34. package/dist/assets/{chunk-HOUHSVGY-XjIcuzH7.js → chunk-HOUHSVGY-tdc-Z9eW.js} +1 -1
  35. package/dist/assets/{chunk-ICXQ74PX-YBXbanq9.js → chunk-ICXQ74PX-Cd7igVGt.js} +1 -1
  36. package/dist/assets/{chunk-JG7HCLWE-CcN3uNXF.js → chunk-JG7HCLWE-iXXGW8nf.js} +1 -1
  37. package/dist/assets/{chunk-JWPE2WC7-Cwg6AUAt.js → chunk-JWPE2WC7-BZ87mXEv.js} +1 -1
  38. package/dist/assets/{chunk-MOJQB5TN-NNspcGIU.js → chunk-MOJQB5TN-CLTjV5ov.js} +1 -1
  39. package/dist/assets/{chunk-MOZMSUNE-pMJ_jMos.js → chunk-MOZMSUNE-B3SCBrRH.js} +1 -1
  40. package/dist/assets/{chunk-OGEWGWER-CJy-3Ldv.js → chunk-OGEWGWER-TgvkXkFX.js} +1 -1
  41. package/dist/assets/{chunk-OSBZ3O6U-D7cLHHOA.js → chunk-OSBZ3O6U-B9E2E5v1.js} +1 -1
  42. package/dist/assets/{chunk-PUDLZKDR-D5YBi1Pi.js → chunk-PUDLZKDR-C5GMVYup.js} +1 -1
  43. package/dist/assets/{chunk-Q4XR5HBZ-DYmqQdIf.js → chunk-Q4XR5HBZ-fo5-HsqY.js} +1 -1
  44. package/dist/assets/{chunk-QBLGF6JB-Bfcx00X0.js → chunk-QBLGF6JB-Dwfg8sKc.js} +1 -1
  45. package/dist/assets/{chunk-R7FJI6CG-oXzadpZY.js → chunk-R7FJI6CG-DpBrv4ni.js} +1 -1
  46. package/dist/assets/{chunk-RYQCIY6F-Dt3-8yUh.js → chunk-RYQCIY6F-DzzmTl11.js} +1 -1
  47. package/dist/assets/{chunk-U6XO7XAA-B55eSvpx.js → chunk-U6XO7XAA-Dw6aviKa.js} +1 -1
  48. package/dist/assets/{chunk-V7JOEXUC-Btk4JwCt.js → chunk-V7JOEXUC-CEDazV6w.js} +1 -1
  49. package/dist/assets/chunk-VAUOI2AC-CWYjg8zE.js +1 -0
  50. package/dist/assets/{chunk-VR4S4FIN-DgaSiVOY.js → chunk-VR4S4FIN-C_-eEj-9.js} +1 -1
  51. package/dist/assets/{chunk-WYO6CB5R-rRDZ_Tta.js → chunk-WYO6CB5R-NF-MsJJR.js} +3 -3
  52. package/dist/assets/chunk-XXDRQBXY-DzTwx8ZH.js +1 -0
  53. package/dist/assets/{chunk-YOTPTUD7-BnFxj5o4.js → chunk-YOTPTUD7-DqzqtdII.js} +1 -1
  54. package/dist/assets/{chunk-ZGVPDNZ5-CrnlPlmJ.js → chunk-ZGVPDNZ5-CRrAYi4g.js} +1 -1
  55. package/dist/assets/{chunk-ZIRB5QZD-BYTiATIH.js → chunk-ZIRB5QZD-Dc1s5bLM.js} +1 -1
  56. package/dist/assets/classDiagram-OUVF2IWQ-Cc63fLWP.js +1 -0
  57. package/dist/assets/classDiagram-v2-EOCWNBFH-CoXU0qT6.js +1 -0
  58. package/dist/assets/{cose-bilkent-JH36ORCC-BORKJRwF.js → cose-bilkent-JH36ORCC-cL247pHg.js} +1 -1
  59. package/dist/assets/cynefin-VYW2F7L2-C8yCvH_o.js +1 -0
  60. package/dist/assets/{cynefinDiagram-TSTJHNR4-BXKeaSl8.js → cynefinDiagram-TSTJHNR4-D4sZ7LYI.js} +1 -1
  61. package/dist/assets/{dagre-CrA0XVwJ.js → dagre-DmHb3Ctt.js} +1 -1
  62. package/dist/assets/{dagre-VKFMJZFB-ouq_lA1a.js → dagre-VKFMJZFB-BlsnSh-Q.js} +1 -1
  63. package/dist/assets/{diagram-FQU43EPY-BxR5kUiQ.js → diagram-FQU43EPY-DW1yQiRO.js} +1 -1
  64. package/dist/assets/{diagram-G47NLZAW-DWonk1sL.js → diagram-G47NLZAW-BTUAsFjb.js} +1 -1
  65. package/dist/assets/{diagram-NH7WQ7WH-fmV_wV5e.js → diagram-NH7WQ7WH-BcaomqSd.js} +1 -1
  66. package/dist/assets/{diagram-OA4YK3LP-DvOSRRiV.js → diagram-OA4YK3LP-DDLX7Xwr.js} +1 -1
  67. package/dist/assets/{diagram-WEI45ONY-Bp-xb3uS.js → diagram-WEI45ONY-CIave0An.js} +1 -1
  68. package/dist/assets/{dist-DLKC_odd.js → dist-yDHbi5ux.js} +1 -1
  69. package/dist/assets/{ebnfDiagram-CCIWWBDH-BTlnwneA.js → ebnfDiagram-CCIWWBDH-Cr1Y6lPu.js} +1 -1
  70. package/dist/assets/{erDiagram-Q63AITRT-C3fFWGCm.js → erDiagram-Q63AITRT-D3EgiVVH.js} +1 -1
  71. package/dist/assets/{esm-oa69b5u4.js → esm-sdsK6gaB.js} +2 -2
  72. package/dist/assets/eventmodeling-45OFAUF4-DO1vRTM0.js +1 -0
  73. package/dist/assets/flowDiagram-23GEKE2U-BS7KXcqn.js +1 -0
  74. package/dist/assets/{ganttDiagram-NO4QXBWP-DzAiojNi.js → ganttDiagram-NO4QXBWP-B0oyDIqK.js} +1 -1
  75. package/dist/assets/gitGraph-TEB2WS4Q-m5F5ef09.js +1 -0
  76. package/dist/assets/{gitGraphDiagram-IHSO6WYX-Cv57oNEA.js → gitGraphDiagram-IHSO6WYX-uQtr6IqK.js} +1 -1
  77. package/dist/assets/icons-2D18YbSI.js +1 -0
  78. package/dist/assets/index-1ei6nhs-.css +3 -0
  79. package/dist/assets/index-BivLuZvX.js +65 -0
  80. package/dist/assets/info-DKCQHKI2-CZxPlQJ0.js +1 -0
  81. package/dist/assets/{infoDiagram-FWYZ7A6U-B5Z_O3Ro.js → infoDiagram-FWYZ7A6U-B_GzlLIM.js} +1 -1
  82. package/dist/assets/{ishikawaDiagram-FXEZZL3T-CKTK_TKV.js → ishikawaDiagram-FXEZZL3T-CKQIsSdU.js} +1 -1
  83. package/dist/assets/{journeyDiagram-5HDEW3XC-Cm9k4ywW.js → journeyDiagram-5HDEW3XC-DrrvzZL1.js} +1 -1
  84. package/dist/assets/{kanban-definition-HUTT4EX6-DzNteLwx.js → kanban-definition-HUTT4EX6-CJQRTdXz.js} +1 -1
  85. package/dist/assets/{line-BG78c_WJ.js → line-B4DxX7tC.js} +1 -1
  86. package/dist/assets/{linear-1X20_EGu.js → linear-U-nKUhyW.js} +1 -1
  87. package/dist/assets/{map-CCI8WZkS.js → map-BneTM-ri.js} +1 -1
  88. package/dist/assets/{mcp-servers-dialog-GXXD9K0U.js → mcp-servers-dialog-BEV4_TXC.js} +2 -2
  89. package/dist/assets/{mermaid-parser.core-Bb8HI_OS.js → mermaid-parser.core-BlKWrEdZ.js} +2 -2
  90. package/dist/assets/{mermaid-renderer-PIaNiXAz.js → mermaid-renderer-jEbqFaTe.js} +2 -2
  91. package/dist/assets/{mermaid.core-DUHOIwL5.js → mermaid.core-DWpShXCs.js} +3 -3
  92. package/dist/assets/{mindmap-definition-LN4V7U3C-Tnbszo7b.js → mindmap-definition-LN4V7U3C-CEVwZD78.js} +1 -1
  93. package/dist/assets/{logger-8REuLPGK.js → model-display-label-C6-0eDxF.js} +1 -1
  94. package/dist/assets/model-identity-CgA6_Pbm.js +1 -0
  95. package/dist/assets/model-reference-o41fJ6jQ.js +1 -0
  96. package/dist/assets/{monaco-BVtCspYZ.js → monaco-DvXErxp2.js} +1 -1
  97. package/dist/assets/{ordinal-v4prQcuq.js → ordinal-B2r5L1v4.js} +1 -1
  98. package/dist/assets/{otel-D1LswXpM.js → otel-BFnphzjt.js} +1 -1
  99. package/dist/assets/packet-7NZHBO7P-zIioIZVN.js +1 -0
  100. package/dist/assets/{pegDiagram-2B236MQR-CX824Kd0.js → pegDiagram-2B236MQR-CBLkBdnQ.js} +1 -1
  101. package/dist/assets/{pi-ai-DAJlO9ZB.js → pi-ai-Cr2ee4a0.js} +2 -2
  102. package/dist/assets/{pi-web-ui-B-RJSxjR.js → pi-web-ui-DV5jKjSg.js} +1 -1
  103. package/dist/assets/pie-RZYD4A2V-GGWijBvB.js +1 -0
  104. package/dist/assets/{pieDiagram-ENE6RG2P-BT04VWPQ.js → pieDiagram-ENE6RG2P-8tCnwtWS.js} +1 -1
  105. package/dist/assets/{quadrantDiagram-ABIIQ3AL-BvOZxkBr.js → quadrantDiagram-ABIIQ3AL-CWd28cHF.js} +1 -1
  106. package/dist/assets/radar-I7S5WNFK-yvwFH3RV.js +1 -0
  107. package/dist/assets/railroad-3IZDKUUU-BVadlswB.js +1 -0
  108. package/dist/assets/railroad-abnf-AHOZXSZD-B7R-p_2-.js +1 -0
  109. package/dist/assets/railroad-ebnf-EBAXGLYW-BnbJxPug.js +1 -0
  110. package/dist/assets/railroad-peg-LSFZ7HO6-Ci3DcEuQ.js +1 -0
  111. package/dist/assets/{railroadDiagram-RFXS5EU6-DnTU--BV.js → railroadDiagram-RFXS5EU6-CI4ouOBs.js} +1 -1
  112. package/dist/assets/{react-vendor-eWH0QhNU.js → react-vendor-3UaA-C9W.js} +1 -1
  113. package/dist/assets/{requirementDiagram-TGXJPOKE--MZX_yEv.js → requirementDiagram-TGXJPOKE-DtFz7sw4.js} +1 -1
  114. package/dist/assets/{sankeyDiagram-HTMAVEWB-SEZkHhYn.js → sankeyDiagram-HTMAVEWB--VeOwwk9.js} +1 -1
  115. package/dist/assets/{sequenceDiagram-DBY2YBRQ-BhC2OHiz.js → sequenceDiagram-DBY2YBRQ-B8AHk5Oe.js} +1 -1
  116. package/dist/assets/{sizeCapture-X5ZJPWSS-Ci5LeToV.js → sizeCapture-X5ZJPWSS-vV_ArM8D.js} +1 -1
  117. package/dist/assets/{skills-dialog-DDh1h8eH.js → skills-dialog-BTJ7mzk_.js} +3 -3
  118. package/dist/assets/{src-CovulcRC.js → src-PYsDme0X.js} +1 -1
  119. package/dist/assets/{stateDiagram-2N3HPSRC-DKK5tFT4.js → stateDiagram-2N3HPSRC-etXnJ8X8.js} +1 -1
  120. package/dist/assets/stateDiagram-v2-6OUMAXLB-Dms3pICj.js +1 -0
  121. package/dist/assets/{swimlanes-5IMT3BWC-DxxGCk67.js → swimlanes-5IMT3BWC-pabIKAmn.js} +2 -2
  122. package/dist/assets/swimlanesDiagram-G3AALYLV-B14aWjv6.js +8 -0
  123. package/dist/assets/{timeline-definition-FHXFAJF6-BPXB3Sue.js → timeline-definition-FHXFAJF6-C55MKm3L.js} +1 -1
  124. package/dist/assets/treeView-QDETBFTQ-Cru9B2zv.js +1 -0
  125. package/dist/assets/treemap-6X3UGDF4-DNOVHFaJ.js +1 -0
  126. package/dist/assets/{vennDiagram-L72KCM5P-CxcBCKye.js → vennDiagram-L72KCM5P-CgNf1Q9W.js} +1 -1
  127. package/dist/assets/wardley-OPB4EBWU-E3m1BOJQ.js +1 -0
  128. package/dist/assets/{wardleyDiagram-EHGQE667-DEL6Yes5.js → wardleyDiagram-EHGQE667-Drt5ytec.js} +1 -1
  129. package/dist/assets/{web-RgX7H-OH.js → web-B5qgZJRv.js} +1 -1
  130. package/dist/assets/{xychartDiagram-FW5EYKEG-N8cPbcjC.js → xychartDiagram-FW5EYKEG-hhoEo_Ar.js} +1 -1
  131. package/dist/index.html +11 -10
  132. package/dist/sw.js +17 -0
  133. package/package.json +4 -1
  134. package/plugins/{openai-documents → documents}/plugin.json +2 -2
  135. package/plugins/{openai-presentations → presentations}/plugin.json +2 -2
  136. package/plugins/{openai-spreadsheets → spreadsheets}/plugin.json +2 -2
  137. package/runtime-assets/agent/darwin-arm64/qf-agent +0 -0
  138. package/runtime-assets/agent/darwin-x64/qf-agent +0 -0
  139. package/runtime-assets/agent/linux-arm64/qf-agent +0 -0
  140. package/runtime-assets/agent/linux-x64/qf-agent +0 -0
  141. package/runtime-assets/agent/win32-x64/qf-agent.exe +0 -0
  142. package/server/access-policy.mjs +11 -0
  143. package/server/acp/server.mjs +36 -26
  144. package/server/agent-manager.mjs +474 -71
  145. package/server/agent-profile-files.mjs +8 -4
  146. package/server/agent-profile-schema.mjs +47 -23
  147. package/server/agent-profiles.mjs +28 -9
  148. package/server/ai-http-logger.mjs +98 -12
  149. package/server/auto-compaction.mjs +35 -8
  150. package/server/cloud/chat-idempotency-store.mjs +96 -0
  151. package/server/cloud/client.mjs +99 -0
  152. package/server/cloud/config.mjs +54 -0
  153. package/server/cloud/credential-store.mjs +200 -0
  154. package/server/cloud/identity.mjs +312 -0
  155. package/server/cloud/index.mjs +7 -0
  156. package/server/cloud/models.mjs +80 -0
  157. package/server/cloud/qf-agent-process.mjs +571 -0
  158. package/server/cloud/runtime.mjs +135 -0
  159. package/server/cloud/service-config.mjs +81 -0
  160. package/server/index.mjs +124 -48
  161. package/server/mcp/registry.mjs +8 -7
  162. package/server/message-converters.mjs +40 -19
  163. package/server/model-catalog.mjs +269 -0
  164. package/server/opencode-acp-agent.mjs +1151 -0
  165. package/server/plugins/registry.mjs +29 -5
  166. package/server/public-api.mjs +11 -5
  167. package/server/routes/agent-profiles.mjs +46 -9
  168. package/server/routes/agent.mjs +60 -11
  169. package/server/routes/backup.mjs +8 -1
  170. package/server/routes/cloud.mjs +246 -0
  171. package/server/routes/models.mjs +7 -1
  172. package/server/routes/scheduled-tasks.mjs +38 -8
  173. package/server/routes/shared-conversation.mjs +62 -58
  174. package/server/routes/shares.mjs +75 -1
  175. package/server/routes/storage.mjs +9 -1
  176. package/server/routes/system.mjs +7 -4
  177. package/server/routes/workspace.mjs +6 -4
  178. package/server/share-store.mjs +168 -13
  179. package/server/skills.mjs +30 -3
  180. package/server/storage.mjs +7 -1
  181. package/server/subagents.mjs +3 -3
  182. package/server/tools/definitions.mjs +1 -1
  183. package/server/tools/index.mjs +2 -2
  184. package/server/utils/network.mjs +9 -0
  185. package/server/utils/process-tree.mjs +78 -0
  186. package/server/utils/response.mjs +8 -1
  187. package/skills/skill-creator/LICENSE.txt +202 -0
  188. package/skills/skill-creator/SKILL.md +485 -0
  189. package/skills/skill-creator/UPSTREAM.md +6 -0
  190. package/skills/skill-creator/agents/analyzer.md +274 -0
  191. package/skills/skill-creator/agents/comparator.md +202 -0
  192. package/skills/skill-creator/agents/grader.md +223 -0
  193. package/skills/skill-creator/assets/eval_review.html +146 -0
  194. package/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  195. package/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  196. package/skills/skill-creator/references/schemas.md +430 -0
  197. package/skills/skill-creator/scripts/__init__.py +0 -0
  198. package/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  199. package/skills/skill-creator/scripts/generate_report.py +326 -0
  200. package/skills/skill-creator/scripts/improve_description.py +247 -0
  201. package/skills/skill-creator/scripts/package_skill.py +136 -0
  202. package/skills/skill-creator/scripts/quick_validate.py +103 -0
  203. package/skills/skill-creator/scripts/run_eval.py +310 -0
  204. package/skills/skill-creator/scripts/run_loop.py +328 -0
  205. package/skills/skill-creator/scripts/utils.py +47 -0
  206. package/dist/assets/AgentProfilesPage-B84w-r9S.js +0 -1
  207. package/dist/assets/ChatPanelHost-CyO8fEoj.js +0 -288
  208. package/dist/assets/PluginsPage-D2862wul.js +0 -1
  209. package/dist/assets/ScheduledTasksPage-GqbiJDPX.js +0 -2
  210. package/dist/assets/SettingsWorkspacePage-B53_eDf2.js +0 -1628
  211. package/dist/assets/SharedConversationPage-Cw2QFnbC.js +0 -1
  212. package/dist/assets/TerminalDock-Dgg-_ZmU.js +0 -2
  213. package/dist/assets/WorkspaceInspector-3uLiu6Bv.js +0 -13
  214. package/dist/assets/architecture-TIHT7OUA-7FQo5GbP.js +0 -1
  215. package/dist/assets/channel-DToA8d-4.js +0 -1
  216. package/dist/assets/chunk-2Q5K7J3B-D_Wxpk-T.js +0 -1
  217. package/dist/assets/chunk-VAUOI2AC-CIZseNLi.js +0 -1
  218. package/dist/assets/chunk-XXDRQBXY-Do-XYxat.js +0 -1
  219. package/dist/assets/classDiagram-OUVF2IWQ-Bu24Sy-k.js +0 -1
  220. package/dist/assets/classDiagram-v2-EOCWNBFH-DKhh5HSW.js +0 -1
  221. package/dist/assets/cynefin-VYW2F7L2-oyiAgYC4.js +0 -1
  222. package/dist/assets/eventmodeling-45OFAUF4-YB_0MefX.js +0 -1
  223. package/dist/assets/flowDiagram-23GEKE2U-Bc73xdBd.js +0 -1
  224. package/dist/assets/gitGraph-TEB2WS4Q-Bvp27L0n.js +0 -1
  225. package/dist/assets/icons-cTkqD_Gi.js +0 -1
  226. package/dist/assets/index-B48prgLL.js +0 -65
  227. package/dist/assets/index-DQhwHHer.css +0 -3
  228. package/dist/assets/info-DKCQHKI2-BsLOivgq.js +0 -1
  229. package/dist/assets/packet-7NZHBO7P-BKQkXRtk.js +0 -1
  230. package/dist/assets/pie-RZYD4A2V-BTiTN5Ge.js +0 -1
  231. package/dist/assets/radar-I7S5WNFK-BU-oVjKM.js +0 -1
  232. package/dist/assets/railroad-3IZDKUUU-7H2hxNbL.js +0 -1
  233. package/dist/assets/railroad-abnf-AHOZXSZD-Mx6wf6XQ.js +0 -1
  234. package/dist/assets/railroad-ebnf-EBAXGLYW-BgXSKhYy.js +0 -1
  235. package/dist/assets/railroad-peg-LSFZ7HO6-DTEl2rAC.js +0 -1
  236. package/dist/assets/stateDiagram-v2-6OUMAXLB-D8x3mkni.js +0 -1
  237. package/dist/assets/swimlanesDiagram-G3AALYLV-C4nOBNp8.js +0 -8
  238. package/dist/assets/treeView-QDETBFTQ-DOE6NSdf.js +0 -1
  239. package/dist/assets/treemap-6X3UGDF4-Ca7QD_b4.js +0 -1
  240. package/dist/assets/wardley-OPB4EBWU-D9O4RKRm.js +0 -1
  241. /package/dist/assets/{array-DqLCdDFv.js → array-B9UHiPd-.js} +0 -0
  242. /package/dist/assets/{chunk-KEIR6QF5-s7Qq-R3i.js → chunk-KEIR6QF5-jrz1pRUS.js} +0 -0
  243. /package/dist/assets/{chunk-Y2CYZVJY-BgISNSiw.js → chunk-Y2CYZVJY-D6N-CrJv.js} +0 -0
  244. /package/dist/assets/{cytoscape.esm-iekjSZ2n.js → cytoscape.esm-DyfqweF2.js} +0 -0
  245. /package/dist/assets/{defaultLocale-CrJzLgRD.js → defaultLocale-5eAKkKJC.js} +0 -0
  246. /package/dist/assets/{dist-zzfqk5hx.js → dist-CLg13NtV.js} +0 -0
  247. /package/dist/assets/{graphlib-CIz_CfxW.js → graphlib-DJkmx0Cq.js} +0 -0
  248. /package/dist/assets/{init-C0r9Gk5G.js → init-DlZdxViB.js} +0 -0
  249. /package/dist/assets/{path-DIKpVbHL.js → path-6uRLdFF7.js} +0 -0
  250. /package/dist/assets/{plugin-api-C8hMT5W6.js → plugin-api-Uu2xAB4d.js} +0 -0
  251. /package/dist/assets/{rough.esm-BMOtDaaS.js → rough.esm-Bud4g4fh.js} +0 -0
  252. /package/dist/assets/{xterm-BFzZ1FH4.js → xterm-Bm_IbozX.js} +0 -0
  253. /package/plugins/{openai-documents → documents}/commands/document.md +0 -0
  254. /package/plugins/{openai-documents → documents}/skills/documents/SKILL.md +0 -0
  255. /package/plugins/{openai-presentations → presentations}/commands/presentation.md +0 -0
  256. /package/plugins/{openai-presentations → presentations}/skills/presentations/SKILL.md +0 -0
  257. /package/plugins/{openai-spreadsheets → spreadsheets}/commands/spreadsheet.md +0 -0
  258. /package/plugins/{openai-spreadsheets → spreadsheets}/skills/spreadsheets/SKILL.md +0 -0
@@ -8,11 +8,13 @@ import { loadSkillToolContext, abortRunningCommand } from './tools/index.mjs'
8
8
  import { createSkillTools, globalMemoryTool, workspaceTools } from './tools/definitions.mjs'
9
9
  import { createMcpToolDefinitions, isMcpToolName } from './mcp/registry.mjs'
10
10
  import { createPluginToolDefinitions, isPluginToolName } from './plugins/registry.mjs'
11
+ import { createOpenCodeAcpAgent } from './opencode-acp-agent.mjs'
11
12
  import {
12
13
  composeSubagentSystemPrompt,
13
14
  formatSubagentTask,
14
15
  } from './subagents.mjs'
15
16
  import { agentProfileSnapshot, getAgentProfile } from './agent-profiles.mjs'
17
+ import { modelBindingFromModel, resolveImplicitModelPreference, resolveModelBinding } from './model-catalog.mjs'
16
18
  import { agentProfileFromMarkdown } from './agent-profile-files.mjs'
17
19
  import {
18
20
  applyCapabilityPolicy,
@@ -147,6 +149,10 @@ async function createServerTools(projectId, projectContext, skillsContext, inclu
147
149
  }
148
150
 
149
151
  async function rebuildSessionTools(session) {
152
+ if (session.harness === AGENT_HARNESS_OPENCODE) {
153
+ session.agent.state.tools = []
154
+ return
155
+ }
150
156
  const profileToolNames = Array.isArray(session.agentProfile?.allowedTools) ? session.agentProfile.allowedTools : null
151
157
  session.agent.state.tools = await createServerTools(
152
158
  session.projectId,
@@ -179,6 +185,25 @@ const agentSessions = new Map()
179
185
 
180
186
  const AGENT_ACCESS_MODE_DEFAULT = 'default'
181
187
  const AGENT_ACCESS_MODE_FULL_ACCESS = 'full-access'
188
+ const AGENT_HARNESS_QUICKFORGE = 'quickforge'
189
+ const AGENT_HARNESS_OPENCODE = 'opencode'
190
+
191
+ export function normalizeAgentHarness(value, fallback = AGENT_HARNESS_QUICKFORGE) {
192
+ if (value === AGENT_HARNESS_QUICKFORGE || value === AGENT_HARNESS_OPENCODE) return value
193
+ if (fallback !== value) return normalizeAgentHarness(fallback, AGENT_HARNESS_QUICKFORGE)
194
+ return AGENT_HARNESS_QUICKFORGE
195
+ }
196
+
197
+ export function validateAgentHarness(value) {
198
+ if (value === undefined || value === null || value === '') return AGENT_HARNESS_QUICKFORGE
199
+ if (value === 'claude-code') {
200
+ throw Object.assign(new Error('Claude Code Harness is not available yet.'), { statusCode: 400 })
201
+ }
202
+ if (value !== AGENT_HARNESS_QUICKFORGE && value !== AGENT_HARNESS_OPENCODE) {
203
+ throw Object.assign(new Error(`Unsupported Harness: ${value}`), { statusCode: 400 })
204
+ }
205
+ return value
206
+ }
182
207
 
183
208
  function normalizeAccessMode(value, fallback = AGENT_ACCESS_MODE_DEFAULT) {
184
209
  if (value === AGENT_ACCESS_MODE_DEFAULT || value === AGENT_ACCESS_MODE_FULL_ACCESS) return value
@@ -200,7 +225,7 @@ function hasFullAccess(session) {
200
225
 
201
226
  const IDLE_TIMEOUT_MS = 30 * 60 * 1000 // 30 minutes
202
227
  const ABORT_IDLE_WAIT_TIMEOUT_MS = 3000
203
- const SUBAGENT_DEFAULT_TIMEOUT_MS = 30 * 60 * 1000
228
+ const SUBAGENT_DEFAULT_TIMEOUT_MS = 60 * 60 * 1000
204
229
  const SUBAGENT_TRACE_THROTTLE_MS = 150
205
230
 
206
231
  /**
@@ -278,6 +303,58 @@ function createApprovalPromise(session, toolCallId, toolName, args, source) {
278
303
  })
279
304
  }
280
305
 
306
+ function createAcpApprovalPromise(session, request) {
307
+ return new Promise((resolve, reject) => {
308
+ let settled = false
309
+ const requestedAt = Date.now()
310
+ const expiresAt = requestedAt + APPROVAL_TIMEOUT_MS
311
+ const timeout = setTimeout(() => {
312
+ if (settled) return
313
+ settled = true
314
+ pendingApprovals.delete(request.toolCallId)
315
+ resolve({ outcome: { outcome: 'cancelled' } })
316
+ }, APPROVAL_TIMEOUT_MS)
317
+
318
+ const cleanup = () => {
319
+ clearTimeout(timeout)
320
+ if (settled) return
321
+ settled = true
322
+ pendingApprovals.delete(request.toolCallId)
323
+ }
324
+ const allowOption = request.options.find((option) => option.kind === 'allow_once' || option.kind === 'allow_always')
325
+ const rejectOption = request.options.find((option) => option.kind === 'reject_once' || option.kind === 'reject_always')
326
+
327
+ pendingApprovals.set(request.toolCallId, {
328
+ resolve: (approved) => {
329
+ cleanup()
330
+ const option = approved ? allowOption : rejectOption
331
+ resolve(option
332
+ ? { outcome: { outcome: 'selected', optionId: option.optionId } }
333
+ : { outcome: { outcome: 'cancelled' } })
334
+ },
335
+ reject: (error) => {
336
+ cleanup()
337
+ reject(error)
338
+ },
339
+ sessionId: session.sessionId,
340
+ toolName: request.toolName,
341
+ args: request.args,
342
+ source: 'opencode',
343
+ requestedAt,
344
+ expiresAt,
345
+ })
346
+
347
+ emitSessionEvent(session, {
348
+ type: 'tool_approval_required',
349
+ sessionId: session.sessionId,
350
+ toolCallId: request.toolCallId,
351
+ toolName: request.toolName,
352
+ args: request.args,
353
+ source: 'opencode',
354
+ })
355
+ })
356
+ }
357
+
281
358
  function createAutoCompactApprovalPromise(session, details = {}) {
282
359
  if (!session) return Promise.resolve(false)
283
360
  const approvalId = randomUUID()
@@ -376,6 +453,24 @@ function userTextMessage(text, details) {
376
453
  }
377
454
  }
378
455
 
456
+ function assistantErrorMessage(text, model) {
457
+ return {
458
+ ...assistantTextMessage('', model),
459
+ stopReason: 'error',
460
+ errorMessage: text,
461
+ timestamp: Date.now(),
462
+ }
463
+ }
464
+
465
+ export function appendAssistantErrorMessageOnce(messages, errorMessage, model) {
466
+ const current = Array.isArray(messages) ? messages : []
467
+ const lastMessage = current[current.length - 1]
468
+ const errorAlreadyShown = lastMessage?.role === 'assistant'
469
+ && lastMessage?.stopReason === 'error'
470
+ && lastMessage?.errorMessage
471
+ return errorAlreadyShown ? current : [...current, assistantErrorMessage(errorMessage, model)]
472
+ }
473
+
379
474
  function compactedSessionTitle(title) {
380
475
  const base = typeof title === 'string' && title.trim() ? title.trim() : 'New chat'
381
476
  if (base === 'New chat') return 'Compacted chat'
@@ -515,6 +610,37 @@ function updateSessionMessages(session, messages) {
515
610
  session.agent.state.messages = messages
516
611
  }
517
612
 
613
+ const CLIENT_MESSAGE_ID_FIELD = 'quickforgeClientMessageId'
614
+
615
+ function isManagedCloudModel(model) {
616
+ return model?.provider === 'quickforge-cloud' && model?.quickforgeModelSource === 'cloud'
617
+ }
618
+
619
+ function objectMetadata(message) {
620
+ const metadata = message?.metadata
621
+ return metadata && typeof metadata === 'object' && !Array.isArray(metadata) ? metadata : {}
622
+ }
623
+
624
+ function logicalMessageId(message) {
625
+ const value = objectMetadata(message)[CLIENT_MESSAGE_ID_FIELD]
626
+ return typeof value === 'string' && /^[A-Za-z0-9._:-]{1,128}$/.test(value) ? value : undefined
627
+ }
628
+
629
+ function messageWithLogicalId(message) {
630
+ if (!message || typeof message !== 'object' || logicalMessageId(message)) return message
631
+ return {
632
+ ...message,
633
+ metadata: {
634
+ ...objectMetadata(message),
635
+ [CLIENT_MESSAGE_ID_FIELD]: `qfcm_${randomUUID()}`,
636
+ },
637
+ }
638
+ }
639
+
640
+ function prepareCloudUserMessage(session, message) {
641
+ return isManagedCloudModel(session?.model) ? messageWithLogicalId(message) : message
642
+ }
643
+
518
644
  function resetSessionCompaction(session) {
519
645
  session.contextCompaction = null
520
646
  session.lastAutoCompactAt = null
@@ -532,6 +658,9 @@ function finishManualSessionRun(session, status, errorMessage) {
532
658
  }
533
659
 
534
660
  async function summarySession(session, initialUserMessage, summaryOptions) {
661
+ if (session.harness === AGENT_HARNESS_OPENCODE) {
662
+ throw Object.assign(new Error('OpenCode Harness does not support QuickForge summary derivation yet.'), { statusCode: 409 })
663
+ }
535
664
  if (session.agent.state.isStreaming) {
536
665
  session.agent.state.messages = [
537
666
  ...session.agent.state.messages,
@@ -625,8 +754,13 @@ async function summarySession(session, initialUserMessage, summaryOptions) {
625
754
  scope: session.scope,
626
755
  projectId: session.projectId,
627
756
  accessMode: session.accessMode,
757
+ harness: session.harness,
758
+ sourceHarnessSessionId: session.harness === AGENT_HARNESS_OPENCODE ? session.agent.harnessSessionId : null,
628
759
  yoloMode: session.yoloMode,
629
760
  model: session.model,
761
+ modelRef: session.modelRef,
762
+ modelAccessContext: session.modelAccessContext,
763
+ resolvePersistedModel: true,
630
764
  thinkingLevel: session.thinkingLevel,
631
765
  messages: compactedMessages,
632
766
  title: compactedTitle,
@@ -1081,7 +1215,7 @@ model:
1081
1215
  modelId: ${frontmatterString(spec.model.modelId)}${spec.model.api ? `
1082
1216
  api: ${frontmatterString(spec.model.api)}` : ''}${spec.model.baseUrl ? `
1083
1217
  baseUrl: ${frontmatterString(spec.model.baseUrl)}` : ''}` : ''}
1084
- max-runtime-ms: ${Math.max(1000, Math.min(Number(spec.maxRuntimeMs || SUBAGENT_DEFAULT_TIMEOUT_MS), 30 * 60 * 1000))}
1218
+ max-runtime-ms: ${Math.max(1000, Math.min(Number(spec.maxRuntimeMs || SUBAGENT_DEFAULT_TIMEOUT_MS), 60 * 60 * 1000))}
1085
1219
  max-tool-calls: ${Math.max(1, Math.min(Number(spec.maxToolCalls || 300), 300))}
1086
1220
  ---
1087
1221
  ${spec.systemPrompt}
@@ -1135,10 +1269,15 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
1135
1269
  throw new Error('No active model is configured for the parent session.')
1136
1270
  }
1137
1271
 
1138
- const timeoutMs = Math.max(1000, Math.min(Number(definition.maxRuntimeMs || SUBAGENT_DEFAULT_TIMEOUT_MS), 30 * 60 * 1000))
1272
+ const timeoutMs = Math.max(1000, Math.min(Number(definition.maxRuntimeMs || SUBAGENT_DEFAULT_TIMEOUT_MS), 60 * 60 * 1000))
1139
1273
  definition.capabilityPolicy ||= inferCapabilityPolicy(definition.allowedTools || [])
1140
1274
  definition.allowedTools = applyCapabilityPolicy(definition.allowedTools || [], definition.capabilityPolicy)
1141
- const { model: subagentModel, info: subagentModelInfo } = await resolveAgentProfileModel(definition, parentSession.model, readStore)
1275
+ const { model: subagentModel, info: subagentModelInfo } = await resolveAgentProfileModel(
1276
+ definition,
1277
+ parentSession.model,
1278
+ readStore,
1279
+ parentSession.modelAccessContext || {},
1280
+ )
1142
1281
  const subagentThinkingLevel = resolveAgentProfileThinkingLevel(definition, parentSession.thinkingLevel, subagentModel)
1143
1282
  const subagentSessionId = `${parentSession.sessionId}:subagent:${definition.name}:${randomUUID()}`
1144
1283
  const startedAt = Date.now()
@@ -1511,6 +1650,9 @@ export async function createAgent(sessionId, config = {}) {
1511
1650
  accessMode: rawAccessMode,
1512
1651
  yoloMode = false,
1513
1652
  model = null,
1653
+ modelRef = null,
1654
+ modelAccessContext = null,
1655
+ resolvePersistedModel = false,
1514
1656
  thinkingLevel = 'off',
1515
1657
  messages = [],
1516
1658
  systemPrompt = null,
@@ -1519,12 +1661,17 @@ export async function createAgent(sessionId, config = {}) {
1519
1661
  createdAt = new Date().toISOString(),
1520
1662
  lastModified = null,
1521
1663
  contextCompaction = null,
1664
+ harness: rawHarness,
1665
+ harnessSessionId = null,
1666
+ sourceHarnessSessionId = null,
1667
+ openCodeUsage = null,
1522
1668
  agentProfile = null,
1523
1669
  idleRetention = null,
1524
1670
  stateVersion = 0,
1525
1671
  } = config
1526
1672
  const accessMode = normalizeAccessMode(rawAccessMode, yoloMode)
1527
1673
  const resolvedYoloMode = yoloModeFromAccessMode(accessMode)
1674
+ const harness = normalizeAgentHarness(rawHarness)
1528
1675
 
1529
1676
  // Resolve project context for tool calls. Project conversations resolve to
1530
1677
  // their directory; global conversations (no projectId) and any fallback fall
@@ -1539,8 +1686,8 @@ export async function createAgent(sessionId, config = {}) {
1539
1686
  }
1540
1687
  projectContext ??= defaultGlobalWorkspaceContext()
1541
1688
 
1542
- // Build system prompt
1543
- const projectConfig = await readProjectConfig()
1689
+ // Build system prompt and tools only for the native QuickForge runtime.
1690
+ const projectConfig = harness === AGENT_HARNESS_QUICKFORGE ? await readProjectConfig() : { projects: [], globalSkills: [] }
1544
1691
  const configuredProject = projectId
1545
1692
  ? projectConfig.projects.find((project) => project.id === projectId)
1546
1693
  : null
@@ -1549,61 +1696,82 @@ export async function createAgent(sessionId, config = {}) {
1549
1696
  projectSkillNames: configuredProject?.skills,
1550
1697
  }
1551
1698
  const profileSystemPrompt = agentProfileSystemPrompt(agentProfile)
1552
- const resolvedSystemPrompt = systemPrompt ?? `${await buildSystemPrompt(projectId)}${profileSystemPrompt}`
1699
+ const resolvedSystemPrompt = harness === AGENT_HARNESS_QUICKFORGE
1700
+ ? systemPrompt ?? `${await buildSystemPrompt(projectId)}${profileSystemPrompt}`
1701
+ : ''
1702
+ if (harness === AGENT_HARNESS_OPENCODE && messages.length > 0 && !harnessSessionId && !sourceHarnessSessionId) {
1703
+ throw Object.assign(new Error('OpenCode history requires a persisted ACP session ID or an ACP fork source.'), { statusCode: 400 })
1704
+ }
1553
1705
 
1554
1706
  let resolvedAgentProfile = agentProfile
1555
1707
 
1556
- // Resolve model
1708
+ // OpenCode owns its login, model, tools and context. QuickForge runtime keeps
1709
+ // the existing model resolution path unchanged.
1557
1710
  let resolvedModel = model
1558
- if (!resolvedModel) {
1559
- // Try to load from storage
1711
+ let resolvedModelRef = modelRef
1712
+ if (!resolvedModel && harness === AGENT_HARNESS_QUICKFORGE) {
1713
+ // Try to load the active preference from storage. A stale/hidden implicit
1714
+ // preference may fall back, while an explicit session binding never does.
1560
1715
  try {
1561
1716
  const settings = await readStore('settings')
1562
1717
  const raw = settings?.['active-model']
1563
- if (raw) resolvedModel = typeof raw === 'string' ? JSON.parse(raw) : raw
1718
+ resolvedModel = await resolveImplicitModelPreference(raw, modelAccessContext || {})
1564
1719
  } catch {
1565
1720
  // ignore
1566
1721
  }
1567
1722
  }
1568
1723
  if (agentProfile?.model?.mode === 'fixed') {
1569
- resolvedModel = (await resolveAgentProfileModel(agentProfile, resolvedModel, readStore)).model
1724
+ const profileBinding = await resolveAgentProfileModel(agentProfile, resolvedModel, readStore, modelAccessContext || {})
1725
+ resolvedModel = profileBinding.model
1726
+ resolvedModelRef = profileBinding.modelRef || null
1570
1727
  resolvedAgentProfile = {
1571
1728
  ...agentProfile,
1572
1729
  model: modelReferenceSnapshot(agentProfile.model),
1573
1730
  }
1574
1731
  }
1575
1732
  if (resolvedAgentProfile && !resolvedModel) throw new Error('No active model is configured for the agent session.')
1733
+ const resolvedBinding = harness === AGENT_HARNESS_QUICKFORGE && resolvedModel
1734
+ ? (resolvedModelRef
1735
+ ? { model: resolvedModel, modelRef: resolvedModelRef }
1736
+ : model
1737
+ ? { model: resolvedModel, modelRef: null }
1738
+ : await modelBindingFromModel(resolvedModel))
1739
+ : { model: resolvedModel, modelRef: null }
1740
+ resolvedModel = resolvedBinding.model
1576
1741
  const resolvedThinkingLevel = agentProfile
1577
1742
  ? resolveAgentProfileThinkingLevel(agentProfile, thinkingLevel, resolvedModel)
1578
1743
  : thinkingLevel
1579
1744
 
1580
- // Build skills tools for enabled skills, plus workspace tools when a project is available.
1745
+ // Build native tools only for QuickForge. OpenCode uses its own tools and
1746
+ // must not receive QuickForge MCP, Skills, Memory or workspace definitions.
1581
1747
  const profileToolNames = Array.isArray(agentProfile?.allowedTools) ? agentProfile.allowedTools : null
1582
- const tools = await createServerTools(
1583
- projectId,
1584
- projectContext,
1585
- skillsContext,
1586
- !!projectContext,
1587
- (toolName) => {
1588
- if (profileToolNames && !profileToolNames.includes(toolName)) return `Agent profile ${agentProfile.name} is not allowed to use ${toolName}.`
1589
- const session = agentSessions.get(sessionId)
1590
- return session ? createCommandToolPermissions(session)(toolName) : null
1591
- },
1592
- agentProfile
1593
- ? {
1594
- allowedToolNames: profileToolNames,
1595
- includeSubagentTool: false,
1596
- includeMcpTools: false,
1597
- parentSessionId: sessionId,
1598
- sessionId,
1599
- scope,
1600
- }
1601
- : {
1602
- parentSessionId: sessionId,
1603
- sessionId,
1604
- scope,
1748
+ const tools = harness === AGENT_HARNESS_QUICKFORGE
1749
+ ? await createServerTools(
1750
+ projectId,
1751
+ projectContext,
1752
+ skillsContext,
1753
+ !!projectContext,
1754
+ (toolName) => {
1755
+ if (profileToolNames && !profileToolNames.includes(toolName)) return `Agent profile ${agentProfile.name} is not allowed to use ${toolName}.`
1756
+ const session = agentSessions.get(sessionId)
1757
+ return session ? createCommandToolPermissions(session)(toolName) : null
1605
1758
  },
1606
- )
1759
+ agentProfile
1760
+ ? {
1761
+ allowedToolNames: profileToolNames,
1762
+ includeSubagentTool: false,
1763
+ includeMcpTools: false,
1764
+ parentSessionId: sessionId,
1765
+ sessionId,
1766
+ scope,
1767
+ }
1768
+ : {
1769
+ parentSessionId: sessionId,
1770
+ sessionId,
1771
+ scope,
1772
+ },
1773
+ )
1774
+ : []
1607
1775
 
1608
1776
  // Resolve API key
1609
1777
  const getApiKey = async (provider) => {
@@ -1615,10 +1783,21 @@ export async function createAgent(sessionId, config = {}) {
1615
1783
  }
1616
1784
  }
1617
1785
 
1618
- const initialMemoryEnabled = await isGlobalMemoryEnabled()
1786
+ const initialMemoryEnabled = harness === AGENT_HARNESS_QUICKFORGE ? await isGlobalMemoryEnabled() : false
1619
1787
  const initialMemoryRevision = initialMemoryEnabled ? await getGlobalMemoryRevision() : null
1620
1788
  let session
1621
- const agent = new Agent({
1789
+ const agent = harness === AGENT_HARNESS_OPENCODE
1790
+ ? await createOpenCodeAcpAgent({
1791
+ sessionId,
1792
+ cwd: projectContext.workspaceRoot,
1793
+ messages,
1794
+ harnessSessionId,
1795
+ sourceHarnessSessionId,
1796
+ restoredUsage: openCodeUsage,
1797
+ logger,
1798
+ requestPermission: (request) => createAcpApprovalPromise(session, request),
1799
+ })
1800
+ : new Agent({
1622
1801
  initialState: {
1623
1802
  systemPrompt: resolvedSystemPrompt,
1624
1803
  model: resolvedModel,
@@ -1674,6 +1853,8 @@ export async function createAgent(sessionId, config = {}) {
1674
1853
  session = {
1675
1854
  sessionId,
1676
1855
  agent,
1856
+ harness,
1857
+ harnessSessionId: agent.harnessSessionId || null,
1677
1858
  projectContext,
1678
1859
  projectId,
1679
1860
  source: typeof source === 'string' && source.trim() ? source.trim() : null,
@@ -1682,6 +1863,9 @@ export async function createAgent(sessionId, config = {}) {
1682
1863
  accessMode,
1683
1864
  yoloMode: resolvedYoloMode,
1684
1865
  model: resolvedModel,
1866
+ modelRef: resolvedBinding.modelRef,
1867
+ modelAccessContext: modelAccessContext || null,
1868
+ resolvePersistedModel: resolvePersistedModel === true || Boolean(resolvedBinding.modelRef),
1685
1869
  thinkingLevel: resolvedThinkingLevel,
1686
1870
  scope,
1687
1871
  title,
@@ -1743,6 +1927,15 @@ export async function createAgent(sessionId, config = {}) {
1743
1927
  : undefined
1744
1928
  if (timedEvent.type === 'agent_end') {
1745
1929
  markLatestAssistantProcessFinished(agent.state.messages)
1930
+ // Ensure a failed run is visible in the conversation. Most failures emit
1931
+ // an assistant error message through the agent loop (handleRunFailure),
1932
+ // but a few paths (e.g. concurrent-run rejection) only set
1933
+ // `state.errorMessage`. Append an error message so the user sees the
1934
+ // reason at the end of the transcript instead of only a toast.
1935
+ const runError = session.agent.state.errorMessage
1936
+ if (runError) {
1937
+ agent.state.messages = appendAssistantErrorMessageOnce(agent.state.messages, runError, session.model)
1938
+ }
1746
1939
  }
1747
1940
  const forwardEvent = timedEvent.type === 'agent_end'
1748
1941
  ? {
@@ -1758,6 +1951,12 @@ export async function createAgent(sessionId, config = {}) {
1758
1951
  emitSessionEvent(session, forwardEvent)
1759
1952
  }
1760
1953
 
1954
+ // OpenCode runtime usage snapshots are authoritative and lightweight; the
1955
+ // debounced write keeps the latest usage durable without blocking the run.
1956
+ if (event.type === 'acp_session_usage_update') {
1957
+ scheduleSessionPersist(session)
1958
+ }
1959
+
1761
1960
  // Track status
1762
1961
  if (event.type === 'agent_start') {
1763
1962
  session.abortEndEmitted = false
@@ -1789,22 +1988,23 @@ export async function createAgent(sessionId, config = {}) {
1789
1988
  }
1790
1989
 
1791
1990
  if (event.type === 'message_end') {
1792
- const isInitialUserMessage = (event.message?.role === 'user' || event.message?.role === 'user-with-attachments')
1793
- && session.agent.state.messages.length === 1
1794
- if (isInitialUserMessage) {
1795
- // Persist the first user message before announcing the session or
1796
- // starting the asynchronous AI title request.
1991
+ const isUserMessage = event.message?.role === 'user' || event.message?.role === 'user-with-attachments'
1992
+ const isInitialUserMessage = isUserMessage && (event.isInitialUserMessage === true || session.agent.state.messages.length === 1)
1993
+ const requiresDurableCloudMessage = isUserMessage && isManagedCloudModel(session.model) && Boolean(logicalMessageId(event.message))
1994
+ if (isInitialUserMessage || requiresDurableCloudMessage) {
1995
+ // Persist every managed Cloud user message before the provider stream starts,
1996
+ // so its logical ID survives a process restart and resolves to the same private key.
1797
1997
  try {
1798
1998
  const metadata = await flushSessionPersist(session)
1799
- if (metadata) {
1999
+ if (metadata && isInitialUserMessage) {
1800
2000
  if (!session.sessionCreatedEmitted) {
1801
2001
  session.sessionCreatedEmitted = true
1802
2002
  emitSessionEvent(session, { type: 'session_created', metadata })
1803
2003
  }
1804
- scheduleSessionTitleGeneration(session, event.message)
2004
+ if (session.harness === AGENT_HARNESS_QUICKFORGE) scheduleSessionTitleGeneration(session, event.message)
1805
2005
  }
1806
2006
  } catch (err) {
1807
- logger.error(`Failed to persist initial user message for session ${sessionId}:`, err, { sessionId })
2007
+ logger.error(`Failed to persist user message for session ${sessionId}:`, err, { sessionId })
1808
2008
  }
1809
2009
  } else {
1810
2010
  // Debounced persist for crash recovery; coalesces the many message_end
@@ -1848,7 +2048,7 @@ function sessionLastModifiedFromMessages(messages, fallback) {
1848
2048
  * Persist session data to storage.
1849
2049
  */
1850
2050
  async function persistSessionUnlocked(session) {
1851
- const { sessionId, agent, scope, projectId, source, channelId, channelName, title, titleSource, createdAt, lastModified: storedLastModified, status, startedAt, finishedAt, model, thinkingLevel, accessMode, yoloMode, contextCompaction } = session
2051
+ const { sessionId, agent, harness, harnessSessionId, scope, projectId, source, channelId, channelName, title, titleSource, createdAt, lastModified: storedLastModified, status, startedAt, finishedAt, model, modelRef, thinkingLevel, accessMode, yoloMode, contextCompaction } = session
1852
2052
  const messages = agent.state.messages
1853
2053
 
1854
2054
  if (messages.length === 0) {
@@ -1870,7 +2070,11 @@ async function persistSessionUnlocked(session) {
1870
2070
  id: sessionId,
1871
2071
  title,
1872
2072
  titleSource,
2073
+ harness,
2074
+ harnessSessionId: agent.harnessSessionId || harnessSessionId || undefined,
2075
+ openCodeUsage: harness === AGENT_HARNESS_OPENCODE && agent.state.acpSession?.usage ? agent.state.acpSession.usage : undefined,
1873
2076
  model,
2077
+ modelRef: modelRef || undefined,
1874
2078
  thinkingLevel,
1875
2079
  accessMode,
1876
2080
  yoloMode,
@@ -1930,6 +2134,8 @@ async function persistSessionUnlocked(session) {
1930
2134
  messageCount: messages.length,
1931
2135
  usage,
1932
2136
  thinkingLevel,
2137
+ harness,
2138
+ harnessSessionId: agent.harnessSessionId || harnessSessionId || undefined,
1933
2139
  accessMode,
1934
2140
  yoloMode,
1935
2141
  preview,
@@ -2063,7 +2269,13 @@ export async function rollbackSessionMessages(sessionId, rollbackMessageIndex) {
2063
2269
  throw Object.assign(new Error('Session not found'), { statusCode: 404 })
2064
2270
  }
2065
2271
  if (session.agent.state.isStreaming) {
2066
- throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes before rolling back.'), { statusCode: 409 })
2272
+ throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes before rolling back.'), {
2273
+ statusCode: 409,
2274
+ errorCode: 'GENERATION_STILL_RUNNING_BEFORE_ROLLBACK',
2275
+ })
2276
+ }
2277
+ if (session.harness === AGENT_HARNESS_OPENCODE) {
2278
+ throw Object.assign(new Error('OpenCode does not support rollback from a message position. This action is unavailable for OpenCode conversations.'), { statusCode: 409 })
2067
2279
  }
2068
2280
 
2069
2281
  const messages = Array.isArray(session.agent.state.messages) ? session.agent.state.messages : []
@@ -2074,7 +2286,14 @@ export async function rollbackSessionMessages(sessionId, rollbackMessageIndex) {
2074
2286
 
2075
2287
  const nextMessages = messages.slice(0, rollbackIndex)
2076
2288
  updateSessionMessages(session, nextMessages)
2077
- resetSessionCompaction(session)
2289
+ const compactedUpToIndex = Number(session.contextCompaction?.compactedUpToIndex) || 0
2290
+ if (rollbackIndex < compactedUpToIndex) {
2291
+ // 撤回越过压缩点,摘要覆盖的历史被截断,压缩失效
2292
+ resetSessionCompaction(session)
2293
+ } else {
2294
+ // 撤回发生在压缩点之后,摘要仍然有效,保留压缩上下文
2295
+ session.lastTransformedContextMessages = null
2296
+ }
2078
2297
  session.status = 'idle'
2079
2298
  session.finishedAt = new Date().toISOString()
2080
2299
  await persistSession(session)
@@ -2127,29 +2346,59 @@ function scheduleSessionTitleGeneration(session, userMessage) {
2127
2346
  session.titleGenerationPromiseId = generationId
2128
2347
  }
2129
2348
 
2349
+ async function refreshSessionModelBinding(session) {
2350
+ if (!session?.model || (!session.modelRef && session.resolvePersistedModel !== true)) return session
2351
+ const binding = await resolveModelBinding(
2352
+ session.modelRef ? { modelRef: session.modelRef } : { model: session.model },
2353
+ {
2354
+ context: session.modelAccessContext || {},
2355
+ currentModel: session.model,
2356
+ allowCurrentHidden: true,
2357
+ forExecution: true,
2358
+ legacySnapshot: session.model,
2359
+ },
2360
+ )
2361
+ session.model = binding.model
2362
+ session.modelRef = binding.modelRef
2363
+ session.resolvePersistedModel = true
2364
+ session.agent.state.model = binding.model
2365
+ return session
2366
+ }
2367
+
2130
2368
  /**
2131
2369
  * Send a user message to the agent and start the agent loop.
2132
2370
  * Returns immediately; events are streamed via the event bus.
2133
2371
  */
2134
- export async function runPrompt(sessionId, message, selectedCapabilities = [], promptCommand = null, transientContextPrompt = null) {
2372
+ export async function runPrompt(sessionId, message, selectedCapabilities = [], promptCommand = null, transientContextPrompt = null, modelAccessContext = null) {
2135
2373
  let session = await syncSessionFromStorage(sessionId)
2136
2374
  if (!session) {
2137
2375
  throw Object.assign(new Error('Session not found'), { statusCode: 404 })
2138
2376
  }
2377
+ if (modelAccessContext) session.modelAccessContext = modelAccessContext
2378
+ if (session.harness === AGENT_HARNESS_QUICKFORGE) await refreshSessionModelBinding(session)
2139
2379
 
2140
2380
  if (session.agent.state.isStreaming || session.abortPending) {
2141
- throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes.'), { statusCode: 409 })
2381
+ throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes.'), {
2382
+ statusCode: 409,
2383
+ errorCode: 'GENERATION_ALREADY_RUNNING',
2384
+ })
2142
2385
  }
2143
2386
 
2144
- await refreshMemoryState(session)
2387
+ if (session.harness === AGENT_HARNESS_QUICKFORGE) await refreshMemoryState(session)
2145
2388
  resetIdleTimer(session)
2146
2389
 
2147
2390
  // Build user message
2148
2391
  const initialUserMessage = typeof message === 'string'
2149
2392
  ? { role: 'user', content: message, timestamp: new Date().toISOString() }
2150
2393
  : message
2151
- const commandState = await resolveCommandState(session, initialUserMessage, promptCommand)
2152
- const userMessage = commandState.userMessage ?? initialUserMessage
2394
+ const commandState = session.harness === AGENT_HARNESS_QUICKFORGE
2395
+ ? await resolveCommandState(session, initialUserMessage, promptCommand)
2396
+ : { userMessage: initialUserMessage }
2397
+ const resolvedUserMessage = commandState.userMessage ?? initialUserMessage
2398
+
2399
+ if (session.harness === AGENT_HARNESS_OPENCODE) {
2400
+ session.agent.validatePrompt(resolvedUserMessage)
2401
+ }
2153
2402
 
2154
2403
  if (commandState.textResponse) {
2155
2404
  session.agent.state.messages = [
@@ -2176,6 +2425,10 @@ export async function runPrompt(sessionId, message, selectedCapabilities = [], p
2176
2425
  return compactSession(session, initialUserMessage, commandState.compact)
2177
2426
  }
2178
2427
 
2428
+ const userMessage = session.harness === AGENT_HARNESS_QUICKFORGE
2429
+ ? prepareCloudUserMessage(session, resolvedUserMessage)
2430
+ : resolvedUserMessage
2431
+
2179
2432
  // Set a meaningful fallback immediately. The AI title request starts only
2180
2433
  // after the first user message has been persisted by the message_end handler.
2181
2434
  if (session.titleSource === 'default' && session.title === 'New chat') {
@@ -2197,11 +2450,26 @@ export async function runPrompt(sessionId, message, selectedCapabilities = [], p
2197
2450
  // Fire and forget — events come through eventBus
2198
2451
  session.agent.prompt(userMessage).catch((err) => {
2199
2452
  logger.error(`Agent prompt error for session ${sessionId}:`, err, { sessionId })
2200
- const event = {
2201
- type: 'error',
2202
- error: err.message || 'Unknown error',
2203
- }
2204
- emitSessionEvent(session, event)
2453
+ if (session.harness === AGENT_HARNESS_OPENCODE) return
2454
+ const errorMessage = err.message || 'Unknown error'
2455
+ // Surface the failure at the end of the conversation itself so the user
2456
+ // sees the reason in the transcript, not only via toast/notification.
2457
+ session.agent.state.messages = appendAssistantErrorMessageOnce(
2458
+ session.agent.state.messages,
2459
+ errorMessage,
2460
+ session.model,
2461
+ )
2462
+ session.agent.state.errorMessage = errorMessage
2463
+ session.agent.state.isStreaming = false
2464
+ session.status = 'error'
2465
+ session.finishedAt = new Date().toISOString()
2466
+ const messages = session.agent.state.messages
2467
+ emitSessionEvent(session, { type: 'message_end', messages })
2468
+ emitSessionEvent(session, { type: 'error', error: errorMessage })
2469
+ emitSessionEvent(session, { type: 'agent_end', messages, errorMessage, status: 'error' })
2470
+ flushSessionPersist(session).catch((persistErr) =>
2471
+ logger.error(`Failed to persist session ${sessionId} after prompt error:`, persistErr, { sessionId }),
2472
+ )
2205
2473
  }).finally(() => {
2206
2474
  session.activeCommandName = null
2207
2475
  session.activeCommandPermissions = null
@@ -2221,14 +2489,22 @@ export async function runPrompt(sessionId, message, selectedCapabilities = [], p
2221
2489
  * Trims messages to keep up to and including the last user message,
2222
2490
  * removing the assistant response that follows it.
2223
2491
  */
2224
- export async function continueSession(sessionId) {
2492
+ export async function continueSession(sessionId, modelAccessContext = null) {
2225
2493
  const session = agentSessions.get(sessionId)
2226
2494
  if (!session) {
2227
2495
  throw Object.assign(new Error('Session not found'), { statusCode: 404 })
2228
2496
  }
2229
2497
  if (session.agent.state.isStreaming) {
2230
- throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes.'), { statusCode: 409 })
2498
+ throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes.'), {
2499
+ statusCode: 409,
2500
+ errorCode: 'GENERATION_ALREADY_RUNNING',
2501
+ })
2502
+ }
2503
+ if (session.harness === AGENT_HARNESS_OPENCODE) {
2504
+ throw Object.assign(new Error('OpenCode does not support retry from a message position. This action is unavailable for OpenCode conversations.'), { statusCode: 409 })
2231
2505
  }
2506
+ if (modelAccessContext) session.modelAccessContext = modelAccessContext
2507
+ await refreshSessionModelBinding(session)
2232
2508
 
2233
2509
  const messages = Array.isArray(session.agent.state.messages) ? session.agent.state.messages : []
2234
2510
 
@@ -2246,10 +2522,20 @@ export async function continueSession(sessionId) {
2246
2522
 
2247
2523
  const lastUserMessage = messages[lastUserIndex]
2248
2524
  const commandState = await resolveCommandState(session, lastUserMessage)
2249
- const continuedUserMessage = commandState.userMessage ?? lastUserMessage
2525
+ const continuedUserMessage = prepareCloudUserMessage(session, commandState.userMessage ?? lastUserMessage)
2250
2526
  const trimmedMessages = messages.slice(0, lastUserIndex).concat(continuedUserMessage)
2251
2527
  updateSessionMessages(session, trimmedMessages)
2252
- resetSessionCompaction(session)
2528
+ const compactedUpToIndex = Number(session.contextCompaction?.compactedUpToIndex) || 0
2529
+ if (lastUserIndex < compactedUpToIndex) {
2530
+ // 重试点越过压缩点,摘要覆盖的历史被截断,压缩失效
2531
+ resetSessionCompaction(session)
2532
+ } else {
2533
+ // 重试点位于压缩点之后,摘要仍然有效,保留压缩上下文
2534
+ session.lastTransformedContextMessages = null
2535
+ }
2536
+ if (isManagedCloudModel(session.model) && logicalMessageId(continuedUserMessage)) {
2537
+ await flushSessionPersist(session)
2538
+ }
2253
2539
 
2254
2540
  resetIdleTimer(session)
2255
2541
  session.activeCommandName = commandState.commandName ?? null
@@ -2334,9 +2620,9 @@ export function steerAgent(sessionId, message) {
2334
2620
  throw Object.assign(new Error('Session not found'), { statusCode: 404 })
2335
2621
  }
2336
2622
 
2337
- const agentMessage = typeof message === 'string'
2623
+ const agentMessage = prepareCloudUserMessage(session, typeof message === 'string'
2338
2624
  ? { role: 'user', content: message, timestamp: Date.now() }
2339
- : message
2625
+ : message)
2340
2626
 
2341
2627
  session.agent.steer(agentMessage)
2342
2628
  return { sessionId, steered: true }
@@ -2351,9 +2637,9 @@ export function followUpAgent(sessionId, message) {
2351
2637
  throw Object.assign(new Error('Session not found'), { statusCode: 404 })
2352
2638
  }
2353
2639
 
2354
- const agentMessage = typeof message === 'string'
2640
+ const agentMessage = prepareCloudUserMessage(session, typeof message === 'string'
2355
2641
  ? { role: 'user', content: message, timestamp: Date.now() }
2356
- : message
2642
+ : message)
2357
2643
 
2358
2644
  session.agent.followUp(agentMessage)
2359
2645
  return { sessionId, followUp: true }
@@ -2439,10 +2725,13 @@ export function getSessionState(sessionId) {
2439
2725
  source: session.source || undefined,
2440
2726
  channelId: session.channelId || undefined,
2441
2727
  channelName: session.channelName || undefined,
2728
+ harness: session.harness,
2729
+ harnessSessionId: session.agent.harnessSessionId || session.harnessSessionId || undefined,
2442
2730
  accessMode: session.accessMode,
2443
2731
  yoloMode: session.yoloMode,
2444
2732
  systemPrompt: session.agent.state.systemPrompt,
2445
2733
  model: session.model,
2734
+ modelRef: session.modelRef || undefined,
2446
2735
  thinkingLevel: session.thinkingLevel,
2447
2736
  title: session.title,
2448
2737
  titleSource: session.titleSource,
@@ -2459,6 +2748,7 @@ export function getSessionState(sessionId) {
2459
2748
  contextUsage: getSessionContextUsage(session),
2460
2749
  pendingToolApproval: getPendingApprovalForSession(session.sessionId),
2461
2750
  pendingAutoCompactApproval: getPendingAutoCompactApprovalForSession(session.sessionId),
2751
+ acpSession: session.harness === AGENT_HARNESS_OPENCODE ? session.agent.state.acpSession : undefined,
2462
2752
  isStreaming: session.abortPending ? false : session.agent.state.isStreaming,
2463
2753
  errorMessage: session.agent.state.errorMessage,
2464
2754
  }
@@ -2477,6 +2767,8 @@ export function getSessionStatus(sessionId) {
2477
2767
  sessionId: session.sessionId,
2478
2768
  scope: session.scope,
2479
2769
  projectId: session.projectId,
2770
+ harness: session.harness,
2771
+ harnessSessionId: session.agent.harnessSessionId || session.harnessSessionId || undefined,
2480
2772
  source: session.source || undefined,
2481
2773
  channelId: session.channelId || undefined,
2482
2774
  channelName: session.channelName || undefined,
@@ -2548,6 +2840,7 @@ export async function destroyAgent(sessionId) {
2548
2840
 
2549
2841
  try {
2550
2842
  session.agent.abort()
2843
+ if (session.harness === AGENT_HARNESS_OPENCODE) await session.agent.dispose?.()
2551
2844
  } catch {
2552
2845
  // ignore
2553
2846
  }
@@ -2597,6 +2890,8 @@ export async function restoreAgent(sessionId) {
2597
2890
  accessMode: normalizeAccessMode(sessionData.accessMode, sessionData.yoloMode),
2598
2891
  yoloMode: sessionData.yoloMode || false,
2599
2892
  model: sessionData.model,
2893
+ modelRef: sessionData.modelRef || null,
2894
+ resolvePersistedModel: true,
2600
2895
  thinkingLevel: sessionData.thinkingLevel || 'off',
2601
2896
  messages: sessionData.messages || [],
2602
2897
  title: sessionData.title || 'New chat',
@@ -2604,11 +2899,15 @@ export async function restoreAgent(sessionId) {
2604
2899
  createdAt: sessionData.createdAt,
2605
2900
  lastModified: sessionData.lastModified,
2606
2901
  contextCompaction: sessionData.contextCompaction || null,
2902
+ harness: normalizeAgentHarness(sessionData.harness),
2903
+ harnessSessionId: typeof sessionData.harnessSessionId === 'string' ? sessionData.harnessSessionId : null,
2904
+ openCodeUsage: sessionData.openCodeUsage || null,
2607
2905
  idleRetention: sessionData.idleRetention || null,
2608
2906
  stateVersion: sessionData.stateVersion,
2609
2907
  })
2610
2908
  } catch (err) {
2611
2909
  logger.error(`Failed to restore agent ${sessionId}:`, err, { sessionId })
2910
+ if (err?.statusCode === 503 || err?.errorCode === 'OPENCODE_UNAVAILABLE') throw err
2612
2911
  return null
2613
2912
  }
2614
2913
  }
@@ -2678,6 +2977,8 @@ export function listSessions() {
2678
2977
  scope: session.scope,
2679
2978
  status: session.status,
2680
2979
  title: session.title,
2980
+ harness: session.harness,
2981
+ harnessSessionId: session.agent.harnessSessionId || session.harnessSessionId || undefined,
2681
2982
  source: session.source || undefined,
2682
2983
  channelId: session.channelId || undefined,
2683
2984
  channelName: session.channelName || undefined,
@@ -2733,7 +3034,7 @@ export async function updateSessionAccessMode(sessionId, accessMode) {
2733
3034
 
2734
3035
  session.accessMode = normalizeAccessMode(accessMode, session.accessMode)
2735
3036
  session.yoloMode = yoloModeFromAccessMode(session.accessMode)
2736
- await rebuildSessionTools(session)
3037
+ if (session.harness === AGENT_HARNESS_QUICKFORGE) await rebuildSessionTools(session)
2737
3038
  await persistSession(session)
2738
3039
 
2739
3040
  const state = getSessionState(sessionId)
@@ -2746,25 +3047,124 @@ export async function updateSessionYoloMode(sessionId, yoloMode) {
2746
3047
  return updateSessionAccessMode(sessionId, yoloMode ? AGENT_ACCESS_MODE_FULL_ACCESS : AGENT_ACCESS_MODE_DEFAULT)
2747
3048
  }
2748
3049
 
3050
+ function requireOpenCodeHarnessSession(sessionId) {
3051
+ const session = agentSessions.get(sessionId)
3052
+ if (!session) throw Object.assign(new Error('Session not found'), { statusCode: 404 })
3053
+ if (session.harness !== AGENT_HARNESS_OPENCODE) {
3054
+ throw Object.assign(new Error('Harness configuration is only available for OpenCode sessions.'), { statusCode: 409 })
3055
+ }
3056
+ return session
3057
+ }
3058
+
3059
+ export async function updateSessionHarnessConfigOption(sessionId, configId, value) {
3060
+ const session = requireOpenCodeHarnessSession(sessionId)
3061
+ await session.agent.setConfigOption(configId, value)
3062
+ const state = getSessionState(sessionId)
3063
+ emitSessionEvent(session, { type: 'state', ...state })
3064
+ return { sessionId, acpSession: state.acpSession }
3065
+ }
3066
+
3067
+ export async function updateSessionHarnessMode(sessionId, modeId) {
3068
+ const session = requireOpenCodeHarnessSession(sessionId)
3069
+ await session.agent.setMode(modeId)
3070
+ const state = getSessionState(sessionId)
3071
+ emitSessionEvent(session, { type: 'state', ...state })
3072
+ return { sessionId, acpSession: state.acpSession }
3073
+ }
3074
+
3075
+ /**
3076
+ * Fork the entire current OpenCode session into a new QuickForge session.
3077
+ *
3078
+ * OpenCode ACP only supports whole-session `session/fork` (no message-position
3079
+ * fork), so the new agent is created with the full message history and
3080
+ * `sourceHarnessSessionId` pointing at the current ACP session. The new session
3081
+ * is persisted immediately and announced through the existing `session_forked`
3082
+ * event so clients switch to it without a message-level fork semantic.
3083
+ */
3084
+ export async function forkSession(sessionId) {
3085
+ const session = requireOpenCodeHarnessSession(sessionId)
3086
+ if (session.agent.state.isStreaming) {
3087
+ throw Object.assign(new Error('Generation is still running. Stop it or wait until it finishes before forking the conversation.'), {
3088
+ statusCode: 409,
3089
+ errorCode: 'GENERATION_STILL_RUNNING_BEFORE_FORK',
3090
+ })
3091
+ }
3092
+ const sourceHarnessSessionId = session.agent.harnessSessionId
3093
+ if (!sourceHarnessSessionId) {
3094
+ throw Object.assign(new Error('This OpenCode conversation has no ACP session to fork.'), { statusCode: 409 })
3095
+ }
3096
+ const messages = session.agent.state.messages
3097
+ if (messages.length === 0) {
3098
+ throw Object.assign(new Error('There is no conversation to fork yet.'), { statusCode: 400 })
3099
+ }
3100
+ const forkedSessionId = randomUUID()
3101
+ const forkedSession = await createAgent(forkedSessionId, {
3102
+ scope: session.scope,
3103
+ projectId: session.projectId,
3104
+ accessMode: session.accessMode,
3105
+ yoloMode: session.yoloMode,
3106
+ model: session.model,
3107
+ modelRef: session.modelRef,
3108
+ modelAccessContext: session.modelAccessContext,
3109
+ resolvePersistedModel: true,
3110
+ thinkingLevel: session.thinkingLevel,
3111
+ messages,
3112
+ title: session.title,
3113
+ titleSource: session.titleSource === 'default' ? 'manual' : session.titleSource,
3114
+ createdAt: new Date().toISOString(),
3115
+ harness: session.harness,
3116
+ sourceHarnessSessionId,
3117
+ idleRetention: session.idleRetention || null,
3118
+ })
3119
+ updateSessionMessages(forkedSession, messages)
3120
+ await persistSession(forkedSession)
3121
+
3122
+ emitSessionEvent(session, {
3123
+ type: 'session_forked',
3124
+ sourceSessionId: session.sessionId,
3125
+ targetSessionId: forkedSessionId,
3126
+ title: forkedSession.title,
3127
+ createdAt: forkedSession.createdAt,
3128
+ scope: forkedSession.scope,
3129
+ projectId: forkedSession.projectId,
3130
+ messages: forkedSession.agent.state.messages,
3131
+ })
3132
+ emitSessionEvent(forkedSession, { type: 'state', ...getSessionState(forkedSessionId) })
3133
+ emitSessionEvent(forkedSession, { type: 'message_end', messages: forkedSession.agent.state.messages })
3134
+ emitSessionEvent(forkedSession, { type: 'agent_end', messages: forkedSession.agent.state.messages })
3135
+ return {
3136
+ sessionId: forkedSessionId,
3137
+ title: forkedSession.title,
3138
+ createdAt: forkedSession.createdAt,
3139
+ scope: forkedSession.scope,
3140
+ projectId: forkedSession.projectId,
3141
+ }
3142
+ }
3143
+
2749
3144
  /**
2750
3145
  * Update the model for an existing session.
2751
3146
  * Syncs the model to both the session record (for persistence) and the agent state (for API calls).
2752
3147
  * Does NOT force persistence — normal lifecycle events (message_end, agent_end) will persist
2753
3148
  * the updated model.
2754
3149
  */
2755
- export function updateSessionModel(sessionId, model) {
3150
+ export function updateSessionModel(sessionId, model, modelRef = null) {
2756
3151
  const session = agentSessions.get(sessionId)
2757
3152
  if (!session) {
2758
3153
  throw Object.assign(new Error('Session not found'), { statusCode: 404 })
2759
3154
  }
3155
+ if (session.harness === AGENT_HARNESS_OPENCODE) {
3156
+ throw Object.assign(new Error('OpenCode manages its model natively.'), { statusCode: 409 })
3157
+ }
2760
3158
  if (!model) {
2761
3159
  throw Object.assign(new Error('Missing model'), { statusCode: 400 })
2762
3160
  }
2763
3161
 
2764
3162
  session.model = model
3163
+ session.modelRef = modelRef
3164
+ session.resolvePersistedModel = true
2765
3165
  session.agent.state.model = model
2766
3166
 
2767
- return { sessionId, model }
3167
+ return { sessionId, model, modelRef: modelRef || undefined }
2768
3168
  }
2769
3169
 
2770
3170
  /**
@@ -2775,6 +3175,9 @@ export function updateSessionThinkingLevel(sessionId, thinkingLevel) {
2775
3175
  if (!session) {
2776
3176
  throw Object.assign(new Error('Session not found'), { statusCode: 404 })
2777
3177
  }
3178
+ if (session.harness === AGENT_HARNESS_OPENCODE) {
3179
+ throw Object.assign(new Error('OpenCode manages its thinking level natively.'), { statusCode: 409 })
3180
+ }
2778
3181
  if (!thinkingLevel) {
2779
3182
  throw Object.assign(new Error('Missing thinkingLevel'), { statusCode: 400 })
2780
3183
  }