@senpi/runtime 1.1.0-dev.runtime-phase-2.20260609132603 → 1.1.0-dev.runtime-phase-2.20260610130051

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 (205) hide show
  1. package/dist/actions/close-position/close-position.action.d.ts +0 -16
  2. package/dist/actions/close-position/close-position.action.d.ts.map +1 -1
  3. package/dist/actions/close-position/close-position.action.js +38 -299
  4. package/dist/actions/close-position/close-position.action.js.map +1 -1
  5. package/dist/actions/decision-engine.d.ts +0 -2
  6. package/dist/actions/decision-engine.d.ts.map +1 -1
  7. package/dist/actions/decision-engine.js.map +1 -1
  8. package/dist/actions/llm-decision/factory.d.ts +0 -3
  9. package/dist/actions/llm-decision/factory.d.ts.map +1 -1
  10. package/dist/actions/llm-decision/factory.js +0 -1
  11. package/dist/actions/llm-decision/factory.js.map +1 -1
  12. package/dist/actions/llm-decision/openclaw-llm-decision.d.ts +0 -13
  13. package/dist/actions/llm-decision/openclaw-llm-decision.d.ts.map +1 -1
  14. package/dist/actions/llm-decision/openclaw-llm-decision.js +68 -195
  15. package/dist/actions/llm-decision/openclaw-llm-decision.js.map +1 -1
  16. package/dist/actions/open-position/open-position.action.d.ts +0 -12
  17. package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
  18. package/dist/actions/open-position/open-position.action.js +123 -346
  19. package/dist/actions/open-position/open-position.action.js.map +1 -1
  20. package/dist/actions/position-tracker/position-tracker.action.d.ts +0 -6
  21. package/dist/actions/position-tracker/position-tracker.action.d.ts.map +1 -1
  22. package/dist/actions/position-tracker/position-tracker.action.js +0 -48
  23. package/dist/actions/position-tracker/position-tracker.action.js.map +1 -1
  24. package/dist/dsl/events/handlers.d.ts.map +1 -1
  25. package/dist/dsl/events/handlers.js +4 -0
  26. package/dist/dsl/events/handlers.js.map +1 -1
  27. package/dist/recording/emitter.d.ts +21 -0
  28. package/dist/recording/emitter.d.ts.map +1 -0
  29. package/dist/recording/emitter.js +2 -0
  30. package/dist/recording/emitter.js.map +1 -0
  31. package/dist/recording/identity.d.ts +34 -0
  32. package/dist/recording/identity.d.ts.map +1 -0
  33. package/dist/recording/identity.js +2 -0
  34. package/dist/recording/identity.js.map +1 -0
  35. package/dist/recording/index.d.ts +11 -0
  36. package/dist/recording/index.d.ts.map +1 -0
  37. package/dist/recording/index.js +2 -0
  38. package/dist/recording/index.js.map +1 -0
  39. package/dist/recording/noop-emitter.d.ts +17 -0
  40. package/dist/recording/noop-emitter.d.ts.map +1 -0
  41. package/dist/recording/noop-emitter.js +16 -0
  42. package/dist/recording/noop-emitter.js.map +1 -0
  43. package/dist/recording/otel/attributes.d.ts +35 -0
  44. package/dist/recording/otel/attributes.d.ts.map +1 -0
  45. package/dist/recording/otel/attributes.js +131 -0
  46. package/dist/recording/otel/attributes.js.map +1 -0
  47. package/dist/recording/otel/constants.d.ts +81 -0
  48. package/dist/recording/otel/constants.d.ts.map +1 -0
  49. package/dist/recording/otel/constants.js +96 -0
  50. package/dist/recording/otel/constants.js.map +1 -0
  51. package/dist/recording/otel/ids.d.ts +14 -0
  52. package/dist/recording/otel/ids.d.ts.map +1 -0
  53. package/dist/recording/otel/ids.js +12 -0
  54. package/dist/recording/otel/ids.js.map +1 -0
  55. package/dist/recording/otel/index.d.ts +13 -0
  56. package/dist/recording/otel/index.d.ts.map +1 -0
  57. package/dist/recording/otel/index.js +9 -0
  58. package/dist/recording/otel/index.js.map +1 -0
  59. package/dist/recording/otel/mappers.d.ts +42 -0
  60. package/dist/recording/otel/mappers.d.ts.map +1 -0
  61. package/dist/recording/otel/mappers.js +337 -0
  62. package/dist/recording/otel/mappers.js.map +1 -0
  63. package/dist/recording/otel/otel-emitter.d.ts +37 -0
  64. package/dist/recording/otel/otel-emitter.d.ts.map +1 -0
  65. package/dist/recording/otel/otel-emitter.js +75 -0
  66. package/dist/recording/otel/otel-emitter.js.map +1 -0
  67. package/dist/recording/otel/redaction.d.ts +31 -0
  68. package/dist/recording/otel/redaction.d.ts.map +1 -0
  69. package/dist/recording/otel/redaction.js +40 -0
  70. package/dist/recording/otel/redaction.js.map +1 -0
  71. package/dist/recording/records.d.ts +327 -0
  72. package/dist/recording/records.d.ts.map +1 -0
  73. package/dist/recording/records.js +14 -0
  74. package/dist/recording/records.js.map +1 -0
  75. package/dist/recording/transport/batcher.d.ts +59 -0
  76. package/dist/recording/transport/batcher.d.ts.map +1 -0
  77. package/dist/recording/transport/batcher.js +234 -0
  78. package/dist/recording/transport/batcher.js.map +1 -0
  79. package/dist/recording/transport/client.d.ts +41 -0
  80. package/dist/recording/transport/client.d.ts.map +1 -0
  81. package/dist/recording/transport/client.js +69 -0
  82. package/dist/recording/transport/client.js.map +1 -0
  83. package/dist/recording/transport/config.d.ts +53 -0
  84. package/dist/recording/transport/config.d.ts.map +1 -0
  85. package/dist/recording/transport/config.js +41 -0
  86. package/dist/recording/transport/config.js.map +1 -0
  87. package/dist/recording/transport/index.d.ts +16 -0
  88. package/dist/recording/transport/index.d.ts.map +1 -0
  89. package/dist/recording/transport/index.js +12 -0
  90. package/dist/recording/transport/index.js.map +1 -0
  91. package/dist/recording/transport/rows.d.ts +88 -0
  92. package/dist/recording/transport/rows.d.ts.map +1 -0
  93. package/dist/recording/transport/rows.js +15 -0
  94. package/dist/recording/transport/rows.js.map +1 -0
  95. package/dist/recording/wiring/emitter-factory.d.ts +39 -0
  96. package/dist/recording/wiring/emitter-factory.d.ts.map +1 -0
  97. package/dist/recording/wiring/emitter-factory.js +62 -0
  98. package/dist/recording/wiring/emitter-factory.js.map +1 -0
  99. package/dist/recording/wiring/identity.d.ts +27 -0
  100. package/dist/recording/wiring/identity.d.ts.map +1 -0
  101. package/dist/recording/wiring/identity.js +51 -0
  102. package/dist/recording/wiring/identity.js.map +1 -0
  103. package/dist/recording/wiring/index.d.ts +12 -0
  104. package/dist/recording/wiring/index.d.ts.map +1 -0
  105. package/dist/recording/wiring/index.js +9 -0
  106. package/dist/recording/wiring/index.js.map +1 -0
  107. package/dist/recording/wiring/record-builders.d.ts +68 -0
  108. package/dist/recording/wiring/record-builders.d.ts.map +1 -0
  109. package/dist/recording/wiring/record-builders.js +441 -0
  110. package/dist/recording/wiring/record-builders.js.map +1 -0
  111. package/dist/recording/wiring/subscriber.d.ts +21 -0
  112. package/dist/recording/wiring/subscriber.d.ts.map +1 -0
  113. package/dist/recording/wiring/subscriber.js +171 -0
  114. package/dist/recording/wiring/subscriber.js.map +1 -0
  115. package/dist/recording/wiring/values.d.ts +19 -0
  116. package/dist/recording/wiring/values.d.ts.map +1 -0
  117. package/dist/recording/wiring/values.js +49 -0
  118. package/dist/recording/wiring/values.js.map +1 -0
  119. package/dist/runtime/create-actions.d.ts.map +1 -1
  120. package/dist/runtime/create-actions.js +0 -2
  121. package/dist/runtime/create-actions.js.map +1 -1
  122. package/dist/runtime/create-runtime-context.d.ts +0 -2
  123. package/dist/runtime/create-runtime-context.d.ts.map +1 -1
  124. package/dist/runtime/create-runtime-context.js +0 -1
  125. package/dist/runtime/create-runtime-context.js.map +1 -1
  126. package/dist/runtime/create-scanner-module.d.ts +1 -1
  127. package/dist/runtime/create-scanner-module.d.ts.map +1 -1
  128. package/dist/runtime/create-scanner-module.js +1 -2
  129. package/dist/runtime/create-scanner-module.js.map +1 -1
  130. package/dist/runtime/load-runtime-config.d.ts +9 -0
  131. package/dist/runtime/load-runtime-config.d.ts.map +1 -1
  132. package/dist/runtime/load-runtime-config.js +41 -1
  133. package/dist/runtime/load-runtime-config.js.map +1 -1
  134. package/dist/runtime/run.d.ts.map +1 -1
  135. package/dist/runtime/run.js +3 -2
  136. package/dist/runtime/run.js.map +1 -1
  137. package/dist/runtime/runtime-schema.d.ts +182 -24
  138. package/dist/runtime/runtime-schema.d.ts.map +1 -1
  139. package/dist/runtime/runtime-schema.js +17 -1
  140. package/dist/runtime/runtime-schema.js.map +1 -1
  141. package/dist/runtime/runtime.d.ts +15 -5
  142. package/dist/runtime/runtime.d.ts.map +1 -1
  143. package/dist/runtime/runtime.js +248 -273
  144. package/dist/runtime/runtime.js.map +1 -1
  145. package/dist/scanners/engine/data-providers.d.ts +0 -2
  146. package/dist/scanners/engine/data-providers.d.ts.map +1 -1
  147. package/dist/scanners/engine/data-providers.js.map +1 -1
  148. package/dist/scanners/engine/engine.d.ts.map +1 -1
  149. package/dist/scanners/engine/engine.js +9 -68
  150. package/dist/scanners/engine/engine.js.map +1 -1
  151. package/dist/scanners/events.d.ts +6 -0
  152. package/dist/scanners/events.d.ts.map +1 -1
  153. package/dist/scanners/events.js.map +1 -1
  154. package/dist/scanners/runtime-module.d.ts +0 -2
  155. package/dist/scanners/runtime-module.d.ts.map +1 -1
  156. package/dist/scanners/runtime-module.js +9 -2
  157. package/dist/scanners/runtime-module.js.map +1 -1
  158. package/dist/senpi/client.d.ts +2 -2
  159. package/dist/senpi/client.d.ts.map +1 -1
  160. package/dist/senpi/client.js +20 -120
  161. package/dist/senpi/client.js.map +1 -1
  162. package/dist/senpi/types.d.ts +1 -68
  163. package/dist/senpi/types.d.ts.map +1 -1
  164. package/dist/strategy/strategy-state.d.ts +1 -1
  165. package/dist/strategy/strategy-state.d.ts.map +1 -1
  166. package/dist/tracking/spans/plugin-span-tracker.d.ts.map +1 -1
  167. package/dist/tracking/spans/plugin-span-tracker.js +24 -4
  168. package/dist/tracking/spans/plugin-span-tracker.js.map +1 -1
  169. package/dist/tracking/spans/redact-pii.d.ts +0 -10
  170. package/dist/tracking/spans/redact-pii.d.ts.map +1 -1
  171. package/dist/tracking/spans/redact-pii.js +2 -80
  172. package/dist/tracking/spans/redact-pii.js.map +1 -1
  173. package/dist/types/runtime.d.ts +0 -3
  174. package/dist/types/runtime.d.ts.map +1 -1
  175. package/dist/types/strategy.d.ts +2 -2
  176. package/dist/types/strategy.d.ts.map +1 -1
  177. package/package.json +1 -1
  178. package/dist/actions/otel-helpers.d.ts +0 -65
  179. package/dist/actions/otel-helpers.d.ts.map +0 -1
  180. package/dist/actions/otel-helpers.js +0 -122
  181. package/dist/actions/otel-helpers.js.map +0 -1
  182. package/dist/otel/context.d.ts +0 -24
  183. package/dist/otel/context.d.ts.map +0 -1
  184. package/dist/otel/context.js +0 -29
  185. package/dist/otel/context.js.map +0 -1
  186. package/dist/otel/emitter.d.ts +0 -73
  187. package/dist/otel/emitter.d.ts.map +0 -1
  188. package/dist/otel/emitter.js +0 -248
  189. package/dist/otel/emitter.js.map +0 -1
  190. package/dist/otel/encode.d.ts +0 -48
  191. package/dist/otel/encode.d.ts.map +0 -1
  192. package/dist/otel/encode.js +0 -100
  193. package/dist/otel/encode.js.map +0 -1
  194. package/dist/otel/index.d.ts +0 -13
  195. package/dist/otel/index.d.ts.map +0 -1
  196. package/dist/otel/index.js +0 -10
  197. package/dist/otel/index.js.map +0 -1
  198. package/dist/otel/transport.d.ts +0 -57
  199. package/dist/otel/transport.d.ts.map +0 -1
  200. package/dist/otel/transport.js +0 -208
  201. package/dist/otel/transport.js.map +0 -1
  202. package/dist/otel/types.d.ts +0 -62
  203. package/dist/otel/types.d.ts.map +0 -1
  204. package/dist/otel/types.js +0 -2
  205. package/dist/otel/types.js.map +0 -1
