pmx-canvas 0.4.8 → 0.5.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 (210) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +154 -5
  2. package/.github/extensions/pmx-canvas/steering-delivery.mjs +79 -0
  3. package/CHANGELOG.md +139 -0
  4. package/Readme.md +72 -23
  5. package/dist/canvas/global.css +3069 -796
  6. package/dist/canvas/index.js +104 -104
  7. package/dist/json-render/index.css +1 -1
  8. package/dist/types/cli/agent.d.ts +1 -0
  9. package/dist/types/cli/commands/pump.d.ts +9 -0
  10. package/dist/types/client/App.d.ts +1 -1
  11. package/dist/types/client/canvas/AgentPresenceLayer.d.ts +11 -0
  12. package/dist/types/client/canvas/BarHint.d.ts +22 -0
  13. package/dist/types/client/canvas/CanvasNode.d.ts +5 -0
  14. package/dist/types/client/canvas/CanvasViewport.d.ts +4 -4
  15. package/dist/types/client/canvas/CommandBar.d.ts +10 -0
  16. package/dist/types/client/canvas/ConnectionBanner.d.ts +13 -0
  17. package/dist/types/client/canvas/ContextMenu.d.ts +6 -0
  18. package/dist/types/client/canvas/EdgeLayer.d.ts +2 -1
  19. package/dist/types/client/canvas/EmptyState.d.ts +11 -0
  20. package/dist/types/client/canvas/ExternalWriters.d.ts +5 -0
  21. package/dist/types/client/canvas/HumanPresenceLayer.d.ts +8 -0
  22. package/dist/types/client/canvas/Minimap.d.ts +12 -1
  23. package/dist/types/client/canvas/ScopeFenceLayer.d.ts +8 -0
  24. package/dist/types/client/canvas/SelectionBar.d.ts +6 -0
  25. package/dist/types/client/canvas/SessionPanel.d.ts +1 -0
  26. package/dist/types/client/canvas/SessionReceipt.d.ts +17 -0
  27. package/dist/types/client/canvas/TextPrompt.d.ts +25 -0
  28. package/dist/types/client/canvas/ToolRail.d.ts +21 -0
  29. package/dist/types/client/canvas/TopBar.d.ts +7 -0
  30. package/dist/types/client/canvas/canvas-area.d.ts +25 -0
  31. package/dist/types/client/canvas/create-in-view.d.ts +18 -0
  32. package/dist/types/client/canvas/import-files.d.ts +8 -0
  33. package/dist/types/client/canvas/kind-colors.d.ts +3 -0
  34. package/dist/types/client/canvas/use-focus-trap.d.ts +14 -0
  35. package/dist/types/client/canvas/use-now.d.ts +6 -0
  36. package/dist/types/client/icons.d.ts +11 -4
  37. package/dist/types/client/nodes/GroupNode.d.ts +6 -1
  38. package/dist/types/client/nodes/MarkdownNode.d.ts +2 -0
  39. package/dist/types/client/nodes/inline-editor-commands.d.ts +3 -4
  40. package/dist/types/client/state/canvas-store.d.ts +49 -5
  41. package/dist/types/client/state/human-store.d.ts +26 -0
  42. package/dist/types/client/state/iframe-mode.d.ts +1 -0
  43. package/dist/types/client/state/intent-bridge.d.ts +37 -5
  44. package/dist/types/client/state/node-factory.d.ts +0 -1
  45. package/dist/types/client/state/presence-store.d.ts +78 -0
  46. package/dist/types/client/state/session-store.d.ts +176 -0
  47. package/dist/types/client/types.d.ts +2 -1
  48. package/dist/types/client/utils/platform.d.ts +2 -0
  49. package/dist/types/server/agent-presence.d.ts +192 -0
  50. package/dist/types/server/ax-gate-ttl.d.ts +6 -0
  51. package/dist/types/server/ax-interaction.d.ts +4 -10
  52. package/dist/types/server/ax-state-manager.d.ts +21 -2
  53. package/dist/types/server/ax-state.d.ts +49 -3
  54. package/dist/types/server/ax-wait.d.ts +13 -0
  55. package/dist/types/server/canvas-db.d.ts +9 -1
  56. package/dist/types/server/canvas-operations.d.ts +1 -0
  57. package/dist/types/server/canvas-state.d.ts +27 -13
  58. package/dist/types/server/canvas-validation.d.ts +0 -14
  59. package/dist/types/server/html-surface.d.ts +1 -1
  60. package/dist/types/server/human-presence.d.ts +40 -0
  61. package/dist/types/server/index.d.ts +29 -3
  62. package/dist/types/server/intent-registry.d.ts +7 -0
  63. package/dist/types/server/mutation-history.d.ts +15 -2
  64. package/dist/types/server/operations/invoker.d.ts +13 -2
  65. package/dist/types/server/operations/ops/ax-presence.d.ts +2 -0
  66. package/dist/types/server/operations/ops/human-presence.d.ts +3 -0
  67. package/dist/types/server/operations/registry.d.ts +11 -0
  68. package/dist/types/server/operations/types.d.ts +1 -1
  69. package/dist/types/server/scope-fence.d.ts +62 -0
  70. package/dist/types/server/server.d.ts +0 -12
  71. package/dist/types/shared/agent-presence.d.ts +130 -0
  72. package/dist/types/shared/approval-gates.d.ts +19 -0
  73. package/dist/types/shared/auto-arrange.d.ts +0 -1
  74. package/dist/types/shared/ax-kinds.d.ts +11 -0
  75. package/dist/types/shared/human-presence.d.ts +31 -0
  76. package/dist/types/shared/iframe-probe.d.ts +8 -0
  77. package/dist/types/shared/scope-fence.d.ts +21 -0
  78. package/docs/RELEASE.md +6 -0
  79. package/docs/ax-host-adapter-contract.md +30 -14
  80. package/docs/cli.md +37 -1
  81. package/docs/environment.md +3 -0
  82. package/docs/http-api.md +222 -5
  83. package/docs/mcp.md +6 -5
  84. package/docs/node-types.md +3 -1
  85. package/docs/plans/plan-009-tech-debt-backlog.md +13 -0
  86. package/docs/screenshot.png +0 -0
  87. package/docs/screenshots/multi-agent-live.png +0 -0
  88. package/docs/screenshots/workbench-live-dark.png +0 -0
  89. package/docs/screenshots/workbench-live-light.png +0 -0
  90. package/docs/sdk.md +1 -1
  91. package/package.json +1 -1
  92. package/skills/pmx-canvas/SKILL.md +103 -16
  93. package/skills/pmx-canvas/references/codex-app-adapter.md +28 -0
  94. package/skills/pmx-canvas/references/full-reference.md +55 -16
  95. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +41 -24
  96. package/skills/pmx-canvas-orchestration/SKILL.md +56 -2
  97. package/skills/pmx-canvas-testing/SKILL.md +17 -0
  98. package/src/cli/agent.ts +1 -0
  99. package/src/cli/commands/copilot.ts +15 -8
  100. package/src/cli/commands/nodes.ts +1 -19
  101. package/src/cli/commands/pump.ts +252 -0
  102. package/src/cli/index.ts +1 -5
  103. package/src/cli/shared.ts +1 -1
  104. package/src/client/App.tsx +209 -585
  105. package/src/client/canvas/AgentPresenceLayer.tsx +74 -0
  106. package/src/client/canvas/AttentionHistory.tsx +0 -10
  107. package/src/client/canvas/BarHint.tsx +49 -0
  108. package/src/client/canvas/CanvasNode.tsx +488 -136
  109. package/src/client/canvas/CanvasViewport.tsx +190 -94
  110. package/src/client/canvas/CommandBar.tsx +140 -0
  111. package/src/client/canvas/CommandPalette.tsx +133 -58
  112. package/src/client/canvas/ConnectionBanner.tsx +65 -0
  113. package/src/client/canvas/ContextMenu.tsx +88 -27
  114. package/src/client/canvas/ContextPinBar.tsx +2 -2
  115. package/src/client/canvas/EdgeLayer.tsx +121 -38
  116. package/src/client/canvas/EmptyState.tsx +93 -0
  117. package/src/client/canvas/ExpandedNodeOverlay.tsx +64 -118
  118. package/src/client/canvas/ExternalWriters.tsx +308 -0
  119. package/src/client/canvas/FocusFieldLayer.tsx +12 -1
  120. package/src/client/canvas/HumanPresenceLayer.tsx +45 -0
  121. package/src/client/canvas/IntentLayer.tsx +1 -1
  122. package/src/client/canvas/Minimap.tsx +117 -190
  123. package/src/client/canvas/ScopeFenceLayer.tsx +38 -0
  124. package/src/client/canvas/SelectionBar.tsx +175 -23
  125. package/src/client/canvas/SessionPanel.tsx +481 -0
  126. package/src/client/canvas/SessionReceipt.tsx +141 -0
  127. package/src/client/canvas/ShortcutOverlay.tsx +21 -6
  128. package/src/client/canvas/SnapshotPanel.tsx +166 -80
  129. package/src/client/canvas/TextPrompt.tsx +128 -0
  130. package/src/client/canvas/ToolRail.tsx +447 -0
  131. package/src/client/canvas/TopBar.tsx +308 -0
  132. package/src/client/canvas/auto-fit.ts +1 -5
  133. package/src/client/canvas/canvas-area.ts +41 -0
  134. package/src/client/canvas/create-in-view.ts +48 -0
  135. package/src/client/canvas/import-files.ts +64 -0
  136. package/src/client/canvas/kind-colors.ts +22 -0
  137. package/src/client/canvas/snap-guides.ts +1 -1
  138. package/src/client/canvas/use-focus-trap.ts +54 -0
  139. package/src/client/canvas/use-now.ts +17 -0
  140. package/src/client/canvas/use-pan-zoom.ts +13 -2
  141. package/src/client/icons.tsx +57 -18
  142. package/src/client/nodes/GroupNode.tsx +9 -28
  143. package/src/client/nodes/InlineFormatBar.tsx +4 -3
  144. package/src/client/nodes/InlineMarkdownEditor.tsx +27 -0
  145. package/src/client/nodes/MarkdownNode.tsx +59 -24
  146. package/src/client/nodes/MdFormatBar.tsx +2 -4
  147. package/src/client/nodes/inline-editor-commands.ts +17 -6
  148. package/src/client/nodes/use-surface-frame.ts +3 -1
  149. package/src/client/state/attention-bridge.ts +3 -0
  150. package/src/client/state/canvas-store.ts +273 -84
  151. package/src/client/state/human-store.ts +175 -0
  152. package/src/client/state/iframe-mode.ts +33 -9
  153. package/src/client/state/intent-bridge.ts +75 -13
  154. package/src/client/state/node-factory.ts +2 -8
  155. package/src/client/state/presence-store.ts +168 -0
  156. package/src/client/state/session-store.ts +419 -0
  157. package/src/client/state/sse-bridge.ts +93 -30
  158. package/src/client/theme/global.css +3069 -796
  159. package/src/client/types.ts +3 -1
  160. package/src/client/utils/platform.ts +6 -1
  161. package/src/mcp/canvas-access.ts +2 -2
  162. package/src/server/agent-presence.ts +755 -0
  163. package/src/server/ax-context.ts +45 -0
  164. package/src/server/ax-gate-ttl.ts +61 -0
  165. package/src/server/ax-interaction.ts +29 -1
  166. package/src/server/ax-state-manager.ts +78 -6
  167. package/src/server/ax-state.ts +82 -15
  168. package/src/server/ax-wait.ts +33 -0
  169. package/src/server/canvas-db.ts +103 -33
  170. package/src/server/canvas-operations.ts +17 -8
  171. package/src/server/canvas-state.ts +131 -58
  172. package/src/server/canvas-validation.ts +3 -53
  173. package/src/server/demo-state.json +2 -70
  174. package/src/server/html-surface.ts +11 -2
  175. package/src/server/human-presence.ts +160 -0
  176. package/src/server/index.ts +107 -4
  177. package/src/server/intent-registry.ts +13 -0
  178. package/src/server/mutation-history.ts +47 -2
  179. package/src/server/operations/composites.ts +6 -3
  180. package/src/server/operations/http.ts +10 -1
  181. package/src/server/operations/index.ts +4 -0
  182. package/src/server/operations/invoker.ts +33 -6
  183. package/src/server/operations/ops/ax-presence.ts +62 -0
  184. package/src/server/operations/ops/ax-read.ts +8 -0
  185. package/src/server/operations/ops/ax-state.ts +106 -1
  186. package/src/server/operations/ops/ax-timeline.ts +54 -5
  187. package/src/server/operations/ops/ax-work.ts +46 -0
  188. package/src/server/operations/ops/batch.ts +3 -1
  189. package/src/server/operations/ops/edges.ts +88 -2
  190. package/src/server/operations/ops/groups.ts +4 -3
  191. package/src/server/operations/ops/human-presence.ts +35 -0
  192. package/src/server/operations/ops/json-render.ts +11 -1
  193. package/src/server/operations/ops/nodes.ts +65 -16
  194. package/src/server/operations/ops/query.ts +1 -0
  195. package/src/server/operations/ops/snapshots.ts +16 -0
  196. package/src/server/operations/registry.ts +196 -1
  197. package/src/server/operations/types.ts +1 -1
  198. package/src/server/scope-fence.ts +207 -0
  199. package/src/server/server.ts +177 -79
  200. package/src/server/trace-manager.ts +0 -2
  201. package/src/server/web-artifacts.ts +0 -1
  202. package/src/shared/agent-presence.ts +162 -0
  203. package/src/shared/approval-gates.ts +35 -0
  204. package/src/shared/auto-arrange.ts +121 -13
  205. package/src/shared/ax-kinds.ts +23 -0
  206. package/src/shared/human-presence.ts +30 -0
  207. package/src/shared/iframe-probe.ts +8 -0
  208. package/src/shared/scope-fence.ts +31 -0
  209. package/dist/types/client/canvas/DockedNode.d.ts +0 -4
  210. package/src/client/canvas/DockedNode.tsx +0 -165
