@senpi/runtime 3.0.135 → 3.0.139

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 (293) 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 +26 -54
  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 +7 -38
  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 +15 -122
  10. package/dist/actions/close-position/close-position.action.js.map +1 -1
  11. package/dist/actions/decision-engine.d.ts.map +1 -1
  12. package/dist/actions/decision-engine.js +2 -15
  13. package/dist/actions/decision-engine.js.map +1 -1
  14. package/dist/actions/event-derivation.d.ts +2 -71
  15. package/dist/actions/event-derivation.d.ts.map +1 -1
  16. package/dist/actions/event-derivation.js +8 -251
  17. package/dist/actions/event-derivation.js.map +1 -1
  18. package/dist/actions/open-position/open-position.action.d.ts +2 -12
  19. package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
  20. package/dist/actions/open-position/open-position.action.js +41 -141
  21. package/dist/actions/open-position/open-position.action.js.map +1 -1
  22. package/dist/actions/position-tracker/position-tracker.action.d.ts +1 -2
  23. package/dist/actions/position-tracker/position-tracker.action.d.ts.map +1 -1
  24. package/dist/actions/position-tracker/position-tracker.action.js +17 -62
  25. package/dist/actions/position-tracker/position-tracker.action.js.map +1 -1
  26. package/dist/actions/store/types.d.ts +2 -9
  27. package/dist/actions/store/types.d.ts.map +1 -1
  28. package/dist/actions/store/types.js.map +1 -1
  29. package/dist/actions/types.d.ts +0 -13
  30. package/dist/actions/types.d.ts.map +1 -1
  31. package/dist/actions/types.js +0 -13
  32. package/dist/actions/types.js.map +1 -1
  33. package/dist/cli/guide-commands.d.ts.map +1 -1
  34. package/dist/cli/guide-commands.js +7 -4
  35. package/dist/cli/guide-commands.js.map +1 -1
  36. package/dist/constants/dsl/index.d.ts +0 -6
  37. package/dist/constants/dsl/index.d.ts.map +1 -1
  38. package/dist/constants/dsl/index.js +0 -6
  39. package/dist/constants/dsl/index.js.map +1 -1
  40. package/dist/constants/telemetry.d.ts +15 -0
  41. package/dist/constants/telemetry.d.ts.map +1 -0
  42. package/dist/constants/telemetry.js +28 -0
  43. package/dist/constants/telemetry.js.map +1 -0
  44. package/dist/dsl/engine-client/event-mapping.d.ts.map +1 -1
  45. package/dist/dsl/engine-client/event-mapping.js +0 -14
  46. package/dist/dsl/engine-client/event-mapping.js.map +1 -1
  47. package/dist/dsl/events/build-closed-event.d.ts +1 -16
  48. package/dist/dsl/events/build-closed-event.d.ts.map +1 -1
  49. package/dist/dsl/events/build-closed-event.js +0 -6
  50. package/dist/dsl/events/build-closed-event.js.map +1 -1
  51. package/dist/dsl/events/handlers.d.ts +0 -7
  52. package/dist/dsl/events/handlers.d.ts.map +1 -1
  53. package/dist/dsl/events/handlers.js +7 -37
  54. package/dist/dsl/events/handlers.js.map +1 -1
  55. package/dist/dsl/monitor/create-monitor.d.ts +2 -6
  56. package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
  57. package/dist/dsl/monitor/create-monitor.js +9 -82
  58. package/dist/dsl/monitor/create-monitor.js.map +1 -1
  59. package/dist/dsl/monitor/dsl-failure-events.d.ts +2 -45
  60. package/dist/dsl/monitor/dsl-failure-events.d.ts.map +1 -1
  61. package/dist/dsl/monitor/dsl-failure-events.js +3 -52
  62. package/dist/dsl/monitor/dsl-failure-events.js.map +1 -1
  63. package/dist/dsl/monitor/process-one-position.d.ts +0 -1
  64. package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
  65. package/dist/dsl/monitor/process-one-position.js +36 -159
  66. package/dist/dsl/monitor/process-one-position.js.map +1 -1
  67. package/dist/dsl/plugin/index.d.ts +2 -8
  68. package/dist/dsl/plugin/index.d.ts.map +1 -1
  69. package/dist/dsl/plugin/index.js +8 -20
  70. package/dist/dsl/plugin/index.js.map +1 -1
  71. package/dist/health/types.d.ts +0 -13
  72. package/dist/health/types.d.ts.map +1 -1
  73. package/dist/health/types.js +0 -12
  74. package/dist/health/types.js.map +1 -1
  75. package/dist/index.d.ts +8 -27
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +105 -272
  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 +3 -4
  81. package/dist/runtime/action-registry.js.map +1 -1
  82. package/dist/runtime/domain-events.d.ts.map +1 -1
  83. package/dist/runtime/domain-events.js +0 -7
  84. package/dist/runtime/domain-events.js.map +1 -1
  85. package/dist/runtime/events-telemetry.d.ts +96 -0
  86. package/dist/runtime/events-telemetry.d.ts.map +1 -0
  87. package/dist/runtime/events-telemetry.js +232 -0
  88. package/dist/runtime/events-telemetry.js.map +1 -0
  89. package/dist/runtime/lifecycle-events.d.ts +3 -15
  90. package/dist/runtime/lifecycle-events.d.ts.map +1 -1
  91. package/dist/runtime/lifecycle-events.js +3 -17
  92. package/dist/runtime/lifecycle-events.js.map +1 -1
  93. package/dist/runtime/lifecycle-reaper.d.ts +6 -4
  94. package/dist/runtime/lifecycle-reaper.d.ts.map +1 -1
  95. package/dist/runtime/lifecycle-reaper.js +6 -4
  96. package/dist/runtime/lifecycle-reaper.js.map +1 -1
  97. package/dist/runtime/reconcile-run.d.ts +3 -13
  98. package/dist/runtime/reconcile-run.d.ts.map +1 -1
  99. package/dist/runtime/reconcile-run.js +3 -4
  100. package/dist/runtime/reconcile-run.js.map +1 -1
  101. package/dist/runtime/reconcile-state.d.ts +0 -8
  102. package/dist/runtime/reconcile-state.d.ts.map +1 -1
  103. package/dist/runtime/reconcile-state.js +1 -4
  104. package/dist/runtime/reconcile-state.js.map +1 -1
  105. package/dist/runtime/risk-guard.d.ts +20 -1
  106. package/dist/runtime/risk-guard.d.ts.map +1 -1
  107. package/dist/runtime/risk-guard.js +47 -6
  108. package/dist/runtime/risk-guard.js.map +1 -1
  109. package/dist/runtime/run.d.ts +7 -1
  110. package/dist/runtime/run.d.ts.map +1 -1
  111. package/dist/runtime/run.js +7 -0
  112. package/dist/runtime/run.js.map +1 -1
  113. package/dist/runtime/runtime-schema.js +3 -3
  114. package/dist/runtime/runtime-schema.js.map +1 -1
  115. package/dist/runtime/runtime.d.ts +22 -2
  116. package/dist/runtime/runtime.d.ts.map +1 -1
  117. package/dist/runtime/runtime.js +74 -39
  118. package/dist/runtime/runtime.js.map +1 -1
  119. package/dist/runtime/strategies-store.d.ts +26 -0
  120. package/dist/runtime/strategies-store.d.ts.map +1 -0
  121. package/dist/runtime/strategies-store.js +47 -0
  122. package/dist/runtime/strategies-store.js.map +1 -0
  123. package/dist/runtime/strategies-telemetry.d.ts +91 -0
  124. package/dist/runtime/strategies-telemetry.d.ts.map +1 -0
  125. package/dist/runtime/strategies-telemetry.js +128 -0
  126. package/dist/runtime/strategies-telemetry.js.map +1 -0
  127. package/dist/runtime/strategy-lifecycle.d.ts +108 -2
  128. package/dist/runtime/strategy-lifecycle.d.ts.map +1 -1
  129. package/dist/runtime/strategy-lifecycle.js +325 -4
  130. package/dist/runtime/strategy-lifecycle.js.map +1 -1
  131. package/dist/runtime/supervisor/long-running-supervisor.d.ts +0 -13
  132. package/dist/runtime/supervisor/long-running-supervisor.d.ts.map +1 -1
  133. package/dist/runtime/supervisor/long-running-supervisor.js +0 -1
  134. package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
  135. package/dist/runtime/validate-dsl-position-tracking.d.ts.map +1 -1
  136. package/dist/runtime/validate-dsl-position-tracking.js +1 -2
  137. package/dist/runtime/validate-dsl-position-tracking.js.map +1 -1
  138. package/dist/runtime-api/handlers/ingest-signals.d.ts.map +1 -1
  139. package/dist/runtime-api/handlers/ingest-signals.js +0 -1
  140. package/dist/runtime-api/handlers/ingest-signals.js.map +1 -1
  141. package/dist/runtime-api/handlers/types.d.ts +0 -5
  142. package/dist/runtime-api/handlers/types.d.ts.map +1 -1
  143. package/dist/runtime-api/intake/intake-events.d.ts +1 -1
  144. package/dist/runtime-api/intake/intake-events.d.ts.map +1 -1
  145. package/dist/runtime-api/intake/intake-events.js +3 -9
  146. package/dist/runtime-api/intake/intake-events.js.map +1 -1
  147. package/dist/runtime-api/intake/intake-mount.d.ts +2 -0
  148. package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
  149. package/dist/runtime-api/intake/intake-mount.js +25 -7
  150. package/dist/runtime-api/intake/intake-mount.js.map +1 -1
  151. package/dist/runtime-api/intake/process-error-post.d.ts +1 -6
  152. package/dist/runtime-api/intake/process-error-post.d.ts.map +1 -1
  153. package/dist/runtime-api/intake/process-error-post.js +1 -5
  154. package/dist/runtime-api/intake/process-error-post.js.map +1 -1
  155. package/dist/runtime-api/intake/process-signal-post.d.ts +1 -7
  156. package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
  157. package/dist/runtime-api/intake/process-signal-post.js +0 -5
  158. package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
  159. package/dist/runtime-api/intake/scaffold-error-router.js.map +1 -1
  160. package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
  161. package/dist/runtime-api/intake/scanner-launcher.js +0 -15
  162. package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
  163. package/dist/runtime-api/intake/types.d.ts +0 -55
  164. package/dist/runtime-api/intake/types.d.ts.map +1 -1
  165. package/dist/runtime-api/routes/signals.d.ts +0 -2
  166. package/dist/runtime-api/routes/signals.d.ts.map +1 -1
  167. package/dist/runtime-api/routes/signals.js +11 -5
  168. package/dist/runtime-api/routes/signals.js.map +1 -1
  169. package/dist/runtime-api/routes/signals.schema.d.ts +0 -43
  170. package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
  171. package/dist/runtime-api/routes/signals.schema.js +0 -43
  172. package/dist/runtime-api/routes/signals.schema.js.map +1 -1
  173. package/dist/runtime-api/server.d.ts +1 -7
  174. package/dist/runtime-api/server.d.ts.map +1 -1
  175. package/dist/runtime-api/server.js +0 -1
  176. package/dist/runtime-api/server.js.map +1 -1
  177. package/dist/scanners/engine/engine.d.ts +1 -22
  178. package/dist/scanners/engine/engine.d.ts.map +1 -1
  179. package/dist/scanners/engine/engine.js +11 -78
  180. package/dist/scanners/engine/engine.js.map +1 -1
  181. package/dist/scanners/engine/lifecycle.d.ts +1 -7
  182. package/dist/scanners/engine/lifecycle.d.ts.map +1 -1
  183. package/dist/scanners/events.d.ts +0 -4
  184. package/dist/scanners/events.d.ts.map +1 -1
  185. package/dist/scanners/events.js +0 -2
  186. package/dist/scanners/events.js.map +1 -1
  187. package/dist/scanners/external/scaffold/delivery.py +25 -194
  188. package/dist/scanners/external/scaffold/scaffold.py +31 -172
  189. package/dist/scanners/external/scaffold/senpi_mcp.py +8 -150
  190. package/dist/scanners/external-scanner-receiver.d.ts +0 -17
  191. package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
  192. package/dist/scanners/external-scanner-receiver.js +0 -8
  193. package/dist/scanners/external-scanner-receiver.js.map +1 -1
  194. package/dist/scanners/runtime-module.d.ts +0 -1
  195. package/dist/scanners/runtime-module.d.ts.map +1 -1
  196. package/dist/scanners/runtime-module.js +9 -66
  197. package/dist/scanners/runtime-module.js.map +1 -1
  198. package/dist/state/state-manager.d.ts +2 -0
  199. package/dist/state/state-manager.d.ts.map +1 -1
  200. package/dist/state/state-manager.js +8 -0
  201. package/dist/state/state-manager.js.map +1 -1
  202. package/dist/telemetry/attributes.d.ts +0 -20
  203. package/dist/telemetry/attributes.d.ts.map +1 -1
  204. package/dist/telemetry/attributes.js +0 -24
  205. package/dist/telemetry/attributes.js.map +1 -1
  206. package/dist/telemetry/event-builders.d.ts +6 -81
  207. package/dist/telemetry/event-builders.d.ts.map +1 -1
  208. package/dist/telemetry/event-builders.js +17 -141
  209. package/dist/telemetry/event-builders.js.map +1 -1
  210. package/dist/telemetry/event-store.d.ts.map +1 -1
  211. package/dist/telemetry/event-store.js +2 -6
  212. package/dist/telemetry/event-store.js.map +1 -1
  213. package/dist/telemetry/metrics.d.ts +2 -14
  214. package/dist/telemetry/metrics.d.ts.map +1 -1
  215. package/dist/telemetry/metrics.js +4 -19
  216. package/dist/telemetry/metrics.js.map +1 -1
  217. package/dist/telemetry/spans/redact-pii.d.ts.map +1 -1
  218. package/dist/telemetry/spans/redact-pii.js +11 -16
  219. package/dist/telemetry/spans/redact-pii.js.map +1 -1
  220. package/dist/telemetry/tracing.d.ts +0 -9
  221. package/dist/telemetry/tracing.d.ts.map +1 -1
  222. package/dist/telemetry/tracing.js +0 -36
  223. package/dist/telemetry/tracing.js.map +1 -1
  224. package/dist/types/dsl/index.d.ts +0 -48
  225. package/dist/types/dsl/index.d.ts.map +1 -1
  226. package/dist/types/scanner.d.ts +0 -15
  227. package/dist/types/scanner.d.ts.map +1 -1
  228. package/dist/utils/event-catalog.d.ts +0 -202
  229. package/dist/utils/event-catalog.d.ts.map +1 -1
  230. package/dist/utils/event-catalog.js +0 -26
  231. package/dist/utils/event-catalog.js.map +1 -1
  232. package/dist/utils/logger.d.ts +1 -4
  233. package/dist/utils/logger.d.ts.map +1 -1
  234. package/dist/utils/logger.js.map +1 -1
  235. package/dist/validate/recipe-checks.d.ts.map +1 -1
  236. package/dist/validate/recipe-checks.js +2 -2
  237. package/dist/validate/recipe-checks.js.map +1 -1
  238. package/openclaw.plugin.json +1 -1
  239. package/package.json +1 -1
  240. package/dist/actions/skipped-signal-results.d.ts +0 -25
  241. package/dist/actions/skipped-signal-results.d.ts.map +0 -1
  242. package/dist/actions/skipped-signal-results.js +0 -71
  243. package/dist/actions/skipped-signal-results.js.map +0 -1
  244. package/dist/runtime-api/intake/ingest-request.d.ts +0 -17
  245. package/dist/runtime-api/intake/ingest-request.d.ts.map +0 -1
  246. package/dist/runtime-api/intake/ingest-request.js +0 -34
  247. package/dist/runtime-api/intake/ingest-request.js.map +0 -1
  248. package/dist/runtime-api/intake/scaffold-tick-router.d.ts +0 -39
  249. package/dist/runtime-api/intake/scaffold-tick-router.d.ts.map +0 -1
  250. package/dist/runtime-api/intake/scaffold-tick-router.js +0 -122
  251. package/dist/runtime-api/intake/scaffold-tick-router.js.map +0 -1
  252. package/dist/scanners/external/scaffold/tick_facts.py +0 -130
  253. package/dist/scanners/external/scaffold/ulid.py +0 -58
  254. package/dist/telemetry/dsl-positions.d.ts +0 -111
  255. package/dist/telemetry/dsl-positions.d.ts.map +0 -1
  256. package/dist/telemetry/dsl-positions.js +0 -118
  257. package/dist/telemetry/dsl-positions.js.map +0 -1
  258. package/dist/telemetry/live-telemetry.d.ts +0 -257
  259. package/dist/telemetry/live-telemetry.d.ts.map +0 -1
  260. package/dist/telemetry/live-telemetry.js +0 -959
  261. package/dist/telemetry/live-telemetry.js.map +0 -1
  262. package/dist/telemetry/scanner-stats.d.ts +0 -143
  263. package/dist/telemetry/scanner-stats.d.ts.map +0 -1
  264. package/dist/telemetry/scanner-stats.js +0 -426
  265. package/dist/telemetry/scanner-stats.js.map +0 -1
  266. package/dist/telemetry/scanner-tick-feed.d.ts +0 -87
  267. package/dist/telemetry/scanner-tick-feed.d.ts.map +0 -1
  268. package/dist/telemetry/scanner-tick-feed.js +0 -48
  269. package/dist/telemetry/scanner-tick-feed.js.map +0 -1
  270. package/dist/telemetry/strategy-topology.d.ts +0 -99
  271. package/dist/telemetry/strategy-topology.d.ts.map +0 -1
  272. package/dist/telemetry/strategy-topology.js +0 -93
  273. package/dist/telemetry/strategy-topology.js.map +0 -1
  274. package/dist/telemetry/telemetry-publisher.d.ts +0 -83
  275. package/dist/telemetry/telemetry-publisher.d.ts.map +0 -1
  276. package/dist/telemetry/telemetry-publisher.js +0 -256
  277. package/dist/telemetry/telemetry-publisher.js.map +0 -1
  278. package/dist/telemetry/tick-frame.d.ts +0 -212
  279. package/dist/telemetry/tick-frame.d.ts.map +0 -1
  280. package/dist/telemetry/tick-frame.js +0 -421
  281. package/dist/telemetry/tick-frame.js.map +0 -1
  282. package/dist/telemetry/timeline-buffer.d.ts +0 -95
  283. package/dist/telemetry/timeline-buffer.d.ts.map +0 -1
  284. package/dist/telemetry/timeline-buffer.js +0 -237
  285. package/dist/telemetry/timeline-buffer.js.map +0 -1
  286. package/dist/utils/telemetry-required-log.d.ts +0 -60
  287. package/dist/utils/telemetry-required-log.d.ts.map +0 -1
  288. package/dist/utils/telemetry-required-log.js +0 -105
  289. package/dist/utils/telemetry-required-log.js.map +0 -1
  290. package/dist/utils/ulid.d.ts +0 -9
  291. package/dist/utils/ulid.d.ts.map +0 -1
  292. package/dist/utils/ulid.js +0 -43
  293. package/dist/utils/ulid.js.map +0 -1
