pmx-canvas 0.11.0 → 0.12.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 (300) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +4 -1
  2. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  3. package/.github/extensions/pmx-canvas/tools.json +440 -18
  4. package/CHANGELOG.md +51 -0
  5. package/dist/amp-plugin/pmx-canvas/LICENSE +21 -0
  6. package/dist/amp-plugin/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
  7. package/dist/amp-plugin/pmx-canvas/index.js +5445 -0
  8. package/dist/amp-plugin/pmx-canvas/runtime.json +3 -0
  9. package/dist/amp-plugin/pmx-canvas/shared-runtime.mjs +194 -0
  10. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/SKILL.md +267 -0
  11. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/evals.json +1168 -0
  12. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/jwt.ts +17 -0
  13. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/login.ts +12 -0
  14. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/middleware.ts +13 -0
  15. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/routes/auth.ts +13 -0
  16. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/investigation-board/src/handlers/users.ts +27 -0
  17. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/mcp.json +36 -0
  18. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/ax-html-control-surface.md +101 -0
  19. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/codex-app-adapter.md +151 -0
  20. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +150 -0
  21. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/full-reference.md +1839 -0
  22. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/github-copilot-app-adapter.md +223 -0
  23. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/html-primitives.md +139 -0
  24. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/importing-context.md +132 -0
  25. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/installing-pmx-canvas.md +301 -0
  26. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/tours-and-recording.md +117 -0
  27. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/workbench-workflows.md +908 -0
  28. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas-orchestration/SKILL.md +141 -0
  29. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas-testing/SKILL.md +266 -0
  30. package/dist/amp-plugin/pmx-canvas/start-mcp.mjs +14857 -0
  31. package/dist/amp-plugin/pmx-canvas/tools.json +10963 -0
  32. package/dist/canvas/chunks/{abnfDiagram-N423BO3Z-p8dtp6va.js → abnfDiagram-N423BO3Z-cwtsds5d.js} +1 -1
  33. package/dist/canvas/chunks/architecture-TIHT7OUA-x63cwejs.js +1 -0
  34. package/dist/canvas/chunks/{architectureDiagram-T3A2C74G-5jhmxrzf.js → architectureDiagram-T3A2C74G-b4xc9tpz.js} +3 -3
  35. package/dist/canvas/chunks/{blockDiagram-VBNYF7ZC-pw1fa9yb.js → blockDiagram-VBNYF7ZC-ahp3yxxs.js} +7 -7
  36. package/dist/canvas/chunks/{c4Diagram-5PPSVZJV-x07nq0tt.js → c4Diagram-5PPSVZJV-nappnay7.js} +4 -4
  37. package/dist/canvas/chunks/classDiagram-JCYQIIEL-k1j2xc1t.js +1 -0
  38. package/dist/canvas/chunks/classDiagram-v2-OCEON4UE-33x08xf2.js +1 -0
  39. package/dist/canvas/chunks/{cose-bilkent-JH36ORCC-nm39npq0.js → cose-bilkent-JH36ORCC-a428syda.js} +1 -1
  40. package/dist/canvas/chunks/cynefin-VYW2F7L2-8v6jxfz8.js +1 -0
  41. package/dist/canvas/chunks/{cynefinDiagram-MW4NZA55-vdxrwahz.js → cynefinDiagram-MW4NZA55-nspsqebj.js} +1 -1
  42. package/dist/canvas/chunks/{dagre-VZM6K2ZE-bqdm7wpd.js → dagre-VZM6K2ZE-8y6177fh.js} +2 -2
  43. package/dist/canvas/chunks/{diagram-7IWD3JNH-gs5gpr93.js → diagram-7IWD3JNH-vym028wn.js} +4 -4
  44. package/dist/canvas/chunks/{diagram-B4RE2ZJO-cgwjg47s.js → diagram-B4RE2ZJO-jddqb8sy.js} +1 -1
  45. package/dist/canvas/chunks/{diagram-LBJQPF4R-w3vahpav.js → diagram-LBJQPF4R-bjwbpkrn.js} +1 -1
  46. package/dist/canvas/chunks/{diagram-Q27KOJAE-42837z6h.js → diagram-Q27KOJAE-j6ph6hcx.js} +2 -2
  47. package/dist/canvas/chunks/{diagram-UB23O5K3-4fz83q59.js → diagram-UB23O5K3-p09w8jej.js} +1 -1
  48. package/dist/canvas/chunks/{ebnfDiagram-BXEA7PRR-zphtgegj.js → ebnfDiagram-BXEA7PRR-j975ycq5.js} +1 -1
  49. package/dist/canvas/chunks/{erDiagram-JOGREHBK-zvbbdxp4.js → erDiagram-JOGREHBK-sxp6hjn8.js} +5 -5
  50. package/dist/canvas/chunks/eventmodeling-45OFAUF4-xzfgn6xg.js +1 -0
  51. package/dist/canvas/chunks/flowDiagram-UKHOOZJN-2nsmkskb.js +1 -0
  52. package/dist/canvas/chunks/{ganttDiagram-PKOTCBZU-282jzycm.js → ganttDiagram-PKOTCBZU-4tv230y2.js} +2 -2
  53. package/dist/canvas/chunks/gitGraph-TEB2WS4Q-macbhand.js +1 -0
  54. package/dist/canvas/chunks/{gitGraphDiagram-DS77QQ5N-vqqy6p1b.js → gitGraphDiagram-DS77QQ5N-mqgmnw9g.js} +1 -1
  55. package/dist/canvas/chunks/{index-wgv7en21.js → index-037z5dz2.js} +2 -2
  56. package/dist/canvas/chunks/{index-w2mra17t.js → index-081n3har.js} +1 -1
  57. package/dist/canvas/chunks/{index-353m7b0p.js → index-5n03dmb9.js} +1 -1
  58. package/dist/canvas/chunks/{index-yw8h9vha.js → index-60pgzdqe.js} +1 -1
  59. package/dist/canvas/chunks/index-8g1vb57n.js +2 -0
  60. package/dist/canvas/chunks/{index-vv56h019.js → index-8rjes9r6.js} +1 -1
  61. package/dist/canvas/chunks/{index-nprm96xp.js → index-9022d1n9.js} +1 -1
  62. package/dist/canvas/chunks/{index-194m3hc3.js → index-9b02042t.js} +1 -1
  63. package/dist/canvas/chunks/{index-0rk4gp1g.js → index-ap4q4vez.js} +1 -1
  64. package/dist/canvas/chunks/{index-rh7nv68n.js → index-dyp2nhd3.js} +1 -1
  65. package/dist/canvas/chunks/index-frvta4tm.js +2 -0
  66. package/dist/canvas/chunks/{index-s4j678gj.js → index-h9ycj8n2.js} +1 -1
  67. package/dist/canvas/chunks/{index-fjtdggjn.js → index-mergmj97.js} +2 -2
  68. package/dist/canvas/chunks/index-nwsya6dj.js +301 -0
  69. package/dist/canvas/chunks/{index-3cy1bzwh.js → index-nz8qbd1p.js} +1 -1
  70. package/dist/canvas/chunks/{index-d3b3gqe9.js → index-qhfrazv3.js} +1 -1
  71. package/dist/canvas/chunks/{index-6qmsnxwy.js → index-v119nznk.js} +4 -4
  72. package/dist/canvas/chunks/{index-r2fyc9w1.js → index-vpt37m3d.js} +6 -6
  73. package/dist/canvas/chunks/{index-qj8fxwsf.js → index-y9ma6azy.js} +1 -1
  74. package/dist/canvas/chunks/{index-tb1wf525.js → index-zx0j3a2z.js} +26 -26
  75. package/dist/canvas/chunks/info-DKCQHKI2-a0ykzs9z.js +1 -0
  76. package/dist/canvas/chunks/{infoDiagram-6WML65LV-v1yvggqj.js → infoDiagram-6WML65LV-qv97qq6a.js} +2 -2
  77. package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-p9306gh0.js +70 -0
  78. package/dist/canvas/chunks/{journeyDiagram-NVQOT4AX-zjcffveh.js → journeyDiagram-NVQOT4AX-sv67zats.js} +5 -5
  79. package/dist/canvas/chunks/{kanban-definition-27J2QSJJ-dakqtfp1.js → kanban-definition-27J2QSJJ-k2pfam6p.js} +1 -1
  80. package/dist/canvas/chunks/{mermaid.core-j2geswrd.js → mermaid.core-83r0fdce.js} +4 -4
  81. package/dist/canvas/chunks/{mindmap-definition-FAOFIHXS-142xhr3k.js → mindmap-definition-FAOFIHXS-vg2newm1.js} +1 -1
  82. package/dist/canvas/chunks/packet-7NZHBO7P-tqnabcdv.js +1 -0
  83. package/dist/canvas/chunks/{pegDiagram-VL7TDLO6-5wvk7vsd.js → pegDiagram-VL7TDLO6-k2h7zc5n.js} +1 -1
  84. package/dist/canvas/chunks/pie-RZYD4A2V-wke6gyhe.js +1 -0
  85. package/dist/canvas/chunks/{pieDiagram-7S7Q4E2Y-ztdhwycc.js → pieDiagram-7S7Q4E2Y-hg169p1v.js} +2 -2
  86. package/dist/canvas/chunks/{quadrantDiagram-CIZ2JOQS-c5p9gmg6.js → quadrantDiagram-CIZ2JOQS-bxtvsq0b.js} +5 -5
  87. package/dist/canvas/chunks/radar-I7S5WNFK-w7ka0drw.js +1 -0
  88. package/dist/canvas/chunks/railroad-3IZDKUUU-y3vg318x.js +1 -0
  89. package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-6hqvcz3c.js +1 -0
  90. package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-vcae260m.js +1 -0
  91. package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-0dbyme23.js +1 -0
  92. package/dist/canvas/chunks/{railroadDiagram-AXF67PYL-xkmqkjwf.js → railroadDiagram-AXF67PYL-njyhcp4p.js} +1 -1
  93. package/dist/canvas/chunks/{requirementDiagram-LRYGKXZP-9wm2dyaw.js → requirementDiagram-LRYGKXZP-2z6v09gm.js} +4 -4
  94. package/dist/canvas/chunks/{sankeyDiagram-W5VNT64P-2w19etgb.js → sankeyDiagram-W5VNT64P-15mzam0e.js} +1 -1
  95. package/dist/canvas/chunks/{sequenceDiagram-SI44F4Z6-0jn0vwmz.js → sequenceDiagram-SI44F4Z6-rbshyd2c.js} +12 -12
  96. package/dist/canvas/chunks/{sizeCapture-X5ZJPWSS-x75epwdq.js → sizeCapture-X5ZJPWSS-sg26xwts.js} +1 -1
  97. package/dist/canvas/chunks/{stateDiagram-OKZ733FA-schjpy6d.js → stateDiagram-OKZ733FA-6eahp98e.js} +1 -1
  98. package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-tty0ggn5.js +1 -0
  99. package/dist/canvas/chunks/{swimlanes-SLNWSIFB-gkw4xce3.js → swimlanes-SLNWSIFB-ze5db10t.js} +1 -1
  100. package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-thze75gq.js +8 -0
  101. package/dist/canvas/chunks/{timeline-definition-Z64GVDOM-x7nkcy01.js → timeline-definition-Z64GVDOM-vnp7jyy8.js} +3 -3
  102. package/dist/canvas/chunks/treeView-QDETBFTQ-6ysffnm6.js +1 -0
  103. package/dist/canvas/chunks/treemap-6X3UGDF4-m3h9p066.js +1 -0
  104. package/dist/canvas/chunks/vennDiagram-T6HMQDX7-0qrnb7mj.js +34 -0
  105. package/dist/canvas/chunks/wardley-OPB4EBWU-dp37drwx.js +1 -0
  106. package/dist/canvas/chunks/{wardleyDiagram-T6FBY63Y-t1j52t9r.js → wardleyDiagram-T6FBY63Y-8nxg8wqt.js} +4 -4
  107. package/dist/canvas/chunks/{xychartDiagram-ELKLHX3M-38t16pme.js → xychartDiagram-ELKLHX3M-nj921e7s.js} +5 -5
  108. package/dist/canvas/global.css +94 -1
  109. package/dist/canvas/index.js +154 -145
  110. package/dist/canvas/mcp-app.html +600 -498
  111. package/dist/json-render/index.css +1 -1
  112. package/dist/types/client/canvas/BoardMoveDialog.d.ts +2 -0
  113. package/dist/types/client/canvas/EdgeEditor.d.ts +19 -0
  114. package/dist/types/client/canvas/GroupNavigation.d.ts +2 -0
  115. package/dist/types/client/canvas/TextPrompt.d.ts +4 -0
  116. package/dist/types/client/nodes/PromptNode.d.ts +1 -0
  117. package/dist/types/client/nodes/ViewerFrame.d.ts +1 -1
  118. package/dist/types/client/nodes/WorkCard.d.ts +7 -0
  119. package/dist/types/client/nodes/use-ax-surface-bridge.d.ts +1 -0
  120. package/dist/types/client/state/canvas-store.d.ts +15 -0
  121. package/dist/types/client/state/human-store.d.ts +2 -0
  122. package/dist/types/client/state/intent-bridge.d.ts +4 -2
  123. package/dist/types/client/state/relation-store.d.ts +11 -0
  124. package/dist/types/client/types.d.ts +1 -1
  125. package/dist/types/json-render/catalog.d.ts +2 -2
  126. package/dist/types/server/agent-presence.d.ts +1 -0
  127. package/dist/types/server/ax-interaction.d.ts +1 -0
  128. package/dist/types/server/ax-state-manager.d.ts +10 -2
  129. package/dist/types/server/board-local.d.ts +4 -0
  130. package/dist/types/server/board-move.d.ts +38 -0
  131. package/dist/types/server/canvas-db.d.ts +12 -2
  132. package/dist/types/server/canvas-operations.d.ts +5 -6
  133. package/dist/types/server/canvas-provenance.d.ts +1 -1
  134. package/dist/types/server/canvas-serialization.d.ts +1 -0
  135. package/dist/types/server/canvas-state.d.ts +84 -20
  136. package/dist/types/server/card-relocation.d.ts +6 -0
  137. package/dist/types/server/context-reads.d.ts +3 -1
  138. package/dist/types/server/human-presence.d.ts +1 -1
  139. package/dist/types/server/index.d.ts +16 -5
  140. package/dist/types/server/intent-registry.d.ts +8 -4
  141. package/dist/types/server/knowledge-lifecycle.d.ts +4 -2
  142. package/dist/types/server/mutation-history.d.ts +7 -5
  143. package/dist/types/server/operations/registry.d.ts +2 -0
  144. package/dist/types/server/trace-manager.d.ts +1 -0
  145. package/dist/types/shared/ax-kinds.d.ts +6 -0
  146. package/dist/types/shared/ax-surface-protocol.d.ts +8 -0
  147. package/dist/types/shared/human-presence.d.ts +1 -0
  148. package/dist/types/shared/lifecycle-status.d.ts +2 -0
  149. package/docs/RELEASE.md +21 -14
  150. package/docs/agent-plugin.md +22 -2
  151. package/docs/amp-plugin.md +270 -0
  152. package/docs/cli.md +42 -3
  153. package/docs/design/GuestName.dc.html +50 -0
  154. package/docs/design/PluginLogo.svg +7 -0
  155. package/docs/design/WorkCards.dc.html +39 -0
  156. package/docs/design/canvas.json +16 -0
  157. package/docs/design/plugin-logo.png +0 -0
  158. package/docs/design.md +111 -8
  159. package/docs/evals/agents-md-sections.md +38 -0
  160. package/docs/http-api.md +128 -10
  161. package/docs/mcp.md +61 -9
  162. package/docs/node-types.md +71 -1
  163. package/docs/plans/plan-009-tech-debt-backlog.md +1 -1
  164. package/docs/plans/plan-019-post-0.9-roadmap.md +44 -20
  165. package/docs/plans/plan-020-wave-5-work-planning.md +553 -0
  166. package/docs/product-vision-2026-09.md +33 -17
  167. package/docs/sdk.md +18 -0
  168. package/package.json +6 -3
  169. package/plugins/pmx-canvas/assets/preview.png +0 -0
  170. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +4 -1
  171. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  172. package/plugins/pmx-canvas/plugin.json +2 -2
  173. package/plugins/pmx-canvas/runtime.json +1 -1
  174. package/plugins/pmx-canvas/tools.json +440 -18
  175. package/skills/data-analysis/SKILL.md +13 -26
  176. package/skills/frontend-design/LICENSE.txt +177 -0
  177. package/skills/frontend-design/SKILL.md +3 -0
  178. package/skills/pmx-canvas/SKILL.md +248 -842
  179. package/skills/pmx-canvas/evals/evals.json +110 -5
  180. package/skills/pmx-canvas/references/full-reference.md +142 -28
  181. package/skills/pmx-canvas/references/html-primitives.md +7 -0
  182. package/skills/pmx-canvas/references/installing-pmx-canvas.md +61 -11
  183. package/skills/pmx-canvas/references/tours-and-recording.md +39 -6
  184. package/skills/pmx-canvas/references/workbench-workflows.md +908 -0
  185. package/skills/pmx-canvas-testing/SKILL.md +7 -5
  186. package/skills/published-consumer-e2e/SKILL.md +25 -8
  187. package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +51 -75
  188. package/skills/web-artifacts-builder/LICENSE.txt +202 -0
  189. package/skills/web-artifacts-builder/SKILL.md +6 -0
  190. package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +2 -2
  191. package/skills/web-artifacts-builder/scripts/init-artifact.sh +8 -8
  192. package/src/cli/commands/apps.ts +1 -0
  193. package/src/cli/commands/boards.ts +30 -0
  194. package/src/client/App.tsx +20 -4
  195. package/src/client/canvas/AttentionHistory.tsx +10 -2
  196. package/src/client/canvas/BoardMoveDialog.tsx +225 -0
  197. package/src/client/canvas/CanvasNode.tsx +45 -2
  198. package/src/client/canvas/CanvasViewport.tsx +38 -9
  199. package/src/client/canvas/ContextMenu.tsx +34 -12
  200. package/src/client/canvas/EdgeEditor.tsx +149 -0
  201. package/src/client/canvas/EdgeLayer.tsx +87 -16
  202. package/src/client/canvas/ExpandedNodeOverlay.tsx +4 -0
  203. package/src/client/canvas/GroupNavigation.tsx +85 -0
  204. package/src/client/canvas/RelationsPanel.tsx +9 -0
  205. package/src/client/canvas/SelectionBar.tsx +21 -0
  206. package/src/client/canvas/SessionPanel.tsx +35 -1
  207. package/src/client/canvas/TextPrompt.tsx +18 -1
  208. package/src/client/canvas/TopBar.tsx +30 -0
  209. package/src/client/canvas/canvas-area.ts +1 -1
  210. package/src/client/nodes/ExtAppFrame.tsx +26 -10
  211. package/src/client/nodes/MarkdownNode.tsx +6 -7
  212. package/src/client/nodes/PromptNode.tsx +32 -3
  213. package/src/client/nodes/StatusNode.tsx +12 -0
  214. package/src/client/nodes/StatusSummary.tsx +11 -0
  215. package/src/client/nodes/ViewerFrame.tsx +21 -10
  216. package/src/client/nodes/WorkCard.tsx +175 -0
  217. package/src/client/nodes/use-ax-surface-bridge.ts +18 -14
  218. package/src/client/state/attention-bridge.ts +5 -0
  219. package/src/client/state/attention-store.ts +1 -0
  220. package/src/client/state/canvas-store.ts +185 -15
  221. package/src/client/state/human-store.ts +22 -2
  222. package/src/client/state/intent-bridge.ts +19 -5
  223. package/src/client/state/relation-store.ts +127 -1
  224. package/src/client/theme/global.css +94 -1
  225. package/src/client/types.ts +1 -0
  226. package/src/mcp/server.ts +13 -3
  227. package/src/server/agent-presence.ts +43 -23
  228. package/src/server/ax-flow-loop.ts +6 -0
  229. package/src/server/ax-gate-ttl.ts +8 -1
  230. package/src/server/ax-interaction.ts +16 -10
  231. package/src/server/ax-state-manager.ts +17 -4
  232. package/src/server/ax-wait.ts +20 -6
  233. package/src/server/board-export.ts +19 -6
  234. package/src/server/board-local.ts +20 -0
  235. package/src/server/board-move.ts +297 -0
  236. package/src/server/canvas-db.ts +262 -160
  237. package/src/server/canvas-operations.ts +139 -109
  238. package/src/server/canvas-schema.ts +21 -0
  239. package/src/server/canvas-serialization.ts +2 -0
  240. package/src/server/canvas-state.ts +792 -159
  241. package/src/server/card-relocation.ts +53 -0
  242. package/src/server/code-graph.ts +4 -3
  243. package/src/server/context-reads.ts +83 -55
  244. package/src/server/demo-state.json +35 -15
  245. package/src/server/file-watcher.ts +30 -10
  246. package/src/server/human-presence.ts +18 -5
  247. package/src/server/index.ts +35 -15
  248. package/src/server/intent-registry.ts +43 -5
  249. package/src/server/knowledge-lifecycle.ts +32 -9
  250. package/src/server/lifecycle-status.ts +29 -1
  251. package/src/server/mcp-app-runtime.ts +26 -7
  252. package/src/server/mutation-history.ts +37 -8
  253. package/src/server/operations/composites.ts +6 -3
  254. package/src/server/operations/mcp-contracts.ts +15 -0
  255. package/src/server/operations/mcp.ts +36 -9
  256. package/src/server/operations/ops/annotation.ts +22 -1
  257. package/src/server/operations/ops/ax-await.ts +1 -0
  258. package/src/server/operations/ops/ax-timeline.ts +28 -2
  259. package/src/server/operations/ops/batch.ts +25 -11
  260. package/src/server/operations/ops/boards.ts +44 -3
  261. package/src/server/operations/ops/nodes.ts +70 -0
  262. package/src/server/operations/ops/query.ts +32 -3
  263. package/src/server/operations/ops/snapshots.ts +1 -0
  264. package/src/server/operations/registry.ts +49 -4
  265. package/src/server/server.ts +62 -16
  266. package/src/server/tour-control.ts +37 -12
  267. package/src/server/trace-manager.ts +4 -0
  268. package/src/server/web-artifacts/scripts/bundle-artifact.sh +2 -2
  269. package/src/server/web-artifacts/scripts/init-artifact.sh +8 -8
  270. package/src/shared/agent-context.ts +7 -1
  271. package/src/shared/auto-arrange.ts +5 -4
  272. package/src/shared/ax-kinds.ts +13 -0
  273. package/src/shared/ax-surface-protocol.ts +22 -0
  274. package/src/shared/human-presence.ts +1 -0
  275. package/src/shared/lifecycle-status.ts +2 -0
  276. package/dist/canvas/chunks/architecture-TIHT7OUA-9tffvpsv.js +0 -1
  277. package/dist/canvas/chunks/classDiagram-JCYQIIEL-nc6g5x1r.js +0 -1
  278. package/dist/canvas/chunks/classDiagram-v2-OCEON4UE-0qnbpwvh.js +0 -1
  279. package/dist/canvas/chunks/cynefin-VYW2F7L2-ys8p46ts.js +0 -1
  280. package/dist/canvas/chunks/eventmodeling-45OFAUF4-v9q6dn4c.js +0 -1
  281. package/dist/canvas/chunks/flowDiagram-UKHOOZJN-yrr5z8tc.js +0 -1
  282. package/dist/canvas/chunks/gitGraph-TEB2WS4Q-kdwb8m0a.js +0 -1
  283. package/dist/canvas/chunks/index-1r72hdzb.js +0 -301
  284. package/dist/canvas/chunks/index-g3pwnqeb.js +0 -2
  285. package/dist/canvas/chunks/index-rft2hcz5.js +0 -2
  286. package/dist/canvas/chunks/info-DKCQHKI2-wpzs6sz3.js +0 -1
  287. package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-5hnq1a9p.js +0 -70
  288. package/dist/canvas/chunks/packet-7NZHBO7P-xn439f0s.js +0 -1
  289. package/dist/canvas/chunks/pie-RZYD4A2V-0kf16rwh.js +0 -1
  290. package/dist/canvas/chunks/radar-I7S5WNFK-0g2pz36t.js +0 -1
  291. package/dist/canvas/chunks/railroad-3IZDKUUU-fcaran6j.js +0 -1
  292. package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-wgdya41q.js +0 -1
  293. package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-gy07a4ka.js +0 -1
  294. package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-ewk6wtkh.js +0 -1
  295. package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-k8sfp2e7.js +0 -1
  296. package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-nqysw67j.js +0 -8
  297. package/dist/canvas/chunks/treeView-QDETBFTQ-cesw6gp0.js +0 -1
  298. package/dist/canvas/chunks/treemap-6X3UGDF4-zdv4knp9.js +0 -1
  299. package/dist/canvas/chunks/vennDiagram-T6HMQDX7-x8dqn9yf.js +0 -34
  300. package/dist/canvas/chunks/wardley-OPB4EBWU-44pdddez.js +0 -1
