@senpi/runtime 3.0.186 → 3.0.190
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/llm-decision/openclaw-llm-decision.d.ts +1 -1
- package/dist/actions/llm-decision/openclaw-llm-decision.js +1 -1
- package/dist/approvals/telemetry.d.ts +3 -2
- package/dist/approvals/telemetry.d.ts.map +1 -1
- package/dist/approvals/telemetry.js.map +1 -1
- package/dist/approvals/tool-approval-gate.d.ts +9 -3
- package/dist/approvals/tool-approval-gate.d.ts.map +1 -1
- package/dist/approvals/tool-approval-gate.js +18 -5
- package/dist/approvals/tool-approval-gate.js.map +1 -1
- package/dist/approvals/turn-context.d.ts +43 -18
- package/dist/approvals/turn-context.d.ts.map +1 -1
- package/dist/approvals/turn-context.js +67 -24
- package/dist/approvals/turn-context.js.map +1 -1
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +8 -1
- 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 -2
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/deploy/box-cap.d.ts +7 -0
- package/dist/deploy/box-cap.d.ts.map +1 -0
- package/dist/deploy/box-cap.js +13 -0
- package/dist/deploy/box-cap.js.map +1 -0
- package/dist/deploy/funding.d.ts +13 -1
- package/dist/deploy/funding.d.ts.map +1 -1
- package/dist/deploy/funding.js +13 -4
- package/dist/deploy/funding.js.map +1 -1
- package/dist/deploy/job.d.ts +2 -0
- package/dist/deploy/job.d.ts.map +1 -1
- package/dist/deploy/job.js +2 -2
- package/dist/deploy/job.js.map +1 -1
- package/dist/deploy/messages.d.ts +14 -0
- package/dist/deploy/messages.d.ts.map +1 -1
- package/dist/deploy/messages.js +59 -25
- package/dist/deploy/messages.js.map +1 -1
- package/dist/deploy/orchestrator.d.ts +23 -1
- package/dist/deploy/orchestrator.d.ts.map +1 -1
- package/dist/deploy/orchestrator.js +87 -30
- package/dist/deploy/orchestrator.js.map +1 -1
- package/dist/deploy/package.d.ts +14 -1
- package/dist/deploy/package.d.ts.map +1 -1
- package/dist/deploy/package.js +37 -2
- package/dist/deploy/package.js.map +1 -1
- package/dist/deploy/register.d.ts +3 -0
- package/dist/deploy/register.d.ts.map +1 -1
- package/dist/deploy/register.js +50 -33
- package/dist/deploy/register.js.map +1 -1
- package/dist/dsl/monitor/dsl-failure-events.d.ts +38 -2
- package/dist/dsl/monitor/dsl-failure-events.d.ts.map +1 -1
- package/dist/dsl/monitor/dsl-failure-events.js +67 -4
- package/dist/dsl/monitor/dsl-failure-events.js.map +1 -1
- package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
- package/dist/dsl/monitor/process-one-position.js +25 -1
- package/dist/dsl/monitor/process-one-position.js.map +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +527 -158
- package/dist/index.js.map +1 -1
- package/dist/runtime/auto-update/coordinator.d.ts +19 -2
- package/dist/runtime/auto-update/coordinator.d.ts.map +1 -1
- package/dist/runtime/auto-update/coordinator.js +104 -20
- package/dist/runtime/auto-update/coordinator.js.map +1 -1
- package/dist/runtime/auto-update/gateway-restart.d.ts +2 -2
- package/dist/runtime/auto-update/gateway-restart.js +1 -1
- package/dist/runtime/auto-update/idle-gate.d.ts +45 -9
- package/dist/runtime/auto-update/idle-gate.d.ts.map +1 -1
- package/dist/runtime/auto-update/idle-gate.js +26 -14
- package/dist/runtime/auto-update/idle-gate.js.map +1 -1
- package/dist/runtime/auto-update/register-idle-gate.d.ts +1 -1
- package/dist/runtime/auto-update/register-idle-gate.d.ts.map +1 -1
- package/dist/runtime/auto-update/register-idle-gate.js +8 -4
- package/dist/runtime/auto-update/register-idle-gate.js.map +1 -1
- package/dist/runtime/auto-update/run-update-command.d.ts +1 -1
- package/dist/runtime/auto-update/run-update-command.js +1 -1
- package/dist/runtime/auto-update/types.d.ts +2 -1
- package/dist/runtime/auto-update/types.d.ts.map +1 -1
- package/dist/runtime/auto-update/types.js.map +1 -1
- package/dist/runtime/auto-update/update-schedule.d.ts +14 -4
- package/dist/runtime/auto-update/update-schedule.d.ts.map +1 -1
- package/dist/runtime/auto-update/update-schedule.js +21 -5
- package/dist/runtime/auto-update/update-schedule.js.map +1 -1
- package/dist/runtime/boot-skip.d.ts +18 -0
- package/dist/runtime/boot-skip.d.ts.map +1 -1
- package/dist/runtime/boot-skip.js +25 -0
- package/dist/runtime/boot-skip.js.map +1 -1
- package/dist/runtime/lifecycle-events.d.ts +2 -2
- package/dist/runtime/lifecycle-events.d.ts.map +1 -1
- package/dist/runtime/lifecycle-events.js.map +1 -1
- package/dist/runtime/lifecycle-reaper.d.ts +11 -6
- package/dist/runtime/lifecycle-reaper.d.ts.map +1 -1
- package/dist/runtime/lifecycle-reaper.js +25 -15
- package/dist/runtime/lifecycle-reaper.js.map +1 -1
- package/dist/runtime/notifications.d.ts +10 -0
- package/dist/runtime/notifications.d.ts.map +1 -1
- package/dist/runtime/notifications.js +17 -6
- package/dist/runtime/notifications.js.map +1 -1
- package/dist/runtime/process-state.d.ts +168 -0
- package/dist/runtime/process-state.d.ts.map +1 -0
- package/dist/runtime/process-state.js +81 -0
- package/dist/runtime/process-state.js.map +1 -0
- package/dist/runtime/reconcile-state.d.ts.map +1 -1
- package/dist/runtime/reconcile-state.js +18 -4
- package/dist/runtime/reconcile-state.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +4 -1
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/strategy-registry.d.ts +16 -0
- package/dist/runtime/strategy-registry.d.ts.map +1 -1
- package/dist/runtime/strategy-registry.js +7 -3
- package/dist/runtime/strategy-registry.js.map +1 -1
- package/dist/runtime/update-apply-deps.d.ts +5 -3
- package/dist/runtime/update-apply-deps.d.ts.map +1 -1
- package/dist/runtime/update-apply-deps.js +15 -3
- package/dist/runtime/update-apply-deps.js.map +1 -1
- package/dist/scanners/external/scaffold/preflight.py +492 -12
- package/dist/senpi/__tests__/helpers/fake-paper-mcp.d.ts +44 -0
- package/dist/senpi/__tests__/helpers/fake-paper-mcp.d.ts.map +1 -0
- package/dist/senpi/__tests__/helpers/fake-paper-mcp.js +129 -0
- package/dist/senpi/__tests__/helpers/fake-paper-mcp.js.map +1 -0
- package/dist/senpi/client.d.ts +40 -2
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +82 -15
- package/dist/senpi/client.js.map +1 -1
- package/dist/senpi/paper.d.ts +28 -0
- package/dist/senpi/paper.d.ts.map +1 -0
- package/dist/senpi/paper.js +35 -0
- package/dist/senpi/paper.js.map +1 -0
- package/dist/senpi/strategy-status.d.ts.map +1 -1
- package/dist/senpi/strategy-status.js +2 -0
- package/dist/senpi/strategy-status.js.map +1 -1
- package/dist/senpi/types.d.ts +8 -1
- package/dist/senpi/types.d.ts.map +1 -1
- package/dist/telemetry/box-memory.d.ts +29 -0
- package/dist/telemetry/box-memory.d.ts.map +1 -0
- package/dist/telemetry/box-memory.js +113 -0
- package/dist/telemetry/box-memory.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +22 -1
- package/dist/telemetry/metrics.d.ts.map +1 -1
- package/dist/telemetry/metrics.js +83 -1
- package/dist/telemetry/metrics.js.map +1 -1
- package/dist/telemetry/spans/span-hooks.d.ts +1 -1
- package/dist/telemetry/spans/span-hooks.d.ts.map +1 -1
- package/dist/telemetry/spans/span-hooks.js +5 -2
- package/dist/telemetry/spans/span-hooks.js.map +1 -1
- package/dist/telemetry/trace-identity.d.ts.map +1 -1
- package/dist/telemetry/trace-identity.js +2 -0
- package/dist/telemetry/trace-identity.js.map +1 -1
- package/dist/test-setup.js +7 -1
- package/dist/test-setup.js.map +1 -1
- package/dist/tool-surface/crib-hook.d.ts.map +1 -1
- package/dist/tool-surface/crib-hook.js +8 -0
- package/dist/tool-surface/crib-hook.js.map +1 -1
- package/dist/tool-surface/crib-sheet.d.ts +1 -1
- package/dist/tool-surface/crib-sheet.js +1 -1
- package/dist/tool-surface/mcp-tool-source.d.ts +1 -1
- package/dist/tool-surface/mcp-tool-source.js +2 -2
- package/dist/universe/package-universe.js +1 -1
- package/dist/universe/package-universe.js.map +1 -1
- package/dist/utils/event-catalog.d.ts +34 -4
- package/dist/utils/event-catalog.d.ts.map +1 -1
- package/dist/utils/event-catalog.js +6 -0
- package/dist/utils/event-catalog.js.map +1 -1
- package/dist/validate/codes.d.ts +2 -0
- package/dist/validate/codes.d.ts.map +1 -1
- package/dist/validate/codes.js +6 -0
- package/dist/validate/codes.js.map +1 -1
- package/dist/validate/live-stage.d.ts +12 -0
- package/dist/validate/live-stage.d.ts.map +1 -1
- package/dist/validate/live-stage.js +16 -0
- package/dist/validate/live-stage.js.map +1 -1
- package/dist/widgets/tool.d.ts +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import { join, dirname } from "node:path";
|
|
|
17
17
|
import { isMoneyCallTool, SenpiClient, senpiOrderCallsInFlight } from "./senpi/client.js";
|
|
18
18
|
import { MoneyCallTracker, registerMoneyCallHooks } from "./runtime/auto-update/money-call-tracker.js";
|
|
19
19
|
import { buildWalletStatusHint } from "./senpi/strategy-status.js";
|
|
20
|
-
import { bootSkippedStatusLabel, partitionByWalletStatus, readBootWalletStatuses, trackableWalletKeys, } from "./runtime/boot-skip.js";
|
|
20
|
+
import { bootSkippedStatusLabel, partitionByWalletStatus, readBootWalletStatuses, readWalletStatusesByWorld, trackableWalletKeys, } from "./runtime/boot-skip.js";
|
|
21
21
|
import { DeployJobController } from "./deploy/job.js";
|
|
22
22
|
import { DEPLOY_CAPABILITIES, registerDeployGatewayMethods } from "./deploy/register.js";
|
|
23
23
|
import { createQuantDesk } from "./quant/register.js";
|
|
@@ -25,6 +25,7 @@ import { registerUpdateGatewayMethod } from "./runtime/update-register.js";
|
|
|
25
25
|
import { createStrategiesPublisher } from "./runtime/strategies-telemetry.js";
|
|
26
26
|
import { buildEventsReplay, createEventsPublisher, } from "./runtime/events-telemetry.js";
|
|
27
27
|
import { strategiesStore, strategyRecordFromConfig } from "./runtime/strategies-store.js";
|
|
28
|
+
import { bindProcessLogger, processLog, runtimeProcessState, } from "./runtime/process-state.js";
|
|
28
29
|
import { checkProofCoversRecipe, makeApplyToRuntime, makeWriteRegistry, runtimeChangeLock, } from "./runtime/update-apply-deps.js";
|
|
29
30
|
import { INSTALL_DEADLINE_MS, WALLET_INSTALL_IN_FLIGHT_CODE } from "./deploy/deadlines.js";
|
|
30
31
|
import { AuditQueryError } from "./runtime/audit-query.types.js";
|
|
@@ -49,7 +50,7 @@ import { readEvents } from "./telemetry/event-store.js";
|
|
|
49
50
|
import { VERSION } from "./version.js";
|
|
50
51
|
import { resolvePackageName } from "./runtime/auto-update/resolve-package-name.js";
|
|
51
52
|
import { runOpenClawPluginsUpdate } from "./runtime/auto-update/run-update-command.js";
|
|
52
|
-
import { UpdateIdleGate } from "./runtime/auto-update/idle-gate.js";
|
|
53
|
+
import { UpdateIdleGate, createIdleGateActivity } from "./runtime/auto-update/idle-gate.js";
|
|
53
54
|
import { registerIdleGateHooks, resolveUpdateMid } from "./runtime/auto-update/register-idle-gate.js";
|
|
54
55
|
import { resolveUpdateMarkerPath } from "./runtime/auto-update/update-marker.js";
|
|
55
56
|
import { INSTALL_RESULTS_OTEL_FLUSH_MS, INSTALL_RESULTS_OTEL_READY_MS, resolveInstallResultsPath, } from "./runtime/auto-update/install-results.js";
|
|
@@ -59,6 +60,9 @@ import { registerSkillRouterHook } from "./prompt/skill-router-hook.js";
|
|
|
59
60
|
import { registerCribSheet } from "./tool-surface/crib-hook.js";
|
|
60
61
|
import { getDefaultPiiRedactor, initDefaultPiiRedactor } from "./telemetry/spans/redact-pii.js";
|
|
61
62
|
import { flushOtelLogs, initOtelSdk, shutdownOtelSdk, waitForOtelInit } from "./telemetry/otel-sdk.js";
|
|
63
|
+
import { createBoxMemoryReader } from "./telemetry/box-memory.js";
|
|
64
|
+
import { registerBoxMemoryGauges } from "./telemetry/metrics.js";
|
|
65
|
+
import { walletMode } from "./senpi/paper.js";
|
|
62
66
|
import { getLogLevel, getLogLevelSource, parseLogLevel, setLogLevel, } from "./utils/logger.js";
|
|
63
67
|
import { ExternalScannerIngestError, } from "./scanners/external-scanner-receiver.js";
|
|
64
68
|
import { createLogger } from "./utils/logger.js";
|
|
@@ -81,7 +85,7 @@ import { ingestDispatchMutex, ingestDispatchKey } from "./runtime-api/intake/ing
|
|
|
81
85
|
import { buildScaffoldErrorRouter } from "./runtime-api/intake/scaffold-error-router.js";
|
|
82
86
|
import { loadRuntimeConfig } from "./runtime/load-runtime-config.js";
|
|
83
87
|
import { errorText } from "./deploy/error-text.js";
|
|
84
|
-
import { initOpenClawHost } from "./host/openclaw-host.js";
|
|
88
|
+
import { getOpenClawHost, initOpenClawHost } from "./host/openclaw-host.js";
|
|
85
89
|
import { createRegisterPluginLogger, withEventLogger } from "./host/plugin-logger.js";
|
|
86
90
|
import { hostname } from "node:os";
|
|
87
91
|
import { BOOT_FACTS_CALL_TIMEOUT_MS, planBootFacts, syncBootFacts, } from "./strategy/facts-sync.js";
|
|
@@ -182,23 +186,29 @@ const WALLET_INSTALL_IN_FLIGHT = `[${WALLET_INSTALL_IN_FLIGHT_CODE}]`;
|
|
|
182
186
|
*/
|
|
183
187
|
const RUNTIME_ID_TAKEN = "[E_RUNTIME_ID_TAKEN]";
|
|
184
188
|
/**
|
|
185
|
-
* One StrategyFacts send per runtime at a time, keyed on runtime id, across the whole process
|
|
186
|
-
*
|
|
187
|
-
*
|
|
189
|
+
* One StrategyFacts send per runtime at a time, keyed on runtime id, across the whole process.
|
|
190
|
+
* Held in the process state (`runtime/process-state.ts`), not this module: a 10.x plugin reload
|
|
191
|
+
* evaluates a new copy of this module, and the previous registration's send may still be running.
|
|
192
|
+
* Separate from `runtimeChangeLock`. Delete does not take it; an apply queues its send here and
|
|
193
|
+
* does not wait.
|
|
188
194
|
*/
|
|
189
|
-
|
|
195
|
+
function factsSendLock() {
|
|
196
|
+
const state = runtimeProcessState();
|
|
197
|
+
state.factsSendLock ??= new KeyedMutex();
|
|
198
|
+
return state.factsSendLock;
|
|
199
|
+
}
|
|
190
200
|
/**
|
|
191
|
-
*
|
|
201
|
+
* Clears the process-state latch for the 5.x "cannot classify runs" notice
|
|
202
|
+
* (`legacyTurnClassificationNoticeLogged`). Tests only.
|
|
192
203
|
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* registration and does not read
|
|
204
|
+
* The latch lives in the process state so a plugin reload (which re-registers the plugin and
|
|
205
|
+
* restarts its service in the same process, from a new copy of this module) does not repeat the
|
|
206
|
+
* notice. The 5.x case is a host-version limit with no config fix, so one info line per process is
|
|
207
|
+
* the whole message; the fixable 10.x case stays an ERROR at every registration and does not read
|
|
208
|
+
* this latch.
|
|
197
209
|
*/
|
|
198
|
-
let legacyTurnClassificationNoticeLogged = false;
|
|
199
|
-
/** Clears {@link legacyTurnClassificationNoticeLogged}. Tests only. */
|
|
200
210
|
export function resetLegacyTurnClassificationNoticeForTests() {
|
|
201
|
-
legacyTurnClassificationNoticeLogged = false;
|
|
211
|
+
runtimeProcessState().legacyTurnClassificationNoticeLogged = false;
|
|
202
212
|
}
|
|
203
213
|
/**
|
|
204
214
|
* The whole of `register()` in the `"cli-metadata"` pass: the `senpi` CLI root, under the same
|
|
@@ -220,10 +230,19 @@ function registerSenpiCliMetadata(api) {
|
|
|
220
230
|
registerSenpiCli(ctx.program);
|
|
221
231
|
}, { commands: ["senpi"] });
|
|
222
232
|
}
|
|
233
|
+
/** Running runtimes by world, read from each handle's wallet: a paper wallet is a simulation. */
|
|
234
|
+
function countRuntimesByMode(handles) {
|
|
235
|
+
const counts = { live: 0, paper: 0 };
|
|
236
|
+
for (const handle of handles)
|
|
237
|
+
counts[walletMode(handle.wallet)] += 1;
|
|
238
|
+
return counts;
|
|
239
|
+
}
|
|
223
240
|
/**
|
|
224
241
|
* OpenClaw plugin entry.
|
|
225
242
|
* Boots from internal registry (installed_runtimes.json) in stateDir; does not read runtime list from config.
|
|
226
243
|
*/
|
|
244
|
+
/** How long service start waits for the OTel SDK before registering the box memory gauges anyway. */
|
|
245
|
+
const BOX_MEMORY_OTEL_READY_MS = 120_000;
|
|
227
246
|
const senpiRuntimePlugin = {
|
|
228
247
|
// Must match openclaw.plugin.json `id` — openclaw loader rejects the plugin
|
|
229
248
|
// when `installed_plugins.json` record id (sourced from the manifest) and
|
|
@@ -261,8 +280,17 @@ const senpiRuntimePlugin = {
|
|
|
261
280
|
registerSenpiCliMetadata(api);
|
|
262
281
|
return;
|
|
263
282
|
}
|
|
264
|
-
// Host facts first: the logger and every version-gated registration below read them.
|
|
265
|
-
|
|
283
|
+
// Host facts first: the logger and every version-gated registration below read them. 10.x also
|
|
284
|
+
// registers plugins in non-full modes inside the gateway (`tool-discovery` for tools.catalog);
|
|
285
|
+
// that pass must keep registering tools, but its throwaway api must not replace the record the
|
|
286
|
+
// live registration made.
|
|
287
|
+
const fullRegistration = api.registrationMode === undefined || api.registrationMode === "full";
|
|
288
|
+
const openClawHost = (!fullRegistration && getOpenClawHost()) || initOpenClawHost(api);
|
|
289
|
+
// State kept across a reload (the deploy controller, the gate trackers) logs through the latest
|
|
290
|
+
// full registration's logger, never through an `api` a reload has replaced.
|
|
291
|
+
const processState = runtimeProcessState();
|
|
292
|
+
if (fullRegistration)
|
|
293
|
+
bindProcessLogger(api.logger);
|
|
266
294
|
const pluginLogger = createRegisterPluginLogger(api.logger, {
|
|
267
295
|
jsonMode: process.argv.includes("--json"),
|
|
268
296
|
hostRoutesJsonLogsToStderr: openClawHost.modern,
|
|
@@ -403,10 +431,14 @@ const senpiRuntimePlugin = {
|
|
|
403
431
|
// decisions persist in <stateDir>/tool-approvals.json.
|
|
404
432
|
// See docs/mcp-tool-approvals-design.md.
|
|
405
433
|
// Open trade-approval prompts, read by the self-update idle gate (R2).
|
|
406
|
-
|
|
434
|
+
// This and the turn tracker are process state: a 10.x reload re-registers in the same process
|
|
435
|
+
// while a prompt is open or a run is in flight (r4467406809, r4427720076).
|
|
436
|
+
processState.pendingApprovals ??= new PendingApprovalTracker();
|
|
437
|
+
const pendingApprovalTracker = processState.pendingApprovals;
|
|
407
438
|
// The turn kind (chat / cron / heartbeat) and session key of each gated
|
|
408
439
|
// call, for approval telemetry and the 10.x scheduled-turn block (R6, D13).
|
|
409
|
-
|
|
440
|
+
processState.turnContext ??= new TurnContextTracker();
|
|
441
|
+
const turnContextTracker = processState.turnContext;
|
|
410
442
|
// One boot-time fact (D15): can this host classify runs at all? Only then
|
|
411
443
|
// does the gate stop prompts outside a direct chat turn. Without it every
|
|
412
444
|
// run reads unknown and keeps the pre-10.x prompt, so a box missing the
|
|
@@ -442,7 +474,7 @@ const senpiRuntimePlugin = {
|
|
|
442
474
|
? "on this OpenClaw version; a 10.x host lifts this."
|
|
443
475
|
: "until the runtime registers its turn-context hooks.");
|
|
444
476
|
const legacyTurnClassificationNotice = turnClassificationHostLimit ? turnClassificationNotice : undefined;
|
|
445
|
-
if (turnClassificationNotice !== undefined && !turnClassificationHostLimit) {
|
|
477
|
+
if (turnClassificationNotice !== undefined && !turnClassificationHostLimit && fullRegistration) {
|
|
446
478
|
pluginLogger.error(turnClassificationNotice);
|
|
447
479
|
}
|
|
448
480
|
const approvalsHostStatus = turnClassification.available
|
|
@@ -452,12 +484,28 @@ const senpiRuntimePlugin = {
|
|
|
452
484
|
reason: turnClassification.reason,
|
|
453
485
|
...(turnClassificationHostLimit ? { hostLimit: true } : {}),
|
|
454
486
|
};
|
|
487
|
+
// Process state, like the run-kind tracker beside it: a reload must not zero the count that
|
|
488
|
+
// `senpi status` warns about while chat trades are being refused.
|
|
489
|
+
const approvalsStatus = () => approvalsHostStatus.turnClassification === "available"
|
|
490
|
+
? {
|
|
491
|
+
...approvalsHostStatus,
|
|
492
|
+
runsRecorded: turnContextTracker.runsRecorded,
|
|
493
|
+
unclassifiedBlocks: processState.unclassifiedBlocks,
|
|
494
|
+
...(processState.lastUnclassifiedBlockAt
|
|
495
|
+
? { lastUnclassifiedBlockAt: processState.lastUnclassifiedBlockAt }
|
|
496
|
+
: {}),
|
|
497
|
+
}
|
|
498
|
+
: approvalsHostStatus;
|
|
455
499
|
try {
|
|
456
500
|
registerToolApprovalGate({
|
|
457
501
|
api,
|
|
458
502
|
pendingApprovals: pendingApprovalTracker,
|
|
459
503
|
turnContext: turnContextTracker,
|
|
460
504
|
turnClassification: turnClassification.available,
|
|
505
|
+
onUnclassifiedBlock: (at) => {
|
|
506
|
+
processState.unclassifiedBlocks += 1;
|
|
507
|
+
processState.lastUnclassifiedBlockAt = new Date(at).toISOString();
|
|
508
|
+
},
|
|
461
509
|
mode: resolveToolApprovalsMode({
|
|
462
510
|
pluginConfig: config,
|
|
463
511
|
env: process.env,
|
|
@@ -605,8 +653,22 @@ const senpiRuntimePlugin = {
|
|
|
605
653
|
* row, sees no live handle, deletes it and reinstalls — and the first install then lands on top,
|
|
606
654
|
* clobbering `runtimeHandles` under a runtime id both installs derive identically. Two exit
|
|
607
655
|
* engines, one margin account: the exact state `[E_WALLET_HAS_RUNTIME]` exists to prevent.
|
|
656
|
+
*
|
|
657
|
+
* Process state (`runtime/process-state.ts`), not this registration: a 10.x plugin reload
|
|
658
|
+
* re-registers in the same process while a deploy job's install from the previous registration
|
|
659
|
+
* runs on, and the new registration must still refuse a second install on that wallet.
|
|
608
660
|
*/
|
|
609
|
-
const installsInFlight =
|
|
661
|
+
const installsInFlight = processState.installsInFlight;
|
|
662
|
+
/**
|
|
663
|
+
* This registration's service lifecycle, for an install or adoption that outlives it. A 10.x
|
|
664
|
+
* plugin reload stops this service and starts the next registration's in the same process;
|
|
665
|
+
* `serviceStops` counts this registration's stops, so a stop followed by a later start is still
|
|
666
|
+
* seen. Only a stop counts: an install that began before the first start (the gateway may call
|
|
667
|
+
* installRuntime before start()) and spans it is not orphaned.
|
|
668
|
+
*/
|
|
669
|
+
let serviceState = "idle";
|
|
670
|
+
let serviceStops = 0;
|
|
671
|
+
let disposeBoxMemoryGauges;
|
|
610
672
|
// Dedicated logger for the runtime-api subtree — required because the
|
|
611
673
|
// PluginLogger surface (info/warn/error) lacks `debug`, while the
|
|
612
674
|
// Logger contract used by `runtime-api/` includes it.
|
|
@@ -663,7 +725,10 @@ const senpiRuntimePlugin = {
|
|
|
663
725
|
// the agent's senpi__ calls (direct or through the dispatcher), seen at the
|
|
664
726
|
// tool hooks. A deploy job is read from the controller (this process) and
|
|
665
727
|
// its journals (any process).
|
|
666
|
-
|
|
728
|
+
// Process state, like the approval trackers: a call that started under the previous
|
|
729
|
+
// registration still counts, and its after-hook (now this registration's) closes it.
|
|
730
|
+
processState.moneyCalls ??= new MoneyCallTracker();
|
|
731
|
+
const moneyCalls = processState.moneyCalls;
|
|
667
732
|
try {
|
|
668
733
|
registerMoneyCallHooks({
|
|
669
734
|
api: api,
|
|
@@ -681,6 +746,13 @@ const senpiRuntimePlugin = {
|
|
|
681
746
|
ordersInFlight: () => senpiOrderCallsInFlight() + moneyCalls.count(Date.now()),
|
|
682
747
|
// Read only from ticks, after register() has constructed the controller below.
|
|
683
748
|
deploysInFlight: () => deployController.inFlight(Date.now()),
|
|
749
|
+
// The process start, not this registration: a config edit under `plugins` re-registers in
|
|
750
|
+
// the same process (10.10 reloadGatewayPlugins), and repeated reloads under 15 min apart
|
|
751
|
+
// must not hold an update until the forced window.
|
|
752
|
+
startedAt: Date.now() - Math.round(process.uptime() * 1000),
|
|
753
|
+
// Process state: a reload builds this gate again, and must not forget a user turn that is
|
|
754
|
+
// running, a message that is queued, or when the user last wrote.
|
|
755
|
+
activity: (processState.idleGateActivity ??= createIdleGateActivity()),
|
|
684
756
|
logger: pluginLogger,
|
|
685
757
|
});
|
|
686
758
|
try {
|
|
@@ -774,11 +846,20 @@ const senpiRuntimePlugin = {
|
|
|
774
846
|
// boot scan: declaring it after would leave that reference in the temporal dead zone for any
|
|
775
847
|
// future refactor that starts the service earlier. The rest of the deploy wiring lives in
|
|
776
848
|
// src/deploy/register.ts behind an explicit context; this closure contributes collaborators only.
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
849
|
+
//
|
|
850
|
+
// One controller per deploys dir per process (process state): a full re-registration (10.10
|
|
851
|
+
// reloadGatewayPlugins, same process) reuses the one that may be running a deploy, so its
|
|
852
|
+
// `running()` keeps single-flight and `inFlight()` keeps the idle gate's deploy blocker. It
|
|
853
|
+
// logs through `processLog`, the current registration's logger, so a job that outlives its
|
|
854
|
+
// registration still narrates and writes its terminal journal line.
|
|
855
|
+
const deploysDir = join(baseStateDir, "deploys");
|
|
856
|
+
const deployController = processState.deployControllers.get(deploysDir) ??
|
|
857
|
+
new DeployJobController({
|
|
858
|
+
deploysDir,
|
|
859
|
+
now: () => Date.now(),
|
|
860
|
+
log: (line) => processLog.info(`[senpi-runtime] ${line}`),
|
|
861
|
+
});
|
|
862
|
+
processState.deployControllers.set(deploysDir, deployController);
|
|
782
863
|
// ── lifecycle reaper ─────────────────────────────────────────────────────────────────────────
|
|
783
864
|
// A runtime exists iff its strategy is open. Every close path ends at the backend and none of
|
|
784
865
|
// them tells this box, so the reaper asks: once after boot, hourly, and whenever the backend
|
|
@@ -797,7 +878,7 @@ const senpiRuntimePlugin = {
|
|
|
797
878
|
if (!mcpTransport)
|
|
798
879
|
await client.connect();
|
|
799
880
|
try {
|
|
800
|
-
return await client.
|
|
881
|
+
return await client.listStrategiesForWallets(wallets, { signal });
|
|
801
882
|
}
|
|
802
883
|
finally {
|
|
803
884
|
if (!mcpTransport)
|
|
@@ -816,15 +897,20 @@ const senpiRuntimePlugin = {
|
|
|
816
897
|
api.registerService({
|
|
817
898
|
id: "senpi-runtime",
|
|
818
899
|
async start() {
|
|
900
|
+
serviceState = "running";
|
|
819
901
|
// The 5.x classification notice (see the approval gate above), once per process.
|
|
820
|
-
if (legacyTurnClassificationNotice !== undefined && !legacyTurnClassificationNoticeLogged) {
|
|
821
|
-
legacyTurnClassificationNoticeLogged = true;
|
|
902
|
+
if (legacyTurnClassificationNotice !== undefined && !processState.legacyTurnClassificationNoticeLogged) {
|
|
903
|
+
processState.legacyTurnClassificationNoticeLogged = true;
|
|
822
904
|
pluginLogger.info(legacyTurnClassificationNotice);
|
|
823
905
|
}
|
|
824
906
|
// Deploy-journal janitor: stamp anything a restart left unterminated as `interrupted`.
|
|
825
907
|
// It never re-enters a deploy state machine — discovery is pull-based via
|
|
826
|
-
// `senpi deploy status`.
|
|
827
|
-
|
|
908
|
+
// `senpi deploy status`. Once per process: after a reload an unterminated journal may be
|
|
909
|
+
// the deploy this process is still running (r4469086155).
|
|
910
|
+
if (!processState.bootScanned.has(deploysDir)) {
|
|
911
|
+
processState.bootScanned.add(deploysDir);
|
|
912
|
+
await deployController.scanOnBoot();
|
|
913
|
+
}
|
|
828
914
|
// Quant runs a restart cut short belong to a process that no longer exists: stamp them
|
|
829
915
|
// failed with the re-run command, so `senpi quant status` never reports a dead run as live.
|
|
830
916
|
// The controller also gates every quant start/read on this same (memoized) janitor, since
|
|
@@ -924,26 +1010,26 @@ const senpiRuntimePlugin = {
|
|
|
924
1010
|
// lower-case storage, and rejects the whole batch on one malformed address.
|
|
925
1011
|
const registeredWallets = trackableWalletKeys(registry.runtimes);
|
|
926
1012
|
if (registeredWallets.length > 0) {
|
|
927
|
-
const read = await
|
|
1013
|
+
const read = await readWalletStatusesByWorld(registeredWallets, async (wallets, signal) => {
|
|
928
1014
|
const client = mcpTransport ? new SenpiClient(mcpTransport) : new SenpiClient({ apiKey });
|
|
929
1015
|
if (!mcpTransport)
|
|
930
1016
|
await client.connect();
|
|
931
1017
|
try {
|
|
932
|
-
return await client.
|
|
1018
|
+
return await client.listStrategiesForWallets(wallets, { signal });
|
|
933
1019
|
}
|
|
934
1020
|
finally {
|
|
935
1021
|
if (!mcpTransport)
|
|
936
1022
|
await client.disconnect().catch(() => { });
|
|
937
1023
|
}
|
|
938
1024
|
});
|
|
939
|
-
if (read.
|
|
1025
|
+
if (read.answered.size > 0) {
|
|
940
1026
|
walletStatuses = read.statuses;
|
|
941
1027
|
bootStrategyIds = read.strategyIds;
|
|
942
|
-
reaper.recordStatuses(
|
|
1028
|
+
reaper.recordStatuses([...read.answered], read.statuses);
|
|
943
1029
|
}
|
|
944
|
-
|
|
945
|
-
pluginLogger.warn("[senpi-runtime] Could not read strategy statuses at boot; booting every registered runtime", { reason:
|
|
946
|
-
emitEvent(apiLogger, () => buildRuntimeBootStatusUnreadEvent({ reason:
|
|
1030
|
+
for (const failure of read.failures) {
|
|
1031
|
+
pluginLogger.warn("[senpi-runtime] Could not read strategy statuses at boot; booting every registered runtime of that world", { reason: failure.reason, err: failure.message, paper: failure.paper });
|
|
1032
|
+
emitEvent(apiLogger, () => buildRuntimeBootStatusUnreadEvent({ reason: failure.reason, registeredCount: failure.walletCount }));
|
|
947
1033
|
}
|
|
948
1034
|
}
|
|
949
1035
|
const { boot: entriesToBoot, skipped } = partitionByWalletStatus(registry.runtimes, walletStatuses);
|
|
@@ -1156,10 +1242,32 @@ const senpiRuntimePlugin = {
|
|
|
1156
1242
|
}
|
|
1157
1243
|
}
|
|
1158
1244
|
}
|
|
1245
|
+
// This registration now runs the box's runtimes: publish it, then start what an install from a
|
|
1246
|
+
// stopped registration handed over while none was live (see `handOffRuntime`). Detached and
|
|
1247
|
+
// never throwing, like the facts send: one adoption must not hold or fail the boot.
|
|
1248
|
+
processState.runtimeHost = runtimeHost;
|
|
1249
|
+
const pendingHandOffs = [...processState.pendingRuntimeHandOffs];
|
|
1250
|
+
processState.pendingRuntimeHandOffs.clear();
|
|
1251
|
+
for (const [runtimeId, walletKey] of pendingHandOffs) {
|
|
1252
|
+
void runtimeHost.adoptRuntime({ runtimeId, walletKey }).then((handOff) => {
|
|
1253
|
+
pluginLogger.info("[senpi-runtime] handed-over runtime settled at service start", { runtimeId, outcome: handOff.outcome });
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1159
1256
|
// Ticks once now, with every booted runtime in the store, then every 30 s.
|
|
1160
1257
|
strategiesPublisher.start();
|
|
1161
1258
|
// Sends the events added since the previous send, every 1 s.
|
|
1162
1259
|
eventsPublisher.start();
|
|
1260
|
+
// The metrics API has no proxy meter: gauges made before the SDK sets its provider never export.
|
|
1261
|
+
const stopsAtStart = serviceStops;
|
|
1262
|
+
void waitForOtelInit(BOX_MEMORY_OTEL_READY_MS).then(() => {
|
|
1263
|
+
if (serviceStops !== stopsAtStart)
|
|
1264
|
+
return;
|
|
1265
|
+
disposeBoxMemoryGauges?.();
|
|
1266
|
+
disposeBoxMemoryGauges = registerBoxMemoryGauges({
|
|
1267
|
+
read: createBoxMemoryReader({ warn: (message, meta) => pluginLogger.warn(message, meta) }),
|
|
1268
|
+
runtimes: () => countRuntimesByMode(runtimeHandles.values()),
|
|
1269
|
+
});
|
|
1270
|
+
});
|
|
1163
1271
|
// Last, once everything above is up: the reaper's post-boot pass and hourly sweep. Its
|
|
1164
1272
|
// not-active escalation is also off until here, so no delete can land while the boot loop is
|
|
1165
1273
|
// still wiring runtimes and launchers. Nothing here may fail start() — a throw from inside the boot loop takes every runtime down, which
|
|
@@ -1177,12 +1285,20 @@ const senpiRuntimePlugin = {
|
|
|
1177
1285
|
}
|
|
1178
1286
|
},
|
|
1179
1287
|
async stop() {
|
|
1288
|
+
// First: an install or adoption still running in this registration sees the stop from here
|
|
1289
|
+
// on and hands its runtime to the next registration instead of keeping it in this one.
|
|
1290
|
+
serviceState = "stopped";
|
|
1291
|
+
serviceStops += 1;
|
|
1292
|
+
if (processState.runtimeHost === runtimeHost)
|
|
1293
|
+
processState.runtimeHost = null;
|
|
1180
1294
|
// Stop the reaper first so no pass starts deleting while the runtimes are torn down below,
|
|
1181
1295
|
// and wait (bounded) for a pass already under way, so its delete cannot stop a handle the
|
|
1182
1296
|
// teardown below is stopping too.
|
|
1183
1297
|
await reaper.stop();
|
|
1184
1298
|
strategiesPublisher.stop();
|
|
1185
1299
|
eventsPublisher.stop();
|
|
1300
|
+
disposeBoxMemoryGauges?.();
|
|
1301
|
+
disposeBoxMemoryGauges = undefined;
|
|
1186
1302
|
// Drain the HTTP server FIRST so in-flight POST /signals requests
|
|
1187
1303
|
// finish before the underlying runtime they target is torn down.
|
|
1188
1304
|
// `apiHandle.close()` never throws.
|
|
@@ -1228,7 +1344,310 @@ const senpiRuntimePlugin = {
|
|
|
1228
1344
|
* `senpi deploy` installs through the SAME gates (wallet ownership, one-runtime-per-wallet,
|
|
1229
1345
|
* scanner-path resolution) instead of a parallel path that could drift.
|
|
1230
1346
|
*/
|
|
1347
|
+
/**
|
|
1348
|
+
* Start an already-registered runtime in THIS registration while its service runs: `run()`,
|
|
1349
|
+
* the handle in `runtimeHandles`, then the hot-install scanner wire (the API is already
|
|
1350
|
+
* listening, so the launcher starts at once). Shared by `installRuntimeCore` and by
|
|
1351
|
+
* {@link runtimeHost}'s adoption of a runtime another registration handed over.
|
|
1352
|
+
*
|
|
1353
|
+
* `orphaned` says whether this registration's service stopped since the caller began (a 10.x
|
|
1354
|
+
* plugin reload). It is read after `run()` returns and again after the wire: a stopped
|
|
1355
|
+
* registration's `stop()` has already torn its runtimes down, so a runtime started here now
|
|
1356
|
+
* would keep ticking in a registration nothing can stop or delete. In that case this stops what
|
|
1357
|
+
* it started (the launcher, then the handle), removes them from this registration's maps, and
|
|
1358
|
+
* returns `orphaned`; the caller hands the runtime to the live registration.
|
|
1359
|
+
*
|
|
1360
|
+
* Throws what `run()` throws. Wire failures do not throw: they are reported as `unwiredPhase`,
|
|
1361
|
+
* exactly as the install has always reported them.
|
|
1362
|
+
*/
|
|
1363
|
+
async function startRuntimeLive(spec, orphaned,
|
|
1364
|
+
/** Called once the runtime runs in this registration, before the scanner wire. */
|
|
1365
|
+
onRunning) {
|
|
1366
|
+
const { runtimeId, pathOrContent, fromPath, recipeDir, wallet, parsedConfig } = spec;
|
|
1367
|
+
/**
|
|
1368
|
+
* The unwired verdict for THIS start, tracked locally rather than read back out of
|
|
1369
|
+
* `unwiredRuntimes`, so the result reports what this call did and never a leftover verdict
|
|
1370
|
+
* from an earlier runtime that happened to share the id. Set at each of the three sites that
|
|
1371
|
+
* leave the runtime without live entry scanners; cleared by the wire-success path.
|
|
1372
|
+
*/
|
|
1373
|
+
let unwiredPhase;
|
|
1374
|
+
let hotLauncher = null;
|
|
1375
|
+
const scannerProviders = await createScannerProvidersForRun({
|
|
1376
|
+
mcpTransport,
|
|
1377
|
+
apiKey,
|
|
1378
|
+
});
|
|
1379
|
+
const entryStateDir = buildRuntimeStateDir(baseStateDir, runtimeId, wallet);
|
|
1380
|
+
const handle = await run(pathOrContent, {
|
|
1381
|
+
fromPath,
|
|
1382
|
+
stateDir: entryStateDir,
|
|
1383
|
+
mcpTransport,
|
|
1384
|
+
apiKey,
|
|
1385
|
+
scannerProviders,
|
|
1386
|
+
runtimeId,
|
|
1387
|
+
onStrategyNotActive: (address) => reaper.checkWallet(address),
|
|
1388
|
+
});
|
|
1389
|
+
/** Stops what this call started and leaves this registration's maps as they were. */
|
|
1390
|
+
const abandon = async () => {
|
|
1391
|
+
if (hotLauncher) {
|
|
1392
|
+
await hotLauncher.stop().catch(() => { });
|
|
1393
|
+
if (launchers.get(runtimeId) === hotLauncher)
|
|
1394
|
+
launchers.delete(runtimeId);
|
|
1395
|
+
}
|
|
1396
|
+
await handle.stop().catch((err) => {
|
|
1397
|
+
pluginLogger.warn("[senpi-runtime] could not stop a runtime started by a stopped registration", {
|
|
1398
|
+
runtimeId,
|
|
1399
|
+
err: err instanceof Error ? err.message : String(err),
|
|
1400
|
+
});
|
|
1401
|
+
});
|
|
1402
|
+
if (runtimeHandles.get(runtimeId) === handle) {
|
|
1403
|
+
runtimeHandles.delete(runtimeId);
|
|
1404
|
+
unwiredRuntimes.delete(runtimeId);
|
|
1405
|
+
}
|
|
1406
|
+
return { status: "orphaned" };
|
|
1407
|
+
};
|
|
1408
|
+
if (orphaned())
|
|
1409
|
+
return abandon();
|
|
1410
|
+
runtimeHandles.set(runtimeId, handle);
|
|
1411
|
+
onRunning?.();
|
|
1412
|
+
// ── hot-install supervised scanner wire ───────────────────────────────
|
|
1413
|
+
// Live-install path: used when a runtime is registered after boot via
|
|
1414
|
+
// senpi.installRuntime, while the API is already listening — the
|
|
1415
|
+
// counterpart to the boot-time `launcher` above. A runtime takes exactly
|
|
1416
|
+
// one path: boot-present → `launcher` (deferred start); installed live →
|
|
1417
|
+
// `hotLauncher` (immediate start). The two are mutually exclusive because
|
|
1418
|
+
// installRuntime rejects any id/wallet already in the registry before
|
|
1419
|
+
// wiring (the hasId guard), so a boot-present runtime never re-enters here;
|
|
1420
|
+
// an adoption starts only a row the live registration does not run.
|
|
1421
|
+
// The API is already listening at this point, so we wire and start
|
|
1422
|
+
// immediately (no deferred start needed). Any failure is swallowed so
|
|
1423
|
+
// the install itself is not rolled back — the runtime is already up.
|
|
1424
|
+
// Gate on the same API-disabled flag as the boot path: when the HTTP API
|
|
1425
|
+
// is off (a test-only seam), the /signals route is never mounted, so
|
|
1426
|
+
// starting scanners would only spawn children that POST to a closed port.
|
|
1427
|
+
if (parsedConfig &&
|
|
1428
|
+
sharedIntakeContext &&
|
|
1429
|
+
process.env.SENPI_RUNTIME_API_DISABLED !== "true") {
|
|
1430
|
+
try {
|
|
1431
|
+
const apiConfig = loadApiConfig(api.pluginConfig);
|
|
1432
|
+
const scaffoldPackageDir = fileURLToPath(new URL("./scanners/external", import.meta.url));
|
|
1433
|
+
hotLauncher = wireRuntime({
|
|
1434
|
+
runtimeConfig: parsedConfig,
|
|
1435
|
+
recipeDir,
|
|
1436
|
+
runtimeId,
|
|
1437
|
+
wallet,
|
|
1438
|
+
sharedCtx: sharedIntakeContext,
|
|
1439
|
+
runtimeHandles,
|
|
1440
|
+
intakeUrl: intakeUrlFor(apiConfig.host, apiConfig.port),
|
|
1441
|
+
scaffoldPackageDir,
|
|
1442
|
+
runtimeStateDir: entryStateDir,
|
|
1443
|
+
logger: apiLogger,
|
|
1444
|
+
});
|
|
1445
|
+
if (hotLauncher) {
|
|
1446
|
+
const launcher = hotLauncher;
|
|
1447
|
+
try {
|
|
1448
|
+
await launcher.start();
|
|
1449
|
+
launchers.set(runtimeId, launcher);
|
|
1450
|
+
unwiredRuntimes.delete(runtimeId);
|
|
1451
|
+
unwiredPhase = undefined;
|
|
1452
|
+
runtimeHandles.get(runtimeId)?.attachSupervisionSource(() => ({
|
|
1453
|
+
unwired: false,
|
|
1454
|
+
rows: launcher.describeSupervision(),
|
|
1455
|
+
}));
|
|
1456
|
+
}
|
|
1457
|
+
catch (startErr) {
|
|
1458
|
+
await launcher.stop().catch(() => { });
|
|
1459
|
+
hotLauncher = null;
|
|
1460
|
+
unwiredRuntimes.set(runtimeId, "install_launch");
|
|
1461
|
+
unwiredPhase = "install_launch";
|
|
1462
|
+
runtimeHandles.get(runtimeId)?.attachSupervisionSource(() => ({
|
|
1463
|
+
unwired: true,
|
|
1464
|
+
unwiredPhase: "install_launch",
|
|
1465
|
+
rows: [],
|
|
1466
|
+
}));
|
|
1467
|
+
eventScannersUnwired("install_launch", runtimeId, wallet, appendCause("[senpi-runtime] hot-install supervised scanner launch failed — runtime running without it", startErr), startErr);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
catch (wireErr) {
|
|
1472
|
+
// Wire failure must NOT fail the install — the runtime is already up. It IS reported
|
|
1473
|
+
// across the result boundary (payload.unwired) so a funding caller can refuse it.
|
|
1474
|
+
unwiredRuntimes.set(runtimeId, "install_wire");
|
|
1475
|
+
unwiredPhase = "install_wire";
|
|
1476
|
+
runtimeHandles.get(runtimeId)?.attachSupervisionSource(() => ({
|
|
1477
|
+
unwired: true,
|
|
1478
|
+
unwiredPhase: "install_wire",
|
|
1479
|
+
rows: [],
|
|
1480
|
+
}));
|
|
1481
|
+
eventScannersUnwired("install_wire", runtimeId, wallet, appendCause("[senpi-runtime] hot-install wiring failed — runtime running without supervised scanners", wireErr), wireErr);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
else if (parsedConfig && !sharedIntakeContext && selectScanners(parsedConfig).length > 0) {
|
|
1485
|
+
// Supervised intake is DISABLED for the whole box (corrupt acceptance record).
|
|
1486
|
+
// The wire block above is skipped, so this hot-installed runtime declares
|
|
1487
|
+
// external scanners but has NO live entry scanners. Record that fail-closed
|
|
1488
|
+
// state (state only, no new event) so listRuntimes/health don't lie.
|
|
1489
|
+
unwiredRuntimes.set(runtimeId, "no_intake");
|
|
1490
|
+
unwiredPhase = "no_intake";
|
|
1491
|
+
runtimeHandles.get(runtimeId)?.attachSupervisionSource(() => ({
|
|
1492
|
+
unwired: true,
|
|
1493
|
+
unwiredPhase: "no_intake",
|
|
1494
|
+
rows: [],
|
|
1495
|
+
}));
|
|
1496
|
+
}
|
|
1497
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1498
|
+
// The stop may have landed during the wire: its teardown then missed the launcher above.
|
|
1499
|
+
if (orphaned())
|
|
1500
|
+
return abandon();
|
|
1501
|
+
return unwiredPhase ? { status: "started", unwiredPhase } : { status: "started" };
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Give a runtime an orphaned install (or adoption) started to the live registration
|
|
1505
|
+
* ({@link RuntimeProcessState.runtimeHost}), or queue it for the next service start when no
|
|
1506
|
+
* registration is live. Never throws.
|
|
1507
|
+
*/
|
|
1508
|
+
async function handOffRuntime(runtimeId, walletKey) {
|
|
1509
|
+
const host = processState.runtimeHost;
|
|
1510
|
+
if (!host) {
|
|
1511
|
+
// The strategies store is left as it is: a registration that is starting may already run this
|
|
1512
|
+
// row from its boot, and the next start's boot or adoption sets the entry either way.
|
|
1513
|
+
processState.pendingRuntimeHandOffs.set(runtimeId, walletKey);
|
|
1514
|
+
pluginLogger.info("[senpi-runtime] runtime handed to the next service start (no live registration)", { runtimeId });
|
|
1515
|
+
return { outcome: "pending" };
|
|
1516
|
+
}
|
|
1517
|
+
try {
|
|
1518
|
+
return await host.adoptRuntime({ runtimeId, walletKey });
|
|
1519
|
+
}
|
|
1520
|
+
catch (err) {
|
|
1521
|
+
// Nothing runs it: drop the entry the stopped start wrote, so the apps do not draw it.
|
|
1522
|
+
strategiesStore.delete(runtimeId);
|
|
1523
|
+
return { outcome: "failed", message: err instanceof Error ? err.message : String(err) };
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* The live registration, when this registration's service has stopped (a 10.x plugin reload)
|
|
1528
|
+
* and another one is running. A deploy job from before the reload keeps this registration's
|
|
1529
|
+
* closures; its deletes and liveness reads go to the registration that holds the runtimes.
|
|
1530
|
+
*/
|
|
1531
|
+
function liveHostIfStopped() {
|
|
1532
|
+
if (serviceState !== "stopped")
|
|
1533
|
+
return undefined;
|
|
1534
|
+
const host = processState.runtimeHost;
|
|
1535
|
+
return host && host !== runtimeHost ? host : undefined;
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* The install result for a runtime this install handed over. A stopped runtime is never
|
|
1539
|
+
* reported as running here: `handedOff` names who runs it, and the result is an error when
|
|
1540
|
+
* nothing does.
|
|
1541
|
+
*/
|
|
1542
|
+
function installHandOffResult(handOff, runtime) {
|
|
1543
|
+
switch (handOff.outcome) {
|
|
1544
|
+
case "running":
|
|
1545
|
+
case "booted":
|
|
1546
|
+
return {
|
|
1547
|
+
ok: true,
|
|
1548
|
+
payload: {
|
|
1549
|
+
...runtime,
|
|
1550
|
+
id: handOff.runtimeId,
|
|
1551
|
+
...(handOff.unwired ? { unwired: handOff.unwired } : {}),
|
|
1552
|
+
handedOff: "The runtime plugin's service stopped during this install (a plugin reload or a gateway " +
|
|
1553
|
+
"stop), so the plugin registration that is running now runs this runtime " +
|
|
1554
|
+
`(runtime "${handOff.runtimeId}"). Check it: openclaw senpi runtime list.`,
|
|
1555
|
+
},
|
|
1556
|
+
};
|
|
1557
|
+
case "pending":
|
|
1558
|
+
return {
|
|
1559
|
+
ok: true,
|
|
1560
|
+
payload: {
|
|
1561
|
+
...runtime,
|
|
1562
|
+
handedOff: "The runtime plugin's service stopped during this install (a plugin reload or a gateway " +
|
|
1563
|
+
"stop), and no plugin registration had finished starting when it ended. The runtime is " +
|
|
1564
|
+
"installed; the plugin's service runs it when its start finishes. Check it: " +
|
|
1565
|
+
"openclaw senpi runtime list.",
|
|
1566
|
+
},
|
|
1567
|
+
};
|
|
1568
|
+
case "no-row":
|
|
1569
|
+
return { ok: false, error: {
|
|
1570
|
+
code: "UNAVAILABLE",
|
|
1571
|
+
message: `The runtime plugin's service stopped during this install and runtime "${runtime.id}" is no ` +
|
|
1572
|
+
"longer in the registry, so nothing runs it. Nothing is trading on this wallet from this " +
|
|
1573
|
+
"install. Check: openclaw senpi runtime list.",
|
|
1574
|
+
} };
|
|
1575
|
+
case "failed":
|
|
1576
|
+
return { ok: false, error: {
|
|
1577
|
+
code: "UNAVAILABLE",
|
|
1578
|
+
message: `The runtime plugin's service stopped during this install and the running plugin ` +
|
|
1579
|
+
`registration could not start runtime "${runtime.id}": ${handOff.message}. It is installed; ` +
|
|
1580
|
+
"the next restart tries to start it. Check: openclaw senpi runtime list.",
|
|
1581
|
+
} };
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* This registration as the process's {@link RuntimeHost}: published at the end of the service
|
|
1586
|
+
* start, withdrawn at the start of its stop.
|
|
1587
|
+
*/
|
|
1588
|
+
const runtimeHost = {
|
|
1589
|
+
async adoptRuntime(request) {
|
|
1590
|
+
const handOff = await adoptRuntimeCore(request);
|
|
1591
|
+
// Nothing runs it here: drop the entry the stopped registration's start wrote (run() sets it,
|
|
1592
|
+
// and that start's abandon does not clear it), so the apps do not draw a strategy that is not
|
|
1593
|
+
// running. A different id on the wallet leaves this id's entry just as stale.
|
|
1594
|
+
if (handOff.outcome === "no-row" ||
|
|
1595
|
+
handOff.outcome === "failed" ||
|
|
1596
|
+
(handOff.outcome === "running" && handOff.runtimeId !== request.runtimeId)) {
|
|
1597
|
+
strategiesStore.delete(request.runtimeId);
|
|
1598
|
+
}
|
|
1599
|
+
return handOff;
|
|
1600
|
+
},
|
|
1601
|
+
deleteRuntime: (params) => deleteRuntimeCore(params),
|
|
1602
|
+
runtimeIsLive: async (id) => runtimeHandles.has(id) && !unwiredRuntimes.has(id),
|
|
1603
|
+
getScannerRows: (id) => readScannerRows(id),
|
|
1604
|
+
};
|
|
1605
|
+
async function adoptRuntimeCore({ runtimeId, walletKey }) {
|
|
1606
|
+
const stopsAtAdopt = serviceStops;
|
|
1607
|
+
const orphaned = () => serviceState !== "running" || serviceStops !== stopsAtAdopt;
|
|
1608
|
+
try {
|
|
1609
|
+
const onWallet = runtimeHandles.has(runtimeId)
|
|
1610
|
+
? runtimeId
|
|
1611
|
+
: walletKey
|
|
1612
|
+
? [...runtimeHandles].find(([, h]) => (h.wallet ?? "").trim().toLowerCase() === walletKey)?.[0]
|
|
1613
|
+
: undefined;
|
|
1614
|
+
if (onWallet !== undefined) {
|
|
1615
|
+
// The live verdict, as for a start: a runtime running blind is not a clean install.
|
|
1616
|
+
const unwired = unwiredRuntimes.get(onWallet);
|
|
1617
|
+
return unwired ? { outcome: "running", runtimeId: onWallet, unwired } : { outcome: "running", runtimeId: onWallet };
|
|
1618
|
+
}
|
|
1619
|
+
const { readRegistry } = await import("./runtime/strategy-registry.js");
|
|
1620
|
+
const entry = (await readRegistry(baseStateDir)).runtimes.find((e) => e.id === runtimeId);
|
|
1621
|
+
if (!entry)
|
|
1622
|
+
return { outcome: "no-row" };
|
|
1623
|
+
const inputs = factsPlanInputs(entry);
|
|
1624
|
+
if (!inputs)
|
|
1625
|
+
return { outcome: "failed", message: "its registry row has no readable recipe" };
|
|
1626
|
+
const parsedConfig = await loadRuntimeConfig(inputs.pathOrContent, { fromPath: inputs.fromPath });
|
|
1627
|
+
const started = await startRuntimeLive({
|
|
1628
|
+
runtimeId: entry.id,
|
|
1629
|
+
pathOrContent: inputs.pathOrContent,
|
|
1630
|
+
fromPath: inputs.fromPath,
|
|
1631
|
+
recipeDir: inputs.runtimeYamlDir,
|
|
1632
|
+
wallet: entry.wallet ?? "",
|
|
1633
|
+
parsedConfig,
|
|
1634
|
+
}, orphaned);
|
|
1635
|
+
if (started.status === "orphaned")
|
|
1636
|
+
return handOffRuntime(runtimeId, walletKey);
|
|
1637
|
+
pluginLogger.info("[senpi-runtime] started a runtime handed over by a stopped registration", {
|
|
1638
|
+
runtimeId: entry.id,
|
|
1639
|
+
});
|
|
1640
|
+
return started.unwiredPhase
|
|
1641
|
+
? { outcome: "booted", runtimeId: entry.id, unwired: started.unwiredPhase }
|
|
1642
|
+
: { outcome: "booted", runtimeId: entry.id };
|
|
1643
|
+
}
|
|
1644
|
+
catch (err) {
|
|
1645
|
+
return { outcome: "failed", message: err instanceof Error ? err.message : String(err) };
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1231
1648
|
async function installRuntimeCore(params) {
|
|
1649
|
+
// How many times this registration's service had stopped when this install began; see `orphaned` below.
|
|
1650
|
+
const stopsAtInstall = serviceStops;
|
|
1232
1651
|
const toError = (message) => ({ code: "INVALID_REQUEST", message });
|
|
1233
1652
|
const toUnavailable = (message) => ({ code: "UNAVAILABLE", message });
|
|
1234
1653
|
const { readRegistry, writeRegistry, withRegistryLock, validateEntry, hasId, findByWallet, deriveRuntimeId, sanitizeId } = await import("./runtime/strategy-registry.js");
|
|
@@ -1440,7 +1859,7 @@ const senpiRuntimePlugin = {
|
|
|
1440
1859
|
if (!mcpTransport)
|
|
1441
1860
|
await client.connect();
|
|
1442
1861
|
try {
|
|
1443
|
-
const strategies = await client.
|
|
1862
|
+
const strategies = await client.listStrategiesForWallets([wallet]);
|
|
1444
1863
|
const match = strategies.find((s) => s.strategyWalletAddress.toLowerCase() === wallet.toLowerCase());
|
|
1445
1864
|
if (!match) {
|
|
1446
1865
|
pluginLogger.info("[senpi-runtime] Wallet validation failed: not owned", { wallet });
|
|
@@ -1496,127 +1915,64 @@ const senpiRuntimePlugin = {
|
|
|
1496
1915
|
const current = await readRegistry(baseStateDir);
|
|
1497
1916
|
await writeRegistry(baseStateDir, { ...current, runtimes: [...current.runtimes, entry] });
|
|
1498
1917
|
});
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1918
|
+
// The registration's service stopped since this install began (a 10.x plugin reload): the
|
|
1919
|
+
// runtime belongs to the live registration, so this install hands it over instead of keeping
|
|
1920
|
+
// one in a stopped registration that nothing can stop or delete.
|
|
1921
|
+
const orphaned = () => serviceState === "stopped" || serviceStops !== stopsAtInstall;
|
|
1922
|
+
// Registry create fact — distinct from the runtime.started boot transition (which also fires
|
|
1923
|
+
// on a transient restart). Once per install, as soon as a registration runs the row: right
|
|
1924
|
+
// after run() here, or at the hand-off. Inside emitEvent's guard so telemetry never fails the
|
|
1925
|
+
// install.
|
|
1926
|
+
let createdEmitted = false;
|
|
1927
|
+
const emitCreated = () => {
|
|
1928
|
+
if (createdEmitted)
|
|
1929
|
+
return;
|
|
1930
|
+
createdEmitted = true;
|
|
1931
|
+
emitEvent(apiLogger, () => buildRuntimeCreatedEvent({ runtimeId: resolvedId, address: wallet, recipeName: runtimeName }));
|
|
1932
|
+
};
|
|
1933
|
+
const handOffInstall = async () => {
|
|
1934
|
+
const handOff = await handOffRuntime(resolvedId, walletKey);
|
|
1935
|
+
if (handOff.outcome === "running" || handOff.outcome === "booted" || handOff.outcome === "pending")
|
|
1936
|
+
emitCreated();
|
|
1937
|
+
return installHandOffResult(handOff, { id: resolvedId, name: runtimeName, wallet });
|
|
1938
|
+
};
|
|
1506
1939
|
try {
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
const entryStateDir = buildRuntimeStateDir(baseStateDir, resolvedId, wallet);
|
|
1512
|
-
const handle = await run(pathOrContent, {
|
|
1513
|
-
fromPath: hasPath,
|
|
1514
|
-
stateDir: entryStateDir,
|
|
1515
|
-
mcpTransport,
|
|
1516
|
-
apiKey,
|
|
1517
|
-
scannerProviders,
|
|
1940
|
+
// Stopped before the start: do not start a runtime here at all.
|
|
1941
|
+
if (orphaned())
|
|
1942
|
+
return await handOffInstall();
|
|
1943
|
+
const started = await startRuntimeLive({
|
|
1518
1944
|
runtimeId: resolvedId,
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
// senpi.installRuntime, while the API is already listening — the
|
|
1528
|
-
// counterpart to the boot-time `launcher` above. A runtime takes exactly
|
|
1529
|
-
// one path: boot-present → `launcher` (deferred start); installed live →
|
|
1530
|
-
// `hotLauncher` (immediate start). The two are mutually exclusive because
|
|
1531
|
-
// installRuntime rejects any id/wallet already in the registry before
|
|
1532
|
-
// wiring (the hasId guard), so a boot-present runtime never re-enters here.
|
|
1533
|
-
// The API is already listening at this point, so we wire and start
|
|
1534
|
-
// immediately (no deferred start needed). Any failure is swallowed so
|
|
1535
|
-
// the install itself is not rolled back — the runtime is already up.
|
|
1536
|
-
// Gate on the same API-disabled flag as the boot path: when the HTTP API
|
|
1537
|
-
// is off (a test-only seam), the /signals route is never mounted, so
|
|
1538
|
-
// starting scanners would only spawn children that POST to a closed port.
|
|
1539
|
-
if (parsedConfig &&
|
|
1540
|
-
sharedIntakeContext &&
|
|
1541
|
-
process.env.SENPI_RUNTIME_API_DISABLED !== "true") {
|
|
1542
|
-
try {
|
|
1543
|
-
const apiConfig = loadApiConfig(api.pluginConfig);
|
|
1544
|
-
const scaffoldPackageDir = fileURLToPath(new URL("./scanners/external", import.meta.url));
|
|
1545
|
-
const entryStateDir = buildRuntimeStateDir(baseStateDir, resolvedId, wallet);
|
|
1546
|
-
const hotLauncher = wireRuntime({
|
|
1547
|
-
runtimeConfig: parsedConfig,
|
|
1548
|
-
recipeDir: hasPath ? dirname(pathOrContent) : runtimeYamlDir,
|
|
1549
|
-
runtimeId: resolvedId,
|
|
1550
|
-
wallet,
|
|
1551
|
-
sharedCtx: sharedIntakeContext,
|
|
1552
|
-
runtimeHandles,
|
|
1553
|
-
intakeUrl: intakeUrlFor(apiConfig.host, apiConfig.port),
|
|
1554
|
-
scaffoldPackageDir,
|
|
1555
|
-
runtimeStateDir: entryStateDir,
|
|
1556
|
-
logger: apiLogger,
|
|
1557
|
-
});
|
|
1558
|
-
if (hotLauncher) {
|
|
1559
|
-
try {
|
|
1560
|
-
await hotLauncher.start();
|
|
1561
|
-
launchers.set(resolvedId, hotLauncher);
|
|
1562
|
-
unwiredRuntimes.delete(resolvedId);
|
|
1563
|
-
unwiredPhase = undefined;
|
|
1564
|
-
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1565
|
-
unwired: false,
|
|
1566
|
-
rows: hotLauncher.describeSupervision(),
|
|
1567
|
-
}));
|
|
1568
|
-
}
|
|
1569
|
-
catch (startErr) {
|
|
1570
|
-
await hotLauncher.stop().catch(() => { });
|
|
1571
|
-
unwiredRuntimes.set(resolvedId, "install_launch");
|
|
1572
|
-
unwiredPhase = "install_launch";
|
|
1573
|
-
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1574
|
-
unwired: true,
|
|
1575
|
-
unwiredPhase: "install_launch",
|
|
1576
|
-
rows: [],
|
|
1577
|
-
}));
|
|
1578
|
-
eventScannersUnwired("install_launch", resolvedId, wallet, appendCause("[senpi-runtime] hot-install supervised scanner launch failed — runtime running without it", startErr), startErr);
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
catch (wireErr) {
|
|
1583
|
-
// Wire failure must NOT fail the install — the runtime is already up. It IS reported
|
|
1584
|
-
// across the result boundary (payload.unwired) so a funding caller can refuse it.
|
|
1585
|
-
unwiredRuntimes.set(resolvedId, "install_wire");
|
|
1586
|
-
unwiredPhase = "install_wire";
|
|
1587
|
-
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1588
|
-
unwired: true,
|
|
1589
|
-
unwiredPhase: "install_wire",
|
|
1590
|
-
rows: [],
|
|
1591
|
-
}));
|
|
1592
|
-
eventScannersUnwired("install_wire", resolvedId, wallet, appendCause("[senpi-runtime] hot-install wiring failed — runtime running without supervised scanners", wireErr), wireErr);
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
else if (parsedConfig && !sharedIntakeContext && selectScanners(parsedConfig).length > 0) {
|
|
1596
|
-
// Supervised intake is DISABLED for the whole box (corrupt acceptance record).
|
|
1597
|
-
// The wire block above is skipped, so this hot-installed runtime declares
|
|
1598
|
-
// external scanners but has NO live entry scanners. Record that fail-closed
|
|
1599
|
-
// state (state only, no new event) so listRuntimes/health don't lie.
|
|
1600
|
-
unwiredRuntimes.set(resolvedId, "no_intake");
|
|
1601
|
-
unwiredPhase = "no_intake";
|
|
1602
|
-
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1603
|
-
unwired: true,
|
|
1604
|
-
unwiredPhase: "no_intake",
|
|
1605
|
-
rows: [],
|
|
1606
|
-
}));
|
|
1607
|
-
}
|
|
1608
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
1945
|
+
pathOrContent,
|
|
1946
|
+
fromPath: hasPath,
|
|
1947
|
+
recipeDir: hasPath ? dirname(pathOrContent) : runtimeYamlDir,
|
|
1948
|
+
wallet,
|
|
1949
|
+
parsedConfig,
|
|
1950
|
+
}, orphaned, emitCreated);
|
|
1951
|
+
if (started.status === "orphaned")
|
|
1952
|
+
return await handOffInstall();
|
|
1609
1953
|
return {
|
|
1610
1954
|
ok: true,
|
|
1611
1955
|
payload: {
|
|
1612
1956
|
id: resolvedId,
|
|
1613
1957
|
name: runtimeName,
|
|
1614
1958
|
wallet,
|
|
1615
|
-
...(unwiredPhase ? { unwired: unwiredPhase } : {}),
|
|
1959
|
+
...(started.unwiredPhase ? { unwired: started.unwiredPhase } : {}),
|
|
1616
1960
|
},
|
|
1617
1961
|
};
|
|
1618
1962
|
}
|
|
1619
1963
|
catch (err) {
|
|
1964
|
+
// run() threw after this registration's service stopped: the live registration may already
|
|
1965
|
+
// run this row (its boot read it), so it decides, not this catch. It runs it, or starts it,
|
|
1966
|
+
// or (no registration live yet) the next start does; only when it cannot start the row
|
|
1967
|
+
// either is the row removed, as below for an install with no reload.
|
|
1968
|
+
if (orphaned()) {
|
|
1969
|
+
const handOff = await handOffRuntime(resolvedId, walletKey);
|
|
1970
|
+
if (handOff.outcome !== "failed") {
|
|
1971
|
+
if (handOff.outcome !== "no-row")
|
|
1972
|
+
emitCreated();
|
|
1973
|
+
return installHandOffResult(handOff, { id: resolvedId, name: runtimeName, wallet });
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1620
1976
|
await withRegistryLock(baseStateDir, async () => {
|
|
1621
1977
|
const current = await readRegistry(baseStateDir);
|
|
1622
1978
|
await writeRegistry(baseStateDir, { ...current, runtimes: current.runtimes.filter((e) => e.id !== resolvedId) });
|
|
@@ -1681,11 +2037,18 @@ const senpiRuntimePlugin = {
|
|
|
1681
2037
|
* unwired-verdict cleanup all happen exactly once, here.
|
|
1682
2038
|
*/
|
|
1683
2039
|
async function deleteRuntimeCore(params) {
|
|
2040
|
+
// This registration's service stopped (a 10.x reload) and holds no runtimes; the live one does.
|
|
2041
|
+
// A delete from here (a deploy job from before the reload) would remove the row and archive the
|
|
2042
|
+
// state dir under a runtime that keeps trading.
|
|
2043
|
+
const live = liveHostIfStopped();
|
|
2044
|
+
if (live)
|
|
2045
|
+
return live.deleteRuntime(params);
|
|
1684
2046
|
let targetId = typeof params?.id === "string" ? params.id : undefined;
|
|
1685
2047
|
const targetAddress = typeof params?.address === "string" ? params.address : undefined;
|
|
1686
2048
|
// Set only by the lifecycle reaper: why the strategy counts as gone (stamped on runtime.deleted)
|
|
1687
2049
|
// and which wallet that verdict was about (the concurrency guard below).
|
|
1688
|
-
const deleteReason = params?.deleteReason === "closed_status"
|
|
2050
|
+
const deleteReason = params?.deleteReason === "closed_status" || params?.deleteReason === "paper_failed_status"
|
|
2051
|
+
? params.deleteReason
|
|
1689
2052
|
: undefined;
|
|
1690
2053
|
const expectWallet = typeof params?.expectWallet === "string" && params.expectWallet.trim().length > 0
|
|
1691
2054
|
? params.expectWallet.trim().toLowerCase()
|
|
@@ -1918,7 +2281,7 @@ const senpiRuntimePlugin = {
|
|
|
1918
2281
|
* the strategy id is read for this one wallet; a failed read logs its reason and sends nothing.
|
|
1919
2282
|
*/
|
|
1920
2283
|
function queueFactsSend(runtimeId, strategyIds) {
|
|
1921
|
-
return factsSendLock.withKeyLock(runtimeId, async () => {
|
|
2284
|
+
return factsSendLock().withKeyLock(runtimeId, async () => {
|
|
1922
2285
|
const { readRegistry } = await import("./runtime/strategy-registry.js");
|
|
1923
2286
|
const entry = (await readRegistry(baseStateDir)).runtimes.find((e) => e.id === runtimeId);
|
|
1924
2287
|
const inputs = entry ? factsPlanInputs(entry) : null;
|
|
@@ -1929,7 +2292,7 @@ const senpiRuntimePlugin = {
|
|
|
1929
2292
|
return;
|
|
1930
2293
|
let ids = strategyIds ?? null;
|
|
1931
2294
|
if (strategyIds === undefined && !plan.unchanged) {
|
|
1932
|
-
const read = await readBootWalletStatuses([plan.wallet.trim().toLowerCase()], (wallets, signal) => withSenpiClient((client) => client.
|
|
2295
|
+
const read = await readBootWalletStatuses([plan.wallet.trim().toLowerCase()], (wallets, signal) => withSenpiClient((client) => client.listStrategiesForWallets(wallets, { signal })));
|
|
1933
2296
|
if (!read.ok) {
|
|
1934
2297
|
factsLogger.warn("strategy facts failed: could not read the strategy id; retrying next boot", undefined, {
|
|
1935
2298
|
runtimeId: plan.runtimeId,
|
|
@@ -2177,18 +2540,24 @@ const senpiRuntimePlugin = {
|
|
|
2177
2540
|
},
|
|
2178
2541
|
registryFindByWallet: findRegistryEntryByWallet,
|
|
2179
2542
|
registryFindById: findRegistryEntryById,
|
|
2180
|
-
|
|
2543
|
+
countRegistryRuntimes: async () => {
|
|
2544
|
+
const { readRegistry } = await import("./runtime/strategy-registry.js");
|
|
2545
|
+
return (await readRegistry(baseStateDir)).runtimes.length;
|
|
2546
|
+
},
|
|
2547
|
+
// A deploy job outlives a reload; it reads the runtime where it runs (see `liveHostIfStopped`).
|
|
2548
|
+
getScannerRows: (id) => liveHostIfStopped()?.getScannerRows(id) ?? readScannerRows(id),
|
|
2181
2549
|
// H4: `runtimeHandles` holds the live `run()` handles. A registry row without one is a
|
|
2182
2550
|
// previous install whose runtime died before start — deploy must reinstall, not report it
|
|
2183
2551
|
// adopted. `unwiredRuntimes` is the same judgement one step further in: a runtime that is up
|
|
2184
2552
|
// but whose entry scanners never wired is running BLIND (the health surface already renders
|
|
2185
2553
|
// it "running — NO ENTRY SCANNERS"), and reporting `install: skipped` over it would be the
|
|
2186
2554
|
// same clean line over a runtime that cannot tick.
|
|
2187
|
-
runtimeIsLive: async (id) => runtimeHandles.has(id) && !unwiredRuntimes.has(id),
|
|
2555
|
+
runtimeIsLive: async (id) => liveHostIfStopped()?.runtimeIsLive(id) ?? (runtimeHandles.has(id) && !unwiredRuntimes.has(id)),
|
|
2188
2556
|
installPendingOnWallet: async (wallet) => installsInFlight.has(wallet.trim().toLowerCase()),
|
|
2557
|
+
// The process logger, not this registration's: a deploy job outlives a reload.
|
|
2189
2558
|
logger: {
|
|
2190
|
-
info: (line) =>
|
|
2191
|
-
warn: (line) =>
|
|
2559
|
+
info: (line) => processLog.info(`[senpi-runtime] ${line}`),
|
|
2560
|
+
warn: (line) => processLog.warn(`[senpi-runtime] ${line}`),
|
|
2192
2561
|
},
|
|
2193
2562
|
});
|
|
2194
2563
|
/**
|
|
@@ -2392,7 +2761,7 @@ const senpiRuntimePlugin = {
|
|
|
2392
2761
|
respond(true, {
|
|
2393
2762
|
status: status ? { ...status, runtimeId } : null,
|
|
2394
2763
|
updater,
|
|
2395
|
-
approvals:
|
|
2764
|
+
approvals: approvalsStatus(),
|
|
2396
2765
|
deploy: { capabilities: DEPLOY_CAPABILITIES },
|
|
2397
2766
|
});
|
|
2398
2767
|
}
|
|
@@ -2406,7 +2775,7 @@ const senpiRuntimePlugin = {
|
|
|
2406
2775
|
return;
|
|
2407
2776
|
}
|
|
2408
2777
|
const statuses = await collectRuntimeHealthStatuses(runtimeHandles, api.logger);
|
|
2409
|
-
respond(true, { statuses, updater, approvals:
|
|
2778
|
+
respond(true, { statuses, updater, approvals: approvalsStatus(), deploy: { capabilities: DEPLOY_CAPABILITIES } });
|
|
2410
2779
|
}, { scope: "operator.read" });
|
|
2411
2780
|
api.registerGatewayMethod("senpi.getSystemState", async (ctx) => {
|
|
2412
2781
|
const respond = ctx.respond;
|