@xiaobuyu/nodesign 0.0.1

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 (372) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +140 -0
  3. package/bin/nodesign.js +176 -0
  4. package/package.json +104 -0
  5. package/server/api/README.md +45 -0
  6. package/server/api/_guard.js +75 -0
  7. package/server/api/admin.js +187 -0
  8. package/server/api/assets/docx-page.js +103 -0
  9. package/server/api/assets/helpers.js +22 -0
  10. package/server/api/assets/notes.js +144 -0
  11. package/server/api/assets.js +896 -0
  12. package/server/api/board.js +57 -0
  13. package/server/api/browse.js +138 -0
  14. package/server/api/canvas.js +444 -0
  15. package/server/api/chatai.js +324 -0
  16. package/server/api/exports/build-standalone.js +980 -0
  17. package/server/api/exports/cards.js +118 -0
  18. package/server/api/exports/docx-pdf.js +46 -0
  19. package/server/api/exports/export-page.js +142 -0
  20. package/server/api/exports/handoff.js +147 -0
  21. package/server/api/exports.js +690 -0
  22. package/server/api/instruction.js +75 -0
  23. package/server/api/local.js +115 -0
  24. package/server/api/me.js +116 -0
  25. package/server/api/memory.js +185 -0
  26. package/server/api/pending-changes.js +366 -0
  27. package/server/api/plugins.js +164 -0
  28. package/server/api/projects.js +220 -0
  29. package/server/api/publish.js +64 -0
  30. package/server/api/recent.js +73 -0
  31. package/server/api/sessions.js +589 -0
  32. package/server/api/skills.js +77 -0
  33. package/server/api/standalone-fit.js +168 -0
  34. package/server/api/turn-compose.js +180 -0
  35. package/server/api/turn-inflight.js +44 -0
  36. package/server/api/turn.js +669 -0
  37. package/server/auth/README.md +17 -0
  38. package/server/auth/middleware.js +138 -0
  39. package/server/auth/origin-guard.js +71 -0
  40. package/server/auth/session.js +108 -0
  41. package/server/auth/tier.js +113 -0
  42. package/server/auth/users-store.js +348 -0
  43. package/server/edit/README.md +29 -0
  44. package/server/engine/README.md +113 -0
  45. package/server/engine/agent/agent-shared.js +550 -0
  46. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -0
  47. package/server/engine/agent/auto-mode-rules.js +78 -0
  48. package/server/engine/agent/context.js +418 -0
  49. package/server/engine/agent/events.js +464 -0
  50. package/server/engine/agent/hooks/canvas-validate.js +199 -0
  51. package/server/engine/agent/hooks/failure.js +120 -0
  52. package/server/engine/agent/hooks/file-events.js +75 -0
  53. package/server/engine/agent/hooks/lifecycle.js +233 -0
  54. package/server/engine/agent/hooks/post-canvas-focus.js +80 -0
  55. package/server/engine/agent/hooks/post-guidance.js +206 -0
  56. package/server/engine/agent/hooks/post-subagent-report.js +104 -0
  57. package/server/engine/agent/hooks/post-trim.js +58 -0
  58. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -0
  59. package/server/engine/agent/hooks/pre-defaults.js +84 -0
  60. package/server/engine/agent/hooks/pre-injectors.js +277 -0
  61. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -0
  62. package/server/engine/agent/hooks/pre-starter-files.js +61 -0
  63. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -0
  64. package/server/engine/agent/hooks/site-validate.js +94 -0
  65. package/server/engine/agent/hooks/tool-prompts.js +31 -0
  66. package/server/engine/agent/hooks/turn-state-memory.js +56 -0
  67. package/server/engine/agent/hooks/user-prompt-submit.js +215 -0
  68. package/server/engine/agent/hooks.js +354 -0
  69. package/server/engine/agent/init-contract.js +69 -0
  70. package/server/engine/agent/isolation.js +159 -0
  71. package/server/engine/agent/model-context.js +359 -0
  72. package/server/engine/agent/model-table.js +292 -0
  73. package/server/engine/agent/plugin-loader.js +266 -0
  74. package/server/engine/agent/prompts/nodesign-prelude.md +336 -0
  75. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -0
  76. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -0
  77. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -0
  78. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -0
  79. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -0
  80. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -0
  81. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -0
  82. package/server/engine/agent/prompts/tools/site-reference.md +106 -0
  83. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -0
  84. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -0
  85. package/server/engine/agent/session-loop.js +1024 -0
  86. package/server/engine/agent/session-model.js +164 -0
  87. package/server/engine/agent/skill.js +204 -0
  88. package/server/engine/agent/system-prompts.js +86 -0
  89. package/server/engine/agent/task-events.js +78 -0
  90. package/server/engine/agents/ds-extractor.md +132 -0
  91. package/server/engine/agents/explorer.md +190 -0
  92. package/server/engine/agents/index.js +230 -0
  93. package/server/engine/agents/schemas/design-system.json +156 -0
  94. package/server/engine/agents/schemas/tweak-schema.json +113 -0
  95. package/server/engine/agents/tweak-proposer.md +127 -0
  96. package/server/engine/agents/vision-checker.md +254 -0
  97. package/server/engine/browse/capture.js +422 -0
  98. package/server/engine/browse/card.js +137 -0
  99. package/server/engine/browse/handover.js +59 -0
  100. package/server/engine/browse/page-digest.js +119 -0
  101. package/server/engine/browse/refs.js +196 -0
  102. package/server/engine/browse/registry.js +307 -0
  103. package/server/engine/browse/screencast.js +170 -0
  104. package/server/engine/browse/state.js +121 -0
  105. package/server/engine/chatai/chat-log.js +79 -0
  106. package/server/engine/chatai/index.js +193 -0
  107. package/server/engine/chatai/openai-compat.js +152 -0
  108. package/server/engine/chatai/orchestrate.js +309 -0
  109. package/server/engine/chatai/perform.js +40 -0
  110. package/server/engine/chatai/summarize.js +79 -0
  111. package/server/engine/mcp/capability-gate.js +52 -0
  112. package/server/engine/mcp/index.js +339 -0
  113. package/server/engine/mcp/param-sanitizer.js +142 -0
  114. package/server/engine/mcp/tools/arrange-on-board.js +124 -0
  115. package/server/engine/mcp/tools/artifact-session.js +278 -0
  116. package/server/engine/mcp/tools/browse-computer.js +421 -0
  117. package/server/engine/mcp/tools/browse-find-batch.js +183 -0
  118. package/server/engine/mcp/tools/browse-screenshot.js +157 -0
  119. package/server/engine/mcp/tools/browse.js +480 -0
  120. package/server/engine/mcp/tools/build-docx.js +101 -0
  121. package/server/engine/mcp/tools/clear-pending-changes.js +99 -0
  122. package/server/engine/mcp/tools/create-on-board.js +155 -0
  123. package/server/engine/mcp/tools/crystallize-skill.js +168 -0
  124. package/server/engine/mcp/tools/deliver-files.js +0 -0
  125. package/server/engine/mcp/tools/explain-style.js +237 -0
  126. package/server/engine/mcp/tools/export-handoff.js +133 -0
  127. package/server/engine/mcp/tools/expose-tweaks.js +149 -0
  128. package/server/engine/mcp/tools/generate-image.js +842 -0
  129. package/server/engine/mcp/tools/get-computed-styles.js +164 -0
  130. package/server/engine/mcp/tools/get-pending-changes.js +205 -0
  131. package/server/engine/mcp/tools/h3box-ssh.js +84 -0
  132. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -0
  133. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -0
  134. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -0
  135. package/server/engine/mcp/tools/helpers/perception-page.js +215 -0
  136. package/server/engine/mcp/tools/helpers/reference-download.js +103 -0
  137. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -0
  138. package/server/engine/mcp/tools/helpers/rembg.js +273 -0
  139. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -0
  140. package/server/engine/mcp/tools/highlight.js +68 -0
  141. package/server/engine/mcp/tools/list-pages.js +222 -0
  142. package/server/engine/mcp/tools/lookup-tags.js +59 -0
  143. package/server/engine/mcp/tools/navigate-to-page.js +60 -0
  144. package/server/engine/mcp/tools/organize-board.js +64 -0
  145. package/server/engine/mcp/tools/paint-still.js +434 -0
  146. package/server/engine/mcp/tools/pin-to-board.js +130 -0
  147. package/server/engine/mcp/tools/preview-deck.js +130 -0
  148. package/server/engine/mcp/tools/profile-scroll.js +305 -0
  149. package/server/engine/mcp/tools/publish-site.js +102 -0
  150. package/server/engine/mcp/tools/query-elements.js +195 -0
  151. package/server/engine/mcp/tools/read-board.js +103 -0
  152. package/server/engine/mcp/tools/read-document.js +93 -0
  153. package/server/engine/mcp/tools/read-page.js +264 -0
  154. package/server/engine/mcp/tools/read-tavern-json.js +143 -0
  155. package/server/engine/mcp/tools/record-decision.js +140 -0
  156. package/server/engine/mcp/tools/relate-on-board.js +110 -0
  157. package/server/engine/mcp/tools/remove-background.js +391 -0
  158. package/server/engine/mcp/tools/report-issue.js +115 -0
  159. package/server/engine/mcp/tools/roll-film.js +266 -0
  160. package/server/engine/mcp/tools/screenshot-docx.js +138 -0
  161. package/server/engine/mcp/tools/screenshot-url.js +195 -0
  162. package/server/engine/mcp/tools/screenshot.js +583 -0
  163. package/server/engine/mcp/tools/tier-gate.js +96 -0
  164. package/server/engine/mcp/tools/trace-motion.js +215 -0
  165. package/server/engine/mcp/tools/web-search.js +548 -0
  166. package/server/engine/motion/inventory.js +349 -0
  167. package/server/engine/perception/session.js +235 -0
  168. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -0
  169. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -0
  170. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -0
  171. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -0
  172. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -0
  173. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -0
  174. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -0
  175. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -0
  176. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -0
  177. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -0
  178. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -0
  179. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -0
  180. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -0
  181. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -0
  182. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -0
  183. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -0
  184. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -0
  185. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -0
  186. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -0
  187. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -0
  188. package/server/engine/runs/active-runs.js +698 -0
  189. package/server/engine/runs/live-turn.js +252 -0
  190. package/server/engine/runs/store.js +364 -0
  191. package/server/engine/runs/turn-relay.js +217 -0
  192. package/server/engine/runtime/workspace.js +164 -0
  193. package/server/index.js +235 -0
  194. package/server/lib/artifact-file-path.js +80 -0
  195. package/server/lib/artifact-target.js +284 -0
  196. package/server/lib/asset-refs.js +239 -0
  197. package/server/lib/async-queue.js +104 -0
  198. package/server/lib/auto-relations.js +130 -0
  199. package/server/lib/binding-types.js +60 -0
  200. package/server/lib/board-kind-sizes.js +62 -0
  201. package/server/lib/board-relations.js +133 -0
  202. package/server/lib/browse-proxy.js +180 -0
  203. package/server/lib/canvas-id.js +36 -0
  204. package/server/lib/cover.js +227 -0
  205. package/server/lib/danbooru-tags.js +291 -0
  206. package/server/lib/doc-extract.js +156 -0
  207. package/server/lib/docx/build-from-source.js +260 -0
  208. package/server/lib/docx/build.js +490 -0
  209. package/server/lib/docx/dump-styles.js +287 -0
  210. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -0
  211. package/server/lib/docx/merge-runs.js +129 -0
  212. package/server/lib/docx/numbering.js +218 -0
  213. package/server/lib/docx/order.js +152 -0
  214. package/server/lib/docx/rawzip.js +172 -0
  215. package/server/lib/docx/render.js +92 -0
  216. package/server/lib/docx/text-lint.js +225 -0
  217. package/server/lib/docx/tokens.js +335 -0
  218. package/server/lib/docx/units.js +29 -0
  219. package/server/lib/docx/xml.js +291 -0
  220. package/server/lib/docx-pages.js +187 -0
  221. package/server/lib/export-collect.js +289 -0
  222. package/server/lib/export-package.js +210 -0
  223. package/server/lib/html-srcset.js +145 -0
  224. package/server/lib/image-variant.js +460 -0
  225. package/server/lib/ingress/forward-openai-chat.js +305 -0
  226. package/server/lib/ingress/openai-chat.js +491 -0
  227. package/server/lib/ingress/session-notice.js +66 -0
  228. package/server/lib/ingress/session-routes.js +68 -0
  229. package/server/lib/ingress/slot-probe.js +130 -0
  230. package/server/lib/ingress/upstream-billing.js +52 -0
  231. package/server/lib/ingress/upstream-fail-streak.js +72 -0
  232. package/server/lib/ingress/upstream-truncation.js +48 -0
  233. package/server/lib/issues-store.js +182 -0
  234. package/server/lib/kinds/deck.js +72 -0
  235. package/server/lib/kinds/docx.js +283 -0
  236. package/server/lib/kinds/file-kinds.js +113 -0
  237. package/server/lib/kinds/index.js +202 -0
  238. package/server/lib/kinds/site.js +161 -0
  239. package/server/lib/model-ingress.js +543 -0
  240. package/server/lib/moderation.js +255 -0
  241. package/server/lib/notice-store.js +103 -0
  242. package/server/lib/plugin-install.js +104 -0
  243. package/server/lib/plugin-validator.js +721 -0
  244. package/server/lib/publish-store.js +115 -0
  245. package/server/lib/quick-summary.js +52 -0
  246. package/server/lib/quota.js +270 -0
  247. package/server/lib/rate-window.js +29 -0
  248. package/server/lib/reference-assets.js +92 -0
  249. package/server/lib/region-shot.js +135 -0
  250. package/server/lib/safe-path.js +73 -0
  251. package/server/lib/sdk-session.js +33 -0
  252. package/server/lib/showcase-store.js +92 -0
  253. package/server/lib/site-publish.js +465 -0
  254. package/server/lib/ssrf-guard.js +432 -0
  255. package/server/lib/task-scan.js +158 -0
  256. package/server/lib/tavern-json.js +154 -0
  257. package/server/lib/video-variant.js +237 -0
  258. package/server/lib/workspace-path.js +33 -0
  259. package/server/ops/fix-sdk-musl.mjs +40 -0
  260. package/server/ops/install-macos-fonts.sh +114 -0
  261. package/server/ops/macos-fonts.conf +336 -0
  262. package/server/ops/sandbox-shim/bwrap +57 -0
  263. package/server/projects/assets-summary.js +119 -0
  264. package/server/projects/auto-name.js +53 -0
  265. package/server/projects/board-store.js +640 -0
  266. package/server/projects/move-entry.js +103 -0
  267. package/server/projects/store.js +270 -0
  268. package/server/projects/ui-config.js +54 -0
  269. package/server/projects/workspace-templates.js +53 -0
  270. package/server/projects/workspace.js +1025 -0
  271. package/server/runtime/capabilities.js +153 -0
  272. package/server/runtime/local-config.js +177 -0
  273. package/server/runtime/local-env.js +88 -0
  274. package/server/runtime/platform.js +342 -0
  275. package/server/runtime/profile.js +72 -0
  276. package/server/services/rembg-launcher.js +313 -0
  277. package/server/services/rembg-service.py +334 -0
  278. package/server/shared/README.md +25 -0
  279. package/server/shared/deck.js +54 -0
  280. package/server/shared/time.js +49 -0
  281. package/server/style-pipeline/README.md +53 -0
  282. package/server/ws/broker.js +36 -0
  283. package/server/ws/browse-channel.js +177 -0
  284. package/server/ws/index.js +386 -0
  285. package/web/README.md +89 -0
  286. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +11 -0
  287. package/web/dist/assets/DeckWindow-4G0xX7P3.js +16 -0
  288. package/web/dist/assets/DocxWindow-BXQsoPkh.js +16 -0
  289. package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  290. package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  291. package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  292. package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  293. package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  294. package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  295. package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  296. package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  297. package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  298. package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  299. package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  300. package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  301. package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  302. package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  303. package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  304. package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  305. package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  306. package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  307. package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  308. package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  309. package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  310. package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  311. package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  312. package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  313. package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  314. package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  315. package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  316. package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  317. package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  318. package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  319. package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  320. package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  321. package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  322. package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  323. package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  324. package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  325. package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  326. package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  327. package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  328. package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  329. package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  330. package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  331. package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  332. package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  333. package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  334. package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  335. package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  336. package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  337. package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  338. package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  339. package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  340. package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  341. package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  342. package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  343. package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  344. package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  345. package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  346. package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  347. package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  348. package/web/dist/assets/SiteWindow-D6eS-2gM.js +18 -0
  349. package/web/dist/assets/again-D8tfgKKO.webp +0 -0
  350. package/web/dist/assets/caveat-nd-CP6HlsNg.woff2 +0 -0
  351. package/web/dist/assets/clock-xJhvfYpS.webp +0 -0
  352. package/web/dist/assets/film-cut-BBKMwH7q.webp +0 -0
  353. package/web/dist/assets/film-still-BZ_DL9Tt.webp +0 -0
  354. package/web/dist/assets/index-C0KUf6Wd.css +1 -0
  355. package/web/dist/assets/index-Cbl3ATvp.js +2125 -0
  356. package/web/dist/assets/ink-desk-BRbxeDsR.webp +0 -0
  357. package/web/dist/assets/ink-night-Bz4BT3XH.webp +0 -0
  358. package/web/dist/assets/ink-portrait-C0DM4V8Q.webp +0 -0
  359. package/web/dist/assets/longcang-regular-C0uvTuKa.woff2 +0 -0
  360. package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
  361. package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
  362. package/web/dist/assets/pending-edit-apply-DTjo2geW.js +31 -0
  363. package/web/dist/assets/question-8H4HSqxh.webp +0 -0
  364. package/web/dist/assets/reject-BaPJqElj.webp +0 -0
  365. package/web/dist/assets/rotate-cw-LMT3x1Zj.js +11 -0
  366. package/web/dist/assets/rp-portrait-CbrGpew2.webp +0 -0
  367. package/web/dist/assets/rp-street-C4jVKjxx.webp +0 -0
  368. package/web/dist/assets/square-dashed-mouse-pointer-CBoAkfhi.js +11 -0
  369. package/web/dist/assets/tangle-DIxHVNuT.webp +0 -0
  370. package/web/dist/assets/thisone-BylZKglV.webp +0 -0
  371. package/web/dist/assets/thumb-DO1wfK1I.webp +0 -0
  372. package/web/dist/index.html +18 -0
