yaml-flow 5.4.2 → 6.0.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 (199) hide show
  1. package/board-live-cards-cli.js +2 -2
  2. package/board-livecards-server-runtime.js +486 -547
  3. package/browser/asset-integrity.json +10 -0
  4. package/browser/board-livegraph-engine.js +2 -1676
  5. package/browser/board-livegraph-engine.js.map +1 -1
  6. package/browser/live-cards.js +347 -26
  7. package/browser/live-cards.schema.json +418 -132
  8. package/card-store.js +37 -0
  9. package/dist/batch/index.cjs +1 -108
  10. package/dist/batch/index.cjs.map +1 -1
  11. package/dist/batch/index.js +1 -106
  12. package/dist/batch/index.js.map +1 -1
  13. package/dist/board-live-cards-lib-Bg6EvCo5.d.cts +136 -0
  14. package/dist/board-live-cards-lib-jM2uYG1v.d.ts +136 -0
  15. package/dist/board-live-cards-public-CltXYgaY.d.cts +314 -0
  16. package/dist/board-live-cards-public-f-E-FAyp.d.ts +314 -0
  17. package/dist/board-livegraph-runtime/index.cjs +2 -1671
  18. package/dist/board-livegraph-runtime/index.cjs.map +1 -1
  19. package/dist/board-livegraph-runtime/index.d.cts +1 -2
  20. package/dist/board-livegraph-runtime/index.d.ts +1 -2
  21. package/dist/board-livegraph-runtime/index.js +2 -1662
  22. package/dist/board-livegraph-runtime/index.js.map +1 -1
  23. package/dist/board-livegraph-runtime/jsonata-sync.cjs +7587 -0
  24. package/dist/card-compute/index.cjs +9 -7159
  25. package/dist/card-compute/index.cjs.map +1 -1
  26. package/dist/card-compute/index.d.cts +22 -0
  27. package/dist/card-compute/index.d.ts +22 -0
  28. package/dist/card-compute/index.js +9 -7145
  29. package/dist/card-compute/index.js.map +1 -1
  30. package/dist/card-compute/jsonata-sync.cjs +7587 -0
  31. package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs +2 -0
  32. package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs.map +1 -0
  33. package/dist/cli/browser-api/board-live-cards-browser-adapter.d.cts +24 -0
  34. package/dist/cli/browser-api/board-live-cards-browser-adapter.d.ts +24 -0
  35. package/dist/cli/browser-api/board-live-cards-browser-adapter.js +2 -0
  36. package/dist/cli/browser-api/board-live-cards-browser-adapter.js.map +1 -0
  37. package/dist/cli/browser-api/card-store-browser-api.cjs +2 -0
  38. package/dist/cli/browser-api/card-store-browser-api.cjs.map +1 -0
  39. package/dist/cli/browser-api/card-store-browser-api.d.cts +26 -0
  40. package/dist/cli/browser-api/card-store-browser-api.d.ts +26 -0
  41. package/dist/cli/browser-api/card-store-browser-api.js +2 -0
  42. package/dist/cli/browser-api/card-store-browser-api.js.map +1 -0
  43. package/dist/cli/browser-api/jsonata-sync.cjs +7587 -0
  44. package/dist/cli/node/artifacts-store-cli.cjs +11 -0
  45. package/dist/cli/node/artifacts-store-cli.cjs.map +1 -0
  46. package/dist/cli/node/artifacts-store-cli.d.cts +8 -0
  47. package/dist/cli/node/artifacts-store-cli.d.ts +8 -0
  48. package/dist/cli/node/artifacts-store-cli.js +11 -0
  49. package/dist/cli/node/artifacts-store-cli.js.map +1 -0
  50. package/dist/cli/node/board-live-cards-cli.cjs +15 -0
  51. package/dist/cli/node/board-live-cards-cli.cjs.map +1 -0
  52. package/dist/cli/node/board-live-cards-cli.d.cts +20 -0
  53. package/dist/cli/node/board-live-cards-cli.d.ts +20 -0
  54. package/dist/cli/node/board-live-cards-cli.js +15 -0
  55. package/dist/cli/node/board-live-cards-cli.js.map +1 -0
  56. package/dist/cli/node/card-store-cli.cjs +8 -0
  57. package/dist/cli/node/card-store-cli.cjs.map +1 -0
  58. package/dist/cli/node/card-store-cli.d.cts +15 -0
  59. package/dist/cli/node/card-store-cli.d.ts +15 -0
  60. package/dist/cli/node/card-store-cli.js +8 -0
  61. package/dist/cli/node/card-store-cli.js.map +1 -0
  62. package/dist/cli/node/fs-board-adapter.cjs +14 -0
  63. package/dist/cli/node/fs-board-adapter.cjs.map +1 -0
  64. package/dist/cli/node/fs-board-adapter.d.cts +204 -0
  65. package/dist/cli/node/fs-board-adapter.d.ts +204 -0
  66. package/dist/cli/node/fs-board-adapter.js +14 -0
  67. package/dist/cli/node/fs-board-adapter.js.map +1 -0
  68. package/dist/cli/node/jsonata-sync.cjs +7587 -0
  69. package/dist/cli/node/source-cli-task-executor.cjs +11 -0
  70. package/dist/cli/node/source-cli-task-executor.cjs.map +1 -0
  71. package/dist/cli/node/source-cli-task-executor.d.cts +1 -0
  72. package/dist/cli/node/source-cli-task-executor.d.ts +1 -0
  73. package/dist/cli/node/source-cli-task-executor.js +11 -0
  74. package/dist/cli/node/source-cli-task-executor.js.map +1 -0
  75. package/dist/config/index.cjs +1 -79
  76. package/dist/config/index.cjs.map +1 -1
  77. package/dist/config/index.js +1 -76
  78. package/dist/config/index.js.map +1 -1
  79. package/dist/continuous-event-graph/index.cjs +2 -2129
  80. package/dist/continuous-event-graph/index.cjs.map +1 -1
  81. package/dist/continuous-event-graph/index.d.cts +81 -5
  82. package/dist/continuous-event-graph/index.d.ts +81 -5
  83. package/dist/continuous-event-graph/index.js +2 -2088
  84. package/dist/continuous-event-graph/index.js.map +1 -1
  85. package/dist/continuous-event-graph/jsonata-sync.cjs +7587 -0
  86. package/dist/event-graph/index.cjs +22 -8292
  87. package/dist/event-graph/index.cjs.map +1 -1
  88. package/dist/event-graph/index.js +22 -8237
  89. package/dist/event-graph/index.js.map +1 -1
  90. package/dist/execution-refs.cjs +2 -0
  91. package/dist/execution-refs.cjs.map +1 -0
  92. package/dist/execution-refs.d.cts +222 -0
  93. package/dist/execution-refs.d.ts +222 -0
  94. package/dist/execution-refs.js +2 -0
  95. package/dist/execution-refs.js.map +1 -0
  96. package/dist/index.cjs +29 -13221
  97. package/dist/index.cjs.map +1 -1
  98. package/dist/index.d.cts +2 -4
  99. package/dist/index.d.ts +2 -4
  100. package/dist/index.js +29 -13112
  101. package/dist/index.js.map +1 -1
  102. package/dist/inference/index.cjs +5 -617
  103. package/dist/inference/index.cjs.map +1 -1
  104. package/dist/inference/index.js +5 -610
  105. package/dist/inference/index.js.map +1 -1
  106. package/dist/jsonata-sync.cjs +7587 -0
  107. package/dist/{live-cards-bridge-x5XREkXm.d.cts → live-cards-bridge-BXbVTsna.d.cts} +27 -4
  108. package/dist/{live-cards-bridge-EQjytzI_.d.ts → live-cards-bridge-Ds28XR15.d.ts} +27 -4
  109. package/dist/pycli/quickjs-board-runtime.global.js +9 -0
  110. package/dist/pycli/quickjs-board-runtime.global.js.map +1 -0
  111. package/dist/pycli/quickjs-step-machine-runtime.global.js +5 -0
  112. package/dist/pycli/quickjs-step-machine-runtime.global.js.map +1 -0
  113. package/dist/step-machine/index.cjs +11 -7129
  114. package/dist/step-machine/index.cjs.map +1 -1
  115. package/dist/step-machine/index.js +11 -7113
  116. package/dist/step-machine/index.js.map +1 -1
  117. package/dist/storage-refs.cjs +10 -0
  118. package/dist/storage-refs.cjs.map +1 -0
  119. package/dist/storage-refs.d.cts +92 -0
  120. package/dist/storage-refs.d.ts +92 -0
  121. package/dist/storage-refs.js +10 -0
  122. package/dist/storage-refs.js.map +1 -0
  123. package/dist/stores/file.cjs +1 -114
  124. package/dist/stores/file.cjs.map +1 -1
  125. package/dist/stores/file.js +1 -112
  126. package/dist/stores/file.js.map +1 -1
  127. package/dist/stores/index.cjs +1 -231
  128. package/dist/stores/index.cjs.map +1 -1
  129. package/dist/stores/index.js +1 -227
  130. package/dist/stores/index.js.map +1 -1
  131. package/dist/stores/localStorage.cjs +1 -76
  132. package/dist/stores/localStorage.cjs.map +1 -1
  133. package/dist/stores/localStorage.js +1 -74
  134. package/dist/stores/localStorage.js.map +1 -1
  135. package/dist/stores/memory.cjs +1 -47
  136. package/dist/stores/memory.cjs.map +1 -1
  137. package/dist/stores/memory.js +1 -45
  138. package/dist/stores/memory.js.map +1 -1
  139. package/examples/browser/boards/portfolio-tracker/portfolio-t4.js +292 -0
  140. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-fetch-prices.js +218 -0
  141. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-fetch-prices.py +201 -0
  142. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-inference-adapter.js +25 -16
  143. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-public.js +553 -0
  144. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.py +365 -0
  145. package/examples/cli/step-machine-cli/portfolio-tracker/--base-ref/.runtime-out +1 -0
  146. package/examples/cli/step-machine-cli/portfolio-tracker/--base-ref/board-graph.json +32 -0
  147. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +53 -1
  148. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +15 -6
  149. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +6 -1
  150. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/poll-status-cli.js +57 -0
  151. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +1 -1
  152. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +1 -1
  153. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +7 -2
  154. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +6 -2
  155. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/_board_pycli.py +97 -0
  156. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/add-cards.py +50 -0
  157. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/init-board.py +44 -0
  158. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/poll-status.py +70 -0
  159. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/reset-board-dir.py +36 -0
  160. package/examples/cli/step-machine-cli/portfolio-tracker/inline-python-demo.flow.yaml +26 -0
  161. package/examples/cli/step-machine-cli/portfolio-tracker/inline-python-handlers.py +39 -0
  162. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker-pycli.flow.yaml +80 -0
  163. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +25 -172
  164. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +40 -34
  165. package/examples/cli/step-machine-cli/portfolio-tracker/run-inline-python-demo-pycli.py +46 -0
  166. package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker-pycli.py +77 -0
  167. package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +1 -2
  168. package/examples/example-board/agent-instructions.md +11 -5
  169. package/examples/example-board/demo-chat-handler.js +14 -4
  170. package/examples/example-board/demo-server-config.json +1 -0
  171. package/examples/example-board/demo-server.js +14 -7
  172. package/examples/example-board/demo-shell-browser.html +5 -4
  173. package/examples/example-board/demo-shell-with-server.html +6 -5
  174. package/examples/example-board/demo-task-executor.js +81 -35
  175. package/examples/index.html +0 -14
  176. package/examples/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +0 -1
  177. package/examples/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +1 -2
  178. package/package.json +39 -3
  179. package/schema/live-cards.schema.json +418 -132
  180. package/dist/cli/board-live-cards-cli.cjs +0 -10650
  181. package/dist/cli/board-live-cards-cli.cjs.map +0 -1
  182. package/dist/cli/board-live-cards-cli.d.cts +0 -179
  183. package/dist/cli/board-live-cards-cli.d.ts +0 -179
  184. package/dist/cli/board-live-cards-cli.js +0 -10598
  185. package/dist/cli/board-live-cards-cli.js.map +0 -1
  186. package/dist/journal-9HEgs7dU.d.ts +0 -28
  187. package/dist/journal-B-JCfQnh.d.cts +0 -28
  188. package/dist/schedule-Cszq9LYY.d.ts +0 -21
  189. package/dist/schedule-qWNL0RQh.d.cts +0 -21
  190. package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +0 -22
  191. package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +0 -16
  192. package/examples/browser/boards/portfolio-tracker/cards/portfolio-risk-assessment.json +0 -28
  193. package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +0 -15
  194. package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +0 -15
  195. package/examples/browser/boards/portfolio-tracker/cards/rebalancing-strategy.json +0 -28
  196. package/examples/browser/boards/portfolio-tracker/fetch-prices.js +0 -43
  197. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-task-executor.cjs +0 -96
  198. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.bat +0 -7
  199. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +0 -351
