@senpi/runtime 3.0.102 → 3.0.106

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 (190) hide show
  1. package/dist/actions/action-events.d.ts +7 -21
  2. package/dist/actions/action-events.d.ts.map +1 -1
  3. package/dist/actions/action-events.js +25 -56
  4. package/dist/actions/action-events.js.map +1 -1
  5. package/dist/actions/action-runtime-module.d.ts.map +1 -1
  6. package/dist/actions/action-runtime-module.js +1 -18
  7. package/dist/actions/action-runtime-module.js.map +1 -1
  8. package/dist/actions/close-position/close-position.action.d.ts.map +1 -1
  9. package/dist/actions/close-position/close-position.action.js +12 -86
  10. package/dist/actions/close-position/close-position.action.js.map +1 -1
  11. package/dist/actions/event-derivation.d.ts +2 -40
  12. package/dist/actions/event-derivation.d.ts.map +1 -1
  13. package/dist/actions/event-derivation.js +9 -189
  14. package/dist/actions/event-derivation.js.map +1 -1
  15. package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
  16. package/dist/actions/open-position/open-position.action.js +17 -115
  17. package/dist/actions/open-position/open-position.action.js.map +1 -1
  18. package/dist/actions/position-tracker/position-tracker.action.d.ts +1 -2
  19. package/dist/actions/position-tracker/position-tracker.action.d.ts.map +1 -1
  20. package/dist/actions/position-tracker/position-tracker.action.js +15 -50
  21. package/dist/actions/position-tracker/position-tracker.action.js.map +1 -1
  22. package/dist/dsl/engine-client/event-mapping.d.ts.map +1 -1
  23. package/dist/dsl/engine-client/event-mapping.js +0 -15
  24. package/dist/dsl/engine-client/event-mapping.js.map +1 -1
  25. package/dist/dsl/events/build-closed-event.d.ts +1 -16
  26. package/dist/dsl/events/build-closed-event.d.ts.map +1 -1
  27. package/dist/dsl/events/build-closed-event.js +0 -5
  28. package/dist/dsl/events/build-closed-event.js.map +1 -1
  29. package/dist/dsl/events/handlers.d.ts +0 -7
  30. package/dist/dsl/events/handlers.d.ts.map +1 -1
  31. package/dist/dsl/events/handlers.js +7 -49
  32. package/dist/dsl/events/handlers.js.map +1 -1
  33. package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
  34. package/dist/dsl/monitor/create-monitor.js +0 -7
  35. package/dist/dsl/monitor/create-monitor.js.map +1 -1
  36. package/dist/dsl/monitor/dsl-failure-events.d.ts +0 -2
  37. package/dist/dsl/monitor/dsl-failure-events.d.ts.map +1 -1
  38. package/dist/dsl/monitor/dsl-failure-events.js +1 -2
  39. package/dist/dsl/monitor/dsl-failure-events.js.map +1 -1
  40. package/dist/dsl/monitor/process-one-position.d.ts +0 -6
  41. package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
  42. package/dist/dsl/monitor/process-one-position.js +21 -55
  43. package/dist/dsl/monitor/process-one-position.js.map +1 -1
  44. package/dist/health/types.d.ts +0 -6
  45. package/dist/health/types.d.ts.map +1 -1
  46. package/dist/health/types.js.map +1 -1
  47. package/dist/index.d.ts +4 -16
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +47 -245
  50. package/dist/index.js.map +1 -1
  51. package/dist/runtime/domain-events.d.ts.map +1 -1
  52. package/dist/runtime/domain-events.js +0 -7
  53. package/dist/runtime/domain-events.js.map +1 -1
  54. package/dist/runtime/lifecycle-events.d.ts +1 -9
  55. package/dist/runtime/lifecycle-events.d.ts.map +1 -1
  56. package/dist/runtime/lifecycle-events.js +2 -11
  57. package/dist/runtime/lifecycle-events.js.map +1 -1
  58. package/dist/runtime/reconcile-run.d.ts +0 -2
  59. package/dist/runtime/reconcile-run.d.ts.map +1 -1
  60. package/dist/runtime/reconcile-run.js +1 -2
  61. package/dist/runtime/reconcile-run.js.map +1 -1
  62. package/dist/runtime/reconcile-state.d.ts +0 -8
  63. package/dist/runtime/reconcile-state.d.ts.map +1 -1
  64. package/dist/runtime/reconcile-state.js +1 -3
  65. package/dist/runtime/reconcile-state.js.map +1 -1
  66. package/dist/runtime/runtime.d.ts +5 -5
  67. package/dist/runtime/runtime.d.ts.map +1 -1
  68. package/dist/runtime/runtime.js +20 -44
  69. package/dist/runtime/runtime.js.map +1 -1
  70. package/dist/runtime/strategy-registry.d.ts +1 -0
  71. package/dist/runtime/strategy-registry.d.ts.map +1 -1
  72. package/dist/runtime/strategy-registry.js +19 -0
  73. package/dist/runtime/strategy-registry.js.map +1 -1
  74. package/dist/runtime/supervisor/long-running-supervisor.d.ts +0 -8
  75. package/dist/runtime/supervisor/long-running-supervisor.d.ts.map +1 -1
  76. package/dist/runtime/supervisor/long-running-supervisor.js +0 -1
  77. package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
  78. package/dist/runtime/update-apply-deps.d.ts +6 -0
  79. package/dist/runtime/update-apply-deps.d.ts.map +1 -1
  80. package/dist/runtime/update-apply-deps.js +8 -19
  81. package/dist/runtime/update-apply-deps.js.map +1 -1
  82. package/dist/runtime-api/intake/intake-events.d.ts +3 -3
  83. package/dist/runtime-api/intake/intake-events.d.ts.map +1 -1
  84. package/dist/runtime-api/intake/intake-events.js +3 -21
  85. package/dist/runtime-api/intake/intake-events.js.map +1 -1
  86. package/dist/runtime-api/intake/intake-mount.d.ts +2 -0
  87. package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
  88. package/dist/runtime-api/intake/intake-mount.js +10 -57
  89. package/dist/runtime-api/intake/intake-mount.js.map +1 -1
  90. package/dist/runtime-api/intake/process-signal-post.d.ts +0 -11
  91. package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
  92. package/dist/runtime-api/intake/process-signal-post.js +8 -39
  93. package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
  94. package/dist/runtime-api/intake/scaffold-error-router.d.ts +43 -0
  95. package/dist/runtime-api/intake/scaffold-error-router.d.ts.map +1 -0
  96. package/dist/runtime-api/intake/scaffold-error-router.js +63 -0
  97. package/dist/runtime-api/intake/scaffold-error-router.js.map +1 -0
  98. package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
  99. package/dist/runtime-api/intake/scanner-launcher.js +0 -12
  100. package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
  101. package/dist/runtime-api/intake/types.d.ts +1 -22
  102. package/dist/runtime-api/intake/types.d.ts.map +1 -1
  103. package/dist/runtime-api/routes/signals.d.ts.map +1 -1
  104. package/dist/runtime-api/routes/signals.js +1 -7
  105. package/dist/runtime-api/routes/signals.js.map +1 -1
  106. package/dist/runtime-api/routes/signals.schema.d.ts +0 -1
  107. package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
  108. package/dist/runtime-api/routes/signals.schema.js +0 -6
  109. package/dist/runtime-api/routes/signals.schema.js.map +1 -1
  110. package/dist/scanners/engine/engine.d.ts +0 -5
  111. package/dist/scanners/engine/engine.d.ts.map +1 -1
  112. package/dist/scanners/engine/engine.js +2 -30
  113. package/dist/scanners/engine/engine.js.map +1 -1
  114. package/dist/scanners/external/scaffold/delivery.py +12 -79
  115. package/dist/scanners/external/scaffold/scaffold.py +9 -63
  116. package/dist/scanners/external/scaffold/senpi_mcp.py +81 -10
  117. package/dist/scanners/external-scanner-receiver.d.ts +0 -11
  118. package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
  119. package/dist/scanners/external-scanner-receiver.js.map +1 -1
  120. package/dist/scanners/runtime-module.d.ts.map +1 -1
  121. package/dist/scanners/runtime-module.js +0 -5
  122. package/dist/scanners/runtime-module.js.map +1 -1
  123. package/dist/senpi/client.d.ts.map +1 -1
  124. package/dist/senpi/client.js +4 -12
  125. package/dist/senpi/client.js.map +1 -1
  126. package/dist/senpi/types.d.ts +0 -5
  127. package/dist/senpi/types.d.ts.map +1 -1
  128. package/dist/skills-manager/manifest.d.ts.map +1 -1
  129. package/dist/skills-manager/manifest.js +3 -12
  130. package/dist/skills-manager/manifest.js.map +1 -1
  131. package/dist/skills-manager/skills-manifest.json +3 -3
  132. package/dist/telemetry/event-builders.d.ts +6 -74
  133. package/dist/telemetry/event-builders.d.ts.map +1 -1
  134. package/dist/telemetry/event-builders.js +11 -108
  135. package/dist/telemetry/event-builders.js.map +1 -1
  136. package/dist/telemetry/event-store.d.ts +0 -6
  137. package/dist/telemetry/event-store.d.ts.map +1 -1
  138. package/dist/telemetry/event-store.js +6 -6
  139. package/dist/telemetry/event-store.js.map +1 -1
  140. package/dist/telemetry/metrics.d.ts +2 -14
  141. package/dist/telemetry/metrics.d.ts.map +1 -1
  142. package/dist/telemetry/metrics.js +4 -19
  143. package/dist/telemetry/metrics.js.map +1 -1
  144. package/dist/telemetry/tracing.d.ts +0 -9
  145. package/dist/telemetry/tracing.d.ts.map +1 -1
  146. package/dist/telemetry/tracing.js +0 -36
  147. package/dist/telemetry/tracing.js.map +1 -1
  148. package/dist/types/dsl/index.d.ts +0 -11
  149. package/dist/types/dsl/index.d.ts.map +1 -1
  150. package/dist/types/scanner.d.ts +0 -9
  151. package/dist/types/scanner.d.ts.map +1 -1
  152. package/dist/utils/event-catalog.d.ts +0 -81
  153. package/dist/utils/event-catalog.d.ts.map +1 -1
  154. package/dist/utils/event-catalog.js +0 -1
  155. package/dist/utils/event-catalog.js.map +1 -1
  156. package/dist/utils/keyed-mutex.d.ts +1 -3
  157. package/dist/utils/keyed-mutex.d.ts.map +1 -1
  158. package/dist/utils/keyed-mutex.js +1 -3
  159. package/dist/utils/keyed-mutex.js.map +1 -1
  160. package/openclaw.plugin.json +1 -1
  161. package/package.json +1 -1
  162. package/dist/actions/skipped-signal-results.d.ts +0 -16
  163. package/dist/actions/skipped-signal-results.d.ts.map +0 -1
  164. package/dist/actions/skipped-signal-results.js +0 -47
  165. package/dist/actions/skipped-signal-results.js.map +0 -1
  166. package/dist/scanners/external/scaffold/ulid.py +0 -58
  167. package/dist/telemetry/live-telemetry.d.ts +0 -158
  168. package/dist/telemetry/live-telemetry.d.ts.map +0 -1
  169. package/dist/telemetry/live-telemetry.js +0 -581
  170. package/dist/telemetry/live-telemetry.js.map +0 -1
  171. package/dist/telemetry/scanner-stats.d.ts +0 -136
  172. package/dist/telemetry/scanner-stats.d.ts.map +0 -1
  173. package/dist/telemetry/scanner-stats.js +0 -304
  174. package/dist/telemetry/scanner-stats.js.map +0 -1
  175. package/dist/telemetry/strategy-topology.d.ts +0 -99
  176. package/dist/telemetry/strategy-topology.d.ts.map +0 -1
  177. package/dist/telemetry/strategy-topology.js +0 -93
  178. package/dist/telemetry/strategy-topology.js.map +0 -1
  179. package/dist/telemetry/telemetry-publisher.d.ts +0 -63
  180. package/dist/telemetry/telemetry-publisher.d.ts.map +0 -1
  181. package/dist/telemetry/telemetry-publisher.js +0 -229
  182. package/dist/telemetry/telemetry-publisher.js.map +0 -1
  183. package/dist/telemetry/timeline-buffer.d.ts +0 -95
  184. package/dist/telemetry/timeline-buffer.d.ts.map +0 -1
  185. package/dist/telemetry/timeline-buffer.js +0 -237
  186. package/dist/telemetry/timeline-buffer.js.map +0 -1
  187. package/dist/utils/ulid.d.ts +0 -9
  188. package/dist/utils/ulid.d.ts.map +0 -1
  189. package/dist/utils/ulid.js +0 -43
  190. package/dist/utils/ulid.js.map +0 -1
