pmx-canvas 0.8.0 → 0.9.0

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 (341) hide show
  1. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  2. package/.github/extensions/pmx-canvas/tools.json +601 -22
  3. package/CHANGELOG.md +66 -0
  4. package/Readme.md +131 -532
  5. package/dist/canvas/global.css +2759 -510
  6. package/dist/canvas/index.js +168 -107
  7. package/dist/canvas/mcp-app.html +2936 -632
  8. package/dist/json-render/index.css +81 -1
  9. package/dist/json-render/index.js +20 -20
  10. package/dist/types/client/canvas/BarHint.d.ts +12 -2
  11. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
  12. package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
  13. package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
  14. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  15. package/dist/types/client/canvas/CommandBar.d.ts +3 -4
  16. package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
  17. package/dist/types/client/canvas/ContextChip.d.ts +3 -0
  18. package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
  19. package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
  20. package/dist/types/client/canvas/EmptyState.d.ts +4 -4
  21. package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
  22. package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
  23. package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
  24. package/dist/types/client/canvas/HomeView.d.ts +9 -7
  25. package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
  26. package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
  27. package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
  28. package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
  29. package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
  30. package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
  31. package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
  32. package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
  33. package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
  34. package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
  35. package/dist/types/client/canvas/canvas-area.d.ts +1 -1
  36. package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
  37. package/dist/types/client/canvas/graph-model.d.ts +37 -0
  38. package/dist/types/client/canvas/memory-layout.d.ts +23 -0
  39. package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
  40. package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
  41. package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
  42. package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
  43. package/dist/types/client/icons.d.ts +0 -10
  44. package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
  45. package/dist/types/client/nodes/HtmlNode.d.ts +1 -0
  46. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
  47. package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
  48. package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
  49. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
  50. package/dist/types/client/state/boards-store.d.ts +81 -9
  51. package/dist/types/client/state/canvas-store.d.ts +14 -1
  52. package/dist/types/client/state/context-chip-store.d.ts +22 -0
  53. package/dist/types/client/state/context-status-store.d.ts +35 -0
  54. package/dist/types/client/state/intent-bridge.d.ts +11 -8
  55. package/dist/types/client/state/knowledge-store.d.ts +22 -0
  56. package/dist/types/client/state/near-pin-store.d.ts +44 -1
  57. package/dist/types/client/state/presence-store.d.ts +4 -4
  58. package/dist/types/client/state/presentation.d.ts +28 -0
  59. package/dist/types/client/state/relation-store.d.ts +10 -0
  60. package/dist/types/client/state/session-store.d.ts +41 -2
  61. package/dist/types/client/state/workspace-settings.d.ts +7 -0
  62. package/dist/types/client/types.d.ts +13 -1
  63. package/dist/types/client/utils/keyboard.d.ts +2 -0
  64. package/dist/types/client/utils/time-ago.d.ts +4 -0
  65. package/dist/types/mcp/context-uri-template.d.ts +3 -1
  66. package/dist/types/server/agent-presence.d.ts +10 -3
  67. package/dist/types/server/ax-context.d.ts +1 -1
  68. package/dist/types/server/canvas-db.d.ts +30 -7
  69. package/dist/types/server/canvas-operations.d.ts +24 -2
  70. package/dist/types/server/canvas-provenance.d.ts +22 -2
  71. package/dist/types/server/canvas-serialization.d.ts +2 -0
  72. package/dist/types/server/canvas-state.d.ts +93 -15
  73. package/dist/types/server/canvas-validation.d.ts +1 -1
  74. package/dist/types/server/context-brief.d.ts +94 -6
  75. package/dist/types/server/context-reads.d.ts +33 -7
  76. package/dist/types/server/edit-summary.d.ts +20 -0
  77. package/dist/types/server/index.d.ts +71 -10
  78. package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
  79. package/dist/types/server/lifecycle-status.d.ts +10 -0
  80. package/dist/types/server/memory-graph.d.ts +9 -0
  81. package/dist/types/server/mutation-history.d.ts +2 -0
  82. package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
  83. package/dist/types/server/operations/ops/boards.d.ts +2 -1
  84. package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
  85. package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
  86. package/dist/types/server/operations/ops/nodes.d.ts +5 -0
  87. package/dist/types/server/operations/ops/query.d.ts +23 -0
  88. package/dist/types/server/operations/registry.d.ts +1 -0
  89. package/dist/types/server/placement.d.ts +7 -0
  90. package/dist/types/server/scope-fence.d.ts +2 -1
  91. package/dist/types/server/spatial-analysis.d.ts +6 -3
  92. package/dist/types/server/tour-control.d.ts +32 -0
  93. package/dist/types/server/wiki-links.d.ts +14 -0
  94. package/dist/types/server/workbench-auth.d.ts +2 -0
  95. package/dist/types/shared/agent-context.d.ts +2 -0
  96. package/dist/types/shared/agent-presence.d.ts +15 -12
  97. package/dist/types/shared/auto-arrange.d.ts +1 -0
  98. package/dist/types/shared/boards.d.ts +4 -4
  99. package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
  100. package/dist/types/shared/knowledge-presets.d.ts +6 -0
  101. package/dist/types/shared/lifecycle-status.d.ts +24 -0
  102. package/dist/types/shared/memory-graph.d.ts +53 -0
  103. package/dist/types/shared/relations.d.ts +68 -0
  104. package/dist/types/shared/semantic-attention.d.ts +2 -0
  105. package/dist/types/shared/tour.d.ts +38 -0
  106. package/dist/types/shared/workspace-settings.d.ts +11 -0
  107. package/docs/RELEASE.md +21 -3
  108. package/docs/adr-001-bun-only-runtime.md +7 -1
  109. package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
  110. package/docs/api-reference.md +109 -0
  111. package/docs/ax-host-adapter-contract.md +1 -1
  112. package/docs/ax-state-contract.md +5 -4
  113. package/docs/cli.md +153 -15
  114. package/docs/demos/pmx-canvas-demo.mp4 +0 -0
  115. package/docs/design/AgentContext.dc.html +3 -3
  116. package/docs/design/BoardPins.dc.html +37 -22
  117. package/docs/design/Categories.dc.html +208 -0
  118. package/docs/design/CategoryEditor.dc.html +33 -0
  119. package/docs/design/Chrome.dc.html +55 -9
  120. package/docs/design/ContextChip.dc.html +224 -0
  121. package/docs/design/ContextChip600.dc.html +136 -0
  122. package/docs/design/EmptyState.dc.html +45 -0
  123. package/docs/design/Flows.dc.html +23 -1
  124. package/docs/design/HeaderMarks.dc.html +183 -0
  125. package/docs/design/Home.dc.html +19 -1
  126. package/docs/design/HomeGraph.dc.html +86 -202
  127. package/docs/design/HomeMap.dc.html +23 -1
  128. package/docs/design/HomeTeam.dc.html +32 -14
  129. package/docs/design/Lifecycle.dc.html +22 -0
  130. package/docs/design/Lifecycle600.dc.html +18 -0
  131. package/docs/design/LifecycleVariants.dc.html +157 -0
  132. package/docs/design/LinkAuthorship.dc.html +139 -0
  133. package/docs/design/LinksChanged.dc.html +203 -0
  134. package/docs/design/LinksOptions.dc.html +137 -0
  135. package/docs/design/LinksOptions600.dc.html +134 -0
  136. package/docs/design/NearPin.dc.html +9 -10
  137. package/docs/design/Pane600.dc.html +4 -3
  138. package/docs/design/Relations.dc.html +3 -2
  139. package/docs/design/Restore.dc.html +152 -0
  140. package/docs/design/SeeChange.dc.html +267 -0
  141. package/docs/design/Uses.dc.html +12 -6
  142. package/docs/design/canvas.json +146 -19
  143. package/docs/design.md +454 -18
  144. package/docs/environment.md +0 -1
  145. package/docs/evals/curation-effect.md +128 -0
  146. package/docs/http-api.md +465 -38
  147. package/docs/mcp.md +150 -19
  148. package/docs/node-types.md +58 -3
  149. package/docs/plans/design-gaps-2026-10-08.md +53 -0
  150. package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
  151. package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
  152. package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
  153. package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
  154. package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
  155. package/docs/product-context-vision-2026-09.md +17 -1
  156. package/docs/product-vision-2026-09.md +1082 -43
  157. package/docs/product-vision-review-2026-09.md +13 -1
  158. package/docs/screenshots/workbench-live-dark.png +0 -0
  159. package/docs/screenshots/workbench-live-light.png +0 -0
  160. package/docs/sdk.md +62 -2
  161. package/package.json +1 -1
  162. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  163. package/plugins/pmx-canvas/plugin.json +1 -1
  164. package/plugins/pmx-canvas/runtime.json +1 -1
  165. package/plugins/pmx-canvas/tools.json +601 -22
  166. package/skills/pmx-canvas/SKILL.md +234 -43
  167. package/skills/pmx-canvas/evals/evals.json +62 -0
  168. package/skills/pmx-canvas/references/ax-html-control-surface.md +6 -0
  169. package/skills/pmx-canvas/references/full-reference.md +120 -21
  170. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
  171. package/skills/pmx-canvas/references/importing-context.md +132 -0
  172. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  173. package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
  174. package/skills/pmx-canvas-testing/SKILL.md +20 -0
  175. package/src/cli/agent.ts +2 -0
  176. package/src/cli/commands/ax.ts +12 -0
  177. package/src/cli/commands/boards.ts +87 -11
  178. package/src/cli/commands/edges.ts +28 -0
  179. package/src/cli/commands/groups.ts +7 -0
  180. package/src/cli/commands/nodes.ts +4 -2
  181. package/src/cli/commands/query.ts +34 -2
  182. package/src/cli/commands/record.ts +199 -48
  183. package/src/cli/index.ts +7 -1
  184. package/src/client/App.tsx +177 -70
  185. package/src/client/canvas/AttentionHistory.tsx +0 -2
  186. package/src/client/canvas/BarHint.tsx +40 -2
  187. package/src/client/canvas/BoardCopyDialog.tsx +1 -1
  188. package/src/client/canvas/BoardFolderDialog.tsx +7 -7
  189. package/src/client/canvas/BoardLinkDialog.tsx +138 -33
  190. package/src/client/canvas/BoardPinButton.tsx +33 -0
  191. package/src/client/canvas/BoardSwitcher.tsx +26 -40
  192. package/src/client/canvas/CanvasNode.tsx +360 -237
  193. package/src/client/canvas/CanvasViewport.tsx +45 -7
  194. package/src/client/canvas/CommandBar.tsx +29 -23
  195. package/src/client/canvas/CommandPalette.tsx +5 -12
  196. package/src/client/canvas/ConnectionBanner.tsx +3 -3
  197. package/src/client/canvas/ContextChip.tsx +296 -0
  198. package/src/client/canvas/ContextMenu.tsx +63 -5
  199. package/src/client/canvas/CrossBoardReferences.tsx +60 -0
  200. package/src/client/canvas/EdgeLayer.tsx +339 -76
  201. package/src/client/canvas/EmptyState.tsx +33 -18
  202. package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
  203. package/src/client/canvas/ExternalWriters.tsx +57 -42
  204. package/src/client/canvas/HomeGraphView.tsx +942 -0
  205. package/src/client/canvas/HomeMapView.tsx +641 -0
  206. package/src/client/canvas/HomeReviewView.tsx +167 -0
  207. package/src/client/canvas/HomeView.tsx +687 -160
  208. package/src/client/canvas/IntentLayer.tsx +19 -12
  209. package/src/client/canvas/KnowledgeDialog.tsx +488 -0
  210. package/src/client/canvas/LifecycleChrome.tsx +133 -0
  211. package/src/client/canvas/LifecycleFields.tsx +91 -0
  212. package/src/client/canvas/LifecyclePanel.tsx +533 -0
  213. package/src/client/canvas/MemoryInspector.tsx +104 -0
  214. package/src/client/canvas/Minimap.tsx +3 -3
  215. package/src/client/canvas/NearPinLayer.tsx +187 -0
  216. package/src/client/canvas/NodeContextMark.tsx +175 -97
  217. package/src/client/canvas/Presentation.tsx +23 -57
  218. package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
  219. package/src/client/canvas/RelationsPanel.tsx +82 -0
  220. package/src/client/canvas/SelectionBar.tsx +3 -6
  221. package/src/client/canvas/SessionPanel.tsx +90 -50
  222. package/src/client/canvas/SessionReceipt.tsx +311 -39
  223. package/src/client/canvas/ShortcutOverlay.tsx +1 -1
  224. package/src/client/canvas/SnapshotPanel.tsx +27 -36
  225. package/src/client/canvas/ToolRail.tsx +367 -192
  226. package/src/client/canvas/TopBar.tsx +354 -124
  227. package/src/client/canvas/canvas-area.ts +10 -3
  228. package/src/client/canvas/graph-geometry.ts +195 -0
  229. package/src/client/canvas/graph-model.ts +254 -0
  230. package/src/client/canvas/memory-layout.ts +106 -0
  231. package/src/client/canvas/use-chrome-height.ts +20 -0
  232. package/src/client/canvas/use-focus-trap.ts +31 -5
  233. package/src/client/canvas/use-memory-graph.ts +58 -0
  234. package/src/client/canvas/use-node-drag.ts +2 -1
  235. package/src/client/canvas/use-pan-zoom.ts +64 -7
  236. package/src/client/icons.tsx +0 -63
  237. package/src/client/mcp-app/style.css +0 -6
  238. package/src/client/nodes/AxStepControls.tsx +74 -50
  239. package/src/client/nodes/BoardNode.tsx +5 -1
  240. package/src/client/nodes/ContextNode.tsx +18 -11
  241. package/src/client/nodes/ExtAppFrame.tsx +0 -1
  242. package/src/client/nodes/FileNode.tsx +46 -42
  243. package/src/client/nodes/HtmlNode.tsx +44 -2
  244. package/src/client/nodes/ImageNode.tsx +1 -1
  245. package/src/client/nodes/InlineFormatBar.tsx +6 -9
  246. package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
  247. package/src/client/nodes/MarkdownNode.tsx +377 -109
  248. package/src/client/nodes/McpAppNode.tsx +50 -20
  249. package/src/client/nodes/MdFormatBar.tsx +21 -18
  250. package/src/client/nodes/PromptNode.tsx +1 -1
  251. package/src/client/nodes/ResponseNode.tsx +1 -1
  252. package/src/client/nodes/viewer-runtime-state.ts +54 -22
  253. package/src/client/state/attention-bridge.ts +64 -15
  254. package/src/client/state/boards-store.ts +193 -39
  255. package/src/client/state/canvas-store.ts +99 -12
  256. package/src/client/state/context-chip-store.ts +70 -0
  257. package/src/client/state/context-status-store.ts +53 -0
  258. package/src/client/state/intent-bridge.ts +51 -30
  259. package/src/client/state/knowledge-store.ts +43 -0
  260. package/src/client/state/near-pin-store.ts +115 -6
  261. package/src/client/state/presence-store.ts +7 -10
  262. package/src/client/state/presentation.ts +91 -0
  263. package/src/client/state/relation-store.ts +81 -0
  264. package/src/client/state/session-store.ts +144 -18
  265. package/src/client/state/sse-bridge.ts +206 -29
  266. package/src/client/state/workspace-settings.ts +11 -0
  267. package/src/client/theme/global.css +2759 -510
  268. package/src/client/types.ts +12 -0
  269. package/src/client/utils/keyboard.ts +10 -0
  270. package/src/client/utils/time-ago.ts +14 -0
  271. package/src/json-render/renderer/index.css +6 -1
  272. package/src/json-render/renderer/index.tsx +31 -1
  273. package/src/mcp/canvas-app.ts +12 -2
  274. package/src/mcp/context-uri-template.ts +7 -4
  275. package/src/mcp/server.ts +63 -41
  276. package/src/mcp/workbench-app.ts +39 -6
  277. package/src/server/agent-presence.ts +128 -28
  278. package/src/server/ax-context.ts +27 -13
  279. package/src/server/board-export.ts +12 -1
  280. package/src/server/canvas-db.ts +331 -42
  281. package/src/server/canvas-operations.ts +196 -84
  282. package/src/server/canvas-provenance.ts +53 -22
  283. package/src/server/canvas-serialization.ts +2 -0
  284. package/src/server/canvas-state.ts +950 -122
  285. package/src/server/canvas-validation.ts +9 -2
  286. package/src/server/context-brief.ts +345 -38
  287. package/src/server/context-reads.ts +179 -30
  288. package/src/server/demo-state.json +355 -24
  289. package/src/server/edit-summary.ts +74 -0
  290. package/src/server/html-surface.ts +8 -1
  291. package/src/server/index.ts +203 -13
  292. package/src/server/knowledge-lifecycle.ts +304 -0
  293. package/src/server/lifecycle-status.ts +232 -0
  294. package/src/server/memory-graph.ts +166 -0
  295. package/src/server/mutation-history.ts +18 -6
  296. package/src/server/operations/composites.ts +32 -11
  297. package/src/server/operations/http.ts +7 -2
  298. package/src/server/operations/index.ts +4 -0
  299. package/src/server/operations/mcp.ts +32 -1
  300. package/src/server/operations/ops/ax-presence.ts +1 -1
  301. package/src/server/operations/ops/ax-read.ts +57 -6
  302. package/src/server/operations/ops/ax-timeline.ts +28 -2
  303. package/src/server/operations/ops/backup.ts +5 -3
  304. package/src/server/operations/ops/batch.ts +35 -4
  305. package/src/server/operations/ops/boards.ts +188 -47
  306. package/src/server/operations/ops/canvas-wire.ts +31 -1
  307. package/src/server/operations/ops/edges.ts +36 -2
  308. package/src/server/operations/ops/groups.ts +6 -1
  309. package/src/server/operations/ops/json-render.ts +5 -1
  310. package/src/server/operations/ops/lifecycle.ts +262 -0
  311. package/src/server/operations/ops/memory-graph.ts +45 -0
  312. package/src/server/operations/ops/nodes.ts +107 -24
  313. package/src/server/operations/ops/query.ts +281 -68
  314. package/src/server/operations/ops/snapshots.ts +73 -1
  315. package/src/server/operations/ops/validate.ts +5 -1
  316. package/src/server/operations/ops/viewport.ts +97 -3
  317. package/src/server/operations/registry.ts +77 -26
  318. package/src/server/placement.ts +29 -0
  319. package/src/server/scope-fence.ts +78 -6
  320. package/src/server/server.ts +115 -16
  321. package/src/server/spatial-analysis.ts +48 -23
  322. package/src/server/tour-control.ts +82 -0
  323. package/src/server/wiki-links.ts +137 -0
  324. package/src/server/workbench-auth.ts +11 -0
  325. package/src/shared/agent-context.ts +3 -0
  326. package/src/shared/agent-presence.ts +15 -15
  327. package/src/shared/auto-arrange.ts +2 -0
  328. package/src/shared/boards.ts +4 -4
  329. package/src/shared/knowledge-lifecycle.ts +22 -0
  330. package/src/shared/knowledge-presets.ts +80 -0
  331. package/src/shared/lifecycle-status.ts +26 -0
  332. package/src/shared/memory-graph.ts +74 -0
  333. package/src/shared/relations.ts +98 -0
  334. package/src/shared/semantic-attention.ts +15 -1
  335. package/src/shared/tour.ts +23 -5
  336. package/src/shared/workspace-settings.ts +13 -0
  337. package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
  338. package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
  339. package/dist/types/client/canvas/kind-colors.d.ts +0 -3
  340. package/dist/types/server/agent-context.d.ts +0 -22
  341. package/src/client/canvas/ContextPinBar.tsx +0 -25