@@ -3,7 +3,19 @@ import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { createRequire } from 'node:module';
6
+ import net from 'node:net';
6
7
  import { execFileSync, spawn } from 'node:child_process';
8
+ import {
9
+ createBoardLiveCardsPublic,
10
+ createFsBoardPlatformAdapter,
11
+ createCardStorePublic,
12
+ createCardStore,
13
+ createArtifactsStore,
14
+ createChatArtifactsStore,
15
+ createFileArtifactsStore,
16
+ createCardFileMetadataStore,
17
+ parseRef,
18
+ } from './dist/cli/node/fs-board-adapter.js';
7
19
 
8
20
  const __filename = fileURLToPath(import.meta.url);
9
21
  const __dirname = path.dirname(__filename);
@@ -87,7 +99,6 @@ export function createRuntimeRequestDispatcher(runtime) {
87
99
  * Manages multiple boards under a single DEMO_SETUP_DIR.
88
100
  * Directory layout:
89
101
  * setupDir/
90
- * boards-config.json ← board registry
91
102
  * board-default/ ← built-in example board
92
103
  * runtime/ ← board-graph.json, cards-inventory.jsonl
93
104
  * surface/ ← tmp-cards/
@@ -116,25 +127,34 @@ export function createMultiBoardServerRuntime(options = {}) {
116
127
  const defaultCardsDir = path.resolve(
117
128
  options.defaultCardsDir || path.join(__dirname, 'cards')
118
129
  );
119
-
120
- const boardsConfigFile = path.join(setupDir, 'boards-config.json');
130
+ const configuredServerMetaStoreRef = typeof options.serverMetaStoreRef === 'string'
131
+ && options.serverMetaStoreRef.trim()
132
+ ? options.serverMetaStoreRef.trim()
133
+ : null;
134
+ const serverMetaStoreRef = configuredServerMetaStoreRef || `::fs-path::${setupDir}`;
135
+ const serverMetaArtifacts = createArtifactsStore(
136
+ createFsBoardPlatformAdapter(parseRef(serverMetaStoreRef), __dirname, { suppressSpawn: true })
137
+ .blobStorage('server-meta')
138
+ );
139
+ const boardsRegistryKey = 'boards-config.json';
121
140
  const boardServiceCache = new Map();
122
141
 
123
142
  fs.mkdirSync(setupDir, { recursive: true });
124
143
 
125
144
  function readBoardsConfig() {
126
- if (!fs.existsSync(boardsConfigFile)) {
145
+ const raw = serverMetaArtifacts.getText(boardsRegistryKey);
146
+ if (!raw) {
127
147
  return { boards: [{ id: 'default', label: 'Default Board' }] };
128
148
  }
129
149
  try {
130
- return JSON.parse(fs.readFileSync(boardsConfigFile, 'utf-8'));
150
+ return JSON.parse(raw);
131
151
  } catch {
132
152
  return { boards: [{ id: 'default', label: 'Default Board' }] };
133
153
  }
134
154
  }
135
155
 
136
156
  function writeBoardsConfig(config) {
137
- fs.writeFileSync(boardsConfigFile, JSON.stringify(config, null, 2));
157
+ serverMetaArtifacts.putText(boardsRegistryKey, JSON.stringify(config, null, 2));
138
158
  }
139
159
 
140
160
  function safeBoardId(raw) {
@@ -252,12 +272,6 @@ export function createMultiBoardServerRuntime(options = {}) {
252
272
  config.boards.push(entry);
253
273
  writeBoardsConfig(config);
254
274
 
255
- // Pre-create board directory tree so the board is immediately usable.
256
- const boardRoot = path.join(setupDir, `board-${id}`);
257
- fs.mkdirSync(path.join(boardRoot, 'runtime'), { recursive: true });
258
- fs.mkdirSync(path.join(boardRoot, 'surface'), { recursive: true });
259
- fs.mkdirSync(path.join(boardRoot, 'runtime-out'), { recursive: true });
260
-
261
275
  json(res, 200, { ok: true, board: entry });
262
276
  return true;
263
277
  }
@@ -314,6 +328,8 @@ export function createMultiBoardServerRuntime(options = {}) {
314
328
  apiBasePath,
315
329
  corsHeaders,
316
330
  setupDir,
331
+ serverMetaStoreRef,
332
+ boardsRegistryKey,
317
333
  parseUrl,
318
334
  json,
319
335
  handleBoardsRegistryApi,
@@ -377,18 +393,11 @@ export function createExampleBoardServerRuntime(options = {}) {
377
393
  : path.resolve(process.cwd(), options.defaultInferenceAdapterPath))
378
394
  : null;
379
395
 
380
- const statusSnapshotFile = path.join(runtimeOutDir, 'board-livegraph-status.json');
381
- const boardFile = path.join(boardDir, 'board-graph.json');
382
- const inventoryFile = path.join(boardDir, 'cards-inventory.jsonl');
383
-
384
396
  // Board-cards: parallel runtime dirs for the board-manager board.
385
397
  const gandalfCardsDir = options.gandalfCardsDir ? path.resolve(options.gandalfCardsDir) : null;
386
398
  const gandalfRuntimeDir = path.resolve(options.gandalfRuntimeDir || path.join(path.dirname(boardDir), 'gandalf-runtime'));
387
399
  const gandalfRuntimeOutDir = path.resolve(options.gandalfRuntimeOutDir || path.join(path.dirname(boardDir), 'gandalf-runtime-out'));
388
400
  const tmpGandalfCardsDir = gandalfCardsDir;
389
- const gandalfInventoryFile = path.join(gandalfRuntimeDir, 'cards-inventory.jsonl');
390
- const gandalfBoardFile = path.join(gandalfRuntimeDir, 'board-graph.json');
391
- const gandalfStatusSnapshotFile = path.join(gandalfRuntimeOutDir, 'board-livegraph-status.json');
392
401
 
393
402
  // Explicit gandalf-card executor paths — no fallback to regular-card paths.
394
403
  const configuredGandalfTaskExecutorPath = typeof options.gandalfTaskExecutorPath === 'string' && options.gandalfTaskExecutorPath.trim()
@@ -407,85 +416,201 @@ export function createExampleBoardServerRuntime(options = {}) {
407
416
  ? options.serverUrl.trim().replace(/\/$/, '')
408
417
  : null;
409
418
 
410
- // Board-card ID cache: O(1) lookup, mtime-refreshed each SSE tick.
411
- let _gandalfCardIds = new Set();
412
- let _gandalfInventoryMtime = 0;
413
- function _refreshGandalfCardCache() {
414
- if (!fs.existsSync(gandalfInventoryFile)) { _gandalfCardIds = new Set(); return; }
415
- const mtime = fs.statSync(gandalfInventoryFile).mtimeMs;
416
- if (mtime === _gandalfInventoryMtime) return;
417
- _gandalfInventoryMtime = mtime;
418
- _gandalfCardIds = new Set(
419
- fs.readFileSync(gandalfInventoryFile, 'utf-8')
420
- .split('\n').filter(Boolean)
421
- .map(l => { try { return JSON.parse(l).cardId; } catch { return null; } })
422
- .filter(Boolean)
423
- );
419
+ const sseClients = new Set();
420
+ const cardPathById = new Map();
421
+ const gandalfCardPathById = new Map();
422
+
423
+ function isGandalfCard(cardId) { return gandalfCardPathById.has(cardId); }
424
+
425
+ function namedPipePath(pipeName) {
426
+ if (process.platform === 'win32') return `\\\\.\\pipe\\${pipeName}`;
427
+ return path.join(os.tmpdir(), `${pipeName}.sock`);
424
428
  }
425
- function isGandalfCard(cardId) { return _gandalfCardIds.has(cardId); }
426
429
 
427
- function resolveCliJsPath() {
428
- if (configuredBoardLiveCardsCliJs && fs.existsSync(configuredBoardLiveCardsCliJs)) return configuredBoardLiveCardsCliJs;
430
+ function makeNotificationState() {
431
+ return {
432
+ status: null,
433
+ computedValues: {},
434
+ dataObjects: {},
435
+ cards: {},
436
+ sockets: new Set(),
437
+ };
438
+ }
429
439
 
430
- const envOverride = process.env.BOARD_LIVE_CARDS_CLI_JS;
431
- if (envOverride && fs.existsSync(envOverride)) return envOverride;
440
+ function appendNotification(state, event) {
441
+ if (!event || typeof event !== 'object') return;
442
+ if (event.kind === 'status') state.status = event.status;
443
+ if (event.kind === 'computed_values' && event.cardId) state.computedValues[event.cardId] = event.values;
444
+ if (event.kind === 'data_object' && event.key) state.dataObjects[event.key] = event.payload;
445
+ if (event.kind === 'card_refreshed' && event.cardId) state.cards[event.cardId] = event.card;
446
+ }
447
+
448
+ function makeBoardContext(label, runtimeDir, outputsDir, cardsRootDir, taskExecutorPath, chatHandlerPath, inferenceAdapterPath) {
449
+ const notifyChannel = `yaml-flow-server-${label}-${boardId || 'default'}-${process.pid}`;
450
+ const baseRefStr = `::fs-path::${runtimeDir}`;
451
+ const cardStoreRef = `::fs-path::${path.join(cardsRootDir, 'cards')}`;
452
+ const outputsStoreRef = `::fs-path::${path.join(outputsDir, '.outputs')}`;
453
+ const baseRef = parseRef(baseRefStr);
454
+ const adapter = createFsBoardPlatformAdapter(baseRef, __dirname, {
455
+ onWarn: (msg) => console.warn(`[server-runtime:${label}] ${msg}`),
456
+ suppressSpawn: true,
457
+ notifyChannel,
458
+ });
459
+ const board = createBoardLiveCardsPublic(baseRef, adapter);
460
+ const kv = adapter.kvStorageForRef(cardStoreRef);
461
+ const cardAdapterObj = {
462
+ readIndex: () => kv.read('_index'),
463
+ writeIndex: (idx) => kv.write('_index', idx),
464
+ readCard: (id) => kv.read(id),
465
+ writeCard: (id, card) => { kv.write(id, card); return id; },
466
+ cardExists: (id) => kv.read(id) !== null,
467
+ defaultCardKey: (id) => id,
468
+ };
469
+ const cardStore = createCardStorePublic(createCardStore(cardAdapterObj, console.warn));
470
+ const artifactsRef = parseRef(`::fs-path::${cardsRootDir}`);
471
+ const artifactsAdapter = createFsBoardPlatformAdapter(artifactsRef, __dirname, { suppressSpawn: true });
472
+ const filesArtifacts = createArtifactsStore(artifactsAdapter.blobStorage('files'));
473
+ const chatsArtifacts = createArtifactsStore(artifactsAdapter.blobStorage('chats'));
474
+ return {
475
+ label,
476
+ runtimeDir,
477
+ outputsDir,
478
+ cardsRootDir,
479
+ notifyChannel,
480
+ board,
481
+ cardStore,
482
+ filesArtifacts,
483
+ chatsArtifacts,
484
+ cardStoreRef,
485
+ outputsStoreRef,
486
+ taskExecutorPath,
487
+ chatHandlerPath,
488
+ inferenceAdapterPath,
489
+ notification: makeNotificationState(),
490
+ pipeServer: null,
491
+ initialized: false,
492
+ cardsBootstrapped: false,
493
+ };
494
+ }
432
495
 
433
- const repoDevPath = path.join(path.resolve(__dirname, '../..'), 'dist', 'cli', 'board-live-cards-cli.js');
434
- if (fs.existsSync(repoDevPath)) return repoDevPath;
496
+ const baseCtx = makeBoardContext(
497
+ 'base',
498
+ boardDir,
499
+ runtimeOutDir,
500
+ tmpCardsDir,
501
+ configuredTaskExecutorPath,
502
+ configuredChatHandlerPath,
503
+ configuredInferenceAdapterPath,
504
+ );
435
505
 
436
- try {
437
- const pkgJsonPath = require.resolve('yaml-flow/package.json', { paths: [process.cwd(), __dirname] });
438
- const pkgRoot = path.dirname(pkgJsonPath);
439
- const pkgCli = path.join(pkgRoot, 'board-live-cards-cli.js');
440
- if (fs.existsSync(pkgCli)) return pkgCli;
506
+ const gandalfCtx = configuredGandalfTaskExecutorPath && tmpGandalfCardsDir
507
+ ? makeBoardContext(
508
+ 'gandalf',
509
+ gandalfRuntimeDir,
510
+ gandalfRuntimeOutDir,
511
+ tmpGandalfCardsDir,
512
+ configuredGandalfTaskExecutorPath,
513
+ configuredGandalfChatHandlerPath,
514
+ configuredGandalfInferenceAdapterPath,
515
+ )
516
+ : null;
441
517
 
442
- const pkgDistCli = path.join(pkgRoot, 'dist', 'cli', 'board-live-cards-cli.js');
443
- if (fs.existsSync(pkgDistCli)) return pkgDistCli;
444
- } catch {
445
- // fall through
518
+ function cardFilesFromDir(dirPath, outMap) {
519
+ outMap.clear();
520
+ if (!dirPath || !fs.existsSync(dirPath)) return [];
521
+ const out = [];
522
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
523
+ if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.json')) continue;
524
+ const full = path.join(dirPath, entry.name);
525
+ try {
526
+ const card = JSON.parse(fs.readFileSync(full, 'utf-8'));
527
+ if (!card || typeof card.id !== 'string') continue;
528
+ outMap.set(card.id, full);
529
+ out.push(card);
530
+ } catch {
531
+ // ignore malformed files
532
+ }
446
533
  }
447
-
448
- return null;
534
+ return out;
449
535
  }
450
536
 
451
- const cliJs = resolveCliJsPath();
537
+ function toExecutionRef(scriptPath, extraObj) {
538
+ if (!scriptPath) return null;
539
+ return {
540
+ howToRun: 'local-node',
541
+ whatToRun: `::fs-path::${scriptPath}`,
542
+ extra: extraObj,
543
+ };
544
+ }
452
545
 
453
- if (!process.env.DEMO_STEP_MACHINE_CLI_PATH && configuredStepMachineCliPath && fs.existsSync(configuredStepMachineCliPath)) {
454
- process.env.DEMO_STEP_MACHINE_CLI_PATH = configuredStepMachineCliPath;
546
+ async function ensurePipeConsumer(ctx) {
547
+ if (!ctx || ctx.pipeServer) return;
548
+ const pipePath = namedPipePath(ctx.notifyChannel);
549
+ if (process.platform !== 'win32' && fs.existsSync(pipePath)) {
550
+ try { fs.rmSync(pipePath, { force: true }); } catch { /* best-effort */ }
551
+ }
552
+ const server = net.createServer((socket) => {
553
+ ctx.notification.sockets.add(socket);
554
+ socket.on('close', () => ctx.notification.sockets.delete(socket));
555
+ let buf = '';
556
+ socket.on('data', (chunk) => {
557
+ buf += chunk.toString('utf-8');
558
+ while (true) {
559
+ const i = buf.indexOf('\n');
560
+ if (i < 0) break;
561
+ const line = buf.slice(0, i).trim();
562
+ buf = buf.slice(i + 1);
563
+ if (!line) continue;
564
+ try {
565
+ const msg = JSON.parse(line);
566
+ const n = msg?.notification ?? msg;
567
+ appendNotification(ctx.notification, n);
568
+ } catch {
569
+ // ignore malformed lines
570
+ }
571
+ }
572
+ broadcastToSseClients();
573
+ });
574
+ });
575
+ await new Promise((resolve, reject) => {
576
+ server.once('error', reject);
577
+ server.listen(pipePath, () => resolve());
578
+ });
579
+ ctx.pipeServer = { server, pipePath };
455
580
  }
456
581
 
457
582
  function ensureCardStorageDirs(cardId) {
458
583
  const safeCardId = String(cardId || '').replace(/[^a-zA-Z0-9_-]/g, '_') || 'unknown-card';
459
584
  const baseDir = isGandalfCard(cardId) ? tmpGandalfCardsDir : tmpCardsDir;
460
- const cardDir = path.join(baseDir, safeCardId);
461
- const filesDir = path.join(cardDir, 'files');
462
- const chatsDir = path.join(cardDir, 'chats');
585
+ const filesDir = path.join(baseDir, 'files', safeCardId);
586
+ const chatsDir = path.join(baseDir, 'chats', safeCardId);
463
587
  fs.mkdirSync(filesDir, { recursive: true });
464
588
  fs.mkdirSync(chatsDir, { recursive: true });
465
- return { filesDir, chatsDir };
589
+ return { filesDir, chatsDir, safeCardId };
466
590
  }
467
591
 
468
- function normalizeDisplayFileName(name) {
469
- const input = String(name || '').trim();
470
- if (!input) return 'upload.bin';
471
- const base = path.basename(input);
472
- return base || 'upload.bin';
592
+ function artifactsStores(cardId) {
593
+ const ctx = isGandalfCard(cardId) ? gandalfCtx : baseCtx;
594
+ return {
595
+ files: ctx ? ctx.filesArtifacts : null,
596
+ chats: ctx ? ctx.chatsArtifacts : null,
597
+ };
473
598
  }
474
599
 
475
- function normalizeStem(rawStem) {
476
- const normalized = String(rawStem || '')
477
- .toLowerCase()
478
- .replace(/\s+/g, '_')
479
- .replace(/[^a-z0-9_-]/g, '_')
480
- .replace(/_+/g, '_')
481
- .replace(/^_+|_+$/g, '');
482
- return normalized || 'file';
600
+ function chatArtifactsForCard(cardId) {
601
+ const stores = artifactsStores(cardId);
602
+ if (!stores.chats) return null;
603
+ return createChatArtifactsStore(stores.chats, { indexFileName: '.index.json' });
483
604
  }
484
605
 
485
- function normalizeExt(rawExt) {
486
- if (!rawExt || rawExt === '.') return '';
487
- const extBody = String(rawExt).replace(/^\./, '').toLowerCase().replace(/[^a-z0-9]/g, '');
488
- return extBody ? `.${extBody}` : '';
606
+ function fileArtifactsForCard(cardId) {
607
+ const stores = artifactsStores(cardId);
608
+ if (!stores.files) return null;
609
+ return createFileArtifactsStore(stores.files);
610
+ }
611
+
612
+ function cardFileMetadataStore() {
613
+ return createCardFileMetadataStore();
489
614
  }
490
615
 
491
616
  function parseLeadingSerial(fileName) {
@@ -493,56 +618,19 @@ export function createExampleBoardServerRuntime(options = {}) {
493
618
  return m ? parseInt(m[1], 10) : 0;
494
619
  }
495
620
 
496
- function nextSerialFromNames(names) {
497
- let maxSeen = 0;
498
- for (const name of names) {
499
- const n = parseLeadingSerial(name);
500
- if (Number.isFinite(n) && n > maxSeen) maxSeen = n;
501
- }
502
- return maxSeen + 1;
503
- }
504
-
505
- function buildStoredFileName(displayName, serial) {
506
- const base = normalizeDisplayFileName(displayName);
507
- const ext = normalizeExt(path.extname(base));
508
- const stemRaw = ext ? base.slice(0, -path.extname(base).length) : base;
509
- const stemNorm = normalizeStem(stemRaw);
510
- const prefix = `${String(serial).padStart(3, '0')}-`;
511
-
512
- let keepExt = ext;
513
- let stemBudget = MAX_STORED_FILE_NAME_LEN - prefix.length - keepExt.length;
514
- if (stemBudget < 1) {
515
- keepExt = '';
516
- stemBudget = MAX_STORED_FILE_NAME_LEN - prefix.length;
517
- }
518
-
519
- const stem = stemNorm.slice(0, Math.max(1, stemBudget));
520
- let out = `${prefix}${stem}${keepExt}`;
521
- if (out.length > MAX_STORED_FILE_NAME_LEN) {
522
- out = out.slice(0, MAX_STORED_FILE_NAME_LEN).replace(/\.$/, '');
523
- }
524
- return out;
621
+ function normalizeDisplayFileName(name) {
622
+ const input = String(name || '').trim();
623
+ if (!input) return 'upload.bin';
624
+ const base = path.basename(input);
625
+ return base || 'upload.bin';
525
626
  }
526
627
 
527
628
  function shellQuote(s) {
528
629
  return '"' + String(s).replace(/"/g, '\\"') + '"';
529
630
  }
530
631
 
531
- function ensureBuilt() {
532
- if (!cliJs || !fs.existsSync(cliJs)) {
533
- throw new Error(
534
- 'Unable to locate board-live-cards CLI. Set boardLiveCardsCliJs option, BOARD_LIVE_CARDS_CLI_JS, or install yaml-flow in this project.'
535
- );
536
- }
537
- }
538
-
539
- function runCli(args) {
540
- ensureBuilt();
541
- return execFileSync(process.execPath, [cliJs, ...args], {
542
- cwd: process.cwd(),
543
- stdio: 'pipe',
544
- encoding: 'utf-8',
545
- });
632
+ function runCli(_args) {
633
+ throw new Error('CLI path is no longer used by server runtime. Use board public APIs.');
546
634
  }
547
635
 
548
636
  function clearDirContents(dirPath) {
@@ -559,78 +647,97 @@ export function createExampleBoardServerRuntime(options = {}) {
559
647
  }
560
648
 
561
649
  function readInventory() {
562
- if (!fs.existsSync(inventoryFile)) return [];
563
- return fs
564
- .readFileSync(inventoryFile, 'utf-8')
565
- .split('\n')
566
- .map((l) => l.trim())
567
- .filter(Boolean)
568
- .map((l) => JSON.parse(l));
650
+ return [...cardPathById.entries()].map(([cardId, cardFilePath]) => ({ cardId, cardFilePath }));
569
651
  }
570
652
 
571
653
  function readGandalfInventory() {
572
- if (!fs.existsSync(gandalfInventoryFile)) return [];
573
- return fs
574
- .readFileSync(gandalfInventoryFile, 'utf-8')
575
- .split('\n')
576
- .map((l) => l.trim())
577
- .filter(Boolean)
578
- .map((l) => JSON.parse(l));
654
+ return [...gandalfCardPathById.entries()].map(([cardId, cardFilePath]) => ({ cardId, cardFilePath }));
579
655
  }
580
656
 
581
657
  function readStatusSnapshot() {
582
- const base = fs.existsSync(statusSnapshotFile) ? readJson(statusSnapshotFile) : null;
583
- const boardSnap = fs.existsSync(gandalfStatusSnapshotFile) ? readJson(gandalfStatusSnapshotFile) : null;
584
- if (!base && !boardSnap) return null;
585
- if (!boardSnap) return base;
586
- if (!base) return boardSnap;
587
- return { ...base, tasks: { ...(base.tasks || {}), ...(boardSnap.tasks || {}) } };
658
+ const base = baseCtx.notification.status;
659
+ const side = gandalfCtx ? gandalfCtx.notification.status : null;
660
+ if (!base && !side) return null;
661
+ if (!side) return base;
662
+ if (!base) return side;
663
+
664
+ const baseCards = Array.isArray(base.cards) ? base.cards : [];
665
+ const sideCards = Array.isArray(side.cards) ? side.cards : [];
666
+ const mergedCards = [...baseCards, ...sideCards];
667
+
668
+ const sum = (obj, k) => Number(obj?.summary?.[k] || 0);
669
+ return {
670
+ ...base,
671
+ cards: mergedCards,
672
+ summary: {
673
+ ...(base.summary || {}),
674
+ card_count: mergedCards.length,
675
+ completed: sum(base, 'completed') + sum(side, 'completed'),
676
+ eligible: sum(base, 'eligible') + sum(side, 'eligible'),
677
+ pending: sum(base, 'pending') + sum(side, 'pending'),
678
+ blocked: sum(base, 'blocked') + sum(side, 'blocked'),
679
+ unresolved: sum(base, 'unresolved') + sum(side, 'unresolved'),
680
+ failed: sum(base, 'failed') + sum(side, 'failed'),
681
+ in_progress: sum(base, 'in_progress') + sum(side, 'in_progress'),
682
+ orphan_cards: sum(base, 'orphan_cards') + sum(side, 'orphan_cards'),
683
+ },
684
+ };
588
685
  }
589
686
 
590
687
  function readCardDefinitions() {
591
- function readFromInventory(invFile) {
592
- if (!fs.existsSync(invFile)) return [];
593
- const inv = fs.readFileSync(invFile, 'utf-8').split('\n').map(l => l.trim()).filter(Boolean).map(l => JSON.parse(l));
594
- const out = [];
595
- for (const entry of inv) {
596
- if (!entry || !entry.cardId || !entry.cardFilePath) continue;
597
- if (!fs.existsSync(entry.cardFilePath)) continue;
598
- out.push(readJson(entry.cardFilePath));
688
+ const fromCtx = (ctx, fallbackDir, fallbackMap) => {
689
+ if (!ctx || !ctx.cardStore) return cardFilesFromDir(fallbackDir, fallbackMap);
690
+ const result = ctx.cardStore.get({});
691
+ if (result.status !== 'success' || !Array.isArray(result.data?.cards)) {
692
+ return cardFilesFromDir(fallbackDir, fallbackMap);
599
693
  }
600
- return out;
601
- }
602
- return [...readFromInventory(inventoryFile), ...readFromInventory(gandalfInventoryFile)];
694
+ return result.data.cards;
695
+ };
696
+
697
+ const base = fromCtx(baseCtx, tmpCardsDir, cardPathById);
698
+ const side = gandalfCtx ? fromCtx(gandalfCtx, tmpGandalfCardsDir, gandalfCardPathById) : [];
699
+ return [...base, ...side];
603
700
  }
604
701
 
605
702
  function readCardRuntimeArtifacts() {
606
- function readFromDir(dir) {
607
- const cardsOutDir = path.join(dir, 'cards');
608
- if (!fs.existsSync(cardsOutDir)) return {};
609
- const out = {};
610
- for (const entry of fs.readdirSync(cardsOutDir, { withFileTypes: true })) {
611
- if (!entry.isFile() || !entry.name.endsWith('.computed.json')) continue;
612
- const cardId = entry.name.slice(0, -'.computed.json'.length);
613
- out[cardId] = readJson(path.join(cardsOutDir, entry.name));
703
+ const out = {};
704
+ for (const [cardId, values] of Object.entries(baseCtx.notification.computedValues)) {
705
+ const card = baseCtx.notification.cards[cardId];
706
+ out[cardId] = {
707
+ schema_version: 'v1',
708
+ card_id: cardId,
709
+ card_data: card?.card_data ?? {},
710
+ computed_values: values ?? {},
711
+ fetched_sources: {},
712
+ requires: {},
713
+ };
714
+ }
715
+ if (gandalfCtx) {
716
+ for (const [cardId, values] of Object.entries(gandalfCtx.notification.computedValues)) {
717
+ const card = gandalfCtx.notification.cards[cardId];
718
+ out[cardId] = {
719
+ schema_version: 'v1',
720
+ card_id: cardId,
721
+ card_data: card?.card_data ?? {},
722
+ computed_values: values ?? {},
723
+ fetched_sources: {},
724
+ requires: {},
725
+ };
614
726
  }
615
- return out;
616
727
  }
617
- return { ...readFromDir(runtimeOutDir), ...readFromDir(gandalfRuntimeOutDir) };
728
+ return out;
618
729
  }
619
730
 
620
731
  function readSourcePayloads(cardDefinition) {
621
732
  const out = {};
622
733
  if (!cardDefinition || !Array.isArray(cardDefinition.source_defs)) return out;
623
734
 
735
+ const ctx = isGandalfCard(cardDefinition.id) ? gandalfCtx : baseCtx;
736
+ const dataObjects = ctx ? ctx.notification.dataObjects : {};
624
737
  for (const sourceDef of cardDefinition.source_defs) {
625
- if (!sourceDef || !sourceDef.bindTo || !sourceDef.outputFile) continue;
626
- const filePath = path.join(boardDir, cardDefinition.id, sourceDef.outputFile);
627
- if (!fs.existsSync(filePath)) continue;
628
-
629
- const raw = fs.readFileSync(filePath, 'utf-8').trim();
630
- try {
631
- out[sourceDef.bindTo] = JSON.parse(raw);
632
- } catch {
633
- out[sourceDef.bindTo] = raw;
738
+ if (!sourceDef || !sourceDef.bindTo) continue;
739
+ if (Object.prototype.hasOwnProperty.call(dataObjects, sourceDef.bindTo)) {
740
+ out[sourceDef.bindTo] = dataObjects[sourceDef.bindTo];
634
741
  }
635
742
  }
636
743
 
@@ -638,48 +745,17 @@ export function createExampleBoardServerRuntime(options = {}) {
638
745
  }
639
746
 
640
747
  function readDataObjectsByToken() {
641
- const dirPath = path.join(runtimeOutDir, 'data-objects');
642
- if (!fs.existsSync(dirPath)) return {};
643
-
644
- const out = {};
645
- for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
646
- if (!entry.isFile()) continue;
647
- const token = entry.name;
648
- const filePath = path.join(dirPath, entry.name);
649
- try {
650
- out[token] = readJson(filePath);
651
- } catch {
652
- // Ignore malformed token files and continue.
653
- }
654
- }
655
-
656
- return out;
748
+ return {
749
+ ...(baseCtx.notification.dataObjects || {}),
750
+ ...(gandalfCtx ? gandalfCtx.notification.dataObjects : {}),
751
+ };
657
752
  }
658
753
 
659
754
  function readChatSignal(cardId) {
660
- const baseDir = isGandalfCard(cardId) ? tmpGandalfCardsDir : tmpCardsDir;
661
- const chatsDir = path.join(baseDir, cardId, 'chats');
662
- if (!fs.existsSync(chatsDir)) {
663
- return { count: 0, latest_mtime_ms: 0, processing: false };
664
- }
665
-
666
- let count = 0;
667
- let latestMtimeMs = 0;
668
- let processing = false;
669
- for (const entry of fs.readdirSync(chatsDir, { withFileTypes: true })) {
670
- if (!entry.isFile()) continue;
671
- if (entry.name === '.processing') { processing = true; continue; }
672
- count += 1;
673
- try {
674
- const st = fs.statSync(path.join(chatsDir, entry.name));
675
- const mtimeMs = Number(st.mtimeMs || 0);
676
- if (mtimeMs > latestMtimeMs) latestMtimeMs = mtimeMs;
677
- } catch {
678
- // Ignore transient file stat/read errors.
679
- }
680
- }
681
-
682
- return { count, latest_mtime_ms: latestMtimeMs, processing };
755
+ const { safeCardId } = ensureCardStorageDirs(cardId);
756
+ const chatStore = chatArtifactsForCard(cardId);
757
+ if (!chatStore) return { count: 0, latest_mtime_ms: 0, processing: false };
758
+ return chatStore.readSignal(safeCardId);
683
759
  }
684
760
 
685
761
  function buildPublishedRuntimePayload() {
@@ -773,192 +849,115 @@ export function createExampleBoardServerRuntime(options = {}) {
773
849
  return resolved;
774
850
  }
775
851
 
776
- function resolveGandalfTaskExecutorPath() {
777
- if (!configuredGandalfTaskExecutorPath) return null;
778
- if (!fs.existsSync(configuredGandalfTaskExecutorPath)) {
779
- const err = new Error(`Gandalf task executor script not found: ${configuredGandalfTaskExecutorPath}`);
780
- err.statusCode = 400;
781
- throw err;
782
- }
783
- return configuredGandalfTaskExecutorPath;
784
- }
785
-
786
- function resolveGandalfChatHandlerPath() {
787
- if (!configuredGandalfChatHandlerPath) return null;
788
- if (!fs.existsSync(configuredGandalfChatHandlerPath)) {
789
- const err = new Error(`Gandalf chat handler script not found: ${configuredGandalfChatHandlerPath}`);
790
- err.statusCode = 400;
791
- throw err;
792
- }
793
- return configuredGandalfChatHandlerPath;
794
- }
795
-
796
- function resolveGandalfInferenceAdapterPath() {
797
- if (!configuredGandalfInferenceAdapterPath) return null;
798
- if (!fs.existsSync(configuredGandalfInferenceAdapterPath)) {
799
- const err = new Error(`Gandalf inference adapter script not found: ${configuredGandalfInferenceAdapterPath}`);
800
- err.statusCode = 400;
801
- throw err;
802
- }
803
- return configuredGandalfInferenceAdapterPath;
804
- }
805
-
806
- function initGandalfCards() {
807
- const taskExecutorPath = resolveGandalfTaskExecutorPath();
808
- if (!taskExecutorPath) return; // gandalf-cards not configured; skip.
809
- fs.mkdirSync(gandalfRuntimeDir, { recursive: true });
810
- const chatHandlerPath = resolveGandalfChatHandlerPath();
811
- const inferenceAdapterPath = resolveGandalfInferenceAdapterPath();
812
- const taskExecutorCmd = `${shellQuote(process.execPath)} ${shellQuote(taskExecutorPath)}`;
813
- const chatHandlerCmd = chatHandlerPath ? `${shellQuote(process.execPath)} ${shellQuote(chatHandlerPath)}` : null;
814
- const inferenceAdapterCmd = inferenceAdapterPath ? `${shellQuote(process.execPath)} ${shellQuote(inferenceAdapterPath)}` : null;
815
- const boardSetupRoot = path.dirname(boardDir);
816
- const taskExecutorExtra = JSON.stringify({
817
- boardSetupRoot,
818
- boardId,
819
- boardRuntimeDir: path.relative(boardSetupRoot, gandalfRuntimeDir),
820
- runtimeStatusDir: path.relative(boardSetupRoot, gandalfRuntimeOutDir),
821
- cardsDir: path.relative(boardSetupRoot, tmpGandalfCardsDir),
822
- ...(serverUrl ? { serverUrl } : {}),
823
- });
824
- const initArgs = ['init', gandalfRuntimeDir, '--task-executor', taskExecutorCmd, '--task-executor-extra', taskExecutorExtra];
825
- if (chatHandlerCmd) initArgs.push('--chat-handler', chatHandlerCmd);
826
- if (inferenceAdapterCmd) initArgs.push('--inference-adapter', inferenceAdapterCmd);
827
- initArgs.push('--runtime-out', gandalfRuntimeOutDir);
828
- try {
829
- runCli(initArgs);
830
- } catch (err) {
831
- const msg = String((err && err.message) || err);
832
- if (!msg.includes('no valid board-graph.json')) throw err;
833
- clearDirContents(gandalfRuntimeDir);
834
- fs.mkdirSync(gandalfRuntimeDir, { recursive: true });
835
- runCli(initArgs);
836
- }
837
- }
838
-
839
- function initBoard(taskExecutorPathParam, chatHandlerPathParam, inferenceAdapterPathParam) {
840
- fs.mkdirSync(boardDir, { recursive: true });
841
-
842
- const taskExecutorPath = resolveTaskExecutorPath(taskExecutorPathParam);
843
- const chatHandlerPath = resolveChatHandlerPath(chatHandlerPathParam);
844
- const inferenceAdapterPath = resolveInferenceAdapterPath(inferenceAdapterPathParam);
845
- const taskExecutorCmd = `${shellQuote(process.execPath)} ${shellQuote(taskExecutorPath)}`;
846
- const chatHandlerCmd = chatHandlerPath
847
- ? `${shellQuote(process.execPath)} ${shellQuote(chatHandlerPath)}`
848
- : null;
849
- const inferenceAdapterCmd = inferenceAdapterPath
850
- ? `${shellQuote(process.execPath)} ${shellQuote(inferenceAdapterPath)}`
851
- : null;
852
+ async function initContext(ctx, taskExecutorPathParam, chatHandlerPathParam, inferenceAdapterPathParam) {
853
+ if (!ctx) return;
854
+ if (ctx.initialized) return;
852
855
 
856
+ const te = resolveTaskExecutorPath(taskExecutorPathParam || ctx.taskExecutorPath);
857
+ const ch = resolveChatHandlerPath(chatHandlerPathParam || ctx.chatHandlerPath);
858
+ const ia = resolveInferenceAdapterPath(inferenceAdapterPathParam || ctx.inferenceAdapterPath);
853
859
  const boardSetupRoot = path.dirname(boardDir);
854
- const taskExecutorExtra = JSON.stringify({
860
+ const extra = {
855
861
  boardSetupRoot,
856
862
  boardId,
857
- boardRuntimeDir: path.relative(boardSetupRoot, boardDir),
858
- runtimeStatusDir: path.relative(boardSetupRoot, runtimeOutDir),
859
- cardsDir: path.relative(boardSetupRoot, tmpCardsDir),
860
- ...(serverUrl ? { serverUrl } : {}),
861
- });
862
-
863
- const initArgs = ['init', boardDir, '--task-executor', taskExecutorCmd, '--task-executor-extra', taskExecutorExtra];
864
- if (chatHandlerCmd) initArgs.push('--chat-handler', chatHandlerCmd);
865
- if (inferenceAdapterCmd) initArgs.push('--inference-adapter', inferenceAdapterCmd);
866
- initArgs.push('--runtime-out', runtimeOutDir);
867
-
868
- try {
869
- runCli(initArgs);
870
- } catch (err) {
871
- const msg = String((err && err.message) || err);
872
- if (!msg.includes('no valid board-graph.json')) throw err;
873
-
874
- clearDirContents(boardDir);
875
- fs.mkdirSync(boardDir, { recursive: true });
876
- runCli(initArgs);
877
- }
878
- }
879
-
880
- function initBoardAndSetup(taskExecutorPathParam, chatHandlerPathParam, inferenceAdapterPathParam) {
881
- if (!fs.existsSync(boardFile)) {
882
- initBoard(taskExecutorPathParam, chatHandlerPathParam, inferenceAdapterPathParam);
883
- }
884
-
885
- const expectedCardsRoot = path.resolve(tmpCardsDir);
886
- const hasStaleMapping = readInventory().some((entry) => {
887
- if (!entry || !entry.cardFilePath) return false;
888
- const mapped = path.resolve(entry.cardFilePath);
889
- return !mapped.startsWith(expectedCardsRoot + path.sep) && mapped !== expectedCardsRoot;
890
- });
891
-
892
- if (hasStaleMapping) {
893
- clearDirContents(boardDir);
894
- initBoard(taskExecutorPathParam, chatHandlerPathParam, inferenceAdapterPathParam);
895
- }
896
-
897
- // Always refresh the extra in .task-executor so serverUrl and other runtime fields stay current
898
- // even when initBoard is skipped (board already initialized from a previous run).
899
- refreshTaskExecutorExtra(boardDir, {
900
- boardSetupRoot: path.dirname(boardDir),
901
- boardId,
902
- boardRuntimeDir: path.relative(path.dirname(boardDir), boardDir),
903
- runtimeStatusDir: path.relative(path.dirname(boardDir), runtimeOutDir),
904
- cardsDir: path.relative(path.dirname(boardDir), tmpCardsDir),
863
+ boardRuntimeDir: path.relative(boardSetupRoot, ctx.runtimeDir),
864
+ runtimeStatusDir: path.relative(boardSetupRoot, ctx.outputsDir),
865
+ cardsDir: path.relative(boardSetupRoot, ctx.cardsRootDir),
905
866
  ...(serverUrl ? { serverUrl } : {}),
906
867
  ...(configuredBoardLiveCardsCliJs ? { boardLiveCardsCliJs: configuredBoardLiveCardsCliJs } : {}),
907
868
  ...(configuredStepMachineCliPath ? { stepMachineCliPath: configuredStepMachineCliPath } : {}),
908
- });
869
+ };
909
870
 
910
- // Board-cards runtime: init if configured but not yet initialized.
911
- if (resolveGandalfTaskExecutorPath() && !fs.existsSync(gandalfBoardFile)) {
912
- initGandalfCards();
871
+ const params = {
872
+ cardStoreRef: ctx.cardStoreRef,
873
+ outputsStoreRef: ctx.outputsStoreRef,
874
+ };
875
+ const body = {};
876
+ body['task-executor-ref'] = toExecutionRef(te, extra);
877
+ if (ch) body['chat-handler-ref'] = toExecutionRef(ch, extra);
878
+ if (ia) body['inference-adapter-ref'] = toExecutionRef(ia, extra);
879
+
880
+ const initResult = ctx.board.init({ params, body });
881
+ if (initResult.status !== 'success') {
882
+ const err = new Error(initResult.error || `init failed for ${ctx.label}`);
883
+ err.statusCode = 500;
884
+ throw err;
913
885
  }
886
+ await ensurePipeConsumer(ctx);
887
+ ctx.initialized = true;
888
+ }
889
+
890
+ async function upsertCardsFromDir(ctx, outMap) {
891
+ if (!ctx) return;
892
+ if (ctx.cardsBootstrapped) return;
893
+ const cards = cardFilesFromDir(ctx.cardsRootDir, outMap);
894
+ for (const card of cards) {
895
+ const setResult = ctx.cardStore.set({ body: card });
896
+ if (setResult.status !== 'success') continue;
897
+ ctx.board.upsertCard({ params: { cardId: card.id, restart: true } });
898
+ }
899
+ await ctx.board.processAccumulatedEvents({});
900
+ ctx.cardsBootstrapped = true;
914
901
  }
915
902
 
916
- function bootstrapGandalfCards() {
917
- if (!fs.existsSync(tmpGandalfCardsDir)) return;
918
- if (!fs.existsSync(gandalfBoardFile)) return; // runtime not initialized; initBoardAndSetup handles it
919
- const jsonFiles = (fs.readdirSync(tmpGandalfCardsDir)).filter(f => f.endsWith('.json'));
920
- if (!jsonFiles.length) return;
921
- runCli(['upsert-card', '--rg', gandalfRuntimeDir, '--card-glob', path.join(tmpGandalfCardsDir, '*.json')]);
922
- _refreshGandalfCardCache();
903
+ async function initBoardAndSetup(taskExecutorPathParam, chatHandlerPathParam, inferenceAdapterPathParam) {
904
+ await initContext(baseCtx, taskExecutorPathParam, chatHandlerPathParam, inferenceAdapterPathParam);
905
+ if (gandalfCtx && gandalfCtx.taskExecutorPath) {
906
+ await initContext(gandalfCtx, gandalfCtx.taskExecutorPath, gandalfCtx.chatHandlerPath, gandalfCtx.inferenceAdapterPath);
907
+ }
923
908
  }
924
909
 
925
- function bootstrapCards() {
926
- runCli(['upsert-card', '--rg', boardDir, '--card-glob', path.join(tmpCardsDir, '*.json')]);
910
+ async function bootstrapBoard() {
911
+ await initBoardAndSetup();
912
+ await upsertCardsFromDir(baseCtx, cardPathById);
913
+ if (gandalfCtx) await upsertCardsFromDir(gandalfCtx, gandalfCardPathById);
927
914
  }
928
915
 
929
- function bootstrapBoard() {
930
- initBoardAndSetup();
931
- bootstrapCards();
932
- bootstrapGandalfCards();
916
+ function cardContextForCard(cardId) {
917
+ return isGandalfCard(cardId) ? gandalfCtx : baseCtx;
933
918
  }
934
919
 
935
- function findCardPath(cardId) {
936
- if (isGandalfCard(cardId)) {
937
- const found = readGandalfInventory().find((e) => e.cardId === cardId);
938
- return found ? found.cardFilePath : null;
939
- }
940
- const inv = readInventory();
941
- const found = inv.find((e) => e.cardId === cardId);
942
- return found ? found.cardFilePath : null;
920
+ function readCardFromStore(cardId) {
921
+ const ctx = cardContextForCard(cardId);
922
+ if (!ctx) return null;
923
+ const result = ctx.cardStore.get({ params: { id: cardId } });
924
+ if (result.status !== 'success') return null;
925
+ const cards = Array.isArray(result.data?.cards) ? result.data.cards : [];
926
+ return cards.length > 0 ? cards[0] : null;
943
927
  }
944
928
 
945
929
  function mutateCard(cardId, updateFn, opts) {
946
930
  const options = opts && typeof opts === 'object' ? opts : {};
947
931
  const syncBoard = options.syncBoard !== false;
948
- const cardPath = findCardPath(cardId);
949
- if (!cardPath || !fs.existsSync(cardPath)) {
932
+ const ctx = cardContextForCard(cardId);
933
+ if (!ctx) {
934
+ const err = new Error(`Card not found: ${cardId}`);
935
+ err.statusCode = 404;
936
+ throw err;
937
+ }
938
+
939
+ const card = readCardFromStore(cardId);
940
+ if (!card || typeof card !== 'object') {
950
941
  const err = new Error(`Card not found: ${cardId}`);
951
942
  err.statusCode = 404;
952
943
  throw err;
953
944
  }
954
945
 
955
- const card = readJson(cardPath);
956
946
  const nextCard = updateFn(card) || card;
957
- fs.writeFileSync(cardPath, JSON.stringify(nextCard, null, 2));
947
+ const setResult = ctx.cardStore.set({ body: nextCard });
948
+ if (setResult.status !== 'success') {
949
+ const err = new Error(setResult.error || `Failed to persist card: ${cardId}`);
950
+ err.statusCode = 500;
951
+ throw err;
952
+ }
958
953
 
959
954
  if (syncBoard) {
960
- const rg = isGandalfCard(cardId) ? gandalfRuntimeDir : boardDir;
961
- runCli(['upsert-card', '--rg', rg, '--card', cardPath, '--restart']);
955
+ const upsertResult = ctx.board.upsertCard({ params: { cardId, restart: true } });
956
+ if (upsertResult.status !== 'success') {
957
+ const err = new Error(upsertResult.error || `Failed to upsert card: ${cardId}`);
958
+ err.statusCode = 500;
959
+ throw err;
960
+ }
962
961
  }
963
962
  }
964
963
 
@@ -1028,52 +1027,39 @@ export function createExampleBoardServerRuntime(options = {}) {
1028
1027
  }
1029
1028
 
1030
1029
  function clearChatRecords(cardId) {
1031
- const { chatsDir } = ensureCardStorageDirs(cardId);
1032
- clearDirContents(chatsDir);
1030
+ const { safeCardId } = ensureCardStorageDirs(cardId);
1031
+ const chatStore = chatArtifactsForCard(cardId);
1032
+ if (!chatStore) return;
1033
+ chatStore.clear(safeCardId);
1033
1034
  }
1034
1035
 
1035
- function nextFileSerial(cardId) {
1036
+ function readCardStoredFileNames(cardId) {
1036
1037
  const names = [];
1037
-
1038
1038
  try {
1039
- const cardPath = findCardPath(cardId);
1040
- if (cardPath && fs.existsSync(cardPath)) {
1041
- const card = readJson(cardPath);
1042
- const files = card && card.card_data && Array.isArray(card.card_data.files) ? card.card_data.files : [];
1043
- for (const entry of files) {
1044
- if (entry && typeof entry.stored_name === 'string') names.push(entry.stored_name);
1045
- }
1046
- }
1039
+ const card = readCardFromStore(cardId);
1040
+ if (!card) return names;
1041
+ const metadata = cardFileMetadataStore().read(card && card.card_data ? card.card_data : null);
1042
+ for (const entry of metadata) names.push(entry.stored_name);
1047
1043
  } catch {
1048
- // ignore malformed card file and fall back to dir scan
1044
+ // ignore malformed card file
1049
1045
  }
1050
-
1051
- const { filesDir } = ensureCardStorageDirs(cardId);
1052
- if (fs.existsSync(filesDir)) {
1053
- for (const entry of fs.readdirSync(filesDir, { withFileTypes: true })) {
1054
- if (!entry.isFile()) continue;
1055
- names.push(entry.name);
1056
- }
1057
- }
1058
-
1059
- return nextSerialFromNames(names);
1046
+ return names;
1060
1047
  }
1061
1048
 
1062
1049
  function nextChatStoredName(cardId, role) {
1063
- const { chatsDir } = ensureCardStorageDirs(cardId);
1064
- const names = fs.existsSync(chatsDir)
1065
- ? fs.readdirSync(chatsDir, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name)
1066
- : [];
1067
- const serial = nextSerialFromNames(names);
1050
+ const { safeCardId } = ensureCardStorageDirs(cardId);
1051
+ const chatStore = chatArtifactsForCard(cardId);
1052
+ const serial = chatStore ? chatStore.nextSerial(safeCardId) : 1;
1068
1053
  const safeRole = String(role || 'system').toLowerCase().replace(/[^a-z0-9_-]/g, '_') || 'system';
1069
1054
  return `${String(serial).padStart(3, '0')}_${safeRole}.txt`;
1070
1055
  }
1071
1056
 
1072
1057
  function writeChatRecord(cardId, role, text, files) {
1073
1058
  const now = new Date().toISOString();
1074
- const { chatsDir } = ensureCardStorageDirs(cardId);
1059
+ const { safeCardId } = ensureCardStorageDirs(cardId);
1060
+ const stores = artifactsStores(cardId);
1075
1061
  const outName = nextChatStoredName(cardId, role || 'system');
1076
- const outPath = path.join(chatsDir, outName);
1062
+ const artifactKey = `${safeCardId}/${outName}`;
1077
1063
 
1078
1064
  const lines = [];
1079
1065
  const msg = typeof text === 'string' ? text.trim() : '';
@@ -1091,7 +1077,18 @@ export function createExampleBoardServerRuntime(options = {}) {
1091
1077
  }
1092
1078
  }
1093
1079
 
1094
- fs.writeFileSync(outPath, `${lines.join('\n')}\n`, 'utf-8');
1080
+ if (stores.chats) stores.chats.putText(artifactKey, `${lines.join('\n')}\n`);
1081
+ const serial = parseLeadingSerial(outName);
1082
+ const chatStore = chatArtifactsForCard(cardId);
1083
+ if (chatStore) {
1084
+ chatStore.appendIndexRecord(safeCardId, {
1085
+ serial,
1086
+ role: role || 'system',
1087
+ stored_name: outName,
1088
+ path: `${cardId}/chats/${outName}`,
1089
+ updated_at: now,
1090
+ });
1091
+ }
1095
1092
  return {
1096
1093
  at: now,
1097
1094
  role: role || 'system',
@@ -1102,48 +1099,31 @@ export function createExampleBoardServerRuntime(options = {}) {
1102
1099
  }
1103
1100
 
1104
1101
  function readChatRecords(cardId) {
1105
- const { chatsDir } = ensureCardStorageDirs(cardId);
1106
- if (!fs.existsSync(chatsDir)) return [];
1107
-
1108
- const out = [];
1109
- for (const entry of fs.readdirSync(chatsDir, { withFileTypes: true })) {
1110
- if (!entry.isFile()) continue;
1111
- const name = entry.name;
1112
- const parsed = String(name).match(/^(\d+)[-_]([a-z0-9_-]+)\.txt$/i);
1113
- if (!parsed) continue; // skip .processing and other non-chat files
1114
- const serial = parseInt(parsed[1], 10);
1115
- const role = parsed[2].toLowerCase();
1116
- const filePath = path.join(chatsDir, name);
1117
- const text = fs.readFileSync(filePath, 'utf-8');
1118
- const stat = fs.statSync(filePath);
1119
- out.push({
1120
- serial,
1121
- role,
1122
- text,
1123
- path: `${cardId}/chats/${name}`,
1124
- stored_name: name,
1125
- updated_at: new Date(stat.mtimeMs).toISOString(),
1126
- });
1127
- }
1128
-
1129
- out.sort((a, b) => a.serial - b.serial || a.stored_name.localeCompare(b.stored_name));
1130
- return out;
1102
+ const { safeCardId } = ensureCardStorageDirs(cardId);
1103
+ const chatStore = chatArtifactsForCard(cardId);
1104
+ if (!chatStore) return [];
1105
+ return chatStore.readRecords(safeCardId).map((row) => ({
1106
+ ...row,
1107
+ path: `${cardId}/chats/${row.stored_name}`,
1108
+ }));
1131
1109
  }
1132
1110
 
1133
1111
  function persistUploadedFile(cardId, requestedName, contentType, buffer) {
1134
- const { filesDir } = ensureCardStorageDirs(cardId);
1112
+ const { safeCardId } = ensureCardStorageDirs(cardId);
1113
+ const stores = artifactsStores(cardId);
1135
1114
  const displayName = normalizeDisplayFileName(requestedName);
1136
-
1137
- let serial = nextFileSerial(cardId);
1138
- let storedName = buildStoredFileName(displayName, serial);
1139
- while (fs.existsSync(path.join(filesDir, storedName))) {
1140
- serial += 1;
1141
- storedName = buildStoredFileName(displayName, serial);
1115
+ const fileStore = fileArtifactsForCard(cardId);
1116
+ const storedName = fileStore
1117
+ ? fileStore.allocateStoredName(safeCardId, displayName, {
1118
+ seedNames: readCardStoredFileNames(cardId),
1119
+ maxLen: MAX_STORED_FILE_NAME_LEN,
1120
+ })
1121
+ : `${String(Date.now())}-${displayName}`;
1122
+
1123
+ if (stores.files) {
1124
+ stores.files.putBytes(`${safeCardId}/${storedName}`, new Uint8Array(buffer), contentType || 'application/octet-stream');
1142
1125
  }
1143
1126
 
1144
- const targetPath = path.join(filesDir, storedName);
1145
- fs.writeFileSync(targetPath, buffer);
1146
-
1147
1127
  return {
1148
1128
  name: displayName,
1149
1129
  stored_name: storedName,
@@ -1163,6 +1143,7 @@ export function createExampleBoardServerRuntime(options = {}) {
1163
1143
  // runtimeStatusDir — relative: 'runtime-out'
1164
1144
  // cardsDir — relative: 'surface/tmp-cards' (or 'surface/tmp-gandalf-cards')
1165
1145
  // chatDir — relative (from cardsDir): e.g. 'card-portfolio/chats'
1146
+ // chatProcessingMarkerKey — relative marker key in chats artifacts store, e.g. 'card-portfolio/.processing'
1166
1147
  // lastChatFile — filename of the just-written user message, e.g. '001_user.txt'
1167
1148
  // boardLiveCardsCliJs — absolute path to board-live-cards-cli.js (if configured)
1168
1149
  // stepMachineCliPath — absolute path to step-machine-cli.js (if configured)
@@ -1175,14 +1156,25 @@ export function createExampleBoardServerRuntime(options = {}) {
1175
1156
  const handlerCmd = fs.readFileSync(handlerFile, 'utf-8').trim();
1176
1157
  if (!handlerCmd) return;
1177
1158
  const boardSetupRoot = path.dirname(boardDir);
1159
+ const { safeCardId } = ensureCardStorageDirs(cardId);
1160
+ const stores = artifactsStores(cardId);
1161
+ const processingMarkerKey = `${safeCardId}/.processing`;
1178
1162
  const processingFile = path.join(chatsDir, '.processing');
1179
- try { fs.mkdirSync(chatsDir, { recursive: true }); fs.writeFileSync(processingFile, '', 'utf-8'); } catch {}
1163
+ try {
1164
+ if (stores.chats) {
1165
+ stores.chats.putText(processingMarkerKey, '', 'text/plain; charset=utf-8');
1166
+ } else {
1167
+ fs.mkdirSync(chatsDir, { recursive: true });
1168
+ fs.writeFileSync(processingFile, '', 'utf-8');
1169
+ }
1170
+ } catch {}
1180
1171
  const extra = Buffer.from(JSON.stringify({
1181
1172
  boardSetupRoot,
1182
1173
  boardRuntimeDir: path.relative(boardSetupRoot, isGandalf ? gandalfRuntimeDir : boardDir),
1183
1174
  runtimeStatusDir: path.relative(boardSetupRoot, isGandalf ? gandalfRuntimeOutDir : runtimeOutDir),
1184
1175
  cardsDir: path.relative(boardSetupRoot, isGandalf ? tmpGandalfCardsDir : tmpCardsDir),
1185
1176
  chatDir: chatsDir,
1177
+ chatProcessingMarkerKey: processingMarkerKey,
1186
1178
  lastChatFile,
1187
1179
  ...(serverUrl ? { serverUrl } : {}),
1188
1180
  ...(configuredBoardLiveCardsCliJs ? { boardLiveCardsCliJs: configuredBoardLiveCardsCliJs } : {}),
@@ -1192,7 +1184,6 @@ export function createExampleBoardServerRuntime(options = {}) {
1192
1184
  const proc = spawn(handlerCmd, [
1193
1185
  '--boardId', boardId, '--cardId', String(cardId),
1194
1186
  '--extraEncJson', extra,
1195
- '--cleanOnExit', processingFile,
1196
1187
  ], {
1197
1188
  shell: true,
1198
1189
  stdio: 'ignore',
@@ -1200,7 +1191,13 @@ export function createExampleBoardServerRuntime(options = {}) {
1200
1191
  proc.unref();
1201
1192
  console.log(`[chat-handler] invoked for card "${cardId}" (boardId: "${boardId}")`);
1202
1193
  } catch (err) {
1203
- try { fs.unlinkSync(processingFile); } catch {}
1194
+ try {
1195
+ if (stores.chats) {
1196
+ stores.chats.remove(processingMarkerKey);
1197
+ } else {
1198
+ fs.unlinkSync(processingFile);
1199
+ }
1200
+ } catch {}
1204
1201
  console.warn(`[chat-handler] spawn failed for card "${cardId}":`, (err && err.message) || String(err));
1205
1202
  }
1206
1203
  }
@@ -1252,32 +1249,10 @@ export function createExampleBoardServerRuntime(options = {}) {
1252
1249
  }
1253
1250
 
1254
1251
  if (actionType === 'file-upload') {
1255
- const files = Array.isArray(payload && payload.files)
1256
- ? payload.files
1257
- .map((f) => {
1258
- if (!f || typeof f !== 'object') return null;
1259
- if (typeof f.stored_name !== 'string') return null;
1260
- return {
1261
- name: typeof f.name === 'string' ? f.name : f.stored_name,
1262
- stored_name: f.stored_name,
1263
- size: f.size || null,
1264
- mime_type: f.mime_type || null,
1265
- path: f.path || null,
1266
- uploaded_at: f.uploaded_at || now,
1267
- };
1268
- })
1269
- .filter(Boolean)
1270
- : [];
1252
+ const files = cardFileMetadataStore().normalizeIncoming(payload && payload.files, now);
1271
1253
 
1272
1254
  if (files.length > 0) {
1273
- const existing = Array.isArray(cardData.files) ? cardData.files.slice() : [];
1274
- const known = new Set(existing.map((f) => (f && f.stored_name ? f.stored_name : '')));
1275
- for (const f of files) {
1276
- if (known.has(f.stored_name)) continue;
1277
- existing.push(f);
1278
- known.add(f.stored_name);
1279
- }
1280
- cardData.files = existing;
1255
+ cardFileMetadataStore().merge(cardData, files);
1281
1256
  }
1282
1257
 
1283
1258
  return card;
@@ -1323,6 +1298,18 @@ export function createExampleBoardServerRuntime(options = {}) {
1323
1298
  return Buffer.concat(chunks);
1324
1299
  }
1325
1300
 
1301
+ function broadcastToSseClients() {
1302
+ const payload = buildPublishedRuntimePayload();
1303
+ const data = `data: ${JSON.stringify(payload)}\n\n`;
1304
+ for (const client of sseClients) {
1305
+ try {
1306
+ client.write(data);
1307
+ } catch {
1308
+ sseClients.delete(client);
1309
+ }
1310
+ }
1311
+ }
1312
+
1326
1313
  function handleSse(req, res) {
1327
1314
  res.writeHead(200, {
1328
1315
  ...corsHeaders,
@@ -1331,45 +1318,16 @@ export function createExampleBoardServerRuntime(options = {}) {
1331
1318
  Connection: 'keep-alive',
1332
1319
  });
1333
1320
 
1334
- const stablePayloadString = (payload) =>
1335
- JSON.stringify(payload, (key, value) => {
1336
- if (key === 'status_age_ms') return undefined;
1337
- return value;
1338
- });
1339
-
1340
- let lastPublishedHash = '';
1341
-
1342
- const emitCards = (payload) => {
1343
- res.write(`data: ${JSON.stringify(payload)}\n\n`);
1344
- };
1345
-
1346
- const initialPayload = buildPublishedRuntimePayload();
1347
- lastPublishedHash = stablePayloadString(initialPayload);
1348
- emitCards(initialPayload);
1321
+ sseClients.add(res);
1322
+ res.write(`data: ${JSON.stringify(buildPublishedRuntimePayload())}\n\n`);
1349
1323
 
1350
- const poll = setInterval(() => {
1351
- try {
1352
- runCli(['process-accumulated-events', '--rg', boardDir]);
1353
- if (fs.existsSync(gandalfBoardFile)) {
1354
- runCli(['process-accumulated-events', '--rg', gandalfRuntimeDir]);
1355
- }
1356
- _refreshGandalfCardCache();
1357
-
1358
- const nextPayload = buildPublishedRuntimePayload();
1359
- const nextHash = stablePayloadString(nextPayload);
1360
- if (nextHash !== lastPublishedHash) {
1361
- lastPublishedHash = nextHash;
1362
- emitCards(nextPayload);
1363
- } else {
1364
- res.write(': keepalive\n\n');
1365
- }
1366
- } catch (err) {
1367
- res.write(`data: ${JSON.stringify({ error: String((err && err.message) || err) })}\n\n`);
1368
- }
1369
- }, 800);
1324
+ const keepAlive = setInterval(() => {
1325
+ try { res.write(': keepalive\n\n'); } catch { /* ignore */ }
1326
+ }, 15_000);
1370
1327
 
1371
1328
  req.on('close', () => {
1372
- clearInterval(poll);
1329
+ clearInterval(keepAlive);
1330
+ sseClients.delete(res);
1373
1331
  res.end();
1374
1332
  });
1375
1333
  }
@@ -1387,25 +1345,25 @@ export function createExampleBoardServerRuntime(options = {}) {
1387
1345
  if (method === 'GET' && p === `${apiBasePath}/init-board`) {
1388
1346
  const taskExecutorPathParam = url.searchParams.get('taskExecutorPath') || '';
1389
1347
  const chatHandlerPathParam = url.searchParams.get('chatHandlerPath') || '';
1390
- initBoardAndSetup(taskExecutorPathParam, chatHandlerPathParam);
1348
+ await initBoardAndSetup(taskExecutorPathParam, chatHandlerPathParam);
1391
1349
  json(res, 200, buildPublishedRuntimePayload());
1392
1350
  return true;
1393
1351
  }
1394
1352
 
1395
1353
  if (method === 'GET' && p === `${apiBasePath}/bootstrap-cards`) {
1396
- bootstrapCards();
1354
+ await bootstrapBoard();
1397
1355
  json(res, 200, buildPublishedRuntimePayload());
1398
1356
  return true;
1399
1357
  }
1400
1358
 
1401
1359
  if (method === 'GET' && p === `${apiBasePath}/bootstrap`) {
1402
- bootstrapBoard();
1360
+ await bootstrapBoard();
1403
1361
  json(res, 200, buildPublishedRuntimePayload());
1404
1362
  return true;
1405
1363
  }
1406
1364
 
1407
1365
  if (method === 'GET' && p === `${apiBasePath}/sse`) {
1408
- bootstrapBoard();
1366
+ await bootstrapBoard();
1409
1367
  handleSse(req, res);
1410
1368
  return true;
1411
1369
  }
@@ -1417,27 +1375,29 @@ export function createExampleBoardServerRuntime(options = {}) {
1417
1375
 
1418
1376
  const cardMatch = p.match(new RegExp(`^${apiBasePath}/cards/([^/]+)$`));
1419
1377
  if (method === 'PATCH' && cardMatch) {
1420
- bootstrapBoard();
1378
+ await bootstrapBoard();
1421
1379
  const cardId = decodeURIComponent(cardMatch[1]);
1422
1380
  const body = await readJsonBody(req);
1423
1381
  patchCard(cardId, body);
1382
+ broadcastToSseClients();
1424
1383
  json(res, 200, { ok: true });
1425
1384
  return true;
1426
1385
  }
1427
1386
 
1428
1387
  const cardActionMatch = p.match(new RegExp(`^${apiBasePath}/cards/([^/]+)/actions$`));
1429
1388
  if (method === 'POST' && cardActionMatch) {
1430
- bootstrapBoard();
1389
+ await bootstrapBoard();
1431
1390
  const cardId = decodeURIComponent(cardActionMatch[1]);
1432
1391
  const body = await readJsonBody(req);
1433
1392
  applyCardAction(cardId, body && body.actionType, body && body.payload);
1393
+ broadcastToSseClients();
1434
1394
  json(res, 200, { ok: true });
1435
1395
  return true;
1436
1396
  }
1437
1397
 
1438
1398
  const cardChatsMatch = p.match(new RegExp(`^${apiBasePath}/cards/([^/]+)/chats$`));
1439
1399
  if (method === 'GET' && cardChatsMatch) {
1440
- bootstrapBoard();
1400
+ await bootstrapBoard();
1441
1401
  const cardId = decodeURIComponent(cardChatsMatch[1]);
1442
1402
  json(res, 200, { ok: true, messages: readChatRecords(cardId) });
1443
1403
  return true;
@@ -1445,7 +1405,7 @@ export function createExampleBoardServerRuntime(options = {}) {
1445
1405
 
1446
1406
  const cardFileMatch = p.match(new RegExp(`^${apiBasePath}/cards/([^/]+)/files$`));
1447
1407
  if (method === 'POST' && cardFileMatch) {
1448
- bootstrapBoard();
1408
+ await bootstrapBoard();
1449
1409
  const cardId = decodeURIComponent(cardFileMatch[1]);
1450
1410
  const inChat = String(url.searchParams.get('inChat') || '').toLowerCase() === 'true';
1451
1411
  const encodedName = req.headers['x-file-name'];
@@ -1464,23 +1424,20 @@ export function createExampleBoardServerRuntime(options = {}) {
1464
1424
  const now = new Date().toISOString();
1465
1425
  const cardData = card.card_data && typeof card.card_data === 'object' ? card.card_data : {};
1466
1426
  card.card_data = cardData;
1467
- const existing = Array.isArray(cardData.files) ? cardData.files.slice() : [];
1468
- const known = new Set(existing.map((f) => (f && f.stored_name ? f.stored_name : '')));
1469
- if (!known.has(file.stored_name)) {
1470
- existing.push({
1471
- name: typeof file.name === 'string' ? file.name : file.stored_name,
1472
- stored_name: file.stored_name,
1473
- size: file.size || null,
1474
- mime_type: file.mime_type || null,
1475
- path: file.path || null,
1476
- uploaded_at: file.uploaded_at || now,
1477
- });
1478
- cardData.files = existing;
1479
- }
1427
+ const incoming = cardFileMetadataStore().normalizeIncoming([{
1428
+ name: file.name,
1429
+ stored_name: file.stored_name,
1430
+ size: file.size,
1431
+ mime_type: file.mime_type,
1432
+ path: file.path,
1433
+ uploaded_at: file.uploaded_at || now,
1434
+ }], now);
1435
+ cardFileMetadataStore().merge(cardData, incoming);
1480
1436
  return card;
1481
1437
  });
1482
1438
  writeChatRecord(cardId, 'system', `file uploaded: ${file.name} as ${file.stored_name}`, []);
1483
1439
  }
1440
+ broadcastToSseClients();
1484
1441
  json(res, 200, { ok: true, file });
1485
1442
  return true;
1486
1443
  }
@@ -1491,53 +1448,35 @@ export function createExampleBoardServerRuntime(options = {}) {
1491
1448
  const idx = parseInt(cardFileDownloadMatch[2], 10);
1492
1449
  const expectedStoredName = url.searchParams.get('sn');
1493
1450
 
1494
- const cardPath = findCardPath(cardId);
1495
- if (!cardPath || !fs.existsSync(cardPath)) {
1451
+ const card = readCardFromStore(cardId);
1452
+ if (!card || typeof card !== 'object') {
1496
1453
  json(res, 404, { error: 'Card not found' });
1497
1454
  return true;
1498
1455
  }
1499
1456
 
1500
- let card;
1501
- try {
1502
- card = readJson(cardPath);
1503
- } catch {
1504
- json(res, 404, { error: 'Card not found' });
1457
+ const resolved = cardFileMetadataStore().resolve(card.card_data, idx, expectedStoredName);
1458
+ if (!resolved.ok && resolved.reason === 'stale_reference') {
1459
+ json(res, 409, { error: 'File reference is stale. Refresh and try again.' });
1505
1460
  return true;
1506
1461
  }
1507
-
1508
- const files = card.card_data && Array.isArray(card.card_data.files) ? card.card_data.files : [];
1509
- if (idx < 0 || idx >= files.length) {
1462
+ if (!resolved.ok) {
1510
1463
  json(res, 404, { error: 'File not found' });
1511
1464
  return true;
1512
1465
  }
1513
1466
 
1514
- const fileRecord = files[idx];
1515
- if (!fileRecord || !fileRecord.stored_name) {
1516
- json(res, 404, { error: 'File not found' });
1517
- return true;
1518
- }
1519
- if (expectedStoredName && expectedStoredName !== fileRecord.stored_name) {
1520
- json(res, 409, { error: 'File reference is stale. Refresh and try again.' });
1521
- return true;
1522
- }
1523
-
1524
- const { filesDir } = ensureCardStorageDirs(cardId);
1525
- const filePath = path.join(filesDir, fileRecord.stored_name);
1526
-
1527
- const realPath = path.resolve(filePath);
1528
- const realFilesDir = path.resolve(filesDir);
1529
- if (!realPath.startsWith(realFilesDir)) {
1530
- json(res, 403, { error: 'Forbidden' });
1531
- return true;
1532
- }
1467
+ const fileRecord = resolved.file;
1533
1468
 
1534
- if (!fs.existsSync(filePath)) {
1469
+ const { safeCardId } = ensureCardStorageDirs(cardId);
1470
+ const stores = artifactsStores(cardId);
1471
+ const fileKey = `${safeCardId}/${fileRecord.stored_name}`;
1472
+ const bytes = stores.files ? stores.files.getBytes(fileKey) : null;
1473
+ if (!bytes) {
1535
1474
  json(res, 404, { error: 'File not found' });
1536
1475
  return true;
1537
1476
  }
1538
1477
 
1539
- const buffer = fs.readFileSync(filePath);
1540
- const filename = fileRecord.name || path.basename(filePath);
1478
+ const buffer = Buffer.from(bytes);
1479
+ const filename = fileRecord.name || fileRecord.stored_name;
1541
1480
  const mimeType = fileRecord.mime_type || 'application/octet-stream';
1542
1481
  res.writeHead(200, {
1543
1482
  'Content-Type': mimeType,