pmx-canvas 0.4.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +154 -5
  2. package/.github/extensions/pmx-canvas/steering-delivery.mjs +79 -0
  3. package/CHANGELOG.md +139 -0
  4. package/Readme.md +72 -23
  5. package/dist/canvas/global.css +3069 -796
  6. package/dist/canvas/index.js +104 -104
  7. package/dist/json-render/index.css +1 -1
  8. package/dist/types/cli/agent.d.ts +1 -0
  9. package/dist/types/cli/commands/pump.d.ts +9 -0
  10. package/dist/types/client/App.d.ts +1 -1
  11. package/dist/types/client/canvas/AgentPresenceLayer.d.ts +11 -0
  12. package/dist/types/client/canvas/BarHint.d.ts +22 -0
  13. package/dist/types/client/canvas/CanvasNode.d.ts +5 -0
  14. package/dist/types/client/canvas/CanvasViewport.d.ts +4 -4
  15. package/dist/types/client/canvas/CommandBar.d.ts +10 -0
  16. package/dist/types/client/canvas/ConnectionBanner.d.ts +13 -0
  17. package/dist/types/client/canvas/ContextMenu.d.ts +6 -0
  18. package/dist/types/client/canvas/EdgeLayer.d.ts +2 -1
  19. package/dist/types/client/canvas/EmptyState.d.ts +11 -0
  20. package/dist/types/client/canvas/ExternalWriters.d.ts +5 -0
  21. package/dist/types/client/canvas/HumanPresenceLayer.d.ts +8 -0
  22. package/dist/types/client/canvas/Minimap.d.ts +12 -1
  23. package/dist/types/client/canvas/ScopeFenceLayer.d.ts +8 -0
  24. package/dist/types/client/canvas/SelectionBar.d.ts +6 -0
  25. package/dist/types/client/canvas/SessionPanel.d.ts +1 -0
  26. package/dist/types/client/canvas/SessionReceipt.d.ts +17 -0
  27. package/dist/types/client/canvas/TextPrompt.d.ts +25 -0
  28. package/dist/types/client/canvas/ToolRail.d.ts +21 -0
  29. package/dist/types/client/canvas/TopBar.d.ts +7 -0
  30. package/dist/types/client/canvas/canvas-area.d.ts +25 -0
  31. package/dist/types/client/canvas/create-in-view.d.ts +18 -0
  32. package/dist/types/client/canvas/import-files.d.ts +8 -0
  33. package/dist/types/client/canvas/kind-colors.d.ts +3 -0
  34. package/dist/types/client/canvas/use-focus-trap.d.ts +14 -0
  35. package/dist/types/client/canvas/use-now.d.ts +6 -0
  36. package/dist/types/client/icons.d.ts +11 -4
  37. package/dist/types/client/nodes/GroupNode.d.ts +6 -1
  38. package/dist/types/client/nodes/MarkdownNode.d.ts +2 -0
  39. package/dist/types/client/nodes/inline-editor-commands.d.ts +3 -4
  40. package/dist/types/client/state/canvas-store.d.ts +49 -5
  41. package/dist/types/client/state/human-store.d.ts +26 -0
  42. package/dist/types/client/state/iframe-mode.d.ts +1 -0
  43. package/dist/types/client/state/intent-bridge.d.ts +37 -5
  44. package/dist/types/client/state/node-factory.d.ts +0 -1
  45. package/dist/types/client/state/presence-store.d.ts +78 -0
  46. package/dist/types/client/state/session-store.d.ts +176 -0
  47. package/dist/types/client/types.d.ts +2 -1
  48. package/dist/types/client/utils/platform.d.ts +2 -0
  49. package/dist/types/server/agent-presence.d.ts +192 -0
  50. package/dist/types/server/ax-gate-ttl.d.ts +6 -0
  51. package/dist/types/server/ax-interaction.d.ts +4 -10
  52. package/dist/types/server/ax-state-manager.d.ts +21 -2
  53. package/dist/types/server/ax-state.d.ts +49 -3
  54. package/dist/types/server/ax-wait.d.ts +13 -0
  55. package/dist/types/server/canvas-db.d.ts +9 -1
  56. package/dist/types/server/canvas-operations.d.ts +1 -0
  57. package/dist/types/server/canvas-state.d.ts +27 -13
  58. package/dist/types/server/canvas-validation.d.ts +0 -14
  59. package/dist/types/server/html-surface.d.ts +1 -1
  60. package/dist/types/server/human-presence.d.ts +40 -0
  61. package/dist/types/server/index.d.ts +29 -3
  62. package/dist/types/server/intent-registry.d.ts +7 -0
  63. package/dist/types/server/mutation-history.d.ts +15 -2
  64. package/dist/types/server/operations/invoker.d.ts +13 -2
  65. package/dist/types/server/operations/ops/ax-presence.d.ts +2 -0
  66. package/dist/types/server/operations/ops/human-presence.d.ts +3 -0
  67. package/dist/types/server/operations/registry.d.ts +11 -0
  68. package/dist/types/server/operations/types.d.ts +1 -1
  69. package/dist/types/server/scope-fence.d.ts +62 -0
  70. package/dist/types/server/server.d.ts +0 -12
  71. package/dist/types/shared/agent-presence.d.ts +130 -0
  72. package/dist/types/shared/approval-gates.d.ts +19 -0
  73. package/dist/types/shared/auto-arrange.d.ts +0 -1
  74. package/dist/types/shared/ax-kinds.d.ts +11 -0
  75. package/dist/types/shared/human-presence.d.ts +31 -0
  76. package/dist/types/shared/iframe-probe.d.ts +8 -0
  77. package/dist/types/shared/scope-fence.d.ts +21 -0
  78. package/docs/RELEASE.md +6 -0
  79. package/docs/ax-host-adapter-contract.md +30 -14
  80. package/docs/cli.md +37 -1
  81. package/docs/environment.md +3 -0
  82. package/docs/http-api.md +222 -5
  83. package/docs/mcp.md +6 -5
  84. package/docs/node-types.md +3 -1
  85. package/docs/plans/plan-009-tech-debt-backlog.md +13 -0
  86. package/docs/screenshot.png +0 -0
  87. package/docs/screenshots/multi-agent-live.png +0 -0
  88. package/docs/screenshots/workbench-live-dark.png +0 -0
  89. package/docs/screenshots/workbench-live-light.png +0 -0
  90. package/docs/sdk.md +1 -1
  91. package/package.json +1 -1
  92. package/skills/pmx-canvas/SKILL.md +103 -16
  93. package/skills/pmx-canvas/references/codex-app-adapter.md +28 -0
  94. package/skills/pmx-canvas/references/full-reference.md +55 -16
  95. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +41 -24
  96. package/skills/pmx-canvas-orchestration/SKILL.md +56 -2
  97. package/skills/pmx-canvas-testing/SKILL.md +17 -0
  98. package/src/cli/agent.ts +1 -0
  99. package/src/cli/commands/copilot.ts +15 -8
  100. package/src/cli/commands/nodes.ts +1 -19
  101. package/src/cli/commands/pump.ts +252 -0
  102. package/src/cli/index.ts +1 -5
  103. package/src/cli/shared.ts +1 -1
  104. package/src/client/App.tsx +209 -585
  105. package/src/client/canvas/AgentPresenceLayer.tsx +74 -0
  106. package/src/client/canvas/AttentionHistory.tsx +0 -10
  107. package/src/client/canvas/BarHint.tsx +49 -0
  108. package/src/client/canvas/CanvasNode.tsx +488 -136
  109. package/src/client/canvas/CanvasViewport.tsx +190 -94
  110. package/src/client/canvas/CommandBar.tsx +140 -0
  111. package/src/client/canvas/CommandPalette.tsx +133 -58
  112. package/src/client/canvas/ConnectionBanner.tsx +65 -0
  113. package/src/client/canvas/ContextMenu.tsx +88 -27
  114. package/src/client/canvas/ContextPinBar.tsx +2 -2
  115. package/src/client/canvas/EdgeLayer.tsx +121 -38
  116. package/src/client/canvas/EmptyState.tsx +93 -0
  117. package/src/client/canvas/ExpandedNodeOverlay.tsx +64 -118
  118. package/src/client/canvas/ExternalWriters.tsx +308 -0
  119. package/src/client/canvas/FocusFieldLayer.tsx +12 -1
  120. package/src/client/canvas/HumanPresenceLayer.tsx +45 -0
  121. package/src/client/canvas/IntentLayer.tsx +1 -1
  122. package/src/client/canvas/Minimap.tsx +117 -190
  123. package/src/client/canvas/ScopeFenceLayer.tsx +38 -0
  124. package/src/client/canvas/SelectionBar.tsx +175 -23
  125. package/src/client/canvas/SessionPanel.tsx +481 -0
  126. package/src/client/canvas/SessionReceipt.tsx +141 -0
  127. package/src/client/canvas/ShortcutOverlay.tsx +21 -6
  128. package/src/client/canvas/SnapshotPanel.tsx +166 -80
  129. package/src/client/canvas/TextPrompt.tsx +128 -0
  130. package/src/client/canvas/ToolRail.tsx +447 -0
  131. package/src/client/canvas/TopBar.tsx +308 -0
  132. package/src/client/canvas/auto-fit.ts +1 -5
  133. package/src/client/canvas/canvas-area.ts +41 -0
  134. package/src/client/canvas/create-in-view.ts +48 -0
  135. package/src/client/canvas/import-files.ts +64 -0
  136. package/src/client/canvas/kind-colors.ts +22 -0
  137. package/src/client/canvas/snap-guides.ts +1 -1
  138. package/src/client/canvas/use-focus-trap.ts +54 -0
  139. package/src/client/canvas/use-now.ts +17 -0
  140. package/src/client/canvas/use-pan-zoom.ts +13 -2
  141. package/src/client/icons.tsx +57 -18
  142. package/src/client/nodes/GroupNode.tsx +9 -28
  143. package/src/client/nodes/InlineFormatBar.tsx +4 -3
  144. package/src/client/nodes/InlineMarkdownEditor.tsx +27 -0
  145. package/src/client/nodes/MarkdownNode.tsx +59 -24
  146. package/src/client/nodes/MdFormatBar.tsx +2 -4
  147. package/src/client/nodes/inline-editor-commands.ts +17 -6
  148. package/src/client/nodes/use-surface-frame.ts +3 -1
  149. package/src/client/state/attention-bridge.ts +3 -0
  150. package/src/client/state/canvas-store.ts +273 -84
  151. package/src/client/state/human-store.ts +175 -0
  152. package/src/client/state/iframe-mode.ts +33 -9
  153. package/src/client/state/intent-bridge.ts +75 -13
  154. package/src/client/state/node-factory.ts +2 -8
  155. package/src/client/state/presence-store.ts +168 -0
  156. package/src/client/state/session-store.ts +419 -0
  157. package/src/client/state/sse-bridge.ts +93 -30
  158. package/src/client/theme/global.css +3069 -796
  159. package/src/client/types.ts +3 -1
  160. package/src/client/utils/platform.ts +6 -1
  161. package/src/mcp/canvas-access.ts +2 -2
  162. package/src/server/agent-presence.ts +755 -0
  163. package/src/server/ax-context.ts +45 -0
  164. package/src/server/ax-gate-ttl.ts +61 -0
  165. package/src/server/ax-interaction.ts +29 -1
  166. package/src/server/ax-state-manager.ts +78 -6
  167. package/src/server/ax-state.ts +82 -15
  168. package/src/server/ax-wait.ts +33 -0
  169. package/src/server/canvas-db.ts +103 -33
  170. package/src/server/canvas-operations.ts +17 -8
  171. package/src/server/canvas-state.ts +131 -58
  172. package/src/server/canvas-validation.ts +3 -53
  173. package/src/server/demo-state.json +2 -70
  174. package/src/server/html-surface.ts +11 -2
  175. package/src/server/human-presence.ts +160 -0
  176. package/src/server/index.ts +107 -4
  177. package/src/server/intent-registry.ts +13 -0
  178. package/src/server/mutation-history.ts +47 -2
  179. package/src/server/operations/composites.ts +6 -3
  180. package/src/server/operations/http.ts +10 -1
  181. package/src/server/operations/index.ts +4 -0
  182. package/src/server/operations/invoker.ts +33 -6
  183. package/src/server/operations/ops/ax-presence.ts +62 -0
  184. package/src/server/operations/ops/ax-read.ts +8 -0
  185. package/src/server/operations/ops/ax-state.ts +106 -1
  186. package/src/server/operations/ops/ax-timeline.ts +54 -5
  187. package/src/server/operations/ops/ax-work.ts +46 -0
  188. package/src/server/operations/ops/batch.ts +3 -1
  189. package/src/server/operations/ops/edges.ts +88 -2
  190. package/src/server/operations/ops/groups.ts +4 -3
  191. package/src/server/operations/ops/human-presence.ts +35 -0
  192. package/src/server/operations/ops/json-render.ts +11 -1
  193. package/src/server/operations/ops/nodes.ts +65 -16
  194. package/src/server/operations/ops/query.ts +1 -0
  195. package/src/server/operations/ops/snapshots.ts +16 -0
  196. package/src/server/operations/registry.ts +196 -1
  197. package/src/server/operations/types.ts +1 -1
  198. package/src/server/scope-fence.ts +207 -0
  199. package/src/server/server.ts +177 -79
  200. package/src/server/trace-manager.ts +0 -2
  201. package/src/server/web-artifacts.ts +0 -1
  202. package/src/shared/agent-presence.ts +162 -0
  203. package/src/shared/approval-gates.ts +35 -0
  204. package/src/shared/auto-arrange.ts +121 -13
  205. package/src/shared/ax-kinds.ts +23 -0
  206. package/src/shared/human-presence.ts +30 -0
  207. package/src/shared/iframe-probe.ts +8 -0
  208. package/src/shared/scope-fence.ts +31 -0
  209. package/dist/types/client/canvas/DockedNode.d.ts +0 -4
  210. package/src/client/canvas/DockedNode.tsx +0 -165