@@ -0,0 +1,166 @@
1
+ import { canvasState } from './canvas-state.js';
2
+ import { summarizeNodeForAgentContext } from '../shared/agent-context.js';
3
+ import {
4
+ memoryKey,
5
+ wikiLinksFromData,
6
+ type MemoryGraph,
7
+ type MemoryGraphEdge,
8
+ type MemoryGraphNode,
9
+ type MemoryTarget,
10
+ } from '../shared/memory-graph.js';
11
+
12
+ /** One library projection for Home, backlinks, brief discovery and agent exploration. No writes. */
13
+ export function memoryGraph(projection: 'full' | 'boards' = 'full'): MemoryGraph {
14
+ const boards = canvasState.listBoards();
15
+ const reads = boards.map((board) => ({ board, read: canvasState.readBoard(board.id, false) }));
16
+ const keys = new Set<string>();
17
+ const nodes: MemoryGraphNode[] = [];
18
+ const edges: MemoryGraphEdge[] = [];
19
+ const unresolvedLinks: MemoryGraph['unresolvedLinks'] = [];
20
+ const add = (
21
+ kind: MemoryGraphEdge['kind'],
22
+ id: string,
23
+ source: MemoryTarget,
24
+ target: MemoryTarget,
25
+ label?: string,
26
+ type?: string,
27
+ relationKind?: string,
28
+ reason?: string,
29
+ ) => {
30
+ edges.push({
31
+ id,
32
+ kind,
33
+ from: memoryKey(source),
34
+ to: memoryKey(target),
35
+ source,
36
+ target,
37
+ missing: false,
38
+ ...(label ? { label } : {}),
39
+ ...(type ? { type } : {}),
40
+ ...(relationKind ? { relationKind } : {}),
41
+ ...(reason ? { reason } : {}),
42
+ });
43
+ };
44
+ for (const { board, read } of reads) {
45
+ const readme = read?.layout.nodes.find((node) => node.id === board.readmeNodeId);
46
+ const summary = (node: NonNullable<typeof read>['layout']['nodes'][number]) =>
47
+ summarizeNodeForAgentContext(node, { defaultTextLength: 280, webpageTextLength: 280 }).slice(0, 280);
48
+ nodes.push({
49
+ key: memoryKey({ boardId: board.id }),
50
+ boardId: board.id,
51
+ kind: 'board',
52
+ title: board.name,
53
+ summary: readme ? summary(readme) : '',
54
+ folder: board.folder,
55
+ inContext: !!board.pin,
56
+ summaryOnly: true,
57
+ nodeCount: board.nodeCount,
58
+ ...(board.mapPosition ? { position: board.mapPosition } : {}),
59
+ });
60
+ keys.add(memoryKey({ boardId: board.id }));
61
+ if (board.previousBoardId)
62
+ add(
63
+ 'previous',
64
+ `previous:${board.id}`,
65
+ { boardId: board.id },
66
+ { boardId: board.previousBoardId },
67
+ 'previous board',
68
+ );
69
+ for (const node of read?.layout.nodes ?? []) {
70
+ const source = { boardId: board.id, cardId: node.id };
71
+ keys.add(memoryKey(source));
72
+ if (projection === 'full')
73
+ nodes.push({
74
+ ...source,
75
+ key: memoryKey(source),
76
+ kind: 'card',
77
+ title: typeof node.data.title === 'string' ? node.data.title : node.id,
78
+ summary: summary(node),
79
+ folder: board.folder,
80
+ type: node.type,
81
+ summaryOnly: true,
82
+ linksRevision: node.linksRevision ?? 0,
83
+ inContext: !!board.pin || !!read?.state.contextPins.includes(node.id),
84
+ });
85
+ if (projection === 'full') add('member', `member:${memoryKey(source)}`, { boardId: board.id }, source);
86
+ if (node.type === 'board' && typeof node.data.boardId === 'string') {
87
+ add('portal', `portal:${memoryKey(source)}`, source, { boardId: node.data.boardId });
88
+ }
89
+ for (const link of node.type === 'markdown' ? wikiLinksFromData(node.data) : []) {
90
+ if (link.target) add('wiki', `wiki:${memoryKey(source)}:${link.ref}`, source, link.target, link.ref);
91
+ else unresolvedLinks.push({ source, ref: link.ref });
92
+ }
93
+ }
94
+ for (const edge of read?.layout.edges ?? []) {
95
+ add(
96
+ 'card',
97
+ `card:${board.id}:${edge.id}`,
98
+ { boardId: board.id, cardId: edge.from },
99
+ { boardId: edge.toBoardId ?? board.id, cardId: edge.to },
100
+ edge.label,
101
+ edge.type,
102
+ edge.kind,
103
+ edge.reason,
104
+ );
105
+ }
106
+ }
107
+ for (const edge of edges) edge.missing = !keys.has(edge.from) || !keys.has(edge.to);
108
+ const graph = { nodes, edges, unresolvedLinks, focus: null, depth: 2 };
109
+ return projection === 'boards' ? boardMemoryGraph(graph) : graph;
110
+ }
111
+
112
+ /** Keep original qualified endpoints and missing evidence, without card summaries or membership. */
113
+ export function boardMemoryGraph(graph: MemoryGraph): MemoryGraph {
114
+ return {
115
+ ...graph,
116
+ nodes: graph.nodes.filter((node) => node.kind === 'board'),
117
+ edges: graph.edges.filter(
118
+ (edge) => edge.kind !== 'member' && (edge.missing || edge.source.boardId !== edge.target.boardId),
119
+ ),
120
+ };
121
+ }
122
+
123
+ /** Board-level connections are a collapse of the SAME edges, not another link scanner. */
124
+ export function boardConnections(graph: MemoryGraph): MemoryGraphEdge[] {
125
+ return graph.edges.filter((edge) => edge.kind !== 'member' && edge.source.boardId !== edge.target.boardId);
126
+ }
127
+
128
+ /** Undirected exploration. Membership is a hop too; focus accepts a board ID, qualified key, or pins. */
129
+ export function localMemoryGraph(graph: MemoryGraph, focus: string | null, depth: number): MemoryGraph {
130
+ if (!focus) return { ...graph, depth };
131
+ const seed =
132
+ focus === 'pins'
133
+ ? graph.nodes.filter((node) => node.inContext).map((node) => node.key)
134
+ : graph.nodes
135
+ .filter((node) => node.key === focus || (node.kind === 'board' && node.boardId === focus))
136
+ .map((node) => node.key);
137
+ const selected = new Set(seed);
138
+ let frontier = new Set(seed);
139
+ for (let level = 0; level < depth; level++) {
140
+ const next = new Set<string>();
141
+ for (const edge of graph.edges) {
142
+ if (edge.missing) continue;
143
+ if (frontier.has(edge.from) && !selected.has(edge.to)) next.add(edge.to);
144
+ if (frontier.has(edge.to) && !selected.has(edge.from)) next.add(edge.from);
145
+ }
146
+ for (const key of next) selected.add(key);
147
+ frontier = next;
148
+ }
149
+ const edges = graph.edges.filter((edge) => selected.has(edge.from) && (selected.has(edge.to) || edge.missing));
150
+ const deliveredEdges = new Set(edges.map((edge) => edge.id));
151
+ return {
152
+ nodes: graph.nodes
153
+ .filter((node) => selected.has(node.key))
154
+ .map((node) => ({
155
+ ...node,
156
+ linksComplete: graph.edges.every(
157
+ (edge) =>
158
+ edge.kind === 'member' || (edge.from !== node.key && edge.to !== node.key) || deliveredEdges.has(edge.id),
159
+ ),
160
+ })),
161
+ edges,
162
+ unresolvedLinks: graph.unresolvedLinks.filter((link) => selected.has(memoryKey(link.source))),
163
+ focus,
164
+ depth,
165
+ };
166
+ }
@@ -84,8 +84,8 @@ export interface SnapshotDiffResult {
84
84
  title: string | null;
85
85
  changes: string[];
86
86
  }[];