@@ -35,7 +35,7 @@ import { createTrackingHandler, EventTracker, BatchingTransport, NoOpTransport,
35
35
  import { VERSION } from "../version.js";
36
36
  import { randomUUID } from "node:crypto";
37
37
  import { trackingContext } from "../tracking/context.js";
38
- import { OtelEmitter, otelContext } from "../otel/index.js";
38
+ import { buildIdentity, createRecordingComponents, createRecordingSubscriber, resolveTelemetryEnabled, } from "../recording/wiring/index.js";
39
39
  const logger = createLogger("[senpi-core][runtime]");
40
40
  export class Runtime {
41
41
  config;
@@ -49,6 +49,8 @@ export class Runtime {
49
49
  senpi = null;
50
50
  healthStateModule = null;
51
51
  trackingHandler = null;
52
+ recordingSubscriber = null;
53
+ recordingBatcher = null;
52
54
  started = false;
53
55
  startedAt = null;
54
56
  /** Serial signal processing: queue and consumer so only one signal is handled at a time. */
@@ -61,6 +63,8 @@ export class Runtime {
61
63
  actionRuntimeModule;
62
64
  /** Stored so we can call off() in stop() and avoid retaining Runtime via scanner EventEmitter. */
63
65
  onScannerRunComplete = null;
66
+ onScannerRunSkip = null;
67
+ onScannerRunError = null;
64
68
  /** Cached strategy id resolved from the runtime wallet on first {@link auditQuery} call. */
65
69
  cachedStrategyId = null;
66
70
  /** In-flight resolution promise; ensures concurrent first-burst callers reuse one `listStrategies`. */
@@ -73,21 +77,9 @@ export class Runtime {
73
77
  * telemetry).
74
78
  */
75
79
  runtimeBootId = randomUUID();
76
- /** OTEL V2 emitter. Identity envelope baked in at construction. */
77
- otel;
78
80
  constructor(config, options) {
79
81
  this.config = config;
80
82
  this.options = options;
81
- this.otel = new OtelEmitter({
82
- identity: {
83
- serviceName: "senpi-trading-runtime",
84
- strategyAddress: this.config.strategy?.wallet,
85
- recipeName: this.config.name,
86
- recipeVersion: this.config.version != null ? String(this.config.version) : undefined,
87
- runtimeVersion: VERSION,
88
- runtimeBootId: this.runtimeBootId,
89
- },
90
- });
91
83
  }
92
84
  get dsl() {
93
85
  return this.dslPlugin;
@@ -308,165 +300,231 @@ export class Runtime {
308
300
  }
309
301
  this.senpi = senpi;
310
302
  this.strategyConfig = resolveStrategyConfig(this.config);
311
- // try {
312
- // const { reconcileStateWithBackend } = await import("./reconcile-state.js");
313
- // await reconcileStateWithBackend(this.stateManager, senpi, [this.strategyConfig.address]);
314
- // } catch (err) {
315
- // logger.warn("reconcile after hydrate failed, continuing", { err });
316
- // }
317
- const llmDecision = createLlmDecision({ otel: this.otel });
318
- const contextBuilder = createContextBuilder();
319
- const notifications = createNotificationService({
320
- ...this.config.notifications,
321
- mcpTransport: this.options.mcpTransport,
322
- });
323
- const riskBlock = this.config.risk;
324
- const strategyAddress = this.strategyConfig.address;
325
- const auditLog = riskBlock ? createRiskAuditLog(stateDir) : undefined;
326
- // In-memory strategy-lifecycle tracker (§4 seed): owns the self-pause state that
327
- // RiskGuard used to hold and fires the hook + notification on a running→paused
328
- // transition. RiskGuard stays stateless and only reports gate evaluations.
329
- const lifecycle = createStrategyLifecycle({
330
- hookSystem: this.hookSystem,
331
- notifications,
332
- runtimeName: this.config.name,
303
+ // Telemetry recording: subscribe the emitter to the bus (on by default; degrades to a noop
304
+ // emitter when disabled or when no token is present — never a boot failure). Identity + config
305
+ // fingerprint are built lazily from the unresolved config — only on the live path, and never
306
+ // from the resolved config (which would inline secrets).
307
+ const telemetryConfig = this.config.telemetry;
308
+ const recording = createRecordingComponents({
309
+ enabled: resolveTelemetryEnabled(telemetryConfig?.enabled, process.env.SENPI_TELEMETRY_ENABLED),
310
+ token: trackingApiKey,
311
+ identity: () => buildIdentity({
312
+ name: this.config.name,
313
+ recipeVersion: this.config.version,
314
+ strategyAddress: this.strategyConfig.address,
315
+ bootId: this.runtimeBootId,
316
+ unresolvedConfig: this.options.unresolvedConfig,
317
+ }),
318
+ scopeVersion: VERSION,
319
+ baseUrl: telemetryConfig?.url,
320
+ flushIntervalMs: telemetryConfig?.flush_interval_ms,
333
321
  });
334
- const riskGuard = riskBlock && senpi
335
- ? createRiskGuard({
322
+ this.recordingBatcher = recording.batcher;
323
+ // Only attach the subscriber when telemetry is live (batcher present): with a noop emitter,
324
+ // per-event record assembly on the serial signal path would be pure dead work.
325
+ if (recording.batcher) {
326
+ this.recordingSubscriber = createRecordingSubscriber(this.bus, recording.emitter, {
327
+ unresolvedConfig: this.options.unresolvedConfig,
328
+ });
329
+ }
330
+ // Everything from here to `started = true` can throw; without the catch, stop() would
331
+ // early-return on `!this.started` and the recording batcher/subscriber would leak.
332
+ try {
333
+ // try {
334
+ // const { reconcileStateWithBackend } = await import("./reconcile-state.js");
335
+ // await reconcileStateWithBackend(this.stateManager, senpi, [this.strategyConfig.address]);
336
+ // } catch (err) {
337
+ // logger.warn("reconcile after hydrate failed, continuing", { err });
338
+ // }
339
+ const llmDecision = createLlmDecision({});
340
+ const contextBuilder = createContextBuilder();
341
+ const notifications = createNotificationService({
342
+ ...this.config.notifications,
343
+ mcpTransport: this.options.mcpTransport,
344
+ });
345
+ const riskBlock = this.config.risk;
346
+ const strategyAddress = this.strategyConfig.address;
347
+ const auditLog = riskBlock ? createRiskAuditLog(stateDir) : undefined;
348
+ // In-memory strategy-lifecycle tracker (§4 seed): owns the self-pause state that
349
+ // RiskGuard used to hold and fires the hook + notification on a running→paused
350
+ // transition. RiskGuard stays stateless and only reports gate evaluations.
351
+ const lifecycle = createStrategyLifecycle({
352
+ hookSystem: this.hookSystem,
353
+ notifications,
354
+ runtimeName: this.config.name,
355
+ });
356
+ const riskGuard = riskBlock && senpi
357
+ ? createRiskGuard({
358
+ senpi,
359
+ notifications,
360
+ strategies: new Map([[strategyAddress, this.strategyConfig]]),
361
+ // Human strategy label for alerts; risk-guard falls back to the
362
+ // truncated address when an address has no name here.
363
+ strategyNames: { [strategyAddress]: this.config.name },
364
+ auditLog,
365
+ lifecycleTracker: lifecycle,
366
+ })
367
+ : createRiskGuardStub();
368
+ logger.info("Runtime.start() creating scanner module");
369
+ const layout = buildStateLayout(stateDir);
370
+ // Augment caller-supplied providers with StateManager-backed DSL state access
371
+ const augmentedProviders = {
372
+ ...scannerProviders,
373
+ getDslStatesForStrategy: scannerProviders?.getDslStatesForStrategy
374
+ ?? (async (address) => this.stateManager.listDslStates(address)),
375
+ };
376
+ this.scannerModule = createScannerModuleFromRuntimeConfig(this.config, layout.scannerRoot, augmentedProviders);
377
+ logger.info("Runtime.start() scanner module created, calling start()");
378
+ // Pre-load max leverage cache (non-blocking — log warning on failure)
379
+ const maxLeverageCache = new MaxLeverageCache(senpi);
380
+ try {
381
+ await maxLeverageCache.load();
382
+ }
383
+ catch (err) {
384
+ logger.warn("failed to pre-load max leverage cache, will retry on first use", { err });
385
+ }
386
+ // Build RuntimeContext once per trading recipe and reuse for all actions.
387
+ const runtimeContext = createRuntimeContext(this.strategyConfig, {
336
388
  senpi,
389
+ hookSystem: this.hookSystem,
390
+ llmDecision,
391
+ contextBuilder,
392
+ riskGuard,
337
393
  notifications,
338
- strategies: new Map([[strategyAddress, this.strategyConfig]]),
339
- // Human strategy label for alerts; risk-guard falls back to the
340
- // truncated address when an address has no name here.
341
- strategyNames: { [strategyAddress]: this.config.name },
342
- auditLog,
343
- lifecycleTracker: lifecycle,
344
- })
345
- : createRiskGuardStub();
346
- logger.info("Runtime.start() creating scanner module");
347
- const layout = buildStateLayout(stateDir);
348
- // Augment caller-supplied providers with StateManager-backed DSL state access
349
- const augmentedProviders = {
350
- ...scannerProviders,
351
- getDslStatesForStrategy: scannerProviders?.getDslStatesForStrategy
352
- ?? (async (address) => this.stateManager.listDslStates(address)),
353
- };
354
- this.scannerModule = createScannerModuleFromRuntimeConfig(this.config, layout.scannerRoot, augmentedProviders, undefined, this.otel);
355
- logger.info("Runtime.start() scanner module created, calling start()");
356
- // Pre-load max leverage cache (non-blocking — log warning on failure)
357
- const maxLeverageCache = new MaxLeverageCache(senpi);
358
- try {
359
- await maxLeverageCache.load();
360
- }
361
- catch (err) {
362
- logger.warn("failed to pre-load max leverage cache, will retry on first use", { err });
363
- }
364
- // Build RuntimeContext once per trading recipe and reuse for all actions.
365
- const runtimeContext = createRuntimeContext(this.strategyConfig, {
366
- senpi,
367
- hookSystem: this.hookSystem,
368
- llmDecision,
369
- contextBuilder,
370
- riskGuard,
371
- notifications,
372
- scannerModule: this.scannerModule,
373
- maxLeverageCache,
374
- otel: this.otel,
375
- });
376
- this.actionRuntimeModule = new ActionRuntimeModule({ stateDir: layout.actionRoot });
377
- const { runActionsForSignal } = await createActions(this.config, {
378
- bus: this.bus,
379
- senpi,
380
- stateManager: this.stateManager,
381
- hookSystem: this.hookSystem,
382
- llmDecision,
383
- contextBuilder,
384
- riskGuard,
385
- notifications,
386
- runtimeContext,
387
- actionModule: this.actionRuntimeModule,
388
- });
389
- this.runActionsForSignal = runActionsForSignal;
390
- const notificationsConfig = this.config.notifications;
391
- if (hasDsl) {
392
- const dslNotificationsDisabled = notificationsConfig?.dsl_lifecycle === false;
393
- const dslNotifySlUpdates = notificationsConfig?.dsl_notify_sl_updates === true;
394
- this.dslPlugin = await DslPluginClass.create({
395
- config: exitConfig,
394
+ scannerModule: this.scannerModule,
395
+ maxLeverageCache,
396
+ });
397
+ this.actionRuntimeModule = new ActionRuntimeModule({ stateDir: layout.actionRoot });
398
+ const { runActionsForSignal } = await createActions(this.config, {
396
399
  bus: this.bus,
397
- stateDir,
400
+ senpi,
398
401
  stateManager: this.stateManager,
399
- senpiClient: senpi,
400
- strategyStates: createStrategyStatesMap(this.strategyConfig, senpi),
401
- notifications: dslNotificationsDisabled ? undefined : notifications,
402
- notifySlUpdates: dslNotifySlUpdates,
403
- strategyName: this.config.name,
404
- });
405
- }
406
- else {
407
- this.dslPlugin = null;
408
- }
409
- if (isActionLifecycleNotificationsEnabled(notificationsConfig)) {
410
- this.actionNotificationSubscriber = createActionNotificationSubscriber(this.bus, notifications, {
411
- strategyName: this.config.name,
402
+ hookSystem: this.hookSystem,
403
+ llmDecision,
404
+ contextBuilder,
405
+ riskGuard,
406
+ notifications,
407
+ runtimeContext,
408
+ actionModule: this.actionRuntimeModule,
412
409
  });
413
- }
414
- else {
415
- this.actionNotificationSubscriber = null;
416
- }
417
- this.wireDslOtelLogs();
418
- initRisk(this.bus);
419
- this.healthStateModule = createHealthStateModule({
420
- runtimeName: this.config.name,
421
- stateDir,
422
- startedAt: this.startedAt,
423
- providers: {
424
- scanners: this.scannerModule,
425
- dsl: this.dslPlugin ?? createDisabledDslProvider(),
426
- actions: this.actionRuntimeModule,
427
- risk: riskBlock
428
- ? createRiskHealthProvider(riskGuard, strategyAddress)
429
- : undefined,
430
- },
431
- });
432
- this.onScannerRunComplete = (event) => {
433
- console.info("[senpi-runtime] [ingest-trace] onScannerRunComplete → enqueue", JSON.stringify({
434
- address: event.result.address,
435
- scannerId: event.result.scannerId,
436
- signalCount: Array.isArray(event.result.signals) ? event.result.signals.length : 0,
437
- status: event.result.status,
438
- queueDepth: this.signalQueue.length + 1,
439
- }));
440
- const correlationId = randomUUID();
441
- this.signalQueue.push({ result: event.result, correlationId });
442
- if (this.signalQueueWait) {
443
- const w = this.signalQueueWait;
444
- this.signalQueueWait = null;
445
- w();
410
+ this.runActionsForSignal = runActionsForSignal;
411
+ const notificationsConfig = this.config.notifications;
412
+ if (hasDsl) {
413
+ const dslNotificationsDisabled = notificationsConfig?.dsl_lifecycle === false;
414
+ const dslNotifySlUpdates = notificationsConfig?.dsl_notify_sl_updates === true;
415
+ this.dslPlugin = await DslPluginClass.create({
416
+ config: exitConfig,
417
+ bus: this.bus,
418
+ stateDir,
419
+ stateManager: this.stateManager,
420
+ senpiClient: senpi,
421
+ strategyStates: createStrategyStatesMap(this.strategyConfig, senpi),
422
+ notifications: dslNotificationsDisabled ? undefined : notifications,
423
+ notifySlUpdates: dslNotifySlUpdates,
424
+ strategyName: this.config.name,
425
+ });
426
+ }
427
+ else {
428
+ this.dslPlugin = null;
429
+ }
430
+ if (isActionLifecycleNotificationsEnabled(notificationsConfig)) {
431
+ this.actionNotificationSubscriber = createActionNotificationSubscriber(this.bus, notifications, {
432
+ strategyName: this.config.name,
433
+ });
434
+ }
435
+ else {
436
+ this.actionNotificationSubscriber = null;
446
437
  }
447
- // Emit scanner run for tracking
448
- this.bus.emit("tracking.scanner_run", {
449
- address: event.result.address,
450
- data: {
451
- ...event.result,
452
- __correlationId: correlationId,
438
+ initRisk(this.bus);
439
+ this.healthStateModule = createHealthStateModule({
440
+ runtimeName: this.config.name,
441
+ stateDir,
442
+ startedAt: this.startedAt,
443
+ providers: {
444
+ scanners: this.scannerModule,
445
+ dsl: this.dslPlugin ?? createDisabledDslProvider(),
446
+ actions: this.actionRuntimeModule,
447
+ risk: riskBlock
448
+ ? createRiskHealthProvider(riskGuard, strategyAddress)
449
+ : undefined,
453
450
  },
454
- timestamp: new Date().toISOString(),
455
451
  });
456
- // Forward scanner summary alerts (e.g. watchdog) to notification service
457
- const alerts = event.result.summary?.alerts;
458
- if (Array.isArray(alerts) && alerts.length > 0) {
459
- const header = `[${event.result.scannerId}] ${alerts.length} alert(s):`;
460
- const lines = alerts.map((a) => ` ${a.level ?? "ALERT"}: ${a.message ?? ""}`);
461
- notifications.notify(`${header}\n${lines.join("\n")}`).catch(() => { });
462
- }
463
- };
464
- this.scannerModule.on("scanner:run:complete", this.onScannerRunComplete);
465
- this.signalConsumerDone = this.runSignalConsumer();
466
- logger.info("Runtime.start() invoking scannerModule.start()");
467
- await this.scannerModule.start();
468
- logger.info("Runtime.start() scannerModule.start() returned");
469
- this.started = true;
452
+ this.onScannerRunComplete = (event) => {
453
+ console.info("[senpi-runtime] [ingest-trace] onScannerRunComplete → enqueue", JSON.stringify({
454
+ address: event.result.address,
455
+ scannerId: event.result.scannerId,
456
+ signalCount: Array.isArray(event.result.signals) ? event.result.signals.length : 0,
457
+ status: event.result.status,
458
+ queueDepth: this.signalQueue.length + 1,
459
+ }));
460
+ const correlationId = randomUUID();
461
+ this.signalQueue.push({ result: event.result, correlationId });
462
+ if (this.signalQueueWait) {
463
+ const w = this.signalQueueWait;
464
+ this.signalQueueWait = null;
465
+ w();
466
+ }
467
+ // Emit scanner run for tracking (startedAt is data stamping for telemetry durations)
468
+ this.bus.emit("tracking.scanner_run", {
469
+ address: event.result.address,
470
+ data: {
471
+ ...event.result,
472
+ startedAt: event.startedAt,
473
+ __correlationId: correlationId,
474
+ },
475
+ timestamp: new Date().toISOString(),
476
+ });
477
+ // Forward scanner summary alerts (e.g. watchdog) to notification service
478
+ const alerts = event.result.summary?.alerts;
479
+ if (Array.isArray(alerts) && alerts.length > 0) {
480
+ const header = `[${event.result.scannerId}] ${alerts.length} alert(s):`;
481
+ const lines = alerts.map((a) => ` ${a.level ?? "ALERT"}: ${a.message ?? ""}`);
482
+ notifications.notify(`${header}\n${lines.join("\n")}`).catch(() => { });
483
+ }
484
+ };
485
+ this.scannerModule.on("scanner:run:complete", this.onScannerRunComplete);
486
+ // Surface skipped/errored scanner runs on the bus for telemetry (add-only observation events —
487
+ // distinct names so existing `tracking.scanner_run` consumers see an unchanged payload mix).
488
+ // Neither enqueues a signal: only completed runs feed the signal queue.
489
+ this.onScannerRunSkip = (event) => {
490
+ this.bus.emit("tracking.scanner_run_skipped", {
491
+ address: event.address,
492
+ data: {
493
+ scannerId: event.scannerId,
494
+ status: event.status,
495
+ reason: event.reason,
496
+ timestamp: event.timestamp,
497
+ startedAt: event.startedAt,
498
+ },
499
+ timestamp: new Date().toISOString(),
500
+ });
501
+ };
502
+ this.onScannerRunError = (event) => {
503
+ this.bus.emit("tracking.scanner_run_error", {
504
+ address: event.address,
505
+ data: {
506
+ scannerId: event.scannerId,
507
+ status: "error",
508
+ reason: typeof event.error === "string" ? event.error : String(event.error),
509
+ timestamp: event.timestamp,
510
+ startedAt: event.startedAt,
511
+ },
512
+ timestamp: new Date().toISOString(),
513
+ });
514
+ };
515
+ this.scannerModule.on("scanner:run:skip", this.onScannerRunSkip);
516
+ this.scannerModule.on("scanner:run:error", this.onScannerRunError);
517
+ this.signalConsumerDone = this.runSignalConsumer();
518
+ logger.info("Runtime.start() invoking scannerModule.start()");
519
+ await this.scannerModule.start();
520
+ logger.info("Runtime.start() scannerModule.start() returned");
521
+ this.started = true;
522
+ }
523
+ catch (err) {
524
+ // Recording-only cleanup (telemetry must not leak on a failed boot); the error rethrows.
525
+ await this.disposeRecording();
526
+ throw err;
527
+ }
470
528
  this.bus.emit("tracking.runtime_started", {
471
529
  address: this.strategyConfig.address,
472
530
  data: {
@@ -479,49 +537,6 @@ export class Runtime {
479
537
  },
480
538
  timestamp: new Date().toISOString(),
481
539
  });
482
- this.emitRuntimeBootLogs(hasDsl);
483
- }
484
- /** Joins to spans by position.id at query time, replacing trace-tree linkage. */
485
- wireDslOtelLogs() {
486
- const DSL_LOG_EVENTS = [
487
- "dsl.created",
488
- "dsl.tier_advanced",
489
- "dsl.phase_changed",
490
- "dsl.sl_updated",
491
- "dsl.close_pending",
492
- "dsl.closed",
493
- "dsl.deleted",
494
- ];
495
- const on = (name, handler) => {
496
- this.bus.on(name, handler);
497
- };
498
- for (const name of DSL_LOG_EVENTS) {
499
- on(name, (p) => {
500
- this.otel.emitLog({ eventName: name, attrs: dslAttrs(p), body: p });
501
- });
502
- }
503
- }
504
- emitRuntimeBootLogs(hasDsl) {
505
- this.otel.emitLog({
506
- eventName: "runtime.boot",
507
- attrs: { "runtime.boot_id": this.runtimeBootId },
508
- });
509
- // Body carries the full config; attrs are only the summary set kept queryable
510
- // without scanning the body (per remediation-plan D6).
511
- this.otel.emitLog({
512
- eventName: "runtime.config_loaded",
513
- body: this.config,
514
- attrs: {
515
- "config.recipe_name": this.config.name,
516
- // `config.recipe_version` is the recipe author's `version:` field;
517
- // absent when the YAML omits it. `runtime.version` (build version) is
518
- // on ResourceAttributes via the OTEL identity envelope.
519
- "config.recipe_version": this.config.version != null ? String(this.config.version) : null,
520
- "config.dsl_enabled": hasDsl,
521
- "config.scanner_count": this.config.scanners?.length ?? 0,
522
- "config.action_count": this.config.actions?.length ?? 0,
523
- },
524
- });
525
540
  }
526
541
  /**
527
542
  * Cancel exchange SLs and archive DSL state for the strategy, then log removal.
@@ -566,7 +581,7 @@ export class Runtime {
566
581
  runtimeId: this.options.runtimeId ?? "",
567
582
  runtimeVersion: VERSION,
568
583
  runtimeBootId: this.runtimeBootId,
569
- }, () => otelContext.run({}, async () => {
584
+ }, async () => {
570
585
  try {
571
586
  console.info("[senpi-runtime] [ingest-trace] consumer dequeue → runActionsForSignal", JSON.stringify({
572
587
  address: queuedItem.result.address,
@@ -579,24 +594,6 @@ export class Runtime {
579
594
  usedSlotsByStrategy,
580
595
  address: queuedItem.result.address,
581
596
  });
582
- const slots = payload.strategySlots?.[queuedItem.result.address];
583
- if (slots && slots.available <= 0) {
584
- // D9: one log per slot-exhaustion, not one per signal — same query
585
- // power (signal.assets[] preserves cardinality), 20× fewer rows.
586
- const signalList = queuedItem.result.signals ?? [];
587
- this.otel.emitLog({
588
- eventName: "runtime.slot_unavailable",
589
- severity: "warn",
590
- attrs: {
591
- "scanner.id": queuedItem.result.scannerId,
592
- "signal.assets": signalList.map((s) => s.asset),
593
- "signal.directions": signalList.map((s) => s.direction),
594
- "signal.count": signalList.length,
595
- "slots_in_use": slots.used,
596
- "slots_max": slots.total,
597
- },
598
- });
599
- }
600
597
  const hookEvent = {
601
598
  type: HOOK_EVENT.ON_SIGNAL_DETECTED,
602
599
  runtimeName: this.config.name,
@@ -615,18 +612,6 @@ export class Runtime {
615
612
  const message = err instanceof Error ? err.message : String(err);
616
613
  const stack = err instanceof Error ? err.stack : undefined;
617
614
  console.error("[senpi-runtime] signal processing error", message, stack ?? "");
618
- this.otel.emitLog({
619
- eventName: "runtime.task_error",
620
- severity: "error",
621
- attrs: {
622
- "task.name": "signal_consumer",
623
- "task.kind": "loop",
624
- "exception.type": err instanceof Error ? err.name : "Error",
625
- "exception.message": message,
626
- "exception.stacktrace": stack ?? "",
627
- "scanner.id": queuedItem.result.scannerId,
628
- },
629
- });
630
615
  this.bus.emit("signal_processing_error", {
631
616
  error: err,
632
617
  address: queuedItem.result.address,
@@ -634,7 +619,21 @@ export class Runtime {
634
619
  timestamp: new Date().toISOString(),
635
620
  });
636
621
  }
637
- }));
622
+ });
623
+ }
624
+ }
625
+ /**
626
+ * Dispose the recording subscriber, then flush + stop the batcher (best-effort). Idempotent;
627
+ * shared by stop() and the failed-start cleanup path in start().
628
+ */
629
+ async disposeRecording() {
630
+ if (this.recordingSubscriber) {
631
+ this.recordingSubscriber.dispose();
632
+ this.recordingSubscriber = null;
633
+ }
634
+ if (this.recordingBatcher) {
635
+ await this.recordingBatcher.shutdown().catch(() => { });
636
+ this.recordingBatcher = null;
638
637
  }
639
638
  }
640
639
  async stop() {
@@ -665,6 +664,14 @@ export class Runtime {
665
664
  this.scannerModule.off("scanner:run:complete", this.onScannerRunComplete);
666
665
  this.onScannerRunComplete = null;
667
666
  }
667
+ if (this.onScannerRunSkip != null) {
668
+ this.scannerModule.off("scanner:run:skip", this.onScannerRunSkip);
669
+ this.onScannerRunSkip = null;
670
+ }
671
+ if (this.onScannerRunError != null) {
672
+ this.scannerModule.off("scanner:run:error", this.onScannerRunError);
673
+ this.onScannerRunError = null;
674
+ }
668
675
  if (this.actionNotificationSubscriber != null) {
669
676
  this.actionNotificationSubscriber.dispose();
670
677
  this.actionNotificationSubscriber = null;
@@ -683,46 +690,14 @@ export class Runtime {
683
690
  await this.trackingHandler.dispose();
684
691
  this.trackingHandler = null;
685
692
  }
686
- await this.otel.shutdown();
693
+ // Telemetry recording teardown: stop the subscriber, then flush + stop the batcher (best-effort)
694
+ // so the just-recorded runtime_stopped row ships before the process exits.
695
+ await this.disposeRecording();
687
696
  if (this.bus.removeAllListeners) {
688
697
  this.bus.removeAllListeners();
689
698
  }
690
699
  }
691
700
  }
692
- /**
693
- * Pure attr extractor for dsl.* log events.
694
- *
695
- * C12 (DSL position.id plumbing): the DSL state currently exposes
696
- * `__correlationId` (DSL-internal lifecycle id) and `backendDslPositionId`
697
- * (backend-side id, optional). The runtime's minted `position.id` is not
698
- * threaded into the DSL state yet — that's the position-state-machine PR.
699
- * Until then, dsl.* logs join by `dsl.correlation_id` / `dsl.backend_position_id`
700
- * + `dsl.asset` + `strategy.address` (the latter is on ResourceAttributes).
701
- * When the state machine lands, add `position.id` here.
702
- */
703
- function dslAttrs(payload) {
704
- return {
705
- "dsl.asset": payload.asset,
706
- "dsl.dex": payload.dex,
707
- "dsl.phase": payload.phase,
708
- "dsl.tier_index": payload.tier ?? payload.tierIndex,
709
- "dsl.floor_price": payload.newFloorPrice ?? payload.floorPrice,
710
- "dsl.current_price": payload.lastPrice ?? payload.currentPrice,
711
- "dsl.high_water_price": payload.highWaterPrice,
712
- "dsl.high_water_roe": payload.highWaterRoe,
713
- "dsl.current_roe": payload.currentROE,
714
- "dsl.trigger_reason": payload.triggerReason,
715
- "dsl.close_reason": payload.closeReason,
716
- "dsl.locked_profit_pct": payload.lockedProfitPct,
717
- "dsl.new_sl_price": payload.newSlPrice ?? payload.slPrice,
718
- "dsl.direction": payload.direction,
719
- "dsl.entry_price": payload.entryPrice,
720
- "dsl.leverage": payload.leverage,
721
- "dsl.margin": payload.margin,
722
- "dsl.correlation_id": payload.__correlationId,
723
- "dsl.backend_position_id": payload.backendDslPositionId,
724
- };
725
- }
726
701
  /**
727
702
  * Coerce the (already envelope-unwrapped) MCP `audit_query` payload to the
728
703
  * public {@link AuditQueryResult} shape. Missing or malformed fields are