@@ -12,7 +12,17 @@
12
12
  * the one-shot boot migration into SQLite was retired.
13
13
  */
14
14
 
15
+ import { AsyncLocalStorage } from 'node:async_hooks';
15
16
  import { createHash, randomUUID } from 'node:crypto';
17
+ import {
18
+ prepareBoardMove,
19
+ type BoardMoveInput,
20
+ type BoardMovePreview,
21
+ type BoardMoveResult,
22
+ type MoveBoard,
23
+ } from './board-move.js';
24
+ import { boardMoveBoundary, recordCardRelocation } from './card-relocation.js';
25
+ import { mutationHistory } from './mutation-history.js';
16
26
  import {
17
27
  DEFAULT_WORKSPACE_SETTINGS,
18
28
  workspaceSettingsPatchSchema,
@@ -43,6 +53,8 @@ import {
43
53
  openCanvasDb,
44
54
  saveStateToDB,
45
55
  loadStateFromDB,
56
+ boardStorageVersion,
57
+ assertBoardStorageVersion,
46
58
  saveSnapshotToDB,
47
59
  loadSnapshotFromDB,
48
60
  listSnapshotsFromDB,
@@ -455,33 +467,232 @@ function isPersistedBlobRef(value: unknown): value is PersistedBlobRef {
455
467
  );
456
468
  }
457
469
 
470
+ /** The mutable, board-owned partition. Workspace services remain on the manager. */
471
+ interface LiveBoardState {
472
+ id: string | null;
473
+ disposed: boolean;
474
+ cleanup: Array<() => void>;
475
+ nodes: Map<string, CanvasNodeState>;
476
+ edges: Map<string, CanvasEdge>;
477
+ annotations: Map<string, CanvasAnnotation>;
478
+ viewport: ViewportState;
479
+ tour: Tour | undefined;
480
+ contextPinnedNodeIds: Set<string>;
481
+ contextPinMeta: Map<string, ContextPinMeta>;
482
+ storageVersion: number;
483
+ contentRevision: number;
484
+ revisionFloor: number;
485
+ deletionTombstones: NodeDeletionTombstone[];
486
+ generation: number;
487
+ saveTimer: ReturnType<typeof setTimeout> | null;
488
+ persistenceError: { message: string; at: string } | null;
489
+ ax: AxStateManager;
490
+ }
491
+
458
492
  class CanvasStateManager {
459
- private nodes = new Map<string, CanvasNodeState>();
460
- private edges = new Map<string, CanvasEdge>();
461
- private annotations = new Map<string, CanvasAnnotation>();
462
- private _viewport: ViewportState = { x: 0, y: 0, scale: 1 };
463
- private _tour: Tour | undefined;
493
+ private readonly boardExecution = new AsyncLocalStorage<LiveBoardState>();
494
+ private readonly liveBoards = new Map<string, LiveBoardState>();
495
+ private nextGeneration = 0;
496
+ private navigationGeneration = 0;
497
+ private readonly hostState: { capability: PmxAxHostCapability | null } = { capability: null };
498
+ private visibleBoard = this.createBoardState();
499
+
500
+ private get boardState(): LiveBoardState {
501
+ const board = this.boardExecution.getStore() ?? this.visibleBoard;
502
+ if (board.disposed) throw new OperationError('The target board was deleted or the workspace was replaced.', 409);
503
+ return board;
504
+ }
505
+
506
+ /** Identity for board-owned runtime caches. Never shared by a replacement board. */
507
+ get boardIdentity(): object {
508
+ return this.boardState;
509
+ }
510
+
511
+ onBoardDispose(cleanup: () => void): () => void {
512
+ const board = this.boardState;
513
+ board.cleanup.push(cleanup);
514
+ return () => {
515
+ const index = board.cleanup.indexOf(cleanup);
516
+ if (index >= 0) board.cleanup.splice(index, 1);
517
+ };
518
+ }
519
+
520
+ private disposeBoardRuntime(board: LiveBoardState): void {
521
+ for (const cleanup of board.cleanup.splice(0)) cleanup();
522
+ }
523
+
524
+ /** The human's navigation target, independent of the calling operation's board. */
525
+ get visibleBoardId(): string | null {
526
+ return this.visibleBoard.id;
527
+ }
528
+
529
+ get visibleBoardGeneration(): number {
530
+ return this.navigationGeneration;
531
+ }
532
+
533
+ get isBackgroundBoard(): boolean {
534
+ return this.boardState !== this.visibleBoard;
535
+ }
536
+
537
+ /** Execute on a live board without activating it, retaining ownership across awaits. */
538
+ withBoard<T>(id: string, fn: () => T): T {
539
+ const board = this.loadBoardState(id);
540
+ return this.boardExecution.run(board, fn);
541
+ }
542
+
543
+ /** Bind callbacks to their board, rather than the context of whoever invokes them. */
544
+ bindBoard<T extends unknown[], R>(fn: (...args: T) => R): (...args: T) => R {
545
+ const board = this.boardState;
546
+ return (...args) => this.boardExecution.run(board, () => fn(...args));
547
+ }
548
+
549
+ /** Viewer/global projections must not inherit the producer's board scope. */
550
+ withVisibleBoard<T>(fn: () => T): T {
551
+ return this.boardExecution.exit(fn);
552
+ }
553
+
554
+ /** Sweep resident runtime owners without loading the entire stored library. */
555
+ forEachLiveBoard(fn: () => void): void {
556
+ for (const board of new Set([this.visibleBoard, ...this.liveBoards.values()])) {
557
+ this.boardExecution.run(board, fn);
558
+ }
559
+ }
560
+
561
+ private loadBoardState(id: string): LiveBoardState {
562
+ const cached = this.liveBoards.get(id);
563
+ if (cached && this._db && cached.storageVersion === boardStorageVersion(this._db, id)) return cached;
564
+ if (cached?.saveTimer || cached?.persistenceError)
565
+ throw new OperationError(
566
+ `Board "${id}" changed in another writer. Reconcile unsaved edits before continuing.`,
567
+ 409,
568
+ );
569
+ const state = this._db ? this.readProjectedBoard(id) : null;
570
+ if (!state) throw new OperationError(`Board "${id}" not found.`, 404);
571
+ const board = cached ?? this.createBoardState();
572
+ board.id = id;
573
+ this.boardExecution.run(board, () => {
574
+ this.initializePersistedWikiLinks(state, id);
575
+ this.applyPersistedState({ ...state, theme: this._theme });
576
+ });
577
+ this.liveBoards.set(id, board);
578
+ return board;
579
+ }
580
+
581
+ private createBoardState(): LiveBoardState {
582
+ return {
583
+ id: null,
584
+ disposed: false,
585
+ cleanup: [],
586
+ nodes: new Map(),
587
+ edges: new Map(),
588
+ annotations: new Map(),
589
+ viewport: { x: 0, y: 0, scale: 1 },
590
+ tour: undefined,
591
+ contextPinnedNodeIds: new Set(),
592
+ contextPinMeta: new Map(),
593
+ storageVersion: 0,
594
+ contentRevision: 0,
595
+ revisionFloor: 0,
596
+ deletionTombstones: [],
597
+ generation: ++this.nextGeneration,
598
+ saveTimer: null,
599
+ persistenceError: null,
600
+ ax: new AxStateManager(
601
+ {
602
+ getNodeIds: () => this.currentNodeIdSet(),
603
+ getDb: () => this._db,
604
+ scheduleSave: () => this.scheduleSave(),
605
+ notifyChange: (type) => this.notifyChange(type),
606
+ recordMutation: (info) => this.recordMutation(info),
607
+ suppressed: (fn) => this.suppressed(fn),
608
+ },
609
+ this.hostState,
610
+ ),
611
+ };
612
+ }
613
+
614
+ private get nodes() {
615
+ return this.boardState.nodes;
616
+ }
617
+ private get edges() {
618
+ return this.boardState.edges;
619
+ }
620
+ private get annotations() {
621
+ return this.boardState.annotations;
622
+ }
623
+ private get _viewport() {
624
+ return this.boardState.viewport;
625
+ }
626
+ private set _viewport(value: ViewportState) {
627
+ this.boardState.viewport = value;
628
+ }
629
+ private get _tour() {
630
+ return this.boardState.tour;
631
+ }
632
+ private set _tour(value: Tour | undefined) {
633
+ this.boardState.tour = value;
634
+ }
635
+ private get _contextPinnedNodeIds() {
636
+ return this.boardState.contextPinnedNodeIds;
637
+ }
638
+ private set _contextPinnedNodeIds(value: Set<string>) {
639
+ this.boardState.contextPinnedNodeIds = value;
640
+ }
641
+ private get _contextPinMeta() {
642
+ return this.boardState.contextPinMeta;
643
+ }
644
+ private set _contextPinMeta(value: Map<string, ContextPinMeta>) {
645
+ this.boardState.contextPinMeta = value;
646
+ }
647
+ private get _storageVersion() {
648
+ return this.boardState.storageVersion;
649
+ }
650
+ private set _storageVersion(value: number) {
651
+ this.boardState.storageVersion = value;
652
+ }
653
+ private get _contentRevision() {
654
+ return this.boardState.contentRevision;
655
+ }
656
+ private set _contentRevision(value: number) {
657
+ this.boardState.contentRevision = value;
658
+ }
659
+ private get _revisionFloor() {
660
+ return this.boardState.revisionFloor;
661
+ }
662
+ private set _revisionFloor(value: number) {
663
+ this.boardState.revisionFloor = value;
664
+ }
665
+ private get _deletionTombstones() {
666
+ return this.boardState.deletionTombstones;
667
+ }
668
+ private set _deletionTombstones(value: NodeDeletionTombstone[]) {
669
+ this.boardState.deletionTombstones = value;
670
+ }
671
+ private get _boardGeneration() {
672
+ return this.boardState.generation;
673
+ }
674
+ private set _boardGeneration(value: number) {
675
+ this.boardState.generation = value;
676
+ }
677
+ private get _saveTimer() {
678
+ return this.boardState.saveTimer;
679
+ }
680
+ private set _saveTimer(value: ReturnType<typeof setTimeout> | null) {
681
+ this.boardState.saveTimer = value;
682
+ }
683
+ private get _lastPersistenceError() {
684
+ return this.boardState.persistenceError;
685
+ }
686
+ private set _lastPersistenceError(value: LiveBoardState['persistenceError']) {
687
+ this.boardState.persistenceError = value;
688
+ }
689
+ private get ax() {
690
+ return this.boardState.ax;
691
+ }
692
+
464
693
  private _theme: CanvasTheme = 'harbor';
465
- private _contextPinnedNodeIds = new Set<string>();
466
- /** Who pinned each pin, when and why; kept only for ids in `_contextPinnedNodeIds`. */
467
- private _contextPinMeta = new Map<string, ContextPinMeta>();
468
694
  private _workspaceRoot = process.cwd();
469
- private _contentRevision = 0;
470
- private _revisionFloor = 0;
471
- private _deletionTombstones: NodeDeletionTombstone[] = [];
472
-
473
- // ── AX state (canvas-bound + timeline + host partitions) ──────────
474
- // Extracted into a dedicated manager (plan-007 Slice A). CanvasStateManager
475
- // holds it and delegates its public AX methods so the SDK/HTTP/MCP surface is
476
- // byte-stable; the manager receives the host hooks it needs as injected deps.
477
- private readonly ax = new AxStateManager({
478
- getNodeIds: () => this.currentNodeIdSet(),
479
- getDb: () => this._db,
480
- scheduleSave: () => this.scheduleSave(),
481
- notifyChange: (type) => this.notifyChange(type),
482
- recordMutation: (info) => this.recordMutation(info),
483
- suppressed: (fn) => this.suppressed(fn),
484
- });
695
+ private moveRuntimeBlockers: () => ReadonlyMap<string, string> = () => new Map();
485
696
 
486
697
  // ── Change listeners (for MCP resource notifications) ──────
487
698
  private _changeListeners: ((type: CanvasChangeType) => void)[] = [];
@@ -493,9 +704,15 @@ class CanvasStateManager {
493
704
  * — e.g. the long-lived MCP subscription — may ignore the return value).
494
705
  */
495
706
  onChange(cb: (type: CanvasChangeType) => void): () => void {
496
- this._changeListeners.push(cb);
707
+ const owner = this.boardExecution.getStore();
708
+ const listener = owner
709
+ ? (type: CanvasChangeType) => {
710
+ if (owner === this.boardState) this.boardExecution.run(owner, () => cb(type));
711
+ }
712
+ : cb;
713
+ this._changeListeners.push(listener);
497
714
  return () => {
498
- const i = this._changeListeners.indexOf(cb);
715
+ const i = this._changeListeners.indexOf(listener);
499
716
  if (i >= 0) this._changeListeners.splice(i, 1);
500
717
  };
501
718
  }
@@ -529,6 +746,7 @@ class CanvasStateManager {
529
746
  }
530
747
 
531
748
  private notifyChange(type: CanvasChangeType): void {
749
+ if (type === 'ax') this.refreshLiveWorkCards();
532
750
  // Iterate a snapshot: a listener (e.g. a blocking-wait via onChange) may dispose
533
751
  // itself synchronously here, and splicing the live array mid-iteration would skip
534
752
  // the next listener for this notification.
@@ -641,8 +859,29 @@ class CanvasStateManager {
641
859
  return { group, childIds, children };
642
860
  }
643
861
 
644
- private normalizeNode(node: CanvasNodeState): CanvasNodeState {
862
+ private normalizeNode(
863
+ node: CanvasNodeState,
864
+ workItems?: PmxAxWorkItem[],
865
+ options: { boardId?: string; projectWork?: boolean } = {},
866
+ ): CanvasNodeState {
645
867
  const data = { ...node.data };
868
+ // These persisted fields are a derived display cache, never a second task record.
869
+ if (options.projectWork !== false && node.type === 'status' && typeof data.workItemId === 'string') {
870
+ const tasks =
871
+ typeof data.workItemBoardId === 'string' && data.workItemBoardId !== options.boardId
872
+ ? this.getWorkItemsOnBoard(data.workItemBoardId)
873
+ : (workItems ?? this.getWorkItems());
874
+ const task = tasks.find((item) => item.id === data.workItemId);
875
+ data.title = task?.title ?? 'Task unavailable';
876
+ data.detail = task?.detail ?? '';
877
+ data.liveWorkAvailable = !!task;
878
+ data.liveWorkSource = task?.agentId ?? task?.source ?? '';
879
+ delete data.progress;
880
+ delete data.content;
881
+ delete data.axWorkStatus;
882
+ if (task) data.workState = task.status;
883
+ else delete data.workState;
884
+ }
646
885
  // Lifecycle is optional metadata, never a card taxonomy or validation clock.
647
886
  delete data.categoryId;
648
887
  if (data.contentOrigin !== undefined && data.contentOrigin !== 'authored' && data.contentOrigin !== 'imported') {
@@ -666,6 +905,104 @@ class CanvasStateManager {
666
905
  };
667
906
  }
668
907
 
908
+ private refreshLiveWorkCards(): void {
909
+ const owner = this.boardState;
910
+ this.forEachLiveBoard(() => {
911
+ // Derived cache refreshes must not turn a clean stale board into an unsaved conflict.
912
+ if (
913
+ this.boardState !== owner &&
914
+ this._db &&
915
+ this._activeBoardId &&
916
+ !this._saveTimer &&
917
+ !this._lastPersistenceError &&
918
+ this._storageVersion !== boardStorageVersion(this._db, this._activeBoardId)
919
+ )
920
+ return;
921
+ this.refreshCurrentLiveWorkCards();
922
+ });
923
+ }
924
+
925
+ private refreshCurrentLiveWorkCards(preserveAttribution = false): void {
926
+ this.withSuppressedRecording(() => {
927
+ const tasks = this.getWorkItems();
928
+ for (const node of this.nodes.values()) {
929
+ if (node.type !== 'status' || typeof node.data.workItemId !== 'string') continue;
930
+ const next = this.normalizeNode(node, tasks);
931
+ if (!this.nodeContentChanged(node, next)) continue;
932
+ if (preserveAttribution) {
933
+ delete next.data.agentSummary;
934
+ delete next.data.summary;
935
+ this.nodes.set(node.id, {
936
+ ...next,
937
+ contentRevision: this.nextContentRevision(),
938
+ contentChangedAt: new Date().toISOString(),
939
+ });
940
+ this.scheduleSave();
941
+ } else this.updateNode(node.id, { data: next.data });
942
+ }
943
+ });
944
+ }
945
+
946
+ /** Reconcile a stored display cache and its cursor clock in the same SQLite transaction. */
947
+ private readProjectedBoard(boardId: string): PersistedCanvasState | null {
948
+ const db = this._db!;
949
+ // Validation inside another write must not change its captured versions or
950
+ // publish resident clocks before that outer transaction commits.
951
+ if (db.inTransaction) return loadStateFromDB(db, boardId);
952
+ const projected: CanvasNodeState[] = [];
953
+ let previousVersion = 0;
954
+ const state = db
955
+ .transaction(() => {
956
+ const state = loadStateFromDB(db, boardId);
957
+ if (!state) return null;
958
+ previousVersion = state.storageVersion ?? 0;
959
+ state.revisionState ??= { revision: 0, floor: 0, tombstones: [] };
960
+ const revision = state.revisionState;
961
+ state.nodes = state.nodes.map((node) => {
962
+ if (node.type !== 'status' || typeof node.data.workItemId !== 'string') return node;
963
+ const next = this.normalizeNode(node, state.ax?.workItems ?? [], { boardId });
964
+ if (!this.nodeContentChanged(node, next)) return node;
965
+ delete next.data.agentSummary;
966
+ delete next.data.summary;
967
+ next.contentRevision = ++revision.revision;
968
+ next.contentChangedAt = new Date().toISOString();
969
+ projected.push(next);
970
+ return next;
971
+ });
972
+ if (projected.length) state.storageVersion = saveStateToDB(db, boardId, state);
973
+ return state;
974
+ })
975
+ .immediate();
976
+ const live = this.liveBoards.get(boardId);
977
+ if (
978
+ state &&
979
+ projected.length &&
980
+ live &&
981
+ !live.saveTimer &&
982
+ !live.persistenceError &&
983
+ live.storageVersion === previousVersion
984
+ ) {
985
+ // Only the derived cache changed. Do not reinstall AX or renew approval TTLs.
986
+ this.boardExecution.run(live, () => {
987
+ for (const node of projected) this.nodes.set(node.id, structuredClone(node));
988
+ this._contentRevision = state.revisionState!.revision;
989
+ this._storageVersion = state.storageVersion!;
990
+ });
991
+ }
992
+ return state;
993
+ }
994
+
995
+ /** Call before capturing a context snapshot, never from per-node getters used during writes. */
996
+ refreshContextWorkCards(): void {
997
+ if (!this._db || !this._activeBoardId) return;
998
+ if (this._saveTimer || this._lastPersistenceError) {
999
+ this.refreshCurrentLiveWorkCards(true);
1000
+ return;
1001
+ }
1002
+ const state = this.readProjectedBoard(this._activeBoardId);
1003
+ if (state && state.storageVersion !== this._storageVersion) this.applyPersistedState(state, false);
1004
+ }
1005
+
669
1006
  private nextContentRevision(): number {
670
1007
  this._contentRevision += 1;
671
1008
  return this._contentRevision;
@@ -706,7 +1043,9 @@ class CanvasStateManager {
706
1043
  }
707
1044
 
708
1045
  private nodeForRead(node: CanvasNodeState): CanvasNodeState {
709
- const resolved = this.resolveNodeDataBlobs(node);
1046
+ const resolved = this.resolveNodeDataBlobs(
1047
+ node.type === 'status' && typeof node.data.workItemId === 'string' ? this.normalizeNode(node) : node,
1048
+ );
710
1049
  return {
711
1050
  ...resolved,
712
1051
  pinned: resolved.pinned || this._contextPinnedNodeIds.has(resolved.id),
@@ -809,7 +1148,6 @@ class CanvasStateManager {
809
1148
  // ── Persistence ────────────────────────────────────────────
810
1149
  private _db: import('bun:sqlite').Database | null = null;
811
1150
  private _dbPath: string | null = null;
812
- private _saveTimer: ReturnType<typeof setTimeout> | null = null;
813
1151
  readonly knowledge = new KnowledgeLifecycle(
814
1152
  () => this._db,
815
1153
  () => this.notifyChange('nodes'),
@@ -1039,7 +1377,7 @@ class CanvasStateManager {
1039
1377
  if (this._db) {
1040
1378
  try {
1041
1379
  const boardId = getActiveBoardIdFromDB(this._db);
1042
- const state = boardId ? loadStateFromDB(this._db, boardId) : null;
1380
+ const state = boardId ? this.readProjectedBoard(boardId) : null;
1043
1381
  if (boardId && state) {
1044
1382
  this.initializePersistedWikiLinks(state, boardId);
1045
1383
  this.applyPersistedState(state);
@@ -1130,15 +1468,24 @@ class CanvasStateManager {
1130
1468
 
1131
1469
  // ── Boards ──────────────────────────────────────────────────
1132
1470
 
1133
- private _activeBoardId: string | null = null;
1134
- private _boardGeneration = 0;
1471
+ private get _activeBoardId(): string | null {
1472
+ return this.boardState.id;
1473
+ }
1474
+ private set _activeBoardId(id: string | null) {
1475
+ this.boardState.id = id;
1476
+ if (id) this.liveBoards.set(id, this.boardState);
1477
+ }
1135
1478
 
1136
1479
  /** Invalidates asynchronous work whenever the loaded board state is replaced. */
1137
1480
  get boardGeneration(): number {
1481
+ return this.boardExecution.getStore() ? this._boardGeneration : this.navigationGeneration;
1482
+ }
1483
+
1484
+ get boardLifetimeGeneration(): number {
1138
1485
  return this._boardGeneration;
1139
1486
  }
1140
1487
 
1141
- /** The open board, or null on Home. */
1488
+ /** The execution board, or the open board outside a scoped operation. */
1142
1489
  get activeBoardId(): string | null {
1143
1490
  return this._activeBoardId;
1144
1491
  }
@@ -1344,62 +1691,67 @@ class CanvasStateManager {
1344
1691
  let before: PersistedCanvasState | undefined;
1345
1692
  const activeLinkRevisions = new Map<string, number>();
1346
1693
  let activeRevision = this._contentRevision;
1347
- const result = db.transaction(() => {
1348
- const previous = requestId
1349
- ? db
1350
- .query<{ board_id: string; fingerprint: string; node: string }, [string]>(
1351
- 'SELECT board_id, fingerprint, node FROM node_creation_requests WHERE request_id = ?',
1352
- )
1353
- .get(requestId)
1354
- : null;
1355
- if (previous) {
1356
- if (previous.board_id !== boardId || previous.fingerprint !== fingerprint)
1357
- throw new OperationError('requestId was already used with different creation input or destination.', 409);
1358
- // Return the original receipt even if the node has since been edited or removed.
1359
- return JSON.parse(previous.node) as T;
1360
- }
1361
- // Include pending edits in catalog resolution and durable relation clocks,
1362
- // without publishing or changing the active board before commit.
1363
- if (this._activeBoardId) saveStateToDB(db, this._activeBoardId, this.persistedState());
1364
- const state = loadStateFromDB(db, boardId);
1365
- if (!state) throw new OperationError(`Board "${boardId}" not found.`, 404);
1366
- const policies = [this.getPolicy(), state.ax?.policy];
1367
- if (policies.some((policy) => policy && (policy.scope || Object.keys(policy.agentScopes ?? {}).length)))
1368
- throw new OperationError('Destination/retry creation is refused while either board has a scope fence.', 403);
1369
- state.revisionState = {
1370
- ...state.revisionState,
1371
- revision: state.revisionState?.revision ?? 0,
1372
- floor: state.revisionState?.floor ?? 0,
1373
- tombstones: state.revisionState?.tombstones ?? [],
1374
- };
1375
- before = structuredClone(state);
1376
- const invalidations: Array<[string, string]> = [];
1377
- let next = build(state, (targetBoardId, nodeId) => invalidations.push([targetBoardId, nodeId]));
1378
- // Theme is workspace-global; never let an inactive projection overwrite it.
1379
- state.theme = this._theme;
1380
- saveStateToDB(db, boardId, state);
1381
- const invalidate = (targetBoardId: string, targetNodeId: string) => {
1382
- const revision = invalidateLinksInDB(db, targetBoardId, targetNodeId);
1383
- if (targetBoardId === this._activeBoardId && revision !== undefined) {
1384
- activeLinkRevisions.set(targetNodeId, revision);
1385
- activeRevision = Math.max(activeRevision, revision);
1694
+ let activeStorageVersion = this._storageVersion;
1695
+ const result = db
1696
+ .transaction(() => {
1697
+ const previous = requestId
1698
+ ? db
1699
+ .query<{ board_id: string; fingerprint: string; node: string }, [string]>(
1700
+ 'SELECT board_id, fingerprint, node FROM node_creation_requests WHERE request_id = ?',
1701
+ )
1702
+ .get(requestId)
1703
+ : null;
1704
+ if (previous) {
1705
+ if (previous.board_id !== boardId || previous.fingerprint !== fingerprint)
1706
+ throw new OperationError('requestId was already used with different creation input or destination.', 409);
1707
+ // Return the original receipt even if the node has since been edited or removed.
1708
+ return JSON.parse(previous.node) as T;
1386
1709
  }
1387
- };
1388
- for (const [targetBoardId, nodeId] of invalidations) invalidate(targetBoardId, nodeId);
1389
- if ('data' in next) invalidateIncomingLinksInDB(db, boardId, next.id, invalidate);
1390
- committed = loadStateFromDB(db, boardId)!;
1391
- next = ([...committed.nodes, ...committed.edges].find((candidate) => candidate.id === next.id) ?? next) as T;
1392
- if (active) activeRevision = committed.revisionState!.revision;
1393
- if (requestId)
1394
- db.run('INSERT INTO node_creation_requests VALUES (?, ?, ?, ?)', [
1395
- requestId,
1396
- boardId,
1397
- fingerprint,
1398
- JSON.stringify(next),
1399
- ]);
1400
- return next;
1401
- })();
1710
+ // Include pending edits in catalog resolution and durable relation clocks,
1711
+ // without publishing or changing the active board before commit.
1712
+ this.stageLiveBoards(db);
1713
+ const state = loadStateFromDB(db, boardId);
1714
+ if (!state) throw new OperationError(`Board "${boardId}" not found.`, 404);
1715
+ const policies = [this.getPolicy(), state.ax?.policy];
1716
+ if (policies.some((policy) => policy && (policy.scope || Object.keys(policy.agentScopes ?? {}).length)))
1717
+ throw new OperationError('Destination/retry creation is refused while either board has a scope fence.', 403);
1718
+ state.revisionState = {
1719
+ ...state.revisionState,
1720
+ revision: state.revisionState?.revision ?? 0,
1721
+ floor: state.revisionState?.floor ?? 0,
1722
+ tombstones: state.revisionState?.tombstones ?? [],
1723
+ };
1724
+ before = structuredClone(state);
1725
+ const invalidations: Array<[string, string]> = [];
1726
+ let next = build(state, (targetBoardId, nodeId) => invalidations.push([targetBoardId, nodeId]));
1727
+ // Theme is workspace-global; never let an inactive projection overwrite it.
1728
+ state.theme = this._theme;
1729
+ saveStateToDB(db, boardId, state);
1730
+ const invalidate = (targetBoardId: string, targetNodeId: string) => {
1731
+ const revision = invalidateLinksInDB(db, targetBoardId, targetNodeId);
1732
+ if (targetBoardId === this._activeBoardId && revision !== undefined) {
1733
+ activeLinkRevisions.set(targetNodeId, revision);
1734
+ activeRevision = Math.max(activeRevision, revision);
1735
+ }
1736
+ };
1737
+ for (const [targetBoardId, nodeId] of invalidations) invalidate(targetBoardId, nodeId);
1738
+ if ('data' in next) invalidateIncomingLinksInDB(db, boardId, next.id, invalidate);
1739
+ committed = loadStateFromDB(db, boardId)!;
1740
+ next = ([...committed.nodes, ...committed.edges].find((candidate) => candidate.id === next.id) ?? next) as T;
1741
+ if (active) activeRevision = committed.revisionState!.revision;
1742
+ if (this._activeBoardId) activeStorageVersion = boardStorageVersion(db, this._activeBoardId);
1743
+ if (requestId)
1744
+ db.run('INSERT INTO node_creation_requests VALUES (?, ?, ?, ?)', [
1745
+ requestId,
1746
+ boardId,
1747
+ fingerprint,
1748
+ JSON.stringify(next),
1749
+ ]);
1750
+ return next;
1751
+ })
1752
+ .immediate();
1402
1753
  if (committed) {
1754
+ this._storageVersion = activeStorageVersion;
1403
1755
  if (active) {
1404
1756
  const changed = committed.nodes.filter(
1405
1757
  (node) => JSON.stringify(before!.nodes.find((old) => old.id === node.id)) !== JSON.stringify(node),
@@ -1443,16 +1795,48 @@ class CanvasStateManager {
1443
1795
  }
1444
1796
  this._contentRevision = activeRevision;
1445
1797
  if (active || activeLinkRevisions.size) this.notifyChange('nodes');
1798
+ this.reconcileStoredBoards();
1446
1799
  }
1447
1800
  return result;
1448
1801
  }
1449
1802
 
1803
+ /** Called inside a transaction: publish no storage clocks until it commits. */
1804
+ private stageLiveBoards(db: import('bun:sqlite').Database): void {
1805
+ for (const board of this.liveBoards.values()) {
1806
+ if (board !== this.boardState && !board.saveTimer && !board.persistenceError) continue;
1807
+ this.boardExecution.run(board, () => saveStateToDB(db, board.id!, this.persistedState()));
1808
+ }
1809
+ }
1810
+
1811
+ /** A stored multi-board write retains each live owner's runtime and camera. */
1812
+ private reconcileStoredBoards(ids: Iterable<string> = this.liveBoards.keys()): void {
1813
+ if (!this._db) return;
1814
+ for (const id of ids) {
1815
+ const board = this.liveBoards.get(id);
1816
+ if (!board) continue;
1817
+ if (board.storageVersion === boardStorageVersion(this._db, board.id!)) continue;
1818
+ const state = loadStateFromDB(this._db, board.id!)!;
1819
+ this.boardExecution.run(board, () => {
1820
+ if (board.saveTimer) clearTimeout(board.saveTimer);
1821
+ board.saveTimer = null;
1822
+ board.persistenceError = null;
1823
+ this.applyPersistedState(state, false);
1824
+ this.notifyChange('nodes');
1825
+ this.notifyChange('pins');
1826
+ this.notifyChange('ax');
1827
+ });
1828
+ }
1829
+ }
1830
+
1450
1831
  /** Read a board without activating it. This projection is not a write payload. */
1451
1832
  readBoard(
1452
1833
  id: string,
1453
1834
  includeBlobs = true,
1454
1835
  ): { board: CanvasBoard; state: PersistedCanvasState; layout: CanvasLayout } | null {
1455
1836
  if (!this._db) return null;
1837
+ const live = this.liveBoards.get(id);
1838
+ if (id !== this._activeBoardId && live && (live.saveTimer || live.persistenceError))
1839
+ return this.boardExecution.run(live, () => this.readBoard(id, includeBlobs));
1456
1840
  const board = this.listBoards().find((entry) => entry.id === id);
1457
1841
  if (!board) return null;
1458
1842
  if (id === this._activeBoardId) {
@@ -1466,6 +1850,7 @@ class CanvasStateManager {
1466
1850
  board,
1467
1851
  layout,
1468
1852
  state: {
1853
+ storageVersion: this._storageVersion,
1469
1854
  version: 1,
1470
1855
  theme: this._theme,
1471
1856
  tour: layout.tour,
@@ -1480,12 +1865,16 @@ class CanvasStateManager {
1480
1865
  },
1481
1866
  };
1482
1867
  }
1483
- const state = loadStateFromDB(this._db, id);
1868
+ const state = this.readProjectedBoard(id);
1484
1869
  if (!state) return null;
1485
1870
  this.initializePersistedWikiLinks(state, id);
1486
1871
  const pins = new Set(state.contextPins);
1487
1872
  state.nodes = state.nodes.map((node) => ({
1488
- ...(includeBlobs ? this.resolveNodeDataBlobs(node) : node),
1873
+ ...(node.type === 'status' && typeof node.data.workItemId === 'string'
1874
+ ? this.normalizeNode(node, state.ax?.workItems ?? [], { boardId: id })
1875
+ : includeBlobs
1876
+ ? this.resolveNodeDataBlobs(node)
1877
+ : node),
1489
1878
  pinned: node.pinned || pins.has(node.id),
1490
1879
  }));
1491
1880
  return {
@@ -1505,9 +1894,10 @@ class CanvasStateManager {
1505
1894
  /** Every board in the workspace, most recently opened first. */
1506
1895
  listBoards(): CanvasBoard[] {
1507
1896
  if (!this._db) return [];
1508
- return listBoardsFromDB(this._db).map((board) =>
1509
- board.id === this._activeBoardId ? { ...board, nodeCount: this.nodes.size } : board,
1510
- );
1897
+ return listBoardsFromDB(this._db).map((board) => {
1898
+ const live = this.liveBoards.get(board.id);
1899
+ return live ? { ...board, nodeCount: live.nodes.size } : board;
1900
+ });
1511
1901
  }
1512
1902
 
1513
1903
  storeAttachment(input: { boardId: string; name: string; mime: string; bytes: Uint8Array }): Attachment | null {
@@ -1550,10 +1940,15 @@ class CanvasStateManager {
1550
1940
  clearTimeout(this._saveTimer);
1551
1941
  this._saveTimer = null;
1552
1942
  }
1553
- return this._db.transaction(() => {
1554
- if (!this.saveToDisk()) throw new Error(this._lastPersistenceError?.message ?? 'Failed to save canvas state.');
1555
- return updateImport(this._db!, id, 'committed', { committedNodeIds: nodeIds });
1556
- })();
1943
+ let storageVersion = this._storageVersion;
1944
+ const result = this._db
1945
+ .transaction(() => {
1946
+ if (this._activeBoardId) storageVersion = saveStateToDB(this._db!, this._activeBoardId, this.persistedState());
1947
+ return updateImport(this._db!, id, 'committed', { committedNodeIds: nodeIds });
1948
+ })
1949
+ .immediate();
1950
+ this._storageVersion = storageVersion;
1951
+ return result;
1557
1952
  }
1558
1953
 
1559
1954
  getContextBriefCursor(boardId: string, consumer: string): number | null {
@@ -1572,6 +1967,159 @@ class CanvasStateManager {
1572
1967
  return createBoardInDB(this._db, name, folder);
1573
1968
  }
1574
1969
 
1970
+ /** The host supplies runtime ownership, not another copy of board state. */
1971
+ setMoveRuntimeBlockers(read: () => ReadonlyMap<string, string>): void {
1972
+ this.moveRuntimeBlockers = read;
1973
+ }
1974
+
1975
+ moveSelectionToNewBoard(
1976
+ input: BoardMoveInput,
1977
+ ): { preview: BoardMovePreview } | { result: BoardMoveResult; replayed: boolean } {
1978
+ const db = this._db;
1979
+ if (!db) throw new OperationError('Moving cards requires persistent workspace storage.', 409);
1980
+ const name = input.name.trim();
1981
+ if (!name || name.length > 120) throw new OperationError('Board names must be 1–120 characters.');
1982
+ if (!input.requestId.trim() || input.requestId.length > 200)
1983
+ throw new OperationError('A move requires a requestId of 1–200 characters.');
1984
+ const normalized = { ...input, name, nodeIds: [...new Set(input.nodeIds)].sort() };
1985
+ const fingerprint = JSON.stringify({
1986
+ sourceBoardId: input.sourceBoardId,
1987
+ nodeIds: normalized.nodeIds,
1988
+ name,
1989
+ expectedRevision: input.expectedRevision,
1990
+ });
1991
+ let committed: PersistedCanvasState | undefined;
1992
+ const perform = () => {
1993
+ const previous = db
1994
+ .query<{ fingerprint: string; result: string }, [string]>(
1995
+ 'SELECT fingerprint, result FROM board_moves WHERE request_id = ?',
1996
+ )
1997
+ .get(input.requestId);
1998
+ if (previous && !input.preview) {
1999
+ if (previous.fingerprint !== fingerprint)
2000
+ throw new OperationError('requestId was already used for a different move.', 409);
2001
+ return { result: JSON.parse(previous.result) as BoardMoveResult, replayed: true };
2002
+ }
2003
+ if (input.sourceBoardId !== this._activeBoardId)
2004
+ throw new OperationError('The source board is no longer open. Preview the move again.', 409);
2005
+ assertBoardStorageVersion(db, input.sourceBoardId, this._storageVersion);
2006
+ const library = new Map<string, MoveBoard>();
2007
+ for (const board of listBoardsFromDB(db)) {
2008
+ const live = this.liveBoards.get(board.id);
2009
+ const state =
2010
+ live && (board.id === input.sourceBoardId || live.saveTimer || live.persistenceError)
2011
+ ? this.boardExecution.run(live, () => this.persistedState(false))
2012
+ : loadStateFromDB(db, board.id)!;
2013
+ state.nodes = state.nodes.map((node) => this.resolveNodeDataBlobs(node));
2014
+ library.set(board.id, { board, state });
2015
+ }
2016
+ const source = library.get(input.sourceBoardId)!;
2017
+ const destinationId = `board-${randomUUID()}`;
2018
+ const portalId = `board-portal-${randomUUID()}`;
2019
+ const at = new Date().toISOString();
2020
+ const plan = prepareBoardMove(
2021
+ normalized,
2022
+ library,
2023
+ destinationId,
2024
+ portalId,
2025
+ listImports(db, input.sourceBoardId),
2026
+ this.moveRuntimeBlockers(),
2027
+ currentActor(),
2028
+ at,
2029
+ );
2030
+ for (const id of plan.preview.affectedBoardIds) {
2031
+ const policy = library.get(id)!.state.ax?.policy;
2032
+ if (policy && (policy.scope || Object.keys(policy.agentScopes ?? {}).length))
2033
+ plan.preview.blockers.push(`${id}: clear the scope fence before relocating board-owned cards/references`);
2034
+ }
2035
+ for (const [id, state] of plan.states) {
2036
+ const original = library.get(id)?.state;
2037
+ if (!original) continue;
2038
+ const locked = humanPresence.lockedNodes(Date.now(), id);
2039
+ for (const node of original.nodes)
2040
+ if (
2041
+ locked.has(node.id) &&
2042
+ JSON.stringify(node) !== JSON.stringify(state.nodes.find((next) => next.id === node.id))
2043
+ )
2044
+ plan.preview.blockers.push(`${node.id}: being edited by ${locked.get(node.id)}`);
2045
+ }
2046
+ if (input.preview) return { preview: plan.preview };
2047
+ if (plan.preview.blockers.length)
2048
+ throw new OperationError('Move refused. Resolve the listed bindings, then preview again.', 409, {
2049
+ blockers: plan.preview.blockers,
2050
+ });
2051
+ db.run(
2052
+ 'INSERT INTO boards (id, name, folder, created_at, relation_vocabulary, readme_node_id) VALUES (?, ?, ?, ?, ?, ?)',
2053
+ [
2054
+ destinationId,
2055
+ name,
2056
+ source.board.folder,
2057
+ at,
2058
+ JSON.stringify(source.board.relationVocabulary ?? null),
2059
+ plan.preview.readmeNodeId,
2060
+ ],
2061
+ );
2062
+ for (const [id, state] of plan.states) {
2063
+ state.theme = this._theme;
2064
+ saveStateToDB(db, id, this.externalizePersistedStateBlobs(state));
2065
+ db.run(
2066
+ 'INSERT INTO board_move_boundaries VALUES (?, ?) ON CONFLICT(board_id) DO UPDATE SET request_id = excluded.request_id',
2067
+ [id, input.requestId],
2068
+ );
2069
+ }
2070
+ if (plan.preview.readmeNodeId) updateBoardInDB(db, input.sourceBoardId, { readmeNodeId: null });
2071
+ for (const attachment of plan.preview.attachmentIds) {
2072
+ db.run('UPDATE attachments SET board_id = ? WHERE id = ? AND board_id = ?', [
2073
+ destinationId,
2074
+ attachment,
2075
+ input.sourceBoardId,
2076
+ ]);
2077
+ db.run('UPDATE document_imports SET board_id = ? WHERE attachment_id = ? AND board_id = ?', [
2078
+ destinationId,
2079
+ attachment,
2080
+ input.sourceBoardId,
2081
+ ]);
2082
+ }
2083
+ recordCardRelocation(db, input.requestId, input.sourceBoardId, destinationId, plan.preview.nodeIds);
2084
+ const result: BoardMoveResult = {
2085
+ sourceBoardId: input.sourceBoardId,
2086
+ destinationBoardId: destinationId,
2087
+ portalId,
2088
+ nodeIds: plan.preview.nodeIds,
2089
+ affectedBoardIds: [...plan.preview.affectedBoardIds, destinationId],
2090
+ };
2091
+ db.run('INSERT INTO board_moves VALUES (?, ?, ?, ?)', [input.requestId, fingerprint, JSON.stringify(result), at]);
2092
+ committed = loadStateFromDB(db, input.sourceBoardId)!;
2093
+ return { result, replayed: false };
2094
+ };
2095
+ const transaction = db.transaction(perform);
2096
+ const result = input.preview ? transaction() : transaction.immediate();
2097
+ if (committed) {
2098
+ // This is not a board activation: retain the camera, runtime partition and
2099
+ // mounted viewers. Only the committed nodes, relations and pins changed.
2100
+ const retained = new Set(committed.nodes.map((node) => node.id));
2101
+ const removed = [...this.nodes.keys()].filter((id) => !retained.has(id));
2102
+ this.nodes.clear();
2103
+ for (const node of committed.nodes) this.nodes.set(node.id, this.resolveNodeDataBlobs(node));
2104
+ this.edges.clear();
2105
+ for (const edge of committed.edges) this.edges.set(edge.id, edge);
2106
+ this._contextPinnedNodeIds = new Set(committed.contextPins);
2107
+ this._contextPinMeta = new Map(Object.entries(committed.contextPinMeta ?? {}));
2108
+ this._storageVersion = committed.storageVersion!;
2109
+ this._contentRevision = committed.revisionState!.revision;
2110
+ this._revisionFloor = committed.revisionState!.floor;
2111
+ this._deletionTombstones = [];
2112
+ for (const id of removed) this.ax.revalidateAfterNodeRemoval(id);
2113
+ mutationHistory.reset();
2114
+ this._lastPersistenceError = null;
2115
+ this.notifyChange('nodes');
2116
+ this.notifyChange('pins');
2117
+ this.notifyChange('ax');
2118
+ if (result.result) this.reconcileStoredBoards(result.result.affectedBoardIds);
2119
+ }
2120
+ return result;
2121
+ }
2122
+
1575
2123
  updateBoard(
1576
2124
  id: string,
1577
2125
  patch: Pick<Parameters<typeof updateBoardInDB>[2], 'name' | 'folder' | 'mapPosition' | 'relationVocabulary'>,
@@ -1664,6 +2212,9 @@ class CanvasStateManager {
1664
2212
  delete data.axStep;
1665
2213
  delete data.axWorkStatus;
1666
2214
  delete data.workItemId;
2215
+ delete data.workItemBoardId;
2216
+ delete data.liveWorkAvailable;
2217
+ delete data.liveWorkSource;
1667
2218
  delete data.approvalGateId;
1668
2219
  delete data.parentGroup;
1669
2220
  if (typeof node.data.parentGroup === 'string' && ids.has(node.data.parentGroup)) {
@@ -1770,35 +2321,35 @@ class CanvasStateManager {
1770
2321
  /** Deletes a board and its snapshots; deleting the open board returns to Home first. */
1771
2322
  deleteBoard(id: string): boolean {
1772
2323
  if (!this._db) return false;
1773
- if (id === this._activeBoardId && !this.switchBoard(null)) return false;
2324
+ if (id === this.visibleBoard.id && !this.withVisibleBoard(() => this.switchBoard(null))) return false;
1774
2325
  // Reverse relations must include outgoing links still debounced on the open board.
1775
- if (this._saveTimer || this._lastPersistenceError) this.flushToDisk();
1776
- return deleteBoardFromDB(this._db, id, (boardId, nodeId) => this.invalidateRelationTarget(boardId, nodeId));
2326
+ this.flushToDisk();
2327
+ const removed = deleteBoardFromDB(this._db, id, (boardId, nodeId) =>
2328
+ this.invalidateRelationTarget(boardId, nodeId),
2329
+ );
2330
+ if (removed) {
2331
+ const board = this.liveBoards.get(id);
2332
+ if (board) {
2333
+ board.disposed = true;
2334
+ this.disposeBoardRuntime(board);
2335
+ }
2336
+ this.liveBoards.delete(id);
2337
+ }
2338
+ return removed;
1777
2339
  }
1778
2340
 
1779
2341
  /**
1780
- * Save the open board, then open `id` (or Home for null). The canvas state is
1781
- * replaced wholesale; callers own the runtime side (undo history, app
1782
- * sessions, watchers, SSE) — see `openCanvasBoard` in canvas-operations.
2342
+ * Save pending boards, then select the visible owner. Existing partitions
2343
+ * remain live; canvas-operations starts the selected board's runtime/SSE.
1783
2344
  */
1784
2345
  switchBoard(id: string | null): boolean {
1785
2346
  if (!this._db) return false;
1786
- if (this._saveTimer) {
1787
- clearTimeout(this._saveTimer);
1788
- this._saveTimer = null;
1789
- }
1790
- if (!this.saveToDisk()) {
1791
- throw new Error(this._lastPersistenceError?.message ?? 'Failed to save canvas state before switching boards.');
1792
- }
1793
- let next = this.emptyPersistedState();
1794
- if (id) {
1795
- const state = loadStateFromDB(this._db, id);
1796
- if (!state) return false;
1797
- this.initializePersistedWikiLinks(state, id);
1798
- next = state;
1799
- }
1800
- this.applyPersistedState(next);
1801
- this._activeBoardId = id;
2347
+ if (this.boardExecution.getStore())
2348
+ throw new OperationError('Board-scoped operations cannot change navigation.', 409);
2349
+ if (id && !getBoardFromDB(this._db, id)) return false;
2350
+ this.flushToDisk();
2351
+ this.visibleBoard = id ? this.loadBoardState(id) : this.createBoardState();
2352
+ this.navigationGeneration = ++this.nextGeneration;
1802
2353
  setActiveBoardIdInDB(this._db, id);
1803
2354
  this.notifyChange('nodes');
1804
2355
  this.notifyChange('pins');
@@ -1836,20 +2387,27 @@ class CanvasStateManager {
1836
2387
  private scheduleSave(): void {
1837
2388
  if (!this._db) return;
1838
2389
  if (this._saveTimer) clearTimeout(this._saveTimer);
1839
- this._saveTimer = setTimeout(() => {
1840
- this._saveTimer = null;
1841
- this.saveToDisk();
1842
- }, SAVE_DEBOUNCE_MS);
2390
+ this._saveTimer = setTimeout(
2391
+ this.bindBoard(() => {
2392
+ this._saveTimer = null;
2393
+ this.saveToDisk();
2394
+ }),
2395
+ SAVE_DEBOUNCE_MS,
2396
+ );
1843
2397
  }
1844
2398
 
1845
2399
  flushToDisk(): void {
1846
2400
  if (!this._db) return;
1847
- if (this._saveTimer) {
1848
- clearTimeout(this._saveTimer);
1849
- this._saveTimer = null;
1850
- }
1851
- if (!this.saveToDisk()) {
1852
- throw new Error(this._lastPersistenceError?.message ?? 'Failed to save canvas state.');
2401
+ const current = this.boardState;
2402
+ for (const board of new Set([this.visibleBoard, ...this.liveBoards.values()])) {
2403
+ if (board !== current && !board.saveTimer && !board.persistenceError) continue;
2404
+ this.boardExecution.run(board, () => {
2405
+ if (this._saveTimer) {
2406
+ clearTimeout(this._saveTimer);
2407
+ this._saveTimer = null;
2408
+ }
2409
+ if (!this.saveToDisk()) throw new Error(this._lastPersistenceError?.message ?? 'Failed to save canvas state.');
2410
+ });
1853
2411
  }
1854
2412
  if (this._db) {
1855
2413
  try {
@@ -1861,8 +2419,9 @@ class CanvasStateManager {
1861
2419
  }
1862
2420
  }
1863
2421
 
1864
- private persistedState(): PersistedCanvasState {
1865
- return this.externalizePersistedStateBlobs({
2422
+ private persistedState(externalize = true): PersistedCanvasState {
2423
+ const state: PersistedCanvasState = {
2424
+ storageVersion: this._storageVersion,
1866
2425
  version: 1,
1867
2426
  theme: this._theme,
1868
2427
  tour: this._tour,
@@ -1874,7 +2433,8 @@ class CanvasStateManager {
1874
2433
  contextPinMeta: this.pinMetaRecord(),
1875
2434
  ax: this.getAxState(),
1876
2435
  revisionState: this.revisionState(),
1877
- });
2436
+ };
2437
+ return externalize ? this.externalizePersistedStateBlobs(state) : state;
1878
2438
  }
1879
2439
 
1880
2440
  /** Write current state to SQLite immediately. */
@@ -1888,7 +2448,7 @@ class CanvasStateManager {
1888
2448
  }
1889
2449
  const boardId = this.ensureActiveBoard();
1890
2450
  if (!boardId) return false;
1891
- saveStateToDB(this._db, boardId, this.persistedState());
2451
+ this._storageVersion = saveStateToDB(this._db, boardId, this.persistedState());
1892
2452
  this._lastPersistenceError = null;
1893
2453
  return true;
1894
2454
  } catch (error) {
@@ -1904,18 +2464,20 @@ class CanvasStateManager {
1904
2464
  }
1905
2465
  }
1906
2466
 
1907
- private _lastPersistenceError: { message: string; at: string } | null = null;
1908
-
1909
2467
  /** Health view of state persistence: ok until a save fails, ok again after the next success. */
1910
2468
  get persistenceHealth(): { ok: boolean; lastError: { message: string; at: string } | null } {
1911
- return { ok: this._lastPersistenceError === null, lastError: this._lastPersistenceError };
2469
+ const lastError =
2470
+ [this.visibleBoard, ...this.liveBoards.values()].find((board) => board.persistenceError)?.persistenceError ??
2471
+ null;
2472
+ return { ok: lastError === null, lastError };
1912
2473
  }
1913
2474
 
1914
2475
  /** Close the SQLite database cleanly. Call on server shutdown. */
1915
2476
  close(): void {
1916
2477
  // A failed debounce has no timer left, but its edits still need saving.
1917
2478
  // Leave the database and memory intact if the retry fails.
1918
- if (this._saveTimer || this._lastPersistenceError) this.flushToDisk();
2479
+ if ([this.visibleBoard, ...this.liveBoards.values()].some((board) => board.saveTimer || board.persistenceError))
2480
+ this.flushToDisk();
1919
2481
  if (this._db) {
1920
2482
  try {
1921
2483
  finalizeCanvasDbForClose(this._db);
@@ -1929,8 +2491,14 @@ class CanvasStateManager {
1929
2491
  }
1930
2492
  this._db = null;
1931
2493
  }
1932
- this._activeBoardId = null;
1933
- this._boardGeneration += 1;
2494
+ for (const board of new Set([this.visibleBoard, ...this.liveBoards.values()])) {
2495
+ board.disposed = true;
2496
+ this.disposeBoardRuntime(board);
2497
+ }
2498
+ this.liveBoards.clear();
2499
+ this.hostState.capability = null;
2500
+ this.visibleBoard = this.createBoardState();
2501
+ this.navigationGeneration = ++this.nextGeneration;
1934
2502
  }
1935
2503
 
1936
2504
  // ── Snapshots ───────────────────────────────────────────────
@@ -1940,12 +2508,17 @@ class CanvasStateManager {
1940
2508
  return join(this._workspaceRoot, PMX_CANVAS_DIR, SNAPSHOTS_SUBDIR);
1941
2509
  }
1942
2510
 
1943
- private applyPersistedState(state: PersistedCanvasState): void {
2511
+ private applyPersistedState(state: PersistedCanvasState, replaced = true): void {
1944
2512
  // Validate the complete replacement before discarding any live board state.
2513
+ // Keep the cached task fields as the revision comparison baseline. Display
2514
+ // reads project fresh fields; explicit reconciliation stamps and saves them.
1945
2515
  const nodes = (Array.isArray(state.nodes) ? state.nodes : [])
1946
2516
  .filter((node) => node?.id)
1947
- .map((node) => structuredClone(this.normalizeNode(node)));
1948
- this._boardGeneration += 1;
2517
+ .map((node) => structuredClone(this.normalizeNode(node, undefined, { projectWork: false })));
2518
+ if (replaced) {
2519
+ this._boardGeneration = ++this.nextGeneration;
2520
+ if (this.boardState === this.visibleBoard) this.navigationGeneration = ++this.nextGeneration;
2521
+ }
1949
2522
  this._tour = state.tour ? structuredClone(state.tour) : undefined;
1950
2523
  this.nodes.clear();
1951
2524
  this.edges.clear();
@@ -1953,6 +2526,7 @@ class CanvasStateManager {
1953
2526
  this._contextPinnedNodeIds.clear();
1954
2527
  this._contextPinMeta.clear();
1955
2528
  this.ax.resetCanvasBound();
2529
+ this._storageVersion = state.storageVersion ?? 0;
1956
2530
  this._contentRevision = state.revisionState?.revision ?? 0;
1957
2531
  this._revisionFloor = state.revisionState?.floor ?? 0;
1958
2532
  this._deletionTombstones = structuredClone(state.revisionState?.tombstones ?? []);
@@ -2079,6 +2653,7 @@ class CanvasStateManager {
2079
2653
 
2080
2654
  try {
2081
2655
  const payload = this.externalizePersistedStateBlobs({
2656
+ storageVersion: this._storageVersion,
2082
2657
  version: 1,
2083
2658
  theme: this._theme,
2084
2659
  viewport: this._viewport,
@@ -2174,6 +2749,7 @@ class CanvasStateManager {
2174
2749
  restoreSnapshot(idOrName: string): boolean {
2175
2750
  const resolved = this.readResolvedSnapshot(idOrName);
2176
2751
  if (!resolved || resolved.state.version !== 1) return false;
2752
+ this.assertSnapshotRestorable(idOrName);
2177
2753
 
2178
2754
  const previousState: PersistedCanvasState = this.externalizePersistedStateBlobs({
2179
2755
  version: 1,
@@ -2214,7 +2790,11 @@ class CanvasStateManager {
2214
2790
 
2215
2791
  try {
2216
2792
  const revisionBeforeRestore = this._contentRevision;
2217
- this.applyPersistedState({ ...nextState, revisionState: this.revisionState() });
2793
+ this.applyPersistedState({
2794
+ ...nextState,
2795
+ storageVersion: this._storageVersion,
2796
+ revisionState: this.revisionState(),
2797
+ });
2218
2798
  this.restampRestoredContent(previousState.nodes, previousState.edges, revisionBeforeRestore);
2219
2799
  this.scheduleSave();
2220
2800
  this.notifyChange('nodes');
@@ -2227,7 +2807,11 @@ class CanvasStateManager {
2227
2807
  const before = Array.from(this.nodes.values(), (node) => structuredClone(node));
2228
2808
  const edgesBefore = this.getEdges();
2229
2809
  const revisionBeforeReplay = this._contentRevision;
2230
- this.applyPersistedState({ ...nextState, revisionState: this.revisionState() });
2810
+ this.applyPersistedState({
2811
+ ...nextState,
2812
+ storageVersion: this._storageVersion,
2813
+ revisionState: this.revisionState(),
2814
+ });
2231
2815
  this.restampRestoredContent(before, edgesBefore, revisionBeforeReplay);
2232
2816
  this.scheduleSave();
2233
2817
  this.notifyChange('nodes');
@@ -2238,7 +2822,11 @@ class CanvasStateManager {
2238
2822
  const before = Array.from(this.nodes.values(), (node) => structuredClone(node));
2239
2823
  const edgesBefore = this.getEdges();
2240
2824
  const revisionBeforeReplay = this._contentRevision;
2241
- this.applyPersistedState({ ...previousState, revisionState: this.revisionState() });
2825
+ this.applyPersistedState({
2826
+ ...previousState,
2827
+ storageVersion: this._storageVersion,
2828
+ revisionState: this.revisionState(),
2829
+ });
2242
2830
  this.restampRestoredContent(before, edgesBefore, revisionBeforeReplay);
2243
2831
  this.scheduleSave();
2244
2832
  this.notifyChange('nodes');
@@ -2257,6 +2845,19 @@ class CanvasStateManager {
2257
2845
  }
2258
2846
  }
2259
2847
 
2848
+ /** Old snapshots stay readable, but neither whole-board nor one-card restore crosses a move. */
2849
+ assertSnapshotRestorable(idOrName: string): void {
2850
+ if (!this._db || !this._activeBoardId) return;
2851
+ const resolved = this.readResolvedSnapshot(idOrName);
2852
+ if (!resolved) throw new OperationError('Snapshot not found.', 404);
2853
+ assertBoardStorageVersion(this._db, this._activeBoardId, this._storageVersion);
2854
+ if ((resolved.state.moveBoundary ?? null) !== boardMoveBoundary(this._db, this._activeBoardId))
2855
+ throw new OperationError(
2856
+ 'This snapshot predates a move. Inspect it and reapply desired content to the current card, or restore a whole-library backup.',
2857
+ 409,
2858
+ );
2859
+ }
2860
+
2260
2861
  /** Read a snapshot's data without restoring it (for diff). Resolves by ID or name. */
2261
2862
  getSnapshotData(idOrName: string): {
2262
2863
  name: string;
@@ -2453,18 +3054,22 @@ class CanvasStateManager {
2453
3054
  : (loadStateFromDB(this._db!, board.id)?.nodes ?? [])
2454
3055
  ).map((node) => ({ id: node.id, title: typeof node.data.title === 'string' ? node.data.title : node.id })),
2455
3056
  }));
2456
- this._db.transaction(() => {
2457
- for (const node of legacy) {
2458
- const resolved = this.resolveNodeDataBlobs(node);
2459
- const content = typeof resolved.data.content === 'string' ? resolved.data.content : '';
2460
- node.data = { ...node.data, wikiLinks: resolveWikiLinks(content, [], catalog) };
2461
- this._db!.run('UPDATE nodes SET data = ? WHERE board_id = ? AND id = ?', [
2462
- JSON.stringify(node.data),
2463
- boardId,
2464
- node.id,
2465
- ]);
2466
- }
2467
- })();
3057
+ state.storageVersion = this._db
3058
+ .transaction(() => {
3059
+ assertBoardStorageVersion(this._db!, boardId, state.storageVersion ?? 0);
3060
+ for (const node of legacy) {
3061
+ const resolved = this.resolveNodeDataBlobs(node);
3062
+ const content = typeof resolved.data.content === 'string' ? resolved.data.content : '';
3063
+ node.data = { ...node.data, wikiLinks: resolveWikiLinks(content, [], catalog) };
3064
+ this._db!.run('UPDATE nodes SET data = ? WHERE board_id = ? AND id = ?', [
3065
+ JSON.stringify(node.data),
3066
+ boardId,
3067
+ node.id,
3068
+ ]);
3069
+ }
3070
+ return boardStorageVersion(this._db!, boardId);
3071
+ })
3072
+ .immediate();
2468
3073
  }
2469
3074
 
2470
3075
  private bindWikiLinks(
@@ -2588,7 +3193,7 @@ class CanvasStateManager {
2588
3193
  description: `Added ${cloned.type} node "${(cloned.data.title as string) ?? cloned.id}"`,
2589
3194
  forward: this.suppressed(() => {
2590
3195
  this.nodes.set(cloned.id, {
2591
- ...structuredClone(cloned),
3196
+ ...this.normalizeNode(structuredClone(cloned)),
2592
3197
  contentRevision: this.nextContentRevision(),
2593
3198
  contentChangedAt: new Date().toISOString(),
2594
3199
  lastEditedBy: currentActor(),
@@ -2639,6 +3244,14 @@ class CanvasStateManager {
2639
3244
  updateNode(id: string, patch: Partial<CanvasNodeState>, savedWikiLinks?: WikiLink[] | null): void {
2640
3245
  const existing = this.nodes.get(id);
2641
3246
  if (!existing) return;
3247
+ if (
3248
+ existing.type === 'status' &&
3249
+ typeof existing.data.workItemId === 'string' &&
3250
+ ((patch.type !== undefined && patch.type !== existing.type) ||
3251
+ (patch.data && patch.data.workItemId !== existing.data.workItemId))
3252
+ ) {
3253
+ throw new OperationError('Live work card references are immutable.');
3254
+ }
2642
3255
  if (patch.data) {
2643
3256
  if (patch.data.contentOrigin !== existing.data.contentOrigin) {
2644
3257
  throw new OperationError('Content origin is immutable after creation.');
@@ -2914,6 +3527,10 @@ class CanvasStateManager {
2914
3527
  }
2915
3528
 
2916
3529
  private invalidateRelationTarget(boardId: string, nodeId: string): void {
3530
+ if (boardId !== this._activeBoardId && this.liveBoards.has(boardId)) {
3531
+ this.withBoard(boardId, () => this.invalidateRelationTarget(boardId, nodeId));
3532
+ return;
3533
+ }
2917
3534
  if (boardId === this._activeBoardId) {
2918
3535
  const node = this.nodes.get(nodeId);
2919
3536
  if (node) {
@@ -3356,6 +3973,7 @@ class CanvasStateManager {
3356
3973
  */
3357
3974
  applyPersistedAxState(ax: unknown): void {
3358
3975
  this.ax.applyPersistedAx(ax);
3976
+ this.refreshLiveWorkCards();
3359
3977
  }
3360
3978
 
3361
3979
  getAxFocus(): PmxAxFocusState {
@@ -3375,6 +3993,14 @@ class CanvasStateManager {
3375
3993
  return this.ax.getWorkItems();
3376
3994
  }
3377
3995
 
3996
+ getWorkItemsOnBoard(boardId: string): PmxAxWorkItem[] {
3997
+ if (boardId === this._activeBoardId) return this.getWorkItems();
3998
+ const live = this.liveBoards.get(boardId);
3999
+ if (live && (live.saveTimer || live.persistenceError))
4000
+ return this.boardExecution.run(live, () => live.ax.getWorkItems());
4001
+ return this._db ? (loadStateFromDB(this._db, boardId)?.ax?.workItems ?? []) : [];
4002
+ }
4003
+
3378
4004
  addWorkItem(
3379
4005
  input: {
3380
4006
  title: string;
@@ -3420,6 +4046,7 @@ class CanvasStateManager {
3420
4046
  for (const nodeId of nodeIds) {
3421
4047
  const node = this.nodes.get(nodeId);
3422
4048
  if (!node) continue;
4049
+ if (node.type === 'status' && typeof node.data.workItemId === 'string') continue;
3423
4050
  // No-op guard: a title-/agentId-only work-item patch must not push
3424
4051
  // history entries and SSE churn for chips that aren't changing.
3425
4052
  if (node.data.axWorkStatus === status) continue;
@@ -3661,11 +4288,15 @@ class CanvasStateManager {
3661
4288
  return this.ax.getAxSteering(q);
3662
4289
  }
3663
4290
 
3664
- getPendingSteering(options: { consumer?: string; limit?: number } = {}): PmxAxSteeringMessage[] {
4291
+ getPendingSteering(
4292
+ options: { consumer?: string; limit?: number; addressedOnly?: boolean; offset?: number } = {},
4293
+ ): PmxAxSteeringMessage[] {
3665
4294
  return this.ax.getPendingSteering(options);
3666
4295
  }
3667
4296
 
3668
- getPendingSteeringForContext(options: { consumer?: string; limit?: number } = {}): PmxAxSteeringMessage[] {
4297
+ getPendingSteeringForContext(
4298
+ options: { consumer?: string; limit?: number; addressedOnly?: boolean; offset?: number } = {},
4299
+ ): PmxAxSteeringMessage[] {
3669
4300
  return this.ax.getPendingSteeringForContext(options);
3670
4301
  }
3671
4302
 
@@ -3689,6 +4320,8 @@ class CanvasStateManager {
3689
4320
  this._contextReadListener?.(boardId);
3690
4321
  return read;
3691
4322
  } catch (error) {
4323
+ // Explicit receipts need a distinguishable outcome, not best-effort diagnostics.
4324
+ if (input.deliveryId) throw error;
3692
4325
  logCanvasStateWarning('record context read failed', error);
3693
4326
  return null;
3694
4327
  }