@@ -124,7 +124,6 @@ function summarizeNodeResult(node: Record<string, unknown>): Record<string, unkn
124
124
  ...(node.size !== undefined ? { size: node.size } : {}),
125
125
  ...(node.collapsed !== undefined ? { collapsed: node.collapsed } : {}),
126
126
  ...(node.pinned !== undefined ? { pinned: node.pinned } : {}),
127
- ...(node.dockPosition !== undefined ? { dockPosition: node.dockPosition } : {}),
128
127
  ...(node.path !== undefined ? { path: node.path } : {}),
129
128
  ...(node.url !== undefined ? { url: node.url } : {}),
130
129
  ...(node.provenance !== undefined ? { provenance: node.provenance } : {}),
@@ -827,8 +826,6 @@ cmd(
827
826
  'pmx-canvas node update <node-id> --spec-file ./dashboard.json',
828
827
  'pmx-canvas node update <graph-id> --data-file ./metrics.json --chart-height 420',
829
828
  'pmx-canvas node update <node-id> --pinned true',
830
- 'pmx-canvas node update <node-id> --dock-position right',
831
- 'pmx-canvas node update <node-id> --dock-position none # undock back to the canvas',
832
829
  'pmx-canvas node update <node-id> --lock-arrange',
833
830
  'pmx-canvas node update <node-id> --width 840 --full # full node payload instead of the compact envelope',
834
831
  ],
