pmx-canvas 0.1.35 → 0.2.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 (100) hide show
  1. package/CHANGELOG.md +461 -0
  2. package/Readme.md +14 -2
  3. package/dist/canvas/index.js +82 -41
  4. package/dist/json-render/index.js +89 -334
  5. package/dist/types/client/nodes/ExtAppFrame.d.ts +2 -0
  6. package/dist/types/mcp/canvas-access.d.ts +12 -159
  7. package/dist/types/server/ax-context.d.ts +1 -1
  8. package/dist/types/server/ax-state-manager.d.ts +256 -0
  9. package/dist/types/server/ax-state.d.ts +29 -1
  10. package/dist/types/server/ax-wait.d.ts +23 -0
  11. package/dist/types/server/canvas-operations.d.ts +1 -12
  12. package/dist/types/server/canvas-state.d.ts +46 -14
  13. package/dist/types/server/html-surface.d.ts +7 -0
  14. package/dist/types/server/index.d.ts +66 -26
  15. package/dist/types/server/operations/composites.d.ts +121 -0
  16. package/dist/types/server/operations/http.d.ts +7 -0
  17. package/dist/types/server/operations/index.d.ts +8 -0
  18. package/dist/types/server/operations/invoker.d.ts +13 -0
  19. package/dist/types/server/operations/mcp.d.ts +15 -0
  20. package/dist/types/server/operations/ops/annotation.d.ts +2 -0
  21. package/dist/types/server/operations/ops/app.d.ts +33 -0
  22. package/dist/types/server/operations/ops/ax-await.d.ts +2 -0
  23. package/dist/types/server/operations/ops/ax-shared.d.ts +31 -0
  24. package/dist/types/server/operations/ops/ax-state.d.ts +2 -0
  25. package/dist/types/server/operations/ops/ax-timeline.d.ts +2 -0
  26. package/dist/types/server/operations/ops/ax-work.d.ts +2 -0
  27. package/dist/types/server/operations/ops/batch.d.ts +19 -0
  28. package/dist/types/server/operations/ops/edges.d.ts +2 -0
  29. package/dist/types/server/operations/ops/groups.d.ts +2 -0
  30. package/dist/types/server/operations/ops/json-render.d.ts +31 -0
  31. package/dist/types/server/operations/ops/nodes.d.ts +62 -0
  32. package/dist/types/server/operations/ops/query.d.ts +2 -0
  33. package/dist/types/server/operations/ops/snapshots.d.ts +2 -0
  34. package/dist/types/server/operations/ops/validate.d.ts +2 -0
  35. package/dist/types/server/operations/ops/viewport.d.ts +2 -0
  36. package/dist/types/server/operations/ops/webview.d.ts +2 -0
  37. package/dist/types/server/operations/registry.d.ts +15 -0
  38. package/dist/types/server/operations/types.d.ts +116 -0
  39. package/dist/types/server/operations/webview-runner.d.ts +69 -0
  40. package/docs/RELEASE.md +5 -0
  41. package/docs/adr-001-bun-only-runtime.md +46 -0
  42. package/docs/api-stability.md +57 -0
  43. package/docs/ax-host-adapter-contract.md +65 -0
  44. package/docs/ax-state-contract.md +72 -0
  45. package/docs/http-api.md +34 -2
  46. package/docs/mcp.md +64 -11
  47. package/docs/plans/plan-005-operation-registry.md +84 -0
  48. package/docs/plans/plan-006-mcp-tool-consolidation.md +109 -0
  49. package/docs/plans/plan-007-ax-domain.md +99 -0
  50. package/docs/plans/plan-008-registry-finish.md +91 -0
  51. package/docs/screenshot.png +0 -0
  52. package/docs/tech-debt-assessment-2026-06.md +90 -0
  53. package/package.json +3 -3
  54. package/skills/pmx-canvas/SKILL.md +233 -185
  55. package/skills/pmx-canvas/evals/evals.json +3 -3
  56. package/skills/pmx-canvas/references/codex-app-adapter.md +24 -11
  57. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +31 -1
  58. package/src/cli/agent.ts +52 -31
  59. package/src/client/nodes/ExtAppFrame.tsx +73 -5
  60. package/src/client/nodes/HtmlNode.tsx +12 -3
  61. package/src/client/nodes/McpAppNode.tsx +12 -3
  62. package/src/json-render/renderer/index.tsx +3 -0
  63. package/src/mcp/canvas-access.ts +43 -774
  64. package/src/mcp/server.ts +190 -2001
  65. package/src/server/ax-context.ts +7 -1
  66. package/src/server/ax-state-manager.ts +808 -0
  67. package/src/server/ax-state.ts +89 -2
  68. package/src/server/ax-wait.ts +56 -0
  69. package/src/server/canvas-operations.ts +2 -328
  70. package/src/server/canvas-schema.ts +2 -2
  71. package/src/server/canvas-state.ts +140 -382
  72. package/src/server/html-surface.ts +49 -11
  73. package/src/server/index.ts +136 -192
  74. package/src/server/operations/composites.ts +355 -0
  75. package/src/server/operations/http.ts +103 -0
  76. package/src/server/operations/index.ts +65 -0
  77. package/src/server/operations/invoker.ts +87 -0
  78. package/src/server/operations/mcp.ts +221 -0
  79. package/src/server/operations/ops/annotation.ts +60 -0
  80. package/src/server/operations/ops/app.ts +447 -0
  81. package/src/server/operations/ops/ax-await.ts +216 -0
  82. package/src/server/operations/ops/ax-shared.ts +38 -0
  83. package/src/server/operations/ops/ax-state.ts +249 -0
  84. package/src/server/operations/ops/ax-timeline.ts +381 -0
  85. package/src/server/operations/ops/ax-work.ts +635 -0
  86. package/src/server/operations/ops/batch.ts +365 -0
  87. package/src/server/operations/ops/edges.ts +166 -0
  88. package/src/server/operations/ops/groups.ts +176 -0
  89. package/src/server/operations/ops/json-render.ts +691 -0
  90. package/src/server/operations/ops/nodes.ts +1047 -0
  91. package/src/server/operations/ops/query.ts +281 -0
  92. package/src/server/operations/ops/snapshots.ts +366 -0
  93. package/src/server/operations/ops/validate.ts +37 -0
  94. package/src/server/operations/ops/viewport.ts +219 -0
  95. package/src/server/operations/ops/webview.ts +339 -0
  96. package/src/server/operations/registry.ts +79 -0
  97. package/src/server/operations/types.ts +150 -0
  98. package/src/server/operations/webview-runner.ts +77 -0
  99. package/src/server/server.ts +253 -2170
  100. package/src/server/web-artifacts.ts +6 -2
