@senpi/runtime 3.0.118 → 3.0.123
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/open-position/open-position.action.d.ts.map +1 -1
- package/dist/actions/open-position/open-position.action.js +9 -0
- package/dist/actions/open-position/open-position.action.js.map +1 -1
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +16 -8
- package/dist/cli/guide-commands.js.map +1 -1
- package/dist/constants/dsl/index.d.ts +17 -6
- package/dist/constants/dsl/index.d.ts.map +1 -1
- package/dist/constants/dsl/index.js +17 -6
- package/dist/constants/dsl/index.js.map +1 -1
- package/dist/dsl/engine/floors.d.ts +25 -1
- package/dist/dsl/engine/floors.d.ts.map +1 -1
- package/dist/dsl/engine/floors.js +38 -6
- package/dist/dsl/engine/floors.js.map +1 -1
- package/dist/dsl/engine/index.d.ts +1 -1
- package/dist/dsl/engine/index.d.ts.map +1 -1
- package/dist/dsl/engine/index.js +1 -1
- package/dist/dsl/engine/index.js.map +1 -1
- package/dist/dsl/engine/round.d.ts +22 -2
- package/dist/dsl/engine/round.d.ts.map +1 -1
- package/dist/dsl/engine/round.js +78 -5
- package/dist/dsl/engine/round.js.map +1 -1
- package/dist/dsl/engine/tick.d.ts.map +1 -1
- package/dist/dsl/engine/tick.js +31 -16
- package/dist/dsl/engine/tick.js.map +1 -1
- package/dist/dsl/events/initial-state.d.ts +1 -1
- package/dist/dsl/events/initial-state.d.ts.map +1 -1
- package/dist/dsl/events/initial-state.js +4 -7
- package/dist/dsl/events/initial-state.js.map +1 -1
- package/dist/dsl/monitor/exchange-sync.d.ts +8 -3
- package/dist/dsl/monitor/exchange-sync.d.ts.map +1 -1
- package/dist/dsl/monitor/exchange-sync.js +16 -4
- package/dist/dsl/monitor/exchange-sync.js.map +1 -1
- package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
- package/dist/dsl/monitor/process-one-position.js +88 -3
- package/dist/dsl/monitor/process-one-position.js.map +1 -1
- package/dist/dsl/plugin/index.d.ts +8 -4
- package/dist/dsl/plugin/index.d.ts.map +1 -1
- package/dist/dsl/plugin/index.js +8 -4
- package/dist/dsl/plugin/index.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +168 -7
- package/dist/index.js.map +1 -1
- package/dist/runtime/boot-skip.d.ts +73 -0
- package/dist/runtime/boot-skip.d.ts.map +1 -0
- package/dist/runtime/boot-skip.js +113 -0
- package/dist/runtime/boot-skip.js.map +1 -0
- package/dist/runtime/create-runtime-context.d.ts +2 -0
- package/dist/runtime/create-runtime-context.d.ts.map +1 -1
- package/dist/runtime/create-runtime-context.js +1 -0
- package/dist/runtime/create-runtime-context.js.map +1 -1
- package/dist/runtime/lifecycle-events.d.ts +42 -1
- package/dist/runtime/lifecycle-events.d.ts.map +1 -1
- package/dist/runtime/lifecycle-events.js +81 -1
- package/dist/runtime/lifecycle-events.js.map +1 -1
- package/dist/runtime/lifecycle-reaper.d.ts +202 -0
- package/dist/runtime/lifecycle-reaper.d.ts.map +1 -0
- package/dist/runtime/lifecycle-reaper.js +390 -0
- package/dist/runtime/lifecycle-reaper.js.map +1 -0
- package/dist/runtime/run.d.ts +5 -0
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +1 -0
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/runtime.d.ts +2 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +1 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/update-apply-deps.d.ts +23 -2
- package/dist/runtime/update-apply-deps.d.ts.map +1 -1
- package/dist/runtime/update-apply-deps.js +11 -5
- package/dist/runtime/update-apply-deps.js.map +1 -1
- package/dist/skills-manager/manifest.d.ts.map +1 -1
- package/dist/skills-manager/manifest.js +1 -0
- package/dist/skills-manager/manifest.js.map +1 -1
- package/dist/skills-manager/skills-manifest.json +1 -0
- package/dist/types/dsl/index.d.ts +1 -1
- package/dist/types/dsl/index.d.ts.map +1 -1
- package/dist/types/runtime.d.ts +8 -0
- package/dist/types/runtime.d.ts.map +1 -1
- package/dist/utils/event-catalog.d.ts +91 -0
- package/dist/utils/event-catalog.d.ts.map +1 -1
- package/dist/utils/event-catalog.js +13 -0
- package/dist/utils/event-catalog.js.map +1 -1
- package/dist/utils/keyed-mutex.d.ts +3 -2
- package/dist/utils/keyed-mutex.d.ts.map +1 -1
- package/dist/utils/keyed-mutex.js +3 -2
- package/dist/utils/keyed-mutex.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,10 +16,11 @@ import { parse as parseYaml } from "yaml";
|
|
|
16
16
|
import { join, dirname } from "node:path";
|
|
17
17
|
import { SenpiClient } from "./senpi/client.js";
|
|
18
18
|
import { buildWalletStatusHint } from "./senpi/strategy-status.js";
|
|
19
|
+
import { bootSkippedStatusLabel, partitionByWalletStatus, readBootWalletStatuses, trackableWalletKeys, } from "./runtime/boot-skip.js";
|
|
19
20
|
import { DeployJobController } from "./deploy/job.js";
|
|
20
21
|
import { registerDeployGatewayMethods } from "./deploy/register.js";
|
|
21
22
|
import { registerUpdateGatewayMethod } from "./runtime/update-register.js";
|
|
22
|
-
import { checkProofCoversRecipe, makeApplyToRuntime, makeWriteRegistry, } from "./runtime/update-apply-deps.js";
|
|
23
|
+
import { checkProofCoversRecipe, makeApplyToRuntime, makeWriteRegistry, runtimeChangeLock, } from "./runtime/update-apply-deps.js";
|
|
23
24
|
import { INSTALL_DEADLINE_MS, WALLET_INSTALL_IN_FLIGHT_CODE } from "./deploy/deadlines.js";
|
|
24
25
|
import { AuditQueryError } from "./runtime/audit-query.types.js";
|
|
25
26
|
import { buildRuntimeStateDir, archiveStateDir } from "./runtime/state-layout.js";
|
|
@@ -52,7 +53,8 @@ import { registerToolApprovalGate, resolveToolApprovalsMode, } from "./approvals
|
|
|
52
53
|
import { registerShowWidgetTool } from "./widgets/index.js";
|
|
53
54
|
import { EVENT_NAME, SENPI_ERROR_CODE, eventAttrs, scannerUnwiredErrorCode } from "./utils/event-catalog.js";
|
|
54
55
|
import { emitEvent } from "./actions/event-derivation.js";
|
|
55
|
-
import {
|
|
56
|
+
import { createLifecycleReaper, resolveReaperBootDelayMs, resolveReaperEnforcement, } from "./runtime/lifecycle-reaper.js";
|
|
57
|
+
import { buildRuntimeBootSkippedEvent, buildRuntimeBootStatusUnreadEvent, buildRuntimeCreatedEvent, buildRuntimeDeletedEvent, buildRuntimeUpdatedEvent, } from "./runtime/lifecycle-events.js";
|
|
56
58
|
import { isWalletAddress } from "./utils/wallet-address.js";
|
|
57
59
|
import { fileURLToPath } from "node:url";
|
|
58
60
|
import { loadApiConfig } from "./runtime-api/config.js";
|
|
@@ -374,6 +376,10 @@ const senpiRuntimePlugin = {
|
|
|
374
376
|
// cleared on a successful wire/install. senpi.listRuntimes reads it to render
|
|
375
377
|
// "running — NO ENTRY SCANNERS" instead of a bare "running" for a blind runtime.
|
|
376
378
|
const unwiredRuntimes = new Map();
|
|
379
|
+
// Registered runtimes boot left down because the platform reports their strategy wallet dead,
|
|
380
|
+
// keyed runtimeId → that status. senpi.listRuntimes reads it so the row says why it is stopped;
|
|
381
|
+
// cleared when the row is deleted.
|
|
382
|
+
const bootSkippedRuntimes = new Map();
|
|
377
383
|
/**
|
|
378
384
|
* Wallets with an install IN FLIGHT in this process, keyed by lowercased address.
|
|
379
385
|
*
|
|
@@ -638,6 +644,39 @@ const senpiRuntimePlugin = {
|
|
|
638
644
|
now: () => Date.now(),
|
|
639
645
|
log: (line) => api.logger.info(`[senpi-runtime] ${line}`),
|
|
640
646
|
});
|
|
647
|
+
// ── lifecycle reaper ─────────────────────────────────────────────────────────────────────────
|
|
648
|
+
// A runtime exists iff its strategy is open. Every close path ends at the backend and none of
|
|
649
|
+
// them tells this box, so the reaper asks: once after boot, hourly, and whenever the backend
|
|
650
|
+
// rejects an order with STRATEGY_NOT_ACTIVE. Observe-only unless enforcement is on (see
|
|
651
|
+
// runtime/lifecycle-reaper.ts for the safety rule). Constructed here so both `run()` call sites
|
|
652
|
+
// (boot and install) can hand it their not-active rejections; it does no I/O until started.
|
|
653
|
+
const reaperBootDelayOverride = resolveReaperBootDelayMs(process.env);
|
|
654
|
+
const reaper = createLifecycleReaper({
|
|
655
|
+
readRegistry: async () => {
|
|
656
|
+
const { readRegistry } = await import("./runtime/strategy-registry.js");
|
|
657
|
+
return readRegistry(baseStateDir);
|
|
658
|
+
},
|
|
659
|
+
readStatuses: async (wallets, signal) => {
|
|
660
|
+
const client = mcpTransport ? new SenpiClient(mcpTransport) : new SenpiClient({ apiKey });
|
|
661
|
+
if (!mcpTransport)
|
|
662
|
+
await client.connect();
|
|
663
|
+
try {
|
|
664
|
+
return await client.listStrategies({ strategyAddresses: wallets }, { signal });
|
|
665
|
+
}
|
|
666
|
+
finally {
|
|
667
|
+
if (!mcpTransport)
|
|
668
|
+
await client.disconnect().catch(() => { });
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
deleteRuntime: async (id, reason, wallet) => {
|
|
672
|
+
const result = await deleteRuntimeCore({ id, deleteReason: reason, expectWallet: wallet });
|
|
673
|
+
return result.ok ? { ok: true } : { ok: false, code: result.error.code, message: result.error.message };
|
|
674
|
+
},
|
|
675
|
+
emit: (build) => emitEvent(apiLogger, build),
|
|
676
|
+
enforce: resolveReaperEnforcement(process.env),
|
|
677
|
+
logger: pluginLogger,
|
|
678
|
+
...(reaperBootDelayOverride !== undefined ? { bootDelayMs: reaperBootDelayOverride } : {}),
|
|
679
|
+
});
|
|
641
680
|
api.registerService({
|
|
642
681
|
id: "senpi-runtime",
|
|
643
682
|
async start() {
|
|
@@ -714,9 +753,48 @@ const senpiRuntimePlugin = {
|
|
|
714
753
|
// publishes a level rather than recording a run.
|
|
715
754
|
routeScaffoldTickToTelemetry = buildScaffoldTickRouter(ctx);
|
|
716
755
|
}
|
|
756
|
+
// One bounded read: which registered wallets the platform says are dead, so a restart does
|
|
757
|
+
// not re-scaffold runtimes on closed wallets (fail-open — see runtime/boot-skip.ts).
|
|
758
|
+
let walletStatuses = null;
|
|
759
|
+
// Lower-cased and trackable-only: the backend matches addresses case-sensitively against
|
|
760
|
+
// lower-case storage, and rejects the whole batch on one malformed address.
|
|
761
|
+
const registeredWallets = trackableWalletKeys(registry.runtimes);
|
|
762
|
+
if (registeredWallets.length > 0) {
|
|
763
|
+
const read = await readBootWalletStatuses(registeredWallets, async (wallets, signal) => {
|
|
764
|
+
const client = mcpTransport ? new SenpiClient(mcpTransport) : new SenpiClient({ apiKey });
|
|
765
|
+
if (!mcpTransport)
|
|
766
|
+
await client.connect();
|
|
767
|
+
try {
|
|
768
|
+
return await client.listStrategies({ strategyAddresses: wallets }, { signal });
|
|
769
|
+
}
|
|
770
|
+
finally {
|
|
771
|
+
if (!mcpTransport)
|
|
772
|
+
await client.disconnect().catch(() => { });
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
if (read.ok) {
|
|
776
|
+
walletStatuses = read.statuses;
|
|
777
|
+
reaper.recordStatuses(registeredWallets, read.statuses);
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
pluginLogger.warn("[senpi-runtime] Could not read strategy statuses at boot; booting every registered runtime", { reason: read.reason, err: read.message });
|
|
781
|
+
emitEvent(apiLogger, () => buildRuntimeBootStatusUnreadEvent({ reason: read.reason, registeredCount: registeredWallets.length }));
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const { boot: entriesToBoot, skipped } = partitionByWalletStatus(registry.runtimes, walletStatuses);
|
|
785
|
+
bootSkippedRuntimes.clear();
|
|
786
|
+
for (const { entry, status } of skipped) {
|
|
787
|
+
bootSkippedRuntimes.set(entry.id, status);
|
|
788
|
+
pluginLogger.info("[senpi-runtime] Not booting runtime: its strategy wallet is closed", {
|
|
789
|
+
runtimeId: entry.id,
|
|
790
|
+
wallet: entry.wallet,
|
|
791
|
+
status,
|
|
792
|
+
});
|
|
793
|
+
emitEvent(apiLogger, () => buildRuntimeBootSkippedEvent({ runtimeId: entry.id, address: entry.wallet ?? "", status }));
|
|
794
|
+
}
|
|
717
795
|
const started = [];
|
|
718
796
|
try {
|
|
719
|
-
for (const entry of
|
|
797
|
+
for (const entry of entriesToBoot) {
|
|
720
798
|
const hasPath = typeof entry.runtimeYamlPath === "string" && entry.runtimeYamlPath.length > 0;
|
|
721
799
|
const hasContent = typeof entry.runtimeYamlContent === "string";
|
|
722
800
|
if (hasPath && hasContent)
|
|
@@ -735,6 +813,7 @@ const senpiRuntimePlugin = {
|
|
|
735
813
|
apiKey,
|
|
736
814
|
scannerProviders,
|
|
737
815
|
runtimeId: entry.id,
|
|
816
|
+
onStrategyNotActive: (address) => reaper.checkWallet(address),
|
|
738
817
|
});
|
|
739
818
|
runtimeHandles.set(entry.id, handle);
|
|
740
819
|
started.push(entry.id);
|
|
@@ -911,6 +990,21 @@ const senpiRuntimePlugin = {
|
|
|
911
990
|
if (process.env.SENPI_RUNTIME_API_DISABLED !== "true") {
|
|
912
991
|
liveTelemetry.start();
|
|
913
992
|
}
|
|
993
|
+
// Last, once everything above is up: the reaper's post-boot pass and hourly sweep. Its
|
|
994
|
+
// not-active escalation is also off until here, so no delete can land while the boot loop is
|
|
995
|
+
// still wiring runtimes and launchers. Nothing here may fail start() — a throw from inside the boot loop takes every runtime down, which
|
|
996
|
+
// is exactly the failure a lifecycle check must never cause. The unit-test seam
|
|
997
|
+
// SENPI_RUNTIME_API_DISABLED keeps its timers off unless a boot delay is set explicitly.
|
|
998
|
+
try {
|
|
999
|
+
if (process.env.SENPI_RUNTIME_API_DISABLED !== "true" || reaperBootDelayOverride !== undefined) {
|
|
1000
|
+
reaper.start();
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
catch (err) {
|
|
1004
|
+
pluginLogger.warn("[senpi-runtime] lifecycle reaper did not start", {
|
|
1005
|
+
err: err instanceof Error ? err.message : String(err),
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
914
1008
|
},
|
|
915
1009
|
async stop() {
|
|
916
1010
|
// Live telemetry goes first: it owns a timer and an in-memory buffer, nothing downstream
|
|
@@ -918,6 +1012,10 @@ const senpiRuntimePlugin = {
|
|
|
918
1012
|
// runtime handle. Records emitted during the teardown below land in a buffer nobody drains,
|
|
919
1013
|
// which is the right trade for a process on its way out.
|
|
920
1014
|
liveTelemetry.stop();
|
|
1015
|
+
// Then the reaper, before the runtimes are torn down below, so no pass starts deleting
|
|
1016
|
+
// while they go away. The wait is bounded, so a pass already under way cannot stop a handle
|
|
1017
|
+
// the teardown below is stopping too.
|
|
1018
|
+
await reaper.stop();
|
|
921
1019
|
// Drain the HTTP server FIRST so in-flight POST /signals requests
|
|
922
1020
|
// finish before the underlying runtime they target is torn down.
|
|
923
1021
|
// `apiHandle.close()` never throws.
|
|
@@ -1250,6 +1348,7 @@ const senpiRuntimePlugin = {
|
|
|
1250
1348
|
apiKey,
|
|
1251
1349
|
scannerProviders,
|
|
1252
1350
|
runtimeId: resolvedId,
|
|
1351
|
+
onStrategyNotActive: (address) => reaper.checkWallet(address),
|
|
1253
1352
|
});
|
|
1254
1353
|
runtimeHandles.set(resolvedId, handle);
|
|
1255
1354
|
if (parsedConfig)
|
|
@@ -1385,8 +1484,11 @@ const senpiRuntimePlugin = {
|
|
|
1385
1484
|
// say so rather than reporting a bare "running" the operator would read as healthy.
|
|
1386
1485
|
// unwiredRuntimes is populated at the four wire/launch failure sites (and is
|
|
1387
1486
|
// exactly the set of "declared external scanners but has no launcher").
|
|
1487
|
+
const bootSkipStatus = bootSkippedRuntimes.get(e.id);
|
|
1388
1488
|
const status = !runtimeHandles.has(e.id)
|
|
1389
|
-
?
|
|
1489
|
+
? bootSkipStatus !== undefined
|
|
1490
|
+
? bootSkippedStatusLabel(bootSkipStatus)
|
|
1491
|
+
: "stopped"
|
|
1390
1492
|
: unwiredRuntimes.has(e.id)
|
|
1391
1493
|
? "running — NO ENTRY SCANNERS"
|
|
1392
1494
|
: "running";
|
|
@@ -1397,6 +1499,10 @@ const senpiRuntimePlugin = {
|
|
|
1397
1499
|
? e.runtimeYamlPath
|
|
1398
1500
|
: "(inline)",
|
|
1399
1501
|
status,
|
|
1502
|
+
// What the strategy behind this runtime last read as (open / closing / closed / absent /
|
|
1503
|
+
// unchecked / unknown) — separate from `status`, whose running…/stopped… prefixes other
|
|
1504
|
+
// readers key on.
|
|
1505
|
+
...reaper.lifecycleOf(e.wallet),
|
|
1400
1506
|
// The deploy path installs by CONTENT, not path (orchestrator.ts installRuntime), so
|
|
1401
1507
|
// `source` is "(inline)" for every deployed strategy and the YAML is unreachable through
|
|
1402
1508
|
// it. The engine renders the facts instead of shipping a blob to re-interpret.
|
|
@@ -1411,13 +1517,19 @@ const senpiRuntimePlugin = {
|
|
|
1411
1517
|
* unwired-verdict cleanup all happen exactly once, here.
|
|
1412
1518
|
*/
|
|
1413
1519
|
async function deleteRuntimeCore(params) {
|
|
1414
|
-
const { readRegistry, writeRegistry, withRegistryLock } = await import("./runtime/strategy-registry.js");
|
|
1415
|
-
const registry = await readRegistry(baseStateDir);
|
|
1416
1520
|
let targetId = typeof params?.id === "string" ? params.id : undefined;
|
|
1417
1521
|
const targetAddress = typeof params?.address === "string" ? params.address : undefined;
|
|
1522
|
+
// Set only by the lifecycle reaper: why the strategy counts as gone (stamped on runtime.deleted)
|
|
1523
|
+
// and which wallet that verdict was about (the concurrency guard below).
|
|
1524
|
+
const deleteReason = params?.deleteReason === "closed_status" ? params.deleteReason
|
|
1525
|
+
: undefined;
|
|
1526
|
+
const expectWallet = typeof params?.expectWallet === "string" && params.expectWallet.trim().length > 0
|
|
1527
|
+
? params.expectWallet.trim().toLowerCase()
|
|
1528
|
+
: undefined;
|
|
1418
1529
|
// Resolve target by address if id not provided
|
|
1419
1530
|
if (!targetId && targetAddress) {
|
|
1420
|
-
const
|
|
1531
|
+
const { readRegistry } = await import("./runtime/strategy-registry.js");
|
|
1532
|
+
const entry = (await readRegistry(baseStateDir)).runtimes.find((e) => e.wallet === targetAddress);
|
|
1421
1533
|
if (!entry) {
|
|
1422
1534
|
return { ok: false, error: {
|
|
1423
1535
|
code: "NOT_FOUND",
|
|
@@ -1432,6 +1544,24 @@ const senpiRuntimePlugin = {
|
|
|
1432
1544
|
message: "Please provide the runtime id or wallet address. Example: openclaw senpi runtime delete --id my-runtime",
|
|
1433
1545
|
} };
|
|
1434
1546
|
}
|
|
1547
|
+
// One delete or update apply per runtime at a time (see `runtimeChangeLock`). Operator, agent
|
|
1548
|
+
// and deploy deletes wait their turn; the lifecycle reaper never queues behind someone else's
|
|
1549
|
+
// change to the runtime it judged — it backs off and the next lifecycle check re-reads it.
|
|
1550
|
+
// The check and the acquire below run in one synchronous step.
|
|
1551
|
+
const id = targetId;
|
|
1552
|
+
if (expectWallet !== undefined && runtimeChangeLock.hasKey(id)) {
|
|
1553
|
+
return { ok: false, error: {
|
|
1554
|
+
code: "CONFLICT",
|
|
1555
|
+
message: `Runtime "${id}" was not removed: another delete or an update is already running on it. Nothing was changed; the next lifecycle check re-reads it.`,
|
|
1556
|
+
} };
|
|
1557
|
+
}
|
|
1558
|
+
return runtimeChangeLock.withKeyLock(id, () => deleteRuntimeLocked(id, deleteReason, expectWallet));
|
|
1559
|
+
}
|
|
1560
|
+
/** {@link deleteRuntimeCore} past target resolution, under the runtime's change lock. */
|
|
1561
|
+
async function deleteRuntimeLocked(targetId, deleteReason, expectWallet) {
|
|
1562
|
+
const { readRegistry, writeRegistry, withRegistryLock } = await import("./runtime/strategy-registry.js");
|
|
1563
|
+
// Read under the lock: a delete that waited behind another change sees what that change left.
|
|
1564
|
+
const registry = await readRegistry(baseStateDir);
|
|
1435
1565
|
const deletedEntry = registry.runtimes.find((e) => e.id === targetId);
|
|
1436
1566
|
const handle = runtimeHandles.get(targetId);
|
|
1437
1567
|
if (!deletedEntry && !handle) {
|
|
@@ -1440,6 +1570,35 @@ const senpiRuntimePlugin = {
|
|
|
1440
1570
|
message: `No runtime found with id "${targetId}".`,
|
|
1441
1571
|
} };
|
|
1442
1572
|
}
|
|
1573
|
+
/**
|
|
1574
|
+
* The lifecycle reaper's guard. Its verdict was about one wallet at one moment; the delete
|
|
1575
|
+
* below must not land on anything else. Evaluated synchronously between the registry read
|
|
1576
|
+
* above and the first teardown step, so nothing can interleave between check and act:
|
|
1577
|
+
* - an install on that wallet is running (it could land a live runtime under this id);
|
|
1578
|
+
* - a deploy job holds the single-flight slot (it retires and reinstalls runtimes);
|
|
1579
|
+
* - the row under this id no longer carries that wallet (ids derive from name + wallet and
|
|
1580
|
+
* are reused, so a reinstall onto a new wallet can take the old id);
|
|
1581
|
+
* - a live handle under this id trades a different wallet.
|
|
1582
|
+
* Operator, agent and deploy deletes pass no `expectWallet` and are unaffected.
|
|
1583
|
+
*/
|
|
1584
|
+
if (expectWallet !== undefined) {
|
|
1585
|
+
const fold = (w) => (w ?? "").trim().toLowerCase();
|
|
1586
|
+
const conflict = installsInFlight.has(expectWallet)
|
|
1587
|
+
? "an install is running on that wallet"
|
|
1588
|
+
: deployController.running() !== null
|
|
1589
|
+
? "a deploy is in progress"
|
|
1590
|
+
: !deletedEntry || fold(deletedEntry.wallet) !== expectWallet
|
|
1591
|
+
? "the registry row under that id no longer carries that wallet"
|
|
1592
|
+
: handle && fold(handle.wallet) !== expectWallet
|
|
1593
|
+
? "the running runtime under that id trades a different wallet"
|
|
1594
|
+
: undefined;
|
|
1595
|
+
if (conflict !== undefined) {
|
|
1596
|
+
return { ok: false, error: {
|
|
1597
|
+
code: "CONFLICT",
|
|
1598
|
+
message: `Runtime "${targetId}" was not removed: ${conflict}. Nothing was changed; the next lifecycle check re-reads it.`,
|
|
1599
|
+
} };
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1443
1602
|
if (handle) {
|
|
1444
1603
|
await handle.teardownDslForRuntimeDeletion();
|
|
1445
1604
|
await handle.stop();
|
|
@@ -1462,6 +1621,7 @@ const senpiRuntimePlugin = {
|
|
|
1462
1621
|
// Same reuse hazard: a deleted id left in the index would keep publishing a strategy that no
|
|
1463
1622
|
// longer exists, and would hand a reinstall the old name and scanner list.
|
|
1464
1623
|
telemetryStrategies.delete(targetId);
|
|
1624
|
+
bootSkippedRuntimes.delete(targetId);
|
|
1465
1625
|
// Archive state directory before removing from registry
|
|
1466
1626
|
if (deletedEntry) {
|
|
1467
1627
|
const runtimeStateDir = buildRuntimeStateDir(baseStateDir, targetId, deletedEntry.wallet ?? "");
|
|
@@ -1487,6 +1647,7 @@ const senpiRuntimePlugin = {
|
|
|
1487
1647
|
runtimeId: targetId,
|
|
1488
1648
|
address: deletedEntry?.wallet,
|
|
1489
1649
|
hadLiveHandle: Boolean(handle),
|
|
1650
|
+
deleteReason,
|
|
1490
1651
|
}));
|
|
1491
1652
|
return { ok: true, payload: { id: targetId } };
|
|
1492
1653
|
}
|