@senpi/runtime 3.0.81 → 3.0.83
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/decision-engine.d.ts +16 -0
- package/dist/actions/decision-engine.d.ts.map +1 -1
- package/dist/actions/decision-engine.js +137 -1
- package/dist/actions/decision-engine.js.map +1 -1
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +33 -0
- package/dist/cli/guide-commands.js.map +1 -1
- package/dist/cli/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +12 -0
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/cli/update-command.d.ts +70 -0
- package/dist/cli/update-command.d.ts.map +1 -0
- package/dist/cli/update-command.js +132 -0
- package/dist/cli/update-command.js.map +1 -0
- package/dist/dsl/plugin/index.d.ts +35 -1
- package/dist/dsl/plugin/index.d.ts.map +1 -1
- package/dist/dsl/plugin/index.js +62 -7
- package/dist/dsl/plugin/index.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +87 -6
- package/dist/index.js.map +1 -1
- package/dist/runtime/effective-config.d.ts +67 -0
- package/dist/runtime/effective-config.d.ts.map +1 -0
- package/dist/runtime/effective-config.js +324 -0
- package/dist/runtime/effective-config.js.map +1 -0
- package/dist/runtime/lifecycle-events.d.ts +19 -0
- package/dist/runtime/lifecycle-events.d.ts.map +1 -1
- package/dist/runtime/lifecycle-events.js +27 -0
- package/dist/runtime/lifecycle-events.js.map +1 -1
- package/dist/runtime/rebuild-errors.d.ts +26 -0
- package/dist/runtime/rebuild-errors.d.ts.map +1 -0
- package/dist/runtime/rebuild-errors.js +36 -0
- package/dist/runtime/rebuild-errors.js.map +1 -0
- package/dist/runtime/run.d.ts +10 -0
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +13 -1
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/runtime.d.ts +99 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +321 -127
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/update-apply-deps.d.ts +43 -0
- package/dist/runtime/update-apply-deps.d.ts.map +1 -0
- package/dist/runtime/update-apply-deps.js +62 -0
- package/dist/runtime/update-apply-deps.js.map +1 -0
- package/dist/runtime/update-handler.d.ts +158 -0
- package/dist/runtime/update-handler.d.ts.map +1 -0
- package/dist/runtime/update-handler.js +252 -0
- package/dist/runtime/update-handler.js.map +1 -0
- package/dist/runtime/update-plan.d.ts +60 -0
- package/dist/runtime/update-plan.d.ts.map +1 -0
- package/dist/runtime/update-plan.js +164 -0
- package/dist/runtime/update-plan.js.map +1 -0
- package/dist/runtime/update-register.d.ts +72 -0
- package/dist/runtime/update-register.d.ts.map +1 -0
- package/dist/runtime/update-register.js +146 -0
- package/dist/runtime/update-register.js.map +1 -0
- package/dist/runtime/update-report.d.ts +93 -0
- package/dist/runtime/update-report.d.ts.map +1 -0
- package/dist/runtime/update-report.js +133 -0
- package/dist/runtime/update-report.js.map +1 -0
- package/dist/skills-manager/manifest.d.ts.map +1 -1
- package/dist/skills-manager/manifest.js +1 -0
- package/dist/skills-manager/manifest.js.map +1 -1
- package/dist/skills-manager/skills-manifest.json +1 -0
- package/dist/utils/event-catalog.d.ts +26 -0
- package/dist/utils/event-catalog.d.ts.map +1 -1
- package/dist/utils/event-catalog.js +1 -0
- package/dist/utils/event-catalog.js.map +1 -1
- package/dist/widgets/errors.d.ts +3 -15
- package/dist/widgets/errors.d.ts.map +1 -1
- package/dist/widgets/errors.js +3 -20
- package/dist/widgets/errors.js.map +1 -1
- package/dist/widgets/index.d.ts +3 -10
- package/dist/widgets/index.d.ts.map +1 -1
- package/dist/widgets/index.js +3 -6
- package/dist/widgets/index.js.map +1 -1
- package/dist/widgets/registry.d.ts.map +1 -1
- package/dist/widgets/registry.js +10 -3
- package/dist/widgets/registry.js.map +1 -1
- package/dist/widgets/tool.d.ts +6 -9
- package/dist/widgets/tool.d.ts.map +1 -1
- package/dist/widgets/tool.js +37 -147
- package/dist/widgets/tool.js.map +1 -1
- package/dist/widgets/types.d.ts +20 -58
- package/dist/widgets/types.d.ts.map +1 -1
- package/dist/widgets/types.js +4 -4
- package/dist/widgets/widgets/fund-user-wallet.d.ts +4 -5
- package/dist/widgets/widgets/fund-user-wallet.d.ts.map +1 -1
- package/dist/widgets/widgets/fund-user-wallet.js +27 -67
- package/dist/widgets/widgets/fund-user-wallet.js.map +1 -1
- package/openclaw.plugin.json +1 -4
- package/package.json +2 -3
- package/dist/widgets/strategy-lookup.d.ts +0 -39
- package/dist/widgets/strategy-lookup.d.ts.map +0 -1
- package/dist/widgets/strategy-lookup.js +0 -70
- package/dist/widgets/strategy-lookup.js.map +0 -1
- package/dist/widgets/widgets/fund-strategy-wallet.d.ts +0 -13
- package/dist/widgets/widgets/fund-strategy-wallet.d.ts.map +0 -1
- package/dist/widgets/widgets/fund-strategy-wallet.js +0 -139
- package/dist/widgets/widgets/fund-strategy-wallet.js.map +0 -1
- package/skills/senpi-widgets/SKILL.md +0 -167
|
@@ -25,7 +25,8 @@ export interface RuntimeOptions {
|
|
|
25
25
|
unresolvedConfig?: Record<string, unknown>;
|
|
26
26
|
}
|
|
27
27
|
export declare class Runtime {
|
|
28
|
-
|
|
28
|
+
/** Not readonly: {@link Runtime.rebuildComponents} swaps it, which is the whole point. */
|
|
29
|
+
private config;
|
|
29
30
|
private readonly options;
|
|
30
31
|
private bus;
|
|
31
32
|
private hookSystem;
|
|
@@ -60,6 +61,8 @@ export declare class Runtime {
|
|
|
60
61
|
private actionRuntimeModule;
|
|
61
62
|
/** Stored so we can call off() in stop() and avoid retaining Runtime via scanner EventEmitter. */
|
|
62
63
|
private onScannerRunComplete;
|
|
64
|
+
/** Set at the end of start()'s dependency phase; read by every build and rebuild. */
|
|
65
|
+
private componentDeps;
|
|
63
66
|
/** Cached strategy id resolved from the runtime wallet on first {@link auditQuery} call. */
|
|
64
67
|
private cachedStrategyId;
|
|
65
68
|
/** In-flight resolution promise; ensures concurrent first-burst callers reuse one `listStrategies`. */
|
|
@@ -184,6 +187,101 @@ export declare class Runtime {
|
|
|
184
187
|
* Cancel exchange SLs and archive DSL state for the strategy, then log removal.
|
|
185
188
|
*/
|
|
186
189
|
/** @returns Count of active DSL positions archived during teardown. */
|
|
190
|
+
/**
|
|
191
|
+
* Build every component derived from the recipe: scanners, actions, the DSL plugin, and the
|
|
192
|
+
* health surface that holds references to all three.
|
|
193
|
+
*
|
|
194
|
+
* Called by `start()` and again by {@link rebuildComponents}. Deliberately does NOT create the
|
|
195
|
+
* reconcile gate, launch the boot-reconcile loop, or call `initRisk` — those are once-per-runtime
|
|
196
|
+
* and re-running them would re-block trading on an already-converged runtime, duplicate a
|
|
197
|
+
* detached loop, and double-subscribe the bus respectively.
|
|
198
|
+
*/
|
|
199
|
+
private buildComponents;
|
|
200
|
+
/**
|
|
201
|
+
* Phase one of a build: everything the recipe determines, constructed but NOT installed on `this`
|
|
202
|
+
* and NOT started.
|
|
203
|
+
*
|
|
204
|
+
* The split exists so a rebuild can do all of its fallible work while the OLD components are
|
|
205
|
+
* still running. A recipe that names a scanner which will not construct, or an action class that
|
|
206
|
+
* does not exist, throws here — with the live runtime untouched, still scanning and still holding
|
|
207
|
+
* its stops. The alternative (the previous shape) tore everything down first and then discovered
|
|
208
|
+
* the new config was unbuildable, leaving a funded wallet with an open book, no scanners and no
|
|
209
|
+
* exit engine.
|
|
210
|
+
*
|
|
211
|
+
* Nothing in here may subscribe to the bus, start a timer, or touch disk. That is what makes it
|
|
212
|
+
* safe to run twice over — once for the attempt, once more for a rollback. `DslPlugin.create` is
|
|
213
|
+
* deliberately NOT called here for exactly that reason: it subscribes position handlers and
|
|
214
|
+
* starts the monitor loop, so pre-building it would put two plugins on one bus, both ticking the
|
|
215
|
+
* same positions. Its config is parsed here instead, which is where a bad preset actually throws.
|
|
216
|
+
*/
|
|
217
|
+
private constructComponents;
|
|
218
|
+
/**
|
|
219
|
+
* Phase two of a build: install the constructed components on `this`, then create the one
|
|
220
|
+
* component that cannot be constructed without side effects.
|
|
221
|
+
*
|
|
222
|
+
* Everything fallible that COULD be done earlier already has been, in
|
|
223
|
+
* {@link constructComponents}. What remains is assignment — which cannot throw — plus
|
|
224
|
+
* `DslPlugin.create`, whose config this build already parsed and whose state manager, senpi
|
|
225
|
+
* client and strategy-state map are all passed in (each of those resolvers returns immediately
|
|
226
|
+
* when supplied). That is why the teardown window in {@link rebuildComponents} is narrow enough
|
|
227
|
+
* to be worth taking, and why its rollback is a backstop rather than the plan.
|
|
228
|
+
*/
|
|
229
|
+
private commitComponents;
|
|
230
|
+
/**
|
|
231
|
+
* Register the scan-complete listener on the CURRENT scanner module.
|
|
232
|
+
*
|
|
233
|
+
* Separate from {@link buildComponents} because the listener is held against a specific module
|
|
234
|
+
* instance: a rebuild replaces the module, so the old registration goes with it and a fresh one
|
|
235
|
+
* has to be made against the new one.
|
|
236
|
+
*/
|
|
237
|
+
private wireScannerRunListener;
|
|
238
|
+
/**
|
|
239
|
+
* Swap this runtime onto a new recipe without restarting it or touching its state.
|
|
240
|
+
*
|
|
241
|
+
* Teardown is the reverse of the build, and both halves matter. The scan listener is released
|
|
242
|
+
* before the module it is registered on; `scannerModule.stop()` clears its timers and awaits
|
|
243
|
+
* in-flight runs; `dslPlugin.stop()` is AWAITED, because it releases its bus subscriptions and
|
|
244
|
+
* drains the tick that may still be mid-flight at the venue — without that the outgoing monitor
|
|
245
|
+
* keeps writing state while the incoming one is already ticking the same positions, and the two
|
|
246
|
+
* hold independent StateManagers so nothing else serialises them.
|
|
247
|
+
*
|
|
248
|
+
* State on disk is untouched throughout: DSL position files, scanner stores and action history
|
|
249
|
+
* all survive, which is the entire reason an update is worth having over a delete-and-recreate.
|
|
250
|
+
*/
|
|
251
|
+
rebuildComponents(config: RuntimeConfig): Promise<void>;
|
|
252
|
+
/**
|
|
253
|
+
* Install the freshly built components, serialized against the trade loop.
|
|
254
|
+
*
|
|
255
|
+
* The signal consumer runs every action pass inside
|
|
256
|
+
* `reconcileMutex.withKeyLock("reconcile", () => this.runActionsForSignal(...))`, reading
|
|
257
|
+
* `runActionsForSignal` at call time from inside the lock. Taking the same lock here means a
|
|
258
|
+
* signal already in flight finishes against the OLD actions before the swap happens, and the next
|
|
259
|
+
* one picks up the new ones — no pass can straddle the change.
|
|
260
|
+
*
|
|
261
|
+
* The lock is scoped to the SWAP and deliberately NOT to the teardown that precedes it.
|
|
262
|
+
* `teardownComponentsForRebuild` awaits the DSL plugin's in-flight tick, and that tick takes this
|
|
263
|
+
* very lock for its periodic reconcile (`runReconcile` is handed `reconcileSerialize`). Holding
|
|
264
|
+
* the lock across the drain would deadlock the runtime — permanently, with an open book and
|
|
265
|
+
* nothing managing it — because `KeyedMutex` is a plain FIFO and grants no reentrancy.
|
|
266
|
+
*
|
|
267
|
+
* Creating the DSL plugin inside the lock is safe for the mirror-image reason: `create()` starts
|
|
268
|
+
* the monitor loop but does not await its first tick, so that tick queues behind this lock
|
|
269
|
+
* instead of blocking the call that holds it.
|
|
270
|
+
*/
|
|
271
|
+
private swapInComponents;
|
|
272
|
+
/**
|
|
273
|
+
* Drop the scan listener, stop the scanner module, stop and release the DSL plugin.
|
|
274
|
+
*
|
|
275
|
+
* Shared by the rebuild and its rollback so the two cannot drift — the rollback runs it a second
|
|
276
|
+
* time to clear whatever the failed commit had already installed, and every step is idempotent
|
|
277
|
+
* for exactly that reason.
|
|
278
|
+
*
|
|
279
|
+
* Order matters: the listener comes off before the module it is registered on;
|
|
280
|
+
* `scannerModule.stop()` clears its timers and awaits in-flight runs; `dslPlugin.stop()` is
|
|
281
|
+
* AWAITED, because it releases its bus subscriptions and drains the tick that may still be
|
|
282
|
+
* mid-flight at the venue.
|
|
283
|
+
*/
|
|
284
|
+
private teardownComponentsForRebuild;
|
|
187
285
|
removeStrategy(address: string): Promise<number>;
|
|
188
286
|
/** Tear down DSL for the runtime strategy before deleting the runtime (gateway / CLI). */
|
|
189
287
|
teardownDslForRuntimeDeletion(): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime/runtime.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAK/E,OAAO,KAAK,EACV,4BAA4B,EAC5B,2BAA2B,EAC5B,MAAM,0CAA0C,CAAC;AAClD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAE3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime/runtime.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime/runtime.js
CHANGED
|
@@ -26,6 +26,8 @@ import { validateDslPositionTracking } from "./validate-dsl-position-tracking.js
|
|
|
26
26
|
import { ScannerRegistry } from "./scanner-registry.js";
|
|
27
27
|
import { ActionRegistry } from "./action-registry.js";
|
|
28
28
|
import { initRisk } from "../risk/index.js";
|
|
29
|
+
import { parseDslPluginConfig } from "../dsl/config/parse-plugin-config.js";
|
|
30
|
+
import { RuntimeRebuildRollbackFailedError } from "./rebuild-errors.js";
|
|
29
31
|
import { createHealthStateModule, } from "../health/index.js";
|
|
30
32
|
import { DslPlugin as DslPluginClass } from "../dsl/plugin/index.js";
|
|
31
33
|
import { HOOK_EVENT } from "../types/hooks.js";
|
|
@@ -59,6 +61,7 @@ const SIGNAL_QUEUE_BACKPRESSURE_THRESHOLD = 10;
|
|
|
59
61
|
/** Default liveness-heartbeat window (~15 min) when `health.window_seconds` is unset. */
|
|
60
62
|
const DEFAULT_LIVENESS_WINDOW_SECONDS = 900;
|
|
61
63
|
export class Runtime {
|
|
64
|
+
/** Not readonly: {@link Runtime.rebuildComponents} swaps it, which is the whole point. */
|
|
62
65
|
config;
|
|
63
66
|
options;
|
|
64
67
|
bus;
|
|
@@ -95,6 +98,8 @@ export class Runtime {
|
|
|
95
98
|
actionRuntimeModule;
|
|
96
99
|
/** Stored so we can call off() in stop() and avoid retaining Runtime via scanner EventEmitter. */
|
|
97
100
|
onScannerRunComplete = null;
|
|
101
|
+
/** Set at the end of start()'s dependency phase; read by every build and rebuild. */
|
|
102
|
+
componentDeps;
|
|
98
103
|
/** Cached strategy id resolved from the runtime wallet on first {@link auditQuery} call. */
|
|
99
104
|
cachedStrategyId = null;
|
|
100
105
|
/** In-flight resolution promise; ensures concurrent first-burst callers reuse one `listStrategies`. */
|
|
@@ -436,50 +441,6 @@ export class Runtime {
|
|
|
436
441
|
})
|
|
437
442
|
: createRiskGuardStub();
|
|
438
443
|
logger.info("Runtime.start() creating scanner module");
|
|
439
|
-
const layout = buildStateLayout(stateDir);
|
|
440
|
-
// Augment caller-supplied providers with StateManager-backed DSL state access
|
|
441
|
-
const augmentedProviders = {
|
|
442
|
-
...scannerProviders,
|
|
443
|
-
getDslStatesForStrategy: scannerProviders?.getDslStatesForStrategy
|
|
444
|
-
?? (async (address) => this.stateManager.listDslStates(address)),
|
|
445
|
-
};
|
|
446
|
-
this.scannerModule = createScannerModuleFromRuntimeConfig(this.config, layout.scannerRoot, augmentedProviders, undefined, buildIdentityEnvelope);
|
|
447
|
-
logger.info("Runtime.start() scanner module created, calling start()");
|
|
448
|
-
// Pre-load max leverage cache (non-blocking — log warning on failure)
|
|
449
|
-
const maxLeverageCache = new MaxLeverageCache(senpi);
|
|
450
|
-
try {
|
|
451
|
-
await maxLeverageCache.load();
|
|
452
|
-
}
|
|
453
|
-
catch (err) {
|
|
454
|
-
logger.warn("failed to pre-load max leverage cache, will retry on first use", { err });
|
|
455
|
-
}
|
|
456
|
-
// Build RuntimeContext once per trading recipe and reuse for all actions.
|
|
457
|
-
const runtimeContext = createRuntimeContext(this.strategyConfig, {
|
|
458
|
-
senpi,
|
|
459
|
-
hookSystem: this.hookSystem,
|
|
460
|
-
llmDecision,
|
|
461
|
-
contextBuilder,
|
|
462
|
-
riskGuard,
|
|
463
|
-
notifications,
|
|
464
|
-
scannerModule: this.scannerModule,
|
|
465
|
-
maxLeverageCache,
|
|
466
|
-
positionIdRegistry: this.positionIdRegistry,
|
|
467
|
-
notificationLabel,
|
|
468
|
-
});
|
|
469
|
-
this.actionRuntimeModule = new ActionRuntimeModule({ stateDir: layout.actionRoot });
|
|
470
|
-
const { runActionsForSignal } = await createActions(this.config, {
|
|
471
|
-
bus: this.bus,
|
|
472
|
-
senpi,
|
|
473
|
-
stateManager: this.stateManager,
|
|
474
|
-
hookSystem: this.hookSystem,
|
|
475
|
-
llmDecision,
|
|
476
|
-
contextBuilder,
|
|
477
|
-
riskGuard,
|
|
478
|
-
notifications,
|
|
479
|
-
runtimeContext,
|
|
480
|
-
actionModule: this.actionRuntimeModule,
|
|
481
|
-
});
|
|
482
|
-
this.runActionsForSignal = runActionsForSignal;
|
|
483
444
|
// Create the reconcile gate BEFORE DslPlugin.create() so the monitor's
|
|
484
445
|
// first synchronous tick (fired inside create()) sees a gate that already exists.
|
|
485
446
|
this.reconcileGate = (() => {
|
|
@@ -496,25 +457,24 @@ export class Runtime {
|
|
|
496
457
|
})();
|
|
497
458
|
// Build strategyStates map once — shared by DslPlugin and the reconcile loop.
|
|
498
459
|
const strategyStatesMap = createStrategyStatesMap(this.strategyConfig, senpi);
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
460
|
+
// Everything a rebuild reuses. Config-derived values are deliberately absent: those are
|
|
461
|
+
// recomputed from the config each build, which is what makes a rebuild a rebuild.
|
|
462
|
+
this.componentDeps = {
|
|
463
|
+
senpi,
|
|
464
|
+
llmDecision,
|
|
465
|
+
contextBuilder,
|
|
466
|
+
riskGuard,
|
|
467
|
+
notifications,
|
|
468
|
+
notificationLabel,
|
|
469
|
+
stateDir,
|
|
470
|
+
scannerProviders,
|
|
471
|
+
buildIdentityEnvelope,
|
|
472
|
+
strategyStates: strategyStatesMap,
|
|
473
|
+
};
|
|
474
|
+
// Once per runtime, not once per build: a rebuild must not re-run it. (It is a no-op today,
|
|
475
|
+
// kept for backward compatibility, but the placement is the contract.)
|
|
476
|
+
initRisk(this.bus);
|
|
477
|
+
await this.buildComponents(this.config);
|
|
518
478
|
// Launch detached boot-reconcile loop (does NOT block start()).
|
|
519
479
|
// Last-resort guard: an unexpected throw here would otherwise become an
|
|
520
480
|
// unhandled rejection and silently kill the loop. Do NOT resolve the gate
|
|
@@ -523,70 +483,7 @@ export class Runtime {
|
|
|
523
483
|
void this.runBootReconcileLoop(strategyStatesMap).catch((err) => {
|
|
524
484
|
logger.error("[runtime][reconcile] boot reconcile loop crashed", { err });
|
|
525
485
|
});
|
|
526
|
-
|
|
527
|
-
this.healthStateModule = createHealthStateModule({
|
|
528
|
-
runtimeName: this.config.name,
|
|
529
|
-
stateDir,
|
|
530
|
-
startedAt: this.startedAt,
|
|
531
|
-
providers: {
|
|
532
|
-
scanners: this.scannerModule,
|
|
533
|
-
dsl: this.dslPlugin ?? createDisabledDslProvider(),
|
|
534
|
-
actions: this.actionRuntimeModule,
|
|
535
|
-
risk: riskBlock
|
|
536
|
-
? createRiskHealthProvider(riskGuard, strategyAddress)
|
|
537
|
-
: undefined,
|
|
538
|
-
},
|
|
539
|
-
});
|
|
540
|
-
this.onScannerRunComplete = (event) => {
|
|
541
|
-
// Per-scan-run id: stamped on the scanner_run emit and echoed through the pass so scan +
|
|
542
|
-
// pass land in one trace. Prefer the id minted at the scanner.run span so scan + pass
|
|
543
|
-
// share one senpi.run.id.
|
|
544
|
-
const runId = event.result.runId ?? randomUUID();
|
|
545
|
-
this.signalQueue.push({ result: event.result, enqueuedAt: Date.now(), runId });
|
|
546
|
-
// Edge-triggered backpressure alarm: depth rises one enqueue at a time, so `=== threshold`
|
|
547
|
-
// fires exactly once as it crosses up and re-arms only after the consumer drains back below.
|
|
548
|
-
if (this.signalQueue.length === SIGNAL_QUEUE_BACKPRESSURE_THRESHOLD) {
|
|
549
|
-
emitEvent(logger, () => buildQueueBackpressureEvent({
|
|
550
|
-
depth: this.signalQueue.length,
|
|
551
|
-
threshold: SIGNAL_QUEUE_BACKPRESSURE_THRESHOLD,
|
|
552
|
-
scannerId: event.result.scannerId,
|
|
553
|
-
address: this.strategyConfig.address,
|
|
554
|
-
}));
|
|
555
|
-
}
|
|
556
|
-
if (this.signalQueueWait) {
|
|
557
|
-
const w = this.signalQueueWait;
|
|
558
|
-
this.signalQueueWait = null;
|
|
559
|
-
w();
|
|
560
|
-
}
|
|
561
|
-
// Scanner-run summary payload (startedAt is data stamping for telemetry durations).
|
|
562
|
-
const scannerRunPayload = {
|
|
563
|
-
address: event.result.address,
|
|
564
|
-
data: {
|
|
565
|
-
...event.result,
|
|
566
|
-
startedAt: event.startedAt,
|
|
567
|
-
runId,
|
|
568
|
-
},
|
|
569
|
-
timestamp: new Date().toISOString(),
|
|
570
|
-
};
|
|
571
|
-
// First-class event log. The build runs inside emitEvent's guard — telemetry never
|
|
572
|
-
// disturbs the scan path.
|
|
573
|
-
emitEvent(logger, () => buildScannerSummaryEvent(scannerRunPayload, Date.now()));
|
|
574
|
-
// The once-per-scan record, emitted for empty runs too — the grain scanner.summary drops.
|
|
575
|
-
emitEvent(logger, () => buildScannerRunTickEvent(event, Date.now()));
|
|
576
|
-
recordScannerRun({ scannerId: event.result.scannerId, outcome: event.result.status });
|
|
577
|
-
// TODO: Market-scanner alerts are default-OFF and MUST NOT be delivered
|
|
578
|
-
// until per-user default-mute-list support exists (today we have mute-list-only
|
|
579
|
-
// settings, which can't honour a default-OFF notification). When that lands,
|
|
580
|
-
// forward `event.result.summary?.alerts` as a MARKET_SCANNER notification with:
|
|
581
|
-
// in-app: `🔔 <b>${n} market alert(s)</b><br>${message}<br>Signals only — no trade placed yet.`
|
|
582
|
-
// push: no title (the notification service resolves it), body `🔔 ${message}`
|
|
583
|
-
// Address-less by design (the scanner watches the market, not one strategy), so
|
|
584
|
-
// it opens a chat by category alone and the strategy wallet must not leak into
|
|
585
|
-
// the seed. Re-add per-(scannerId, message) dedupe at that time.
|
|
586
|
-
// Resolve this TODO when default mute list functionality is available.
|
|
587
|
-
};
|
|
588
|
-
this.scannerModule.on("scanner:run:complete", this.onScannerRunComplete);
|
|
589
|
-
this.signalConsumerDone = this.runSignalConsumer();
|
|
486
|
+
this.wireScannerRunListener();
|
|
590
487
|
logger.info("Runtime.start() invoking scannerModule.start()");
|
|
591
488
|
await this.scannerModule.start();
|
|
592
489
|
logger.info("Runtime.start() scannerModule.start() returned");
|
|
@@ -707,6 +604,303 @@ export class Runtime {
|
|
|
707
604
|
* Cancel exchange SLs and archive DSL state for the strategy, then log removal.
|
|
708
605
|
*/
|
|
709
606
|
/** @returns Count of active DSL positions archived during teardown. */
|
|
607
|
+
/**
|
|
608
|
+
* Build every component derived from the recipe: scanners, actions, the DSL plugin, and the
|
|
609
|
+
* health surface that holds references to all three.
|
|
610
|
+
*
|
|
611
|
+
* Called by `start()` and again by {@link rebuildComponents}. Deliberately does NOT create the
|
|
612
|
+
* reconcile gate, launch the boot-reconcile loop, or call `initRisk` — those are once-per-runtime
|
|
613
|
+
* and re-running them would re-block trading on an already-converged runtime, duplicate a
|
|
614
|
+
* detached loop, and double-subscribe the bus respectively.
|
|
615
|
+
*/
|
|
616
|
+
async buildComponents(config) {
|
|
617
|
+
const constructed = await this.constructComponents(config);
|
|
618
|
+
await this.commitComponents(config, constructed);
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Phase one of a build: everything the recipe determines, constructed but NOT installed on `this`
|
|
622
|
+
* and NOT started.
|
|
623
|
+
*
|
|
624
|
+
* The split exists so a rebuild can do all of its fallible work while the OLD components are
|
|
625
|
+
* still running. A recipe that names a scanner which will not construct, or an action class that
|
|
626
|
+
* does not exist, throws here — with the live runtime untouched, still scanning and still holding
|
|
627
|
+
* its stops. The alternative (the previous shape) tore everything down first and then discovered
|
|
628
|
+
* the new config was unbuildable, leaving a funded wallet with an open book, no scanners and no
|
|
629
|
+
* exit engine.
|
|
630
|
+
*
|
|
631
|
+
* Nothing in here may subscribe to the bus, start a timer, or touch disk. That is what makes it
|
|
632
|
+
* safe to run twice over — once for the attempt, once more for a rollback. `DslPlugin.create` is
|
|
633
|
+
* deliberately NOT called here for exactly that reason: it subscribes position handlers and
|
|
634
|
+
* starts the monitor loop, so pre-building it would put two plugins on one bus, both ticking the
|
|
635
|
+
* same positions. Its config is parsed here instead, which is where a bad preset actually throws.
|
|
636
|
+
*/
|
|
637
|
+
async constructComponents(config) {
|
|
638
|
+
const { senpi, llmDecision, contextBuilder, riskGuard, notifications, notificationLabel, stateDir, scannerProviders, buildIdentityEnvelope, } = this.componentDeps;
|
|
639
|
+
const exitConfig = mapRuntimeExitToDslConfig(config.exit);
|
|
640
|
+
const hasDsl = exitConfig.dsl_preset != null &&
|
|
641
|
+
typeof exitConfig.dsl_preset === "object" &&
|
|
642
|
+
!Array.isArray(exitConfig.dsl_preset);
|
|
643
|
+
// Parse the preset now rather than inside `DslPlugin.create`, so a malformed exit block is
|
|
644
|
+
// rejected while the old plugin is still holding its stops. The result is discarded — this is
|
|
645
|
+
// a validation, and `create` re-parses from the same input.
|
|
646
|
+
if (hasDsl)
|
|
647
|
+
parseDslPluginConfig(exitConfig);
|
|
648
|
+
const layout = buildStateLayout(stateDir);
|
|
649
|
+
// Augment caller-supplied providers with StateManager-backed DSL state access
|
|
650
|
+
const augmentedProviders = {
|
|
651
|
+
...scannerProviders,
|
|
652
|
+
getDslStatesForStrategy: scannerProviders?.getDslStatesForStrategy
|
|
653
|
+
?? (async (address) => this.stateManager.listDslStates(address)),
|
|
654
|
+
};
|
|
655
|
+
const scannerModule = createScannerModuleFromRuntimeConfig(config, layout.scannerRoot, augmentedProviders, undefined, buildIdentityEnvelope);
|
|
656
|
+
logger.info("Runtime.start() scanner module created, calling start()");
|
|
657
|
+
// Pre-load max leverage cache (non-blocking — log warning on failure)
|
|
658
|
+
const maxLeverageCache = new MaxLeverageCache(senpi);
|
|
659
|
+
try {
|
|
660
|
+
await maxLeverageCache.load();
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
logger.warn("failed to pre-load max leverage cache, will retry on first use", { err });
|
|
664
|
+
}
|
|
665
|
+
// Build RuntimeContext once per trading recipe and reuse for all actions.
|
|
666
|
+
const runtimeContext = createRuntimeContext(this.strategyConfig, {
|
|
667
|
+
senpi,
|
|
668
|
+
hookSystem: this.hookSystem,
|
|
669
|
+
llmDecision,
|
|
670
|
+
contextBuilder,
|
|
671
|
+
riskGuard,
|
|
672
|
+
notifications,
|
|
673
|
+
scannerModule,
|
|
674
|
+
maxLeverageCache,
|
|
675
|
+
positionIdRegistry: this.positionIdRegistry,
|
|
676
|
+
notificationLabel,
|
|
677
|
+
});
|
|
678
|
+
const actionRuntimeModule = new ActionRuntimeModule({ stateDir: layout.actionRoot });
|
|
679
|
+
const { runActionsForSignal } = await createActions(config, {
|
|
680
|
+
bus: this.bus,
|
|
681
|
+
senpi,
|
|
682
|
+
stateManager: this.stateManager,
|
|
683
|
+
hookSystem: this.hookSystem,
|
|
684
|
+
llmDecision,
|
|
685
|
+
contextBuilder,
|
|
686
|
+
riskGuard,
|
|
687
|
+
notifications,
|
|
688
|
+
runtimeContext,
|
|
689
|
+
actionModule: actionRuntimeModule,
|
|
690
|
+
});
|
|
691
|
+
return { scannerModule, actionRuntimeModule, runActionsForSignal, exitConfig, hasDsl };
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Phase two of a build: install the constructed components on `this`, then create the one
|
|
695
|
+
* component that cannot be constructed without side effects.
|
|
696
|
+
*
|
|
697
|
+
* Everything fallible that COULD be done earlier already has been, in
|
|
698
|
+
* {@link constructComponents}. What remains is assignment — which cannot throw — plus
|
|
699
|
+
* `DslPlugin.create`, whose config this build already parsed and whose state manager, senpi
|
|
700
|
+
* client and strategy-state map are all passed in (each of those resolvers returns immediately
|
|
701
|
+
* when supplied). That is why the teardown window in {@link rebuildComponents} is narrow enough
|
|
702
|
+
* to be worth taking, and why its rollback is a backstop rather than the plan.
|
|
703
|
+
*/
|
|
704
|
+
async commitComponents(config, built) {
|
|
705
|
+
const { senpi, riskGuard, notifications, stateDir, buildIdentityEnvelope, strategyStates: strategyStatesMap, } = this.componentDeps;
|
|
706
|
+
const riskBlock = config.risk;
|
|
707
|
+
const strategyAddress = this.strategyConfig.address;
|
|
708
|
+
this.scannerModule = built.scannerModule;
|
|
709
|
+
this.actionRuntimeModule = built.actionRuntimeModule;
|
|
710
|
+
this.runActionsForSignal = built.runActionsForSignal;
|
|
711
|
+
if (built.hasDsl) {
|
|
712
|
+
this.dslPlugin = await DslPluginClass.create({
|
|
713
|
+
config: built.exitConfig,
|
|
714
|
+
bus: this.bus,
|
|
715
|
+
stateDir,
|
|
716
|
+
stateManager: this.stateManager,
|
|
717
|
+
senpiClient: senpi,
|
|
718
|
+
strategyStates: strategyStatesMap,
|
|
719
|
+
notifications,
|
|
720
|
+
// Monitor runs the unified periodic reconcile (adopt/resize/gone), serialized
|
|
721
|
+
// against the trade loop on the same reconcileMutex key, and can resolve the boot gate.
|
|
722
|
+
reconcileSerialize: (fn) => this.reconcileMutex.withKeyLock("reconcile", fn),
|
|
723
|
+
reconcileGateResolve: () => this.resolveReconcileGate(),
|
|
724
|
+
identity: buildIdentityEnvelope,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
this.dslPlugin = null;
|
|
729
|
+
}
|
|
730
|
+
this.healthStateModule = createHealthStateModule({
|
|
731
|
+
runtimeName: config.name,
|
|
732
|
+
stateDir,
|
|
733
|
+
startedAt: this.startedAt ?? undefined,
|
|
734
|
+
providers: {
|
|
735
|
+
scanners: this.scannerModule,
|
|
736
|
+
dsl: this.dslPlugin ?? createDisabledDslProvider(),
|
|
737
|
+
actions: this.actionRuntimeModule,
|
|
738
|
+
risk: riskBlock
|
|
739
|
+
? createRiskHealthProvider(riskGuard, strategyAddress)
|
|
740
|
+
: undefined,
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Register the scan-complete listener on the CURRENT scanner module.
|
|
746
|
+
*
|
|
747
|
+
* Separate from {@link buildComponents} because the listener is held against a specific module
|
|
748
|
+
* instance: a rebuild replaces the module, so the old registration goes with it and a fresh one
|
|
749
|
+
* has to be made against the new one.
|
|
750
|
+
*/
|
|
751
|
+
wireScannerRunListener() {
|
|
752
|
+
const { senpi } = this.componentDeps;
|
|
753
|
+
this.onScannerRunComplete = (event) => {
|
|
754
|
+
// Per-scan-run id: stamped on the scanner_run emit and echoed through the pass so scan +
|
|
755
|
+
// pass land in one trace. Prefer the id minted at the scanner.run span so scan + pass
|
|
756
|
+
// share one senpi.run.id.
|
|
757
|
+
const runId = event.result.runId ?? randomUUID();
|
|
758
|
+
this.signalQueue.push({ result: event.result, enqueuedAt: Date.now(), runId });
|
|
759
|
+
// Edge-triggered backpressure alarm: depth rises one enqueue at a time, so `=== threshold`
|
|
760
|
+
// fires exactly once as it crosses up and re-arms only after the consumer drains back below.
|
|
761
|
+
if (this.signalQueue.length === SIGNAL_QUEUE_BACKPRESSURE_THRESHOLD) {
|
|
762
|
+
emitEvent(logger, () => buildQueueBackpressureEvent({
|
|
763
|
+
depth: this.signalQueue.length,
|
|
764
|
+
threshold: SIGNAL_QUEUE_BACKPRESSURE_THRESHOLD,
|
|
765
|
+
scannerId: event.result.scannerId,
|
|
766
|
+
address: this.strategyConfig.address,
|
|
767
|
+
}));
|
|
768
|
+
}
|
|
769
|
+
if (this.signalQueueWait) {
|
|
770
|
+
const w = this.signalQueueWait;
|
|
771
|
+
this.signalQueueWait = null;
|
|
772
|
+
w();
|
|
773
|
+
}
|
|
774
|
+
// Scanner-run summary payload (startedAt is data stamping for telemetry durations).
|
|
775
|
+
const scannerRunPayload = {
|
|
776
|
+
address: event.result.address,
|
|
777
|
+
data: {
|
|
778
|
+
...event.result,
|
|
779
|
+
startedAt: event.startedAt,
|
|
780
|
+
runId,
|
|
781
|
+
},
|
|
782
|
+
timestamp: new Date().toISOString(),
|
|
783
|
+
};
|
|
784
|
+
// First-class event log. The build runs inside emitEvent's guard — telemetry never
|
|
785
|
+
// disturbs the scan path.
|
|
786
|
+
emitEvent(logger, () => buildScannerSummaryEvent(scannerRunPayload, Date.now()));
|
|
787
|
+
// The once-per-scan record, emitted for empty runs too — the grain scanner.summary drops.
|
|
788
|
+
emitEvent(logger, () => buildScannerRunTickEvent(event, Date.now()));
|
|
789
|
+
recordScannerRun({ scannerId: event.result.scannerId, outcome: event.result.status });
|
|
790
|
+
// TODO: Market-scanner alerts are default-OFF and MUST NOT be delivered
|
|
791
|
+
// until per-user default-mute-list support exists (today we have mute-list-only
|
|
792
|
+
// settings, which can't honour a default-OFF notification). When that lands,
|
|
793
|
+
// forward `event.result.summary?.alerts` as a MARKET_SCANNER notification with:
|
|
794
|
+
// in-app: `🔔 <b>${n} market alert(s)</b><br>${message}<br>Signals only — no trade placed yet.`
|
|
795
|
+
// push: no title (the notification service resolves it), body `🔔 ${message}`
|
|
796
|
+
// Address-less by design (the scanner watches the market, not one strategy), so
|
|
797
|
+
// it opens a chat by category alone and the strategy wallet must not leak into
|
|
798
|
+
// the seed. Re-add per-(scannerId, message) dedupe at that time.
|
|
799
|
+
// Resolve this TODO when default mute list functionality is available.
|
|
800
|
+
};
|
|
801
|
+
this.scannerModule.on("scanner:run:complete", this.onScannerRunComplete);
|
|
802
|
+
this.signalConsumerDone = this.runSignalConsumer();
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Swap this runtime onto a new recipe without restarting it or touching its state.
|
|
806
|
+
*
|
|
807
|
+
* Teardown is the reverse of the build, and both halves matter. The scan listener is released
|
|
808
|
+
* before the module it is registered on; `scannerModule.stop()` clears its timers and awaits
|
|
809
|
+
* in-flight runs; `dslPlugin.stop()` is AWAITED, because it releases its bus subscriptions and
|
|
810
|
+
* drains the tick that may still be mid-flight at the venue — without that the outgoing monitor
|
|
811
|
+
* keeps writing state while the incoming one is already ticking the same positions, and the two
|
|
812
|
+
* hold independent StateManagers so nothing else serialises them.
|
|
813
|
+
*
|
|
814
|
+
* State on disk is untouched throughout: DSL position files, scanner stores and action history
|
|
815
|
+
* all survive, which is the entire reason an update is worth having over a delete-and-recreate.
|
|
816
|
+
*/
|
|
817
|
+
async rebuildComponents(config) {
|
|
818
|
+
// Everything fallible happens FIRST, while the old components are still running. A recipe that
|
|
819
|
+
// will not build throws here, and the live runtime never notices: it is still scanning, still
|
|
820
|
+
// ticking, still holding the stops it has already promised.
|
|
821
|
+
const next = await this.constructComponents(config);
|
|
822
|
+
const previousConfig = this.config;
|
|
823
|
+
await this.teardownComponentsForRebuild();
|
|
824
|
+
this.config = config;
|
|
825
|
+
try {
|
|
826
|
+
await this.swapInComponents(config, next);
|
|
827
|
+
}
|
|
828
|
+
catch (err) {
|
|
829
|
+
// Backstop, not the plan. Reaching here means something threw in the narrow window that
|
|
830
|
+
// construction could not cover — realistically `DslPlugin.create` failing to resolve state
|
|
831
|
+
// it has already been handed. The runtime is currently torn down with an open book, so the
|
|
832
|
+
// only acceptable outcome is to get the PREVIOUS recipe running again.
|
|
833
|
+
logger.error("[runtime] rebuild failed after teardown — restoring the previous recipe", { err });
|
|
834
|
+
try {
|
|
835
|
+
await this.teardownComponentsForRebuild();
|
|
836
|
+
const restored = await this.constructComponents(previousConfig);
|
|
837
|
+
this.config = previousConfig;
|
|
838
|
+
await this.swapInComponents(previousConfig, restored);
|
|
839
|
+
}
|
|
840
|
+
catch (rollbackErr) {
|
|
841
|
+
// Both the new recipe and the old one failed to build. Nothing is managing the book, and
|
|
842
|
+
// no further automatic recovery is available — say so in the loudest terms the process has,
|
|
843
|
+
// naming the wallet, because a human has to act.
|
|
844
|
+
logger.error("[runtime] ROLLBACK FAILED — runtime is down with an open book", {
|
|
845
|
+
address: this.strategyConfig.address,
|
|
846
|
+
rebuildError: err,
|
|
847
|
+
rollbackError: rollbackErr,
|
|
848
|
+
});
|
|
849
|
+
throw new RuntimeRebuildRollbackFailedError(this.strategyConfig.address, err, rollbackErr);
|
|
850
|
+
}
|
|
851
|
+
// The runtime is healthy again on its previous recipe; the update itself did not apply.
|
|
852
|
+
throw err;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Install the freshly built components, serialized against the trade loop.
|
|
857
|
+
*
|
|
858
|
+
* The signal consumer runs every action pass inside
|
|
859
|
+
* `reconcileMutex.withKeyLock("reconcile", () => this.runActionsForSignal(...))`, reading
|
|
860
|
+
* `runActionsForSignal` at call time from inside the lock. Taking the same lock here means a
|
|
861
|
+
* signal already in flight finishes against the OLD actions before the swap happens, and the next
|
|
862
|
+
* one picks up the new ones — no pass can straddle the change.
|
|
863
|
+
*
|
|
864
|
+
* The lock is scoped to the SWAP and deliberately NOT to the teardown that precedes it.
|
|
865
|
+
* `teardownComponentsForRebuild` awaits the DSL plugin's in-flight tick, and that tick takes this
|
|
866
|
+
* very lock for its periodic reconcile (`runReconcile` is handed `reconcileSerialize`). Holding
|
|
867
|
+
* the lock across the drain would deadlock the runtime — permanently, with an open book and
|
|
868
|
+
* nothing managing it — because `KeyedMutex` is a plain FIFO and grants no reentrancy.
|
|
869
|
+
*
|
|
870
|
+
* Creating the DSL plugin inside the lock is safe for the mirror-image reason: `create()` starts
|
|
871
|
+
* the monitor loop but does not await its first tick, so that tick queues behind this lock
|
|
872
|
+
* instead of blocking the call that holds it.
|
|
873
|
+
*/
|
|
874
|
+
async swapInComponents(config, built) {
|
|
875
|
+
await this.reconcileMutex.withKeyLock("reconcile", async () => {
|
|
876
|
+
await this.commitComponents(config, built);
|
|
877
|
+
this.wireScannerRunListener();
|
|
878
|
+
await this.scannerModule.start();
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Drop the scan listener, stop the scanner module, stop and release the DSL plugin.
|
|
883
|
+
*
|
|
884
|
+
* Shared by the rebuild and its rollback so the two cannot drift — the rollback runs it a second
|
|
885
|
+
* time to clear whatever the failed commit had already installed, and every step is idempotent
|
|
886
|
+
* for exactly that reason.
|
|
887
|
+
*
|
|
888
|
+
* Order matters: the listener comes off before the module it is registered on;
|
|
889
|
+
* `scannerModule.stop()` clears its timers and awaits in-flight runs; `dslPlugin.stop()` is
|
|
890
|
+
* AWAITED, because it releases its bus subscriptions and drains the tick that may still be
|
|
891
|
+
* mid-flight at the venue.
|
|
892
|
+
*/
|
|
893
|
+
async teardownComponentsForRebuild() {
|
|
894
|
+
if (this.onScannerRunComplete != null) {
|
|
895
|
+
this.scannerModule.off("scanner:run:complete", this.onScannerRunComplete);
|
|
896
|
+
this.onScannerRunComplete = null;
|
|
897
|
+
}
|
|
898
|
+
await this.scannerModule.stop();
|
|
899
|
+
if (this.dslPlugin) {
|
|
900
|
+
await this.dslPlugin.stop();
|
|
901
|
+
this.dslPlugin = null;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
710
904
|
async removeStrategy(address) {
|
|
711
905
|
let abandoned = 0;
|
|
712
906
|
if (this.dslPlugin) {
|