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
@@ -7,6 +7,8 @@
7
7
  */
8
8
  import type { Database } from 'bun:sqlite';
9
9
  import { randomUUID } from 'node:crypto';
10
+ import type { MemoryGraph } from '../shared/memory-graph.js';
11
+ import type { CompiledContextEntry } from './context-brief.js';
10
12
 
11
13
  export const CONTEXT_READ_RETENTION = 5000;
12
14
  const CONTEXT_READ_DEFAULT_LIMIT = 50;
@@ -17,6 +19,7 @@ export type ContextReadChannel = (typeof CONTEXT_READ_CHANNELS)[number];
17
19
 
18
20
  /** Registry reads that hand an agent canvas context; recorded in executeOperation. */
19
21
  export const CONTEXT_READ_OPS = new Set([
22
+ 'node.get',
20
23
  'pinned-context.get',
21
24
  'ax.context.get',
22
25
  'ax.get',
@@ -24,6 +27,7 @@ export const CONTEXT_READ_OPS = new Set([
24
27
  'spatial.get',
25
28
  'layout.get',
26
29
  'context.get',
30
+ 'memory-graph.get',
27
31
  ]);
28
32
 
29
33
  export interface ContextRead {
@@ -39,16 +43,22 @@ export interface ContextRead {
39
43
  deliveredNodeIds: string[];
40
44
  /** Every node on the read board whose content was delivered, with its content revision at read time. */
41
45
  readNodes: Record<string, number>;
46
+ /** Every node on the read board that arrived with its relations (summaries count), with its links revision then. */
47
+ seenLinks: Record<string, number>;
42
48
  bytes: number;
43
49
  /** The board open when the read happened (null on Home). */
44
50
  boardId: string | null;
45
51
  }
46
52
 
47
- export type ContextReadInput = Omit<ContextRead, 'seq' | 'id' | 'at' | 'boardId' | 'readNodes'> & {
53
+ export type ContextReadInput = Omit<ContextRead, 'seq' | 'id' | 'at' | 'boardId' | 'readNodes' | 'seenLinks'> & {
48
54
  /** Explicit read target, captured before asynchronous formatting/proxy work. */
49
55
  boardId?: string | null;
50
56
  /** Ids of every serialized node in what the reader received; the server keeps those on the board. */
51
57
  readNodeIds?: string[];
58
+ /** Ids of every node that arrived, summaries included; defaults to readNodeIds. */
59
+ seenNodeIds?: string[];
60
+ /** Exact link clocks captured in the delivered projection, before asynchronous transport work. */
61
+ seenLinkRevisions?: Record<string, number>;
52
62
  };
53
63
 
54
64
  /** One node's read state on a board: the latest agent read that delivered its content. */
@@ -77,10 +87,13 @@ export interface ContextReadConsumerSummary {
77
87
  * `nodeId` with its text) in what the reader received. A bare id list, a
78
88
  * title, or a clipped-off node does not count — the agent got the node's
79
89
  * name, not its content. Non-node ids (edges, intents) are dropped by the
80
- * caller against the board's nodes.
90
+ * caller against the board's nodes. A summary is "seen", not "read": `read`
91
+ * holds only nodes pulled in full and `reached` adds the summaries. Content
92
+ * delivery alone never establishes that the card's relations were delivered.
81
93
  */
82
- export function deliveredIds(payloadText: string): Set<string> {
83
- const delivered = new Set<string>();
94
+ export function deliveredIds(payloadText: string): { read: Set<string>; reached: Set<string> } {
95
+ const read = new Set<string>();
96
+ const reached = new Set<string>();
84
97
  const objectStarts: number[] = [];
85
98
  let inString = false;
86
99
  let escaped = false;
@@ -102,20 +115,21 @@ export function deliveredIds(payloadText: string): Set<string> {
102
115
  if (start === undefined) continue;
103
116
  try {
104
117
  const value = JSON.parse(payloadText.slice(start, index + 1)) as unknown;
105
- if (
106
- typeof value === 'object' &&
107
- value !== null &&
108
- !Array.isArray(value) &&
109
- (typeof (value as { id?: unknown }).id === 'string' ||
110
- (typeof (value as { nodeId?: unknown }).nodeId === 'string' &&
111
- (value as { titleOnly?: unknown }).titleOnly !== true &&
112
- typeof (value as { text?: unknown }).text === 'string'))
113
- ) {
114
- delivered.add(
115
- typeof (value as { id?: unknown }).id === 'string'
116
- ? (value as { id: string }).id
117
- : (value as { nodeId: string }).nodeId,
118
- );
118
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
119
+ const item = value as {
120
+ id?: unknown;
121
+ nodeId?: unknown;
122
+ titleOnly?: unknown;
123
+ summaryOnly?: unknown;
124
+ text?: unknown;
125
+ };
126
+ if (typeof item.id === 'string') {
127
+ read.add(item.id);
128
+ reached.add(item.id);
129
+ } else if (typeof item.nodeId === 'string' && item.titleOnly !== true && typeof item.text === 'string') {
130
+ reached.add(item.nodeId);
131
+ if (item.summaryOnly !== true) read.add(item.nodeId);
132
+ }
119
133
  }
120
134
  } catch {
121
135
  // An inner object may be complete even when its containing object was clipped.
@@ -123,13 +137,27 @@ export function deliveredIds(payloadText: string): Set<string> {
123
137
  }
124
138
  }
125
139
 
126
- return delivered;
140
+ return { read, reached };
127
141
  }
128
142
 
129
- /** Pinned nodes whose content is in what the reader received (see `deliveredIds`). */
130
- export function deliveredPinnedIds(pinnedNodeIds: string[], payloadText: string): string[] {
131
- const delivered = deliveredIds(payloadText);
132
- return pinnedNodeIds.filter((id) => delivered.has(id));
143
+ /** Only complete, board-qualified brief relations can acknowledge their captured clock. */
144
+ function briefLinkRevisions(entries: Partial<CompiledContextEntry>[], boardId: string): Record<string, number> {
145
+ return Object.fromEntries(
146
+ entries.flatMap((entry) =>
147
+ entry.sourceBoardId === boardId &&
148
+ typeof entry.nodeId === 'string' &&
149
+ !entry.nodeId.startsWith('board:') &&
150
+ typeof entry.text === 'string' &&
151
+ entry.titleOnly !== true &&
152
+ entry.truncated !== true &&
153
+ entry.linksComplete !== false &&
154
+ typeof entry.linksRevision === 'number' &&
155
+ Number.isSafeInteger(entry.linksRevision) &&
156
+ entry.linksRevision >= 0
157
+ ? [[entry.nodeId, entry.linksRevision]]
158
+ : [],
159
+ ),
160
+ );
133
161
  }
134
162
 
135
163
  export function contextReadFromPayload(
@@ -137,15 +165,104 @@ export function contextReadFromPayload(
137
165
  payload: unknown,
138
166
  ): ContextReadInput {
139
167
  const text = typeof payload === 'string' ? payload : (JSON.stringify(payload) ?? '');
140
- const delivered = deliveredIds(text);
168
+ let delivered = text;
169
+ let seenLinkRevisions: Record<string, number> = {};
170
+ try {
171
+ const document = JSON.parse(text) as {
172
+ board?: { boardId?: string };
173
+ entries?: Partial<CompiledContextEntry>[];
174
+ };
175
+ if (document.board?.boardId && Array.isArray(document.entries)) {
176
+ // A brief carries several boards. Bare IDs from a remote README must
177
+ // never acknowledge a same-ID card on the open board.
178
+ const entries = document.entries.filter(
179
+ (entry) => entry.sourceBoardId === (base.boardId ?? document.board?.boardId),
180
+ );
181
+ delivered = JSON.stringify(entries);
182
+ seenLinkRevisions = briefLinkRevisions(entries, base.boardId ?? document.board.boardId);
183
+ }
184
+ } catch {
185
+ // Non-JSON and clipped payloads keep the complete-object delivery scanner.
186
+ }
187
+ // Delivery (did the pins reach the agent?) counts summaries; read marks do not.
188
+ const { read, reached } = deliveredIds(delivered);
141
189
  return {
142
190
  ...base,
143
- deliveredNodeIds: base.pinnedNodeIds.filter((id) => delivered.has(id)),
144
- readNodeIds: [...delivered],
191
+ deliveredNodeIds: base.pinnedNodeIds.filter((id) => reached.has(id)),
192
+ readNodeIds: [...read],
193
+ seenNodeIds: Object.keys(seenLinkRevisions),
194
+ seenLinkRevisions,
145
195
  bytes: Buffer.byteLength(text, 'utf-8'),
146
196
  };
147
197
  }
148
198
 
199
+ /**
200
+ * Reads a `context.get` brief made on pinned boards (vision move 0a): one per
201
+ * pinned board whose map arrived, so the board shows as read. Its cards came as
202
+ * summaries, which mark nothing read until the agent pulls them; only summaries
203
+ * with complete, unclipped relations count as links seen on that board.
204
+ */
205
+ export function pinnedBoardReads(
206
+ base: Omit<ContextReadInput, 'deliveredNodeIds' | 'bytes' | 'readNodeIds' | 'pinnedNodeIds' | 'boardId'>,
207
+ payload: unknown,
208
+ ): ContextReadInput[] {
209
+ let document: unknown = payload;
210
+ if (typeof payload === 'string') {
211
+ try {
212
+ document = JSON.parse(payload);
213
+ } catch {
214
+ return [];
215
+ }
216
+ }
217
+ const entries = (document as { entries?: Partial<CompiledContextEntry>[] } | null)?.entries ?? [];
218
+ return entries
219
+ .filter(
220
+ (entry) =>
221
+ entry.reason === 'pinned-board' &&
222
+ typeof entry.sourceBoardId === 'string' &&
223
+ entry.nodeId === `board:${entry.sourceBoardId}`,
224
+ )
225
+ .map((entry) => {
226
+ const boardId = entry.sourceBoardId as string;
227
+ const seenLinkRevisions = briefLinkRevisions(entries, boardId);
228
+ return {
229
+ ...base,
230
+ boardId,
231
+ pinnedNodeIds: [],
232
+ deliveredNodeIds: [],
233
+ readNodeIds: [],
234
+ seenNodeIds: Object.keys(seenLinkRevisions),
235
+ seenLinkRevisions,
236
+ bytes: Buffer.byteLength(JSON.stringify(entry), 'utf-8'),
237
+ };
238
+ });
239
+ }
240
+
241
+ /** Graph summaries are seen on their own boards, never full-content reads or a board switch. */
242
+ export function graphReads(
243
+ base: Pick<ContextReadInput, 'channel' | 'resource' | 'source' | 'consumer' | 'agentId'>,
244
+ graph: MemoryGraph,
245
+ ): ContextReadInput[] {
246
+ const boards = new Set(graph.nodes.map((node) => node.boardId));
247
+ return [...boards].map((boardId) => {
248
+ const nodes = graph.nodes.filter((node) => node.boardId === boardId);
249
+ return {
250
+ ...base,
251
+ boardId,
252
+ pinnedNodeIds: [],
253
+ deliveredNodeIds: [],
254
+ readNodeIds: [],
255
+ seenNodeIds: nodes.flatMap((node) => (node.cardId && node.linksComplete !== false ? [node.cardId] : [])),
256
+ seenLinkRevisions: Object.fromEntries(
257
+ nodes.flatMap((node) =>
258
+ node.cardId && typeof node.linksRevision === 'number' ? [[node.cardId, node.linksRevision]] : [],
259
+ ),
260
+ ),
261
+ bytes: Buffer.byteLength(JSON.stringify(nodes), 'utf-8'),
262
+ };
263
+ });
264
+ }
265
+
149
266
  export const CONTEXT_READS_SCHEMA_SQL = `
150
267
  CREATE TABLE IF NOT EXISTS context_reads (
151
268
  seq INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -160,7 +277,8 @@ export const CONTEXT_READS_SCHEMA_SQL = `
160
277
  delivered_node_ids TEXT NOT NULL DEFAULT '[]',
161
278
  bytes INTEGER NOT NULL DEFAULT 0,
162
279
  board_id TEXT,
163
- read_nodes TEXT NOT NULL DEFAULT '{}'
280
+ read_nodes TEXT NOT NULL DEFAULT '{}',
281
+ seen_links TEXT NOT NULL DEFAULT '{}'
164
282
  );
165
283
  `;
166
284
 
@@ -169,11 +287,12 @@ export function appendContextReadToDB(
169
287
  input: ContextReadInput,
170
288
  boardId: string | null,
171
289
  readNodes: Record<string, number> = {},
290
+ seenLinks: Record<string, number> = {},
172
291
  ): ContextRead {
173
292
  const id = `read-${randomUUID()}`;
174
293
  const at = new Date().toISOString();
175
294
  db.run(
176
- 'INSERT INTO context_reads (id, at, channel, resource, source, consumer, agent_id, pinned_node_ids, delivered_node_ids, bytes, board_id, read_nodes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
295
+ 'INSERT INTO context_reads (id, at, channel, resource, source, consumer, agent_id, pinned_node_ids, delivered_node_ids, bytes, board_id, read_nodes, seen_links) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
177
296
  [
178
297
  id,
179
298
  at,
@@ -187,12 +306,13 @@ export function appendContextReadToDB(
187
306
  input.bytes,
188
307
  boardId,
189
308
  JSON.stringify(readNodes),
309
+ JSON.stringify(seenLinks),
190
310
  ],
191
311
  );
192
312
  const seq = Number(db.query<{ seq: number }, []>('SELECT last_insert_rowid() AS seq').get()?.seq ?? 0);
193
313
  db.run('DELETE FROM context_reads WHERE seq <= ?', [seq - CONTEXT_READ_RETENTION]);
194
- const { readNodeIds: _readNodeIds, ...record } = input;
195
- return { ...record, seq, id, at, boardId, readNodes };
314
+ const { readNodeIds: _readNodeIds, seenNodeIds: _seenNodeIds, ...record } = input;
315
+ return { ...record, seq, id, at, boardId, readNodes, seenLinks };
196
316
  }
197
317
 
198
318
  interface ContextReadRow {
@@ -209,6 +329,7 @@ interface ContextReadRow {
209
329
  bytes: number;
210
330
  board_id: string | null;
211
331
  read_nodes: string;
332
+ seen_links: string;
212
333
  }
213
334
 
214
335
  function rowToContextRead(row: ContextReadRow): ContextRead {
@@ -224,6 +345,7 @@ function rowToContextRead(row: ContextReadRow): ContextRead {
224
345
  pinnedNodeIds: JSON.parse(row.pinned_node_ids) as string[],
225
346
  deliveredNodeIds: JSON.parse(row.delivered_node_ids) as string[],
226
347
  readNodes: JSON.parse(row.read_nodes) as Record<string, number>,
348
+ seenLinks: JSON.parse(row.seen_links) as Record<string, number>,
227
349
  bytes: row.bytes,
228
350
  boardId: row.board_id,
229
351
  };
@@ -254,6 +376,33 @@ export function loadNodeReadStatusFromDB(db: Database, boardId: string): NodeRea
254
376
  return [...status.values()];
255
377
  }
256
378
 
379
+ /** The latest agent read on a board, of any kind (a brief, a pinned-board map, a pull), or null. */
380
+ export function loadBoardLastReadFromDB(
381
+ db: Database,
382
+ boardId: string,
383
+ ): { lastReadAt: string; lastReadBy: string } | null {
384
+ const row = db
385
+ .query<ContextReadRow, [string]>('SELECT * FROM context_reads WHERE board_id = ? ORDER BY seq DESC LIMIT 1')
386
+ .get(boardId);
387
+ if (!row) return null;
388
+ const read = rowToContextRead(row);
389
+ return { lastReadAt: read.at, lastReadBy: read.consumer ?? read.agentId ?? read.source };
390
+ }
391
+
392
+ /** Per node on a board, the newest links revision any agent read has carried (connection marks). */
393
+ export function loadSeenLinksFromDB(db: Database, boardId: string): Record<string, number> {
394
+ const rows = db
395
+ .query<{ seen_links: string }, [string]>('SELECT seen_links FROM context_reads WHERE board_id = ?')
396
+ .all(boardId);
397
+ const seen: Record<string, number> = {};
398
+ for (const row of rows) {
399
+ for (const [nodeId, revision] of Object.entries(JSON.parse(row.seen_links) as Record<string, number>)) {
400
+ if (revision > (seen[nodeId] ?? -1)) seen[nodeId] = revision;
401
+ }
402
+ }
403
+ return seen;
404
+ }
405
+
257
406
  /** Every node on a board whose content an agent read at or after `since` (ISO). */
258
407
  export function loadReadNodeIdsSince(db: Database, boardId: string, since: string): string[] {
259
408
  const rows = db