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.
- package/CHANGELOG.md +79 -0
- package/Readme.md +40 -8
- package/dist/canvas/global.css +91 -0
- package/dist/canvas/index.js +53 -53
- package/dist/canvas/surface-theme.css +32 -0
- package/dist/types/cli/agent.d.ts +1 -0
- package/dist/types/cli/commands/skills.d.ts +41 -0
- package/dist/types/client/state/intent-store.d.ts +6 -0
- package/dist/types/client/state/sse-bridge.d.ts +1 -0
- package/dist/types/server/ax-state-manager.d.ts +5 -0
- package/dist/types/server/ax-state.d.ts +9 -3
- package/dist/types/server/canvas-db.d.ts +3 -2
- package/dist/types/server/canvas-state.d.ts +12 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +4 -1
- package/dist/types/server/operations/registry.d.ts +9 -1
- package/dist/types/shared/ax-intent.d.ts +6 -0
- package/dist/types/shared/themes.d.ts +1 -1
- package/docs/RELEASE.md +14 -6
- package/docs/bun-webview-integration.md +2 -2
- package/docs/cli.md +24 -1
- package/docs/environment.md +3 -2
- package/docs/http-api.md +6 -3
- package/docs/plans/plan-009-tech-debt-backlog.md +4 -3
- package/docs/screenshot.png +0 -0
- package/docs/screenshots/amp-orb-portal.png +0 -0
- package/docs/screenshots/claude-code-desktop.png +0 -0
- package/docs/screenshots/codex-app.png +0 -0
- package/docs/screenshots/github-copilot-app.png +0 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +25 -5
- package/skills/pmx-canvas/references/full-reference.md +1 -1
- package/skills/pmx-canvas-orchestration/SKILL.md +82 -0
- package/skills/pmx-canvas-testing/SKILL.md +4 -0
- package/src/cli/agent.ts +1 -0
- package/src/cli/commands/skills.ts +183 -0
- package/src/cli/index.ts +17 -3
- package/src/cli/shared.ts +1 -0
- package/src/client/canvas/CanvasNode.tsx +12 -1
- package/src/client/canvas/IntentLayer.tsx +8 -6
- package/src/client/state/intent-bridge.ts +12 -3
- package/src/client/state/intent-store.ts +47 -2
- package/src/client/state/sse-bridge.ts +14 -3
- package/src/client/theme/global.css +91 -0
- package/src/client/theme/surface-theme.css +32 -0
- package/src/mcp/server.ts +3 -5
- package/src/server/ax-state-manager.ts +27 -6
- package/src/server/ax-state.ts +36 -3
- package/src/server/canvas-db.ts +83 -60
- package/src/server/canvas-state.ts +52 -6
- package/src/server/intent-registry.ts +2 -0
- package/src/server/operations/http.ts +5 -1
- package/src/server/operations/ops/ax-shared.ts +20 -1
- package/src/server/operations/ops/ax-state.ts +4 -7
- package/src/server/operations/ops/ax-timeline.ts +26 -3
- package/src/server/operations/ops/ax-work.ts +27 -1
- package/src/server/operations/ops/batch.ts +3 -1
- package/src/server/operations/ops/canvas-wire.ts +1 -1
- package/src/server/operations/ops/nodes.ts +15 -1
- package/src/server/operations/registry.ts +122 -1
- package/src/server/server.ts +23 -4
- package/src/shared/ax-intent.ts +6 -0
- 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(
|
|
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: {
|
|
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: {
|
|
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(
|
|
750
|
-
|
|
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 {
|
package/src/server/ax-state.ts
CHANGED
|
@@ -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>([
|
|
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: {
|
|
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
|
|
package/src/server/canvas-db.ts
CHANGED
|
@@ -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(
|
|
900
|
-
|
|
901
|
-
s.message,
|
|
902
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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<
|
|
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
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
|
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<
|
|
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
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
}
|