@@ -910,25 +907,10 @@ cmd(
910
907
 
911
908
  if (pinned !== undefined) body.pinned = pinned;
912
909
 
913
- // --dock-position left|right|none : dock a node into the top HUD or undock it.
914
- // `none`/`null`/empty map to JS null (undock). Assigned with a !== undefined
915
- // guard so the null survives JSON.stringify to the server (which accepts a
916
- // top-level dockPosition: null). HTTP PATCH already supports this; this is the
917
- // CLI path the report (#40) found missing.
918
- const dockRaw = getStringFlag(flags, 'dock-position', 'dockPosition');
919
- let dockPosition: 'left' | 'right' | null | undefined;
920
- if (dockRaw !== undefined) {
921
- const v = dockRaw.trim().toLowerCase();
922
- if (v === 'left' || v === 'right') dockPosition = v;
923
- else if (v === 'none' || v === 'null' || v === '') dockPosition = null;
924
- else die(`Invalid --dock-position "${dockRaw}".`, 'Use left, right, or none (to undock).');
925
- }
926
- if (dockPosition !== undefined) body.dockPosition = dockPosition;
927
-
928
910
  if (Object.keys(body).length === 0) {
929
911
  die(
930
912
  'No updates specified',
931
- 'Use --title, --content, --x, --y, --width, --height, --strict-size, --pinned, --dock-position, trace fields, --lock-arrange, --unlock-arrange, or --stdin',
913
+ 'Use --title, --content, --x, --y, --width, --height, --strict-size, --pinned, trace fields, --lock-arrange, --unlock-arrange, or --stdin',
932
914
  );
933
915
  }
934
916
 
@@ -0,0 +1,252 @@
1
+ /**
2
+ * `pmx-canvas pump` — the generic reactive steering loop for hosts without a
3
+ * native adapter (Amp, Codex, any CLI agent). Long-polls the per-consumer
4
+ * delivery claim and hands each steer to a shell command; the command's exit
5
+ * code gates the per-consumer mark. This is the same design as the Copilot
6
+ * extension's delivery pump (send-then-mark, awaiting-mark memory, startup
7
+ * backlog watermark, old-server backoff) with the "send into the host" step
8
+ * generalized to "run your command".
9
+ *
10
+ * Safety: the steer text reaches the command as env (PMX_STEER_MESSAGE) and
11
+ * stdin — never spliced into the shell string — so hostile steer content
12
+ * cannot inject into the command line. `{message}` in the template expands to
13
+ * the SHELL VARIABLE reference "$PMX_STEER_MESSAGE" (quoted), not the text.
14
+ * On Windows that guarantee does not hold — cmd.exe expands `%VAR%` and then
15
+ * re-parses the result — so the placeholders are refused there and the command
16
+ * takes the message from stdin instead. See `renderExecTemplate`.
17
+ */
18
+ import { spawn } from 'node:child_process';
19
+ import { cmd, getBaseUrl, getStringFlag, invokeOperation, parseFlags, requireFlag } from '../shared.js';
20
+
21
+ interface PendingSteer {
22
+ id: string;
23
+ message: string;
24
+ source?: string | null;
25
+ target?: string | null;
26
+ createdAt?: string | null;
27
+ }
28
+
29
+ async function claimPending(consumer: string, waitMs: number): Promise<{ pending: PendingSteer[]; tookMs: number }> {
30
+ const started = Date.now();
31
+ const url = `${getBaseUrl()}/api/canvas/ax/delivery/pending?consumer=${encodeURIComponent(consumer)}&limit=1&order=oldest${waitMs > 0 ? `&waitMs=${waitMs}` : ''}`;
32
+ const res = await fetch(url, { signal: AbortSignal.timeout(waitMs + 15_000) });
33
+ if (!res.ok) throw new Error(`pending claim failed: HTTP ${res.status}`);
34
+ const body = (await res.json()) as { pending?: PendingSteer[] };
35
+ return { pending: Array.isArray(body.pending) ? body.pending : [], tookMs: Date.now() - started };
36
+ }
37
+
38
+ async function markDelivered(id: string, consumer: string): Promise<boolean> {
39
+ const result = (await invokeOperation('ax.delivery.mark', { id, consumer })) as { delivered?: boolean };
40
+ return result.delivered === true;
41
+ }
42
+
43
+ /**
44
+ * Windows has no `sh`, so the exec ran nowhere there. cmd.exe can host the
45
+ * stdin pattern, but NOT the `{message}` / `{id}` placeholders: those expand to
46
+ * an env-var reference so hostile steer content is never spliced into the
47
+ * command string, and cmd.exe expands `%VAR%` before re-parsing — a message
48
+ * containing `& del …` would run. Refuse the placeholders there instead of
49
+ * silently reintroducing the injection the substitution exists to prevent.
50
+ */
51
+ export function renderExecTemplate(template: string, windows = process.platform === 'win32'): string {
52
+ if (windows) {
53
+ if (template.includes('{message}') || template.includes('{id}')) {
54
+ throw new Error(
55
+ 'pmx-canvas pump: {message} and {id} are not supported on Windows — cmd.exe re-parses expanded variables, so a steer could inject commands. Read the message from stdin instead (the exec receives it), or use the PMX_STEER_MESSAGE / PMX_STEER_ID environment variables inside your own script.',
56
+ );
57
+ }
58
+ return template;
59
+ }
60
+ return template.replaceAll('{message}', '"$PMX_STEER_MESSAGE"').replaceAll('{id}', '"$PMX_STEER_ID"');
61
+ }
62
+
63
+ function runExec(template: string, steer: PendingSteer, consumer: string): Promise<number> {
64
+ const rendered = renderExecTemplate(template);
65
+ return new Promise((resolve) => {
66
+ // `shell: true` picks sh on POSIX and ComSpec/cmd.exe on Windows, and sets
67
+ // windowsVerbatimArguments itself — hand-rolling the cmd.exe form escaped
68
+ // the quotes inside the command, so `bun "C:\path\x.mjs"` arrived as
69
+ // `""C:\path\x.mjs""`. Let Node own the platform quoting.
70
+ const child = spawn(rendered, {
71
+ shell: true,
72
+ stdio: ['pipe', 'inherit', 'inherit'],
73
+ env: {
74
+ ...process.env,
75
+ PMX_STEER_MESSAGE: steer.message,
76
+ PMX_STEER_ID: steer.id,
77
+ PMX_STEER_SOURCE: steer.source ?? '',
78
+ // Delivery envelope (round-2 review): the command can tell the host
79
+ // WHO steered, whether it was addressed or a broadcast, and how old
80
+ // the steer is — without re-querying the board.
81
+ PMX_STEER_TARGET: steer.target ?? 'ALL',
82
+ PMX_STEER_CREATED_AT: steer.createdAt ?? '',
83
+ PMX_STEER_CONSUMER: consumer,
84
+ },
85
+ });
86
+ child.stdin.write(steer.message);
87
+ child.stdin.end();
88
+ child.on('close', (code) => resolve(code ?? 1));
89
+ child.on('error', () => resolve(1));
90
+ });
91
+ }
92
+
93
+ async function setPresence(consumer: string, label: string, parent: string | null, patch: Record<string, unknown>) {
94
+ try {
95
+ await invokeOperation('ax.presence.set', {
96
+ source: 'sdk',
97
+ agentId: consumer,
98
+ label,
99
+ ...(parent ? { parentAgentId: parent } : {}),
100
+ ...patch,
101
+ });
102
+ } catch {
103
+ // presence is best-effort — the pump must survive a rolling daemon
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Settle back to idle ONLY when the visible phase is still the pump's own
109
+ * `tooling` marker. The host the pump feeds may have set a richer explicit
110
+ * phase (thinking, waiting-approval) while handling the steer — an
111
+ * unconditional idle stomped it and active work read as Idle (0.5.0
112
+ * readiness, Copilot finding).
113
+ */
114
+ async function settlePresence(consumer: string, label: string, parent: string | null): Promise<void> {
115
+ try {
116
+ const res = await fetch(`${getBaseUrl()}/api/canvas/ax/presence`, { signal: AbortSignal.timeout(5_000) });
117
+ if (res.ok) {
118
+ const body = (await res.json()) as {
119
+ presences?: Array<{ sessionId?: string; phase?: string; detail?: string | null }>;
120
+ };
121
+ const own = body.presences?.find((p) => p.sessionId === consumer);
122
+ const ownToolingMarker = own?.phase === 'tooling' && (own.detail ?? '').startsWith('steer: ');
123
+ if (own && !ownToolingMarker && own.phase !== 'idle') return; // richer phase belongs to the host
124
+ }
125
+ } catch {
126
+ // presence read failed — fall through and settle; better idle than stuck tooling
127
+ }
128
+ await setPresence(consumer, label, parent, { phase: 'idle', detail: null });
129
+ }
130
+
131
+ cmd(
132
+ 'pump',
133
+ 'Reactive steering loop for CLI agents (Amp, Codex CLI): long-poll claims for a consumer and run a command per steer',
134
+ [
135
+ `pmx-canvas pump --consumer codex-cli --exec 'codex exec --full-auto {message}'`,
136
+ `pmx-canvas pump --consumer amp --exec 'amp -x {message}' --parent claude-code`,
137
+ `pmx-canvas pump --consumer testbot --exec 'cat' --once`,
138
+ ],
139
+ async (args) => {
140
+ const { flags } = parseFlags(args, { boolFlags: ['once'] });
141
+ const consumer = requireFlag(
142
+ flags,
143
+ 'consumer',
144
+ `pmx-canvas pump --consumer <key> --exec '<command with {message}>'`,
145
+ );
146
+ const execTemplate = requireFlag(
147
+ flags,
148
+ 'exec',
149
+ `pmx-canvas pump --consumer <key> --exec '<command with {message}>'`,
150
+ );
151
+ const once = flags.once === true;
152
+ const waitMsRaw = Number(getStringFlag(flags, 'wait-ms') ?? 120_000);
153
+ const waitMs = Number.isFinite(waitMsRaw) && waitMsRaw >= 0 ? Math.min(waitMsRaw, 120_000) : 120_000;
154
+ const label = getStringFlag(flags, 'label') ?? consumer;
155
+ const parent = getStringFlag(flags, 'parent') ?? null;
156
+ const deliverBacklog = getStringFlag(flags, 'backlog') === 'deliver';
157
+ const retryDelayRaw = Number(getStringFlag(flags, 'retry-delay-ms') ?? 5_000);
158
+ const retryDelayMs = Number.isFinite(retryDelayRaw) && retryDelayRaw >= 0 ? retryDelayRaw : 5_000;
159
+
160
+ // Startup watermark: steers queued BEFORE the pump existed are history,
161
+ // not instructions — mark them silently unless --backlog=deliver.
162
+ if (!deliverBacklog) {
163
+ const { pending } = await claimPending(consumer, 0);
164
+ for (const steer of pending) await markDelivered(steer.id, consumer);
165
+ let swept = pending.length;
166
+ // pending is capped at limit=1 — drain the rest.
167
+ while (swept > 0) {
168
+ const next = await claimPending(consumer, 0);
169
+ if (next.pending.length === 0) break;
170
+ for (const steer of next.pending) await markDelivered(steer.id, consumer);
171
+ swept = next.pending.length;
172
+ }
173
+ }
174
+
175
+ // Park via the guard: a pump RESTART while its host is mid-work must not
176
+ // knock an explicit phase back to idle either.
177
+ await settlePresence(consumer, label, parent);
178
+ console.log(`[pump] ${consumer} parked on ${getBaseUrl()} (waitMs=${waitMs}${once ? ', once' : ''})`);
179
+
180
+ const awaitingMark = new Set<string>();
181
+ const failures = new Map<string, number>();
182
+ let stopped = false;
183
+ let fastEmpties = 0;
184
+ process.on('SIGINT', () => {
185
+ stopped = true;
186
+ console.log('[pump] stopping');
187
+ process.exit(0);
188
+ });
189
+
190
+ while (!stopped) {
191
+ let pending: PendingSteer[];
192
+ let tookMs: number;
193
+ try {
194
+ ({ pending, tookMs } = await claimPending(consumer, waitMs));
195
+ } catch (error) {
196
+ // Daemon roll or network blip: back off and re-park, never exit.
197
+ console.error(`[pump] claim retrying: ${error instanceof Error ? error.message : String(error)}`);
198
+ await new Promise((resolve) => setTimeout(resolve, 10_000));
199
+ continue;
200
+ }
201
+ if (pending.length === 0) {
202
+ // Old-server guard: a 0.4.8 daemon ignores waitMs, returning instantly —
203
+ // without this the loop would spin hot.
204
+ fastEmpties = tookMs < 1_000 ? fastEmpties + 1 : 0;
205
+ if (fastEmpties >= 2) await new Promise((resolve) => setTimeout(resolve, 10_000));
206
+ continue;
207
+ }
208
+ fastEmpties = 0;
209
+ const steer = pending[0]!;
210
+
211
+ // Awaiting-mark memory: the exec already ran for this id — a failed mark
212
+ // must not re-run the command (the duplicate-send race).
213
+ if (awaitingMark.has(steer.id)) {
214
+ if (await markDelivered(steer.id, consumer)) awaitingMark.delete(steer.id);
215
+ continue;
216
+ }
217
+
218
+ await setPresence(consumer, label, parent, { phase: 'tooling', detail: `steer: ${steer.message.slice(0, 60)}` });
219
+ const age = steer.createdAt
220
+ ? `, age ${Math.max(0, Math.round((Date.now() - Date.parse(steer.createdAt)) / 1000))}s`
221
+ : '';
222
+ console.log(
223
+ `[pump] steer ${steer.id} from ${steer.source ?? '?'} → ${steer.target ?? 'ALL'}${age}: ${steer.message.slice(0, 120)}`,
224
+ );
225
+ const code = await runExec(execTemplate, steer, consumer);
226
+ if (code === 0) {
227
+ awaitingMark.add(steer.id);
228
+ if (await markDelivered(steer.id, consumer)) awaitingMark.delete(steer.id);
229
+ failures.delete(steer.id);
230
+ await settlePresence(consumer, label, parent);
231
+ if (once) return;
232
+ } else {
233
+ const count = (failures.get(steer.id) ?? 0) + 1;
234
+ failures.set(steer.id, count);
235
+ console.error(`[pump] exec exited ${code} for ${steer.id} (attempt ${count}/3)`);
236
+ if (count >= 3) {
237
+ // A failed host injection is still pending work; only successful execs may mark delivery.
238
+ failures.delete(steer.id);
239
+ console.error(`[pump] stopped after three failures for ${steer.id} — steer remains pending`);
240
+ await setPresence(consumer, label, parent, {
241
+ phase: 'idle',
242
+ detail: `delivery failed: ${steer.id} remains pending`,
243
+ });
244
+ throw new Error(`Pump delivery failed three times for ${steer.id}; steer remains pending.`);
245
+ } else {
246
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
247
+ }
248
+ await settlePresence(consumer, label, parent);
249
+ }
250
+ }
251
+ },
252
+ );
package/src/cli/index.ts CHANGED
@@ -8,7 +8,6 @@ import { runAgentCli } from './agent.js';
8
8
  import { isCanvasTheme } from '../shared/themes.js';
9
9
  import { createCanvas } from '../server/index.js';
10
10
  import { seedDemoCanvas } from '../server/demo.js';
11
- import { ensureDefaultDockedNodes } from '../server/server.js';
12
11
 
13
12
  const args = process.argv.slice(2);
14
13
 
@@ -45,6 +44,7 @@ const AGENT_COMMANDS = new Set([
45
44
  'screenshot',
46
45
  'pin',
47
46
  'ax',
47
+ 'pump',
48
48
  'undo',
49
49
  'redo',
50
50
  'history',
@@ -404,10 +404,6 @@ Examples:
404
404
 
405
405
  if (demo && canvas.getLayout().nodes.length === 0) {
406
406
  seedDemoCanvas();
407
- } else if (!demo) {
408
- // First-run only: dock a status (left) + context (right) widget by default so
409
- // a fresh canvas isn't empty. No-op once the workspace has saved state.
410
- ensureDefaultDockedNodes();
411
407
  }
412
408
 
413
409
  console.log(`\n PMX Canvas running at http://localhost:${canvas.port}`);
package/src/cli/shared.ts CHANGED
@@ -118,7 +118,7 @@ function output(data: unknown): void {
118
118
  async function invokeOperation(name: string, input: Record<string, unknown>): Promise<unknown> {
119
119
  const base = getBaseUrl();
120
120
  try {
121
- return await new HttpOperationInvoker(base).invoke(name, input);
121
+ return await new HttpOperationInvoker(base, 'cli').invoke(name, input);
122
122
  } catch (error) {
123
123
  if (error instanceof OperationError) {
124
124
  die(error.message);