@@ -0,0 +1,698 @@
1
+ /**
2
+ * server/engine/runs/active-runs.js — 活跃 run registry
3
+ *
4
+ * 为什么需要:
5
+ * session-loop.js 的 ctx.abortController / ctx 是 in-memory 实例,外部(HTTP cancel
6
+ * endpoint)需要根据 runId 找到对应的引用才能控制。
7
+ *
8
+ * 工作流:
9
+ * 1. runAgent 启动时立即 registerRun(runId, { abortController, ctx })
10
+ * —— 此时 query 还没调,先注册让 cancel race condition 兜底
11
+ * 2. query() 拿到 handle 后调 attachQuery(runId, query)
12
+ * —— 之后 control 方法(interrupt/setModel/rewindFiles/...)可用
13
+ * 3. 用户点"停止"→ POST /api/projects/:pid/runs/:runId/cancel
14
+ * → cancelRun(runId) 三条路径:
15
+ * a. query.interrupt() 优雅 + 5s 后兜底 ctx.cancel()
16
+ * b. interrupt 失败兜底 ctx.cancel()
17
+ * c. race window(query 还没 attach)→ 直接 ctx.cancel()
18
+ * 4. ctx.cancel() 幂等:set abort signal + emit run.cancelled(前端据此 setIsStreaming(false))
19
+ * 5. SDK 看到 abort signal 或 interrupt → query 中断
20
+ * → session-loop.js 走 cancelled 路径或 catch
21
+ * 6. runAgent finally 调 unregisterRun(runId)(无论成功失败)
22
+ *
23
+ * 暴露给上层(API/前端)的能力(通过 getQuery):
24
+ * - query.interrupt() 优雅中断
25
+ * - query.setModel(model?) 运行时切模型
26
+ * - query.setPermissionMode(mode) 运行时切权限模式
27
+ * - query.getContextUsage() 真实上下文水位
28
+ * - query.mcpServerStatus() MCP 连接状态
29
+ * - query.rewindFiles(uuid, opts?) file checkpoint 回滚(per user message)
30
+ * - query.toggleMcpServer(name, on) 动态启停 MCP server
31
+ * - query.stopTask(taskId) 停后台子代理任务
32
+ * - query.streamInput(stream) 追加 user message(多轮复用)
33
+ * - 等等(见 sdk.d.ts:2017 Query interface)
34
+ *
35
+ * ⚠️ 这些 control 方法**只在 streaming input/output 模式下可用**
36
+ * (sdk.d.ts:2018-2022)。session-loop.js 已统一把所有 prompt 包成
37
+ * AsyncIterable<SDKUserMessage>,符合此前提。
38
+ *
39
+ * Map 是 in-memory:服务重启 controller / ctx 都没了(活跃 run 也都死了,一致)。
40
+ * 多实例部署时需要分布式协调(Redis pub/sub),stage 1 单进程够用。
41
+ */
42
+
43
+ import { markRunFailed } from './store.js';
44
+
45
+ /**
46
+ * @typedef {object} PendingQuestion
47
+ * @property {(answers: Record<string, string>) => void} resolve
48
+ * @property {(err: Error) => void} reject
49
+ * @property {number} createdAt
50
+ */
51
+
52
+ /**
53
+ * @typedef {object} ActiveRunRecord
54
+ * @property {AbortController} abortController
55
+ * @property {import('../agent/context.js').AgentContext} ctx - AgentContext 引用,cancelRun 走 ctx.cancel() 统一 emit run.cancelled
56
+ * @property {import('@anthropic-ai/claude-agent-sdk').Query|null} query - query handle,先注册时为 null,attachQuery 后填
57
+ * @property {Map<string, PendingQuestion>} pendingQuestions - A4.1:tool_use_id → Promise resolver,AskUserQuestion 等用户答案用
58
+ * @property {Map<string, PendingQuestion>} pendingElicitations - Phase 2.3:reqId → Promise resolver,MCP onElicitation 等用户答案用
59
+ * @property {number} startedAt
60
+ */
61
+
62
+ /** @type {Map<string, ActiveRunRecord>} */
63
+ const activeRuns = new Map();
64
+
65
+ /**
66
+ * @typedef {object} ActiveQuerySession - per-sid long-running query(streamInput 模式)
67
+ * @property {AbortController} abortController - session 级;close session 时触发
68
+ * @property {import('../agent/context.js').AgentContext|null} ctx - 当前 turn 的 ctx(per-turn 切换)
69
+ * @property {import('@anthropic-ai/claude-agent-sdk').Query|null} query
70
+ * @property {import('../../lib/async-queue.js').AsyncQueue} inputQueue - SDK 消费 user message 的源
71
+ * @property {string|null} currentRunId - 当前正处理的 turn run record id
72
+ * @property {Map<string, PendingQuestion>} pendingQuestions - tool_use_id → resolver
73
+ * @property {Map<string, PendingQuestion>} pendingElicitations
74
+ * @property {string} currentPermissionMode - 当前 SDK permissionMode('auto' | 'bypassPermissions' 等)
75
+ * canUseTool 钩子按此分流(auto 模式升级口)。
76
+ * 初值来自 registerQuerySession 的 initialPermissionMode;运行时切 mode(query.
77
+ * setPermissionMode)必须同步调 setSessionPermissionMode 更新本字段,否则 canUseTool
78
+ * 仍按旧 mode 拦截。
79
+ * @property {number} startedAt
80
+ * @property {number} lastActivityAt - 最近一次"活跃信号"时间戳(push message / turn 边界 /
81
+ * WS subscriber 连上)。session-loop.js 的 idle scan 据此判断是否超时关闭。
82
+ * @property {symbol} _token - register 时分配的唯一身份 token。closeQuerySession
83
+ * 立即 unregister 让出 sid 后,session-loop.js finally 的 unregister 调用必须带
84
+ * token 比对——若 sid 已被新 register 占用,token 不匹配 → noop 防误删新 entry。
85
+ */
86
+
87
+ /** @type {Map<string, ActiveQuerySession>} */
88
+ const activeQuerySessions = new Map();
89
+
90
+ /**
91
+ * 注册 run。runAgent 启动后立即调(query 还没拿到 handle)。
92
+ * 后续在 session-loop.js 拿到 query handle 后调 attachQuery 把 query 填上。
93
+ *
94
+ * @param {string} runId
95
+ * @param {object} deps
96
+ * @param {AbortController} deps.abortController
97
+ * @param {import('../agent/context.js').AgentContext} deps.ctx - 必传,cancelRun 通过它调 ctx.cancel() 统一 emit run.cancelled
98
+ */
99
+ export function registerRun(runId, { abortController, ctx } = {}) {
100
+ if (!runId || !abortController) return;
101
+ activeRuns.set(runId, {
102
+ abortController,
103
+ ctx: ctx || null,
104
+ query: null,
105
+ pendingQuestions: new Map(),
106
+ pendingElicitations: new Map(),
107
+ startedAt: Date.now(),
108
+ });
109
+ }
110
+
111
+ /**
112
+ * 把 query handle attach 到已注册的 run。
113
+ * session-loop.js 在 `const stream = query({ ... })` 之后立即调。
114
+ *
115
+ * 之所以分两步注册:query() 调用之前 cancel race(用户极快点停止)能拿到
116
+ * abortController/ctx 兜底;query() 之后 cancel 走 query.interrupt() 优雅路径。
117
+ *
118
+ * @param {string} runId
119
+ * @param {import('@anthropic-ai/claude-agent-sdk').Query} query
120
+ */
121
+ export function attachQuery(runId, query) {
122
+ const rec = activeRuns.get(runId);
123
+ if (!rec) return;
124
+ rec.query = query;
125
+ }
126
+
127
+ /**
128
+ * 注销 run(无论 succeeded / failed / cancelled)。
129
+ * session-loop.js runAgent finally 调,避免引用泄漏。
130
+ *
131
+ * A4.1:reject 任何剩余 pendingQuestions(防止 Promise 永久 hang
132
+ * 让 canUseTool callback 卡死整个 SDK loop 释放)。
133
+ */
134
+ export function unregisterRun(runId) {
135
+ if (!runId) return;
136
+ const rec = activeRuns.get(runId);
137
+ if (rec?.pendingQuestions) {
138
+ for (const [, p] of rec.pendingQuestions) {
139
+ try { p.reject(new Error('run ended before user answered question')); } catch { /* ignore */ }
140
+ }
141
+ rec.pendingQuestions.clear();
142
+ }
143
+ if (rec?.pendingElicitations) {
144
+ for (const [, p] of rec.pendingElicitations) {
145
+ try { p.reject(new Error('run ended before MCP elicitation answered')); } catch { /* ignore */ }
146
+ }
147
+ rec.pendingElicitations.clear();
148
+ }
149
+ activeRuns.delete(runId);
150
+ }
151
+
152
+ /**
153
+ * A4.1:注册一个等待用户答案的 Promise。
154
+ * session-loop.js canUseTool 拦到 AskUserQuestion 时调,emit 事件后 await 返回的
155
+ * Promise;用户在前端点选项 → POST /answer → provideAnswer → resolve。
156
+ *
157
+ * 同 toolUseId 重复 register 视作上一个被覆盖(reject 旧的 + 新建)—— 实际
158
+ * 不应发生(每个 tool_use_id 只 ask 一次),保险处理。
159
+ *
160
+ * 也会监听 abortController.signal —— run cancel 时 reject Promise,让
161
+ * canUseTool 抛错让 SDK 走 cancelled 路径。
162
+ *
163
+ * @param {string} runId
164
+ * @param {string} toolUseId - SDK 的 tool_use_id(canUseTool options.toolUseID)
165
+ * @returns {Promise<Record<string, string>>} - resolve 时返回 answers map(question text → label)
166
+ */
167
+ export function registerPendingQuestion(runId, toolUseId) {
168
+ // streamInput 模式优先:runId 是某个 active query session 的 currentRunId →
169
+ // 用 session 级 pendingQuestions Map(绑 sessionAbortController 寿命)
170
+ const sessionRec = findQuerySessionByRunId(runId);
171
+ const rec = sessionRec || activeRuns.get(runId);
172
+ if (!rec) return Promise.reject(new Error(`run ${runId} not active (no session, no per-turn run)`));
173
+ if (!toolUseId) return Promise.reject(new Error('toolUseId required'));
174
+
175
+ // 若已存在同 toolUseId 的 pending:reject 旧的避免漏 reject
176
+ const existing = rec.pendingQuestions.get(toolUseId);
177
+ if (existing) {
178
+ try { existing.reject(new Error('superseded by new question with same toolUseId')); } catch { /* ignore */ }
179
+ }
180
+
181
+ return new Promise((resolve, reject) => {
182
+ rec.pendingQuestions.set(toolUseId, {
183
+ resolve: (answers) => {
184
+ rec.pendingQuestions.delete(toolUseId);
185
+ resolve(answers);
186
+ },
187
+ reject: (err) => {
188
+ rec.pendingQuestions.delete(toolUseId);
189
+ reject(err);
190
+ },
191
+ createdAt: Date.now(),
192
+ });
193
+
194
+ // session-level / run-level abort → reject pending(防 canUseTool 永久挂)
195
+ const onAbort = () => {
196
+ const p = rec.pendingQuestions.get(toolUseId);
197
+ if (p) {
198
+ rec.pendingQuestions.delete(toolUseId);
199
+ reject(new Error(`aborted before user answered: ${rec.abortController.signal.reason || 'unknown'}`));
200
+ }
201
+ };
202
+ if (rec.abortController.signal.aborted) {
203
+ onAbort();
204
+ } else {
205
+ rec.abortController.signal.addEventListener('abort', onAbort, { once: true });
206
+ }
207
+ });
208
+ }
209
+
210
+ /** runId → activeQuerySession(streamInput 模式);找不到返 undefined */
211
+ function findQuerySessionByRunId(runId) {
212
+ for (const [, qRec] of activeQuerySessions) {
213
+ if (qRec.currentRunId === runId) return qRec;
214
+ }
215
+ return undefined;
216
+ }
217
+
218
+ /**
219
+ * Phase 2.3:注册一个等待 MCP elicitation 答案的 Promise。
220
+ * session-loop.js onElicitation 回调拦到 MCP server elicit 请求时调,emit 事件后 await
221
+ * 返回的 Promise;用户在前端答完 → POST /elicit/:reqId/answer → provideElicitation。
222
+ *
223
+ * @param {string} runId
224
+ * @param {string} reqId - host 端生成的 elicitation 请求 id
225
+ * @returns {Promise<import('@anthropic-ai/claude-agent-sdk').ElicitationResult>}
226
+ */
227
+ export function registerPendingElicitation(runId, reqId) {
228
+ const sessionRec = findQuerySessionByRunId(runId);
229
+ const rec = sessionRec || activeRuns.get(runId);
230
+ if (!rec) return Promise.reject(new Error(`run ${runId} not active (no session, no per-turn run)`));
231
+ if (!reqId) return Promise.reject(new Error('reqId required'));
232
+
233
+ const existing = rec.pendingElicitations.get(reqId);
234
+ if (existing) {
235
+ try { existing.reject(new Error('superseded by new elicitation with same reqId')); } catch { /* ignore */ }
236
+ }
237
+
238
+ return new Promise((resolve, reject) => {
239
+ rec.pendingElicitations.set(reqId, {
240
+ resolve: (result) => {
241
+ rec.pendingElicitations.delete(reqId);
242
+ resolve(result);
243
+ },
244
+ reject: (err) => {
245
+ rec.pendingElicitations.delete(reqId);
246
+ reject(err);
247
+ },
248
+ createdAt: Date.now(),
249
+ });
250
+
251
+ const onAbort = () => {
252
+ const p = rec.pendingElicitations.get(reqId);
253
+ if (p) {
254
+ rec.pendingElicitations.delete(reqId);
255
+ reject(new Error(`run aborted before elicitation answered: ${rec.abortController.signal.reason || 'unknown'}`));
256
+ }
257
+ };
258
+ if (rec.abortController.signal.aborted) {
259
+ onAbort();
260
+ } else {
261
+ rec.abortController.signal.addEventListener('abort', onAbort, { once: true });
262
+ }
263
+ });
264
+ }
265
+
266
+ /**
267
+ * Phase 2.3:用户答完 MCP elicitation,由 POST /elicit/:reqId/answer endpoint 调。
268
+ *
269
+ * @param {string} runId
270
+ * @param {string} reqId
271
+ * @param {import('@anthropic-ai/claude-agent-sdk').ElicitationResult} result
272
+ * - { action: 'accept', content?: {...} } / { action: 'cancel' } / { action: 'decline' }
273
+ * @returns {boolean}
274
+ */
275
+ export function provideElicitation(runId, reqId, result) {
276
+ const sessionRec = findQuerySessionByRunId(runId);
277
+ const rec = sessionRec || activeRuns.get(runId);
278
+ if (!rec) return false;
279
+ const p = rec.pendingElicitations.get(reqId);
280
+ if (!p) return false;
281
+ try {
282
+ p.resolve(result);
283
+ return true;
284
+ } catch {
285
+ return false;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * A4.1:用户在前端答完问题,由 POST /answer endpoint 调。
291
+ * resolve 对应 toolUseId 的 Promise,唤醒 canUseTool callback 让它返回
292
+ * updatedInput。
293
+ *
294
+ * @param {string} runId
295
+ * @param {string} toolUseId
296
+ * @param {Record<string, string>} answers - { [question text]: option label }(multi-select 用 ", " 拼接)
297
+ * @returns {boolean} true=resolved;false=run/toolUseId 不存在或已 resolve
298
+ */
299
+ export function provideAnswer(runId, toolUseId, answers) {
300
+ const sessionRec = findQuerySessionByRunId(runId);
301
+ const rec = sessionRec || activeRuns.get(runId);
302
+ if (!rec) return false;
303
+ const p = rec.pendingQuestions.get(toolUseId);
304
+ if (!p) return false;
305
+ try {
306
+ p.resolve(answers);
307
+ return true;
308
+ } catch {
309
+ return false;
310
+ }
311
+ }
312
+
313
+ /**
314
+ * 取回完整 record(abortController + ctx + query handle + startedAt)。
315
+ * 上层 API endpoint 可用:rewind / setModel / getContextUsage 等都通过 record.query 调。
316
+ *
317
+ * @returns {ActiveRunRecord | undefined}
318
+ */
319
+ export function getRun(runId) {
320
+ return activeRuns.get(runId);
321
+ }
322
+
323
+ /**
324
+ * 快捷方法:从 runId 拿 query handle —— streamInput 模式下走 sessionId 反查。
325
+ *
326
+ * ⚠️ 历史包袱:activeRuns 的 rec.query 字段在 streamInput 模式下**永远是 null**
327
+ * (session-loop.js 只调 attachSessionQuery 不调 attachQuery,因为 query handle
328
+ * 是 per-session 不是 per-run 的)。直接读 activeRuns.get(runId)?.query 永远拿
329
+ * 不到 → turn.js plan-approve / plan-reject / rewindFiles / setModel 4 个
330
+ * endpoint 历史上**全部 broken**,用户体验是 "run not active"(虽然 run 真在跑)。
331
+ *
332
+ * 本函数现在反查:runId → sessionId → activeQuerySessions[sid].query。
333
+ * 兼容老路径:若 activeRuns.get(runId)?.query 真有值(比如非 streamInput 路径
334
+ * 留下的),优先返它。
335
+ *
336
+ * @returns {import('@anthropic-ai/claude-agent-sdk').Query | null | undefined}
337
+ */
338
+ export function getQuery(runId) {
339
+ if (!runId) return null;
340
+ // 老路径(非 streamInput / 测试 stub)
341
+ const runRec = activeRuns.get(runId);
342
+ if (runRec?.query) return runRec.query;
343
+ // streamInput 路径:runId → sessionId → query
344
+ for (const [, rec] of activeQuerySessions) {
345
+ if (rec.currentRunId === runId) return rec.query;
346
+ }
347
+ return null;
348
+ }
349
+
350
+ /**
351
+ * 取消活跃 run。
352
+ *
353
+ * Phase 3c 升级:
354
+ * 优先 query.interrupt() 优雅中断 —— agent 能写完当前 token 块再停。
355
+ * SDK 收到 interrupt 后 query 自然结束,推 SDKResultMessage 含
356
+ * terminal_reason: 'aborted_streaming' | 'aborted_tools'(sdk.d.ts:5339),
357
+ * session-loop.js result 处理识别后调 ctx.cancel() emit run.cancelled。
358
+ *
359
+ * 5s 兜底 ctx.cancel():interrupt 后 SDK 偶尔会卡住(reasoning 进行中),
360
+ * timeout 兜底强制 abort + emit run.cancelled,前端不会卡 streaming。
361
+ *
362
+ * 三条路径全部走 ctx.cancel()(幂等)保证 run.cancelled 恰好 emit 一次:
363
+ * a. interrupt 成功 → session-loop.js result 路径调 ctx.cancel()
364
+ * b. interrupt 失败兜底 → cancelRun 直接调 ctx.cancel()
365
+ * c. race window(query 还没 attach)→ cancelRun 直接调 ctx.cancel()
366
+ *
367
+ * @param {string} runId
368
+ * @param {string} reason - 写入 abort signal.reason,session-loop.js cancelled 路径会读
369
+ * @returns {boolean} true=成功 trigger;false=run 不在 registry(已结束 / 不存在)
370
+ */
371
+ export function cancelRun(runId, reason = 'user_cancel') {
372
+ // streamInput 模式优先:runId 是某个 active query session 的 currentRunId →
373
+ // 调 session.query.interrupt() 中断当前 turn,query 不死继续等下条 message
374
+ for (const [sid, qRec] of activeQuerySessions) {
375
+ if (qRec.currentRunId === runId) {
376
+ if (qRec.query && typeof qRec.query.interrupt === 'function') {
377
+ qRec.query.interrupt().catch((err) => {
378
+ console.warn(`[active-runs] query.interrupt failed for run ${runId} in session ${sid.slice(0, 8)}:`, err?.message);
379
+ // 失败兜底:close 整个 session(用户预期 stop 起码能停下来)
380
+ closeQuerySession(sid, reason + ':interrupt_failed');
381
+ });
382
+ } else {
383
+ // race:query handle 还没 attach(runSession 启动 race window)
384
+ // 直接 close session 兜底
385
+ console.warn(`[active-runs] cancelRun race: query handle not yet attached for ${sid.slice(0, 8)}, closing session`);
386
+ closeQuerySession(sid, reason + ':no_query_handle');
387
+ }
388
+ return true;
389
+ }
390
+ }
391
+
392
+ // 老路径(per-turn runAgent)—— activeRuns 里的 record
393
+ const rec = activeRuns.get(runId);
394
+ if (!rec) return false;
395
+
396
+ if (rec.query && typeof rec.query.interrupt === 'function') {
397
+ rec.query.interrupt().catch((err) => {
398
+ console.warn(`[active-runs] query.interrupt failed for ${runId}:`, err?.message);
399
+ cancelViaCtxOrAbort(rec, reason);
400
+ });
401
+ setTimeout(() => {
402
+ const stillActive = activeRuns.get(runId);
403
+ if (stillActive && stillActive === rec) {
404
+ cancelViaCtxOrAbort(rec, reason + ':timeout');
405
+ }
406
+ }, 5000).unref();
407
+ } else {
408
+ cancelViaCtxOrAbort(rec, reason);
409
+ }
410
+ return true;
411
+ }
412
+
413
+ /**
414
+ * 走 ctx.cancel() 优先(emit run.cancelled),ctx 缺失时退化到 abortController.abort()。
415
+ * ctx.cancel() 幂等(context.js _cancelled flag),多次调用只触发一次 emit。
416
+ */
417
+ function cancelViaCtxOrAbort(rec, reason) {
418
+ try {
419
+ if (rec.ctx && typeof rec.ctx.cancel === 'function') {
420
+ rec.ctx.cancel(reason);
421
+ } else {
422
+ rec.abortController.abort(reason);
423
+ }
424
+ } catch { /* ignore */ }
425
+ }
426
+
427
+ /**
428
+ * 仅供测试 / debug:列当前活跃 runId
429
+ */
430
+ export function listActiveRuns() {
431
+ return Array.from(activeRuns.keys());
432
+ }
433
+
434
+ // ──────────────────────────────────────────────────────────────────────────
435
+ // streamInput 模式(Phase 1.2,per-session long-running query)
436
+ // ──────────────────────────────────────────────────────────────────────────
437
+
438
+ /**
439
+ * 注册一个 long-running query session。session-loop.js runSession 启动时立即调,
440
+ * query handle 在 attachSessionQuery 后填上。
441
+ *
442
+ * @param {string} sessionId
443
+ * @param {object} deps
444
+ * @param {AbortController} deps.abortController
445
+ * @param {import('../../lib/async-queue.js').AsyncQueue} deps.inputQueue
446
+ * @param {string} [deps.initialPermissionMode='bypassPermissions'] - 初始 permission mode
447
+ * @returns {symbol|false} 注册成功返 token(Symbol,唯一身份);同 sid 已活跃返 false。
448
+ * caller 必须保留 token 在 unregister 时回传,让 active-runs 比对身份后再删,
449
+ * 防止"closeQuerySession 已让位 + 新 session register 后,旧 session-loop finally
450
+ * 误删新 entry"的 race(grace timer 关 session 后用户立即重发的真实场景)。
451
+ */
452
+ export function registerQuerySession(sessionId, { abortController, inputQueue, initialPermissionMode = 'bypassPermissions' } = {}) {
453
+ if (!sessionId || !abortController || !inputQueue) return false;
454
+ // 关键去重:同 sid 已注册就拒绝(旧 record .set 覆盖会让旧 abortController + inputQueue
455
+ // 失去引用,旧 SDK binary 仍在跑变孤儿 → 跟新 binary 并行 Write 同 canvas.html
456
+ // = 用户看到"独立 main 进程在 write"的 bug 来源)。caller 拿到 false 不再 spawn
457
+ // 第二个 query。前端 race / 后端 fallback / resume race 都靠这条兜底。
458
+ const existing = activeQuerySessions.get(sessionId);
459
+ if (existing) {
460
+ // aborted 但没被清的残留 entry(abort 路径绕过了 unregister)—— 清掉重注册。
461
+ // 老逻辑对这种残留一律拒绝,跟 turn.js 的 hasActiveQuerySession(aborted 返
462
+ // false)判定不一致:窗口内用户消息 push 进无人消费的新 queue = 静默黑洞。
463
+ if (existing.abortController.signal.aborted) {
464
+ console.warn(`[active-runs] registerQuerySession: sid=${sessionId.slice(0, 8)} had stale aborted entry, cleaning up before re-register`);
465
+ unregisterQuerySession(sessionId);
466
+ } else {
467
+ console.warn(
468
+ `[active-runs] registerQuerySession: sid=${sessionId.slice(0, 8)} already active, `
469
+ + `refusing duplicate registration (would orphan existing SDK binary + cause double-write race)`
470
+ );
471
+ return false;
472
+ }
473
+ }
474
+ const token = Symbol('querySession');
475
+ activeQuerySessions.set(sessionId, {
476
+ abortController,
477
+ query: null,
478
+ inputQueue,
479
+ currentRunId: null,
480
+ pendingRunIds: [],
481
+ // uuid → runId:pushUserMessage 盖在出站 SDKUserMessage 上的章。CLI 开着
482
+ // --replay-user-messages 时会把这条消息原样回显(带同一个 uuid),回显的
483
+ // 时刻 = 它真正被并进对话的时刻 —— 那才是 turn 边界的锚(见 claimRunByUuid)。
484
+ runIdByUuid: new Map(),
485
+ pendingQuestions: new Map(),
486
+ pendingElicitations: new Map(),
487
+ currentPermissionMode: initialPermissionMode,
488
+ startedAt: Date.now(),
489
+ lastActivityAt: Date.now(),
490
+ _token: token,
491
+ });
492
+ return token;
493
+ }
494
+
495
+ /**
496
+ * 标记 session 活跃(更新 lastActivityAt)。每个有意义的"用户/系统在用这个 session"
497
+ * 信号都该调:pushUserMessage / turn 边界 / WS subscriber 连上 / WS 收 hydrate 请求等。
498
+ *
499
+ * idle timeout 扫描(session-loop.js)按此字段判断 now - lastActivityAt > IDLE_TIMEOUT
500
+ * 时关 session。
501
+ *
502
+ * @param {string} sessionId
503
+ */
504
+ export function markSessionActivity(sessionId) {
505
+ const rec = activeQuerySessions.get(sessionId);
506
+ if (!rec) return;
507
+ rec.lastActivityAt = Date.now();
508
+ }
509
+
510
+ /**
511
+ * @param {string} sessionId
512
+ * @returns {number|null} lastActivityAt 时间戳;session 不存在时 null
513
+ */
514
+ export function getSessionLastActivity(sessionId) {
515
+ const rec = activeQuerySessions.get(sessionId);
516
+ return rec?.lastActivityAt ?? null;
517
+ }
518
+
519
+ /**
520
+ * 同步更新 session 当前 permissionMode(canUseTool 钩子按此分流)。
521
+ * 必在 query.setPermissionMode(mode) 成功后调,否则 canUseTool 仍按旧 mode 拦截。
522
+ *
523
+ * @param {string} sessionId
524
+ * @param {string} mode - 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'dontAsk' | 'auto'
525
+ */
526
+ export function setSessionPermissionMode(sessionId, mode) {
527
+ const rec = activeQuerySessions.get(sessionId);
528
+ if (!rec) return;
529
+ rec.currentPermissionMode = mode;
530
+ }
531
+
532
+ /**
533
+ * @param {string} sessionId
534
+ * @returns {string|null}
535
+ */
536
+ export function getSessionPermissionMode(sessionId) {
537
+ const rec = activeQuerySessions.get(sessionId);
538
+ return rec?.currentPermissionMode || null;
539
+ }
540
+
541
+ /**
542
+ * @param {string} sessionId
543
+ * @param {import('@anthropic-ai/claude-agent-sdk').Query} query
544
+ */
545
+ export function attachSessionQuery(sessionId, query) {
546
+ const rec = activeQuerySessions.get(sessionId);
547
+ if (!rec) return;
548
+ rec.query = query;
549
+ }
550
+
551
+ /**
552
+ * @param {string} sessionId
553
+ * @returns {ActiveQuerySession | undefined}
554
+ */
555
+ export function getQuerySession(sessionId) {
556
+ return activeQuerySessions.get(sessionId);
557
+ }
558
+
559
+ /**
560
+ * @param {string} sessionId
561
+ * @returns {boolean} true=session 已存在且 query 活着
562
+ */
563
+ export function hasActiveQuerySession(sessionId) {
564
+ const rec = activeQuerySessions.get(sessionId);
565
+ if (!rec) return false;
566
+ if (rec.abortController.signal.aborted) return false;
567
+ return true;
568
+ }
569
+
570
+ /**
571
+ * @param {string} sessionId
572
+ * @returns {string|null}
573
+ */
574
+ export function getCurrentTurnRunId(sessionId) {
575
+ return activeQuerySessions.get(sessionId)?.currentRunId || null;
576
+ }
577
+
578
+ /**
579
+ * 反向查找:给定 runId 找到所属 sessionId(streamInput 模式 run/session 多对一)。
580
+ * 用于 turn.js /permission-mode endpoint:endpoint 拿到 runId 但要更新 session 级
581
+ * 的 permission mode(canUseTool 按 session 查)。
582
+ *
583
+ * @param {string} runId
584
+ * @returns {string|null}
585
+ */
586
+ export function getSessionIdByRunId(runId) {
587
+ if (!runId) return null;
588
+ for (const [sid, rec] of activeQuerySessions) {
589
+ if (rec.currentRunId === runId) return sid;
590
+ }
591
+ return null;
592
+ }
593
+
594
+ /**
595
+ * 并发闸门数据源(2026-07-30 内测):此刻真正在跑的 turn 数。
596
+ * activeQuerySessions 里 currentRunId 非空 = 该 session 有 turn 在飞。
597
+ * idle 的活跃 session(query 开着等消息)不算并发。
598
+ */
599
+ export function countRunningTurns() {
600
+ let n = 0;
601
+ for (const rec of activeQuerySessions.values()) {
602
+ if (rec.currentRunId) n += 1;
603
+ }
604
+ return n;
605
+ }
606
+
607
+ /** 此刻正在跑的 turn 的 runId 列表(配额层按 runs.user_id 归属到用户) */
608
+ export function listRunningTurnRunIds() {
609
+ const ids = [];
610
+ for (const rec of activeQuerySessions.values()) {
611
+ if (rec.currentRunId) ids.push(rec.currentRunId);
612
+ }
613
+ return ids;
614
+ }
615
+
616
+
617
+ /**
618
+ * 直接设 currentRunId(不 push message)。给 turn.js 起新 runSession 时
619
+ * 提前设第一 turn runId 用 —— pushUserMessage 在 register 之前没法调,
620
+ * runSession 启动后用 initialRunId 参数把 currentRunId 提前填上。
621
+ *
622
+ * @param {string} sessionId
623
+ * @param {string|null} runId
624
+ */
625
+ export function setCurrentTurnRunId(sessionId, runId) {
626
+ const rec = activeQuerySessions.get(sessionId);
627
+ if (!rec) return;
628
+ rec.currentRunId = runId || null;
629
+ }
630
+
631
+ /**
632
+ * 关闭 session 的 input queue → runSession 那头 for-await-of 自然结束 → query
633
+ * 退出后 finally 调 unregisterQuerySession(带 token 比对)。
634
+ *
635
+ * 跟 cancelRun(per-turn interrupt)不同 —— close session 是终结整个 query,
636
+ * 包括所有未处理消息,下次 turn 必须新建 session。
637
+ *
638
+ * 关键:**立即 unregister 让出 sid**(同步 delete entry + 清 pending),让用户在 SDK
639
+ * subprocess 完整退出之前重发新 chat 时,registerQuerySession 不被"同 sid 已活跃"
640
+ * 拒绝(grace timer 关 session 后用户立即重连重发的真实生产 race)。session-loop
641
+ * 的 finally 仍会 unregister,但带 token 比对——若 sid 已被新 register 占用,
642
+ * 旧 token 不匹配 → noop 不误删新 entry。
643
+ *
644
+ * @param {string} sessionId
645
+ * @param {string} reason
646
+ * @returns {boolean}
647
+ */
648
+ export function closeQuerySession(sessionId, reason = 'user_close') {
649
+ const rec = activeQuerySessions.get(sessionId);
650
+ if (!rec) return false;
651
+ try { rec.inputQueue.close(); } catch { /* */ }
652
+ try { rec.abortController.abort(reason); } catch { /* */ }
653
+ // 同步 unregister:立刻让出 sid。abort() 已触发 pending 的 onAbort listener
654
+ // reject Promise;这里再清一次(pending Maps clear + delete entry)双保险幂等。
655
+ unregisterQuerySession(sessionId);
656
+ return true;
657
+ }
658
+
659
+ /**
660
+ * 注销 session — runSession 自然结束(inputQueue.close 后 for-await-of 退出)
661
+ * 时 finally 里调,或 closeQuerySession 同步调。
662
+ *
663
+ * 顺手 reject 所有 pending questions / elicitations 防止外部 await 永久挂。
664
+ *
665
+ * @param {string} sessionId
666
+ * @param {symbol} [expectedToken] - 可选身份 token(registerQuerySession 返回的)。
667
+ * 传了就比对:rec._token !== token → noop(说明 sid 已被新 register 占用,
668
+ * 不是我的 entry,不能动)。closeQuerySession 路径不传,无条件清当前 entry。
669
+ */
670
+ export function unregisterQuerySession(sessionId, expectedToken = null) {
671
+ if (!sessionId) return;
672
+ const rec = activeQuerySessions.get(sessionId);
673
+ if (!rec) return;
674
+ // 身份比对:sid 已被新 register 占用时,旧 caller 不能误删新 entry
675
+ if (expectedToken != null && rec._token !== expectedToken) return;
676
+ for (const [, p] of rec.pendingQuestions) {
677
+ try { p.reject(new Error('session ended before user answered question')); } catch { /* */ }
678
+ }
679
+ rec.pendingQuestions.clear();
680
+ for (const [, p] of rec.pendingElicitations) {
681
+ try { p.reject(new Error('session ended before MCP elicitation answered')); } catch { /* */ }
682
+ }
683
+ rec.pendingElicitations.clear();
684
+ // 排队中还没跑到的 turn:run 行不能永远停 pending,标 failed 让前端/审计有终态
685
+ for (const rid of (rec.pendingRunIds || [])) {
686
+ try { markRunFailed(rid, 'session ended before queued turn started'); } catch { /* */ }
687
+ }
688
+ if (rec.pendingRunIds) rec.pendingRunIds.length = 0;
689
+ rec.runIdByUuid?.clear?.();
690
+ activeQuerySessions.delete(sessionId);
691
+ }
692
+
693
+ /**
694
+ * 仅供测试 / debug:列当前活跃 sessionId
695
+ */
696
+ export function listActiveQuerySessions() {
697
+ return Array.from(activeQuerySessions.keys());
698
+ }