pmx-canvas 0.8.2 → 0.9.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 (338) 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 +59 -0
  4. package/Readme.md +131 -532
  5. package/dist/canvas/global.css +2724 -476
  6. package/dist/canvas/index.js +168 -107
  7. package/dist/canvas/mcp-app.html +2899 -596
  8. package/dist/json-render/index.js +20 -20
  9. package/dist/types/client/canvas/BarHint.d.ts +12 -2
  10. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
  11. package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
  12. package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
  13. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  14. package/dist/types/client/canvas/CommandBar.d.ts +3 -4
  15. package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
  16. package/dist/types/client/canvas/ContextChip.d.ts +3 -0
  17. package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
  18. package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
  19. package/dist/types/client/canvas/EmptyState.d.ts +4 -4
  20. package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
  21. package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
  22. package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
  23. package/dist/types/client/canvas/HomeView.d.ts +9 -7
  24. package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
  25. package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
  26. package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
  27. package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
  28. package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
  29. package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
  30. package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
  31. package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
  32. package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
  33. package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
  34. package/dist/types/client/canvas/canvas-area.d.ts +1 -1
  35. package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
  36. package/dist/types/client/canvas/graph-model.d.ts +37 -0
  37. package/dist/types/client/canvas/memory-layout.d.ts +23 -0
  38. package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
  39. package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
  40. package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
  41. package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
  42. package/dist/types/client/icons.d.ts +0 -10
  43. package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
  44. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
  45. package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
  46. package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
  47. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
  48. package/dist/types/client/state/boards-store.d.ts +81 -9
  49. package/dist/types/client/state/canvas-store.d.ts +14 -1
  50. package/dist/types/client/state/context-chip-store.d.ts +22 -0
  51. package/dist/types/client/state/context-status-store.d.ts +35 -0
  52. package/dist/types/client/state/human-store.d.ts +2 -1
  53. package/dist/types/client/state/intent-bridge.d.ts +11 -8
  54. package/dist/types/client/state/knowledge-store.d.ts +22 -0
  55. package/dist/types/client/state/near-pin-store.d.ts +44 -1
  56. package/dist/types/client/state/presence-store.d.ts +4 -4
  57. package/dist/types/client/state/presentation.d.ts +28 -0
  58. package/dist/types/client/state/relation-store.d.ts +10 -0
  59. package/dist/types/client/state/session-store.d.ts +41 -2
  60. package/dist/types/client/state/workspace-settings.d.ts +7 -0
  61. package/dist/types/client/types.d.ts +13 -1
  62. package/dist/types/client/utils/keyboard.d.ts +2 -0
  63. package/dist/types/client/utils/time-ago.d.ts +4 -0
  64. package/dist/types/mcp/context-uri-template.d.ts +3 -1
  65. package/dist/types/server/agent-presence.d.ts +10 -3
  66. package/dist/types/server/ax-context.d.ts +1 -1
  67. package/dist/types/server/canvas-db.d.ts +30 -7
  68. package/dist/types/server/canvas-operations.d.ts +24 -2
  69. package/dist/types/server/canvas-provenance.d.ts +22 -2
  70. package/dist/types/server/canvas-serialization.d.ts +2 -0
  71. package/dist/types/server/canvas-state.d.ts +93 -15
  72. package/dist/types/server/canvas-validation.d.ts +1 -1
  73. package/dist/types/server/context-brief.d.ts +94 -6
  74. package/dist/types/server/context-reads.d.ts +33 -7
  75. package/dist/types/server/edit-summary.d.ts +20 -0
  76. package/dist/types/server/index.d.ts +71 -10
  77. package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
  78. package/dist/types/server/lifecycle-status.d.ts +10 -0
  79. package/dist/types/server/memory-graph.d.ts +9 -0
  80. package/dist/types/server/mutation-history.d.ts +2 -0
  81. package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
  82. package/dist/types/server/operations/ops/boards.d.ts +2 -1
  83. package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
  84. package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
  85. package/dist/types/server/operations/ops/nodes.d.ts +5 -0
  86. package/dist/types/server/operations/ops/query.d.ts +23 -0
  87. package/dist/types/server/operations/registry.d.ts +1 -0
  88. package/dist/types/server/placement.d.ts +7 -0
  89. package/dist/types/server/scope-fence.d.ts +2 -1
  90. package/dist/types/server/spatial-analysis.d.ts +6 -3
  91. package/dist/types/server/tour-control.d.ts +32 -0
  92. package/dist/types/server/wiki-links.d.ts +14 -0
  93. package/dist/types/server/workbench-auth.d.ts +2 -0
  94. package/dist/types/shared/agent-context.d.ts +2 -0
  95. package/dist/types/shared/agent-presence.d.ts +15 -12
  96. package/dist/types/shared/auto-arrange.d.ts +1 -0
  97. package/dist/types/shared/boards.d.ts +4 -4
  98. package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
  99. package/dist/types/shared/knowledge-presets.d.ts +6 -0
  100. package/dist/types/shared/lifecycle-status.d.ts +24 -0
  101. package/dist/types/shared/memory-graph.d.ts +53 -0
  102. package/dist/types/shared/relations.d.ts +68 -0
  103. package/dist/types/shared/semantic-attention.d.ts +2 -0
  104. package/dist/types/shared/tour.d.ts +38 -0
  105. package/dist/types/shared/workspace-settings.d.ts +11 -0
  106. package/docs/RELEASE.md +21 -3
  107. package/docs/adr-001-bun-only-runtime.md +7 -1
  108. package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
  109. package/docs/api-reference.md +109 -0
  110. package/docs/ax-host-adapter-contract.md +1 -1
  111. package/docs/ax-state-contract.md +5 -4
  112. package/docs/cli.md +153 -15
  113. package/docs/demos/pmx-canvas-demo.mp4 +0 -0
  114. package/docs/design/AgentContext.dc.html +3 -3
  115. package/docs/design/BoardPins.dc.html +37 -22
  116. package/docs/design/Categories.dc.html +208 -0
  117. package/docs/design/CategoryEditor.dc.html +33 -0
  118. package/docs/design/Chrome.dc.html +52 -9
  119. package/docs/design/ContextChip.dc.html +224 -0
  120. package/docs/design/ContextChip600.dc.html +136 -0
  121. package/docs/design/EmptyState.dc.html +45 -0
  122. package/docs/design/Flows.dc.html +23 -1
  123. package/docs/design/HeaderMarks.dc.html +183 -0
  124. package/docs/design/Home.dc.html +19 -1
  125. package/docs/design/HomeGraph.dc.html +86 -202
  126. package/docs/design/HomeMap.dc.html +23 -1
  127. package/docs/design/HomeTeam.dc.html +32 -14
  128. package/docs/design/Lifecycle.dc.html +22 -0
  129. package/docs/design/Lifecycle600.dc.html +18 -0
  130. package/docs/design/LifecycleVariants.dc.html +157 -0
  131. package/docs/design/LinkAuthorship.dc.html +139 -0
  132. package/docs/design/LinksChanged.dc.html +203 -0
  133. package/docs/design/LinksOptions.dc.html +137 -0
  134. package/docs/design/LinksOptions600.dc.html +134 -0
  135. package/docs/design/NearPin.dc.html +9 -10
  136. package/docs/design/Pane600.dc.html +4 -3
  137. package/docs/design/Relations.dc.html +3 -2
  138. package/docs/design/Restore.dc.html +152 -0
  139. package/docs/design/SeeChange.dc.html +267 -0
  140. package/docs/design/Uses.dc.html +12 -6
  141. package/docs/design/canvas.json +146 -19
  142. package/docs/design.md +449 -16
  143. package/docs/environment.md +0 -1
  144. package/docs/evals/curation-effect.md +128 -0
  145. package/docs/http-api.md +459 -37
  146. package/docs/mcp.md +146 -18
  147. package/docs/node-types.md +58 -3
  148. package/docs/plans/design-gaps-2026-10-08.md +53 -0
  149. package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
  150. package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
  151. package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
  152. package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
  153. package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
  154. package/docs/product-context-vision-2026-09.md +17 -1
  155. package/docs/product-vision-2026-09.md +1082 -43
  156. package/docs/product-vision-review-2026-09.md +13 -1
  157. package/docs/screenshots/workbench-live-dark.png +0 -0
  158. package/docs/screenshots/workbench-live-light.png +0 -0
  159. package/docs/sdk.md +62 -2
  160. package/package.json +1 -1
  161. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  162. package/plugins/pmx-canvas/plugin.json +1 -1
  163. package/plugins/pmx-canvas/runtime.json +1 -1
  164. package/plugins/pmx-canvas/tools.json +601 -22
  165. package/skills/pmx-canvas/SKILL.md +216 -42
  166. package/skills/pmx-canvas/evals/evals.json +224 -0
  167. package/skills/pmx-canvas/references/full-reference.md +120 -21
  168. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
  169. package/skills/pmx-canvas/references/importing-context.md +132 -0
  170. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  171. package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
  172. package/skills/pmx-canvas-testing/SKILL.md +20 -0
  173. package/src/cli/agent.ts +2 -0
  174. package/src/cli/commands/ax.ts +12 -0
  175. package/src/cli/commands/boards.ts +88 -11
  176. package/src/cli/commands/edges.ts +28 -0
  177. package/src/cli/commands/groups.ts +7 -0
  178. package/src/cli/commands/nodes.ts +1 -0
  179. package/src/cli/commands/query.ts +34 -2
  180. package/src/cli/commands/record.ts +199 -48
  181. package/src/cli/index.ts +6 -1
  182. package/src/client/App.tsx +177 -70
  183. package/src/client/canvas/AttentionHistory.tsx +0 -2
  184. package/src/client/canvas/BarHint.tsx +40 -2
  185. package/src/client/canvas/BoardCopyDialog.tsx +1 -1
  186. package/src/client/canvas/BoardFolderDialog.tsx +7 -7
  187. package/src/client/canvas/BoardLinkDialog.tsx +138 -33
  188. package/src/client/canvas/BoardPinButton.tsx +33 -0
  189. package/src/client/canvas/BoardSwitcher.tsx +26 -40
  190. package/src/client/canvas/CanvasNode.tsx +360 -237
  191. package/src/client/canvas/CanvasViewport.tsx +45 -7
  192. package/src/client/canvas/CommandBar.tsx +29 -23
  193. package/src/client/canvas/CommandPalette.tsx +5 -12
  194. package/src/client/canvas/ConnectionBanner.tsx +3 -3
  195. package/src/client/canvas/ContextChip.tsx +296 -0
  196. package/src/client/canvas/ContextMenu.tsx +63 -5
  197. package/src/client/canvas/CrossBoardReferences.tsx +60 -0
  198. package/src/client/canvas/EdgeLayer.tsx +339 -76
  199. package/src/client/canvas/EmptyState.tsx +33 -18
  200. package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
  201. package/src/client/canvas/ExternalWriters.tsx +57 -42
  202. package/src/client/canvas/HomeGraphView.tsx +942 -0
  203. package/src/client/canvas/HomeMapView.tsx +641 -0
  204. package/src/client/canvas/HomeReviewView.tsx +167 -0
  205. package/src/client/canvas/HomeView.tsx +687 -160
  206. package/src/client/canvas/IntentLayer.tsx +19 -12
  207. package/src/client/canvas/KnowledgeDialog.tsx +488 -0
  208. package/src/client/canvas/LifecycleChrome.tsx +133 -0
  209. package/src/client/canvas/LifecycleFields.tsx +91 -0
  210. package/src/client/canvas/LifecyclePanel.tsx +543 -0
  211. package/src/client/canvas/MemoryInspector.tsx +104 -0
  212. package/src/client/canvas/Minimap.tsx +3 -3
  213. package/src/client/canvas/NearPinLayer.tsx +187 -0
  214. package/src/client/canvas/NodeContextMark.tsx +175 -97
  215. package/src/client/canvas/Presentation.tsx +23 -57
  216. package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
  217. package/src/client/canvas/RelationsPanel.tsx +82 -0
  218. package/src/client/canvas/SelectionBar.tsx +3 -6
  219. package/src/client/canvas/SessionPanel.tsx +90 -50
  220. package/src/client/canvas/SessionReceipt.tsx +311 -39
  221. package/src/client/canvas/ShortcutOverlay.tsx +1 -1
  222. package/src/client/canvas/SnapshotPanel.tsx +27 -36
  223. package/src/client/canvas/ToolRail.tsx +367 -192
  224. package/src/client/canvas/TopBar.tsx +354 -124
  225. package/src/client/canvas/canvas-area.ts +10 -3
  226. package/src/client/canvas/graph-geometry.ts +195 -0
  227. package/src/client/canvas/graph-model.ts +254 -0
  228. package/src/client/canvas/memory-layout.ts +106 -0
  229. package/src/client/canvas/use-chrome-height.ts +20 -0
  230. package/src/client/canvas/use-focus-trap.ts +31 -5
  231. package/src/client/canvas/use-memory-graph.ts +58 -0
  232. package/src/client/canvas/use-node-drag.ts +2 -1
  233. package/src/client/canvas/use-pan-zoom.ts +64 -7
  234. package/src/client/icons.tsx +0 -63
  235. package/src/client/mcp-app/style.css +0 -6
  236. package/src/client/nodes/AxStepControls.tsx +74 -50
  237. package/src/client/nodes/BoardNode.tsx +5 -1
  238. package/src/client/nodes/ContextNode.tsx +18 -11
  239. package/src/client/nodes/ExtAppFrame.tsx +6 -3
  240. package/src/client/nodes/FileNode.tsx +46 -42
  241. package/src/client/nodes/HtmlNode.tsx +32 -0
  242. package/src/client/nodes/ImageNode.tsx +1 -1
  243. package/src/client/nodes/InlineFormatBar.tsx +6 -9
  244. package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
  245. package/src/client/nodes/MarkdownNode.tsx +377 -109
  246. package/src/client/nodes/McpAppNode.tsx +50 -20
  247. package/src/client/nodes/MdFormatBar.tsx +21 -18
  248. package/src/client/nodes/PromptNode.tsx +1 -1
  249. package/src/client/nodes/ResponseNode.tsx +1 -1
  250. package/src/client/nodes/viewer-runtime-state.ts +54 -22
  251. package/src/client/state/attention-bridge.ts +64 -15
  252. package/src/client/state/boards-store.ts +193 -39
  253. package/src/client/state/canvas-store.ts +99 -12
  254. package/src/client/state/context-chip-store.ts +70 -0
  255. package/src/client/state/context-status-store.ts +53 -0
  256. package/src/client/state/human-store.ts +13 -7
  257. package/src/client/state/intent-bridge.ts +51 -30
  258. package/src/client/state/knowledge-store.ts +43 -0
  259. package/src/client/state/near-pin-store.ts +115 -6
  260. package/src/client/state/presence-store.ts +7 -10
  261. package/src/client/state/presentation.ts +91 -0
  262. package/src/client/state/relation-store.ts +81 -0
  263. package/src/client/state/session-store.ts +144 -18
  264. package/src/client/state/sse-bridge.ts +235 -32
  265. package/src/client/state/workspace-settings.ts +11 -0
  266. package/src/client/theme/global.css +2724 -476
  267. package/src/client/types.ts +12 -0
  268. package/src/client/utils/keyboard.ts +10 -0
  269. package/src/client/utils/time-ago.ts +14 -0
  270. package/src/json-render/renderer/index.tsx +31 -0
  271. package/src/mcp/canvas-app.ts +12 -2
  272. package/src/mcp/context-uri-template.ts +7 -4
  273. package/src/mcp/server.ts +63 -41
  274. package/src/mcp/workbench-app.ts +39 -6
  275. package/src/server/agent-presence.ts +128 -28
  276. package/src/server/ax-context.ts +27 -13
  277. package/src/server/board-export.ts +12 -1
  278. package/src/server/canvas-db.ts +331 -42
  279. package/src/server/canvas-operations.ts +195 -83
  280. package/src/server/canvas-provenance.ts +53 -22
  281. package/src/server/canvas-serialization.ts +2 -0
  282. package/src/server/canvas-state.ts +950 -122
  283. package/src/server/canvas-validation.ts +9 -2
  284. package/src/server/context-brief.ts +345 -38
  285. package/src/server/context-reads.ts +178 -30
  286. package/src/server/demo-state.json +355 -24
  287. package/src/server/edit-summary.ts +74 -0
  288. package/src/server/html-surface.ts +8 -1
  289. package/src/server/index.ts +181 -11
  290. package/src/server/knowledge-lifecycle.ts +304 -0
  291. package/src/server/lifecycle-status.ts +232 -0
  292. package/src/server/memory-graph.ts +166 -0
  293. package/src/server/mutation-history.ts +18 -6
  294. package/src/server/operations/composites.ts +32 -11
  295. package/src/server/operations/http.ts +7 -2
  296. package/src/server/operations/index.ts +4 -0
  297. package/src/server/operations/mcp.ts +26 -1
  298. package/src/server/operations/ops/ax-presence.ts +1 -1
  299. package/src/server/operations/ops/ax-read.ts +57 -6
  300. package/src/server/operations/ops/ax-timeline.ts +28 -2
  301. package/src/server/operations/ops/backup.ts +5 -3
  302. package/src/server/operations/ops/batch.ts +35 -4
  303. package/src/server/operations/ops/boards.ts +188 -47
  304. package/src/server/operations/ops/canvas-wire.ts +31 -1
  305. package/src/server/operations/ops/edges.ts +36 -2
  306. package/src/server/operations/ops/groups.ts +6 -1
  307. package/src/server/operations/ops/lifecycle.ts +262 -0
  308. package/src/server/operations/ops/memory-graph.ts +45 -0
  309. package/src/server/operations/ops/nodes.ts +107 -24
  310. package/src/server/operations/ops/query.ts +281 -68
  311. package/src/server/operations/ops/snapshots.ts +73 -1
  312. package/src/server/operations/ops/validate.ts +5 -1
  313. package/src/server/operations/ops/viewport.ts +97 -3
  314. package/src/server/operations/registry.ts +74 -25
  315. package/src/server/placement.ts +29 -0
  316. package/src/server/scope-fence.ts +78 -6
  317. package/src/server/server.ts +115 -16
  318. package/src/server/spatial-analysis.ts +48 -23
  319. package/src/server/tour-control.ts +82 -0
  320. package/src/server/wiki-links.ts +137 -0
  321. package/src/server/workbench-auth.ts +11 -0
  322. package/src/shared/agent-context.ts +3 -0
  323. package/src/shared/agent-presence.ts +15 -15
  324. package/src/shared/auto-arrange.ts +2 -0
  325. package/src/shared/boards.ts +4 -4
  326. package/src/shared/knowledge-lifecycle.ts +22 -0
  327. package/src/shared/knowledge-presets.ts +80 -0
  328. package/src/shared/lifecycle-status.ts +26 -0
  329. package/src/shared/memory-graph.ts +74 -0
  330. package/src/shared/relations.ts +98 -0
  331. package/src/shared/semantic-attention.ts +15 -1
  332. package/src/shared/tour.ts +23 -5
  333. package/src/shared/workspace-settings.ts +13 -0
  334. package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
  335. package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
  336. package/dist/types/client/canvas/kind-colors.d.ts +0 -3
  337. package/dist/types/server/agent-context.d.ts +0 -22
  338. 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;