package/dist/index.js CHANGED
@@ -42,7 +42,6 @@ import { VERSION } from "./version.js";
42
42
  import { resolvePackageName } from "./runtime/auto-update/resolve-package-name.js";
43
43
  import { runOpenClawPluginsUpdate } from "./runtime/auto-update/run-update-command.js";
44
44
  import { registerSpanHooks } from "./telemetry/spans/index.js";
45
- import { recordSignalIntake } from "./telemetry/metrics.js";
46
45
  import { registerSessionContextSeedHook } from "./prompt/session-context-seed-hook.js";
47
46
  import { initDefaultPiiRedactor } from "./telemetry/spans/redact-pii.js";
48
47
  import { initOtelSdk, shutdownOtelSdk } from "./telemetry/otel-sdk.js";
@@ -62,8 +61,8 @@ import { ingestSignalsHandler } from "./runtime-api/handlers/ingest-signals.js";
62
61
  import { createIntakeContext } from "./runtime-api/intake/intake-context.js";
63
62
  import { wireRuntime, intakeUrlFor, selectScanners } from "./runtime-api/intake/intake-mount.js";
64
63
  import { ingestDispatchMutex, ingestDispatchKey } from "./runtime-api/intake/ingest-mutex.js";
64
+ import { buildScaffoldErrorRouter } from "./runtime-api/intake/scaffold-error-router.js";
65
65
  import { loadRuntimeConfig } from "./runtime/load-runtime-config.js";
