pmx-canvas 0.4.3 → 0.4.5

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 (62) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/Readme.md +40 -8
  3. package/dist/canvas/global.css +91 -0
  4. package/dist/canvas/index.js +53 -53
  5. package/dist/canvas/surface-theme.css +32 -0
  6. package/dist/types/cli/agent.d.ts +1 -0
  7. package/dist/types/cli/commands/skills.d.ts +41 -0
  8. package/dist/types/client/state/intent-store.d.ts +6 -0
  9. package/dist/types/client/state/sse-bridge.d.ts +1 -0
  10. package/dist/types/server/ax-state-manager.d.ts +5 -0
  11. package/dist/types/server/ax-state.d.ts +9 -3
  12. package/dist/types/server/canvas-db.d.ts +3 -2
  13. package/dist/types/server/canvas-state.d.ts +12 -0
  14. package/dist/types/server/operations/ops/ax-shared.d.ts +4 -1
  15. package/dist/types/server/operations/registry.d.ts +9 -1
  16. package/dist/types/shared/ax-intent.d.ts +6 -0
  17. package/dist/types/shared/themes.d.ts +1 -1
  18. package/docs/RELEASE.md +14 -6
  19. package/docs/bun-webview-integration.md +2 -2
  20. package/docs/cli.md +24 -1
  21. package/docs/environment.md +3 -2
  22. package/docs/http-api.md +6 -3
  23. package/docs/plans/plan-009-tech-debt-backlog.md +4 -3
  24. package/docs/screenshot.png +0 -0
  25. package/docs/screenshots/amp-orb-portal.png +0 -0
  26. package/docs/screenshots/claude-code-desktop.png +0 -0
  27. package/docs/screenshots/codex-app.png +0 -0
  28. package/docs/screenshots/github-copilot-app.png +0 -0
  29. package/package.json +1 -1
  30. package/skills/pmx-canvas/SKILL.md +25 -5
  31. package/skills/pmx-canvas/references/full-reference.md +1 -1
  32. package/skills/pmx-canvas-orchestration/SKILL.md +82 -0
  33. package/skills/pmx-canvas-testing/SKILL.md +4 -0
  34. package/src/cli/agent.ts +1 -0
  35. package/src/cli/commands/skills.ts +183 -0
  36. package/src/cli/index.ts +17 -3
  37. package/src/cli/shared.ts +1 -0
  38. package/src/client/canvas/CanvasNode.tsx +12 -1
  39. package/src/client/canvas/IntentLayer.tsx +8 -6
  40. package/src/client/state/intent-bridge.ts +12 -3
  41. package/src/client/state/intent-store.ts +47 -2
  42. package/src/client/state/sse-bridge.ts +14 -3
  43. package/src/client/theme/global.css +91 -0
  44. package/src/client/theme/surface-theme.css +32 -0
  45. package/src/mcp/server.ts +3 -5
  46. package/src/server/ax-state-manager.ts +27 -6
  47. package/src/server/ax-state.ts +36 -3
  48. package/src/server/canvas-db.ts +83 -60
  49. package/src/server/canvas-state.ts +52 -6
  50. package/src/server/intent-registry.ts +2 -0
  51. package/src/server/operations/http.ts +5 -1
  52. package/src/server/operations/ops/ax-shared.ts +20 -1
  53. package/src/server/operations/ops/ax-state.ts +4 -7
  54. package/src/server/operations/ops/ax-timeline.ts +26 -3
  55. package/src/server/operations/ops/ax-work.ts +27 -1
  56. package/src/server/operations/ops/batch.ts +3 -1
  57. package/src/server/operations/ops/canvas-wire.ts +1 -1
  58. package/src/server/operations/ops/nodes.ts +15 -1
  59. package/src/server/operations/registry.ts +122 -1
  60. package/src/server/server.ts +23 -4
  61. package/src/shared/ax-intent.ts +6 -0
  62. package/src/shared/themes.ts +2 -0
@@ -446,6 +446,61 @@
446
446
  --c-annotation: #EDF5EF;
447
447
  }
448
448
 