@@ -25,6 +27,7 @@ export const CONTEXT_READ_OPS = new Set([
25
27
  'spatial.get',
26
28
  'layout.get',
27
29
  'context.get',
30
+ 'memory-graph.get',
28
31
  ]);
29
32
 
30
33
  export interface ContextRead {
@@ -40,16 +43,22 @@ export interface ContextRead {
40
43
  deliveredNodeIds: string[];
41
44
  /** Every node on the read board whose content was delivered, with its content revision at read time. */
42
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>;
43
48
  bytes: number;
44
49
  /** The board open when the read happened (null on Home). */
45
50
  boardId: string | null;
46
51
  }
47
52
 
48
- export type ContextReadInput = Omit<ContextRead, 'seq' | 'id' | 'at' | 'boardId' | 'readNodes'> & {
53
+ export type ContextReadInput = Omit<ContextRead, 'seq' | 'id' | 'at' | 'boardId' | 'readNodes' | 'seenLinks'> & {
49
54
  /** Explicit read target, captured before asynchronous formatting/proxy work. */
50
55
  boardId?: string | null;
51
56
  /** Ids of every serialized node in what the reader received; the server keeps those on the board. */
52
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>;
53
62
  };
54
63
 
55
64
  /** One node's read state on a board: the latest agent read that delivered its content. */
@@ -78,10 +87,13 @@ export interface ContextReadConsumerSummary {
78
87
  * `nodeId` with its text) in what the reader received. A bare id list, a
79
88
  * title, or a clipped-off node does not count — the agent got the node's
80
89
  * name, not its content. Non-node ids (edges, intents) are dropped by the
81
- * 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.
82
93
  */
83
- export function deliveredIds(payloadText: string): Set<string> {
84
- 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>();
85
97
  const objectStarts: number[] = [];
86
98
  let inString = false;
87
99
  let escaped = false;
@@ -103,20 +115,21 @@ export function deliveredIds(payloadText: string): Set<string> {
103
115
  if (start === undefined) continue;
104
116
  try {
105
117
  const value = JSON.parse(payloadText.slice(start, index + 1)) as unknown;
106
- if (
107
- typeof value === 'object' &&
108
- value !== null &&
109
- !Array.isArray(value) &&
110
- (typeof (value as { id?: unknown }).id === 'string' ||
111
- (typeof (value as { nodeId?: unknown }).nodeId === 'string' &&
112
- (value as { titleOnly?: unknown }).titleOnly !== true &&
113
- typeof (value as { text?: unknown }).text === 'string'))
114
- ) {
115
- delivered.add(
116
- typeof (value as { id?: unknown }).id === 'string'
117
- ? (value as { id: string }).id
118
- : (value as { nodeId: string }).nodeId,
119
- );
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
+ }
120
133
  }
121
134
  } catch {
122
135
  // An inner object may be complete even when its containing object was clipped.
@@ -124,13 +137,27 @@ export function deliveredIds(payloadText: string): Set<string> {
124
137
  }
125
138
  }
126
139
 
127
- return delivered;
140
+ return { read, reached };
128
141
  }
129
142
 
130
- /** Pinned nodes whose content is in what the reader received (see `deliveredIds`). */
131
- export function deliveredPinnedIds(pinnedNodeIds: string[], payloadText: string): string[] {
132
- const delivered = deliveredIds(payloadText);
133
- 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
+ );
134
161
  }
135
162
 
136
163
  export function contextReadFromPayload(
@@ -138,15 +165,104 @@ export function contextReadFromPayload(
138
165
  payload: unknown,
139
166
  ): ContextReadInput {
140
167
  const text = typeof payload === 'string' ? payload : (JSON.stringify(payload) ?? '');
141
- 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);
142
189
  return {
143
190
  ...base,
144
- deliveredNodeIds: base.pinnedNodeIds.filter((id) => delivered.has(id)),
145
- readNodeIds: [...delivered],
191
+ deliveredNodeIds: base.pinnedNodeIds.filter((id) => reached.has(id)),
192
+ readNodeIds: [...read],
193
+ seenNodeIds: Object.keys(seenLinkRevisions),
194
+ seenLinkRevisions,
146
195
  bytes: Buffer.byteLength(text, 'utf-8'),
147
196
  };
148
197
  }
149
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
+
150
266
  export const CONTEXT_READS_SCHEMA_SQL = `
151
267
  CREATE TABLE IF NOT EXISTS context_reads (
152
268
  seq INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -161,7 +277,8 @@ export const CONTEXT_READS_SCHEMA_SQL = `
161
277
  delivered_node_ids TEXT NOT NULL DEFAULT '[]',
162
278
  bytes INTEGER NOT NULL DEFAULT 0,
163
279
  board_id TEXT,
164
- read_nodes TEXT NOT NULL DEFAULT '{}'
280
+ read_nodes TEXT NOT NULL DEFAULT '{}',
281
+ seen_links TEXT NOT NULL DEFAULT '{}'
165
282
  );
