@senpi/runtime 3.0.129 → 3.0.130

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 (267) hide show
  1. package/README.md +12 -4
  2. package/dist/actions/action-events.d.ts +21 -7
  3. package/dist/actions/action-events.d.ts.map +1 -1
  4. package/dist/actions/action-events.js +54 -26
  5. package/dist/actions/action-events.js.map +1 -1
  6. package/dist/actions/action-runtime-module.d.ts.map +1 -1
  7. package/dist/actions/action-runtime-module.js +38 -7
  8. package/dist/actions/action-runtime-module.js.map +1 -1
  9. package/dist/actions/close-position/close-position.action.d.ts.map +1 -1
  10. package/dist/actions/close-position/close-position.action.js +122 -15
  11. package/dist/actions/close-position/close-position.action.js.map +1 -1
  12. package/dist/actions/decision-engine.d.ts.map +1 -1
  13. package/dist/actions/decision-engine.js +15 -2
  14. package/dist/actions/decision-engine.js.map +1 -1
  15. package/dist/actions/event-derivation.d.ts +71 -2
  16. package/dist/actions/event-derivation.d.ts.map +1 -1
  17. package/dist/actions/event-derivation.js +251 -8
  18. package/dist/actions/event-derivation.js.map +1 -1
  19. package/dist/actions/open-position/open-position.action.d.ts +12 -2
  20. package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
  21. package/dist/actions/open-position/open-position.action.js +144 -41
  22. package/dist/actions/open-position/open-position.action.js.map +1 -1
  23. package/dist/actions/position-tracker/position-tracker.action.d.ts +2 -1
  24. package/dist/actions/position-tracker/position-tracker.action.d.ts.map +1 -1
  25. package/dist/actions/position-tracker/position-tracker.action.js +62 -17
  26. package/dist/actions/position-tracker/position-tracker.action.js.map +1 -1
  27. package/dist/actions/skipped-signal-results.d.ts +25 -0
  28. package/dist/actions/skipped-signal-results.d.ts.map +1 -0
  29. package/dist/actions/skipped-signal-results.js +71 -0
  30. package/dist/actions/skipped-signal-results.js.map +1 -0
  31. package/dist/actions/store/types.d.ts +9 -2
  32. package/dist/actions/store/types.d.ts.map +1 -1
  33. package/dist/actions/store/types.js.map +1 -1
  34. package/dist/actions/types.d.ts +13 -0
  35. package/dist/actions/types.d.ts.map +1 -1
  36. package/dist/actions/types.js +13 -0
  37. package/dist/actions/types.js.map +1 -1
  38. package/dist/cli/guide-commands.d.ts.map +1 -1
  39. package/dist/cli/guide-commands.js +7 -6
  40. package/dist/cli/guide-commands.js.map +1 -1
  41. package/dist/cli/senpi-commands.d.ts.map +1 -1
  42. package/dist/cli/senpi-commands.js +4 -11
  43. package/dist/cli/senpi-commands.js.map +1 -1
  44. package/dist/dsl/engine-client/event-mapping.d.ts.map +1 -1
  45. package/dist/dsl/engine-client/event-mapping.js +14 -0
  46. package/dist/dsl/engine-client/event-mapping.js.map +1 -1
  47. package/dist/dsl/events/build-closed-event.d.ts +16 -1
  48. package/dist/dsl/events/build-closed-event.d.ts.map +1 -1
  49. package/dist/dsl/events/build-closed-event.js +6 -0
  50. package/dist/dsl/events/build-closed-event.js.map +1 -1
  51. package/dist/dsl/events/handlers.d.ts +7 -0
  52. package/dist/dsl/events/handlers.d.ts.map +1 -1
  53. package/dist/dsl/events/handlers.js +37 -7
  54. package/dist/dsl/events/handlers.js.map +1 -1
  55. package/dist/dsl/monitor/create-monitor.d.ts +6 -2
  56. package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
  57. package/dist/dsl/monitor/create-monitor.js +15 -9
  58. package/dist/dsl/monitor/create-monitor.js.map +1 -1
  59. package/dist/dsl/monitor/dsl-failure-events.d.ts +2 -0
  60. package/dist/dsl/monitor/dsl-failure-events.d.ts.map +1 -1
  61. package/dist/dsl/monitor/dsl-failure-events.js +2 -1
  62. package/dist/dsl/monitor/dsl-failure-events.js.map +1 -1
  63. package/dist/dsl/monitor/process-one-position.d.ts +1 -0
  64. package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
  65. package/dist/dsl/monitor/process-one-position.js +59 -25
  66. package/dist/dsl/monitor/process-one-position.js.map +1 -1
  67. package/dist/dsl/plugin/index.d.ts +8 -2
  68. package/dist/dsl/plugin/index.d.ts.map +1 -1
  69. package/dist/dsl/plugin/index.js +8 -6
  70. package/dist/dsl/plugin/index.js.map +1 -1
  71. package/dist/health/types.d.ts +13 -0
  72. package/dist/health/types.d.ts.map +1 -1
  73. package/dist/health/types.js +12 -0
  74. package/dist/health/types.js.map +1 -1
  75. package/dist/index.d.ts +27 -4
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +251 -39
  78. package/dist/index.js.map +1 -1
  79. package/dist/runtime/action-registry.d.ts.map +1 -1
  80. package/dist/runtime/action-registry.js +4 -3
  81. package/dist/runtime/action-registry.js.map +1 -1
  82. package/dist/runtime/auto-update/coordinator.d.ts +13 -6
  83. package/dist/runtime/auto-update/coordinator.d.ts.map +1 -1
  84. package/dist/runtime/auto-update/coordinator.js +86 -18
  85. package/dist/runtime/auto-update/coordinator.js.map +1 -1
  86. package/dist/runtime/auto-update/run-update-command.d.ts.map +1 -1
  87. package/dist/runtime/auto-update/run-update-command.js +4 -3
  88. package/dist/runtime/auto-update/run-update-command.js.map +1 -1
  89. package/dist/runtime/auto-update/types.d.ts +11 -5
  90. package/dist/runtime/auto-update/types.d.ts.map +1 -1
  91. package/dist/runtime/auto-update/types.js +11 -0
  92. package/dist/runtime/auto-update/types.js.map +1 -1
  93. package/dist/runtime/domain-events.d.ts.map +1 -1
  94. package/dist/runtime/domain-events.js +7 -0
  95. package/dist/runtime/domain-events.js.map +1 -1
  96. package/dist/runtime/lifecycle-events.d.ts +9 -1
  97. package/dist/runtime/lifecycle-events.d.ts.map +1 -1
  98. package/dist/runtime/lifecycle-events.js +11 -2
  99. package/dist/runtime/lifecycle-events.js.map +1 -1
  100. package/dist/runtime/reconcile-run.d.ts +13 -3
  101. package/dist/runtime/reconcile-run.d.ts.map +1 -1
  102. package/dist/runtime/reconcile-run.js +4 -3
  103. package/dist/runtime/reconcile-run.js.map +1 -1
  104. package/dist/runtime/reconcile-state.d.ts +8 -0
  105. package/dist/runtime/reconcile-state.d.ts.map +1 -1
  106. package/dist/runtime/reconcile-state.js +4 -1
  107. package/dist/runtime/reconcile-state.js.map +1 -1
  108. package/dist/runtime/run.d.ts +1 -0
  109. package/dist/runtime/run.d.ts.map +1 -1
  110. package/dist/runtime/run.js.map +1 -1
  111. package/dist/runtime/runtime-schema.js +3 -3
  112. package/dist/runtime/runtime-schema.js.map +1 -1
  113. package/dist/runtime/runtime.d.ts +1 -0
  114. package/dist/runtime/runtime.d.ts.map +1 -1
  115. package/dist/runtime/runtime.js +27 -3
  116. package/dist/runtime/runtime.js.map +1 -1
  117. package/dist/runtime/supervisor/long-running-supervisor.d.ts +13 -0
  118. package/dist/runtime/supervisor/long-running-supervisor.d.ts.map +1 -1
  119. package/dist/runtime/supervisor/long-running-supervisor.js +1 -0
  120. package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
  121. package/dist/runtime/validate-dsl-position-tracking.d.ts.map +1 -1
  122. package/dist/runtime/validate-dsl-position-tracking.js +2 -1
  123. package/dist/runtime/validate-dsl-position-tracking.js.map +1 -1
  124. package/dist/runtime-api/handlers/ingest-signals.d.ts.map +1 -1
  125. package/dist/runtime-api/handlers/ingest-signals.js +1 -0
  126. package/dist/runtime-api/handlers/ingest-signals.js.map +1 -1
  127. package/dist/runtime-api/handlers/types.d.ts +5 -0
  128. package/dist/runtime-api/handlers/types.d.ts.map +1 -1
  129. package/dist/runtime-api/intake/ingest-request.d.ts +17 -0
  130. package/dist/runtime-api/intake/ingest-request.d.ts.map +1 -0
  131. package/dist/runtime-api/intake/ingest-request.js +34 -0
  132. package/dist/runtime-api/intake/ingest-request.js.map +1 -0
  133. package/dist/runtime-api/intake/intake-events.d.ts +1 -1
  134. package/dist/runtime-api/intake/intake-events.d.ts.map +1 -1
  135. package/dist/runtime-api/intake/intake-events.js +9 -3
  136. package/dist/runtime-api/intake/intake-events.js.map +1 -1
  137. package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
  138. package/dist/runtime-api/intake/intake-mount.js +4 -19
  139. package/dist/runtime-api/intake/intake-mount.js.map +1 -1
  140. package/dist/runtime-api/intake/process-error-post.d.ts +6 -1
  141. package/dist/runtime-api/intake/process-error-post.d.ts.map +1 -1
  142. package/dist/runtime-api/intake/process-error-post.js +5 -1
  143. package/dist/runtime-api/intake/process-error-post.js.map +1 -1
  144. package/dist/runtime-api/intake/process-signal-post.d.ts +7 -1
  145. package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
  146. package/dist/runtime-api/intake/process-signal-post.js +5 -0
  147. package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
  148. package/dist/runtime-api/intake/scaffold-error-router.js.map +1 -1
  149. package/dist/runtime-api/intake/scaffold-tick-router.d.ts +39 -0
  150. package/dist/runtime-api/intake/scaffold-tick-router.d.ts.map +1 -0
  151. package/dist/runtime-api/intake/scaffold-tick-router.js +122 -0
  152. package/dist/runtime-api/intake/scaffold-tick-router.js.map +1 -0
  153. package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
  154. package/dist/runtime-api/intake/scanner-launcher.js +15 -0
  155. package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
  156. package/dist/runtime-api/intake/types.d.ts +55 -0
  157. package/dist/runtime-api/intake/types.d.ts.map +1 -1
  158. package/dist/runtime-api/routes/signals.d.ts +2 -0
  159. package/dist/runtime-api/routes/signals.d.ts.map +1 -1
  160. package/dist/runtime-api/routes/signals.js +5 -1
  161. package/dist/runtime-api/routes/signals.js.map +1 -1
  162. package/dist/runtime-api/routes/signals.schema.d.ts +43 -0
  163. package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
  164. package/dist/runtime-api/routes/signals.schema.js +43 -0
  165. package/dist/runtime-api/routes/signals.schema.js.map +1 -1
  166. package/dist/runtime-api/server.d.ts +7 -1
  167. package/dist/runtime-api/server.d.ts.map +1 -1
  168. package/dist/runtime-api/server.js +1 -0
  169. package/dist/runtime-api/server.js.map +1 -1
  170. package/dist/scanners/engine/engine.d.ts +22 -1
  171. package/dist/scanners/engine/engine.d.ts.map +1 -1
  172. package/dist/scanners/engine/engine.js +78 -11
  173. package/dist/scanners/engine/engine.js.map +1 -1
  174. package/dist/scanners/engine/lifecycle.d.ts +7 -1
  175. package/dist/scanners/engine/lifecycle.d.ts.map +1 -1
  176. package/dist/scanners/events.d.ts +4 -0
  177. package/dist/scanners/events.d.ts.map +1 -1
  178. package/dist/scanners/events.js +2 -0
  179. package/dist/scanners/events.js.map +1 -1
  180. package/dist/scanners/external/scaffold/delivery.py +194 -25
  181. package/dist/scanners/external/scaffold/scaffold.py +172 -31
  182. package/dist/scanners/external/scaffold/senpi_mcp.py +150 -8
  183. package/dist/scanners/external/scaffold/tick_facts.py +130 -0
  184. package/dist/scanners/external/scaffold/ulid.py +58 -0
  185. package/dist/scanners/external-scanner-receiver.d.ts +17 -0
  186. package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
  187. package/dist/scanners/external-scanner-receiver.js +8 -0
  188. package/dist/scanners/external-scanner-receiver.js.map +1 -1
  189. package/dist/scanners/runtime-module.d.ts +1 -0
  190. package/dist/scanners/runtime-module.d.ts.map +1 -1
  191. package/dist/scanners/runtime-module.js +66 -9
  192. package/dist/scanners/runtime-module.js.map +1 -1
  193. package/dist/telemetry/attributes.d.ts +20 -0
  194. package/dist/telemetry/attributes.d.ts.map +1 -1
  195. package/dist/telemetry/attributes.js +24 -0
  196. package/dist/telemetry/attributes.js.map +1 -1
  197. package/dist/telemetry/event-builders.d.ts +81 -6
  198. package/dist/telemetry/event-builders.d.ts.map +1 -1
  199. package/dist/telemetry/event-builders.js +141 -17
  200. package/dist/telemetry/event-builders.js.map +1 -1
  201. package/dist/telemetry/event-store.d.ts +6 -0
  202. package/dist/telemetry/event-store.d.ts.map +1 -1
  203. package/dist/telemetry/event-store.js +12 -8
  204. package/dist/telemetry/event-store.js.map +1 -1
  205. package/dist/telemetry/live-telemetry.d.ts +238 -0
  206. package/dist/telemetry/live-telemetry.d.ts.map +1 -0
  207. package/dist/telemetry/live-telemetry.js +853 -0
  208. package/dist/telemetry/live-telemetry.js.map +1 -0
  209. package/dist/telemetry/metrics.d.ts +14 -2
  210. package/dist/telemetry/metrics.d.ts.map +1 -1
  211. package/dist/telemetry/metrics.js +19 -4
  212. package/dist/telemetry/metrics.js.map +1 -1
  213. package/dist/telemetry/scanner-stats.d.ts +143 -0
  214. package/dist/telemetry/scanner-stats.d.ts.map +1 -0
  215. package/dist/telemetry/scanner-stats.js +426 -0
  216. package/dist/telemetry/scanner-stats.js.map +1 -0
  217. package/dist/telemetry/scanner-tick-feed.d.ts +87 -0
  218. package/dist/telemetry/scanner-tick-feed.d.ts.map +1 -0
  219. package/dist/telemetry/scanner-tick-feed.js +48 -0
  220. package/dist/telemetry/scanner-tick-feed.js.map +1 -0
  221. package/dist/telemetry/spans/redact-pii.d.ts.map +1 -1
  222. package/dist/telemetry/spans/redact-pii.js +18 -2
  223. package/dist/telemetry/spans/redact-pii.js.map +1 -1
  224. package/dist/telemetry/strategy-topology.d.ts +99 -0
  225. package/dist/telemetry/strategy-topology.d.ts.map +1 -0
  226. package/dist/telemetry/strategy-topology.js +93 -0
  227. package/dist/telemetry/strategy-topology.js.map +1 -0
  228. package/dist/telemetry/telemetry-publisher.d.ts +76 -0
  229. package/dist/telemetry/telemetry-publisher.d.ts.map +1 -0
  230. package/dist/telemetry/telemetry-publisher.js +247 -0
  231. package/dist/telemetry/telemetry-publisher.js.map +1 -0
  232. package/dist/telemetry/tick-frame.d.ts +212 -0
  233. package/dist/telemetry/tick-frame.d.ts.map +1 -0
  234. package/dist/telemetry/tick-frame.js +421 -0
  235. package/dist/telemetry/tick-frame.js.map +1 -0
  236. package/dist/telemetry/timeline-buffer.d.ts +95 -0
  237. package/dist/telemetry/timeline-buffer.d.ts.map +1 -0
  238. package/dist/telemetry/timeline-buffer.js +237 -0
  239. package/dist/telemetry/timeline-buffer.js.map +1 -0
  240. package/dist/telemetry/tracing.d.ts +9 -0
  241. package/dist/telemetry/tracing.d.ts.map +1 -1
  242. package/dist/telemetry/tracing.js +36 -0
  243. package/dist/telemetry/tracing.js.map +1 -1
  244. package/dist/types/dsl/index.d.ts +11 -0
  245. package/dist/types/dsl/index.d.ts.map +1 -1
  246. package/dist/types/scanner.d.ts +15 -0
  247. package/dist/types/scanner.d.ts.map +1 -1
  248. package/dist/utils/event-catalog.d.ts +166 -0
  249. package/dist/utils/event-catalog.d.ts.map +1 -1
  250. package/dist/utils/event-catalog.js +18 -0
  251. package/dist/utils/event-catalog.js.map +1 -1
  252. package/dist/utils/logger.d.ts +4 -1
  253. package/dist/utils/logger.d.ts.map +1 -1
  254. package/dist/utils/logger.js.map +1 -1
  255. package/dist/utils/telemetry-required-log.d.ts +60 -0
  256. package/dist/utils/telemetry-required-log.d.ts.map +1 -0
  257. package/dist/utils/telemetry-required-log.js +105 -0
  258. package/dist/utils/telemetry-required-log.js.map +1 -0
  259. package/dist/utils/ulid.d.ts +9 -0
  260. package/dist/utils/ulid.d.ts.map +1 -0
  261. package/dist/utils/ulid.js +43 -0
  262. package/dist/utils/ulid.js.map +1 -0
  263. package/dist/validate/recipe-checks.d.ts.map +1 -1
  264. package/dist/validate/recipe-checks.js +2 -2
  265. package/dist/validate/recipe-checks.js.map +1 -1
  266. package/openclaw.plugin.json +8 -4
  267. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -64,7 +64,11 @@ import { createIntakeContext } from "./runtime-api/intake/intake-context.js";
