@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.
- package/dist/actions/close-position/close-position.action.d.ts +0 -16
- package/dist/actions/close-position/close-position.action.d.ts.map +1 -1
- package/dist/actions/close-position/close-position.action.js +38 -299
- package/dist/actions/close-position/close-position.action.js.map +1 -1
- package/dist/actions/decision-engine.d.ts +0 -2
- package/dist/actions/decision-engine.d.ts.map +1 -1
- package/dist/actions/decision-engine.js.map +1 -1
- package/dist/actions/llm-decision/factory.d.ts +0 -3
- package/dist/actions/llm-decision/factory.d.ts.map +1 -1
- package/dist/actions/llm-decision/factory.js +0 -1
- package/dist/actions/llm-decision/factory.js.map +1 -1
- package/dist/actions/llm-decision/openclaw-llm-decision.d.ts +0 -13
- package/dist/actions/llm-decision/openclaw-llm-decision.d.ts.map +1 -1
- package/dist/actions/llm-decision/openclaw-llm-decision.js +68 -195
- package/dist/actions/llm-decision/openclaw-llm-decision.js.map +1 -1
- package/dist/actions/open-position/open-position.action.d.ts +0 -12
- package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
- package/dist/actions/open-position/open-position.action.js +123 -346
- package/dist/actions/open-position/open-position.action.js.map +1 -1
- package/dist/actions/position-tracker/position-tracker.action.d.ts +0 -6
- package/dist/actions/position-tracker/position-tracker.action.d.ts.map +1 -1
- package/dist/actions/position-tracker/position-tracker.action.js +0 -48
- package/dist/actions/position-tracker/position-tracker.action.js.map +1 -1
- package/dist/dsl/events/handlers.d.ts.map +1 -1
- package/dist/dsl/events/handlers.js +4 -0
- package/dist/dsl/events/handlers.js.map +1 -1
- package/dist/recording/emitter.d.ts +21 -0
- package/dist/recording/emitter.d.ts.map +1 -0
- package/dist/recording/emitter.js +2 -0
- package/dist/recording/emitter.js.map +1 -0
- package/dist/recording/identity.d.ts +34 -0
- package/dist/recording/identity.d.ts.map +1 -0
- package/dist/recording/identity.js +2 -0
- package/dist/recording/identity.js.map +1 -0
- package/dist/recording/index.d.ts +11 -0
- package/dist/recording/index.d.ts.map +1 -0
- package/dist/recording/index.js +2 -0
- package/dist/recording/index.js.map +1 -0
- package/dist/recording/noop-emitter.d.ts +17 -0
- package/dist/recording/noop-emitter.d.ts.map +1 -0
- package/dist/recording/noop-emitter.js +16 -0
- package/dist/recording/noop-emitter.js.map +1 -0
- package/dist/recording/otel/attributes.d.ts +35 -0
- package/dist/recording/otel/attributes.d.ts.map +1 -0
- package/dist/recording/otel/attributes.js +131 -0
- package/dist/recording/otel/attributes.js.map +1 -0
- package/dist/recording/otel/constants.d.ts +81 -0
- package/dist/recording/otel/constants.d.ts.map +1 -0
- package/dist/recording/otel/constants.js +96 -0
- package/dist/recording/otel/constants.js.map +1 -0
- package/dist/recording/otel/ids.d.ts +14 -0
- package/dist/recording/otel/ids.d.ts.map +1 -0
- package/dist/recording/otel/ids.js +12 -0
- package/dist/recording/otel/ids.js.map +1 -0
- package/dist/recording/otel/index.d.ts +13 -0
- package/dist/recording/otel/index.d.ts.map +1 -0
- package/dist/recording/otel/index.js +9 -0
- package/dist/recording/otel/index.js.map +1 -0
- package/dist/recording/otel/mappers.d.ts +42 -0
- package/dist/recording/otel/mappers.d.ts.map +1 -0
- package/dist/recording/otel/mappers.js +337 -0
- package/dist/recording/otel/mappers.js.map +1 -0
- package/dist/recording/otel/otel-emitter.d.ts +37 -0
- package/dist/recording/otel/otel-emitter.d.ts.map +1 -0
- package/dist/recording/otel/otel-emitter.js +75 -0
- package/dist/recording/otel/otel-emitter.js.map +1 -0
- package/dist/recording/otel/redaction.d.ts +31 -0
- package/dist/recording/otel/redaction.d.ts.map +1 -0
- package/dist/recording/otel/redaction.js +40 -0
- package/dist/recording/otel/redaction.js.map +1 -0
- package/dist/recording/records.d.ts +327 -0
- package/dist/recording/records.d.ts.map +1 -0
- package/dist/recording/records.js +14 -0
- package/dist/recording/records.js.map +1 -0
- package/dist/recording/transport/batcher.d.ts +59 -0
- package/dist/recording/transport/batcher.d.ts.map +1 -0
- package/dist/recording/transport/batcher.js +234 -0
- package/dist/recording/transport/batcher.js.map +1 -0
- package/dist/recording/transport/client.d.ts +41 -0
- package/dist/recording/transport/client.d.ts.map +1 -0
- package/dist/recording/transport/client.js +69 -0
- package/dist/recording/transport/client.js.map +1 -0
- package/dist/recording/transport/config.d.ts +53 -0
- package/dist/recording/transport/config.d.ts.map +1 -0
- package/dist/recording/transport/config.js +41 -0
- package/dist/recording/transport/config.js.map +1 -0
- package/dist/recording/transport/index.d.ts +16 -0
- package/dist/recording/transport/index.d.ts.map +1 -0
- package/dist/recording/transport/index.js +12 -0
- package/dist/recording/transport/index.js.map +1 -0
- package/dist/recording/transport/rows.d.ts +88 -0
- package/dist/recording/transport/rows.d.ts.map +1 -0
- package/dist/recording/transport/rows.js +15 -0
- package/dist/recording/transport/rows.js.map +1 -0
- package/dist/recording/wiring/emitter-factory.d.ts +39 -0
- package/dist/recording/wiring/emitter-factory.d.ts.map +1 -0
- package/dist/recording/wiring/emitter-factory.js +62 -0
- package/dist/recording/wiring/emitter-factory.js.map +1 -0
- package/dist/recording/wiring/identity.d.ts +27 -0
- package/dist/recording/wiring/identity.d.ts.map +1 -0
- package/dist/recording/wiring/identity.js +51 -0
- package/dist/recording/wiring/identity.js.map +1 -0
- package/dist/recording/wiring/index.d.ts +12 -0
- package/dist/recording/wiring/index.d.ts.map +1 -0
- package/dist/recording/wiring/index.js +9 -0
- package/dist/recording/wiring/index.js.map +1 -0
- package/dist/recording/wiring/record-builders.d.ts +68 -0
- package/dist/recording/wiring/record-builders.d.ts.map +1 -0
- package/dist/recording/wiring/record-builders.js +441 -0
- package/dist/recording/wiring/record-builders.js.map +1 -0
- package/dist/recording/wiring/subscriber.d.ts +21 -0
- package/dist/recording/wiring/subscriber.d.ts.map +1 -0
- package/dist/recording/wiring/subscriber.js +171 -0
- package/dist/recording/wiring/subscriber.js.map +1 -0
- package/dist/recording/wiring/values.d.ts +19 -0
- package/dist/recording/wiring/values.d.ts.map +1 -0
- package/dist/recording/wiring/values.js +49 -0
- package/dist/recording/wiring/values.js.map +1 -0
- package/dist/runtime/create-actions.d.ts.map +1 -1
- package/dist/runtime/create-actions.js +0 -2
- package/dist/runtime/create-actions.js.map +1 -1
- package/dist/runtime/create-runtime-context.d.ts +0 -2
- package/dist/runtime/create-runtime-context.d.ts.map +1 -1
- package/dist/runtime/create-runtime-context.js +0 -1
- package/dist/runtime/create-runtime-context.js.map +1 -1
- package/dist/runtime/create-scanner-module.d.ts +1 -1
- package/dist/runtime/create-scanner-module.d.ts.map +1 -1
- package/dist/runtime/create-scanner-module.js +1 -2
- package/dist/runtime/create-scanner-module.js.map +1 -1
- package/dist/runtime/load-runtime-config.d.ts +9 -0
- package/dist/runtime/load-runtime-config.d.ts.map +1 -1
- package/dist/runtime/load-runtime-config.js +41 -1
- package/dist/runtime/load-runtime-config.js.map +1 -1
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +3 -2
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/runtime-schema.d.ts +182 -24
- package/dist/runtime/runtime-schema.d.ts.map +1 -1
- package/dist/runtime/runtime-schema.js +17 -1
- package/dist/runtime/runtime-schema.js.map +1 -1
- package/dist/runtime/runtime.d.ts +15 -5
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +248 -273
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/scanners/engine/data-providers.d.ts +0 -2
- package/dist/scanners/engine/data-providers.d.ts.map +1 -1
- package/dist/scanners/engine/data-providers.js.map +1 -1
- package/dist/scanners/engine/engine.d.ts.map +1 -1
- package/dist/scanners/engine/engine.js +9 -68
- package/dist/scanners/engine/engine.js.map +1 -1
- package/dist/scanners/events.d.ts +6 -0
- package/dist/scanners/events.d.ts.map +1 -1
- package/dist/scanners/events.js.map +1 -1
- package/dist/scanners/runtime-module.d.ts +0 -2
- package/dist/scanners/runtime-module.d.ts.map +1 -1
- package/dist/scanners/runtime-module.js +9 -2
- package/dist/scanners/runtime-module.js.map +1 -1
- package/dist/senpi/client.d.ts +2 -2
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +20 -120
- package/dist/senpi/client.js.map +1 -1
- package/dist/senpi/types.d.ts +1 -68
- package/dist/senpi/types.d.ts.map +1 -1
- package/dist/strategy/strategy-state.d.ts +1 -1
- package/dist/strategy/strategy-state.d.ts.map +1 -1
- package/dist/tracking/spans/plugin-span-tracker.d.ts.map +1 -1
- package/dist/tracking/spans/plugin-span-tracker.js +24 -4
- package/dist/tracking/spans/plugin-span-tracker.js.map +1 -1
- package/dist/tracking/spans/redact-pii.d.ts +0 -10
- package/dist/tracking/spans/redact-pii.d.ts.map +1 -1
- package/dist/tracking/spans/redact-pii.js +2 -80
- package/dist/tracking/spans/redact-pii.js.map +1 -1
- package/dist/types/runtime.d.ts +0 -3
- package/dist/types/runtime.d.ts.map +1 -1
- package/dist/types/strategy.d.ts +2 -2
- package/dist/types/strategy.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/actions/otel-helpers.d.ts +0 -65
- package/dist/actions/otel-helpers.d.ts.map +0 -1
- package/dist/actions/otel-helpers.js +0 -122
- package/dist/actions/otel-helpers.js.map +0 -1
- package/dist/otel/context.d.ts +0 -24
- package/dist/otel/context.d.ts.map +0 -1
- package/dist/otel/context.js +0 -29
- package/dist/otel/context.js.map +0 -1
- package/dist/otel/emitter.d.ts +0 -73
- package/dist/otel/emitter.d.ts.map +0 -1
- package/dist/otel/emitter.js +0 -248
- package/dist/otel/emitter.js.map +0 -1
- package/dist/otel/encode.d.ts +0 -48
- package/dist/otel/encode.d.ts.map +0 -1
- package/dist/otel/encode.js +0 -100
- package/dist/otel/encode.js.map +0 -1
- package/dist/otel/index.d.ts +0 -13
- package/dist/otel/index.d.ts.map +0 -1
- package/dist/otel/index.js +0 -10
- package/dist/otel/index.js.map +0 -1
- package/dist/otel/transport.d.ts +0 -57
- package/dist/otel/transport.d.ts.map +0 -1
- package/dist/otel/transport.js +0 -208
- package/dist/otel/transport.js.map +0 -1
- package/dist/otel/types.d.ts +0 -62
- package/dist/otel/types.d.ts.map +0 -1
- package/dist/otel/types.js +0 -2
- package/dist/otel/types.js.map +0 -1
package/dist/runtime/runtime.js
CHANGED
|
@@ -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 {
|
|
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
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
400
|
+
senpi,
|
|
398
401
|
stateManager: this.stateManager,
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
448
|
-
this.
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|