449
+ :root[data-theme="volt"] {
450
+ /* ── Core palette (volt · green-tinted charcoal + coral) ── */
451
+ --c-bg: #0B0D0B;
452
+ --c-panel: #171A17;
453
+ --c-panel-soft: #111311;
454
+ --c-line: #272A27;
455
+ --c-text: #F6FFF5;
456
+ --c-text-soft: #CBD3CA;
457
+ --c-muted: #9CA49C;
458
+ --c-dim: #626962;
459
+ --c-accent: #DFDFC1;
460
+ --c-ok: #52C880;
461
+ --c-warn: #E4C465;
462
+ --c-warn-alt: #FFB300;
463
+ --c-danger: #D67670;
464
+ --c-purple: #BC8FB4;
465
+ --c-thinking: #BC8FB4;
466
+ --c-subagent: #6CA0E0;
467
+ --c-input-bg: #111311;
468
+ --c-dot-grid: rgba(223, 223, 193, 0.08);
469
+ --c-panel-overlay: rgba(11, 13, 11, 0.6);
470
+ --c-panel-glass: rgba(23, 26, 23, 0.92);
471
+ --c-shadow: rgba(0, 0, 0, 0.3);
472
+ --c-shadow-heavy: rgba(0, 0, 0, 0.5);
473
+ --c-glow-accent: rgba(223, 223, 193, 0.15);
474
+ --c-surface-hover: rgba(255, 255, 255, 0.06);
475
+ --c-surface-subtle: rgba(255, 255, 255, 0.03);
476
+ --c-surface-th: rgba(255, 255, 255, 0.04);
477
+ --c-accent-8: rgba(223, 223, 193, 0.08);
478
+ --c-accent-10: rgba(223, 223, 193, 0.1);
479
+ --c-accent-12: rgba(223, 223, 193, 0.12);
480
+ --c-accent-15: rgba(223, 223, 193, 0.15);
481
+ --c-accent-25: rgba(223, 223, 193, 0.25);
482
+ --c-accent-30: rgba(223, 223, 193, 0.3);
483
+ --c-accent-40: rgba(223, 223, 193, 0.4);
484
+ --c-ok-10: rgba(82, 200, 128, 0.1);
485
+ --c-ok-12: rgba(82, 200, 128, 0.12);
486
+ --c-ok-20: rgba(82, 200, 128, 0.2);
487
+ --c-ok-25: rgba(82, 200, 128, 0.25);
488
+ --c-warn-10: rgba(228, 196, 101, 0.1);
489
+ --c-warn-12: rgba(228, 196, 101, 0.12);
490
+ --c-warn-15: rgba(228, 196, 101, 0.15);
491
+ --c-warn-20: rgba(228, 196, 101, 0.2);
492
+ --c-warn-22: rgba(228, 196, 101, 0.22);
493
+ --c-warn-30: rgba(228, 196, 101, 0.3);
494
+ --c-warn-60: rgba(228, 196, 101, 0.6);
495
+ --c-danger-12: rgba(214, 118, 112, 0.12);
496
+ --c-line-30: rgba(39, 42, 39, 0.3);
497
+ --c-contrast-fg: #0B0D0B;
498
+ --c-accent-hover: #EDEDDB;
499
+ --c-warn-hover: #EBD087;
500
+ --c-canvas-wash: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0));
501
+ --c-annotation: #F1F7F0;
502
+ }
503
+
449
504
  * {
450
505
  box-sizing: border-box;
451
506
  margin: 0;
@@ -614,6 +669,29 @@ body,
614
669
  flex-shrink: 0;
615
670
  }
616
671
 