@@ -29,17 +29,6 @@ import {
29
29
  isDbPopulated,
30
30
  checkpointCanvasDb,
31
31
  finalizeCanvasDbForClose,
32
- appendAxEventToDB,
33
- appendAxEvidenceToDB,
34
- appendAxSteeringToDB,
35
- markAxSteeringDeliveredInDB,
36
- loadAxEventsFromDB,
37
- loadAxEvidenceFromDB,
38
- loadAxSteeringFromDB,
39
- loadPendingAxSteeringFromDB,
40
- loadAxTimelineSummaryFromDB,
41
- upsertAxHostCapabilityToDB,
42
- loadAxHostCapabilityFromDB,
43
32
  type PersistedCanvasState,
44
33
  type CanvasTheme,
45
34
  type AxTimelineQuery,
@@ -55,21 +44,7 @@ import {
55
44
  } from './placement.js';
56
45
  import {
57
46
  createEmptyAxState,
58
- createEmptyAxHostCapability,
59
- normalizeAxState,
60
- normalizeAxHostCapability,
61
- createAxWorkItem,
62
- createAxApprovalGate,
63
- createAxReviewAnnotation,
64
- createAxEvent,
65
- createAxEvidence,
66
- createAxSteeringMessage,
67
- createAxElicitation,
68
- createAxModeRequest,
69
- isAxCommand,
70
- listAxCommands,
71
- AX_COMMAND_REGISTRY,
72
- normalizeAxPolicy,
47
+ type PmxAxActivityKind,
73
48
  type PmxAxElicitation,
74
49
  type PmxAxModeRequest,
75
50
  type PmxAxMode,
@@ -95,6 +70,7 @@ import {
95
70
  type PmxAxHostCapability,
96
71
  type PmxAxTimelineSummary,
97
72
  } from './ax-state.js';
73
+ import { AxStateManager } from './ax-state-manager.js';
98
74
 
99
75
  function logCanvasStateWarning(action: string, error: unknown, details?: Record<string, unknown>): void {
100
76
  console.warn(`[canvas-state] ${action}`, { error, ...(details ?? {}) });
@@ -327,20 +303,43 @@ class CanvasStateManager {
327
303
  private _viewport: ViewportState = { x: 0, y: 0, scale: 1 };
328
304
  private _theme: CanvasTheme = 'dark';
329
305
  private _contextPinnedNodeIds = new Set<string>();
330
- private _axState: PmxAxState = createEmptyAxState();
331
- private _axHostCapability: PmxAxHostCapability | null = null;
332
306
  private _workspaceRoot = process.cwd();
333
307
 
308
+ // ── AX state (canvas-bound + timeline + host partitions) ──────────
309
+ // Extracted into a dedicated manager (plan-007 Slice A). CanvasStateManager
310
+ // holds it and delegates its public AX methods so the SDK/HTTP/MCP surface is
311
+ // byte-stable; the manager receives the host hooks it needs as injected deps.
312
+ private readonly ax = new AxStateManager({
313
+ getNodeIds: () => this.currentNodeIdSet(),
314
+ getDb: () => this._db,
315
+ scheduleSave: () => this.scheduleSave(),
316
+ notifyChange: (type) => this.notifyChange(type),
317
+ recordMutation: (info) => this.recordMutation(info),
318
+ suppressed: (fn) => this.suppressed(fn),
319
+ });
320
+
334
321
  // ── Change listeners (for MCP resource notifications) ──────
335
322
  private _changeListeners: ((type: CanvasChangeType) => void)[] = [];
336
323
 
337
- /** Register a listener for state changes. Used by MCP server to emit resource notifications. */
338
- onChange(cb: (type: CanvasChangeType) => void): void {
324
+ /**
325
+ * Register a listener for state changes. Used by MCP server to emit resource
326
+ * notifications and by the blocking-wait endpoints to await an AX transition.
327
+ * Returns a disposer that unregisters the listener (callers that don't need it
328
+ * — e.g. the long-lived MCP subscription — may ignore the return value).
329
+ */
330
+ onChange(cb: (type: CanvasChangeType) => void): () => void {
339
331
  this._changeListeners.push(cb);
332
+ return () => {
333
+ const i = this._changeListeners.indexOf(cb);
334
+ if (i >= 0) this._changeListeners.splice(i, 1);
335
+ };
340
336
  }
341
337
 
342
338
  private notifyChange(type: CanvasChangeType): void {
343
- for (const cb of this._changeListeners) {
339
+ // Iterate a snapshot: a listener (e.g. a blocking-wait via onChange) may dispose
340
+ // itself synchronously here, and splicing the live array mid-iteration would skip
341
+ // the next listener for this notification.
342
+ for (const cb of [...this._changeListeners]) {
344
343
  try {
345
344
  cb(type);
346
345
  } catch (error) {
@@ -382,14 +381,6 @@ class CanvasStateManager {
382
381
  return new Set(this.nodes.keys());
383
382
  }
384
383
 
385
- private normalizeAxForCurrentNodes(state: unknown): PmxAxState {
386
- return normalizeAxState(state, this.currentNodeIdSet());
387
- }
388
-
389
- private applyAxState(state: PmxAxState): void {
390
- this._axState = this.normalizeAxForCurrentNodes(state);
391
- }
392
-
393
384
  private applyResolvedGroupBounds(
394
385
  group: CanvasNodeState,
395
386
  groupId: string,
@@ -885,13 +876,7 @@ class CanvasStateManager {
885
876
  /** Load canvas state from SQLite (or legacy JSON fallback). Call once on server startup. */
886
877
  loadFromDisk(options: LoadFromDiskOptions = {}): boolean {
887
878
  // Host capability lives in its own table (not snapshotted / not in PmxAxState).
888
- if (this._db) {
889
- try {
890
- this._axHostCapability = loadAxHostCapabilityFromDB(this._db);
891
- } catch (error) {
892
- logCanvasStateWarning('load host capability failed', error, {});
893
- }
894
- }
879
+ this.ax.loadHostCapabilityFromDb();
895
880
  // Try SQLite first (only if DB has been populated)
896
881
  if (this._db && isDbPopulated(this._db)) {
897
882
  try {
@@ -1015,7 +1000,7 @@ class CanvasStateManager {
1015
1000
  this.edges.clear();
1016
1001
  this.annotations.clear();
1017
1002
  this._contextPinnedNodeIds.clear();
1018
- this._axState = createEmptyAxState();
1003
+ this.ax.resetCanvasBound();
1019
1004
 
1020
1005
  this._viewport = {
1021
1006
  x: state.viewport?.x ?? 0,
@@ -1046,7 +1031,7 @@ class CanvasStateManager {
1046
1031
  if (this.nodes.has(pinId)) this._contextPinnedNodeIds.add(pinId);
1047
1032
  }
1048
1033
  }
1049
- this._axState = this.normalizeAxForCurrentNodes(state.ax);
1034
+ this.ax.applyPersistedAx(state.ax);
1050
1035
  }
1051
1036
 
1052
1037
  private readResolvedSnapshot(idOrName: string): {
@@ -1442,11 +1427,41 @@ class CanvasStateManager {
1442
1427
  this.nodes.delete(id);
1443
1428
  this.removeEdgesForNode(id);
1444
1429
  this._contextPinnedNodeIds.delete(id);
1445
- this.applyAxState(this._axState);
1430
+ // Re-normalize canvas-bound AX against the surviving node set. This strips the
1431
+ // dangling node ref from work items / approval gates / elicitations / mode
1432
+ // requests (re-anchored) and drops node-anchored review annotations (removed).
1433
+ // Previously SILENT — now audited (plan-007 Slice A): if the deleted node
1434
+ // orphaned anything, record one `note` timeline event so the human and a
1435
+ // resuming agent can see the work that changed instead of it changing quietly.
1436
+ const orphaned = this.ax.revalidateAfterNodeRemoval(id);
1446
1437
  this.scheduleSave();
1447
1438
  this.notifyChange('nodes');
1448
1439
  this.notifyChange('pins');
1449
1440
  this.notifyChange('ax');
1441
+ // Only record the audit note on a real (user-initiated) deletion. Undo/redo
1442
+ // replay removeNode inside `suppressed()` (_suppressRecordingDepth > 0); the
1443
+ // original deletion already recorded the note, so replaying must NOT append a
1444
+ // duplicate (the timeline is append-only). `revalidateAfterNodeRemoval` above
1445
+ // still runs unconditionally — only the timeline note is gated.
1446
+ const affected = orphaned.reanchoredIds.length > 0 || orphaned.removedReviewIds.length > 0 || orphaned.reanchoredFocus;
1447
+ if (existing && this._suppressRecordingDepth === 0 && affected) {
1448
+ const title = (existing.data.title as string) ?? id;
1449
+ const focusNote = orphaned.reanchoredFocus ? ' (focus anchor cleared)' : '';
1450
+ this.recordAxEvent(
1451
+ {
1452
+ kind: 'note',
1453
+ summary: `Node "${title}" deleted — re-anchored ${orphaned.reanchoredIds.length} AX item(s), removed ${orphaned.removedReviewIds.length} node-anchored review annotation(s).${focusNote}`,
1454
+ data: {
1455
+ systemEvent: 'ax-node-orphan',
1456
+ removedNodeId: id,
1457
+ reanchoredIds: orphaned.reanchoredIds,
1458
+ removedReviewIds: orphaned.removedReviewIds,
1459
+ reanchoredFocus: orphaned.reanchoredFocus,
1460
+ },
1461
+ },
1462
+ { source: 'system' },
1463
+ );
1464
+ }
1450
1465
  if (cloned) {
1451
1466
  this.recordMutation({
1452
1467
  operationType: 'removeNode',
@@ -1455,7 +1470,7 @@ class CanvasStateManager {
1455
1470
  inverse: this.suppressed(() => {
1456
1471
  this.addNode(structuredClone(cloned));
1457
1472
  for (const edge of connectedEdges) this.addEdge(structuredClone(edge));
1458
- this.applyAxState(oldAxState);
1473
+ this.ax.applyPersistedAx(oldAxState);
1459
1474
  this.scheduleSave();
1460
1475
  this.notifyChange('ax');
1461
1476
  }),
@@ -1714,73 +1729,35 @@ class CanvasStateManager {
1714
1729
  return new Set(this._contextPinnedNodeIds);
1715
1730
  }
1716
1731
 
1732
+ // ── AX state delegation (canvas-bound + timeline + host) ──────────
1733
+ // All AX state lives in `this.ax` (AxStateManager); these are byte-stable
1734
+ // delegations so SDK/HTTP/MCP keep calling canvasState.<method>(...) unchanged.
1717
1735
  getAxState(): PmxAxState {
1718
- return structuredClone(this.normalizeAxForCurrentNodes(this._axState));
1736
+ return this.ax.getAxState();
1719
1737
  }
1720
1738
 
1721
1739
  getAxFocus(): PmxAxFocusState {
1722
- return this.getAxState().focus;
1740
+ return this.ax.getAxFocus();
1723
1741
  }
1724
1742
 
1725
1743
  setAxFocus(nodeIds: string[], options: { source?: PmxAxSource; recordHistory?: boolean } = {}): PmxAxFocusState {
1726
- const oldAxState = this.getAxState();
1727
- const nextAxState: PmxAxState = {
1728
- ...oldAxState,
1729
- focus: {
1730
- nodeIds,
1731
- primaryNodeId: nodeIds[0] ?? null,
1732
- updatedAt: new Date().toISOString(),
1733
- source: options.source ?? 'api',
1734
- },
1735
- };
1736
- this.applyAxState(nextAxState);
1737
- const appliedAxState = this.getAxState();
1738
- this.scheduleSave();
1739
- this.notifyChange('ax');
1740
- if (options.recordHistory === false) return appliedAxState.focus;
1741
- this.recordMutation({
1742
- operationType: 'setAxFocus',
1743
- description: `Set AX focus (${appliedAxState.focus.nodeIds.length} nodes)`,
1744
- forward: this.suppressed(() => {
1745
- this.applyAxState(appliedAxState);
1746
- this.scheduleSave();
1747
- this.notifyChange('ax');
1748
- }),
1749
- inverse: this.suppressed(() => {
1750
- this.applyAxState(oldAxState);
1751
- this.scheduleSave();
1752
- this.notifyChange('ax');
1753
- }),
1754
- });
1755
- return appliedAxState.focus;
1744
+ return this.ax.setAxFocus(nodeIds, options);
1756
1745
  }
1757
1746
 
1758
1747
  clearAxFocus(): PmxAxFocusState {
1759
- return this.setAxFocus([], { source: 'system' });
1748
+ return this.ax.clearAxFocus();
1760
1749
  }
1761
1750
 
1762
1751
  // ── Work items (canvas-bound; snapshotted via getAxState blob) ────
1763
1752
  getWorkItems(): PmxAxWorkItem[] {
1764
- return this.getAxState().workItems;
1753
+ return this.ax.getWorkItems();
1765
1754
  }
1766
1755
 
1767
1756
  addWorkItem(
1768
1757
  input: { title: string; status?: PmxAxWorkItemStatus; detail?: string | null; nodeIds?: string[] },
1769
1758
  options: { source?: PmxAxSource } = {},
1770
1759
  ): PmxAxWorkItem {
1771
- const oldAxState = this.getAxState();
1772
- const item = createAxWorkItem(input, options.source ?? 'api', this.currentNodeIdSet());
1773
- this.applyAxState({ ...oldAxState, workItems: [...oldAxState.workItems, item] });
1774
- const applied = this.getAxState();
1775
- this.scheduleSave();
1776
- this.notifyChange('ax');
1777
- this.recordMutation({
1778
- operationType: 'addWorkItem',
1779
- description: `Added work item "${item.title}"`,
1780
- forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
1781
- inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
1782
- });
1783
- return applied.workItems.find((w) => w.id === item.id) ?? item;
1760
+ return this.ax.addWorkItem(input, options);
1784
1761
  }
1785
1762
 
1786
1763
  updateWorkItem(
@@ -1788,53 +1765,19 @@ class CanvasStateManager {
1788
1765
  patch: { title?: string; status?: PmxAxWorkItemStatus; detail?: string | null; nodeIds?: string[] },
1789
1766
  options: { source?: PmxAxSource } = {},
1790
1767
  ): PmxAxWorkItem | null {
1791
- const oldAxState = this.getAxState();
1792
- const existing = oldAxState.workItems.find((w) => w.id === id);
1793
- if (!existing) return null;
1794
- const merged: PmxAxWorkItem = {
1795
- ...existing,
1796
- ...(patch.title !== undefined ? { title: patch.title } : {}),
1797
- ...(patch.status !== undefined ? { status: patch.status } : {}),
1798
- ...(patch.detail !== undefined ? { detail: patch.detail } : {}),
1799
- ...(patch.nodeIds !== undefined ? { nodeIds: patch.nodeIds.filter((n) => this.nodes.has(n)) } : {}),
1800
- updatedAt: new Date().toISOString(),
1801
- source: options.source ?? existing.source,
1802
- };
1803
- this.applyAxState({ ...oldAxState, workItems: replaceById(oldAxState.workItems, merged) });
1804
- const applied = this.getAxState();
1805
- this.scheduleSave();
1806
- this.notifyChange('ax');
1807
- this.recordMutation({
1808
- operationType: 'updateWorkItem',
1809
- description: `Updated work item ${id}`,
1810
- forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
1811
- inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
1812
- });
1813
- return applied.workItems.find((w) => w.id === id) ?? null;
1768
+ return this.ax.updateWorkItem(id, patch, options);
1814
1769
  }
1815
1770
 
1816
1771
  // ── Approval gates (canvas-bound) ─────────────────────────────────
1817
1772
  getApprovalGates(): PmxAxApprovalGate[] {
1818
- return this.getAxState().approvalGates;
1773
+ return this.ax.getApprovalGates();
1819
1774
  }
1820
1775
 
1821
1776
  requestApproval(
1822
1777
  input: { title: string; detail?: string | null; action?: string | null; nodeIds?: string[] },
1823
1778
  options: { source?: PmxAxSource } = {},
1824
1779
  ): PmxAxApprovalGate {
1825
- const oldAxState = this.getAxState();
1826
- const gate = createAxApprovalGate(input, options.source ?? 'api', this.currentNodeIdSet());
1827
- this.applyAxState({ ...oldAxState, approvalGates: [...oldAxState.approvalGates, gate] });
1828
- const applied = this.getAxState();
1829
- this.scheduleSave();
1830
- this.notifyChange('ax');
1831
- this.recordMutation({
1832
- operationType: 'requestApproval',
1833
- description: `Requested approval "${gate.title}"`,
1834
- forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
1835
- inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
1836
- });
1837
- return applied.approvalGates.find((g) => g.id === gate.id) ?? gate;
1780
+ return this.ax.requestApproval(input, options);
1838
1781
  }
1839
1782
 
1840
1783
  resolveApproval(
@@ -1842,32 +1785,12 @@ class CanvasStateManager {
1842
1785
  decision: 'approved' | 'rejected',
1843
1786
  options: { resolution?: string; source?: PmxAxSource } = {},
1844
1787
  ): PmxAxApprovalGate | null {
1845
- const oldAxState = this.getAxState();
1846
- const gate = oldAxState.approvalGates.find((g) => g.id === id);
1847
- if (!gate || gate.status !== 'pending') return null;
1848
- const resolved: PmxAxApprovalGate = {
1849
- ...gate,
1850
- status: decision,
1851
- resolvedAt: new Date().toISOString(),
1852
- resolution: options.resolution ?? null,
1853
- source: options.source ?? gate.source,
1854
- };
1855
- this.applyAxState({ ...oldAxState, approvalGates: replaceById(oldAxState.approvalGates, resolved) });
1856
- const applied = this.getAxState();
1857
- this.scheduleSave();
1858
- this.notifyChange('ax');
1859
- this.recordMutation({
1860
- operationType: 'resolveApproval',
1861
- description: `Resolved approval ${id} -> ${decision}`,
1862
- forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
1863
- inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
1864
- });
1865
- return applied.approvalGates.find((g) => g.id === id) ?? null;
1788
+ return this.ax.resolveApproval(id, decision, options);
1866
1789
  }
1867
1790
 
1868
1791
  // ── Review annotations (canvas-bound) ─────────────────────────────
1869
1792
  getReviewAnnotations(): PmxAxReviewAnnotation[] {
1870
- return this.getAxState().reviewAnnotations;
1793
+ return this.ax.getReviewAnnotations();
1871
1794
  }
1872
1795
 
1873
1796
  addReviewAnnotation(
@@ -1883,33 +1806,7 @@ class CanvasStateManager {
1883
1806
  },
1884
1807
  options: { source?: PmxAxSource } = {},
1885
1808
  ): PmxAxReviewAnnotation | null {
1886
- // Validate the node anchor up front. A node-anchored review whose nodeId is
1887
- // missing or unknown would otherwise be silently dropped by
1888
- // normalizeAxForCurrentNodes after apply, yet still returned as a phantom
1889
- // success object — false success / silent data loss. Reject instead so the
1890
- // HTTP/MCP layers surface ok:false / 4xx.
1891
- // Context-aware default: only fall back to a node anchor when a usable nodeId
1892
- // is present; otherwise treat it as an unanchored (body-only) note so a
1893
- // `{ body }`-only annotation succeeds (anchorType is documented optional).
1894
- const anchorType = input.anchorType ?? (typeof input.nodeId === 'string' && input.nodeId ? 'node' : 'file');
1895
- // An EXPLICIT node anchor still requires a real nodeId — reject a phantom
1896
- // node-anchored review rather than silently dropping it post-apply.
1897
- if (anchorType === 'node' && (typeof input.nodeId !== 'string' || !this.currentNodeIdSet().has(input.nodeId))) {
1898
- return null;
1899
- }
1900
- const oldAxState = this.getAxState();
1901
- const annotation = createAxReviewAnnotation(input, options.source ?? 'api');
1902
- this.applyAxState({ ...oldAxState, reviewAnnotations: [...oldAxState.reviewAnnotations, annotation] });
1903
- const applied = this.getAxState();
1904
- this.scheduleSave();
1905
- this.notifyChange('ax');
1906
- this.recordMutation({
1907
- operationType: 'addReviewAnnotation',
1908
- description: `Added review ${annotation.kind} (${annotation.severity})`,
1909
- forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
1910
- inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
1911
- });
1912
- return applied.reviewAnnotations.find((r) => r.id === annotation.id) ?? annotation;
1809
+ return this.ax.addReviewAnnotation(input, options);
1913
1810
  }
1914
1811
 
1915
1812
  updateReviewAnnotation(
@@ -1917,57 +1814,23 @@ class CanvasStateManager {
1917
1814
  patch: { body?: string; status?: PmxAxReviewStatus; severity?: PmxAxReviewSeverity; kind?: PmxAxReviewKind },
1918
1815
  options: { source?: PmxAxSource } = {},
1919
1816
  ): PmxAxReviewAnnotation | null {
1920
- const oldAxState = this.getAxState();
1921
- const existing = oldAxState.reviewAnnotations.find((r) => r.id === id);
1922
- if (!existing) return null;
1923
- const merged: PmxAxReviewAnnotation = {
1924
- ...existing,
1925
- ...(patch.body !== undefined ? { body: patch.body } : {}),
1926
- ...(patch.status !== undefined ? { status: patch.status } : {}),
1927
- ...(patch.severity !== undefined ? { severity: patch.severity } : {}),
1928
- ...(patch.kind !== undefined ? { kind: patch.kind } : {}),
1929
- updatedAt: new Date().toISOString(),
1930
- source: options.source ?? existing.source,
1931
- };
1932
- this.applyAxState({ ...oldAxState, reviewAnnotations: replaceById(oldAxState.reviewAnnotations, merged) });
1933
- const applied = this.getAxState();
1934
- this.scheduleSave();
1935
- this.notifyChange('ax');
1936
- this.recordMutation({
1937
- operationType: 'updateReviewAnnotation',
1938
- description: `Updated review ${id}`,
1939
- forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
1940
- inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
1941
- });
1942
- return applied.reviewAnnotations.find((r) => r.id === id) ?? null;
1817
+ return this.ax.updateReviewAnnotation(id, patch, options);
1943
1818
  }
1944
1819
 
1945
1820
  // ── Host capability (own table; reported by adapters) ─────────────
1946
1821
  getHostCapability(): PmxAxHostCapability | null {
1947
- return this._axHostCapability;
1822
+ return this.ax.getHostCapability();
1948
1823
  }
1949
1824
 
1950
1825
  getElicitations(): PmxAxElicitation[] {
1951
- return this.getAxState().elicitations;
1826
+ return this.ax.getElicitations();
1952
1827
  }
1953
1828
 
1954
1829
  requestElicitation(
1955
1830
  input: { prompt: string; fields?: string[]; nodeIds?: string[] },
1956
1831
  options: { source?: PmxAxSource } = {},
1957
1832
  ): PmxAxElicitation {
1958
- const oldAxState = this.getAxState();
1959
- const elicitation = createAxElicitation(input, options.source ?? 'api', this.currentNodeIdSet());
1960
- this.applyAxState({ ...oldAxState, elicitations: [...oldAxState.elicitations, elicitation] });
1961
- const applied = this.getAxState();
1962
- this.scheduleSave();
1963
- this.notifyChange('ax');
1964
- this.recordMutation({
1965
- operationType: 'requestElicitation',
1966
- description: `Requested elicitation "${elicitation.prompt}"`,
1967
- forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
1968
- inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
1969
- });
1970
- return applied.elicitations.find((e) => e.id === elicitation.id) ?? elicitation;
1833
+ return this.ax.requestElicitation(input, options);
1971
1834
  }
1972
1835
 
1973
1836
  respondElicitation(
@@ -1975,50 +1838,18 @@ class CanvasStateManager {
1975
1838
  response: Record<string, unknown>,
1976
1839
  options: { source?: PmxAxSource } = {},
1977
1840
  ): PmxAxElicitation | null {
1978
- const oldAxState = this.getAxState();
1979
- const existing = oldAxState.elicitations.find((e) => e.id === id);
1980
- if (!existing || existing.status !== 'pending') return null;
1981
- const merged: PmxAxElicitation = {
1982
- ...existing,
1983
- status: 'answered',
1984
- response,
1985
- resolvedAt: new Date().toISOString(),
1986
- source: options.source ?? existing.source,
1987
- };
1988
- this.applyAxState({ ...oldAxState, elicitations: replaceById(oldAxState.elicitations, merged) });
1989
- const applied = this.getAxState();
1990
- this.scheduleSave();
1991
- this.notifyChange('ax');
1992
- this.recordMutation({
1993
- operationType: 'respondElicitation',
1994
- description: `Answered elicitation ${id}`,
1995
- forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
1996
- inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
1997
- });
1998
- return applied.elicitations.find((e) => e.id === id) ?? null;
1841
+ return this.ax.respondElicitation(id, response, options);
1999
1842
  }
2000
1843
 
2001
1844
  getModeRequests(): PmxAxModeRequest[] {
2002
- return this.getAxState().modeRequests;
1845
+ return this.ax.getModeRequests();
2003
1846
  }
2004
1847
 
2005
1848
  requestMode(
2006
1849
  input: { mode: PmxAxMode; reason?: string | null; nodeIds?: string[] },
2007
1850
  options: { source?: PmxAxSource } = {},
2008
1851
  ): PmxAxModeRequest {
2009
- const oldAxState = this.getAxState();
2010
- const request = createAxModeRequest(input, options.source ?? 'api', this.currentNodeIdSet());
2011
- this.applyAxState({ ...oldAxState, modeRequests: [...oldAxState.modeRequests, request] });
2012
- const applied = this.getAxState();
2013
- this.scheduleSave();
2014
- this.notifyChange('ax');
2015
- this.recordMutation({
2016
- operationType: 'requestMode',
2017
- description: `Requested mode "${request.mode}"`,
2018
- forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
2019
- inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
2020
- });
2021
- return applied.modeRequests.find((m) => m.id === request.id) ?? request;
1852
+ return this.ax.requestMode(input, options);
2022
1853
  }
2023
1854
 
2024
1855
  resolveModeRequest(
@@ -2026,85 +1857,45 @@ class CanvasStateManager {
2026
1857
  decision: 'approved' | 'rejected',
2027
1858
  options: { resolution?: string; source?: PmxAxSource } = {},
2028
1859
  ): PmxAxModeRequest | null {
2029
- const oldAxState = this.getAxState();
2030
- const existing = oldAxState.modeRequests.find((m) => m.id === id);
2031
- if (!existing || existing.status !== 'pending') return null;
2032
- const merged: PmxAxModeRequest = {
2033
- ...existing,
2034
- status: decision,
2035
- resolvedAt: new Date().toISOString(),
2036
- resolution: options.resolution ?? null,
2037
- source: options.source ?? existing.source,
2038
- };
2039
- this.applyAxState({ ...oldAxState, modeRequests: replaceById(oldAxState.modeRequests, merged) });
2040
- const applied = this.getAxState();
2041
- this.scheduleSave();
2042
- this.notifyChange('ax');
2043
- this.recordMutation({
2044
- operationType: 'resolveModeRequest',
2045
- description: `Resolved mode request ${id} -> ${decision}`,
2046
- forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
2047
- inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
2048
- });
2049
- return applied.modeRequests.find((m) => m.id === id) ?? null;
1860
+ return this.ax.resolveModeRequest(id, decision, options);
1861
+ }
1862
+
1863
+ // ── Single-item AX readers (canvas-bound; for the blocking-wait endpoints) ──
1864
+ getApproval(id: string): PmxAxApprovalGate | null {
1865
+ return this.ax.getApproval(id);
1866
+ }
1867
+
1868
+ getElicitation(id: string): PmxAxElicitation | null {
1869
+ return this.ax.getElicitation(id);
1870
+ }
1871
+
1872
+ getModeRequest(id: string): PmxAxModeRequest | null {
1873
+ return this.ax.getModeRequest(id);
2050
1874
  }
2051
1875
 
2052
1876
  getCommandRegistry(): PmxAxCommandDescriptor[] {
2053
- return listAxCommands();
1877
+ return this.ax.getCommandRegistry();
2054
1878
  }
2055
1879
 
2056
1880
  /** Invoke a registry-gated PMX command intent — records a timeline event (no execution). */
2057
1881
  invokeCommand(name: string, args: Record<string, unknown> | null = null, options: { source?: PmxAxSource } = {}): PmxAxEvent | null {
2058
- if (!isAxCommand(name)) return null;
2059
- return this.recordAxEvent(
2060
- { kind: 'command', summary: name, detail: AX_COMMAND_REGISTRY[name].description, data: { command: name, ...(args ? { args } : {}) } },
2061
- options,
2062
- );
1882
+ return this.ax.invokeCommand(name, args, options);
2063
1883
  }
2064
1884
 
2065
1885
  getPolicy(): PmxAxPolicy {
2066
- return this.getAxState().policy;
1886
+ return this.ax.getPolicy();
2067
1887
  }
2068
1888
 
2069
1889
  /** Merge a declarative tool/prompt policy patch (canvas-bound, snapshotted). */
2070
1890
  setPolicy(
2071
1891
  patch: { tools?: Partial<PmxAxPolicy['tools']>; prompt?: Partial<PmxAxPolicy['prompt']> },
2072
- _options: { source?: PmxAxSource } = {},
1892
+ options: { source?: PmxAxSource } = {},
2073
1893
  ): PmxAxPolicy {
2074
- const oldAxState = this.getAxState();
2075
- const merged = normalizeAxPolicy({
2076
- tools: { ...oldAxState.policy.tools, ...(patch.tools ?? {}) },
2077
- prompt: { ...oldAxState.policy.prompt, ...(patch.prompt ?? {}) },
2078
- });
2079
- this.applyAxState({ ...oldAxState, policy: merged });
2080
- const applied = this.getAxState();
2081
- this.scheduleSave();
2082
- this.notifyChange('ax');
2083
- this.recordMutation({
2084
- operationType: 'setPolicy',
2085
- description: 'Updated AX policy',
2086
- forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
2087
- inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
2088
- });
2089
- return applied.policy;
1894
+ return this.ax.setPolicy(patch, options);
2090
1895
  }
2091
1896
 
2092
- setHostCapability(input: unknown, _options: { source?: PmxAxSource } = {}): PmxAxHostCapability {
2093
- const cap = normalizeAxHostCapability(
2094
- isRecord(input)
2095
- ? { ...input, reportedAt: new Date().toISOString() }
2096
- : { reportedAt: new Date().toISOString() },
2097
- ) ?? createEmptyAxHostCapability();
2098
- this._axHostCapability = cap;
2099
- if (this._db) {
2100
- try {
2101
- upsertAxHostCapabilityToDB(this._db, cap);
2102
- } catch (error) {
2103
- logCanvasStateWarning('save host capability failed', error, {});
2104
- }
2105
- }
2106
- this.notifyChange('ax');
2107
- return cap;
1897
+ setHostCapability(input: unknown, options: { source?: PmxAxSource } = {}): PmxAxHostCapability {
1898
+ return this.ax.setHostCapability(input, options);
2108
1899
  }
2109
1900
 
2110
1901
  // ── Timeline (DB-direct; NOT in _axState; NOT history-recorded) ───
@@ -2112,99 +1903,66 @@ class CanvasStateManager {
2112
1903
  input: { kind: PmxAxEventKind; summary: string; detail?: string | null; nodeIds?: string[]; data?: Record<string, unknown> | null },
2113
1904
  options: { source?: PmxAxSource } = {},
2114
1905
  ): PmxAxEvent {
2115
- const draft = createAxEvent(input, options.source ?? 'api');
2116
- if (this._db) {
2117
- try {
2118
- const ev = appendAxEventToDB(this._db, draft);
2119
- this.notifyChange('ax-timeline');
2120
- return ev;
2121
- } catch (error) {
2122
- logCanvasStateWarning('record ax event failed', error, { id: draft.id });
2123
- }
2124
- }
2125
- this.notifyChange('ax-timeline');
2126
- return { ...draft, seq: 0 };
1906
+ return this.ax.recordAxEvent(input, options);
2127
1907
  }
2128
1908
 
2129
1909
  addEvidence(
2130
1910
  input: { kind: PmxAxEvidenceKind; title: string; body?: string | null; ref?: string | null; nodeIds?: string[]; data?: Record<string, unknown> | null },
2131
1911
  options: { source?: PmxAxSource } = {},
2132
1912
  ): PmxAxEvidence {
2133
- const draft = createAxEvidence(input, options.source ?? 'api');
2134
- if (this._db) {
2135
- try {
2136
- const ev = appendAxEvidenceToDB(this._db, draft);
2137
- this.notifyChange('ax-timeline');
2138
- return ev;
2139
- } catch (error) {
2140
- logCanvasStateWarning('add evidence failed', error, { id: draft.id });
2141
- }
2142
- }
2143
- this.notifyChange('ax-timeline');
2144
- return { ...draft, seq: 0 };
1913
+ return this.ax.addEvidence(input, options);
2145
1914
  }
2146
1915
 
2147
1916
  recordSteeringMessage(message: string, options: { source?: PmxAxSource } = {}): PmxAxSteeringMessage {
2148
- const draft = createAxSteeringMessage(message, options.source ?? 'api');
2149
- if (this._db) {
2150
- try {
2151
- const s = appendAxSteeringToDB(this._db, draft);
2152
- this.notifyChange('ax-timeline');
2153
- return s;
2154
- } catch (error) {
2155
- logCanvasStateWarning('record steering failed', error, { id: draft.id });
2156
- }
2157
- }
2158
- this.notifyChange('ax-timeline');
2159
- return { ...draft, seq: 0 };
1917
+ return this.ax.recordSteeringMessage(message, options);
2160
1918
  }
2161
1919
 
2162
1920
  markSteeringDelivered(id: string): boolean {
2163
- if (!this._db) return false;
2164
- try {
2165
- const ok = markAxSteeringDeliveredInDB(this._db, id);
2166
- if (ok) this.notifyChange('ax-timeline');
2167
- return ok;
2168
- } catch (error) {
2169
- logCanvasStateWarning('mark steering delivered failed', error, { id });
2170
- return false;
2171
- }
1921
+ return this.ax.markSteeringDelivered(id);
1922
+ }
1923
+
1924
+ ingestActivity(
1925
+ input: {
1926
+ kind: PmxAxActivityKind;
1927
+ title: string;
1928
+ summary?: string | null;
1929
+ outcome?: 'success' | 'failure';
1930
+ ref?: string | null;
1931
+ nodeIds?: string[];
1932
+ data?: Record<string, unknown> | null;
1933
+ reactions?: {
1934
+ workItem?: false | { status?: PmxAxWorkItemStatus; detail?: string | null };
1935
+ evidence?: false | { kind?: PmxAxEvidenceKind; body?: string | null };
1936
+ review?: false | { severity?: PmxAxReviewSeverity; kind?: PmxAxReviewKind; anchorType?: PmxAxReviewAnchorType; nodeId?: string | null };
1937
+ };
1938
+ },
1939
+ options: { source?: PmxAxSource } = {},
1940
+ ): { event: PmxAxEvent; workItem: PmxAxWorkItem | null; evidence: PmxAxEvidence | null; review: PmxAxReviewAnnotation | null } {
1941
+ return this.ax.ingestActivity(input, options);
2172
1942
  }
2173
1943
 
2174
1944
  getAxEvents(q: AxTimelineQuery = {}): PmxAxEvent[] {
2175
- return this._db ? loadAxEventsFromDB(this._db, q) : [];
1945
+ return this.ax.getAxEvents(q);
2176
1946
  }
2177
1947
 
2178
1948
  getAxEvidence(q: AxTimelineQuery = {}): PmxAxEvidence[] {
2179
- return this._db ? loadAxEvidenceFromDB(this._db, q) : [];
1949
+ return this.ax.getAxEvidence(q);
2180
1950
  }
2181
1951
 
2182
1952
  getAxSteering(q: AxTimelineQuery & { onlyPending?: boolean } = {}): PmxAxSteeringMessage[] {
2183
- return this._db ? loadAxSteeringFromDB(this._db, q) : [];
1953
+ return this.ax.getAxSteering(q);
2184
1954
  }
2185
1955
 
2186
- /**
2187
- * Undelivered steering for a consumer (Phase 4 delivery). Excludes messages
2188
- * whose source equals the consumer to prevent delivery loops (e.g. Copilot
2189
- * should not be handed back steering it originated).
2190
- */
2191
1956
  getPendingSteering(options: { consumer?: string; limit?: number } = {}): PmxAxSteeringMessage[] {
2192
- return this._db ? loadPendingAxSteeringFromDB(this._db, options) : [];
1957
+ return this.ax.getPendingSteering(options);
2193
1958
  }
2194
1959
 
2195
1960
  getAxTimelineSummary(): PmxAxTimelineSummary {
2196
- return this._db
2197
- ? loadAxTimelineSummaryFromDB(this._db)
2198
- : { recentEvents: [], recentEvidence: [], pendingSteering: [], counts: { events: 0, evidence: 0, steering: 0 } };
1961
+ return this.ax.getAxTimelineSummary();
2199
1962
  }
2200
1963
 
2201
1964
  getAxTimeline(q: AxTimelineQuery = {}): { events: PmxAxEvent[]; evidence: PmxAxEvidence[]; steering: PmxAxSteeringMessage[]; summary: PmxAxTimelineSummary } {
2202
- return {
2203
- events: this.getAxEvents(q),
2204
- evidence: this.getAxEvidence(q),
2205
- steering: this.getAxSteering(q),
2206
- summary: this.getAxTimelineSummary(),
2207
- };
1965
+ return this.ax.getAxTimeline(q);
2208
1966
  }
2209
1967
 
2210
1968
  setContextPins(nodeIds: string[]): void {
@@ -2341,7 +2099,7 @@ class CanvasStateManager {
2341
2099
  // Clears canvas-bound AX state (focus, work items, approvals, review annotations).
2342
2100
  // Timeline tables (ax_events/ax_evidence/ax_steering) and host capability are
2343
2101
  // deliberately retained per the AX state-partition policy.
2344
- this._axState = createEmptyAxState();
2102
+ this.ax.resetCanvasBound();
2345
2103
  this._viewport = { x: 0, y: 0, scale: 1 };
2346
2104
  this.scheduleSave();
2347
2105
  this.notifyChange('nodes');
@@ -2356,7 +2114,7 @@ class CanvasStateManager {
2356
2114
  for (const e of oldEdges) this.addEdge(structuredClone(e));
2357
2115
  for (const annotation of oldAnnotations) this.addAnnotation(structuredClone(annotation));
2358
2116
  this.setContextPins(oldPins);
2359
- this.applyAxState(oldAxState);
2117
+ this.ax.applyPersistedAx(oldAxState);
2360
2118
  this.setViewport(oldViewport);
2361
2119
  this.notifyChange('ax');
2362
2120
  }),