@@ -65,7 +65,6 @@ const SCHEMA_SQL = `
65
65
  z_index INTEGER NOT NULL DEFAULT 0,
66
66
  collapsed INTEGER NOT NULL DEFAULT 0,
67
67
  pinned INTEGER NOT NULL DEFAULT 0,
68
- dock_position TEXT,
69
68
  data TEXT NOT NULL
70
69
  );
71
70
 
@@ -119,7 +118,6 @@ const SCHEMA_SQL = `
119
118
  z_index INTEGER NOT NULL DEFAULT 0,
120
119
  collapsed INTEGER NOT NULL DEFAULT 0,
121
120
  pinned INTEGER NOT NULL DEFAULT 0,
122
- dock_position TEXT,
123
121
  data TEXT NOT NULL,
124
122
  PRIMARY KEY (snapshot_id, id)
125
123
  );
@@ -209,6 +207,13 @@ const SCHEMA_SQL = `
209
207
  );
210
208
  CREATE INDEX IF NOT EXISTS idx_ax_steering_seq ON ax_steering (seq);
211
209
 
210
+ CREATE TABLE IF NOT EXISTS ax_steering_deliveries (
211
+ steering_id TEXT NOT NULL,
212
+ consumer TEXT NOT NULL,
213
+ at TEXT NOT NULL,
214
+ PRIMARY KEY (steering_id, consumer)
215
+ );
216
+
212
217
  CREATE TABLE IF NOT EXISTS ax_host_capabilities (
213
218
  host TEXT PRIMARY KEY,
214
219
  reported_at TEXT NOT NULL,
@@ -397,18 +402,17 @@ export function saveStateToDB(db: Database, state: PersistedCanvasState): void {
397
402
 
398
403
  // Save nodes
399
404
  const upsertNode = db.prepare(
400
- `INSERT INTO nodes (id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned, dock_position, data)
401
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
405
+ `INSERT INTO nodes (id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned, data)
406
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
402
407
  ON CONFLICT(id) DO UPDATE SET
403
408
  type = excluded.type, pos_x = excluded.pos_x, pos_y = excluded.pos_y,
404
409
  width = excluded.width, height = excluded.height, z_index = excluded.z_index,
405
- collapsed = excluded.collapsed, pinned = excluded.pinned,
406
- dock_position = excluded.dock_position, data = excluded.data
410
+ collapsed = excluded.collapsed, pinned = excluded.pinned, data = excluded.data
407
411
  WHERE type IS NOT excluded.type OR pos_x IS NOT excluded.pos_x
408
412
  OR pos_y IS NOT excluded.pos_y OR width IS NOT excluded.width
409
413
  OR height IS NOT excluded.height OR z_index IS NOT excluded.z_index
410
414
  OR collapsed IS NOT excluded.collapsed OR pinned IS NOT excluded.pinned
411
- OR dock_position IS NOT excluded.dock_position OR data IS NOT excluded.data`,
415
+ OR data IS NOT excluded.data`,
412
416
  );
413
417
  for (const node of state.nodes) {
414
418
  upsertNode.run(
@@ -421,7 +425,6 @@ export function saveStateToDB(db: Database, state: PersistedCanvasState): void {
421
425
  node.zIndex,
422
426
  node.collapsed ? 1 : 0,
423
427
  node.pinned ? 1 : 0,
424
- node.dockPosition,
425
428
  JSON.stringify(node.data),
426
429
  );
427
430
  }
@@ -535,7 +538,6 @@ export function loadStateFromDB(db: Database): PersistedCanvasState | null {
535
538
  z_index: number;
536
539
  collapsed: number;
537
540
  pinned: number;
538
- dock_position: string | null;
539
541
  data: string;
540
542
  }
541
543
  const nodeRows = db.query<NodeRow, []>('SELECT * FROM nodes').all();
@@ -547,7 +549,6 @@ export function loadStateFromDB(db: Database): PersistedCanvasState | null {
547
549
  zIndex: row.z_index,
548
550
  collapsed: row.collapsed === 1,
549
551
  pinned: row.pinned === 1,
550
- dockPosition: row.dock_position as CanvasNodeState['dockPosition'],
551
552
  data: JSON.parse(row.data) as Record<string, unknown>,
552
553
  }));
553
554
 
@@ -655,8 +656,8 @@ export function saveSnapshotToDB(db: Database, snapshot: CanvasSnapshot, state:
655
656
 
656
657
  // Insert snapshot nodes
657
658
  const insertNode = db.prepare(
658
- `INSERT INTO snapshot_nodes (snapshot_id, id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned, dock_position, data)
659
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
659
+ `INSERT INTO snapshot_nodes (snapshot_id, id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned, data)
660
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
660
661
  );
661
662
  for (const node of state.nodes) {
662
663
  insertNode.run(
@@ -670,7 +671,6 @@ export function saveSnapshotToDB(db: Database, snapshot: CanvasSnapshot, state:
670
671
  node.zIndex,
671
672
  node.collapsed ? 1 : 0,
672
673
  node.pinned ? 1 : 0,
673
- node.dockPosition,
674
674
  JSON.stringify(node.data),
675
675
  );
676
676
  }
@@ -780,7 +780,6 @@ export function loadSnapshotFromDB(
780
780
  z_index: number;
781
781
  collapsed: number;
782
782
  pinned: number;
783
- dock_position: string | null;
784
783
  data: string;
785
784
  }
786
785
  const nodeRows = db
@@ -794,7 +793,6 @@ export function loadSnapshotFromDB(
794
793
  zIndex: row.z_index,
795
794
  collapsed: row.collapsed === 1,
796
795
  pinned: row.pinned === 1,
797
- dockPosition: row.dock_position as CanvasNodeState['dockPosition'],
798
796
  data: JSON.parse(row.data) as Record<string, unknown>,
799
797
  }));
800
798
 
@@ -918,6 +916,10 @@ export function listSnapshotsFromDB(db: Database, options: CanvasSnapshotListOpt
918
916
  }));
919
917
  }
920
918
 
919
+ export function renameSnapshotInDB(db: Database, id: string, name: string): boolean {
920
+ return db.run('UPDATE snapshots SET name = ? WHERE id = ?', [name, id]).changes > 0;
921
+ }
922
+
921
923
  export function deleteSnapshotFromDB(db: Database, id: string): boolean {
922
924
  const transaction = db.transaction(() => {
923
925
  db.run('DELETE FROM snapshot_nodes WHERE snapshot_id = ?', [id]);
@@ -979,6 +981,9 @@ function trimAxTable(db: Database, table: 'ax_events' | 'ax_evidence' | 'ax_stee
979
981
  db.run(`DELETE FROM ${table} WHERE seq <= (SELECT seq FROM ${table} ORDER BY seq DESC LIMIT 1 OFFSET ?)`, [
980
982
  AX_TIMELINE_RETENTION,
981
983
  ]);
984
+ if (table === 'ax_steering') {
985
+ db.run('DELETE FROM ax_steering_deliveries WHERE steering_id NOT IN (SELECT id FROM ax_steering)');
986
+ }
982
987
  }
983
988
 
984
989
  function readLastSeq(db: Database, table: 'ax_events' | 'ax_evidence' | 'ax_steering'): number {
@@ -1036,10 +1041,28 @@ export function appendAxSteeringToDB(db: Database, s: Omit<PmxAxSteeringMessage,
1036
1041
  return { ...s, seq };
1037
1042
  }
1038
1043
 
1039
- export function markAxSteeringDeliveredInDB(db: Database, id: string): boolean {
1040
- // Compare-and-set: true ONLY on the undelivered→delivered transition. Two
1041
- // adapters racing the same steer see exactly one true — the loser (and any
1042
- // re-mark) gets false and must not treat the message as its own delivery.
1044
+ export function markAxSteeringDeliveredInDB(db: Database, id: string, consumer?: string | null): boolean {
1045
+ // ADDRESSED steer (target set): global compare-and-set as before — one
1046
+ // legitimate recipient, true only on the undelivered→delivered transition.
1047
+ // BROADCAST + consumer: per-consumer delivery ("all workers: stop" must
1048
+ // reach every worker) — the mark records THIS consumer's pickup and the
1049
+ // message stays pending for everyone else; true iff this consumer had not
1050
+ // marked it before. BROADCAST without a consumer: the legacy anonymous ack
1051
+ // (global CAS) that hides it from all consumers.
1052
+ const row = db.query<{ target: string | null }, [string]>('SELECT target FROM ax_steering WHERE id = ?').get(id);
1053
+ if (!row) return false;
1054
+ // An addressed steer belongs to its target. A different named consumer marking
1055
+ // it delivered would silently swallow the human's instruction (the target then
1056
+ // never sees it). A consumer-less mark is still the single legitimate host ack.
1057
+ if (row.target !== null && consumer && consumer !== row.target) return false;
1058
+ if (row.target === null && consumer) {
1059
+ const r = db.run('INSERT OR IGNORE INTO ax_steering_deliveries (steering_id, consumer, at) VALUES (?, ?, ?)', [
1060
+ id,
1061
+ consumer,
1062
+ new Date().toISOString(),
1063
+ ]);
1064
+ return r.changes > 0;
1065
+ }
1043
1066
  const r = db.run('UPDATE ax_steering SET delivered = 1 WHERE id = ? AND delivered = 0', [id]);
1044
1067
  return r.changes > 0;
1045
1068
  }
@@ -1112,6 +1135,18 @@ interface AxSteeringRow {
1112
1135
  target: string | null;
1113
1136
  }
1114
1137
 
1138
+ /**
1139
+ * Broadcasts age out of PENDING (they stay on the timeline): per-consumer
1140
+ * delivery otherwise means every broadcast greets every FUTURE consumer key
1141
+ * forever — a brand-new pump inherited two-day-old "all workers" calls.
1142
+ * Addressed steers never expire here; they wait for their named consumer.
1143
+ */
1144
+ export const BROADCAST_PENDING_TTL_MS = 15 * 60_000;
1145
+
1146
+ function broadcastCutoffIso(): string {
1147
+ return new Date(Date.now() - BROADCAST_PENDING_TTL_MS).toISOString();
1148
+ }
1149
+
1115
1150
  function mapAxSteeringRow(r: AxSteeringRow): PmxAxSteeringMessage | null {
1116
1151
  return normalizeAxSteeringMessage({
1117
1152
  ...r,
@@ -1129,7 +1164,27 @@ export function loadAxSteeringFromDB(
1129
1164
  ? 'SELECT * FROM ax_steering WHERE delivered = 0 ORDER BY seq DESC LIMIT ?'
1130
1165
  : 'SELECT * FROM ax_steering ORDER BY seq DESC LIMIT ?';
1131
1166
  const rows = db.query<AxSteeringRow, [number]>(sql).all(clampTimelineLimit(q.limit));
1132
- return rows.map(mapAxSteeringRow).filter((s): s is PmxAxSteeringMessage => s !== null);
1167
+ const deliveredTo = new Map<string, string[]>();
1168
+ for (const row of db
1169
+ .query<{ steering_id: string; consumer: string }, []>('SELECT steering_id, consumer FROM ax_steering_deliveries')
1170
+ .all()) {
1171
+ if (!deliveredTo.has(row.steering_id)) deliveredTo.set(row.steering_id, []);
1172
+ deliveredTo.get(row.steering_id)!.push(row.consumer);
1173
+ }
1174
+ return rows
1175
+ .map((row) => {
1176
+ const mapped = mapAxSteeringRow(row);
1177
+ if (!mapped) return null;
1178
+ const pickups = deliveredTo.get(mapped.id) ?? [];
1179
+ // A broadcast reads as delivered once ANY consumer picked it up; the
1180
+ // per-consumer list is exposed for fleet UIs ("picked up by 3").
1181
+ return {
1182
+ ...mapped,
1183
+ delivered: mapped.delivered || pickups.length > 0,
1184
+ ...(pickups.length > 0 ? { deliveredTo: pickups } : {}),
1185
+ };
1186
+ })
1187
+ .filter((s): s is PmxAxSteeringMessage => s !== null);
1133
1188
  }
1134
1189
 
1135
1190
  export function loadPendingAxSteeringFromDB(
@@ -1142,13 +1197,20 @@ export function loadPendingAxSteeringFromDB(
1142
1197
  const limit = clampTimelineLimit(options.limit);
1143
1198
  const rows = options.consumer
1144
1199
  ? db
1145
- .query<AxSteeringRow, [string, string, string, number]>(
1146
- 'SELECT * FROM ax_steering WHERE delivered = 0 AND (source IS NULL OR source != ?) AND (agent_id IS NULL OR agent_id != ?) AND (target IS NULL OR target = ?) ORDER BY seq ASC LIMIT ?',
1200
+ .query<AxSteeringRow, [string, string, string, string, string, number]>(
1201
+ // A broadcast stays pending PER CONSUMER: it leaves this consumer's
1202
+ // queue only once THIS consumer marked it (or an anonymous global ack
1203
+ // set delivered).
1204
+ 'SELECT * FROM ax_steering s WHERE s.delivered = 0 AND (s.source IS NULL OR s.source != ?) AND (s.agent_id IS NULL OR s.agent_id != ?) AND (s.target IS NULL OR s.target = ?) AND (s.target IS NOT NULL OR s.created_at > ?) AND NOT EXISTS (SELECT 1 FROM ax_steering_deliveries d WHERE d.steering_id = s.id AND d.consumer = ?) ORDER BY s.seq ASC LIMIT ?',
1147
1205
  )
1148
- .all(options.consumer, options.consumer, options.consumer, limit)
1206
+ .all(options.consumer, options.consumer, options.consumer, broadcastCutoffIso(), options.consumer, limit)
1149
1207
  : db
1150
- .query<AxSteeringRow, [number]>('SELECT * FROM ax_steering WHERE delivered = 0 ORDER BY seq ASC LIMIT ?')
1151
- .all(limit);
1208
+ .query<AxSteeringRow, [string, number]>(
1209
+ // No consumer given: only broadcasts — addressed steering is claimable
1210
+ // solely by the consumer it names.
1211
+ 'SELECT * FROM ax_steering WHERE delivered = 0 AND target IS NULL AND created_at > ? ORDER BY seq ASC LIMIT ?',
1212
+ )
1213
+ .all(broadcastCutoffIso(), limit);
1152
1214
  return rows.map(mapAxSteeringRow).filter((s): s is PmxAxSteeringMessage => s !== null);
1153
1215
  }
1154
1216
 
@@ -1167,25 +1229,33 @@ export function loadNewestPendingAxSteeringFromDB(
1167
1229
  const limit = clampTimelineLimit(options.limit);
1168
1230
  const rows = options.consumer
1169
1231
  ? db
1170
- .query<AxSteeringRow, [string, string, string, number]>(
1171
- 'SELECT * FROM ax_steering WHERE delivered = 0 AND (source IS NULL OR source != ?) AND (agent_id IS NULL OR agent_id != ?) AND (target IS NULL OR target = ?) ORDER BY seq DESC LIMIT ?',
1232
+ .query<AxSteeringRow, [string, string, string, string, string, number]>(
1233
+ 'SELECT * FROM ax_steering s WHERE s.delivered = 0 AND (s.source IS NULL OR s.source != ?) AND (s.agent_id IS NULL OR s.agent_id != ?) AND (s.target IS NULL OR s.target = ?) AND (s.target IS NOT NULL OR s.created_at > ?) AND NOT EXISTS (SELECT 1 FROM ax_steering_deliveries d WHERE d.steering_id = s.id AND d.consumer = ?) ORDER BY s.seq DESC LIMIT ?',
1172
1234
  )
1173
- .all(options.consumer, options.consumer, options.consumer, limit)
1235
+ .all(options.consumer, options.consumer, options.consumer, broadcastCutoffIso(), options.consumer, limit)
1174
1236
  : db
1175
- .query<AxSteeringRow, [number]>('SELECT * FROM ax_steering WHERE delivered = 0 ORDER BY seq DESC LIMIT ?')
1237
+ .query<AxSteeringRow, [number]>(
1238
+ 'SELECT * FROM ax_steering WHERE delivered = 0 AND target IS NULL ORDER BY seq DESC LIMIT ?',
1239
+ )
1176
1240
  .all(limit);
1177
1241
  return rows.map(mapAxSteeringRow).filter((s): s is PmxAxSteeringMessage => s !== null);
1178
1242
  }
1179
1243
 
1180
1244
  /** Total undelivered steering for a consumer (loop-safe — excludes the consumer's own; target-scoped). */
1181
1245
  export function countPendingAxSteeringFromDB(db: Database, consumer?: string): number {
1246
+ // Must mirror loadPendingAxSteeringFromDB exactly (per-consumer deliveries,
1247
+ // broadcast TTL) — the roster shows this as "what would this consumer claim".
1182
1248
  const n = consumer
1183
1249
  ? db
1184
- .query<{ n: number }, [string, string, string]>(
1185
- 'SELECT COUNT(*) AS n FROM ax_steering WHERE delivered = 0 AND (source IS NULL OR source != ?) AND (agent_id IS NULL OR agent_id != ?) AND (target IS NULL OR target = ?)',
1250
+ .query<{ n: number }, [string, string, string, string, string]>(
1251
+ 'SELECT COUNT(*) AS n FROM ax_steering s WHERE s.delivered = 0 AND (s.source IS NULL OR s.source != ?) AND (s.agent_id IS NULL OR s.agent_id != ?) AND (s.target IS NULL OR s.target = ?) AND (s.target IS NOT NULL OR s.created_at > ?) AND NOT EXISTS (SELECT 1 FROM ax_steering_deliveries d WHERE d.steering_id = s.id AND d.consumer = ?)',
1252
+ )
1253
+ .get(consumer, consumer, consumer, broadcastCutoffIso(), consumer)?.n
1254
+ : db
1255
+ .query<{ n: number }, [string]>(
1256
+ 'SELECT COUNT(*) AS n FROM ax_steering WHERE delivered = 0 AND target IS NULL AND created_at > ?',
1186
1257
  )
1187
- .get(consumer, consumer, consumer)?.n
1188
- : db.query<{ n: number }, []>('SELECT COUNT(*) AS n FROM ax_steering WHERE delivered = 0').get()?.n;
1258
+ .get(broadcastCutoffIso())?.n;
1189
1259
  return Number(n ?? 0);
1190
1260
  }
1191
1261
 
@@ -946,7 +946,6 @@ export function addCanvasNode(input: CanvasAddNodeInput): {
946
946
  zIndex: 1,
947
947
  collapsed: false,
948
948
  pinned: false,
949
- dockPosition: null,
950
949
  data,
951
950
  };
952
951
 
@@ -1133,7 +1132,7 @@ export function removeCanvasNode(id: string): {
1133
1132
  }
1134
1133
 
1135
1134
  function isArrangeLocked(node: CanvasNodeState): boolean {
1136
- return node.pinned || node.dockPosition !== null || node.data.arrangeLocked === true;
1135
+ return node.pinned || node.data.arrangeLocked === true;
1137
1136
  }
1138
1137
 
1139
1138
  function collectArrangeExcludedNodeIds(nodes: CanvasNodeState[]): Set<string> {
@@ -1219,7 +1218,7 @@ function rectsOverlap(a: ArrangeObstacleRect, b: ArrangeObstacleRect): boolean {
1219
1218
 
1220
1219
  function collectGridArrangeObstacles(nodes: CanvasNodeState[], excludedIds: Set<string>): ArrangeObstacleRect[] {
1221
1220
  return nodes
1222
- .filter((node) => excludedIds.has(node.id) && node.dockPosition === null)
1221
+ .filter((node) => excludedIds.has(node.id))
1223
1222
  .map((node) => ({
1224
1223
  id: node.id,
1225
1224
  position: { ...node.position },
@@ -1247,7 +1246,7 @@ function shiftGridUpdatesBelowObstacles(
1247
1246
  ): CanvasNodeUpdate[] {
1248
1247
  if (updates.length === 0 || obstacles.length === 0) return updates;
1249
1248
 
1250
- // Grid arrange only sees movable nodes, so preserved locked/docked-group frames
1249
+ // Grid arrange only sees movable nodes, so preserved locked-group frames
1251
1250
  // need a separate obstacle pass before applying the planned positions.
1252
1251
  const nodesById = new Map(nodes.map((node) => [node.id, node]));
1253
1252
  let shifted = updates;
@@ -1465,6 +1464,20 @@ export function addCanvasEdge(input: {
1465
1464
  return edge;
1466
1465
  }
1467
1466
 
1467
+ export function updateCanvasEdge(
1468
+ id: string,
1469
+ patch: Partial<Pick<CanvasEdge, 'type' | 'label' | 'style' | 'animated'>>,
1470
+ ): CanvasEdge {
1471
+ if (!canvasState.getEdges().some((edge) => edge.id === id)) {
1472
+ throw new Error(`Edge "${id}" not found.`);
1473
+ }
1474
+ const updated = canvasState.updateEdge(id, patch);
1475
+ if (!updated) {
1476
+ throw new Error('Retyping would duplicate an existing edge.');
1477
+ }
1478
+ return updated;
1479
+ }
1480
+
1468
1481
  export function removeCanvasEdge(id: string): { removed: boolean } {
1469
1482
  return { removed: canvasState.removeEdge(id) };
1470
1483
  }
@@ -1512,7 +1525,6 @@ export function createCanvasGroup(input: CanvasCreateGroupInput): { id: string;
1512
1525
  zIndex: 0,
1513
1526
  collapsed: false,
1514
1527
  pinned: false,
1515
- dockPosition: null,
1516
1528
  data,
1517
1529
  });
1518
1530
 
@@ -1583,7 +1595,6 @@ export function createCanvasJsonRenderNode(input: JsonRenderNodeInput): {
1583
1595
  zIndex: 1,
1584
1596
  collapsed: false,
1585
1597
  pinned: false,
1586
- dockPosition: null,
1587
1598
  data: createJsonRenderNodeData(id, input.title?.trim() || inferJsonRenderNodeTitle(spec), spec, {
1588
1599
  viewerType: 'json-render',
1589
1600
  ...(input.strictSize ? { strictSize: true } : {}),
@@ -1627,7 +1638,6 @@ export function createCanvasStreamingJsonRenderNode(input: {
1627
1638
  zIndex: 1,
1628
1639
  collapsed: false,
1629
1640
  pinned: false,
1630
- dockPosition: null,
1631
1641
  data: createJsonRenderNodeData(id, input.title?.trim() || 'Streaming', spec, {
1632
1642
  viewerType: 'json-render',
1633
1643
  streamStatus: 'open',
@@ -1703,7 +1713,6 @@ export function createCanvasGraphNode(input: GraphNodeInput): {
1703
1713
  zIndex: 1,
1704
1714
  collapsed: false,
1705
1715
  pinned: false,
1706
- dockPosition: null,
1707
1716
  data: createJsonRenderNodeData(id, title, spec, {
1708
1717
  viewerType: 'graph',
1709
1718
  graphConfig: buildGraphConfig(input),