672
+ .canvas-node .node-ax-status {
673
+ font-size: calc(9px * var(--node-chrome-scale, 1));
674
+ padding: calc(1px * var(--node-chrome-scale, 1)) calc(6px * var(--node-chrome-scale, 1));
675
+ border-radius: calc(4px * var(--node-chrome-scale, 1));
676
+ background: var(--c-panel-overlay);
677
+ color: var(--c-muted);
678
+ text-transform: uppercase;
679
+ letter-spacing: 0.04em;
680
+ flex-shrink: 0;
681
+ }
682
+
683
+ .canvas-node .node-ax-status-in-progress {
684
+ color: var(--c-accent);
685
+ }
686
+
687
+ .canvas-node .node-ax-status-done {
688
+ color: var(--c-ok);
689
+ }
690
+
691
+ .canvas-node .node-ax-status-blocked {
692
+ color: var(--c-warn);
693
+ }
694
+
617
695
  .canvas-node .node-controls {
618
696
  display: flex;
619
697
  align-items: center;
@@ -3900,6 +3978,19 @@ button.welcome-hint:hover {
3900
3978
  display: contents;
3901
3979
  }
3902
3980
 
3981
+ /* Auto-ghosts (server-synthesized for unlinked agent mutations) read lighter
3982
+ than a deliberate canvas_intent signal: desaturated, dotted, no veto —
3983
+ the opacity drop lives in ghostOpacity() (inline style). */
3984
+ .intent-ghost.is-auto {
3985
+ filter: saturate(0.7);
3986
+ border-style: dotted;
3987
+ }
3988
+
3989
+ .intent-ghost.is-auto .intent-ghost-titlebar,
3990
+ .intent-ghost.is-auto .intent-chip {
3991
+ border-style: dotted;
3992
+ }
3993
+
3903
3994
  .intent-ghost {
3904
3995
  position: absolute;
3905
3996
  pointer-events: none;
@@ -281,6 +281,38 @@
281
281
  --color-danger: #E57373;
282
282
  }
283
283
 
284
+ :root[data-theme="volt"] {
285
+ /* ── Core palette (volt · green-tinted charcoal + coral) ── */
286
+ --c-bg: #0B0D0B;
287
+ --c-panel: #171A17;
288
+ --c-panel-soft: #111311;
289
+ --c-line: #272A27;
290
+ --c-text: #F6FFF5;
291
+ --c-text-soft: #CBD3CA;
292
+ --c-muted: #9CA49C;
293
+ --c-dim: #626962;
294
+ --c-accent: #DFDFC1;
295
+ --c-ok: #52C880;
296
+ --c-warn: #E4C465;
297
+ --c-warn-alt: #FFB300;
298
+ --c-danger: #D67670;
299
+ --c-purple: #BC8FB4;
300
+
301
+ --color-bg: #0B0D0B;
302
+ --color-panel: #171A17;
303
+ --color-surface: #111311;
304
+ --color-border: #272A27;
305
+ --color-text: #F6FFF5;
306
+ --color-text-primary: #F6FFF5;
307
+ --color-text-secondary: #CBD3CA;
308
+ --color-text-muted: #9CA49C;
309
+ --color-text-dim: #626962;
310
+ --color-accent: #DFDFC1;
311
+ --color-success: #52C880;
312
+ --color-warning: #E4C465;
313
+ --color-danger: #D67670;
314
+ }
315
+
284
316
  html,
285
317
  body {
286
318
  margin: 0;
package/src/mcp/server.ts CHANGED
@@ -26,6 +26,7 @@ import { fileURLToPath } from 'node:url';
26
26
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
27
27
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
28
28
  import { z } from 'zod';
29
+ import { AX_SOURCES } from '../server/operations/ops/ax-shared.js';
29
30
  import { canvasState, describeCanvasSchema } from '../server/index.js';
30
31
  import { AX_INTERACTION_TYPES } from '../server/ax-interaction.js';
31
32
  import { buildPendingAxActivity } from '../server/ax-state.js';
@@ -253,10 +254,7 @@ export async function startMcpServer(): Promise<void> {
253
254
  .describe('Type-specific payload, e.g. {"title":"..."} for ax.work.create.'),
254
255
  sourceSurface: z.enum(['native-node', 'json-render', 'html-node', 'mcp-app', 'adapter']).optional(),
255
256
  correlationId: z.string().optional(),
256
- source: z
257
- .enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
258
- .optional()
259
- .describe('Optional host/source label. Defaults to mcp.'),
257
+ source: z.enum(AX_SOURCES).optional().describe('Optional host/source label. Defaults to mcp.'),
260
258
  },
261
259
  async ({ type, sourceNodeId, payload, sourceSurface, correlationId, source }) => {
262
260
  try {
@@ -345,7 +343,7 @@ export async function startMcpServer(): Promise<void> {
345
343
  })
346
344
  .optional()
347
345
  .describe('Override or suppress the kind-driven default reactions.'),
348
- source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system']).optional(),
346
+ source: z.enum(AX_SOURCES).optional(),
349
347
  },
350
348
  async ({ kind, title, summary, outcome, ref, nodeIds, data, reactions, source }) => {
351
349
  try {
@@ -239,7 +239,13 @@ export class AxStateManager {
239
239
  }
240
240
 
241
241
  addWorkItem(
242
- input: { title: string; status?: PmxAxWorkItemStatus; detail?: string | null; nodeIds?: string[] },
242
+ input: {
243
+ title: string;
244
+ status?: PmxAxWorkItemStatus;
245
+ detail?: string | null;
246
+ nodeIds?: string[];
247
+ agentId?: string | null;
248
+ },
243
249
  options: { source?: PmxAxSource } = {},
244
250
  ): PmxAxWorkItem {
245
251
  const oldAxState = this.getAxState();
@@ -267,7 +273,13 @@ export class AxStateManager {
267
273
 
268
274
  updateWorkItem(
269
275
  id: string,
270
- patch: { title?: string; status?: PmxAxWorkItemStatus; detail?: string | null; nodeIds?: string[] },
276
+ patch: {
277
+ title?: string;
278
+ status?: PmxAxWorkItemStatus;
279
+ detail?: string | null;
280
+ nodeIds?: string[];
281
+ agentId?: string | null;
282
+ },
271
283
  options: { source?: PmxAxSource } = {},
272
284
  ): PmxAxWorkItem | null {
273
285
  const oldAxState = this.getAxState();
@@ -280,6 +292,7 @@ export class AxStateManager {
280
292
  ...(patch.status !== undefined ? { status: patch.status } : {}),
281
293
  ...(patch.detail !== undefined ? { detail: patch.detail } : {}),
282
294
  ...(patch.nodeIds !== undefined ? { nodeIds: patch.nodeIds.filter((n) => validNodeIds.has(n)) } : {}),
295
+ ...(patch.agentId !== undefined ? { agentId: patch.agentId } : {}),
283
296
  updatedAt: new Date().toISOString(),
284
297
  source: options.source ?? existing.source,
285
298
  };
@@ -703,9 +716,9 @@ export class AxStateManager {
703
716
  nodeIds?: string[];
704
717
  data?: Record<string, unknown> | null;
705
718
  },
706
- options: { source?: PmxAxSource } = {},
719
+ options: { source?: PmxAxSource; agentId?: string | null } = {},
707
720
  ): PmxAxEvent {
708
- const draft = createAxEvent(input, options.source ?? 'api');
721
+ const draft = createAxEvent(input, options.source ?? 'api', options.agentId ?? null);
709
722
  const db = this.deps.getDb();
710
723
  if (db) {
711
724
  try {
@@ -746,8 +759,16 @@ export class AxStateManager {
746
759
  return { ...draft, seq: 0 };
747
760
  }
748
761
 
749
- recordSteeringMessage(message: string, options: { source?: PmxAxSource } = {}): PmxAxSteeringMessage {
750
- const draft = createAxSteeringMessage(message, options.source ?? 'api');
762
+ recordSteeringMessage(
763
+ message: string,
764
+ options: { source?: PmxAxSource; agentId?: string | null; target?: string | null } = {},
765
+ ): PmxAxSteeringMessage {
766
+ const draft = createAxSteeringMessage(
767
+ message,
768
+ options.source ?? 'api',
769
+ options.agentId ?? null,
770
+ options.target ?? null,
771
+ );
751
772
  const db = this.deps.getDb();
752
773
  if (db) {
753
774
  try {
@@ -1,7 +1,7 @@
1
1
  import type { CanvasLayout, CanvasNodeState } from './canvas-state.js';
2
2
  import type { AgentContextNode } from './agent-context.js';
3
3
 
4
- export type PmxAxSource = 'agent' | 'api' | 'browser' | 'cli' | 'codex' | 'copilot' | 'mcp' | 'sdk' | 'system';
4
+ export type PmxAxSource = 'agent' | 'amp' | 'api' | 'browser' | 'cli' | 'codex' | 'copilot' | 'mcp' | 'sdk' | 'system';
5
5
 
6
6
  export interface PmxAxFocusState {
7
7
  nodeIds: string[];
@@ -39,6 +39,7 @@ export interface PmxAxWorkItem {
39
39
  createdAt: string;
40
40
  updatedAt: string;
41
41
  source: PmxAxSource | null;
42
+ agentId: string | null;
42
43
  }
43
44
 
44
45
  export interface PmxAxApprovalGate {
@@ -87,6 +88,7 @@ export interface PmxAxEvent {
87
88
  data: Record<string, unknown> | null;
88
89
  createdAt: string;
89
90
  source: PmxAxSource | null;
91
+ agentId: string | null;
90
92
  }
91
93
 
92
94
  export interface PmxAxEvidence {
@@ -109,6 +111,9 @@ export interface PmxAxSteeringMessage {
109
111
  delivered: boolean;
110
112
  createdAt: string;
111
113
  source: PmxAxSource | null;
114
+ agentId: string | null;
115
+ /** The agentId/consumer this steer is addressed to; null = broadcast to all consumers. */
116
+ target: string | null;
112
117
  }
113
118
 
114
119
  // ── Host/session capability (own table; reported by adapters) ──────
@@ -269,7 +274,18 @@ export function buildPendingAxActivity(state: PmxAxState, consumer?: string): Pe
269
274
  return out;
270
275
  }
271
276
 
272
- const AX_SOURCES = new Set<PmxAxSource>(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system']);
277
+ const AX_SOURCES = new Set<PmxAxSource>([
278
+ 'agent',
279
+ 'amp',
280
+ 'api',
281
+ 'browser',
282
+ 'cli',
283
+ 'codex',
284
+ 'copilot',
285
+ 'mcp',
286
+ 'sdk',
287
+ 'system',
288
+ ]);
273
289
 
274
290
  function isRecord(value: unknown): value is Record<string, unknown> {
275
291
  return value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -622,6 +638,7 @@ export function normalizeAxWorkItem(input: unknown, validNodeIds?: Set<string>):
622
638
  createdAt,
623
639
  updatedAt: normalizeTimestamp(input.updatedAt) ?? createdAt,
624
640
  source: normalizeSource(input.source),
641
+ agentId: optionalString(input.agentId),
625
642
  };
626
643
  }
627
644
 
@@ -715,6 +732,7 @@ export function normalizeAxEvent(input: unknown): PmxAxEvent | null {
715
732
  data: boundedRecord(input.data),
716
733
  createdAt: normalizeTimestamp(input.createdAt) ?? nowIso(),
717
734
  source: normalizeSource(input.source),
735
+ agentId: optionalString(input.agentId),
718
736
  };
719
737
  }
720
738
 
@@ -745,12 +763,20 @@ export function normalizeAxSteeringMessage(input: unknown): PmxAxSteeringMessage
745
763
  delivered: input.delivered === true,
746
764
  createdAt: normalizeTimestamp(input.createdAt) ?? nowIso(),
747
765
  source: normalizeSource(input.source),
766
+ agentId: optionalString(input.agentId),
767
+ target: optionalString(input.target),
748
768
  };
749
769
  }
750
770
 
751
771
  // ── Factories ──────────────────────────────────────────────────────
752
772
  export function createAxWorkItem(
753
- input: { title: string; status?: PmxAxWorkItemStatus; detail?: string | null; nodeIds?: string[] },
773
+ input: {
774
+ title: string;
775
+ status?: PmxAxWorkItemStatus;
776
+ detail?: string | null;
777
+ nodeIds?: string[];
778
+ agentId?: string | null;
779
+ },
754
780
  source: PmxAxSource | null,
755
781
  validNodeIds?: Set<string>,
756
782
  ): PmxAxWorkItem {
@@ -764,6 +790,7 @@ export function createAxWorkItem(
764
790
  createdAt: now,
765
791
  updatedAt: now,
766
792
  source,
793
+ agentId: input.agentId ?? null,
767
794
  };
768
795
  }
769
796
 
@@ -829,6 +856,7 @@ export function createAxEvent(
829
856
  data?: Record<string, unknown> | null;
830
857
  },
831
858
  source: PmxAxSource | null,
859
+ agentId: string | null = null,
832
860
  ): Omit<PmxAxEvent, 'seq'> {
833
861
  return {
834
862
  id: axId('evt'),
@@ -839,6 +867,7 @@ export function createAxEvent(
839
867
  data: input.data ?? null,
840
868
  createdAt: nowIso(),
841
869
  source,
870
+ agentId,
842
871
  };
843
872
  }
844
873
 
@@ -869,6 +898,8 @@ export function createAxEvidence(
869
898
  export function createAxSteeringMessage(
870
899
  message: string,
871
900
  source: PmxAxSource | null,
901
+ agentId: string | null = null,
902
+ target: string | null = null,
872
903
  ): Omit<PmxAxSteeringMessage, 'seq'> {
873
904
  return {
874
905
  id: axId('steer'),
@@ -876,6 +907,8 @@ export function createAxSteeringMessage(
876
907
  delivered: false,
877
908
  createdAt: nowIso(),
878
909
  source,
910
+ agentId,
911
+ target,
879
912
  };
880
913
  }
881
914
 
@@ -178,7 +178,8 @@ const SCHEMA_SQL = `
178
178
  node_ids TEXT NOT NULL DEFAULT '[]',
179
179
  data TEXT,
180
180
  created_at TEXT NOT NULL,
181
- source TEXT
181
+ source TEXT,
182
+ agent_id TEXT
182
183
  );
183
184
  CREATE INDEX IF NOT EXISTS idx_ax_events_seq ON ax_events (seq);
184
185
 
@@ -202,7 +203,9 @@ const SCHEMA_SQL = `
202
203
  message TEXT NOT NULL,
203
204
  delivered INTEGER NOT NULL DEFAULT 0,
204
205
  created_at TEXT NOT NULL,
205
- source TEXT
206
+ source TEXT,
207
+ agent_id TEXT,
208
+ target TEXT
206
209
  );
207
210
  CREATE INDEX IF NOT EXISTS idx_ax_steering_seq ON ax_steering (seq);
208
211
 
@@ -213,6 +216,22 @@ const SCHEMA_SQL = `
213
216
  );
214
217
  `;
215
218
 
219
+ /** Idempotently add a column to an existing table (no prior-migrations system in this repo). */
220
+ function ensureColumn(db: Database, table: string, column: string, ddl: string): void {
221
+ interface ColumnInfoRow {
222
+ name: string;
223
+ }
224
+ const columns = db.query<ColumnInfoRow, []>(`PRAGMA table_info(${table})`).all();
225
+ if (columns.some((c) => c.name === column)) return;
226
+ try {
227
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
228
+ } catch (error) {
229
+ // Two processes can race the check-then-ALTER on the first post-upgrade
230
+ // open; the loser's duplicate-column error means the column exists — done.
231
+ if (!/duplicate column/i.test(error instanceof Error ? error.message : String(error))) throw error;
232
+ }
233
+ }
234
+
216
235
  function normalizePositiveInteger(value: number | undefined): number | undefined {
217
236
  if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined;
218
237
  return Math.floor(value);
@@ -258,6 +277,11 @@ export function openCanvasDb(dbPath: string): Database {
258
277
  db.exec('PRAGMA busy_timeout=5000');
259
278
  db.exec(SCHEMA_SQL);
260
279
 
280
+ // Additive columns for pre-existing DBs (fresh installs already get them via SCHEMA_SQL above).
281
+ ensureColumn(db, 'ax_events', 'agent_id', 'agent_id TEXT');
282
+ ensureColumn(db, 'ax_steering', 'agent_id', 'agent_id TEXT');
283
+ ensureColumn(db, 'ax_steering', 'target', 'target TEXT');
284
+
261
285
  // Set schema version if not present
262
286
  const row = db.query<{ value: string }, [string]>('SELECT value FROM meta WHERE key = ?').get('schema_version');
263
287
  if (!row) {
@@ -858,7 +882,7 @@ function readLastSeq(db: Database, table: 'ax_events' | 'ax_evidence' | 'ax_stee
858
882
 
859
883
  export function appendAxEventToDB(db: Database, ev: Omit<PmxAxEvent, 'seq'>): PmxAxEvent {
860
884
  db.run(
861
- 'INSERT INTO ax_events (id, kind, summary, detail, node_ids, data, created_at, source) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
885
+ 'INSERT INTO ax_events (id, kind, summary, detail, node_ids, data, created_at, source, agent_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
862
886
  [
863
887
  ev.id,
864
888
  ev.kind,
@@ -868,6 +892,7 @@ export function appendAxEventToDB(db: Database, ev: Omit<PmxAxEvent, 'seq'>): Pm
868
892
  ev.data ? JSON.stringify(ev.data) : null,
869
893
  ev.createdAt,
870
894
  ev.source,
895
+ ev.agentId,
871
896
  ],
872
897
  );
873
898
  const seq = readLastSeq(db, 'ax_events');
@@ -896,20 +921,20 @@ export function appendAxEvidenceToDB(db: Database, ev: Omit<PmxAxEvidence, 'seq'
896
921
  }
897
922
 
898
923
  export function appendAxSteeringToDB(db: Database, s: Omit<PmxAxSteeringMessage, 'seq'>): PmxAxSteeringMessage {
899
- db.run('INSERT INTO ax_steering (id, message, delivered, created_at, source) VALUES (?, ?, ?, ?, ?)', [
900
- s.id,
901
- s.message,
902
- s.delivered ? 1 : 0,
903
- s.createdAt,
904
- s.source,
905
- ]);
924
+ db.run(
925
+ 'INSERT INTO ax_steering (id, message, delivered, created_at, source, agent_id, target) VALUES (?, ?, ?, ?, ?, ?, ?)',
926
+ [s.id, s.message, s.delivered ? 1 : 0, s.createdAt, s.source, s.agentId, s.target],
927
+ );
906
928
  const seq = readLastSeq(db, 'ax_steering');
907
929
  trimAxTable(db, 'ax_steering');
908
930
  return { ...s, seq };
909
931
  }
910
932
 
911
933
  export function markAxSteeringDeliveredInDB(db: Database, id: string): boolean {
912
- const r = db.run('UPDATE ax_steering SET delivered = 1 WHERE id = ?', [id]);
934
+ // Compare-and-set: true ONLY on the undelivered→delivered transition. Two
935
+ // adapters racing the same steer see exactly one true — the loser (and any
936
+ // re-mark) gets false and must not treat the message as its own delivery.
937
+ const r = db.run('UPDATE ax_steering SET delivered = 1 WHERE id = ? AND delivered = 0', [id]);
913
938
  return r.changes > 0;
914
939
  }
915
940
 
@@ -924,6 +949,7 @@ export function loadAxEventsFromDB(db: Database, q: AxTimelineQuery = {}): PmxAx
924
949
  data: string | null;
925
950
  created_at: string;
926
951
  source: string | null;
952
+ agent_id: string | null;
927
953
  }
928
954
  const rows = db
929
955
  .query<Row, [number]>('SELECT * FROM ax_events ORDER BY seq DESC LIMIT ?')
@@ -935,6 +961,7 @@ export function loadAxEventsFromDB(db: Database, q: AxTimelineQuery = {}): PmxAx
935
961
  createdAt: r.created_at,
936
962
  nodeIds: safeParseJson(r.node_ids),
937
963
  data: safeParseJson(r.data),
964
+ agentId: r.agent_id,
938
965
  }),
939
966
  )
940
967
  .filter((e): e is PmxAxEvent => e !== null);
@@ -968,58 +995,62 @@ export function loadAxEvidenceFromDB(db: Database, q: AxTimelineQuery = {}): Pmx
968
995
  .filter((e): e is PmxAxEvidence => e !== null);
969
996
  }
970
997
 
998
+ interface AxSteeringRow {
999
+ seq: number;
1000
+ id: string;
1001
+ message: string;
1002
+ delivered: number;
1003
+ created_at: string;
1004
+ source: string | null;
1005
+ agent_id: string | null;
1006
+ target: string | null;
1007
+ }
1008
+
1009
+ function mapAxSteeringRow(r: AxSteeringRow): PmxAxSteeringMessage | null {
1010
+ return normalizeAxSteeringMessage({
1011
+ ...r,
1012
+ createdAt: r.created_at,
1013
+ delivered: r.delivered === 1,
1014
+ agentId: r.agent_id,
1015
+ });
1016
+ }
1017
+
971
1018
  export function loadAxSteeringFromDB(
972
1019
  db: Database,
973
1020
  q: AxTimelineQuery & { onlyPending?: boolean } = {},
974
1021
  ): PmxAxSteeringMessage[] {
975
- interface Row {
976
- seq: number;
977
- id: string;
978
- message: string;
979
- delivered: number;
980
- created_at: string;
981
- source: string | null;
982
- }
983
1022
  const sql = q.onlyPending
984
1023
  ? 'SELECT * FROM ax_steering WHERE delivered = 0 ORDER BY seq DESC LIMIT ?'
985
1024
  : 'SELECT * FROM ax_steering ORDER BY seq DESC LIMIT ?';
986
- const rows = db.query<Row, [number]>(sql).all(clampTimelineLimit(q.limit));
987
- return rows
988
- .map((r) => normalizeAxSteeringMessage({ ...r, createdAt: r.created_at, delivered: r.delivered === 1 }))
989
- .filter((s): s is PmxAxSteeringMessage => s !== null);
1025
+ const rows = db.query<AxSteeringRow, [number]>(sql).all(clampTimelineLimit(q.limit));
1026
+ return rows.map(mapAxSteeringRow).filter((s): s is PmxAxSteeringMessage => s !== null);
990
1027
  }
991
1028
 
992
1029
  export function loadPendingAxSteeringFromDB(
993
1030
  db: Database,
994
1031
  options: { consumer?: string; limit?: number } = {},
995
1032
  ): PmxAxSteeringMessage[] {
996
- interface Row {
997
- seq: number;
998
- id: string;
999
- message: string;
1000
- delivered: number;
1001
- created_at: string;
1002
- source: string | null;
1003
- }
1004
- // FIFO (oldest undelivered first); exclude the consumer's own steering in SQL
1005
- // so the LIMIT is applied AFTER loop-prevention, not before.
1033
+ // FIFO (oldest undelivered first); exclude the consumer's own steering (loop
1034
+ // prevention) and any steering addressed to a DIFFERENT consumer (target scoping)
1035
+ // in SQL so the LIMIT is applied AFTER both filters, not before.
1006
1036
  const limit = clampTimelineLimit(options.limit);
1007
1037
  const rows = options.consumer
1008
1038
  ? db
1009
- .query<Row, [string, number]>(
1010
- 'SELECT * FROM ax_steering WHERE delivered = 0 AND (source IS NULL OR source != ?) ORDER BY seq ASC LIMIT ?',
1039
+ .query<AxSteeringRow, [string, string, string, number]>(
1040
+ '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 ?',
1011
1041
  )
1012
- .all(options.consumer, limit)
1013
- : db.query<Row, [number]>('SELECT * FROM ax_steering WHERE delivered = 0 ORDER BY seq ASC LIMIT ?').all(limit);
1014
- return rows
1015
- .map((r) => normalizeAxSteeringMessage({ ...r, createdAt: r.created_at, delivered: r.delivered === 1 }))
1016
- .filter((s): s is PmxAxSteeringMessage => s !== null);
1042
+ .all(options.consumer, options.consumer, options.consumer, limit)
1043
+ : db
1044
+ .query<AxSteeringRow, [number]>('SELECT * FROM ax_steering WHERE delivered = 0 ORDER BY seq ASC LIMIT ?')
1045
+ .all(limit);
1046
+ return rows.map(mapAxSteeringRow).filter((s): s is PmxAxSteeringMessage => s !== null);
1017
1047
  }
1018
1048
 
1019
1049
  /**
1020
1050
  * NEWEST undelivered steering first (report #57) for the compact AX context lead
1021
1051
  * block — so a fresh steer is visible even behind a long backlog. Loop-safe: excludes
1022
- * the consumer's own steering in SQL so the LIMIT applies after loop-prevention.
1052
+ * the consumer's own steering and steering targeted at a different consumer in SQL
1053
+ * so the LIMIT applies after both filters.
1023
1054
  * Distinct from loadPendingAxSteeringFromDB (FIFO oldest-first) which the claim/ack
1024
1055
  * delivery queue uses for ordered processing.
1025
1056
  */
@@ -1027,35 +1058,27 @@ export function loadNewestPendingAxSteeringFromDB(
1027
1058
  db: Database,
1028
1059
  options: { consumer?: string; limit?: number } = {},
1029
1060
  ): PmxAxSteeringMessage[] {
1030
- interface Row {
1031
- seq: number;
1032
- id: string;
1033
- message: string;
1034
- delivered: number;
1035
- created_at: string;
1036
- source: string | null;
1037
- }
1038
1061
  const limit = clampTimelineLimit(options.limit);
1039
1062
  const rows = options.consumer
1040
1063
  ? db
1041
- .query<Row, [string, number]>(
1042
- 'SELECT * FROM ax_steering WHERE delivered = 0 AND (source IS NULL OR source != ?) ORDER BY seq DESC LIMIT ?',
1064
+ .query<AxSteeringRow, [string, string, string, number]>(
1065
+ '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 ?',
1043
1066
  )
1044
- .all(options.consumer, limit)
1045
- : db.query<Row, [number]>('SELECT * FROM ax_steering WHERE delivered = 0 ORDER BY seq DESC LIMIT ?').all(limit);
1046
- return rows
1047
- .map((r) => normalizeAxSteeringMessage({ ...r, createdAt: r.created_at, delivered: r.delivered === 1 }))
1048
- .filter((s): s is PmxAxSteeringMessage => s !== null);
1067
+ .all(options.consumer, options.consumer, options.consumer, limit)
1068
+ : db
1069
+ .query<AxSteeringRow, [number]>('SELECT * FROM ax_steering WHERE delivered = 0 ORDER BY seq DESC LIMIT ?')
1070
+ .all(limit);
1071
+ return rows.map(mapAxSteeringRow).filter((s): s is PmxAxSteeringMessage => s !== null);
1049
1072
  }
1050
1073
 
1051
- /** Total undelivered steering for a consumer (loop-safe — excludes the consumer's own). */
1074
+ /** Total undelivered steering for a consumer (loop-safe — excludes the consumer's own; target-scoped). */
1052
1075
  export function countPendingAxSteeringFromDB(db: Database, consumer?: string): number {
1053
1076
  const n = consumer
1054
1077
  ? db
1055
- .query<{ n: number }, [string]>(
1056
- 'SELECT COUNT(*) AS n FROM ax_steering WHERE delivered = 0 AND (source IS NULL OR source != ?)',
1078
+ .query<{ n: number }, [string, string, string]>(
1079
+ '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 = ?)',
1057
1080
  )
1058
- .get(consumer)?.n
1081
+ .get(consumer, consumer, consumer)?.n
1059
1082
  : db.query<{ n: number }, []>('SELECT COUNT(*) AS n FROM ax_steering WHERE delivered = 0').get()?.n;
1060
1083
  return Number(n ?? 0);
1061
1084
  }