66
- import { createLiveTelemetry } from "./telemetry/live-telemetry.js";
67
66
  /** Initialize all core features (DSL, risk, health) on the given event bus. */
68
67
  export function initCore(bus) {
69
68
  initDsl(bus);
@@ -360,30 +359,6 @@ const senpiRuntimePlugin = {
360
359
  // cleared on a successful wire/install. senpi.listRuntimes reads it to render
361
360
  // "running — NO ENTRY SCANNERS" instead of a bare "running" for a blind runtime.
362
361
  const unwiredRuntimes = new Map();
363
- // ── Live telemetry ────────────────────────────────────────────────
364
- // Every runtime this box knows about, running or not, keyed runtimeId. The topology payload has
365
- // to name a stopped strategy too, so this is populated from the registry rather than from
366
- // `runtimeHandles` (which holds only the live ones) and is the source of the two facts the
367
- // registry entry does not carry: the strategy's display name and its declared scanners. Both come
368
- // from the parsed YAML — `name` is the same `config.name` that `RuntimeHealthStatus.runtimeName`
369
- // is built from, so a runtime whose config never parsed falls back to its id rather than to a
370
- // guess, and its scanner list stays empty rather than inventing rows.
371
- const telemetryStrategies = new Map();
372
- const noteTelemetryRuntime = (runtimeId, address) => {
373
- const existing = telemetryStrategies.get(runtimeId);
374
- telemetryStrategies.set(runtimeId, {
375
- address,
376
- name: existing?.name ?? runtimeId,
377
- scanners: existing?.scanners ?? [],
378
- });
379
- };
380
- const noteTelemetryRuntimeConfig = (runtimeId, config) => {
381
- telemetryStrategies.set(runtimeId, {
382
- address: config.strategy?.wallet ?? telemetryStrategies.get(runtimeId)?.address ?? "",
383
- name: config.name,
384
- scanners: config.scanners ?? [],
385
- });
386
- };
387
362
  /**
388
363
  * Wallets with an install IN FLIGHT in this process, keyed by lowercased address.
389
364
  *
@@ -405,112 +380,6 @@ const senpiRuntimePlugin = {
405
380
  // PluginLogger surface (info/warn/error) lacks `debug`, while the
406
381
  // Logger contract used by `runtime-api/` includes it.
407
382
  const apiLogger = createLogger("[senpi-core][api]");
408
- /**
409
- * One gather per publish tick. Health and system state are read together because the two payloads
410
- * need both: the topology reads health and `runtimeName`, the stats read the per-scanner rows,
411
- * and `restartCount` / `degradedReason` live only on the health rows while every other scanner
412
- * field lives only on the system-state rows — so they are joined here on `(runtimeId, scannerId)`.
413
- */
414
- const collectTelemetryFacts = async () => {
415
- const [statuses, states] = await Promise.all([
416
- collectRuntimeHealthStatuses(runtimeHandles, api.logger),
417
- collectRuntimeSystemStates(runtimeHandles, api.logger, baseStateDir),
418
- ]);
419
- const healthByRuntime = new Map();
420
- for (const status of statuses) {
421
- if (status.runtimeId !== undefined)
422
- healthByRuntime.set(status.runtimeId, status);
423
- }
424
- const scannerState = [];
425
- // Registration facts per (runtimeId, scannerId), so the topology can report a scanner's live
426
- // schedule rather than only what its YAML declared.
427
- const registrations = new Map();
428
- for (const state of states) {
429
- const runtimeId = state.runtimeId;
430
- const healthRows = runtimeId === undefined
431
- ? []
432
- : healthByRuntime.get(runtimeId)?.components.scanners.scanners ?? [];
433
- for (const row of state.components.scanners.state.scanners) {
434
- const supervision = healthRows.find((r) => r.scannerId === row.scannerId);
435
- const snapshot = {
436
- address: row.address,
437
- scannerId: row.scannerId,
438
- health: row.health,
439
- degradedReason: supervision?.degradedReason ?? null,
440
- enabled: row.enabled,
441
- inFlightSince: row.inFlightSince,
442
- lastRunStartedAt: row.lastRunStartedAt,
443
- lastRunFinishedAt: row.lastRunFinishedAt,
444
- lastRunStatus: row.lastRunStatus,
445
- lastRunReason: row.lastRunReason,
446
- nextRunAt: row.nextRunAt,
447
- lastAliveAt: row.lastAliveAt,
448
- // No supervision row means the restart count is unknown, not zero. A zero here would
449
- // read as "this scanner has never restarted" on a scanner nobody is supervising.
450
- restartCount: supervision?.restartCount ?? null,
451
- };
452
- scannerState.push(snapshot);
453
- if (runtimeId !== undefined) {
454
- registrations.set(`${runtimeId}::${row.scannerId}`, {
455
- ...snapshot,
456
- scheduleMode: row.scheduleMode,
457
- configVersion: row.configVersion,
458
- intervalSeconds: row.intervalSeconds,
459
- });
460
- }
461
- }
462
- }
463
- const strategies = [];
464
- for (const [runtimeId, entry] of telemetryStrategies) {
465
- const health = healthByRuntime.get(runtimeId);
466
- const scanners = entry.scanners.map((declared) => {
467
- const live = registrations.get(`${runtimeId}::${declared.name}`);
468
- return live === undefined
469
- ? { entry: declared }
470
- : {
471
- entry: declared,
472
- registration: {
473
- enabled: live.enabled,
474
- scheduleMode: live.scheduleMode,
475
- // The engine's own cadence, which is what the scanner actually polls on — a
476
- // recipe may omit it and take the scanner definition's default. An external
477
- // scanner registers zero because it is POSTed to rather than polled, and only
478
- // the recipe knows the cadence its scaffold was told to tick at.
479
- intervalSeconds: live.intervalSeconds > 0 ? live.intervalSeconds : declared.interval_seconds ?? null,
480
- configVersion: live.configVersion,
481
- },
482
- };
483
- });
484
- strategies.push({
485
- runtimeId,
486
- address: entry.address,
487
- // `runtimeName` is built from the same `config.name`; the indexed value is used because it
488
- // is available for a stopped runtime too, where there is no health status to read.
489
- name: health?.runtimeName ?? entry.name,
490
- scanners,
491
- });
492
- }
493
- return {
494
- strategies,
495
- runningRuntimeIds: runtimeHandles,
496
- unwiredRuntimeIds: unwiredRuntimes,
497
- readHealth: (runtimeId) => healthByRuntime.get(runtimeId),
498
- scannerState,
499
- };
500
- };
501
- const liveTelemetry = createLiveTelemetry({ collectFacts: collectTelemetryFacts });
502
- /**
503
- * Every gateway method goes through here so the broadcast handle is captured on the FIRST request
504
- * of any kind. `ctx.context` is the only route to the websocket push layer and it exists only
505
- * inside a handler — capturing it in the subscribe method alone would mean a box whose client
506
- * never subscribes can never publish anything, silently.
507
- */
508
- const registerGatewayMethod = (name, handler, opts) => {
509
- api.registerGatewayMethod(name, (ctx) => {
510
- liveTelemetry.captureBroadcast(ctx, name);
511
- return handler(ctx);
512
- }, opts);
513
- };
514
383
  // A gateway seam failed to wire/launch a runtime's external scanner stack: the runtime is up but
515
384
  // runs blind on internal scanners only. Two records, by design: a first-class `runtime.scanners_unwired`
516
385
  // event (the queryable/alertable fact, by phase) and the migrated exception-form `logger.error`
@@ -651,7 +520,7 @@ const senpiRuntimePlugin = {
651
520
  // /signals will not be mounted and no signals will be routed.
652
521
  if (sharedIntakeContext) {
653
522
  const ctx = sharedIntakeContext;
654
- routeAcceptedSignalToRuntime = (signal, correlationId, scannerId) => {
523
+ routeAcceptedSignalToRuntime = (signal, _correlationId, scannerId) => {
655
524
  const resolved = ctx.registry.resolve(scannerId);
656
525
  if (!resolved)
657
526
  return;
@@ -670,12 +539,6 @@ const senpiRuntimePlugin = {
670
539
  // supervised signal's sizing is lost ("no margin source resolved").
671
540
  ...(signal.marginPct !== undefined && { marginPct: signal.marginPct }),
672
541
  ...(signal.leverage !== undefined && { leverage: signal.leverage }),
673
- // The producing scan and the admitting acceptance row. Spread rather
674
- // than assigned so a signal without a tick id yields no key at all — an
675
- // undefined-valued key would ride on to an event attribute that exists
676
- // and says nothing.
677
- ...(signal.tick_id !== undefined && { tick_id: signal.tick_id }),
678
- ...(correlationId !== undefined && { correlationId }),
679
542
  };
680
543
  // Trace the sizing-relevant fields the narrowed request carries, so a
681
544
  // field dropped at this seam is visible instead of vanishing silently.
@@ -691,37 +554,20 @@ const senpiRuntimePlugin = {
691
554
  .then(() => {
692
555
  apiLogger.info(`[intake] ingest ok signal_id=${signal.signal_id} scanner=${resolved.scannerName} asset=${signal.asset} scanner_id=${scannerId}`);
693
556
  })
694
- .catch((err) => {
695
- // Post-acceptance failure: the scaffold journaled this signal
696
- // delivered and will NOT retry (the high-water-mark already
697
- // advanced — a retry would double-trade). This is the one
698
- // remaining silent-drop path, so it must be maximally visible.
699
- apiLogger.error(`[intake] ingest failed AFTER acceptance — signal dropped (signal_id=${signal.signal_id} scanner=${resolved.scannerName} asset=${signal.asset})`, { scannerId, err });
700
- recordSignalIntake({
701
- scannerName: resolved.scannerName,
702
- disposition: "dropped_after_accept",
703
- });
704
- });
557
+ .catch((err) => apiLogger.error("[intake] ingest failed", { scannerId, err }));
705
558
  };
706
559
  // The single server-level onError closure — the `/errors` counterpart of
707
560
  // routeAcceptedSignalToRuntime. It records a scaffold-reported tick failure into
708
561
  // the owning runtime's scanner telemetry so a scanner that fails every tick stops
709
- // rendering "healthy". Filter: only tick-level failure types count — a
710
- // `candidate_rejected` body is a per-candidate reject on an otherwise-OK tick, not
711
- // a failed run. (Mirrors buildIntakeMount's onScaffoldError, the unit-testable seam.)
712
- routeScaffoldErrorToRuntime = (scannerId, error) => {
713
- const type = typeof error.type === "string" ? error.type : undefined;
714
- if (type !== "error" && type !== "timeout" && type !== "state_persist_failed") {
715
- return;
716
- }
717
- const resolved = ctx.registry.resolve(scannerId);
718
- if (!resolved)
719
- return;
720
- const h = runtimeHandles.get(resolved.runtimeId);
721
- if (!h)
722
- return;
723
- h.recordExternalScannerError(resolved.scannerName, error);
724
- };
562
+ // rendering "healthy".
563
+ //
564
+ // This is the SHARED implementation (scaffold-error-router.ts), not a copy of
565
+ // buildIntakeMount's onScaffoldError. It used to be a copy, and the copy had its
566
+ // own hardcoded three-type filter: when `mcp_error` was added to the tick-failure
567
+ // list it reached the tested seam and never reached this route, so the
568
+ // wedged-scanner signal was green in CI and inert in production. There is now one
569
+ // filter, and scaffold-error-router.test.ts drives THIS closure.
570
+ routeScaffoldErrorToRuntime = buildScaffoldErrorRouter(ctx, runtimeHandles);
725
571
  }
726
572
  const started = [];
727
573
  try {
@@ -734,9 +580,6 @@ const senpiRuntimePlugin = {
734
580
  continue;
735
581
  const pathOrContent = entry.runtimeYamlPath ?? entry.runtimeYamlContent;
736
582
  const entryStateDir = buildRuntimeStateDir(baseStateDir, entry.id, entry.wallet ?? "");
737
- // Indexed before the boot attempt: a runtime whose start throws is installed but stopped,
738
- // and the telemetry topology has to be able to say so.
739
- noteTelemetryRuntime(entry.id, entry.wallet ?? "");
740
583
  const handle = await run(pathOrContent, {
741
584
  fromPath: hasPath,
742
585
  stateDir: entryStateDir,
@@ -759,7 +602,6 @@ const senpiRuntimePlugin = {
759
602
  if (sharedIntakeContext) {
760
603
  try {
761
604
  const runtimeConfig = await loadRuntimeConfig(pathOrContent, { fromPath: hasPath });
762
- noteTelemetryRuntimeConfig(entry.id, runtimeConfig);
763
605
  // Content-installed runtimes carry their recipe dir in the registry
764
606
  // entry (runtimeYamlDir) so relative scanner paths still resolve
765
607
  // after a restart; path-installed ones derive it from the path.
@@ -811,7 +653,6 @@ const senpiRuntimePlugin = {
811
653
  // event) so listRuntimes/health don't render a bare "running".
812
654
  try {
813
655
  const runtimeConfig = await loadRuntimeConfig(pathOrContent, { fromPath: hasPath });
814
- noteTelemetryRuntimeConfig(entry.id, runtimeConfig);
815
656
  if (selectScanners(runtimeConfig).length > 0) {
816
657
  unwiredRuntimes.set(entry.id, "no_intake");
817
658
  handle.attachSupervisionSource(() => ({
@@ -913,19 +754,8 @@ const senpiRuntimePlugin = {
913
754
  }
914
755
  }
915
756
  }
916
- // Publish ticks start only once the runtimes are up, so the first frames describe a real
917
- // box. Gated by the same switch the skills coordinator uses: unit tests boot `register()`
918
- // and must not acquire background timers. The subscribe method still answers without them.
919
- if (process.env.SENPI_RUNTIME_API_DISABLED !== "true") {
920
- liveTelemetry.start();
921
- }
922
757
  },
923
758
  async stop() {
924
- // Live telemetry goes first: it owns a timer and an in-memory buffer, nothing downstream
925
- // depends on it, and stopping it here guarantees no publish tick reads a half-torn-down
926
- // runtime handle. Records emitted during the teardown below land in a buffer nobody drains,
927
- // which is the right trade for a process on its way out.
928
- liveTelemetry.stop();
929
759
  // Drain the HTTP server FIRST so in-flight POST /signals requests
930
760
  // finish before the underlying runtime they target is torn down.
931
761
  // `apiHandle.close()` never throws.
@@ -973,7 +803,7 @@ const senpiRuntimePlugin = {
973
803
  async function installRuntimeCore(params) {
974
804
  const toError = (message) => ({ code: "INVALID_REQUEST", message });
975
805
  const toUnavailable = (message) => ({ code: "UNAVAILABLE", message });
976
- const { readRegistry, writeRegistry, validateEntry, hasId, findByWallet, deriveRuntimeId, sanitizeId } = await import("./runtime/strategy-registry.js");
806
+ const { readRegistry, writeRegistry, withRegistryLock, validateEntry, hasId, findByWallet, deriveRuntimeId, sanitizeId } = await import("./runtime/strategy-registry.js");
977
807
  const id = typeof params?.id === "string" ? params.id : undefined;
978
808
  const runtimeYamlPath = typeof params?.runtimeYamlPath === "string" ? params.runtimeYamlPath : undefined;
979
809
  const runtimeYamlContent = typeof params?.runtimeYamlContent === "string" ? params.runtimeYamlContent : undefined;
@@ -1231,8 +1061,13 @@ const senpiRuntimePlugin = {
1231
1061
  if (!valid.ok) {
1232
1062
  return { ok: false, error: toError(valid.error) };
1233
1063
  }
1234
- registry.runtimes.push(entry);
1235
- await writeRegistry(baseStateDir, registry);
1064
+ // Re-read under the registry lock rather than writing back `registry`: that snapshot predates
1065
+ // the gate and the checks above, and writing it would drop whatever another writer (an
1066
+ // update, a delete) changed in between. Same for the rollback and for delete below.
1067
+ await withRegistryLock(baseStateDir, async () => {
1068
+ const current = await readRegistry(baseStateDir);
1069
+ await writeRegistry(baseStateDir, { ...current, runtimes: [...current.runtimes, entry] });
1070
+ });
1236
1071
  /**
1237
1072
  * The unwired verdict for THIS install, tracked locally rather than read back out of
1238
1073
  * `unwiredRuntimes`, so the result reports what this call did and never a leftover verdict
@@ -1255,10 +1090,6 @@ const senpiRuntimePlugin = {
1255
1090
  runtimeId: resolvedId,
1256
1091
  });
1257
1092
  runtimeHandles.set(resolvedId, handle);
1258
- if (parsedConfig)
1259
- noteTelemetryRuntimeConfig(resolvedId, parsedConfig);
1260
- else
1261
- noteTelemetryRuntime(resolvedId, wallet);
1262
1093
  // Registry create fact — distinct from the runtime.started boot transition (which also fires
1263
1094
  // on a transient restart). Emitted inside emitEvent's guard so telemetry never fails the install.
1264
1095
  emitEvent(apiLogger, () => buildRuntimeCreatedEvent({ runtimeId: resolvedId, address: wallet, recipeName: runtimeName }));
@@ -1357,8 +1188,10 @@ const senpiRuntimePlugin = {
1357
1188
  };
1358
1189
  }
1359
1190
  catch (err) {
1360
- const next = registry.runtimes.filter((e) => e.id !== resolvedId);
1361
- await writeRegistry(baseStateDir, { ...registry, runtimes: next });
1191
+ await withRegistryLock(baseStateDir, async () => {
1192
+ const current = await readRegistry(baseStateDir);
1193
+ await writeRegistry(baseStateDir, { ...current, runtimes: current.runtimes.filter((e) => e.id !== resolvedId) });
1194
+ });
1362
1195
  const msg = err instanceof Error ? err.message : String(err);
1363
1196
  return { ok: false, error: toUnavailable(`Could not start the runtime. This is usually due to invalid or incomplete YAML.\n\n${msg}`) };
1364
1197
  }
@@ -1369,7 +1202,7 @@ const senpiRuntimePlugin = {
1369
1202
  }
1370
1203
  }
1371
1204
  /** Gateway respond accepts (ok, payload?, error?) with error as { code, message }; plugin SDK types may omit the third arg. */
1372
- registerGatewayMethod("senpi.installRuntime", async (ctx) => {
1205
+ api.registerGatewayMethod("senpi.installRuntime", async (ctx) => {
1373
1206
  const respond = ctx.respond;
1374
1207
  const result = await installRuntimeCore(ctx.params);
1375
1208
  if (result.ok)
@@ -1377,7 +1210,7 @@ const senpiRuntimePlugin = {
1377
1210
  else
1378
1211
  respond(false, undefined, result.error);
1379
1212
  }, { scope: "operator.write" });
1380
- registerGatewayMethod("senpi.listRuntimes", async (ctx) => {
1213
+ api.registerGatewayMethod("senpi.listRuntimes", async (ctx) => {
1381
1214
  const respond = ctx.respond;
1382
1215
  const { readRegistry } = await import("./runtime/strategy-registry.js");
1383
1216
  const registry = await readRegistry(baseStateDir);
@@ -1412,7 +1245,7 @@ const senpiRuntimePlugin = {
1412
1245
  * unwired-verdict cleanup all happen exactly once, here.
1413
1246
  */
1414
1247
  async function deleteRuntimeCore(params) {
1415
- const { readRegistry, writeRegistry } = await import("./runtime/strategy-registry.js");
1248
+ const { readRegistry, writeRegistry, withRegistryLock } = await import("./runtime/strategy-registry.js");
1416
1249
  const registry = await readRegistry(baseStateDir);
1417
1250
  let targetId = typeof params?.id === "string" ? params.id : undefined;
1418
1251
  const targetAddress = typeof params?.address === "string" ? params.address : undefined;
@@ -1460,9 +1293,6 @@ const senpiRuntimePlugin = {
1460
1293
  // "running — NO ENTRY SCANNERS" forever. The map is otherwise only cleared on a
1461
1294
  // successful wire/install, which a scanner-less reinstall never reaches.
1462
1295
  unwiredRuntimes.delete(targetId);
1463
- // Same reuse hazard: a deleted id left in the index would keep publishing a strategy that no
1464
- // longer exists, and would hand a reinstall the old name and scanner list.
1465
- telemetryStrategies.delete(targetId);
1466
1296
  // Archive state directory before removing from registry
1467
1297
  if (deletedEntry) {
1468
1298
  const runtimeStateDir = buildRuntimeStateDir(baseStateDir, targetId, deletedEntry.wallet ?? "");
@@ -1476,8 +1306,10 @@ const senpiRuntimePlugin = {
1476
1306
  }
1477
1307
  }
1478
1308
  if (deletedEntry) {
1479
- const next = registry.runtimes.filter((e) => e.id !== targetId);
1480
- await writeRegistry(baseStateDir, { ...registry, runtimes: next });
1309
+ await withRegistryLock(baseStateDir, async () => {
1310
+ const current = await readRegistry(baseStateDir);
1311
+ await writeRegistry(baseStateDir, { ...current, runtimes: current.runtimes.filter((e) => e.id !== targetId) });
1312
+ });
1481
1313
  }
1482
1314
  // Registry delete fact — the permanent-removal discriminator a transient runtime.stopped can't
1483
1315
  // give. Unconditional (a never-booted runtime stops no handle, so emits no runtime.stopped) and
@@ -1489,7 +1321,7 @@ const senpiRuntimePlugin = {
1489
1321
  }));
1490
1322
  return { ok: true, payload: { id: targetId } };
1491
1323
  }
1492
- registerGatewayMethod("senpi.deleteRuntime", async (ctx) => {
1324
+ api.registerGatewayMethod("senpi.deleteRuntime", async (ctx) => {
1493
1325
  const respond = ctx.respond;
1494
1326
  const result = await deleteRuntimeCore(ctx.params);
1495
1327
  if (result.ok)
@@ -1743,7 +1575,7 @@ const senpiRuntimePlugin = {
1743
1575
  * producers — slated for removal in Task 5 once the migration counter
1744
1576
  * decays to zero (see `runtime-phase-2-api-plan.md` Section 8).
1745
1577
  */
1746
- registerGatewayMethod("senpi.ingestExternalScannerData", async (ctx) => {
1578
+ api.registerGatewayMethod("senpi.ingestExternalScannerData", async (ctx) => {
1747
1579
  const respond = ctx.respond;
1748
1580
  // TODO(s11): wire actual telemetry counter for
1749
1581
  // runtime.deprecated_gateway_ingest_call (event name, properties:
@@ -1819,7 +1651,7 @@ const senpiRuntimePlugin = {
1819
1651
  });
1820
1652
  }
1821
1653
  }, { scope: "operator.write" });
1822
- registerGatewayMethod("senpi.listDslPositions", async (ctx) => {
1654
+ api.registerGatewayMethod("senpi.listDslPositions", async (ctx) => {
1823
1655
  const respond = ctx.respond;
1824
1656
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
1825
1657
  const address = typeof ctx.params?.address === "string" ? ctx.params.address : undefined;
@@ -1844,7 +1676,7 @@ const senpiRuntimePlugin = {
1844
1676
  }
1845
1677
  respond(true, { positions });
1846
1678
  }, { scope: "operator.read" });
1847
- registerGatewayMethod("senpi.getDslPositionState", async (ctx) => {
1679
+ api.registerGatewayMethod("senpi.getDslPositionState", async (ctx) => {
1848
1680
  const respond = ctx.respond;
1849
1681
  const asset = typeof ctx.params?.asset === "string" ? ctx.params.asset : undefined;
1850
1682
  if (!asset) {
@@ -1885,7 +1717,7 @@ const senpiRuntimePlugin = {
1885
1717
  }
1886
1718
  respond(true, { positions: matches });
1887
1719
  }, { scope: "operator.read" });
1888
- registerGatewayMethod("senpi.listDslArchives", async (ctx) => {
1720
+ api.registerGatewayMethod("senpi.listDslArchives", async (ctx) => {
1889
1721
  const respond = ctx.respond;
1890
1722
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
1891
1723
  const address = typeof ctx.params?.address === "string" ? ctx.params.address : undefined;
@@ -1911,7 +1743,7 @@ const senpiRuntimePlugin = {
1911
1743
  closes = closes.slice(0, limit);
1912
1744
  respond(true, { closes });
1913
1745
  }, { scope: "operator.read" });
1914
- registerGatewayMethod("senpi.getHealthStatus", async (ctx) => {
1746
+ api.registerGatewayMethod("senpi.getHealthStatus", async (ctx) => {
1915
1747
  const respond = ctx.respond;
1916
1748
  const updater = autoUpdateCoordinator.getSnapshot();
1917
1749
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
@@ -1939,7 +1771,7 @@ const senpiRuntimePlugin = {
1939
1771
  const statuses = await collectRuntimeHealthStatuses(runtimeHandles, api.logger);
1940
1772
  respond(true, { statuses, updater });
1941
1773
  }, { scope: "operator.read" });
1942
- registerGatewayMethod("senpi.getSystemState", async (ctx) => {
1774
+ api.registerGatewayMethod("senpi.getSystemState", async (ctx) => {
1943
1775
  const respond = ctx.respond;
1944
1776
  const updater = autoUpdateCoordinator.getSnapshot();
1945
1777
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
@@ -1967,36 +1799,6 @@ const senpiRuntimePlugin = {
1967
1799
  const states = await collectRuntimeSystemStates(runtimeHandles, api.logger, baseStateDir);
1968
1800
  respond(true, { states, updater });
1969
1801
  }, { scope: "operator.read" });
1970
- /**
1971
- * Live telemetry — the one method of the stream, called once per connect.
1972
- *
1973
- * It returns the current value of all three broadcast payloads plus the recent record window, so
1974
- * a client joining a box that has been running for hours renders a full screen immediately
1975
- * instead of waiting for the next thing to happen. `(afterEpoch, afterSeq)` resumes a client that
1976
- * already holds part of this epoch's run — a cursor naming any other epoch is dropped, because a
1977
- * seq from a previous run cannot address these records. `truncated` says when the records between
1978
- * a cursor of this epoch and the oldest record still held are gone, which nothing in `recent`
1979
- * alone can express.
1980
- *
1981
- * Read-only, like every other inspection method here. The scope that decides who RECEIVES the
1982
- * broadcasts is a property of the client's own connection, not of this method.
1983
- */
1984
- registerGatewayMethod("senpi.telemetry.subscribe", async (ctx) => {
1985
- const respond = ctx.respond;
1986
- try {
1987
- respond(true, await liveTelemetry.snapshot({
1988
- afterEpoch: ctx.params?.afterEpoch,
1989
- afterSeq: ctx.params?.afterSeq,
1990
- }));
1991
- }
1992
- catch (err) {
1993
- api.logger.warn("[senpi-runtime] telemetry subscribe error", { err });
1994
- respond(false, undefined, {
1995
- code: "INTERNAL_ERROR",
1996
- message: `Failed to build the telemetry snapshot: ${formatGatewayError(err)}`,
1997
- });
1998
- }
1999
- }, { scope: "operator.read" });
2000
1802
  /**
2001
1803
  * Action layer — read-only inspection.
2002
1804
  *
@@ -2016,7 +1818,7 @@ const senpiRuntimePlugin = {
2016
1818
  * persisted action-store machinery (`src/actions/store/`) usable
2017
1819
  * from the operator-facing CLI again.
2018
1820
  */
2019
- registerGatewayMethod("senpi.listActions", async (ctx) => {
1821
+ api.registerGatewayMethod("senpi.listActions", async (ctx) => {
2020
1822
  const respond = ctx.respond;
2021
1823
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2022
1824
  const address = typeof ctx.params?.address === "string" ? ctx.params.address : undefined;
@@ -2046,7 +1848,7 @@ const senpiRuntimePlugin = {
2046
1848
  }
2047
1849
  respond(true, { actions: merged });
2048
1850
  }, { scope: "operator.read" });
2049
- registerGatewayMethod("senpi.getActionState", async (ctx) => {
1851
+ api.registerGatewayMethod("senpi.getActionState", async (ctx) => {
2050
1852
  const respond = ctx.respond;
2051
1853
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2052
1854
  const actionName = typeof ctx.params?.actionName === "string" ? ctx.params.actionName : undefined;
@@ -2083,7 +1885,7 @@ const senpiRuntimePlugin = {
2083
1885
  });
2084
1886
  }
2085
1887
  }, { scope: "operator.read" });
2086
- registerGatewayMethod("senpi.getActionHistory", async (ctx) => {
1888
+ api.registerGatewayMethod("senpi.getActionHistory", async (ctx) => {
2087
1889
  const respond = ctx.respond;
2088
1890
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2089
1891
  const actionName = typeof ctx.params?.actionName === "string" ? ctx.params.actionName : undefined;
@@ -2151,7 +1953,7 @@ const senpiRuntimePlugin = {
2151
1953
  // reachable on HTTP `/audit`. Passes through the caller-controlled subset
2152
1954
  // of the audit_query contract; resource scoping (wallet → strategyId) is
2153
1955
  // owned by handle.auditQuery, never the caller.
2154
- registerGatewayMethod("senpi.auditQuery", async (ctx) => {
1956
+ api.registerGatewayMethod("senpi.auditQuery", async (ctx) => {
2155
1957
  const respond = ctx.respond;
2156
1958
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2157
1959
  if (!runtimeId) {
@@ -2199,7 +2001,7 @@ const senpiRuntimePlugin = {
2199
2001
  // its state root private, so re-derive it from baseStateDir + the handle's
2200
2002
  // wallet (same helper the writer's stateDir was built with); the writer
2201
2003
  // nests entries under `<root>/<address>/risk-guard/audit.jsonl`.
2202
- registerGatewayMethod("senpi.getRiskAudit", async (ctx) => {
2004
+ api.registerGatewayMethod("senpi.getRiskAudit", async (ctx) => {
2203
2005
  const respond = ctx.respond;
2204
2006
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2205
2007
  if (!runtimeId) {
@@ -2242,7 +2044,7 @@ const senpiRuntimePlugin = {
2242
2044
  // Per-strategy local domain-event ring (on-disk JSONL) — the trade narrative, queryable locally.
2243
2045
  // Same state-root re-derivation as getRiskAudit: the Runtime keeps its root private, so rebuild it
2244
2046
  // from baseStateDir + the handle's wallet; the writer nests rows under `<root>/strategies/<address>/events/`.
2245
- registerGatewayMethod("senpi.getEvents", async (ctx) => {
2047
+ api.registerGatewayMethod("senpi.getEvents", async (ctx) => {
2246
2048
  const respond = ctx.respond;
2247
2049
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2248
2050
  if (!runtimeId) {
@@ -2299,12 +2101,12 @@ const senpiRuntimePlugin = {
2299
2101
  }
2300
2102
  }, { scope: "operator.read" });
2301
2103
  // ─── Skills manager — read + mutate ──────────────────────────────────────
2302
- registerGatewayMethod("senpi.skills.status", async (ctx) => {
2104
+ api.registerGatewayMethod("senpi.skills.status", async (ctx) => {
2303
2105
  const respond = ctx.respond;
2304
2106
  const snapshot = skillsCoordinator.getSnapshot();
2305
2107
  respond(true, { snapshot });
2306
2108
  }, { scope: "operator.read" });
2307
- registerGatewayMethod("senpi.skills.update", async (ctx) => {
2109
+ api.registerGatewayMethod("senpi.skills.update", async (ctx) => {
2308
2110
  const respond = ctx.respond;
2309
2111
  const name = typeof ctx.params?.name === "string" ? ctx.params.name.trim() : undefined;
2310
2112
  if (!name) {
@@ -2329,7 +2131,7 @@ const senpiRuntimePlugin = {
2329
2131
  });
2330
2132
  }
2331
2133
  }, { scope: "operator.write" });
2332
- registerGatewayMethod("senpi.skills.updateAll", async (ctx) => {
2134
+ api.registerGatewayMethod("senpi.skills.updateAll", async (ctx) => {
2333
2135
  const respond = ctx.respond;
2334
2136
  try {
2335
2137
  const results = await skillsCoordinator.updateAll();