package/dist/index.js CHANGED
@@ -20,6 +20,9 @@ import { bootSkippedStatusLabel, partitionByWalletStatus, readBootWalletStatuses
20
20
  import { DeployJobController } from "./deploy/job.js";
21
21
  import { registerDeployGatewayMethods } from "./deploy/register.js";
22
22
  import { registerUpdateGatewayMethod } from "./runtime/update-register.js";
23
+ import { createStrategiesPublisher } from "./runtime/strategies-telemetry.js";
24
+ import { buildEventsReplay, createEventsPublisher, } from "./runtime/events-telemetry.js";
25
+ import { strategiesStore, strategyRecordFromConfig } from "./runtime/strategies-store.js";
23
26
  import { checkProofCoversRecipe, makeApplyToRuntime, makeWriteRegistry, runtimeChangeLock, } from "./runtime/update-apply-deps.js";
24
27
  import { INSTALL_DEADLINE_MS, WALLET_INSTALL_IN_FLIGHT_CODE } from "./deploy/deadlines.js";
25
28
  import { AuditQueryError } from "./runtime/audit-query.types.js";
@@ -44,7 +47,7 @@ import { resolvePackageName } from "./runtime/auto-update/resolve-package-name.j
44
47
  import { runOpenClawPluginsUpdate } from "./runtime/auto-update/run-update-command.js";
45
48
  import { registerSpanHooks } from "./telemetry/spans/index.js";
46
49
  import { registerSessionContextSeedHook } from "./prompt/session-context-seed-hook.js";
47
- import { initDefaultPiiRedactor } from "./telemetry/spans/redact-pii.js";
50
+ import { getDefaultPiiRedactor, initDefaultPiiRedactor } from "./telemetry/spans/redact-pii.js";
48
51
  import { initOtelSdk, shutdownOtelSdk } from "./telemetry/otel-sdk.js";
49
52
  import { getLogLevel, getLogLevelSource, parseLogLevel, setLogLevel, } from "./utils/logger.js";
50
53
  import { ExternalScannerIngestError, } from "./scanners/external-scanner-receiver.js";
@@ -64,11 +67,7 @@ import { createIntakeContext } from "./runtime-api/intake/intake-context.js";
64
67
  import { wireRuntime, intakeUrlFor, selectScanners } from "./runtime-api/intake/intake-mount.js";
65
68
  import { ingestDispatchMutex, ingestDispatchKey } from "./runtime-api/intake/ingest-mutex.js";
66
69
  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";
69
70
  import { loadRuntimeConfig } from "./runtime/load-runtime-config.js";
70
- import { createLiveTelemetry } from "./telemetry/live-telemetry.js";
71
- import { resolveTickTelemetry } from "./telemetry/tick-frame.js";
72
71
  /** Initialize all core features (DSL, risk, health) on the given event bus. */
73
72
  export function initCore(bus) {
74
73
  initDsl(bus);
@@ -227,16 +226,6 @@ const senpiRuntimePlugin = {
227
226
  ? "env SENPI_LOG_LEVEL"
228
227
  : "default";
229
228
  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.`);
240
229
  const senpiCliStore = readSenpiCliStore();
241
230
  const baseStateDir = resolveSenpiBaseStateDir({
242
231
  store: senpiCliStore,
@@ -369,8 +358,37 @@ const senpiRuntimePlugin = {
369
358
  }
370
359
  let routeAcceptedSignalToRuntime;
371
360
  let routeScaffoldErrorToRuntime;
372
- let routeScaffoldTickToTelemetry;
373
361
  const launchers = new Map();
362
+ // Captured from the `senpi.telemetry.subscribe` handler's gateway context. The plugin API
363
+ // offers no broadcast function outside a request handler. Used by the strategies and events loops.
364
+ let telemetryBroadcast;
365
+ const strategiesPublisher = createStrategiesPublisher({
366
+ store: strategiesStore.view(),
367
+ // Same join as the scanner system state: the launcher's supervision row for the scanner name,
368
+ // whose lastAliveAt is read from the liveness store under the scanner's current id.
369
+ lastAliveAt: (runtimeId, scannerName) => launchers
370
+ .get(runtimeId)
371
+ ?.describeSupervision()
372
+ .find((row) => row.scannerName === scannerName)?.lastAliveAt ?? null,
373
+ // In-memory read from the runtime's state manager, which outlives the DSL plugin across an
374
+ // in-place update. Empty when the committed recipe has no DSL.
375
+ dslStates: (runtimeId) => runtimeHandles.get(runtimeId)?.listActiveDslStates() ?? [],
376
+ getBroadcast: () => telemetryBroadcast,
377
+ onError: (err) => pluginLogger.warn("[senpi-runtime] strategies telemetry tick failed", {
378
+ err: err instanceof Error ? err.message : String(err),
379
+ }),
380
+ onDslError: (runtimeId, err) => pluginLogger.warn("[senpi-runtime] strategies telemetry dsl read failed; sending dsl []", {
381
+ runtimeId,
382
+ err: err instanceof Error ? err.message : String(err),
383
+ }),
384
+ });
385
+ const eventsPublisher = createEventsPublisher({
386
+ getBroadcast: () => telemetryBroadcast,
387
+ warn: (message, meta) => pluginLogger.warn(message, meta),
388
+ onError: (err) => pluginLogger.warn("[senpi-runtime] events telemetry tick failed", {
389
+ err: err instanceof Error ? err.message : String(err),
390
+ }),
391
+ });
374
392
  // Runtimes that are UP but whose external scanners never wired (mount/launch/install
375
393
  // failure), keyed runtimeId → failed phase. Populated at the four unwired sites,
376
394
  // cleared on a successful wire/install. senpi.listRuntimes reads it to render
@@ -397,168 +415,10 @@ const senpiRuntimePlugin = {
397
415
  * engines, one margin account: the exact state `[E_WALLET_HAS_RUNTIME]` exists to prevent.
398
416
  */
399
417
  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
- };
424
418
  // Dedicated logger for the runtime-api subtree — required because the
425
419
  // PluginLogger surface (info/warn/error) lacks `debug`, while the
426
420
  // Logger contract used by `runtime-api/` includes it.
427
421
  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
- // The DSL states, read straight off each runtime's plugin. Synchronous and in-memory — this is
514
- // the same store the `senpi.listDslPositions` RPC reads — so it costs the gather nothing, and a
515
- // recipe with no DSL exit block has no plugin and contributes no rows. A runtime whose read
516
- // throws is skipped rather than failing the whole gather, on the same rule health follows: a
517
- // strategy mid-teardown must not take the stream down.
518
- const dslPositions = [];
519
- for (const [runtimeId, handle] of runtimeHandles) {
520
- if (!handle.dsl)
521
- continue;
522
- try {
523
- for (const state of handle.dsl.listActiveDslStates()) {
524
- dslPositions.push({ runtimeId, state });
525
- }
526
- }
527
- catch (err) {
528
- api.logger.warn("[senpi-runtime] could not read DSL states for telemetry", { runtimeId, err });
529
- }
530
- }
531
- return {
532
- strategies,
533
- runningRuntimeIds: runtimeHandles,
534
- unwiredRuntimeIds: unwiredRuntimes,
535
- readHealth: (runtimeId) => healthByRuntime.get(runtimeId),
536
- scannerState,
537
- dslPositions,
538
- };
539
- };
540
- const liveTelemetry = createLiveTelemetry({
541
- collectFacts: collectTelemetryFacts,
542
- tickTelemetry: config.tickTelemetry,
543
- });
544
- /**
545
- * Wrapped gateway methods capture the broadcast handle on the FIRST request to ANY of them —
546
- * not only on subscribe. `ctx.context` is the only route to the websocket push layer and it
547
- * exists only inside a handler, so capturing it in the subscribe method alone would mean a box
548
- * whose client never subscribes can never publish anything, silently.
549
- *
550
- * The wrapper covers only what is registered through it. `senpi.deploy.start`,
551
- * `senpi.deploy.status` and `senpi.update` are registered straight onto the plugin api by their
552
- * own registrars and never arm the handle. That is not a gap in practice: one wrapped call arms
553
- * it for the life of the process, and the websocket client admits `senpi.telemetry.subscribe`,
554
- * which is wrapped.
555
- */
556
- const registerGatewayMethod = (name, handler, opts) => {
557
- api.registerGatewayMethod(name, (ctx) => {
558
- liveTelemetry.captureBroadcast(ctx, name);
559
- return handler(ctx);
560
- }, opts);
561
- };
562
422
  // A gateway seam failed to wire/launch a runtime's external scanner stack: the runtime is up but
563
423
  // runs blind on internal scanners only. Two records, by design: a first-class `runtime.scanners_unwired`
564
424
  // event (the queryable/alertable fact, by phase) and the migrated exception-form `logger.error`
@@ -666,7 +526,8 @@ const senpiRuntimePlugin = {
666
526
  // ── lifecycle reaper ─────────────────────────────────────────────────────────────────────────
667
527
  // A runtime exists iff its strategy is open. Every close path ends at the backend and none of
668
528
  // them tells this box, so the reaper asks: once after boot, hourly, and whenever the backend
669
- // rejects an order with STRATEGY_NOT_ACTIVE. Observe-only unless enforcement is on (see
529
+ // rejects an order with STRATEGY_NOT_ACTIVE. Enforcing by default since the 3.0.121 observe-only
530
+ // validation; SENPI_REAPER_ENFORCE=false returns a box to observe-only (see
670
531
  // runtime/lifecycle-reaper.ts for the safety rule). Constructed here so both `run()` call sites
671
532
  // (boot and install) can hand it their not-active rejections; it does no I/O until started.
672
533
  const reaperBootDelayOverride = resolveReaperBootDelayMs(process.env);
@@ -732,14 +593,26 @@ const senpiRuntimePlugin = {
732
593
  // /signals will not be mounted and no signals will be routed.
733
594
  if (sharedIntakeContext) {
734
595
  const ctx = sharedIntakeContext;
735
- routeAcceptedSignalToRuntime = (signal, correlationId, scannerId) => {
596
+ routeAcceptedSignalToRuntime = (signal, _correlationId, scannerId) => {
736
597
  const resolved = ctx.registry.resolve(scannerId);
737
598
  if (!resolved)
738
599
  return;
739
600
  const h = runtimeHandles.get(resolved.runtimeId);
740
601
  if (!h)
741
602
  return;
742
- const request = toIngestRequest(signal, correlationId, resolved.scannerName);
603
+ const request = {
604
+ scanner: resolved.scannerName,
605
+ signal_id: signal.signal_id, // greppable handle across intake→ingest (obs only)
606
+ asset: signal.asset,
607
+ direction: signal.direction,
608
+ ...(signal.signal_type !== undefined && { signal_type: signal.signal_type }),
609
+ data: signal.data,
610
+ // Per-signal sizing rides at the top level; the open-position action
611
+ // reads request.marginPct / request.leverage directly. Without this a
612
+ // supervised signal's sizing is lost ("no margin source resolved").
613
+ ...(signal.marginPct !== undefined && { marginPct: signal.marginPct }),
614
+ ...(signal.leverage !== undefined && { leverage: signal.leverage }),
615
+ };
743
616
  // Trace the sizing-relevant fields the narrowed request carries, so a
744
617
  // field dropped at this seam is visible instead of vanishing silently.
745
618
  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}`);
@@ -759,18 +632,15 @@ const senpiRuntimePlugin = {
759
632
  // The single server-level onError closure — the `/errors` counterpart of
760
633
  // routeAcceptedSignalToRuntime. It records a scaffold-reported tick failure into
761
634
  // the owning runtime's scanner telemetry so a scanner that fails every tick stops
762
- // rendering "healthy". Filter: only tick-level failure types count — a
763
- // `candidate_rejected` body is a per-candidate reject on an otherwise-OK tick, not
764
- // a failed run.
635
+ // rendering "healthy".
765
636
  //
766
- // This is the SHARED implementation (scaffold-error-router.ts), so this route and
767
- // buildIntakeMount's onScaffoldError apply one filter, and scaffold-error-router.test.ts
768
- // drives THIS closure.
637
+ // This is the SHARED implementation (scaffold-error-router.ts), not a copy of
638
+ // buildIntakeMount's onScaffoldError. It used to be a copy, and the copy had its
639
+ // own hardcoded three-type filter: when `mcp_error` was added to the tick-failure
640
+ // list it reached the tested seam and never reached this route, so the
641
+ // wedged-scanner signal was green in CI and inert in production. There is now one
642
+ // filter, and scaffold-error-router.test.ts drives THIS closure.
769
643
  routeScaffoldErrorToRuntime = buildScaffoldErrorRouter(ctx, runtimeHandles);
770
- // The per-tick level for a supervised external scanner. Both intake routes call it, so a
771
- // quiet tick and a failed tick are equally visible; it needs no runtime handle because it
772
- // publishes a level rather than recording a run.
773
- routeScaffoldTickToTelemetry = buildScaffoldTickRouter(ctx);
774
644
  }
775
645
  // One bounded read: which registered wallets the platform says are dead, so a restart does
776
646
  // not re-scaffold runtimes on closed wallets (fail-open — see runtime/boot-skip.ts).
@@ -822,9 +692,6 @@ const senpiRuntimePlugin = {
822
692
  continue;
823
693
  const pathOrContent = entry.runtimeYamlPath ?? entry.runtimeYamlContent;
824
694
  const entryStateDir = buildRuntimeStateDir(baseStateDir, entry.id, entry.wallet ?? "");
825
- // Indexed before the boot attempt: a runtime whose start throws is installed but stopped,
826
- // and the telemetry topology has to be able to say so.
827
- noteTelemetryRuntime(entry.id, entry.wallet ?? "");
828
695
  const handle = await run(pathOrContent, {
829
696
  fromPath: hasPath,
830
697
  stateDir: entryStateDir,
@@ -848,7 +715,6 @@ const senpiRuntimePlugin = {
848
715
  if (sharedIntakeContext) {
849
716
  try {
850
717
  const runtimeConfig = await loadRuntimeConfig(pathOrContent, { fromPath: hasPath });
851
- noteTelemetryRuntimeConfig(entry.id, runtimeConfig);
852
718
  // Content-installed runtimes carry their recipe dir in the registry
853
719
  // entry (runtimeYamlDir) so relative scanner paths still resolve
854
720
  // after a restart; path-installed ones derive it from the path.
@@ -900,7 +766,6 @@ const senpiRuntimePlugin = {
900
766
  // event) so listRuntimes/health don't render a bare "running".
901
767
  try {
902
768
  const runtimeConfig = await loadRuntimeConfig(pathOrContent, { fromPath: hasPath });
903
- noteTelemetryRuntimeConfig(entry.id, runtimeConfig);
904
769
  if (selectScanners(runtimeConfig).length > 0) {
905
770
  unwiredRuntimes.set(entry.id, "no_intake");
906
771
  handle.attachSupervisionSource(() => ({
@@ -925,6 +790,7 @@ const senpiRuntimePlugin = {
925
790
  if (h)
926
791
  await h.stop().catch(() => { });
927
792
  runtimeHandles.delete(started[i]);
793
+ strategiesStore.delete(started[i]);
928
794
  }
929
795
  throw err;
930
796
  }
@@ -959,7 +825,6 @@ const senpiRuntimePlugin = {
959
825
  intakeContext: sharedIntakeContext,
960
826
  onAccepted: routeAcceptedSignalToRuntime,
961
827
  onError: routeScaffoldErrorToRuntime,
962
- onTick: routeScaffoldTickToTelemetry,
963
828
  });
964
829
  apiHandle.ready();
965
830
  }
@@ -977,6 +842,7 @@ const senpiRuntimePlugin = {
977
842
  if (h)
978
843
  await h.stop().catch(() => { });
979
844
  runtimeHandles.delete(started[i]);
845
+ strategiesStore.delete(started[i]);
980
846
  }
981
847
  throw err;
982
848
  }
@@ -1003,12 +869,10 @@ const senpiRuntimePlugin = {
1003
869
  }
1004
870
  }
1005
871
  }
1006
- // Publish ticks start only once the runtimes are up, so the first frames describe a real
1007
- // box. Gated by the same switch the skills coordinator uses: unit tests boot `register()`
1008
- // and must not acquire background timers. The subscribe method still answers without them.
1009
- if (process.env.SENPI_RUNTIME_API_DISABLED !== "true") {
1010
- liveTelemetry.start();
1011
- }
872
+ // Ticks once now, with every booted runtime in the store, then every 30 s.
873
+ strategiesPublisher.start();
874
+ // Sends the events added since the previous send, every 1 s.
875
+ eventsPublisher.start();
1012
876
  // Last, once everything above is up: the reaper's post-boot pass and hourly sweep. Its
1013
877
  // not-active escalation is also off until here, so no delete can land while the boot loop is
1014
878
  // still wiring runtimes and launchers. Nothing here may fail start() — a throw from inside the boot loop takes every runtime down, which
@@ -1026,15 +890,12 @@ const senpiRuntimePlugin = {
1026
890
  }
1027
891
  },
1028
892
  async stop() {
1029
- // Live telemetry goes first: it owns a timer and an in-memory buffer, nothing downstream
1030
- // depends on it, and stopping it here guarantees no publish tick reads a half-torn-down
1031
- // runtime handle. Records emitted during the teardown below land in a buffer nobody drains,
1032
- // which is the right trade for a process on its way out.
1033
- liveTelemetry.stop();
1034
893
  // Stop the reaper first so no pass starts deleting while the runtimes are torn down below,
1035
894
  // and wait (bounded) for a pass already under way, so its delete cannot stop a handle the
1036
895
  // teardown below is stopping too.
1037
896
  await reaper.stop();
897
+ strategiesPublisher.stop();
898
+ eventsPublisher.stop();
1038
899
  // Drain the HTTP server FIRST so in-flight POST /signals requests
1039
900
  // finish before the underlying runtime they target is torn down.
1040
901
  // `apiHandle.close()` never throws.
@@ -1061,6 +922,7 @@ const senpiRuntimePlugin = {
1061
922
  await h.stop();
1062
923
  runtimeHandles.delete(ids[i]);
1063
924
  }
925
+ strategiesStore.clear();
1064
926
  pluginLogger.info("[senpi-runtime] All runtimes stopped");
1065
927
  // Final, process-global OTel SDK flush. Last step, after every runtime is
1066
928
  // stopped and all spans are emitted: we disabled HyperDX's own SIGTERM
@@ -1370,10 +1232,6 @@ const senpiRuntimePlugin = {
1370
1232
  onStrategyNotActive: (address) => reaper.checkWallet(address),
1371
1233
  });
1372
1234
  runtimeHandles.set(resolvedId, handle);
1373
- if (parsedConfig)
1374
- noteTelemetryRuntimeConfig(resolvedId, parsedConfig);
1375
- else
1376
- noteTelemetryRuntime(resolvedId, wallet);
1377
1235
  // Registry create fact — distinct from the runtime.started boot transition (which also fires
1378
1236
  // on a transient restart). Emitted inside emitEvent's guard so telemetry never fails the install.
1379
1237
  emitEvent(apiLogger, () => buildRuntimeCreatedEvent({ runtimeId: resolvedId, address: wallet, recipeName: runtimeName }));
@@ -1486,7 +1344,7 @@ const senpiRuntimePlugin = {
1486
1344
  }
1487
1345
  }
1488
1346
  /** Gateway respond accepts (ok, payload?, error?) with error as { code, message }; plugin SDK types may omit the third arg. */
1489
- registerGatewayMethod("senpi.installRuntime", async (ctx) => {
1347
+ api.registerGatewayMethod("senpi.installRuntime", async (ctx) => {
1490
1348
  const respond = ctx.respond;
1491
1349
  const result = await installRuntimeCore(ctx.params);
1492
1350
  if (result.ok)
@@ -1494,7 +1352,7 @@ const senpiRuntimePlugin = {
1494
1352
  else
1495
1353
  respond(false, undefined, result.error);
1496
1354
  }, { scope: "operator.write" });
1497
- registerGatewayMethod("senpi.listRuntimes", async (ctx) => {
1355
+ api.registerGatewayMethod("senpi.listRuntimes", async (ctx) => {
1498
1356
  const respond = ctx.respond;
1499
1357
  const { readRegistry } = await import("./runtime/strategy-registry.js");
1500
1358
  const registry = await readRegistry(baseStateDir);
@@ -1637,10 +1495,8 @@ const senpiRuntimePlugin = {
1637
1495
  // "running — NO ENTRY SCANNERS" forever. The map is otherwise only cleared on a
1638
1496
  // successful wire/install, which a scanner-less reinstall never reaches.
1639
1497
  unwiredRuntimes.delete(targetId);
1640
- // Same reuse hazard: a deleted id left in the index would keep publishing a strategy that no
1641
- // longer exists, and would hand a reinstall the old name and scanner list.
1642
- telemetryStrategies.delete(targetId);
1643
1498
  bootSkippedRuntimes.delete(targetId);
1499
+ strategiesStore.delete(targetId);
1644
1500
  // Archive state directory before removing from registry
1645
1501
  if (deletedEntry) {
1646
1502
  const runtimeStateDir = buildRuntimeStateDir(baseStateDir, targetId, deletedEntry.wallet ?? "");
@@ -1670,7 +1526,7 @@ const senpiRuntimePlugin = {
1670
1526
  }));
1671
1527
  return { ok: true, payload: { id: targetId } };
1672
1528
  }
1673
- registerGatewayMethod("senpi.deleteRuntime", async (ctx) => {
1529
+ api.registerGatewayMethod("senpi.deleteRuntime", async (ctx) => {
1674
1530
  const respond = ctx.respond;
1675
1531
  const result = await deleteRuntimeCore(ctx.params);
1676
1532
  if (result.ok)
@@ -1777,17 +1633,8 @@ const senpiRuntimePlugin = {
1777
1633
  return checkProofCoversRecipe(packageDir, verifyProof(packageDir), appliedRecipe, (f) => readFileSync(f, "utf8"));
1778
1634
  },
1779
1635
  applyToRuntime: makeApplyToRuntime(runtimeHandles, async (runtimeId, next, recipe) => {
1780
- // Re-index the topology facts FIRST, ahead of every early return and every failure path
1781
- // below. `makeApplyToRuntime` only reaches this callback once the in-process swap has
1782
- // succeeded, so `next` is already the recipe this box runs: its display name and its
1783
- // declared scanner list are the ones the topology payload must name. Leaving the index on
1784
- // the previous config would publish strategy rows the new scanner stats cannot join to.
1785
- //
1786
- // Ahead of the early return because an apply on a box with no intake still swapped the
1787
- // recipe, and ahead of the launcher because a launcher that fails is a runtime running the
1788
- // new recipe with no external scanners — the degraded state a dashboard most needs to show,
1789
- // and one it can only render if the declared scanners are the new ones.
1790
- noteTelemetryRuntimeConfig(runtimeId, next);
1636
+ // Runs only after the in-place swap succeeded.
1637
+ strategiesStore.set(runtimeId, strategyRecordFromConfig(next));
1791
1638
  // Restart this runtime's external scanner children against the new package.
1792
1639
  //
1793
1640
  // The rebuild swaps in-process components; these are separate OS processes, spawned once
@@ -1935,7 +1782,7 @@ const senpiRuntimePlugin = {
1935
1782
  * producers — slated for removal in Task 5 once the migration counter
1936
1783
  * decays to zero (see `runtime-phase-2-api-plan.md` Section 8).
1937
1784
  */
1938
- registerGatewayMethod("senpi.ingestExternalScannerData", async (ctx) => {
1785
+ api.registerGatewayMethod("senpi.ingestExternalScannerData", async (ctx) => {
1939
1786
  const respond = ctx.respond;
1940
1787
  // TODO(s11): wire actual telemetry counter for
1941
1788
  // runtime.deprecated_gateway_ingest_call (event name, properties:
@@ -1954,7 +1801,6 @@ const senpiRuntimePlugin = {
1954
1801
  ...(request.direction !== undefined && { direction: request.direction }),
1955
1802
  ...(request.score !== undefined && { score: request.score }),
1956
1803
  ...(request.signal_type !== undefined && { signal_type: request.signal_type }),
1957
- ...(request.tick_id !== undefined && { tick_id: request.tick_id }),
1958
1804
  ...(request.data !== undefined && { data: request.data }),
1959
1805
  ...(request.signals !== undefined && { signals: request.signals }),
1960
1806
  };
@@ -2012,7 +1858,7 @@ const senpiRuntimePlugin = {
2012
1858
  });
2013
1859
  }
2014
1860
  }, { scope: "operator.write" });
2015
- registerGatewayMethod("senpi.listDslPositions", async (ctx) => {
1861
+ api.registerGatewayMethod("senpi.listDslPositions", async (ctx) => {
2016
1862
  const respond = ctx.respond;
2017
1863
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2018
1864
  const address = typeof ctx.params?.address === "string" ? ctx.params.address : undefined;
@@ -2037,7 +1883,7 @@ const senpiRuntimePlugin = {
2037
1883
  }
2038
1884
  respond(true, { positions });
2039
1885
  }, { scope: "operator.read" });
2040
- registerGatewayMethod("senpi.getDslPositionState", async (ctx) => {
1886
+ api.registerGatewayMethod("senpi.getDslPositionState", async (ctx) => {
2041
1887
  const respond = ctx.respond;
2042
1888
  const asset = typeof ctx.params?.asset === "string" ? ctx.params.asset : undefined;
2043
1889
  if (!asset) {
@@ -2078,7 +1924,7 @@ const senpiRuntimePlugin = {
2078
1924
  }
2079
1925
  respond(true, { positions: matches });
2080
1926
  }, { scope: "operator.read" });
2081
- registerGatewayMethod("senpi.listDslArchives", async (ctx) => {
1927
+ api.registerGatewayMethod("senpi.listDslArchives", async (ctx) => {
2082
1928
  const respond = ctx.respond;
2083
1929
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2084
1930
  const address = typeof ctx.params?.address === "string" ? ctx.params.address : undefined;
@@ -2104,7 +1950,7 @@ const senpiRuntimePlugin = {
2104
1950
  closes = closes.slice(0, limit);
2105
1951
  respond(true, { closes });
2106
1952
  }, { scope: "operator.read" });
2107
- registerGatewayMethod("senpi.getHealthStatus", async (ctx) => {
1953
+ api.registerGatewayMethod("senpi.getHealthStatus", async (ctx) => {
2108
1954
  const respond = ctx.respond;
2109
1955
  const updater = autoUpdateCoordinator.getSnapshot();
2110
1956
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
@@ -2132,7 +1978,7 @@ const senpiRuntimePlugin = {
2132
1978
  const statuses = await collectRuntimeHealthStatuses(runtimeHandles, api.logger);
2133
1979
  respond(true, { statuses, updater });
2134
1980
  }, { scope: "operator.read" });
2135
- registerGatewayMethod("senpi.getSystemState", async (ctx) => {
1981
+ api.registerGatewayMethod("senpi.getSystemState", async (ctx) => {
2136
1982
  const respond = ctx.respond;
2137
1983
  const updater = autoUpdateCoordinator.getSnapshot();
2138
1984
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
@@ -2160,6 +2006,24 @@ const senpiRuntimePlugin = {
2160
2006
  const states = await collectRuntimeSystemStates(runtimeHandles, api.logger, baseStateDir);
2161
2007
  respond(true, { states, updater });
2162
2008
  }, { scope: "operator.read" });
2009
+ /**
2010
+ * Registered with scope `operator.read`. Optional params `afterEpoch` (string) and `afterSeq`
2011
+ * (non-negative integer); a cursor needs both. Replies
2012
+ * `{ v: 1, strategies: <last strategies payload>, events: { epoch, events, truncated } }`, where
2013
+ * `v` is the version of the reply. Keeps the gateway context's broadcast function for the
2014
+ * strategies and events loops.
2015
+ */
2016
+ api.registerGatewayMethod("senpi.telemetry.subscribe", async (ctx) => {
2017
+ const respond = ctx.respond;
2018
+ if (typeof ctx.context?.broadcast === "function")
2019
+ telemetryBroadcast = ctx.context.broadcast;
2020
+ const { afterEpoch, afterSeq } = ctx.params ?? {};
2021
+ const cursor = typeof afterEpoch === "string" && Number.isSafeInteger(afterSeq) && afterSeq >= 0
2022
+ ? { afterEpoch, afterSeq: afterSeq }
2023
+ : undefined;
2024
+ const events = await buildEventsReplay({ cursor, redactor: getDefaultPiiRedactor() });
2025
+ respond(true, { v: 1, strategies: strategiesPublisher.lastPayload(), events });
2026
+ }, { scope: "operator.read" });
2163
2027
  /**
2164
2028
  * Force the auto-update poll to run now instead of waiting for its timer.
2165
2029
  *
@@ -2199,36 +2063,6 @@ const senpiRuntimePlugin = {
2199
2063
  });
2200
2064
  }
2201
2065
  }, { scope: "operator.write" });
2202
- /**
2203
- * Live telemetry — the one method of the stream, called once per connect.
2204
- *
2205
- * It returns the current value of all three broadcast payloads plus the recent record window, so
2206
- * a client joining a box that has been running for hours renders a full screen immediately
2207
- * instead of waiting for the next thing to happen. `(afterEpoch, afterSeq)` resumes a client that
2208
- * already holds part of this epoch's run — a cursor naming any other epoch is dropped, because a
2209
- * seq from a previous run cannot address these records. `truncated` says when the records between
2210
- * a cursor of this epoch and the oldest record still held are gone, which nothing in `recent`
2211
- * alone can express.
2212
- *
2213
- * Read-only, like every other inspection method here. The scope that decides who RECEIVES the
2214
- * broadcasts is a property of the client's own connection, not of this method.
2215
- */
2216
- registerGatewayMethod("senpi.telemetry.subscribe", async (ctx) => {
2217
- const respond = ctx.respond;
2218
- try {
2219
- respond(true, await liveTelemetry.snapshot({
2220
- afterEpoch: ctx.params?.afterEpoch,
2221
- afterSeq: ctx.params?.afterSeq,
2222
- }));
2223
- }
2224
- catch (err) {
2225
- api.logger.warn("[senpi-runtime] telemetry subscribe error", { err });
2226
- respond(false, undefined, {
2227
- code: "INTERNAL_ERROR",
2228
- message: `Failed to build the telemetry snapshot: ${formatGatewayError(err)}`,
2229
- });
2230
- }
2231
- }, { scope: "operator.read" });
2232
2066
  /**
2233
2067
  * Action layer — read-only inspection.
2234
2068
  *
@@ -2248,7 +2082,7 @@ const senpiRuntimePlugin = {
2248
2082
  * persisted action-store machinery (`src/actions/store/`) usable
2249
2083
  * from the operator-facing CLI again.
2250
2084
  */
2251
- registerGatewayMethod("senpi.listActions", async (ctx) => {
2085
+ api.registerGatewayMethod("senpi.listActions", async (ctx) => {
2252
2086
  const respond = ctx.respond;
2253
2087
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2254
2088
  const address = typeof ctx.params?.address === "string" ? ctx.params.address : undefined;
@@ -2278,7 +2112,7 @@ const senpiRuntimePlugin = {
2278
2112
  }
2279
2113
  respond(true, { actions: merged });
2280
2114
  }, { scope: "operator.read" });
2281
- registerGatewayMethod("senpi.getActionState", async (ctx) => {
2115
+ api.registerGatewayMethod("senpi.getActionState", async (ctx) => {
2282
2116
  const respond = ctx.respond;
2283
2117
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2284
2118
  const actionName = typeof ctx.params?.actionName === "string" ? ctx.params.actionName : undefined;
@@ -2315,7 +2149,7 @@ const senpiRuntimePlugin = {
2315
2149
  });
2316
2150
  }
2317
2151
  }, { scope: "operator.read" });
2318
- registerGatewayMethod("senpi.getActionHistory", async (ctx) => {
2152
+ api.registerGatewayMethod("senpi.getActionHistory", async (ctx) => {
2319
2153
  const respond = ctx.respond;
2320
2154
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2321
2155
  const actionName = typeof ctx.params?.actionName === "string" ? ctx.params.actionName : undefined;
@@ -2383,7 +2217,7 @@ const senpiRuntimePlugin = {
2383
2217
  // reachable on HTTP `/audit`. Passes through the caller-controlled subset
2384
2218
  // of the audit_query contract; resource scoping (wallet → strategyId) is
2385
2219
  // owned by handle.auditQuery, never the caller.
2386
- registerGatewayMethod("senpi.auditQuery", async (ctx) => {
2220
+ api.registerGatewayMethod("senpi.auditQuery", async (ctx) => {
2387
2221
  const respond = ctx.respond;
2388
2222
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2389
2223
  if (!runtimeId) {
@@ -2431,7 +2265,7 @@ const senpiRuntimePlugin = {
2431
2265
  // its state root private, so re-derive it from baseStateDir + the handle's
2432
2266
  // wallet (same helper the writer's stateDir was built with); the writer
2433
2267
  // nests entries under `<root>/<address>/risk-guard/audit.jsonl`.
2434
- registerGatewayMethod("senpi.getRiskAudit", async (ctx) => {
2268
+ api.registerGatewayMethod("senpi.getRiskAudit", async (ctx) => {
2435
2269
  const respond = ctx.respond;
2436
2270
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2437
2271
  if (!runtimeId) {
@@ -2474,7 +2308,7 @@ const senpiRuntimePlugin = {
2474
2308
  // Per-strategy local domain-event ring (on-disk JSONL) — the trade narrative, queryable locally.
2475
2309
  // Same state-root re-derivation as getRiskAudit: the Runtime keeps its root private, so rebuild it
2476
2310
  // from baseStateDir + the handle's wallet; the writer nests rows under `<root>/strategies/<address>/events/`.
2477
- registerGatewayMethod("senpi.getEvents", async (ctx) => {
2311
+ api.registerGatewayMethod("senpi.getEvents", async (ctx) => {
2478
2312
  const respond = ctx.respond;
2479
2313
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2480
2314
  if (!runtimeId) {
@@ -2531,12 +2365,12 @@ const senpiRuntimePlugin = {
2531
2365
  }
2532
2366
  }, { scope: "operator.read" });
2533
2367
  // ─── Skills manager — read + mutate ──────────────────────────────────────
2534
- registerGatewayMethod("senpi.skills.status", async (ctx) => {
2368
+ api.registerGatewayMethod("senpi.skills.status", async (ctx) => {
2535
2369
  const respond = ctx.respond;
2536
2370
  const snapshot = skillsCoordinator.getSnapshot();
2537
2371
  respond(true, { snapshot });
2538
2372
  }, { scope: "operator.read" });
2539
- registerGatewayMethod("senpi.skills.update", async (ctx) => {
2373
+ api.registerGatewayMethod("senpi.skills.update", async (ctx) => {
2540
2374
  const respond = ctx.respond;
2541
2375
  const name = typeof ctx.params?.name === "string" ? ctx.params.name.trim() : undefined;
2542
2376
  if (!name) {
@@ -2561,7 +2395,7 @@ const senpiRuntimePlugin = {
2561
2395
  });
2562
2396
  }
2563
2397
  }, { scope: "operator.write" });
2564
- registerGatewayMethod("senpi.skills.updateAll", async (ctx) => {
2398
+ api.registerGatewayMethod("senpi.skills.updateAll", async (ctx) => {
2565
2399
  const respond = ctx.respond;
2566
2400
  try {
2567
2401
  const results = await skillsCoordinator.updateAll();
@@ -2627,7 +2461,6 @@ function parseGatewayExternalScannerIngestParams(params) {
2627
2461
  direction: params?.direction,
2628
2462
  score: params?.score,
2629
2463
  signal_type: params?.signal_type,
2630
- tick_id: params?.tick_id,
2631
2464
  data: params?.data,
2632
2465
  signals: params?.signals,
2633
2466
  },