166
283
  `;
167
284
 
@@ -170,11 +287,12 @@ export function appendContextReadToDB(
170
287
  input: ContextReadInput,
171
288
  boardId: string | null,
172
289
  readNodes: Record<string, number> = {},
290
+ seenLinks: Record<string, number> = {},
173
291
  ): ContextRead {
174
292
  const id = `read-${randomUUID()}`;
175
293
  const at = new Date().toISOString();
176
294
  db.run(
177
- '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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
178
296
  [
179
297
  id,
180
298
  at,
@@ -188,12 +306,13 @@ export function appendContextReadToDB(
188
306
  input.bytes,
189
307
  boardId,
190
308
  JSON.stringify(readNodes),
309
+ JSON.stringify(seenLinks),
191
310
  ],
192
311
  );
193
312
  const seq = Number(db.query<{ seq: number }, []>('SELECT last_insert_rowid() AS seq').get()?.seq ?? 0);
194
313
  db.run('DELETE FROM context_reads WHERE seq <= ?', [seq - CONTEXT_READ_RETENTION]);
195
- const { readNodeIds: _readNodeIds, ...record } = input;
196
- return { ...record, seq, id, at, boardId, readNodes };
314
+ const { readNodeIds: _readNodeIds, seenNodeIds: _seenNodeIds, ...record } = input;
315
+ return { ...record, seq, id, at, boardId, readNodes, seenLinks };
197
316
  }
198
317
 
199
318
  interface ContextReadRow {
@@ -210,6 +329,7 @@ interface ContextReadRow {
210
329
  bytes: number;
211
330
  board_id: string | null;
212
331
  read_nodes: string;
332
+ seen_links: string;
213
333
  }
214
334
 
215
335
  function rowToContextRead(row: ContextReadRow): ContextRead {
@@ -225,6 +345,7 @@ function rowToContextRead(row: ContextReadRow): ContextRead {
225
345
  pinnedNodeIds: JSON.parse(row.pinned_node_ids) as string[],
226
346
  deliveredNodeIds: JSON.parse(row.delivered_node_ids) as string[],
227
347
  readNodes: JSON.parse(row.read_nodes) as Record<string, number>,
348
+ seenLinks: JSON.parse(row.seen_links) as Record<string, number>,
228
349
  bytes: row.bytes,
229
350
  boardId: row.board_id,
230
351
  };
@@ -255,6 +376,33 @@ export function loadNodeReadStatusFromDB(db: Database, boardId: string): NodeRea
255
376
  return [...status.values()];
256
377
  }
257
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
+
258
406
  /** Every node on a board whose content an agent read at or after `since` (ISO). */
259
407
  export function loadReadNodeIdsSince(db: Database, boardId: string, since: string): string[] {
260
408
  const rows = db