64
64
  import { wireRuntime, intakeUrlFor, selectScanners } from "./runtime-api/intake/intake-mount.js";
65
65
  import { ingestDispatchMutex, ingestDispatchKey } from "./runtime-api/intake/ingest-mutex.js";
66
66
  import { buildScaffoldErrorRouter } from "./runtime-api/intake/scaffold-error-router.js";
67
+ import { buildScaffoldTickRouter } from "./runtime-api/intake/scaffold-tick-router.js";
68
+ import { toIngestRequest } from "./runtime-api/intake/ingest-request.js";
67
69
  import { loadRuntimeConfig } from "./runtime/load-runtime-config.js";
70
+ import { createLiveTelemetry } from "./telemetry/live-telemetry.js";
71
+ import { resolveTickTelemetry } from "./telemetry/tick-frame.js";
68
72
  /** Initialize all core features (DSL, risk, health) on the given event bus. */
69
73
  export function initCore(bus) {
70
74
  initDsl(bus);
@@ -223,6 +227,16 @@ const senpiRuntimePlugin = {
223
227
  ? "env SENPI_LOG_LEVEL"
224
228
  : "default";
225
229
  pluginLogger.info(`[senpi-runtime] log level: ${startupLogLevel} (source: ${logLevelSourceLabel}). Set plugin config "logLevel" or env SENPI_LOG_LEVEL to debug|info|warn|error to override.`);
230
+ // Same shape as the log-level line above, and for the same reason: a box publishing less than
231
+ // an operator expects must say so at boot rather than be diagnosed from an empty screen. No env
232
+ // var is named because there is none — see `tickTelemetry` on SenpiRuntimePluginConfig.
233
+ const tickTelemetry = resolveTickTelemetry(config.tickTelemetry);
234
+ if (tickTelemetry.invalid !== undefined) {
235
+ pluginLogger.warn(`[senpi-runtime] Ignoring invalid plugin config ${tickTelemetry.invalid} (expected off|summary|verbose); keeping mcp=${tickTelemetry.settings.mcp} state=${tickTelemetry.settings.state}.`);
236
+ }
237
+ pluginLogger.info(tickTelemetry.settings.enabled
238
+ ? `[senpi-runtime] tick telemetry: mcp=${tickTelemetry.settings.mcp} state=${tickTelemetry.settings.state} (source: ${config.tickTelemetry === undefined ? "default" : "plugin config tickTelemetry"}). Set plugin config "tickTelemetry" to off|summary|verbose, or {mcp,state}, to override.`
239
+ : `[senpi-runtime] tick telemetry: off (source: plugin config tickTelemetry). No per-tick frame is published.`);
226
240
  const senpiCliStore = readSenpiCliStore();
227
241
  const baseStateDir = resolveSenpiBaseStateDir({
228
242
  store: senpiCliStore,
@@ -355,6 +369,7 @@ const senpiRuntimePlugin = {
355
369
  }
356
370
  let routeAcceptedSignalToRuntime;
357
371
  let routeScaffoldErrorToRuntime;
372
+ let routeScaffoldTickToTelemetry;
358
373
  const launchers = new Map();
359
374
  // Runtimes that are UP but whose external scanners never wired (mount/launch/install
360
375
  // failure), keyed runtimeId → failed phase. Populated at the four unwired sites,
@@ -382,10 +397,149 @@ const senpiRuntimePlugin = {
382
397
  * engines, one margin account: the exact state `[E_WALLET_HAS_RUNTIME]` exists to prevent.
383
398
  */
384
399
  const installsInFlight = new Map();
400
+ // ── Live telemetry ────────────────────────────────────────────────
401
+ // Every runtime this box knows about, running or not, keyed runtimeId. The topology payload has
402
+ // to name a stopped strategy too, so this is populated from the registry rather than from
403
+ // `runtimeHandles` (which holds only the live ones) and is the source of the two facts the
404
+ // registry entry does not carry: the strategy's display name and its declared scanners. Both come
405
+ // from the parsed YAML — `name` is the same `config.name` that `RuntimeHealthStatus.runtimeName`
406
+ // is built from, so a runtime whose config never parsed falls back to its id rather than to a
407
+ // guess, and its scanner list stays empty rather than inventing rows.
408
+ const telemetryStrategies = new Map();
409
+ const noteTelemetryRuntime = (runtimeId, address) => {
410
+ const existing = telemetryStrategies.get(runtimeId);
411
+ telemetryStrategies.set(runtimeId, {
412
+ address,
413
+ name: existing?.name ?? runtimeId,
414
+ scanners: existing?.scanners ?? [],
415
+ });
416
+ };
417
+ const noteTelemetryRuntimeConfig = (runtimeId, config) => {
418
+ telemetryStrategies.set(runtimeId, {
419
+ address: config.strategy?.wallet ?? telemetryStrategies.get(runtimeId)?.address ?? "",
420
+ name: config.name,
421
+ scanners: config.scanners ?? [],
422
+ });
423
+ };
385
424
  // Dedicated logger for the runtime-api subtree — required because the
386
425
  // PluginLogger surface (info/warn/error) lacks `debug`, while the
387
426
  // Logger contract used by `runtime-api/` includes it.
388
427
  const apiLogger = createLogger("[senpi-core][api]");
428
+ /**
429
+ * One gather per publish tick. Health and system state are read together because the two payloads
430
+ * need both: the topology reads health and `runtimeName`, the stats read the per-scanner rows,
431
+ * and `restartCount` / `degradedReason` live only on the health rows while every other scanner
432
+ * field lives only on the system-state rows — so they are joined here on `(runtimeId, scannerId)`.
433
+ */
434
+ const collectTelemetryFacts = async () => {
435
+ const [statuses, states] = await Promise.all([
436
+ collectRuntimeHealthStatuses(runtimeHandles, api.logger),
437
+ collectRuntimeSystemStates(runtimeHandles, api.logger, baseStateDir),
438
+ ]);
439
+ const healthByRuntime = new Map();
440
+ for (const status of statuses) {
441
+ if (status.runtimeId !== undefined)
442
+ healthByRuntime.set(status.runtimeId, status);
443
+ }
444
+ const scannerState = [];
445
+ // Registration facts per (runtimeId, scannerId), so the topology can report a scanner's live
446
+ // schedule rather than only what its YAML declared.
447
+ const registrations = new Map();
448
+ for (const state of states) {
449
+ const runtimeId = state.runtimeId;
450
+ const healthRows = runtimeId === undefined
451
+ ? []
452
+ : healthByRuntime.get(runtimeId)?.components.scanners.scanners ?? [];
453
+ for (const row of state.components.scanners.state.scanners) {
454
+ const supervision = healthRows.find((r) => r.scannerId === row.scannerId);
455
+ const snapshot = {
456
+ address: row.address,
457
+ scannerId: row.scannerId,
458
+ health: row.health,
459
+ degradedReason: supervision?.degradedReason ?? null,
460
+ enabled: row.enabled,
461
+ inFlightSince: row.inFlightSince,
462
+ lastRunStartedAt: row.lastRunStartedAt,
463
+ lastRunFinishedAt: row.lastRunFinishedAt,
464
+ lastRunStatus: row.lastRunStatus,
465
+ lastRunReason: row.lastRunReason,
466
+ nextRunAt: row.nextRunAt,
467
+ lastAliveAt: row.lastAliveAt,
468
+ // No supervision row means the restart count is unknown, not zero. A zero here would
469
+ // read as "this scanner has never restarted" on a scanner nobody is supervising.
470
+ restartCount: supervision?.restartCount ?? null,
471
+ };
472
+ scannerState.push(snapshot);
473
+ if (runtimeId !== undefined) {
474
+ registrations.set(`${runtimeId}::${row.scannerId}`, {
475
+ ...snapshot,
476
+ scheduleMode: row.scheduleMode,
477
+ configVersion: row.configVersion,
478
+ intervalSeconds: row.intervalSeconds,
479
+ });
480
+ }
481
+ }
482
+ }
483
+ const strategies = [];
484
+ for (const [runtimeId, entry] of telemetryStrategies) {
485
+ const health = healthByRuntime.get(runtimeId);
486
+ const scanners = entry.scanners.map((declared) => {
487
+ const live = registrations.get(`${runtimeId}::${declared.name}`);
488
+ return live === undefined
489
+ ? { entry: declared }
490
+ : {
491
+ entry: declared,
492
+ registration: {
493
+ enabled: live.enabled,
494
+ scheduleMode: live.scheduleMode,
495
+ // The engine's own cadence, which is what the scanner actually polls on — a
496
+ // recipe may omit it and take the scanner definition's default. An external
497
+ // scanner registers zero because it is POSTed to rather than polled, and only
498
+ // the recipe knows the cadence its scaffold was told to tick at.
499
+ intervalSeconds: live.intervalSeconds > 0 ? live.intervalSeconds : declared.interval_seconds ?? null,
500
+ configVersion: live.configVersion,
501
+ },
502
+ };
503
+ });
504
+ strategies.push({
505
+ runtimeId,
506
+ address: entry.address,
507
+ // `runtimeName` is built from the same `config.name`; the indexed value is used because it
508
+ // is available for a stopped runtime too, where there is no health status to read.
509
+ name: health?.runtimeName ?? entry.name,
510
+ scanners,
511
+ });
512
+ }
513
+ return {
514
+ strategies,
515
+ runningRuntimeIds: runtimeHandles,
516
+ unwiredRuntimeIds: unwiredRuntimes,
517
+ readHealth: (runtimeId) => healthByRuntime.get(runtimeId),
518
+ scannerState,
519
+ };
520
+ };
521
+ const liveTelemetry = createLiveTelemetry({
522
+ collectFacts: collectTelemetryFacts,
523
+ tickTelemetry: config.tickTelemetry,
524
+ });
525
+ /**
526
+ * Wrapped gateway methods capture the broadcast handle on the FIRST request to ANY of them —
527
+ * not only on subscribe. `ctx.context` is the only route to the websocket push layer and it
528
+ * exists only inside a handler, so capturing it in the subscribe method alone would mean a box
529
+ * whose client never subscribes can never publish anything, silently.
530
+ *
531
+ * The wrapper covers only what is registered through it. `senpi.deploy.start`,
532
+ * `senpi.deploy.status` and `senpi.update` are registered straight onto the plugin api by their
533
+ * own registrars and never arm the handle. That is not a gap in practice: one wrapped call arms
534
+ * it for the life of the process, and the websocket client admits `senpi.telemetry.subscribe`,
535
+ * which is wrapped.
536
+ */
537
+ const registerGatewayMethod = (name, handler, opts) => {
538
+ api.registerGatewayMethod(name, (ctx) => {
539
+ liveTelemetry.captureBroadcast(ctx, name);
540
+ return handler(ctx);
541
+ }, opts);
542
+ };
389
543
  // A gateway seam failed to wire/launch a runtime's external scanner stack: the runtime is up but
390
544
  // runs blind on internal scanners only. Two records, by design: a first-class `runtime.scanners_unwired`
391
545
  // event (the queryable/alertable fact, by phase) and the migrated exception-form `logger.error`
@@ -559,26 +713,14 @@ const senpiRuntimePlugin = {
559
713
  // /signals will not be mounted and no signals will be routed.
560
714
  if (sharedIntakeContext) {
561
715
  const ctx = sharedIntakeContext;
562
- routeAcceptedSignalToRuntime = (signal, _correlationId, scannerId) => {
716
+ routeAcceptedSignalToRuntime = (signal, correlationId, scannerId) => {
563
717
  const resolved = ctx.registry.resolve(scannerId);
564
718
  if (!resolved)
565
719
  return;
566
720
  const h = runtimeHandles.get(resolved.runtimeId);
567
721
  if (!h)
568
722
  return;
569
- const request = {
570
- scanner: resolved.scannerName,
571
- signal_id: signal.signal_id, // greppable handle across intake→ingest (obs only)
572
- asset: signal.asset,
573
- direction: signal.direction,
574
- ...(signal.signal_type !== undefined && { signal_type: signal.signal_type }),
575
- data: signal.data,
576
- // Per-signal sizing rides at the top level; the open-position action
577
- // reads request.marginPct / request.leverage directly. Without this a
578
- // supervised signal's sizing is lost ("no margin source resolved").
579
- ...(signal.marginPct !== undefined && { marginPct: signal.marginPct }),
580
- ...(signal.leverage !== undefined && { leverage: signal.leverage }),
581
- };
723
+ const request = toIngestRequest(signal, correlationId, resolved.scannerName);
582
724
  // Trace the sizing-relevant fields the narrowed request carries, so a
583
725
  // field dropped at this seam is visible instead of vanishing silently.
584
726
  apiLogger.debug(`[intake] ingest req signal_id=${signal.signal_id} scanner=${resolved.scannerName} asset=${signal.asset} direction=${signal.direction} marginPct=${signal.marginPct} leverage=${signal.leverage}`);
@@ -598,15 +740,18 @@ const senpiRuntimePlugin = {
598
740
  // The single server-level onError closure — the `/errors` counterpart of
599
741
  // routeAcceptedSignalToRuntime. It records a scaffold-reported tick failure into
600
742
  // the owning runtime's scanner telemetry so a scanner that fails every tick stops
601
- // rendering "healthy".
743
+ // rendering "healthy". Filter: only tick-level failure types count — a
744
+ // `candidate_rejected` body is a per-candidate reject on an otherwise-OK tick, not
745
+ // a failed run.
602
746
  //
603
- // This is the SHARED implementation (scaffold-error-router.ts), not a copy of
604
- // buildIntakeMount's onScaffoldError. It used to be a copy, and the copy had its
605
- // own hardcoded three-type filter: when `mcp_error` was added to the tick-failure
606
- // list it reached the tested seam and never reached this route, so the
607
- // wedged-scanner signal was green in CI and inert in production. There is now one
608
- // filter, and scaffold-error-router.test.ts drives THIS closure.
747
+ // This is the SHARED implementation (scaffold-error-router.ts), so this route and
748
+ // buildIntakeMount's onScaffoldError apply one filter, and scaffold-error-router.test.ts
749
+ // drives THIS closure.
609
750
  routeScaffoldErrorToRuntime = buildScaffoldErrorRouter(ctx, runtimeHandles);
751
+ // The per-tick level for a supervised external scanner. Both intake routes call it, so a
752
+ // quiet tick and a failed tick are equally visible; it needs no runtime handle because it
753
+ // publishes a level rather than recording a run.
754
+ routeScaffoldTickToTelemetry = buildScaffoldTickRouter(ctx);
610
755
  }
611
756
  // One bounded read: which registered wallets the platform says are dead, so a restart does
612
757
  // not re-scaffold runtimes on closed wallets (fail-open — see runtime/boot-skip.ts).
@@ -658,6 +803,9 @@ const senpiRuntimePlugin = {
658
803
  continue;
659
804
  const pathOrContent = entry.runtimeYamlPath ?? entry.runtimeYamlContent;
660
805
  const entryStateDir = buildRuntimeStateDir(baseStateDir, entry.id, entry.wallet ?? "");
806
+ // Indexed before the boot attempt: a runtime whose start throws is installed but stopped,
807
+ // and the telemetry topology has to be able to say so.
808
+ noteTelemetryRuntime(entry.id, entry.wallet ?? "");
661
809
  const handle = await run(pathOrContent, {
662
810
  fromPath: hasPath,
663
811
  stateDir: entryStateDir,
@@ -681,6 +829,7 @@ const senpiRuntimePlugin = {
681
829
  if (sharedIntakeContext) {
682
830
  try {
683
831
  const runtimeConfig = await loadRuntimeConfig(pathOrContent, { fromPath: hasPath });
832
+ noteTelemetryRuntimeConfig(entry.id, runtimeConfig);
684
833
  // Content-installed runtimes carry their recipe dir in the registry
685
834
  // entry (runtimeYamlDir) so relative scanner paths still resolve
686
835
  // after a restart; path-installed ones derive it from the path.
@@ -732,6 +881,7 @@ const senpiRuntimePlugin = {
732
881
  // event) so listRuntimes/health don't render a bare "running".
733
882
  try {
734
883
  const runtimeConfig = await loadRuntimeConfig(pathOrContent, { fromPath: hasPath });
884
+ noteTelemetryRuntimeConfig(entry.id, runtimeConfig);
735
885
  if (selectScanners(runtimeConfig).length > 0) {
736
886
  unwiredRuntimes.set(entry.id, "no_intake");
737
887
  handle.attachSupervisionSource(() => ({
@@ -790,6 +940,7 @@ const senpiRuntimePlugin = {
790
940
  intakeContext: sharedIntakeContext,
791
941
  onAccepted: routeAcceptedSignalToRuntime,
792
942
  onError: routeScaffoldErrorToRuntime,
943
+ onTick: routeScaffoldTickToTelemetry,
793
944
  });
794
945
  apiHandle.ready();
795
946
  }
@@ -833,6 +984,12 @@ const senpiRuntimePlugin = {
833
984
  }
834
985
  }
835
986
  }
987
+ // Publish ticks start only once the runtimes are up, so the first frames describe a real
988
+ // box. Gated by the same switch the skills coordinator uses: unit tests boot `register()`
989
+ // and must not acquire background timers. The subscribe method still answers without them.
990
+ if (process.env.SENPI_RUNTIME_API_DISABLED !== "true") {
991
+ liveTelemetry.start();
992
+ }
836
993
  // Last, once everything above is up: the reaper's post-boot pass and hourly sweep. Its
837
994
  // not-active escalation is also off until here, so no delete can land while the boot loop is
838
995
  // still wiring runtimes and launchers. Nothing here may fail start() — a throw from inside the boot loop takes every runtime down, which
@@ -850,6 +1007,11 @@ const senpiRuntimePlugin = {
850
1007
  }
851
1008
  },
852
1009
  async stop() {
1010
+ // Live telemetry goes first: it owns a timer and an in-memory buffer, nothing downstream
1011
+ // depends on it, and stopping it here guarantees no publish tick reads a half-torn-down
1012
+ // runtime handle. Records emitted during the teardown below land in a buffer nobody drains,
1013
+ // which is the right trade for a process on its way out.
1014
+ liveTelemetry.stop();
853
1015
  // Stop the reaper first so no pass starts deleting while the runtimes are torn down below,
854
1016
  // and wait (bounded) for a pass already under way, so its delete cannot stop a handle the
855
1017
  // teardown below is stopping too.
@@ -1189,6 +1351,10 @@ const senpiRuntimePlugin = {
1189
1351
  onStrategyNotActive: (address) => reaper.checkWallet(address),
1190
1352
  });
1191
1353
  runtimeHandles.set(resolvedId, handle);
1354
+ if (parsedConfig)
1355
+ noteTelemetryRuntimeConfig(resolvedId, parsedConfig);
1356
+ else
1357
+ noteTelemetryRuntime(resolvedId, wallet);
1192
1358
  // Registry create fact — distinct from the runtime.started boot transition (which also fires
1193
1359
  // on a transient restart). Emitted inside emitEvent's guard so telemetry never fails the install.
1194
1360
  emitEvent(apiLogger, () => buildRuntimeCreatedEvent({ runtimeId: resolvedId, address: wallet, recipeName: runtimeName }));
@@ -1301,7 +1467,7 @@ const senpiRuntimePlugin = {
1301
1467
  }
1302
1468
  }
1303
1469
  /** Gateway respond accepts (ok, payload?, error?) with error as { code, message }; plugin SDK types may omit the third arg. */
1304
- api.registerGatewayMethod("senpi.installRuntime", async (ctx) => {
1470
+ registerGatewayMethod("senpi.installRuntime", async (ctx) => {
1305
1471
  const respond = ctx.respond;
1306
1472
  const result = await installRuntimeCore(ctx.params);
1307
1473
  if (result.ok)
@@ -1309,7 +1475,7 @@ const senpiRuntimePlugin = {
1309
1475
  else
1310
1476
  respond(false, undefined, result.error);
1311
1477
  }, { scope: "operator.write" });
1312
- api.registerGatewayMethod("senpi.listRuntimes", async (ctx) => {
1478
+ registerGatewayMethod("senpi.listRuntimes", async (ctx) => {
1313
1479
  const respond = ctx.respond;
1314
1480
  const { readRegistry } = await import("./runtime/strategy-registry.js");
1315
1481
  const registry = await readRegistry(baseStateDir);
@@ -1452,6 +1618,9 @@ const senpiRuntimePlugin = {
1452
1618
  // "running — NO ENTRY SCANNERS" forever. The map is otherwise only cleared on a
1453
1619
  // successful wire/install, which a scanner-less reinstall never reaches.
1454
1620
  unwiredRuntimes.delete(targetId);
1621
+ // Same reuse hazard: a deleted id left in the index would keep publishing a strategy that no
1622
+ // longer exists, and would hand a reinstall the old name and scanner list.
1623
+ telemetryStrategies.delete(targetId);
1455
1624
  bootSkippedRuntimes.delete(targetId);
1456
1625
  // Archive state directory before removing from registry
1457
1626
  if (deletedEntry) {
@@ -1482,7 +1651,7 @@ const senpiRuntimePlugin = {
1482
1651
  }));
1483
1652
  return { ok: true, payload: { id: targetId } };
1484
1653
  }
1485
- api.registerGatewayMethod("senpi.deleteRuntime", async (ctx) => {
1654
+ registerGatewayMethod("senpi.deleteRuntime", async (ctx) => {
1486
1655
  const respond = ctx.respond;
1487
1656
  const result = await deleteRuntimeCore(ctx.params);
1488
1657
  if (result.ok)
@@ -1589,6 +1758,17 @@ const senpiRuntimePlugin = {
1589
1758
  return checkProofCoversRecipe(packageDir, verifyProof(packageDir), appliedRecipe, (f) => readFileSync(f, "utf8"));
1590
1759
  },
1591
1760
  applyToRuntime: makeApplyToRuntime(runtimeHandles, async (runtimeId, next, recipe) => {
1761
+ // Re-index the topology facts FIRST, ahead of every early return and every failure path
1762
+ // below. `makeApplyToRuntime` only reaches this callback once the in-process swap has
1763
+ // succeeded, so `next` is already the recipe this box runs: its display name and its
1764
+ // declared scanner list are the ones the topology payload must name. Leaving the index on
1765
+ // the previous config would publish strategy rows the new scanner stats cannot join to.
1766
+ //
1767
+ // Ahead of the early return because an apply on a box with no intake still swapped the
1768
+ // recipe, and ahead of the launcher because a launcher that fails is a runtime running the
1769
+ // new recipe with no external scanners — the degraded state a dashboard most needs to show,
1770
+ // and one it can only render if the declared scanners are the new ones.
1771
+ noteTelemetryRuntimeConfig(runtimeId, next);
1592
1772
  // Restart this runtime's external scanner children against the new package.
1593
1773
  //
1594
1774
  // The rebuild swaps in-process components; these are separate OS processes, spawned once
@@ -1736,7 +1916,7 @@ const senpiRuntimePlugin = {
1736
1916
  * producers — slated for removal in Task 5 once the migration counter
1737
1917
  * decays to zero (see `runtime-phase-2-api-plan.md` Section 8).
1738
1918
  */
1739
- api.registerGatewayMethod("senpi.ingestExternalScannerData", async (ctx) => {
1919
+ registerGatewayMethod("senpi.ingestExternalScannerData", async (ctx) => {
1740
1920
  const respond = ctx.respond;
1741
1921
  // TODO(s11): wire actual telemetry counter for
1742
1922
  // runtime.deprecated_gateway_ingest_call (event name, properties:
@@ -1755,6 +1935,7 @@ const senpiRuntimePlugin = {
1755
1935
  ...(request.direction !== undefined && { direction: request.direction }),
1756
1936
  ...(request.score !== undefined && { score: request.score }),
1757
1937
  ...(request.signal_type !== undefined && { signal_type: request.signal_type }),
1938
+ ...(request.tick_id !== undefined && { tick_id: request.tick_id }),
1758
1939
  ...(request.data !== undefined && { data: request.data }),
1759
1940
  ...(request.signals !== undefined && { signals: request.signals }),
1760
1941
  };
@@ -1812,7 +1993,7 @@ const senpiRuntimePlugin = {
1812
1993
  });
1813
1994
  }
1814
1995
  }, { scope: "operator.write" });
1815
- api.registerGatewayMethod("senpi.listDslPositions", async (ctx) => {
1996
+ registerGatewayMethod("senpi.listDslPositions", async (ctx) => {
1816
1997
  const respond = ctx.respond;
1817
1998
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
1818
1999
  const address = typeof ctx.params?.address === "string" ? ctx.params.address : undefined;
@@ -1837,7 +2018,7 @@ const senpiRuntimePlugin = {
1837
2018
  }
1838
2019
  respond(true, { positions });
1839
2020
  }, { scope: "operator.read" });
1840
- api.registerGatewayMethod("senpi.getDslPositionState", async (ctx) => {
2021
+ registerGatewayMethod("senpi.getDslPositionState", async (ctx) => {
1841
2022
  const respond = ctx.respond;
1842
2023
  const asset = typeof ctx.params?.asset === "string" ? ctx.params.asset : undefined;
1843
2024
  if (!asset) {
@@ -1878,7 +2059,7 @@ const senpiRuntimePlugin = {
1878
2059
  }
1879
2060
  respond(true, { positions: matches });
1880
2061
  }, { scope: "operator.read" });
1881
- api.registerGatewayMethod("senpi.listDslArchives", async (ctx) => {
2062
+ registerGatewayMethod("senpi.listDslArchives", async (ctx) => {
1882
2063
  const respond = ctx.respond;
1883
2064
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
1884
2065
  const address = typeof ctx.params?.address === "string" ? ctx.params.address : undefined;
@@ -1904,7 +2085,7 @@ const senpiRuntimePlugin = {
1904
2085
  closes = closes.slice(0, limit);
1905
2086
  respond(true, { closes });
1906
2087
  }, { scope: "operator.read" });
1907
- api.registerGatewayMethod("senpi.getHealthStatus", async (ctx) => {
2088
+ registerGatewayMethod("senpi.getHealthStatus", async (ctx) => {
1908
2089
  const respond = ctx.respond;
1909
2090
  const updater = autoUpdateCoordinator.getSnapshot();
1910
2091
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
@@ -1932,7 +2113,7 @@ const senpiRuntimePlugin = {
1932
2113
  const statuses = await collectRuntimeHealthStatuses(runtimeHandles, api.logger);
1933
2114
  respond(true, { statuses, updater });
1934
2115
  }, { scope: "operator.read" });
1935
- api.registerGatewayMethod("senpi.getSystemState", async (ctx) => {
2116
+ registerGatewayMethod("senpi.getSystemState", async (ctx) => {
1936
2117
  const respond = ctx.respond;
1937
2118
  const updater = autoUpdateCoordinator.getSnapshot();
1938
2119
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
@@ -1960,6 +2141,36 @@ const senpiRuntimePlugin = {
1960
2141
  const states = await collectRuntimeSystemStates(runtimeHandles, api.logger, baseStateDir);
1961
2142
  respond(true, { states, updater });
1962
2143
  }, { scope: "operator.read" });
2144
+ /**
2145
+ * Live telemetry — the one method of the stream, called once per connect.
2146
+ *
2147
+ * It returns the current value of all three broadcast payloads plus the recent record window, so
2148
+ * a client joining a box that has been running for hours renders a full screen immediately
2149
+ * instead of waiting for the next thing to happen. `(afterEpoch, afterSeq)` resumes a client that
2150
+ * already holds part of this epoch's run — a cursor naming any other epoch is dropped, because a
2151
+ * seq from a previous run cannot address these records. `truncated` says when the records between
2152
+ * a cursor of this epoch and the oldest record still held are gone, which nothing in `recent`
2153
+ * alone can express.
2154
+ *
2155
+ * Read-only, like every other inspection method here. The scope that decides who RECEIVES the
2156
+ * broadcasts is a property of the client's own connection, not of this method.
2157
+ */
2158
+ registerGatewayMethod("senpi.telemetry.subscribe", async (ctx) => {
2159
+ const respond = ctx.respond;
2160
+ try {
2161
+ respond(true, await liveTelemetry.snapshot({
2162
+ afterEpoch: ctx.params?.afterEpoch,
2163
+ afterSeq: ctx.params?.afterSeq,
2164
+ }));
2165
+ }
2166
+ catch (err) {
2167
+ api.logger.warn("[senpi-runtime] telemetry subscribe error", { err });
2168
+ respond(false, undefined, {
2169
+ code: "INTERNAL_ERROR",
2170
+ message: `Failed to build the telemetry snapshot: ${formatGatewayError(err)}`,
2171
+ });
2172
+ }
2173
+ }, { scope: "operator.read" });
1963
2174
  /**
1964
2175
  * Force the auto-update poll to run now instead of waiting for its timer.
1965
2176
  *
@@ -2018,7 +2229,7 @@ const senpiRuntimePlugin = {
2018
2229
  * persisted action-store machinery (`src/actions/store/`) usable
2019
2230
  * from the operator-facing CLI again.
2020
2231
  */
2021
- api.registerGatewayMethod("senpi.listActions", async (ctx) => {
2232
+ registerGatewayMethod("senpi.listActions", async (ctx) => {
2022
2233
  const respond = ctx.respond;
2023
2234
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2024
2235
  const address = typeof ctx.params?.address === "string" ? ctx.params.address : undefined;
@@ -2048,7 +2259,7 @@ const senpiRuntimePlugin = {
2048
2259
  }
2049
2260
  respond(true, { actions: merged });
2050
2261
  }, { scope: "operator.read" });
2051
- api.registerGatewayMethod("senpi.getActionState", async (ctx) => {
2262
+ registerGatewayMethod("senpi.getActionState", async (ctx) => {
2052
2263
  const respond = ctx.respond;
2053
2264
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2054
2265
  const actionName = typeof ctx.params?.actionName === "string" ? ctx.params.actionName : undefined;
@@ -2085,7 +2296,7 @@ const senpiRuntimePlugin = {
2085
2296
  });
2086
2297
  }
2087
2298
  }, { scope: "operator.read" });
2088
- api.registerGatewayMethod("senpi.getActionHistory", async (ctx) => {
2299
+ registerGatewayMethod("senpi.getActionHistory", async (ctx) => {
2089
2300
  const respond = ctx.respond;
2090
2301
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2091
2302
  const actionName = typeof ctx.params?.actionName === "string" ? ctx.params.actionName : undefined;
@@ -2153,7 +2364,7 @@ const senpiRuntimePlugin = {
2153
2364
  // reachable on HTTP `/audit`. Passes through the caller-controlled subset
2154
2365
  // of the audit_query contract; resource scoping (wallet → strategyId) is
2155
2366
  // owned by handle.auditQuery, never the caller.
2156
- api.registerGatewayMethod("senpi.auditQuery", async (ctx) => {
2367
+ registerGatewayMethod("senpi.auditQuery", async (ctx) => {
2157
2368
  const respond = ctx.respond;
2158
2369
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2159
2370
  if (!runtimeId) {
@@ -2201,7 +2412,7 @@ const senpiRuntimePlugin = {
2201
2412
  // its state root private, so re-derive it from baseStateDir + the handle's
2202
2413
  // wallet (same helper the writer's stateDir was built with); the writer
2203
2414
  // nests entries under `<root>/<address>/risk-guard/audit.jsonl`.
2204
- api.registerGatewayMethod("senpi.getRiskAudit", async (ctx) => {
2415
+ registerGatewayMethod("senpi.getRiskAudit", async (ctx) => {
2205
2416
  const respond = ctx.respond;
2206
2417
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2207
2418
  if (!runtimeId) {
@@ -2244,7 +2455,7 @@ const senpiRuntimePlugin = {
2244
2455
  // Per-strategy local domain-event ring (on-disk JSONL) — the trade narrative, queryable locally.
2245
2456
  // Same state-root re-derivation as getRiskAudit: the Runtime keeps its root private, so rebuild it
2246
2457
  // from baseStateDir + the handle's wallet; the writer nests rows under `<root>/strategies/<address>/events/`.
2247
- api.registerGatewayMethod("senpi.getEvents", async (ctx) => {
2458
+ registerGatewayMethod("senpi.getEvents", async (ctx) => {
2248
2459
  const respond = ctx.respond;
2249
2460
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2250
2461
  if (!runtimeId) {
@@ -2301,12 +2512,12 @@ const senpiRuntimePlugin = {
2301
2512
  }
2302
2513
  }, { scope: "operator.read" });
2303
2514
  // ─── Skills manager — read + mutate ──────────────────────────────────────
2304
- api.registerGatewayMethod("senpi.skills.status", async (ctx) => {
2515
+ registerGatewayMethod("senpi.skills.status", async (ctx) => {
2305
2516
  const respond = ctx.respond;
2306
2517
  const snapshot = skillsCoordinator.getSnapshot();
2307
2518
  respond(true, { snapshot });
2308
2519
  }, { scope: "operator.read" });
2309
- api.registerGatewayMethod("senpi.skills.update", async (ctx) => {
2520
+ registerGatewayMethod("senpi.skills.update", async (ctx) => {
2310
2521
  const respond = ctx.respond;
2311
2522
  const name = typeof ctx.params?.name === "string" ? ctx.params.name.trim() : undefined;
2312
2523
  if (!name) {
@@ -2331,7 +2542,7 @@ const senpiRuntimePlugin = {
2331
2542
  });
2332
2543
  }
2333
2544
  }, { scope: "operator.write" });
2334
- api.registerGatewayMethod("senpi.skills.updateAll", async (ctx) => {
2545
+ registerGatewayMethod("senpi.skills.updateAll", async (ctx) => {
2335
2546
  const respond = ctx.respond;
2336
2547
  try {
2337
2548
  const results = await skillsCoordinator.updateAll();
@@ -2397,6 +2608,7 @@ function parseGatewayExternalScannerIngestParams(params) {
2397
2608
  direction: params?.direction,
2398
2609
  score: params?.score,
2399
2610
  signal_type: params?.signal_type,
2611
+ tick_id: params?.tick_id,
2400
2612
  data: params?.data,
2401
2613
  signals: params?.signals,
2402
2614
  },