@senpi/runtime 3.0.65 → 3.0.67
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/event-derivation.d.ts.map +1 -1
- package/dist/actions/event-derivation.js +2 -0
- package/dist/actions/event-derivation.js.map +1 -1
- package/dist/cli/deploy-render.d.ts +3 -1
- package/dist/cli/deploy-render.d.ts.map +1 -1
- package/dist/cli/deploy-render.js +233 -11
- package/dist/cli/deploy-render.js.map +1 -1
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +9 -7
- package/dist/cli/guide-commands.js.map +1 -1
- package/dist/cli/senpi-commands.js +4 -4
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/deploy/deadlines.d.ts +3 -3
- package/dist/deploy/deadlines.js +3 -3
- package/dist/deploy/job.d.ts +16 -0
- package/dist/deploy/job.d.ts.map +1 -1
- package/dist/deploy/job.js +7 -1
- package/dist/deploy/job.js.map +1 -1
- package/dist/deploy/orchestrator.d.ts +2 -1
- package/dist/deploy/orchestrator.d.ts.map +1 -1
- package/dist/deploy/orchestrator.js +402 -84
- package/dist/deploy/orchestrator.js.map +1 -1
- package/dist/deploy/proof-gate.d.ts.map +1 -1
- package/dist/deploy/proof-gate.js +8 -3
- package/dist/deploy/proof-gate.js.map +1 -1
- package/dist/deploy/register.js +1 -1
- package/dist/health/types.d.ts +6 -0
- package/dist/health/types.d.ts.map +1 -1
- package/dist/health/types.js.map +1 -1
- package/dist/index.d.ts +16 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +234 -21
- package/dist/index.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +7 -1
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/scanners/engine/engine.d.ts +5 -0
- package/dist/scanners/engine/engine.d.ts.map +1 -1
- package/dist/scanners/engine/engine.js +1 -0
- package/dist/scanners/engine/engine.js.map +1 -1
- package/dist/scanners/runtime-module.d.ts.map +1 -1
- package/dist/scanners/runtime-module.js +5 -0
- package/dist/scanners/runtime-module.js.map +1 -1
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +72 -5
- package/dist/senpi/client.js.map +1 -1
- package/dist/senpi/types.d.ts +18 -0
- package/dist/senpi/types.d.ts.map +1 -1
- package/dist/telemetry/event-builders.d.ts +12 -0
- package/dist/telemetry/event-builders.d.ts.map +1 -1
- package/dist/telemetry/event-builders.js +2 -0
- package/dist/telemetry/event-builders.js.map +1 -1
- package/dist/telemetry/event-store.d.ts +6 -0
- package/dist/telemetry/event-store.d.ts.map +1 -1
- package/dist/telemetry/event-store.js +6 -6
- package/dist/telemetry/event-store.js.map +1 -1
- package/dist/telemetry/live-telemetry.d.ts +153 -0
- package/dist/telemetry/live-telemetry.d.ts.map +1 -0
- package/dist/telemetry/live-telemetry.js +471 -0
- package/dist/telemetry/live-telemetry.js.map +1 -0
- package/dist/telemetry/scanner-stats.d.ts +136 -0
- package/dist/telemetry/scanner-stats.d.ts.map +1 -0
- package/dist/telemetry/scanner-stats.js +304 -0
- package/dist/telemetry/scanner-stats.js.map +1 -0
- package/dist/telemetry/strategy-topology.d.ts +99 -0
- package/dist/telemetry/strategy-topology.d.ts.map +1 -0
- package/dist/telemetry/strategy-topology.js +93 -0
- package/dist/telemetry/strategy-topology.js.map +1 -0
- package/dist/telemetry/telemetry-publisher.d.ts +63 -0
- package/dist/telemetry/telemetry-publisher.d.ts.map +1 -0
- package/dist/telemetry/telemetry-publisher.js +229 -0
- package/dist/telemetry/telemetry-publisher.js.map +1 -0
- package/dist/telemetry/timeline-buffer.d.ts +95 -0
- package/dist/telemetry/timeline-buffer.d.ts.map +1 -0
- package/dist/telemetry/timeline-buffer.js +237 -0
- package/dist/telemetry/timeline-buffer.js.map +1 -0
- package/dist/telemetry/tracing.d.ts +9 -0
- package/dist/telemetry/tracing.d.ts.map +1 -1
- package/dist/telemetry/tracing.js +36 -0
- package/dist/telemetry/tracing.js.map +1 -1
- package/dist/utils/event-catalog.d.ts +8 -0
- package/dist/utils/event-catalog.d.ts.map +1 -1
- package/dist/utils/event-catalog.js.map +1 -1
- package/dist/utils/ulid.d.ts +9 -0
- package/dist/utils/ulid.d.ts.map +1 -0
- package/dist/utils/ulid.js +43 -0
- package/dist/utils/ulid.js.map +1 -0
- package/dist/validate/proof.js +4 -1
- package/dist/validate/proof.js.map +1 -1
- package/dist/validate/run.d.ts.map +1 -1
- package/dist/validate/run.js +4 -1
- package/dist/validate/run.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -56,6 +56,7 @@ import { createIntakeContext } from "./runtime-api/intake/intake-context.js";
|
|
|
56
56
|
import { wireRuntime, intakeUrlFor, selectScanners } from "./runtime-api/intake/intake-mount.js";
|
|
57
57
|
import { ingestDispatchMutex, ingestDispatchKey } from "./runtime-api/intake/ingest-mutex.js";
|
|
58
58
|
import { loadRuntimeConfig } from "./runtime/load-runtime-config.js";
|
|
59
|
+
import { createLiveTelemetry } from "./telemetry/live-telemetry.js";
|
|
59
60
|
/** Initialize all core features (DSL, risk, health) on the given event bus. */
|
|
60
61
|
export function initCore(bus) {
|
|
61
62
|
initDsl(bus);
|
|
@@ -116,6 +117,35 @@ const PLUGIN_MANIFEST_VERSION = readPluginManifestVersion();
|
|
|
116
117
|
* See `runtime/auto-update/resolve-package-name.ts`.
|
|
117
118
|
*/
|
|
118
119
|
const PLUGIN_PACKAGE_NAME = resolvePackageName();
|
|
120
|
+
/**
|
|
121
|
+
* The taxonomy code both one-runtime-per-wallet refusals lead with.
|
|
122
|
+
*
|
|
123
|
+
* `openclaw gateway call` collapses two different worlds into one process status — it catches
|
|
124
|
+
* whatever its RPC throws, prints `Gateway call failed: <error>` and exits 1, whether the gateway
|
|
125
|
+
* was unreachable or the gateway ANSWERED with a refusal. So the CLI separates them by message
|
|
126
|
+
* taxonomy instead: `isRefusalMessage` (`deploy/taxonomy.ts`) reads a leading bracketed code, and
|
|
127
|
+
* no transport failure carries one. These two refusals were bare prose, so the classifier could not
|
|
128
|
+
* see them and a deliberate policy decision reached its reader framed as a broken gateway — the one
|
|
129
|
+
* conclusion that is false here, and the one that sends whoever reads it to look at infrastructure
|
|
130
|
+
* instead of at their own action.
|
|
131
|
+
*
|
|
132
|
+
* ONE code for both branches. The condition is identical — this wallet already has a runtime, and
|
|
133
|
+
* two exit engines on one wallet's margin disagree about what is protected and stop each other's
|
|
134
|
+
* positions out on different floors. What differs between the two messages is only how much this
|
|
135
|
+
* gateway could see (a live handle, or a registry row), which the prose states and which changes
|
|
136
|
+
* nothing about the decision a caller has to make.
|
|
137
|
+
*/
|
|
138
|
+
const WALLET_HAS_RUNTIME = "[E_WALLET_HAS_RUNTIME]";
|
|
139
|
+
/**
|
|
140
|
+
* The id-collision refusal's code — the third gate on the same `senpi runtime create` path, and
|
|
141
|
+
* plausibly the one hit most often, since re-running create with the same id lands on it.
|
|
142
|
+
*
|
|
143
|
+
* A SEPARATE code from `WALLET_HAS_RUNTIME`, where those two share one: the condition here is an
|
|
144
|
+
* id already taken, not a wallet already under management, and the remedy differs with it — pick
|
|
145
|
+
* another id, rather than stop or inspect the runtime holding a margin account. Nothing about
|
|
146
|
+
* anyone's money is being claimed, which is precisely why it must not answer with the wallet code.
|
|
147
|
+
*/
|
|
148
|
+
const RUNTIME_ID_TAKEN = "[E_RUNTIME_ID_TAKEN]";
|
|
119
149
|
/**
|
|
120
150
|
* OpenClaw plugin entry.
|
|
121
151
|
* Boots from internal registry (installed_runtimes.json) in stateDir; does not read runtime list from config.
|
|
@@ -287,10 +317,140 @@ const senpiRuntimePlugin = {
|
|
|
287
317
|
// cleared on a successful wire/install. senpi.listRuntimes reads it to render
|
|
288
318
|
// "running — NO ENTRY SCANNERS" instead of a bare "running" for a blind runtime.
|
|
289
319
|
const unwiredRuntimes = new Map();
|
|
320
|
+
// ── Live telemetry ────────────────────────────────────────────────
|
|
321
|
+
// Every runtime this box knows about, running or not, keyed runtimeId. The topology payload has
|
|
322
|
+
// to name a stopped strategy too, so this is populated from the registry rather than from
|
|
323
|
+
// `runtimeHandles` (which holds only the live ones) and is the source of the two facts the
|
|
324
|
+
// registry entry does not carry: the strategy's display name and its declared scanners. Both come
|
|
325
|
+
// from the parsed YAML — `name` is the same `config.name` that `RuntimeHealthStatus.runtimeName`
|
|
326
|
+
// is built from, so a runtime whose config never parsed falls back to its id rather than to a
|
|
327
|
+
// guess, and its scanner list stays empty rather than inventing rows.
|
|
328
|
+
const telemetryStrategies = new Map();
|
|
329
|
+
const noteTelemetryRuntime = (runtimeId, address) => {
|
|
330
|
+
const existing = telemetryStrategies.get(runtimeId);
|
|
331
|
+
telemetryStrategies.set(runtimeId, {
|
|
332
|
+
address,
|
|
333
|
+
name: existing?.name ?? runtimeId,
|
|
334
|
+
scanners: existing?.scanners ?? [],
|
|
335
|
+
});
|
|
336
|
+
};
|
|
337
|
+
const noteTelemetryRuntimeConfig = (runtimeId, config) => {
|
|
338
|
+
telemetryStrategies.set(runtimeId, {
|
|
339
|
+
address: config.strategy?.wallet ?? telemetryStrategies.get(runtimeId)?.address ?? "",
|
|
340
|
+
name: config.name,
|
|
341
|
+
scanners: config.scanners ?? [],
|
|
342
|
+
});
|
|
343
|
+
};
|
|
290
344
|
// Dedicated logger for the runtime-api subtree — required because the
|
|
291
345
|
// PluginLogger surface (info/warn/error) lacks `debug`, while the
|
|
292
346
|
// Logger contract used by `runtime-api/` includes it.
|
|
293
347
|
const apiLogger = createLogger("[senpi-core][api]");
|
|
348
|
+
/**
|
|
349
|
+
* One gather per publish tick. Health and system state are read together because the two payloads
|
|
350
|
+
* need both: the topology reads health and `runtimeName`, the stats read the per-scanner rows,
|
|
351
|
+
* and `restartCount` / `degradedReason` live only on the health rows while every other scanner
|
|
352
|
+
* field lives only on the system-state rows — so they are joined here on `(runtimeId, scannerId)`.
|
|
353
|
+
*/
|
|
354
|
+
const collectTelemetryFacts = async () => {
|
|
355
|
+
const [statuses, states] = await Promise.all([
|
|
356
|
+
collectRuntimeHealthStatuses(runtimeHandles, api.logger),
|
|
357
|
+
collectRuntimeSystemStates(runtimeHandles, api.logger, baseStateDir),
|
|
358
|
+
]);
|
|
359
|
+
const healthByRuntime = new Map();
|
|
360
|
+
for (const status of statuses) {
|
|
361
|
+
if (status.runtimeId !== undefined)
|
|
362
|
+
healthByRuntime.set(status.runtimeId, status);
|
|
363
|
+
}
|
|
364
|
+
const scannerState = [];
|
|
365
|
+
// Registration facts per (runtimeId, scannerId), so the topology can report a scanner's live
|
|
366
|
+
// schedule rather than only what its YAML declared.
|
|
367
|
+
const registrations = new Map();
|
|
368
|
+
for (const state of states) {
|
|
369
|
+
const runtimeId = state.runtimeId;
|
|
370
|
+
const healthRows = runtimeId === undefined
|
|
371
|
+
? []
|
|
372
|
+
: healthByRuntime.get(runtimeId)?.components.scanners.scanners ?? [];
|
|
373
|
+
for (const row of state.components.scanners.state.scanners) {
|
|
374
|
+
const supervision = healthRows.find((r) => r.scannerId === row.scannerId);
|
|
375
|
+
const snapshot = {
|
|
376
|
+
address: row.address,
|
|
377
|
+
scannerId: row.scannerId,
|
|
378
|
+
health: row.health,
|
|
379
|
+
degradedReason: supervision?.degradedReason ?? null,
|
|
380
|
+
enabled: row.enabled,
|
|
381
|
+
inFlightSince: row.inFlightSince,
|
|
382
|
+
lastRunStartedAt: row.lastRunStartedAt,
|
|
383
|
+
lastRunFinishedAt: row.lastRunFinishedAt,
|
|
384
|
+
lastRunStatus: row.lastRunStatus,
|
|
385
|
+
lastRunReason: row.lastRunReason,
|
|
386
|
+
nextRunAt: row.nextRunAt,
|
|
387
|
+
lastAliveAt: row.lastAliveAt,
|
|
388
|
+
// No supervision row means the restart count is unknown, not zero. A zero here would
|
|
389
|
+
// read as "this scanner has never restarted" on a scanner nobody is supervising.
|
|
390
|
+
restartCount: supervision?.restartCount ?? null,
|
|
391
|
+
};
|
|
392
|
+
scannerState.push(snapshot);
|
|
393
|
+
if (runtimeId !== undefined) {
|
|
394
|
+
registrations.set(`${runtimeId}::${row.scannerId}`, {
|
|
395
|
+
...snapshot,
|
|
396
|
+
scheduleMode: row.scheduleMode,
|
|
397
|
+
configVersion: row.configVersion,
|
|
398
|
+
intervalSeconds: row.intervalSeconds,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const strategies = [];
|
|
404
|
+
for (const [runtimeId, entry] of telemetryStrategies) {
|
|
405
|
+
const health = healthByRuntime.get(runtimeId);
|
|
406
|
+
const scanners = entry.scanners.map((declared) => {
|
|
407
|
+
const live = registrations.get(`${runtimeId}::${declared.name}`);
|
|
408
|
+
return live === undefined
|
|
409
|
+
? { entry: declared }
|
|
410
|
+
: {
|
|
411
|
+
entry: declared,
|
|
412
|
+
registration: {
|
|
413
|
+
enabled: live.enabled,
|
|
414
|
+
scheduleMode: live.scheduleMode,
|
|
415
|
+
// The engine's own cadence, which is what the scanner actually polls on — a
|
|
416
|
+
// recipe may omit it and take the scanner definition's default. An external
|
|
417
|
+
// scanner registers zero because it is POSTed to rather than polled, and only
|
|
418
|
+
// the recipe knows the cadence its scaffold was told to tick at.
|
|
419
|
+
intervalSeconds: live.intervalSeconds > 0 ? live.intervalSeconds : declared.interval_seconds ?? null,
|
|
420
|
+
configVersion: live.configVersion,
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
strategies.push({
|
|
425
|
+
runtimeId,
|
|
426
|
+
address: entry.address,
|
|
427
|
+
// `runtimeName` is built from the same `config.name`; the indexed value is used because it
|
|
428
|
+
// is available for a stopped runtime too, where there is no health status to read.
|
|
429
|
+
name: health?.runtimeName ?? entry.name,
|
|
430
|
+
scanners,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
strategies,
|
|
435
|
+
runningRuntimeIds: runtimeHandles,
|
|
436
|
+
unwiredRuntimeIds: unwiredRuntimes,
|
|
437
|
+
readHealth: (runtimeId) => healthByRuntime.get(runtimeId),
|
|
438
|
+
scannerState,
|
|
439
|
+
};
|
|
440
|
+
};
|
|
441
|
+
const liveTelemetry = createLiveTelemetry({ collectFacts: collectTelemetryFacts });
|
|
442
|
+
/**
|
|
443
|
+
* Every gateway method goes through here so the broadcast handle is captured on the FIRST request
|
|
444
|
+
* of any kind. `ctx.context` is the only route to the websocket push layer and it exists only
|
|
445
|
+
* inside a handler — capturing it in the subscribe method alone would mean a box whose client
|
|
446
|
+
* never subscribes can never publish anything, silently.
|
|
447
|
+
*/
|
|
448
|
+
const registerGatewayMethod = (name, handler, opts) => {
|
|
449
|
+
api.registerGatewayMethod(name, (ctx) => {
|
|
450
|
+
liveTelemetry.captureBroadcast(ctx, name);
|
|
451
|
+
return handler(ctx);
|
|
452
|
+
}, opts);
|
|
453
|
+
};
|
|
294
454
|
// A gateway seam failed to wire/launch a runtime's external scanner stack: the runtime is up but
|
|
295
455
|
// runs blind on internal scanners only. Two records, by design: a first-class `runtime.scanners_unwired`
|
|
296
456
|
// event (the queryable/alertable fact, by phase) and the migrated exception-form `logger.error`
|
|
@@ -498,6 +658,9 @@ const senpiRuntimePlugin = {
|
|
|
498
658
|
continue;
|
|
499
659
|
const pathOrContent = entry.runtimeYamlPath ?? entry.runtimeYamlContent;
|
|
500
660
|
const entryStateDir = buildRuntimeStateDir(baseStateDir, entry.id, entry.wallet ?? "");
|
|
661
|
+
// Indexed before the boot attempt: a runtime whose start throws is installed but stopped,
|
|
662
|
+
// and the telemetry topology has to be able to say so.
|
|
663
|
+
noteTelemetryRuntime(entry.id, entry.wallet ?? "");
|
|
501
664
|
const handle = await run(pathOrContent, {
|
|
502
665
|
fromPath: hasPath,
|
|
503
666
|
stateDir: entryStateDir,
|
|
@@ -520,6 +683,7 @@ const senpiRuntimePlugin = {
|
|
|
520
683
|
if (sharedIntakeContext) {
|
|
521
684
|
try {
|
|
522
685
|
const runtimeConfig = await loadRuntimeConfig(pathOrContent, { fromPath: hasPath });
|
|
686
|
+
noteTelemetryRuntimeConfig(entry.id, runtimeConfig);
|
|
523
687
|
// Content-installed runtimes carry their recipe dir in the registry
|
|
524
688
|
// entry (runtimeYamlDir) so relative scanner paths still resolve
|
|
525
689
|
// after a restart; path-installed ones derive it from the path.
|
|
@@ -571,6 +735,7 @@ const senpiRuntimePlugin = {
|
|
|
571
735
|
// event) so listRuntimes/health don't render a bare "running".
|
|
572
736
|
try {
|
|
573
737
|
const runtimeConfig = await loadRuntimeConfig(pathOrContent, { fromPath: hasPath });
|
|
738
|
+
noteTelemetryRuntimeConfig(entry.id, runtimeConfig);
|
|
574
739
|
if (selectScanners(runtimeConfig).length > 0) {
|
|
575
740
|
unwiredRuntimes.set(entry.id, "no_intake");
|
|
576
741
|
handle.attachSupervisionSource(() => ({
|
|
@@ -672,8 +837,19 @@ const senpiRuntimePlugin = {
|
|
|
672
837
|
}
|
|
673
838
|
}
|
|
674
839
|
}
|
|
840
|
+
// Publish ticks start only once the runtimes are up, so the first frames describe a real
|
|
841
|
+
// box. Gated by the same switch the skills coordinator uses: unit tests boot `register()`
|
|
842
|
+
// and must not acquire background timers. The subscribe method still answers without them.
|
|
843
|
+
if (process.env.SENPI_RUNTIME_API_DISABLED !== "true") {
|
|
844
|
+
liveTelemetry.start();
|
|
845
|
+
}
|
|
675
846
|
},
|
|
676
847
|
async stop() {
|
|
848
|
+
// Live telemetry goes first: it owns a timer and an in-memory buffer, nothing downstream
|
|
849
|
+
// depends on it, and stopping it here guarantees no publish tick reads a half-torn-down
|
|
850
|
+
// runtime handle. Records emitted during the teardown below land in a buffer nobody drains,
|
|
851
|
+
// which is the right trade for a process on its way out.
|
|
852
|
+
liveTelemetry.stop();
|
|
677
853
|
// Drain the HTTP server FIRST so in-flight POST /signals requests
|
|
678
854
|
// finish before the underlying runtime they target is torn down.
|
|
679
855
|
// `apiHandle.close()` never throws.
|
|
@@ -839,7 +1015,7 @@ const senpiRuntimePlugin = {
|
|
|
839
1015
|
? (sanitizeId(id) || "runtime")
|
|
840
1016
|
: deriveRuntimeId(runtimeName, wallet);
|
|
841
1017
|
if (hasId(registry, resolvedId)) {
|
|
842
|
-
return { ok: false, error: toError(
|
|
1018
|
+
return { ok: false, error: toError(`${RUNTIME_ID_TAKEN} A runtime with id "${resolvedId}" already exists. Use a different id (e.g. --runtime-id another-name) or remove the existing one first: openclaw senpi runtime delete ${resolvedId}`) };
|
|
843
1019
|
}
|
|
844
1020
|
/**
|
|
845
1021
|
* One runtime per wallet address — asked of PERSISTED state, not only of this process.
|
|
@@ -875,7 +1051,7 @@ const senpiRuntimePlugin = {
|
|
|
875
1051
|
// two reads found it. They agree in practice; the refusal has to agree with the surface
|
|
876
1052
|
// it sends the reader to, because that is where the reader checks it.
|
|
877
1053
|
if (runtimeHandles.has(holderId)) {
|
|
878
|
-
return { ok: false, error: toError(
|
|
1054
|
+
return { ok: false, error: toError(`${WALLET_HAS_RUNTIME} A runtime for wallet "${wallet}" is already running (runtime "${holderId}"). ` +
|
|
879
1055
|
`Stop it first: openclaw senpi runtime delete --id ${holderId}`) };
|
|
880
1056
|
}
|
|
881
1057
|
return { ok: false, error: toError(
|
|
@@ -886,7 +1062,7 @@ const senpiRuntimePlugin = {
|
|
|
886
1062
|
// next restart TRIES. The next step leads with the read, because the operator has to
|
|
887
1063
|
// find out which of the two runtimes they actually want before removing either, and
|
|
888
1064
|
// deleting the wrong one leaves a funded wallet watched by nothing.
|
|
889
|
-
|
|
1065
|
+
`${WALLET_HAS_RUNTIME} A runtime for wallet "${wallet}" is already installed (runtime "${holderId}"). ` +
|
|
890
1066
|
`It is not running in this gateway right now, but it is in the registry, so the next ` +
|
|
891
1067
|
`restart will try to start it — installing a second runtime on the same wallet would ` +
|
|
892
1068
|
`put two exit engines on one wallet's margin, each tracking its own high-water floor. ` +
|
|
@@ -980,6 +1156,10 @@ const senpiRuntimePlugin = {
|
|
|
980
1156
|
runtimeId: resolvedId,
|
|
981
1157
|
});
|
|
982
1158
|
runtimeHandles.set(resolvedId, handle);
|
|
1159
|
+
if (parsedConfig)
|
|
1160
|
+
noteTelemetryRuntimeConfig(resolvedId, parsedConfig);
|
|
1161
|
+
else
|
|
1162
|
+
noteTelemetryRuntime(resolvedId, wallet);
|
|
983
1163
|
// Registry create fact — distinct from the runtime.started boot transition (which also fires
|
|
984
1164
|
// on a transient restart). Emitted inside emitEvent's guard so telemetry never fails the install.
|
|
985
1165
|
emitEvent(apiLogger, () => buildRuntimeCreatedEvent({ runtimeId: resolvedId, address: wallet, recipeName: runtimeName }));
|
|
@@ -1085,7 +1265,7 @@ const senpiRuntimePlugin = {
|
|
|
1085
1265
|
}
|
|
1086
1266
|
}
|
|
1087
1267
|
/** Gateway respond accepts (ok, payload?, error?) with error as { code, message }; plugin SDK types may omit the third arg. */
|
|
1088
|
-
|
|
1268
|
+
registerGatewayMethod("senpi.installRuntime", async (ctx) => {
|
|
1089
1269
|
const respond = ctx.respond;
|
|
1090
1270
|
const result = await installRuntimeCore(ctx.params);
|
|
1091
1271
|
if (result.ok)
|
|
@@ -1093,7 +1273,7 @@ const senpiRuntimePlugin = {
|
|
|
1093
1273
|
else
|
|
1094
1274
|
respond(false, undefined, result.error);
|
|
1095
1275
|
}, { scope: "operator.write" });
|
|
1096
|
-
|
|
1276
|
+
registerGatewayMethod("senpi.listRuntimes", async (ctx) => {
|
|
1097
1277
|
const respond = ctx.respond;
|
|
1098
1278
|
const { readRegistry } = await import("./runtime/strategy-registry.js");
|
|
1099
1279
|
const registry = await readRegistry(baseStateDir);
|
|
@@ -1172,6 +1352,9 @@ const senpiRuntimePlugin = {
|
|
|
1172
1352
|
// "running — NO ENTRY SCANNERS" forever. The map is otherwise only cleared on a
|
|
1173
1353
|
// successful wire/install, which a scanner-less reinstall never reaches.
|
|
1174
1354
|
unwiredRuntimes.delete(targetId);
|
|
1355
|
+
// Same reuse hazard: a deleted id left in the index would keep publishing a strategy that no
|
|
1356
|
+
// longer exists, and would hand a reinstall the old name and scanner list.
|
|
1357
|
+
telemetryStrategies.delete(targetId);
|
|
1175
1358
|
// Archive state directory before removing from registry
|
|
1176
1359
|
if (deletedEntry) {
|
|
1177
1360
|
const runtimeStateDir = buildRuntimeStateDir(baseStateDir, targetId, deletedEntry.wallet ?? "");
|
|
@@ -1198,7 +1381,7 @@ const senpiRuntimePlugin = {
|
|
|
1198
1381
|
}));
|
|
1199
1382
|
return { ok: true, payload: { id: targetId } };
|
|
1200
1383
|
}
|
|
1201
|
-
|
|
1384
|
+
registerGatewayMethod("senpi.deleteRuntime", async (ctx) => {
|
|
1202
1385
|
const respond = ctx.respond;
|
|
1203
1386
|
const result = await deleteRuntimeCore(ctx.params);
|
|
1204
1387
|
if (result.ok)
|
|
@@ -1292,7 +1475,7 @@ const senpiRuntimePlugin = {
|
|
|
1292
1475
|
* producers — slated for removal in Task 5 once the migration counter
|
|
1293
1476
|
* decays to zero (see `runtime-phase-2-api-plan.md` Section 8).
|
|
1294
1477
|
*/
|
|
1295
|
-
|
|
1478
|
+
registerGatewayMethod("senpi.ingestExternalScannerData", async (ctx) => {
|
|
1296
1479
|
const respond = ctx.respond;
|
|
1297
1480
|
// TODO(s11): wire actual telemetry counter for
|
|
1298
1481
|
// runtime.deprecated_gateway_ingest_call (event name, properties:
|
|
@@ -1368,7 +1551,7 @@ const senpiRuntimePlugin = {
|
|
|
1368
1551
|
});
|
|
1369
1552
|
}
|
|
1370
1553
|
}, { scope: "operator.write" });
|
|
1371
|
-
|
|
1554
|
+
registerGatewayMethod("senpi.listDslPositions", async (ctx) => {
|
|
1372
1555
|
const respond = ctx.respond;
|
|
1373
1556
|
const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
|
|
1374
1557
|
const address = typeof ctx.params?.address === "string" ? ctx.params.address : undefined;
|
|
@@ -1393,7 +1576,7 @@ const senpiRuntimePlugin = {
|
|
|
1393
1576
|
}
|
|
1394
1577
|
respond(true, { positions });
|
|
1395
1578
|
}, { scope: "operator.read" });
|
|
1396
|
-
|
|
1579
|
+
registerGatewayMethod("senpi.getDslPositionState", async (ctx) => {
|
|
1397
1580
|
const respond = ctx.respond;
|
|
1398
1581
|
const asset = typeof ctx.params?.asset === "string" ? ctx.params.asset : undefined;
|
|
1399
1582
|
if (!asset) {
|
|
@@ -1434,7 +1617,7 @@ const senpiRuntimePlugin = {
|
|
|
1434
1617
|
}
|
|
1435
1618
|
respond(true, { positions: matches });
|
|
1436
1619
|
}, { scope: "operator.read" });
|
|
1437
|
-
|
|
1620
|
+
registerGatewayMethod("senpi.listDslArchives", async (ctx) => {
|
|
1438
1621
|
const respond = ctx.respond;
|
|
1439
1622
|
const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
|
|
1440
1623
|
const address = typeof ctx.params?.address === "string" ? ctx.params.address : undefined;
|
|
@@ -1460,7 +1643,7 @@ const senpiRuntimePlugin = {
|
|
|
1460
1643
|
closes = closes.slice(0, limit);
|
|
1461
1644
|
respond(true, { closes });
|
|
1462
1645
|
}, { scope: "operator.read" });
|
|
1463
|
-
|
|
1646
|
+
registerGatewayMethod("senpi.getHealthStatus", async (ctx) => {
|
|
1464
1647
|
const respond = ctx.respond;
|
|
1465
1648
|
const updater = autoUpdateCoordinator.getSnapshot();
|
|
1466
1649
|
const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
|
|
@@ -1488,7 +1671,7 @@ const senpiRuntimePlugin = {
|
|
|
1488
1671
|
const statuses = await collectRuntimeHealthStatuses(runtimeHandles, api.logger);
|
|
1489
1672
|
respond(true, { statuses, updater });
|
|
1490
1673
|
}, { scope: "operator.read" });
|
|
1491
|
-
|
|
1674
|
+
registerGatewayMethod("senpi.getSystemState", async (ctx) => {
|
|
1492
1675
|
const respond = ctx.respond;
|
|
1493
1676
|
const updater = autoUpdateCoordinator.getSnapshot();
|
|
1494
1677
|
const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
|
|
@@ -1516,6 +1699,36 @@ const senpiRuntimePlugin = {
|
|
|
1516
1699
|
const states = await collectRuntimeSystemStates(runtimeHandles, api.logger, baseStateDir);
|
|
1517
1700
|
respond(true, { states, updater });
|
|
1518
1701
|
}, { scope: "operator.read" });
|
|
1702
|
+
/**
|
|
1703
|
+
* Live telemetry — the one method of the stream, called once per connect.
|
|
1704
|
+
*
|
|
1705
|
+
* It returns the current value of all three broadcast payloads plus the recent record window, so
|
|
1706
|
+
* a client joining a box that has been running for hours renders a full screen immediately
|
|
1707
|
+
* instead of waiting for the next thing to happen. `(afterEpoch, afterSeq)` resumes a client that
|
|
1708
|
+
* already holds part of this epoch's run — a cursor naming any other epoch is dropped, because a
|
|
1709
|
+
* seq from a previous run cannot address these records. `truncated` says when the records between
|
|
1710
|
+
* a cursor of this epoch and the oldest record still held are gone, which nothing in `recent`
|
|
1711
|
+
* alone can express.
|
|
1712
|
+
*
|
|
1713
|
+
* Read-only, like every other inspection method here. The scope that decides who RECEIVES the
|
|
1714
|
+
* broadcasts is a property of the client's own connection, not of this method.
|
|
1715
|
+
*/
|
|
1716
|
+
registerGatewayMethod("senpi.telemetry.subscribe", async (ctx) => {
|
|
1717
|
+
const respond = ctx.respond;
|
|
1718
|
+
try {
|
|
1719
|
+
respond(true, await liveTelemetry.snapshot({
|
|
1720
|
+
afterEpoch: ctx.params?.afterEpoch,
|
|
1721
|
+
afterSeq: ctx.params?.afterSeq,
|
|
1722
|
+
}));
|
|
1723
|
+
}
|
|
1724
|
+
catch (err) {
|
|
1725
|
+
api.logger.warn("[senpi-runtime] telemetry subscribe error", { err });
|
|
1726
|
+
respond(false, undefined, {
|
|
1727
|
+
code: "INTERNAL_ERROR",
|
|
1728
|
+
message: `Failed to build the telemetry snapshot: ${formatGatewayError(err)}`,
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
}, { scope: "operator.read" });
|
|
1519
1732
|
/**
|
|
1520
1733
|
* Action layer — read-only inspection.
|
|
1521
1734
|
*
|
|
@@ -1535,7 +1748,7 @@ const senpiRuntimePlugin = {
|
|
|
1535
1748
|
* persisted action-store machinery (`src/actions/store/`) usable
|
|
1536
1749
|
* from the operator-facing CLI again.
|
|
1537
1750
|
*/
|
|
1538
|
-
|
|
1751
|
+
registerGatewayMethod("senpi.listActions", async (ctx) => {
|
|
1539
1752
|
const respond = ctx.respond;
|
|
1540
1753
|
const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
|
|
1541
1754
|
const address = typeof ctx.params?.address === "string" ? ctx.params.address : undefined;
|
|
@@ -1565,7 +1778,7 @@ const senpiRuntimePlugin = {
|
|
|
1565
1778
|
}
|
|
1566
1779
|
respond(true, { actions: merged });
|
|
1567
1780
|
}, { scope: "operator.read" });
|
|
1568
|
-
|
|
1781
|
+
registerGatewayMethod("senpi.getActionState", async (ctx) => {
|
|
1569
1782
|
const respond = ctx.respond;
|
|
1570
1783
|
const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
|
|
1571
1784
|
const actionName = typeof ctx.params?.actionName === "string" ? ctx.params.actionName : undefined;
|
|
@@ -1602,7 +1815,7 @@ const senpiRuntimePlugin = {
|
|
|
1602
1815
|
});
|
|
1603
1816
|
}
|
|
1604
1817
|
}, { scope: "operator.read" });
|
|
1605
|
-
|
|
1818
|
+
registerGatewayMethod("senpi.getActionHistory", async (ctx) => {
|
|
1606
1819
|
const respond = ctx.respond;
|
|
1607
1820
|
const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
|
|
1608
1821
|
const actionName = typeof ctx.params?.actionName === "string" ? ctx.params.actionName : undefined;
|
|
@@ -1670,7 +1883,7 @@ const senpiRuntimePlugin = {
|
|
|
1670
1883
|
// reachable on HTTP `/audit`. Passes through the caller-controlled subset
|
|
1671
1884
|
// of the audit_query contract; resource scoping (wallet → strategyId) is
|
|
1672
1885
|
// owned by handle.auditQuery, never the caller.
|
|
1673
|
-
|
|
1886
|
+
registerGatewayMethod("senpi.auditQuery", async (ctx) => {
|
|
1674
1887
|
const respond = ctx.respond;
|
|
1675
1888
|
const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
|
|
1676
1889
|
if (!runtimeId) {
|
|
@@ -1718,7 +1931,7 @@ const senpiRuntimePlugin = {
|
|
|
1718
1931
|
// its state root private, so re-derive it from baseStateDir + the handle's
|
|
1719
1932
|
// wallet (same helper the writer's stateDir was built with); the writer
|
|
1720
1933
|
// nests entries under `<root>/<address>/risk-guard/audit.jsonl`.
|
|
1721
|
-
|
|
1934
|
+
registerGatewayMethod("senpi.getRiskAudit", async (ctx) => {
|
|
1722
1935
|
const respond = ctx.respond;
|
|
1723
1936
|
const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
|
|
1724
1937
|
if (!runtimeId) {
|
|
@@ -1761,7 +1974,7 @@ const senpiRuntimePlugin = {
|
|
|
1761
1974
|
// Per-strategy local domain-event ring (on-disk JSONL) — the trade narrative, queryable locally.
|
|
1762
1975
|
// Same state-root re-derivation as getRiskAudit: the Runtime keeps its root private, so rebuild it
|
|
1763
1976
|
// from baseStateDir + the handle's wallet; the writer nests rows under `<root>/strategies/<address>/events/`.
|
|
1764
|
-
|
|
1977
|
+
registerGatewayMethod("senpi.getEvents", async (ctx) => {
|
|
1765
1978
|
const respond = ctx.respond;
|
|
1766
1979
|
const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
|
|
1767
1980
|
if (!runtimeId) {
|
|
@@ -1818,12 +2031,12 @@ const senpiRuntimePlugin = {
|
|
|
1818
2031
|
}
|
|
1819
2032
|
}, { scope: "operator.read" });
|
|
1820
2033
|
// ─── Skills manager — read + mutate ──────────────────────────────────────
|
|
1821
|
-
|
|
2034
|
+
registerGatewayMethod("senpi.skills.status", async (ctx) => {
|
|
1822
2035
|
const respond = ctx.respond;
|
|
1823
2036
|
const snapshot = skillsCoordinator.getSnapshot();
|
|
1824
2037
|
respond(true, { snapshot });
|
|
1825
2038
|
}, { scope: "operator.read" });
|
|
1826
|
-
|
|
2039
|
+
registerGatewayMethod("senpi.skills.update", async (ctx) => {
|
|
1827
2040
|
const respond = ctx.respond;
|
|
1828
2041
|
const name = typeof ctx.params?.name === "string" ? ctx.params.name.trim() : undefined;
|
|
1829
2042
|
if (!name) {
|
|
@@ -1848,7 +2061,7 @@ const senpiRuntimePlugin = {
|
|
|
1848
2061
|
});
|
|
1849
2062
|
}
|
|
1850
2063
|
}, { scope: "operator.write" });
|
|
1851
|
-
|
|
2064
|
+
registerGatewayMethod("senpi.skills.updateAll", async (ctx) => {
|
|
1852
2065
|
const respond = ctx.respond;
|
|
1853
2066
|
try {
|
|
1854
2067
|
const results = await skillsCoordinator.updateAll();
|