87
- addedEdges: { id: string; from: string; to: string; type: string }[];
88
- removedEdges: { id: string; from: string; to: string; type: string }[];
87
+ addedEdges: { id: string; from: string; to: string; toBoardId?: string; type: string }[];
88
+ removedEdges: { id: string; from: string; to: string; toBoardId?: string; type: string }[];
89
89
  }
90
90
 
91
91
  function comparableNodeData(data: Record<string, unknown>): Record<string, unknown> {
@@ -335,12 +335,24 @@ export function diffLayouts(
335
335
 
336
336
  for (const [id, edge] of curEdges) {
337
337
  if (!snapEdges.has(id)) {
338
- addedEdges.push({ id, from: edge.from, to: edge.to, type: edge.type });
338
+ addedEdges.push({
339
+ id,
340
+ from: edge.from,
341
+ to: edge.to,
342
+ ...(edge.toBoardId !== undefined ? { toBoardId: edge.toBoardId } : {}),
343
+ type: edge.type,
344
+ });
339
345
  }
340
346
  }
341
347
  for (const [id, edge] of snapEdges) {
342
348
  if (!curEdges.has(id)) {
343
- removedEdges.push({ id, from: edge.from, to: edge.to, type: edge.type });
349
+ removedEdges.push({
350
+ id,
351
+ from: edge.from,
352
+ to: edge.to,
353
+ ...(edge.toBoardId !== undefined ? { toBoardId: edge.toBoardId } : {}),
354
+ type: edge.type,
355
+ });
344
356
  }
345
357
  }
346
358
 
@@ -395,7 +407,7 @@ export function formatDiff(diff: SnapshotDiffResult): string {
395
407
  if (diff.addedEdges.length > 0) {
396
408
  lines.push(`Added edges (${diff.addedEdges.length}):`);
397
409
  for (const e of diff.addedEdges) {
398
- lines.push(` + ${e.type}: ${e.from} → ${e.to}`);
410
+ lines.push(` + ${e.type}: ${e.from} → ${e.toBoardId ? `${e.toBoardId}/` : ''}${e.to}`);
399
411
  }
400
412
  lines.push('');
401
413
  }
@@ -403,7 +415,7 @@ export function formatDiff(diff: SnapshotDiffResult): string {
403
415
  if (diff.removedEdges.length > 0) {
404
416
  lines.push(`Removed edges (${diff.removedEdges.length}):`);
405
417
  for (const e of diff.removedEdges) {
406
- lines.push(` - ${e.type}: ${e.from} → ${e.to}`);
418
+ lines.push(` - ${e.type}: ${e.from} → ${e.toBoardId ? `${e.toBoardId}/` : ''}${e.to}`);
407
419
  }
408
420
  lines.push('');
409
421
  }
@@ -137,12 +137,13 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
137
137
  // compositeFoldedOpNames and frees the name in the same change.
138
138
  toolName: 'canvas_snapshot',
139
139
  description:
140
- 'Canvas snapshots: save the current state, list saved snapshots, restore one, delete one, garbage-collect old ones, or diff the live canvas against one. Action "save" stores a named snapshot; "list" returns saved snapshots (newest first); "restore" replaces the live canvas with a snapshot; "delete" removes one; "gc" keeps the newest N and deletes the rest (dryRun to preview); "diff" shows added/removed/modified nodes and edges versus a snapshot (by name or id).',
141
- actionSummary: 'save | list | restore | delete | gc | diff',
140
+ 'Canvas snapshots: save the current state, list saved snapshots, restore one, delete one, garbage-collect old ones, or diff the live canvas against one. Action "save" stores a named snapshot; "list" returns saved snapshots (newest first); "restore" replaces the live canvas with a snapshot; "delete" removes one; "gc" keeps the newest N and deletes the rest (dryRun to preview); "diff" shows added/removed/modified nodes and edges versus a snapshot (by name or id); "restore-node" puts one card (nodeId) back to its content in a snapshot, leaving the rest of the board as it is.',
141
+ actionSummary: 'save | list | restore | restore-node | delete | gc | diff',
142
142
  actions: {
143
143
  save: 'snapshot.save',
144
144
  list: 'snapshot.list',
145
145
  restore: 'snapshot.restore',
146
+ 'restore-node': 'snapshot.restore-node',
146
147
  delete: 'snapshot.delete',
147
148
  gc: 'snapshot.gc',
148
149
  diff: 'snapshot.diff',
@@ -151,8 +152,10 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
151
152
  {
152
153
  toolName: 'canvas_board',
153
154
  description:
154
- 'Boards in this workspace — the library of the human\'s and your working memory, filed in categories on Home. Your writes always go to the open board (activeBoardId); on Home (no board open) your first write opens the most recent board, or a new one when there are none. Action "list" returns every board (with its category), most recently opened first; "get" reads one (default: the open board); "create" makes a new empty board WITHOUT opening it (optional category); "open" explicitly switches the shared workbench to id (null or omitted opens Home), changing the visible board and subsequent write target. After creating a board, open its returned id and verify activeBoardId before writing; another participant can switch the board later. "update" renames a board, files it under a category (category "" removes it), or designates its README via readmeNodeId. As part of normal board authoring, read the board metadata; if readmeNodeId is null, choose or create a concise markdown introduction explaining its purpose and key context, then designate that node with "update". Preserve an existing README unless the human asks to change it; humans can override the designation manually. Only the human deletes boards.',
155
- actionSummary: 'list | get | create | create-from | open | update',
155
+ 'Knowledge data: knowledge-list reads categories/folders/shared relation presets; category-set replaces category (id/name/glyph/decayType/rate/triggers/revalidationMethod); category-remove deletes an unused category by id (explicit folder assignments, including empty folders, refuse deletion); relation-preset-set saves preset (id/name/vocabulary); relation-preset-remove deletes a template without changing existing board vocabularies; folder-set replaces path/categoryId/lifecycleOverride (null inherits). These workspace actions never open a board and are refused under agent scope fences. ' +
156
+ 'Boards in this workspace — the library of the human\'s and your working memory, filed in folders on Home. Your writes always go to the open board (activeBoardId); on Home (no board open) your first write opens the most recent board, or a new one when there are none. Action "list" returns every board (with its folder), most recently opened first; "get" reads one (default: the open board); "create" makes a new empty board WITHOUT opening it (optional folder); "open" explicitly switches the server active board to id (null or omitted opens Home), changing the subsequent write target. Humans browsing Map/Graph stay in the library until they explicitly open a board/card; do not infer activeBoardId from their visible library. After creating a board, open its returned id and verify activeBoardId before writing; another participant can switch the board later. "update" renames a board, files it under a folder (folder "" removes it), or designates its README via readmeNodeId. As part of normal board authoring, read the board metadata; if readmeNodeId is null, choose or create a concise markdown introduction explaining its purpose and key context, then designate that node with "update". Preserve an existing README unless the human asks to change it; humans can override the designation manually. "pin" adds a board to the working set (its README and pinned cards reach your brief whichever board is open; optional reason), "unpin" removes it; a pinned board is read-only context, never a write target. Only the human deletes boards.',
157
+ actionSummary:
158
+ 'list | get | create | create-from | open | update | pin | unpin | knowledge-list | category-set | category-remove | relation-preset-set | relation-preset-remove | folder-set',
156
159
  actions: {
157
160
  list: 'board.list',
158
161
  get: 'board.get',
@@ -160,18 +163,31 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
160
163
  'create-from': 'board.create-from',
161
164
  open: 'board.open',
162
165
  update: 'board.update',
166
+ 'knowledge-list': 'knowledge.list',
167
+ 'category-set': 'knowledge.category-set',
168
+ 'category-remove': 'knowledge.category-remove',
169
+ 'relation-preset-set': 'knowledge.relation-preset-set',
170
+ 'relation-preset-remove': 'knowledge.relation-preset-remove',
171
+ 'folder-set': 'knowledge.folder-set',
172
+ pin: 'board.pin',
173
+ unpin: 'board.unpin',
163
174
  },
164
175
  },
165
176
  {
166
177
  toolName: 'canvas_node',
167
178
  description:
168
- 'Create, read, update, or remove a canvas node. One tool for node CRUD: action "add" creates a node (requires type — markdown, status, context, ledger, trace, file, image, webpage, html, group, etc.); "get" reads one node by id; "update" patches an existing node (title, content, position, size, data); "remove" deletes a node by id. For spec-driven content (json-render, graph) use canvas_render; for external/built apps use canvas_app (actions: open-mcp-app, diagram, build-artifact).',
169
- actionSummary: 'add | get | update | remove',
179
+ 'Lifecycle data: lifecycle-get reads effective inheritance, due/source qualification and review history (optional board); lifecycle-set sets full lifecycleOverride or null; review requires outcome retain/revise/externalize/retire/unresolved, evidence and applied. Applied retain/revise/externalize with nonempty evidence also requires expectedContentRevision and expectedSources from the content and sources actually checked; stale reviews fail. Records keep those observations and persist content before evidence. lifecycle-restore requires evidence and explicitly restores retirement, never validation. Retired cards are excluded from active briefs by default. Writes accept expectedBoardId and obey edit locks/scope. ' +
180
+ 'Create, read, update, or remove a canvas node. One tool for node CRUD: action "add" creates a node (requires type — markdown, board with data.boardId for a portal, status, context, ledger, trace, file, image, webpage, html, group, etc.); "get" reads one node by id; "update" patches an existing node (title, content, position, size, data); "remove" deletes a node by id. For spec-driven content (json-render, graph) use canvas_render; for external/built apps use canvas_app (actions: open-mcp-app, diagram, build-artifact).',
181
+ actionSummary: 'add | get | update | remove | lifecycle-get | lifecycle-set | review | lifecycle-restore',
170
182
  actions: {
171
183
  add: 'node.add',
172
184
  get: 'node.get',
173
185
  update: 'node.update',
174
186
  remove: 'node.remove',
187
+ 'lifecycle-get': 'node.lifecycle-get',
188
+ 'lifecycle-set': 'node.lifecycle-set',
189
+ review: 'node.review',
190
+ 'lifecycle-restore': 'node.lifecycle-restore',
175
191
  },
176
192
  },
177
193
  {
@@ -223,11 +239,15 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
223
239
  {
224
240
  toolName: 'canvas_view',
225
241
  description:
226
- 'Canvas viewport and layout control. Action "arrange" auto-lays-out nodes (grid/columns/etc.); "focus" pans/zooms the viewport to a node; "fit" zooms to fit all nodes in view; "clear" removes every node and edge from the canvas; "remove-annotation" deletes a human-drawn annotation by id.',
227
- actionSummary: 'arrange | focus | fit | clear | remove-annotation | get-tour | set-tour',
242
+ 'Canvas viewport and layout control. Action "arrange" auto-lays-out nodes (grid/columns/etc.); "focus" pans/zooms the viewport to a node; "fit" zooms to fit all nodes in view; "clear" removes every node and edge from the canvas; "remove-annotation" deletes a human-drawn annotation by id; "get-tour"/"set-tour" read or persist the presentation tour; "tour-go" presents a stop (stop index or step next/previous); "tour-exit" leaves presentation; "move" eases the camera to a nodeId, world rect or viewport over a duration. Camera actions never enter undo history.',
243
+ actionSummary:
244
+ 'arrange | focus | fit | clear | remove-annotation | get-tour | set-tour | tour-go | tour-exit | move',
228
245
  actions: {
229
246
  'get-tour': 'tour.get',
230
247
  'set-tour': 'tour.set',
248
+ 'tour-go': 'tour.go',
249
+ 'tour-exit': 'tour.exit',
250
+ move: 'camera.move',
231
251
  arrange: 'arrange',
232
252
  focus: 'node.focus',
233
253
  fit: 'view.fit',
@@ -238,10 +258,11 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
238
258
  {
239
259
  toolName: 'canvas_query',
240
260
  description:
241
- 'Read the board cheapest-first. Action "context" returns a budgeted cross-board brief with a durable consumer cursor (budget is UTF-16 characters); "search" finds nodes by title/content keywords; "layout" returns the full layout; "validate" checks geometry and endpoints.',
242
- actionSummary: 'context | search | layout | validate',
261
+ 'Read the board cheapest-first. Action "context" returns a budgeted cross-board brief with a durable consumer cursor (budget is UTF-16 characters); "graph" explores the library relation graph (focus: board ID, qualified node key, or pins; depth: 0–4), summaries only; "search" finds nodes by title/content keywords; "layout" returns the full layout; "validate" checks geometry and endpoints.',
262
+ actionSummary: 'context | graph | search | layout | validate',
243
263
  actions: {
244
264
  context: 'context.get',
265
+ graph: 'memory-graph.get',
245
266
  search: 'search',
246
267
  layout: 'layout.get',
247
268
  validate: 'validate.get',
@@ -275,7 +296,7 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
275
296
  {
276
297
  toolName: 'canvas_ax_state',
277
298
  description:
278
- 'Read or set host-agnostic PMX AX state. Action "get" reads the AX state + agent-ready context (pinned/focused context, focus field); "set-focus" places node IDs in the AX focus field (nodeIds); "set-policy" patches the tool/prompt policy (tools.allowed|excluded|approvalRequired, prompt.systemAppend|mode — merges with existing); "report-capability" records a host/session capability for diagnostics; "presence" reads agent presence (who is writing, phase, attached session, context budget); "set-presence" updates this caller\'s presence (phase idle|thinking|tooling|waiting-approval, detail, focusNodeId, cursor, attached — attached:true starts a session, false ends it; contextUsage {used,total} reports the agent\'s real token window so the board\'s meter shows it instead of the pinned-context estimate). Adapters may pass a source label (e.g. codex).',
299
+ 'Read or set host-agnostic PMX AX state. Action "get" reads the AX state + agent-ready context (pinned/focused context, focus field); "set-focus" places node IDs in the AX focus field (nodeIds); "set-policy" patches the tool/prompt policy (tools.allowed|excluded|approvalRequired, prompt.systemAppend|mode — merges with existing); "report-capability" records a host/session capability for diagnostics; "presence" reads agent presence (who is writing, phase, attached session); "set-presence" updates this caller\'s presence (phase idle|thinking|tooling|waiting-approval, detail, focusNodeId, cursor, attached — attached:true starts a session, false ends it; contextUsage {used,total} reports the agent\'s real token usage, which the board\'s context panel names). Adapters may pass a source label (e.g. codex).',
279
300
  actionSummary: 'get | set-focus | set-policy | report-capability | presence | set-presence',
280
301
  actions: {
281
302
  get: 'ax.get',
@@ -9,7 +9,7 @@
9
9
  import { executeOperation, listOperations } from './registry.js';
10
10
  import { SOURCE_LABEL_RE } from '../agent-presence.js';
11
11
  import { OperationError } from './types.js';
12
- import { isWorkbenchToken } from '../workbench-auth.js';
12
+ import { isWorkbenchToken, refuseStaleWorkbenchWrite } from '../workbench-auth.js';
13
13
 
14
14
  function responseJson(data: unknown, status = 200): Response {
15
15
  return Response.json(data, {
@@ -90,7 +90,12 @@ export async function dispatchOperationRoute(req: Request, url: URL): Promise<Re
90
90
  // the browser is not agent activity, so it never synthesizes an
91
91
  // auto-ghost. Everything else (CLI, scripts, adapters) counts as agent.
92
92
  const workbenchMarked = req.headers.get('x-pmx-workbench') === '1';
93
- const humanAuthor = isWorkbenchToken(req.headers.get('x-pmx-workbench-token'));
93
+ const token = req.headers.get('x-pmx-workbench-token');
94
+ const humanAuthor = isWorkbenchToken(token);
95
+ // Before reconnect delivers the rotated capability, do not silently
96
+ // downgrade an old tab's human save to an agent edit.
97
+ const staleWrite = refuseStaleWorkbenchWrite(req);
98
+ if (staleWrite) return staleWrite;
94
99
  // Presence writer label: adapters/CLI may identify themselves; plain
95
100
  // HTTP callers read as 'api'.
96
101
  const sourceHeader = req.headers.get('x-pmx-source');
@@ -12,6 +12,7 @@ import { validateOperations } from './ops/validate.js';
12
12
  import { annotationOperations } from './ops/annotation.js';
13
13
  import { snapshotOperations } from './ops/snapshots.js';
14
14
  import { boardOperations } from './ops/boards.js';
15
+ import { memoryGraphOperations } from './ops/memory-graph.js';
15
16
  import { backupOperations } from './ops/backup.js';
16
17
  import { exportOperations } from './ops/export.js';
17
18
  import { jsonRenderOperations } from './ops/json-render.js';
@@ -29,8 +30,10 @@ import { axPresenceOperations } from './ops/ax-presence.js';
29
30
  import { humanPresenceOperations } from './ops/human-presence.js';
30
31
  import { canvasWireOperations } from './ops/canvas-wire.js';
31
32
  import { importOperations } from './ops/imports.js';
33
+ import { lifecycleOperations } from './ops/lifecycle.js';
32
34
 
33
35
  for (const op of [
36
+ ...lifecycleOperations,
34
37
  ...nodeOperations,
35
38
  ...edgeOperations,
36
39
  ...viewportOperations,
@@ -40,6 +43,7 @@ for (const op of [
40
43
  ...annotationOperations,
41
44
  ...snapshotOperations,
42
45
  ...boardOperations,
46
+ ...memoryGraphOperations,
43
47
  ...backupOperations,
44
48
  ...exportOperations,
45
49
  ...jsonRenderOperations,
@@ -12,7 +12,14 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
12
  import { getOperation, listOperations } from './registry.js';
13
13
  import { agentSourceLabel, type OperationInvoker } from './invoker.js';
14
14
  import { OperationError, type Operation, type OperationMcpToolHost } from './types.js';
15
- import { CONTEXT_READ_OPS, contextReadFromPayload, type ContextReadInput } from '../context-reads.js';
15
+ import {
16
+ CONTEXT_READ_OPS,
17
+ contextReadFromPayload,
18
+ pinnedBoardReads,
19
+ graphReads,
20
+ type ContextReadInput,
21
+ } from '../context-reads.js';
22
+ import type { MemoryGraph } from '../../shared/memory-graph.js';
16
23
  import { compositeFoldedOpNames, compositeToolDefinitions, type CompositeToolDefinition } from './composites.js';
17
24
 
18
25
  export interface OperationToolHost extends OperationMcpToolHost {
@@ -43,7 +50,21 @@ async function invokeMcpOperation(
43
50
  .filter((item): item is Extract<(typeof formatted.content)[number], { type: 'text' }> => item.type === 'text')
44
51
  .map((item) => item.text)
45
52
  .join('');
53
+ if (op.name === 'node.get') {
54
+ // Compact responses can still deliver text. Metadata without text or
55
+ // data must not mark unseen content read.
56
+ const node = JSON.parse(text) as { data?: unknown; content?: string | null };
57
+ if (node.data === undefined && !node.content) return formatted;
58
+ }
46
59
  try {
60
+ if (op.name === 'memory-graph.get') {
61
+ for (const read of graphReads(
62
+ { channel: 'operation', resource: op.name, source: agentSourceLabel('mcp'), consumer, agentId: null },
63
+ JSON.parse(text) as MemoryGraph,
64
+ ))
65
+ await host.recordContextRead(read);
66
+ return formatted;
67
+ }
47
68
  await host.recordContextRead(
48
69
  contextReadFromPayload(
49
70
  {
@@ -58,6 +79,16 @@ async function invokeMcpOperation(
58
79
  text,
59
80
  ),
60
81
  );
82
+ if (op.name === 'context.get') {
83
+ const base = {
84
+ channel: 'operation' as const,
85
+ resource: op.name,
86
+ source: agentSourceLabel('mcp'),
87
+ consumer,
88
+ agentId: null,
89
+ };
90
+ for (const read of pinnedBoardReads(base, text)) await host.recordContextRead(read);
91
+ }
61
92
  } catch (error) {
62
93
  console.error('[pmx-canvas mcp] recording a context read failed:', error);
63
94
  }
@@ -39,7 +39,7 @@ const presenceSetShape = {
39
39
  'true attaches a session; false detaches it. Read presence first and reuse your host adapter session instead of attaching another. Only detach sessions you own.',
40
40
  ),
41
41
  contextUsage: PRESENCE_SET_SHAPE.contextUsage.describe(
42
- "The agent's real token window as {used,total} — shown on the board's context meter instead of the pinned-context estimate (null clears it)",
42
+ "The agent's real token usage as {used,total} — the board's context panel names how many tokens its whole context holds (null clears it)",
43
43
  ),
44
44
  };
45
45
  const presenceSetSchema = z.looseObject(presenceSetShape);
@@ -13,7 +13,11 @@
13
13
  */
14
14
  import { agentPresence } from '../../agent-presence.js';
15
15
  import { z } from 'zod';
16
- import { buildAgentContextPreamble, serializeNodeForAgentContext } from '../../../shared/agent-context.js';
16
+ import {
17
+ buildAgentContextPreamble,
18
+ serializeNodeForAgentContext,
19
+ summarizeNodeForAgentContext,
20
+ } from '../../../shared/agent-context.js';
17
21
  import { buildCanvasAxContext, buildCanvasAxSurfaceSnapshot } from '../../ax-context.js';
18
22
  import { applyAxInteraction } from '../../ax-interaction.js';
19
23
  import {
@@ -31,6 +35,9 @@ import { defineOperation, OperationError, type Operation, type OperationContext
31
35
  import { readTargetBoard } from './boards.js';
32
36
  import { normalizeAxNodeIds, normalizeAxSource } from './ax-shared.js';
33
37
  import { isRecord } from './nodes.js';
38
+ import { lifecycleQualifications } from '../../lifecycle-status.js';
39
+ import { memoryKey } from '../../../shared/memory-graph.js';
40
+ import { findNeighborhoods } from '../../spatial-analysis.js';
34
41
 
35
42
  // ── Activity-reaction validation (moved from server.ts) ───────
36
43
 
@@ -167,20 +174,64 @@ const pinnedContextOperation = defineOperation<z.infer<typeof pinnedContextSchem
167
174
  },
168
175
  handler: ({ board }) => {
169
176
  const target = readTargetBoard(board);
170
- const pinnedIds = Array.from(target.pinnedNodeIds);
177
+ const qualifications = lifecycleQualifications(Date.now(), target.boardId ? [{ boardId: target.boardId }] : []);
178
+ const qualification = (id: string, boardId = target.boardId) =>
179
+ boardId ? qualifications[memoryKey({ boardId, cardId: id })] : undefined;
180
+ const pinnedIds = Array.from(target.pinnedNodeIds).filter((id) => !qualification(id)?.retired);
171
181
  const nodes = pinnedIds
172
182
  .map((id) => target.layout.nodes.find((node) => node.id === id))
173
183
  .filter((node): node is CanvasNodeState => node !== undefined);
174
184
  const preamble =
175
185
  pinnedIds.length > 0 ? buildAgentContextPreamble(nodes, { defaultTextLength: 700, webpageTextLength: 1600 }) : '';
176
- const serialized = nodes.map((node) =>
177
- serializeNodeForAgentContext(node, {
186
+ const serialized = nodes.map((node) => ({
187
+ ...serializeNodeForAgentContext(node, {
178
188
  defaultTextLength: 700,
179
189
  webpageTextLength: 1600,
180
190
  includePosition: true,
181
191
  }),
182
- );
183
- return { preamble, nodeIds: pinnedIds, count: pinnedIds.length, nodes: serialized };
192
+ qualification: qualification(node.id),
193
+ }));
194
+ const activeNodes = target.layout.nodes.filter((node) => !qualification(node.id)?.retired);
195
+ return {
196
+ preamble,
197
+ nodeIds: pinnedIds,
198
+ count: pinnedIds.length,
199
+ pinnedCount: pinnedIds.length,
200
+ nodes: serialized,
201
+ edges: target.layout.edges.filter(
202
+ (edge) =>
203
+ pinnedIds.includes(edge.from) &&
204
+ (!edge.toBoardId || edge.toBoardId === target.boardId) &&
205
+ pinnedIds.includes(edge.to),
206
+ ),
207
+ neighborhoods: findNeighborhoods(activeNodes, new Set(pinnedIds)).map((neighborhood) => ({
208
+ pinnedNodeId: neighborhood.pinnedNodeId,
209
+ pinnedNodeTitle: neighborhood.pinnedNodeTitle,
210
+ nearbyNodes: neighborhood.neighbors.map((node) => ({ ...node, qualification: qualification(node.id) })),
211
+ })),
212
+ pinnedBoards: canvasState
213
+ .listBoards()
214
+ .filter((board) => board.pin)
215
+ .map((board) => {
216
+ const read = canvasState.readBoard(board.id, true);
217
+ const cards = (read?.layout.nodes ?? []).filter((node) => !qualification(node.id, board.id)?.retired);
218
+ const readme = cards.find((node) => node.id === board.readmeNodeId && node.type === 'markdown');
219
+ return {
220
+ boardId: board.id,
221
+ name: board.name,
222
+ pin: board.pin,
223
+ summary: readme ? summarizeNodeForAgentContext(readme, { defaultTextLength: 1000 }) : null,
224
+ readmeQualification: readme ? qualification(readme.id, board.id) : null,
225
+ pinnedTitles: cards
226
+ .filter((node) => read?.state.contextPins.includes(node.id))
227
+ .map((node) => ({
228
+ nodeId: node.id,
229
+ title: String(node.data.title ?? node.id),
230
+ qualification: qualification(node.id, board.id),
231
+ })),
232
+ };
233
+ }),
234
+ };
184
235
  },
185
236
  });
186
237
 
@@ -506,6 +506,14 @@ const axReadsRecordShape = {
506
506
  .array(z.string())
507
507
  .optional()
508
508
  .describe('Ids of every serialized node in what the agent received; defaults to deliveredNodeIds.'),
509
+ seenNodeIds: z
510
+ .array(z.string())
511
+ .optional()
512
+ .describe('Ids of every node that arrived with its relations, summaries included; defaults to readNodeIds.'),
513
+ seenLinkRevisions: z
514
+ .record(z.string(), z.number().int().nonnegative())
515
+ .optional()
516
+ .describe('Exact link revisions captured in the delivered graph or brief before transport work.'),
509
517
  bytes: z.number().int().nonnegative().describe('Size of what the agent received.'),
510
518
  boardId: z.string().nullable().optional().describe('Board that was read, captured before delivery.'),
511
519
  };
@@ -532,6 +540,8 @@ const axReadsRecordOperation = defineOperation<z.infer<typeof axReadsRecordSchem
532
540
  pinnedNodeIds: input.pinnedNodeIds ?? [...canvasState.contextPinnedNodeIds],
533
541
  deliveredNodeIds: input.deliveredNodeIds,
534
542
  ...(input.readNodeIds ? { readNodeIds: input.readNodeIds } : {}),
543
+ ...(input.seenNodeIds ? { seenNodeIds: input.seenNodeIds } : {}),
544
+ ...(input.seenLinkRevisions ? { seenLinkRevisions: input.seenLinkRevisions } : {}),
535
545
  bytes: input.bytes,
536
546
  },
537
547
  input.boardId === undefined ? canvasState.activeBoardId : input.boardId,
@@ -542,12 +552,17 @@ const axReadsRecordOperation = defineOperation<z.infer<typeof axReadsRecordSchem
542
552
 
543
553
  const axReadsStatusShape = {
544
554
  board: z.string().optional().describe('Board id; defaults to the open board.'),
555
+ boards: z
556
+ .string()
557
+ .optional()
558
+ .describe("Comma-separated board ids: also return each one's latest agent read (pinned boards, in one request)."),
545
559
  };
546
560
  const axReadsStatusSchema = z.looseObject(axReadsStatusShape);
547
561
 
548
562
  /**
549
563
  * Per node: when an agent last read its content, who, and at which revision
550
- * (vs the node's revision now); and per pin: who pinned it, when and why.
564
+ * (vs the node's revision now); per node, the newest links revision a read
565
+ * carried (connection marks); and per pin: who pinned it, when and why.
551
566
  */
552
567
  const axReadsStatusOperation = defineOperation<z.infer<typeof axReadsStatusSchema>, Record<string, unknown>>({
553
568
  name: 'ax.reads.status',
@@ -567,7 +582,18 @@ const axReadsStatusOperation = defineOperation<z.infer<typeof axReadsStatusSchem
567
582
  : boardId
568
583
  ? (canvasState.readBoard(boardId, false)?.state.contextPinMeta ?? {})
569
584
  : {};
570
- return { ok: true, boardId, nodes: canvasState.getNodeReadStatus(boardId), pins };
585
+ const others = typeof input.boards === 'string' ? input.boards.split(',').map((id) => id.trim()) : [];
586
+ return {
587
+ ok: true,
588
+ boardId,
589
+ board: canvasState.getBoardLastRead(boardId),
590
+ ...(others.length > 0
591
+ ? { boards: Object.fromEntries(others.filter(Boolean).map((id) => [id, canvasState.getBoardLastRead(id)])) }
592
+ : {}),
593
+ nodes: canvasState.getNodeReadStatus(boardId),
594
+ links: canvasState.getSeenLinks(boardId),
595
+ pins,
596
+ };
571
597
  },
572
598
  });
573
599
 
@@ -120,9 +120,11 @@ const backupRestoreOperation = defineOperation<z.infer<typeof restoreSchema>, Re
120
120
  }
121
121
  const previous = `${canvasState.databasePath}.before-restore`;
122
122
  restoreCanvasLibrary(file);
123
- // `reloaded` tells open tabs to take the board fresh even when its id is unchanged.
124
- ctx.emit('boards-changed', { ...boardsPayload(), reloaded: true });
125
- return { ok: true, restored: file, previous, ...boardsPayload() };
123
+ // Keep the reload marker in later catalogs too, if a GET overtakes this event.
124
+ const catalog = boardsPayload(true);
125
+ ctx.emit('boards-changed', catalog);
126
+ ctx.emit('settings-changed', { workspaceSettings: canvasState.getWorkspaceSettings() });
127
+ return { ok: true, restored: file, previous, ...catalog };
126
128
  },
127
129
  });
128
130