@senpi/runtime 3.0.173 → 3.0.181
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/close-position/close-position.action.d.ts.map +1 -1
- package/dist/actions/close-position/close-position.action.js +48 -3
- package/dist/actions/close-position/close-position.action.js.map +1 -1
- package/dist/actions/llm-decision/openclaw-llm-decision.d.ts +8 -0
- package/dist/actions/llm-decision/openclaw-llm-decision.d.ts.map +1 -1
- package/dist/actions/llm-decision/openclaw-llm-decision.js +56 -1
- package/dist/actions/llm-decision/openclaw-llm-decision.js.map +1 -1
- package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
- package/dist/actions/open-position/open-position.action.js +42 -6
- package/dist/actions/open-position/open-position.action.js.map +1 -1
- package/dist/actions/open-position/resolve-margin.d.ts +1 -1
- package/dist/actions/open-position/resolve-margin.d.ts.map +1 -1
- package/dist/actions/open-position/resolve-margin.js +7 -0
- package/dist/actions/open-position/resolve-margin.js.map +1 -1
- package/dist/approvals/approval-store.d.ts +37 -7
- package/dist/approvals/approval-store.d.ts.map +1 -1
- package/dist/approvals/approval-store.js +127 -23
- package/dist/approvals/approval-store.js.map +1 -1
- package/dist/approvals/manifest.d.ts.map +1 -1
- package/dist/approvals/manifest.js +0 -5
- package/dist/approvals/manifest.js.map +1 -1
- package/dist/approvals/telemetry.d.ts +27 -3
- package/dist/approvals/telemetry.d.ts.map +1 -1
- package/dist/approvals/telemetry.js +8 -4
- package/dist/approvals/telemetry.js.map +1 -1
- package/dist/approvals/tool-approval-gate.d.ts +79 -0
- package/dist/approvals/tool-approval-gate.d.ts.map +1 -1
- package/dist/approvals/tool-approval-gate.js +184 -21
- package/dist/approvals/tool-approval-gate.js.map +1 -1
- package/dist/approvals/tool-approvals-manifest.json +0 -2
- package/dist/approvals/turn-context.d.ts +271 -0
- package/dist/approvals/turn-context.d.ts.map +1 -0
- package/dist/approvals/turn-context.js +551 -0
- package/dist/approvals/turn-context.js.map +1 -0
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +43 -8
- 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 +91 -12
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/constants/dsl/index.d.ts +2 -0
- package/dist/constants/dsl/index.d.ts.map +1 -1
- package/dist/constants/dsl/index.js +2 -0
- package/dist/constants/dsl/index.js.map +1 -1
- package/dist/constants/order-types.d.ts.map +1 -1
- package/dist/constants/order-types.js +4 -1
- package/dist/constants/order-types.js.map +1 -1
- package/dist/deploy/job.d.ts +32 -0
- package/dist/deploy/job.d.ts.map +1 -1
- package/dist/deploy/job.js +72 -7
- package/dist/deploy/job.js.map +1 -1
- package/dist/deploy/messages.d.ts +15 -1
- package/dist/deploy/messages.d.ts.map +1 -1
- package/dist/deploy/messages.js +78 -0
- package/dist/deploy/messages.js.map +1 -1
- package/dist/deploy/orchestrator.d.ts +11 -1
- package/dist/deploy/orchestrator.d.ts.map +1 -1
- package/dist/deploy/orchestrator.js +111 -15
- package/dist/deploy/orchestrator.js.map +1 -1
- package/dist/deploy/register.d.ts +18 -0
- package/dist/deploy/register.d.ts.map +1 -1
- package/dist/deploy/register.js +93 -2
- package/dist/deploy/register.js.map +1 -1
- package/dist/dsl/engine/exchange-stop.d.ts +21 -1
- package/dist/dsl/engine/exchange-stop.d.ts.map +1 -1
- package/dist/dsl/engine/exchange-stop.js +27 -1
- package/dist/dsl/engine/exchange-stop.js.map +1 -1
- package/dist/dsl/engine/floors.d.ts +2 -1
- package/dist/dsl/engine/floors.d.ts.map +1 -1
- package/dist/dsl/engine/floors.js +2 -1
- 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/tick.d.ts.map +1 -1
- package/dist/dsl/engine/tick.js +4 -3
- package/dist/dsl/engine/tick.js.map +1 -1
- package/dist/dsl/events/handlers.d.ts +7 -3
- package/dist/dsl/events/handlers.d.ts.map +1 -1
- package/dist/dsl/events/handlers.js +10 -4
- package/dist/dsl/events/handlers.js.map +1 -1
- package/dist/dsl/events/initial-state.d.ts.map +1 -1
- package/dist/dsl/events/initial-state.js +2 -0
- package/dist/dsl/events/initial-state.js.map +1 -1
- package/dist/dsl/monitor/exchange-sync.d.ts +1 -0
- package/dist/dsl/monitor/exchange-sync.d.ts.map +1 -1
- package/dist/dsl/monitor/exchange-sync.js +7 -14
- 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 +108 -17
- package/dist/dsl/monitor/process-one-position.js.map +1 -1
- package/dist/dsl/notifications/formatter.d.ts +2 -1
- package/dist/dsl/notifications/formatter.d.ts.map +1 -1
- package/dist/dsl/notifications/formatter.js +18 -1
- package/dist/dsl/notifications/formatter.js.map +1 -1
- package/dist/health/types.d.ts +6 -1
- package/dist/health/types.d.ts.map +1 -1
- package/dist/health/types.js.map +1 -1
- package/dist/host/openclaw-host.d.ts +138 -0
- package/dist/host/openclaw-host.d.ts.map +1 -0
- package/dist/host/openclaw-host.js +177 -0
- package/dist/host/openclaw-host.js.map +1 -0
- package/dist/index.d.ts +46 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +318 -38
- package/dist/index.js.map +1 -1
- package/dist/leverage/max-leverage-cache.d.ts +25 -0
- package/dist/leverage/max-leverage-cache.d.ts.map +1 -1
- package/dist/leverage/max-leverage-cache.js +77 -0
- package/dist/leverage/max-leverage-cache.js.map +1 -1
- package/dist/runtime/auto-update/coordinator.d.ts +203 -0
- package/dist/runtime/auto-update/coordinator.d.ts.map +1 -1
- package/dist/runtime/auto-update/coordinator.js +621 -12
- package/dist/runtime/auto-update/coordinator.js.map +1 -1
- package/dist/runtime/auto-update/gateway-restart.d.ts +60 -1
- package/dist/runtime/auto-update/gateway-restart.d.ts.map +1 -1
- package/dist/runtime/auto-update/gateway-restart.js +95 -21
- package/dist/runtime/auto-update/gateway-restart.js.map +1 -1
- package/dist/runtime/auto-update/idle-gate.d.ts +139 -0
- package/dist/runtime/auto-update/idle-gate.d.ts.map +1 -0
- package/dist/runtime/auto-update/idle-gate.js +217 -0
- package/dist/runtime/auto-update/idle-gate.js.map +1 -0
- package/dist/runtime/auto-update/install-results.d.ts +130 -0
- package/dist/runtime/auto-update/install-results.d.ts.map +1 -0
- package/dist/runtime/auto-update/install-results.js +264 -0
- package/dist/runtime/auto-update/install-results.js.map +1 -0
- package/dist/runtime/auto-update/money-call-tracker.d.ts +36 -0
- package/dist/runtime/auto-update/money-call-tracker.d.ts.map +1 -0
- package/dist/runtime/auto-update/money-call-tracker.js +129 -0
- package/dist/runtime/auto-update/money-call-tracker.js.map +1 -0
- package/dist/runtime/auto-update/register-idle-gate.d.ts +70 -0
- package/dist/runtime/auto-update/register-idle-gate.d.ts.map +1 -0
- package/dist/runtime/auto-update/register-idle-gate.js +123 -0
- package/dist/runtime/auto-update/register-idle-gate.js.map +1 -0
- package/dist/runtime/auto-update/run-update-command.d.ts +13 -0
- package/dist/runtime/auto-update/run-update-command.d.ts.map +1 -1
- package/dist/runtime/auto-update/run-update-command.js +18 -0
- package/dist/runtime/auto-update/run-update-command.js.map +1 -1
- package/dist/runtime/auto-update/state-store.d.ts +14 -1
- package/dist/runtime/auto-update/state-store.d.ts.map +1 -1
- package/dist/runtime/auto-update/state-store.js +37 -3
- package/dist/runtime/auto-update/state-store.js.map +1 -1
- package/dist/runtime/auto-update/types.d.ts +51 -0
- 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-marker.d.ts +95 -0
- package/dist/runtime/auto-update/update-marker.d.ts.map +1 -0
- package/dist/runtime/auto-update/update-marker.js +185 -0
- package/dist/runtime/auto-update/update-marker.js.map +1 -0
- package/dist/runtime/auto-update/update-schedule.d.ts +57 -0
- package/dist/runtime/auto-update/update-schedule.d.ts.map +1 -0
- package/dist/runtime/auto-update/update-schedule.js +75 -0
- package/dist/runtime/auto-update/update-schedule.js.map +1 -0
- package/dist/runtime/create-scanner-module.d.ts +1 -1
- package/dist/runtime/create-scanner-module.d.ts.map +1 -1
- package/dist/runtime/create-scanner-module.js +2 -1
- package/dist/runtime/create-scanner-module.js.map +1 -1
- package/dist/runtime/decision-errors.d.ts +7 -0
- package/dist/runtime/decision-errors.d.ts.map +1 -1
- package/dist/runtime/decision-errors.js +15 -0
- package/dist/runtime/decision-errors.js.map +1 -1
- package/dist/runtime/lifecycle-events.d.ts +1 -1
- package/dist/runtime/lifecycle-events.d.ts.map +1 -1
- package/dist/runtime/lifecycle-events.js +10 -1
- package/dist/runtime/lifecycle-events.js.map +1 -1
- package/dist/runtime/runtime-descriptor.d.ts +11 -2
- package/dist/runtime/runtime-descriptor.d.ts.map +1 -1
- package/dist/runtime/runtime-descriptor.js +11 -2
- package/dist/runtime/runtime-descriptor.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +4 -3
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/strategy-lifecycle.js +1 -1
- package/dist/runtime/strategy-lifecycle.js.map +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.js +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
- package/dist/runtime-api/intake/acceptance-record.d.ts +17 -3
- package/dist/runtime-api/intake/acceptance-record.d.ts.map +1 -1
- package/dist/runtime-api/intake/acceptance-record.js +57 -32
- package/dist/runtime-api/intake/acceptance-record.js.map +1 -1
- package/dist/runtime-api/intake/intake-events.d.ts +6 -5
- package/dist/runtime-api/intake/intake-events.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-events.js +9 -3
- package/dist/runtime-api/intake/intake-events.js.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.d.ts +1 -1
- package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.js +34 -31
- package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
- package/dist/runtime-api/intake/scaffold-error-router.d.ts +8 -0
- package/dist/runtime-api/intake/scaffold-error-router.d.ts.map +1 -1
- package/dist/runtime-api/intake/scaffold-error-router.js +9 -0
- package/dist/runtime-api/intake/scaffold-error-router.js.map +1 -1
- package/dist/runtime-api/intake/types.d.ts +9 -0
- package/dist/runtime-api/intake/types.d.ts.map +1 -1
- package/dist/runtime-api/intake/types.js +5 -1
- package/dist/runtime-api/intake/types.js.map +1 -1
- package/dist/runtime-api/routes/signals.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.js +25 -1
- package/dist/runtime-api/routes/signals.js.map +1 -1
- package/dist/runtime-api/routes/signals.schema.d.ts +2 -0
- package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.js +4 -0
- package/dist/runtime-api/routes/signals.schema.js.map +1 -1
- package/dist/scanners/engine/data-providers.d.ts +6 -0
- package/dist/scanners/engine/data-providers.d.ts.map +1 -1
- package/dist/scanners/engine/data-providers.js.map +1 -1
- package/dist/scanners/engine/engine.d.ts +9 -0
- package/dist/scanners/engine/engine.d.ts.map +1 -1
- package/dist/scanners/engine/engine.js +29 -6
- package/dist/scanners/engine/engine.js.map +1 -1
- package/dist/scanners/external/scaffold/ctx.py +55 -1
- package/dist/scanners/external/scaffold/delivery.py +92 -28
- package/dist/scanners/external/scaffold/preflight.py +1 -1
- package/dist/scanners/external/scaffold/scaffold.py +75 -2
- package/dist/scanners/runtime-module.d.ts +2 -0
- package/dist/scanners/runtime-module.d.ts.map +1 -1
- package/dist/scanners/runtime-module.js +14 -0
- package/dist/scanners/runtime-module.js.map +1 -1
- package/dist/senpi/client.d.ts +36 -1
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +100 -8
- package/dist/senpi/client.js.map +1 -1
- package/dist/senpi/types.d.ts +54 -2
- package/dist/senpi/types.d.ts.map +1 -1
- package/dist/telemetry/metrics.d.ts +15 -1
- package/dist/telemetry/metrics.d.ts.map +1 -1
- package/dist/telemetry/metrics.js +25 -1
- package/dist/telemetry/metrics.js.map +1 -1
- package/dist/telemetry/otel-sdk.d.ts +12 -0
- package/dist/telemetry/otel-sdk.d.ts.map +1 -1
- package/dist/telemetry/otel-sdk.js +47 -1
- package/dist/telemetry/otel-sdk.js.map +1 -1
- package/dist/telemetry/spans/span-hooks.d.ts +18 -1
- package/dist/telemetry/spans/span-hooks.d.ts.map +1 -1
- package/dist/telemetry/spans/span-hooks.js +306 -96
- package/dist/telemetry/spans/span-hooks.js.map +1 -1
- package/dist/telemetry/tool-plane.d.ts +2 -0
- package/dist/telemetry/tool-plane.d.ts.map +1 -1
- package/dist/telemetry/tool-plane.js +1 -0
- package/dist/telemetry/tool-plane.js.map +1 -1
- package/dist/tool-surface/crib-cache.d.ts +21 -0
- package/dist/tool-surface/crib-cache.d.ts.map +1 -0
- package/dist/tool-surface/crib-cache.js +54 -0
- package/dist/tool-surface/crib-cache.js.map +1 -0
- package/dist/tool-surface/crib-hook.d.ts +78 -0
- package/dist/tool-surface/crib-hook.d.ts.map +1 -0
- package/dist/tool-surface/crib-hook.js +217 -0
- package/dist/tool-surface/crib-hook.js.map +1 -0
- package/dist/tool-surface/crib-sheet.d.ts +56 -0
- package/dist/tool-surface/crib-sheet.d.ts.map +1 -0
- package/dist/tool-surface/crib-sheet.js +129 -0
- package/dist/tool-surface/crib-sheet.js.map +1 -0
- package/dist/tool-surface/mcp-tool-source.d.ts +64 -0
- package/dist/tool-surface/mcp-tool-source.d.ts.map +1 -0
- package/dist/tool-surface/mcp-tool-source.js +189 -0
- package/dist/tool-surface/mcp-tool-source.js.map +1 -0
- package/dist/types/dsl/index.d.ts +4 -0
- package/dist/types/dsl/index.d.ts.map +1 -1
- package/dist/utils/event-catalog.d.ts +141 -10
- package/dist/utils/event-catalog.d.ts.map +1 -1
- package/dist/utils/event-catalog.js +23 -1
- package/dist/utils/event-catalog.js.map +1 -1
- package/dist/utils/perp-coin.d.ts.map +1 -1
- package/dist/utils/perp-coin.js +2 -1
- package/dist/utils/perp-coin.js.map +1 -1
- package/dist/utils/resolve-order-type.d.ts.map +1 -1
- package/dist/utils/resolve-order-type.js +5 -1
- package/dist/utils/resolve-order-type.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/index.d.ts +1 -1
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +1 -1
- package/dist/validate/index.js.map +1 -1
- package/dist/validate/recipe-checks.d.ts +7 -0
- package/dist/validate/recipe-checks.d.ts.map +1 -1
- package/dist/validate/recipe-checks.js +18 -0
- package/dist/validate/recipe-checks.js.map +1 -1
- package/dist/validate/recipe.d.ts.map +1 -1
- package/dist/validate/recipe.js +2 -1
- package/dist/validate/recipe.js.map +1 -1
- package/dist/widgets/tool.d.ts +9 -0
- package/dist/widgets/tool.d.ts.map +1 -1
- package/dist/widgets/tool.js +1 -0
- package/dist/widgets/tool.js.map +1 -1
- package/examples/strategies/README.md +2 -1
- package/examples/strategies/iguana/README.md +3 -3
- package/examples/strategies/raptor/README.md +3 -3
- package/openclaw.plugin.json +11 -1
- package/package.json +5 -2
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { runCommand } from "./command-runner.js";
|
|
2
|
-
import { evaluateGatewayRestartCapability } from "./gateway-restart.js";
|
|
2
|
+
import { evaluateGatewayRestartCapability, evaluateModernRestartCapability, triggerGatewayRestartViaMutate, } from "./gateway-restart.js";
|
|
3
3
|
import { classifyUpdate, parseSemver } from "./policy.js";
|
|
4
4
|
import { evaluatePreflight } from "./preflight.js";
|
|
5
5
|
import { resolveConfiguredSpec } from "./resolve-package-name.js";
|
|
6
6
|
import { AutoUpdateStateStore } from "./state-store.js";
|
|
7
7
|
import { createDefaultAutoUpdateState, } from "./types.js";
|
|
8
|
+
import { consumeInstallResults, describeInstallResult, installResultLevel, } from "./install-results.js";
|
|
9
|
+
import { buildExactUpdateSpec, clearUpdateMarker, readUpdateMarker, writeUpdateMarker } from "./update-marker.js";
|
|
10
|
+
import { eligibleAt as computeEligibleAt, mustForce } from "./update-schedule.js";
|
|
8
11
|
import { NpmVersionSource } from "./version-source.js";
|
|
9
12
|
import { EVENT_NAME, eventAttrs } from "../../utils/event-catalog.js";
|
|
10
13
|
/**
|
|
@@ -22,6 +25,50 @@ const CHECK_NOW_REASON = {
|
|
|
22
25
|
disabledInConfig: "Auto-update is turned off in config; no check was run.",
|
|
23
26
|
coordinatorStopped: "The auto-update coordinator is not running; no check was run.",
|
|
24
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* How long a requested restart may take to load its target before the
|
|
30
|
+
* awaiting-restart state is treated as failed (the 3.0.171 latch fix). A 10.x
|
|
31
|
+
* restart-to-apply measured about 71 s on Box A, with starts up to 93 s; 30 min
|
|
32
|
+
* leaves room for the gateway's own deferral (forced after 300 s) and a slow
|
|
33
|
+
* offline install.
|
|
34
|
+
*/
|
|
35
|
+
export const RESTART_LOAD_TIMEOUT_MS = 30 * 60_000;
|
|
36
|
+
/**
|
|
37
|
+
* Restarts requested for one target that may fail to load before the box
|
|
38
|
+
* stops trying that version and waits for a newer one. Bounds the loop of a
|
|
39
|
+
* version that can never install (a blocked scan, a yanked version, a wrapper
|
|
40
|
+
* without the marker step): without it an idle box would restart about every
|
|
41
|
+
* 80 min forever.
|
|
42
|
+
*/
|
|
43
|
+
export const MAX_APPLY_ATTEMPTS = 3;
|
|
44
|
+
/**
|
|
45
|
+
* How often a 10.x box that is `pending-idle` re-reads its apply gate (the
|
|
46
|
+
* spread time and the idle gate) between registry polls. The idle gate has no
|
|
47
|
+
* change notification, so the change is caught by this re-read; when the gate
|
|
48
|
+
* opens, an ordinary check runs at once instead of up to one poll interval
|
|
49
|
+
* (60 min by default) later. The re-read itself is local: no registry call,
|
|
50
|
+
* no `onTickComplete`.
|
|
51
|
+
*/
|
|
52
|
+
export const PENDING_IDLE_RECHECK_MS = 3 * 60_000;
|
|
53
|
+
/**
|
|
54
|
+
* The only idle-gate blockers the 24 h forced restart may override: a busy
|
|
55
|
+
* user (`user-run`, `recent-message`). Every other blocker holds it, money
|
|
56
|
+
* first: an order call in flight (the restart would cut it), an open trade
|
|
57
|
+
* approval (it would be cancelled), a running `senpi deploy` (it would leave a
|
|
58
|
+
* funded wallet with no runtime), a queued user message, and a gate that
|
|
59
|
+
* threw (it cannot rule any of those out).
|
|
60
|
+
*/
|
|
61
|
+
const FORCE_MAY_OVERRIDE = new Set(["user-run", "recent-message"]);
|
|
62
|
+
/** Whether every blocker in `blockers` is one the forced restart may override. */
|
|
63
|
+
function onlyOverridable(blockers) {
|
|
64
|
+
return blockers.every((b) => FORCE_MAY_OVERRIDE.has(b));
|
|
65
|
+
}
|
|
66
|
+
/** Whether `loaded` is at or past `target` (major.minor.patch; both must parse). */
|
|
67
|
+
function isAtLeast(loaded, target) {
|
|
68
|
+
if (!parseSemver(loaded) || !parseSemver(target))
|
|
69
|
+
return false;
|
|
70
|
+
return classifyUpdate(loaded, target) === "none";
|
|
71
|
+
}
|
|
25
72
|
/** Cap a registry-supplied string before it reaches a log line or an event body. */
|
|
26
73
|
function summarizeForLog(value, maxLength = 120) {
|
|
27
74
|
const collapsed = value.replace(/\s+/g, " ").trim();
|
|
@@ -49,6 +96,8 @@ export class AutoUpdateCoordinator {
|
|
|
49
96
|
versionSource;
|
|
50
97
|
runUpdateCommand;
|
|
51
98
|
timer = null;
|
|
99
|
+
/** 10.x `pending-idle` gate re-read; see {@link PENDING_IDLE_RECHECK_MS}. */
|
|
100
|
+
idleRecheckTimer = null;
|
|
52
101
|
running = false;
|
|
53
102
|
stopped = true;
|
|
54
103
|
runToken = 0;
|
|
@@ -58,8 +107,24 @@ export class AutoUpdateCoordinator {
|
|
|
58
107
|
/** Spec the last resolve used. Seeded in the constructor, so the snapshot
|
|
59
108
|
* reports a line before the first tick. */
|
|
60
109
|
updateSpec;
|
|
110
|
+
/** Why the modern path is still waiting; reset every tick. See {@link AutoUpdateSnapshot.pendingBlockers}. */
|
|
111
|
+
pendingBlockers = [];
|
|
112
|
+
now;
|
|
113
|
+
rand;
|
|
114
|
+
loadedVersion;
|
|
115
|
+
/**
|
|
116
|
+
* 10.x: `<version>|<sorted blockers>` of the last `auto_update.deferred`, so the event fires on
|
|
117
|
+
* entering `pending-idle` and on a change only. Cleared when a tick ends outside `pending-idle` or fails.
|
|
118
|
+
* In memory: a new gateway process reports a wait that is still on once.
|
|
119
|
+
*/
|
|
120
|
+
lastDeferredKey = null;
|
|
121
|
+
/** The install-results report the last {@link start} began; see {@link whenInstallResultsReported}. */
|
|
122
|
+
installResultsReport = Promise.resolve();
|
|
61
123
|
constructor(options) {
|
|
62
124
|
this.options = options;
|
|
125
|
+
this.now = options.now ?? Date.now;
|
|
126
|
+
this.rand = options.rand ?? Math.random;
|
|
127
|
+
this.loadedVersion = options.loadedVersion ?? options.currentVersion;
|
|
63
128
|
this.store = new AutoUpdateStateStore(options.stateDir, options.logger);
|
|
64
129
|
this.versionSource = options.versionSource ?? new NpmVersionSource();
|
|
65
130
|
this.runUpdateCommand = options.runUpdateCommand ?? defaultRunUpdateCommand;
|
|
@@ -79,6 +144,8 @@ export class AutoUpdateCoordinator {
|
|
|
79
144
|
this.stopped = true;
|
|
80
145
|
this.snapshot = this.buildSnapshot("disabled", "auto-update-disabled");
|
|
81
146
|
logger.info(`[senpi-auto-update] disabled by config (pluginEnabled=${String(this.options.enabled)}, configEnabled=${String(config.enabled)})`);
|
|
147
|
+
// Operator markers and the boot floor still install on a box with auto-update off.
|
|
148
|
+
this.startInstallResultsReport(null);
|
|
82
149
|
return;
|
|
83
150
|
}
|
|
84
151
|
logger.info(`[senpi-auto-update] starting | localVersion=${this.options.currentVersion} package=${this.options.packageName} spec=${this.resolveUpdateSpec()} mode=${config.mode} pollInterval=${config.pollIntervalMinutes}m startupDelay=${config.startupDelaySeconds}s failureBackoff=${config.failureBackoffMinutes}m maxBackoff=${config.maxBackoffMinutes}m`);
|
|
@@ -90,17 +157,188 @@ export class AutoUpdateCoordinator {
|
|
|
90
157
|
logger.info(`[senpi-auto-update] preflight result | ok=${String(preflight.ok)} effectiveMode=${preflight.effectiveMode} reasons=${preflight.reasons.join(",") || "none"}`);
|
|
91
158
|
this.state = await this.store.load(this.options.currentVersion, preflight.effectiveMode);
|
|
92
159
|
this.state.effectiveMode = preflight.effectiveMode;
|
|
160
|
+
const reconciled = await this.reconcileLoadedVersion(this.state);
|
|
93
161
|
try {
|
|
94
162
|
await this.store.save(this.state);
|
|
95
163
|
}
|
|
96
164
|
catch (error) {
|
|
97
165
|
this.options.logger.warn("[senpi-auto-update] failed to persist initial state", error);
|
|
98
166
|
}
|
|
99
|
-
this.
|
|
167
|
+
this.startInstallResultsReport(this.state);
|
|
168
|
+
this.snapshot = this.buildSnapshot("healthy", reconciled ?? "started");
|
|
100
169
|
const delayMs = config.startupDelaySeconds * 1000;
|
|
101
170
|
logger.info(`[senpi-auto-update] first check scheduled | delayMs=${delayMs} nextCheckAt=${new Date(Date.now() + delayMs).toISOString()}`);
|
|
102
171
|
this.scheduleNext(delayMs, this.runToken);
|
|
103
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Startup half of the latch fix: compare what this process loaded with what
|
|
175
|
+
* a previous process asked for.
|
|
176
|
+
*
|
|
177
|
+
* When the loaded version reaches the requested restart's target, the update
|
|
178
|
+
* took effect: the pending restart and pending update are cleared, a stale
|
|
179
|
+
* install marker is removed, and on a 10.x host `auto_update.applied` fires
|
|
180
|
+
* (nothing was installed before this boot there). On 5.x `applied` already
|
|
181
|
+
* fired when the restart was requested, so it is not counted twice.
|
|
182
|
+
*
|
|
183
|
+
* A target the loaded version has not reached is left for the tick, which
|
|
184
|
+
* expires it once the request is older than {@link RESTART_LOAD_TIMEOUT_MS}.
|
|
185
|
+
*
|
|
186
|
+
* @returns `updated:<v>` when a requested update is now loaded, else `undefined`.
|
|
187
|
+
*/
|
|
188
|
+
async reconcileLoadedVersion(state) {
|
|
189
|
+
const loaded = this.loadedVersion;
|
|
190
|
+
let action;
|
|
191
|
+
const pending = state.pendingRestart;
|
|
192
|
+
if (pending && isAtLeast(loaded, pending.toVersion)) {
|
|
193
|
+
action = `updated:${pending.toVersion}`;
|
|
194
|
+
this.options.logger.info(`[senpi-auto-update] update to v${pending.toVersion} is loaded (requested ${pending.requestedAt}, mode ${pending.mode})`);
|
|
195
|
+
// A 5.x pending restart already reported `applied` when it was requested.
|
|
196
|
+
if (this.options.modernHost && pending.mode === "restart-to-apply") {
|
|
197
|
+
// How long the apply took: request to this boot (stop + wrapper install + start), and
|
|
198
|
+
// first seen to this boot (spread + idle wait + apply). Read before pendingUpdate is cleared.
|
|
199
|
+
const now = this.now();
|
|
200
|
+
const sinceMs = (iso) => {
|
|
201
|
+
const ms = iso ? Date.parse(iso) : Number.NaN;
|
|
202
|
+
return Number.isFinite(ms) ? Math.max(0, now - ms) : undefined;
|
|
203
|
+
};
|
|
204
|
+
const firstSeenAt = state.pendingUpdate?.firstSeenAt;
|
|
205
|
+
const pendingMs = sinceMs(firstSeenAt);
|
|
206
|
+
this.emitEvent(EVENT_NAME.autoUpdateApplied, "info", {
|
|
207
|
+
body: `Update applied: ${pending.fromVersion} -> v${pending.toVersion} (${pending.mode})`,
|
|
208
|
+
attributes: eventAttrs({
|
|
209
|
+
"senpi.auto_update.from_version": pending.fromVersion,
|
|
210
|
+
"senpi.auto_update.to_version": pending.toVersion,
|
|
211
|
+
"senpi.auto_update.restart_mode": pending.mode,
|
|
212
|
+
"senpi.auto_update.requested_at": pending.requestedAt,
|
|
213
|
+
"senpi.auto_update.first_seen_at": pendingMs === undefined ? undefined : firstSeenAt,
|
|
214
|
+
"senpi.auto_update.apply_ms": sinceMs(pending.requestedAt),
|
|
215
|
+
"senpi.auto_update.pending_ms": pendingMs,
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
state.pendingRestart = undefined;
|
|
220
|
+
}
|
|
221
|
+
if (state.pendingUpdate && isAtLeast(loaded, state.pendingUpdate.version)) {
|
|
222
|
+
state.pendingUpdate = undefined;
|
|
223
|
+
}
|
|
224
|
+
if (state.applyFailures && isAtLeast(loaded, state.applyFailures.version)) {
|
|
225
|
+
state.applyFailures = undefined;
|
|
226
|
+
}
|
|
227
|
+
if (state.lastAppliedVersion && isAtLeast(loaded, state.lastAppliedVersion)) {
|
|
228
|
+
state.lastLoggedAwaitingRestartVersion = undefined;
|
|
229
|
+
}
|
|
230
|
+
const markerPath = this.options.updateMarkerPath;
|
|
231
|
+
if (markerPath) {
|
|
232
|
+
const marker = await readUpdateMarker(markerPath);
|
|
233
|
+
if (marker && isAtLeast(loaded, marker.targetVersion)) {
|
|
234
|
+
await this.clearMarkerQuietly();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return action;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Resolves when the install-results report begun by the last {@link start} has finished.
|
|
241
|
+
* `start` does not wait for it: the report waits for the OTel SDK first.
|
|
242
|
+
*/
|
|
243
|
+
whenInstallResultsReported() {
|
|
244
|
+
return this.installResultsReport;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* 10.x: start reporting the wrapper's install outcomes in the background. `liveState` is the
|
|
248
|
+
* running coordinator's state (the ids are saved with it); `null` when auto-update is
|
|
249
|
+
* disabled, which loads only the ids and patches only them.
|
|
250
|
+
*/
|
|
251
|
+
startInstallResultsReport(liveState) {
|
|
252
|
+
if (!this.options.modernHost || !this.options.installResultsPath)
|
|
253
|
+
return;
|
|
254
|
+
this.installResultsReport = this.reportInstallResults(this.options.installResultsPath, liveState);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* One `auto_update.install_result` per entry not reported before (`install-results.ts`), then
|
|
258
|
+
* the file is deleted. Runs from {@link start}, which only the gateway's plugin service calls,
|
|
259
|
+
* so a CLI process never consumes the file. Never throws: telemetry must not break the
|
|
260
|
+
* coordinator.
|
|
261
|
+
*/
|
|
262
|
+
async reportInstallResults(path, liveState) {
|
|
263
|
+
try {
|
|
264
|
+
const reportedIds = liveState
|
|
265
|
+
? liveState.reportedInstallResultIds
|
|
266
|
+
: (await this.store.load(this.options.currentVersion, this.options.config.mode)).reportedInstallResultIds;
|
|
267
|
+
const result = await consumeInstallResults({
|
|
268
|
+
path,
|
|
269
|
+
reportedIds: Array.isArray(reportedIds) ? reportedIds : [],
|
|
270
|
+
emit: (entry) => this.emitInstallResult(entry),
|
|
271
|
+
persistReportedIds: async (ids) => {
|
|
272
|
+
if (!liveState)
|
|
273
|
+
return this.store.patch({ reportedInstallResultIds: ids });
|
|
274
|
+
// The running coordinator saves this same object on every tick; saving it whole
|
|
275
|
+
// never writes back an older copy.
|
|
276
|
+
liveState.reportedInstallResultIds = ids;
|
|
277
|
+
await this.store.save(liveState);
|
|
278
|
+
},
|
|
279
|
+
telemetry: this.options.installResultsTelemetry,
|
|
280
|
+
logger: this.options.logger,
|
|
281
|
+
});
|
|
282
|
+
if (result.emitted > 0 || result.skipped > 0) {
|
|
283
|
+
this.options.logger.info(`[senpi-auto-update] reported ${result.emitted} wrapper install result(s)` +
|
|
284
|
+
(result.skipped > 0 ? ` (${result.skipped} already reported)` : ""));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
this.options.logger.warn(`[senpi-auto-update] reading the wrapper's install results failed | err=${formatErr(error)}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
emitInstallResult(e) {
|
|
292
|
+
const opt = (v) => (v === null ? undefined : v);
|
|
293
|
+
this.emitEvent(EVENT_NAME.autoUpdateInstallResult, installResultLevel(e.outcome), {
|
|
294
|
+
body: describeInstallResult(e),
|
|
295
|
+
attributes: eventAttrs({
|
|
296
|
+
"senpi.auto_update.source": opt(e.source),
|
|
297
|
+
"senpi.auto_update.outcome": e.outcome,
|
|
298
|
+
"senpi.auto_update.spec": opt(e.spec),
|
|
299
|
+
"senpi.auto_update.from_version": opt(e.fromVersion),
|
|
300
|
+
"senpi.auto_update.to_version": opt(e.toVersion),
|
|
301
|
+
"senpi.auto_update.target_version": opt(e.targetVersion),
|
|
302
|
+
"senpi.auto_update.duration_ms": opt(e.durationMs),
|
|
303
|
+
"senpi.auto_update.reason": opt(e.reason),
|
|
304
|
+
"senpi.auto_update.ended_at": opt(e.at),
|
|
305
|
+
"senpi.auto_update.result_id": opt(e.id),
|
|
306
|
+
"senpi.auto_update.openclaw_version": opt(e.openclawVersion),
|
|
307
|
+
}),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* 10.x: the box waits (`pending-idle:<v>`). `auto_update.deferred` on entering the wait and when
|
|
312
|
+
* the target or the sorted blocker set changes; an unchanged re-check or poll is silent.
|
|
313
|
+
*/
|
|
314
|
+
noteDeferred(pending, latest, blockers) {
|
|
315
|
+
const sorted = blockerList(blockers);
|
|
316
|
+
const key = `${latest}|${sorted}`;
|
|
317
|
+
if (key === this.lastDeferredKey)
|
|
318
|
+
return;
|
|
319
|
+
this.lastDeferredKey = key;
|
|
320
|
+
this.emitEvent(EVENT_NAME.autoUpdateDeferred, "info", {
|
|
321
|
+
body: `Restart to apply v${latest} deferred: waiting for ${sorted || "nothing"} (eligible at ${pending.eligibleAt})`,
|
|
322
|
+
attributes: eventAttrs({
|
|
323
|
+
"senpi.auto_update.to_version": latest,
|
|
324
|
+
"senpi.auto_update.blockers": sorted,
|
|
325
|
+
"senpi.auto_update.eligible_at": pending.eligibleAt,
|
|
326
|
+
"senpi.auto_update.first_seen_at": pending.firstSeenAt,
|
|
327
|
+
}),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/** Remove the install marker; a failure is logged, never thrown (the wrapper deletes it too). */
|
|
331
|
+
async clearMarkerQuietly() {
|
|
332
|
+
const markerPath = this.options.updateMarkerPath;
|
|
333
|
+
if (!markerPath)
|
|
334
|
+
return;
|
|
335
|
+
try {
|
|
336
|
+
await clearUpdateMarker(markerPath);
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
this.options.logger.warn(`[senpi-auto-update] failed to clear the update marker | path=${markerPath} err=${formatErr(error)}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
104
342
|
/** Cancel the next scheduled poll and prevent in-flight ticks from rescheduling. */
|
|
105
343
|
async stop() {
|
|
106
344
|
this.runToken += 1;
|
|
@@ -109,6 +347,7 @@ export class AutoUpdateCoordinator {
|
|
|
109
347
|
clearTimeout(this.timer);
|
|
110
348
|
this.timer = null;
|
|
111
349
|
}
|
|
350
|
+
this.clearIdleRecheck();
|
|
112
351
|
}
|
|
113
352
|
/** Return the latest point-in-time snapshot for health/state gateway RPCs. */
|
|
114
353
|
getSnapshot() {
|
|
@@ -154,9 +393,7 @@ export class AutoUpdateCoordinator {
|
|
|
154
393
|
// way to tell a completed check from a failed one is whether it counted a
|
|
155
394
|
// failure. Reporting "ran" either way tells an operator testing a config
|
|
156
395
|
// change that it worked when nothing reached the registry.
|
|
157
|
-
// Baseline on live state
|
|
158
|
-
// snapshot before `tick` zeroes the counter, so after a recovery the
|
|
159
|
-
// snapshot still carries the old total and 0 -> 1 reads as no change.
|
|
396
|
+
// Baseline on live state: it is what `tick` changes.
|
|
160
397
|
const failuresBefore = this.state.consecutiveFailures;
|
|
161
398
|
await this.tick(this.runToken);
|
|
162
399
|
if (this.state.consecutiveFailures > failuresBefore) {
|
|
@@ -194,15 +431,24 @@ export class AutoUpdateCoordinator {
|
|
|
194
431
|
return;
|
|
195
432
|
}
|
|
196
433
|
this.running = true;
|
|
434
|
+
this.clearIdleRecheck();
|
|
197
435
|
this.options.logger.info(`[senpi-auto-update] tick started | localVersion=${this.options.currentVersion}`);
|
|
198
436
|
const state = this.state;
|
|
199
437
|
let nextDelayMs = null;
|
|
438
|
+
let recheckIdle = false;
|
|
200
439
|
try {
|
|
201
440
|
await this.runTick(state);
|
|
202
441
|
state.consecutiveFailures = 0;
|
|
203
442
|
state.lastFailureAt = undefined;
|
|
204
443
|
state.lastFailureReason = undefined;
|
|
205
444
|
await this.persistState(state);
|
|
445
|
+
// `runTick` built the snapshot before the reset above; rebuild it so it
|
|
446
|
+
// carries the state that was just written.
|
|
447
|
+
this.snapshot = this.buildSnapshot(this.snapshot.health, this.snapshot.lastAction, this.snapshot.lastError);
|
|
448
|
+
recheckIdle = this.options.modernHost === true && this.snapshot.lastAction.startsWith("pending-idle:");
|
|
449
|
+
// The wait ended (restart requested, no update, a restart failure): the next wait is new.
|
|
450
|
+
if (!recheckIdle)
|
|
451
|
+
this.lastDeferredKey = null;
|
|
206
452
|
nextDelayMs = this.options.config.pollIntervalMinutes * 60_000;
|
|
207
453
|
this.options.logger.info(`[senpi-auto-update] tick succeeded | nextDelayMs=${nextDelayMs}`);
|
|
208
454
|
}
|
|
@@ -211,6 +457,8 @@ export class AutoUpdateCoordinator {
|
|
|
211
457
|
state.lastFailureAt = new Date().toISOString();
|
|
212
458
|
state.lastFailureReason = formatErr(error);
|
|
213
459
|
await this.persistState(state);
|
|
460
|
+
// A failed tick ends the wait it was in: the next wait is reported as new.
|
|
461
|
+
this.lastDeferredKey = null;
|
|
214
462
|
const base = this.options.config.failureBackoffMinutes * 60_000;
|
|
215
463
|
const max = this.options.config.maxBackoffMinutes * 60_000;
|
|
216
464
|
const backoff = Math.min(max, base * Math.max(1, state.consecutiveFailures));
|
|
@@ -247,8 +495,64 @@ export class AutoUpdateCoordinator {
|
|
|
247
495
|
this.options.logger.info(`[senpi-auto-update] scheduling next tick after onTickComplete | nextDelayMs=${nextDelayMs}`);
|
|
248
496
|
this.scheduleNext(nextDelayMs, token);
|
|
249
497
|
}
|
|
498
|
+
if (recheckIdle)
|
|
499
|
+
this.scheduleIdleRecheck(token);
|
|
250
500
|
}
|
|
251
501
|
}
|
|
502
|
+
/** Re-read the apply gate of a `pending-idle` box after {@link PENDING_IDLE_RECHECK_MS}. */
|
|
503
|
+
scheduleIdleRecheck(token) {
|
|
504
|
+
if (this.stopped || token !== this.runToken)
|
|
505
|
+
return;
|
|
506
|
+
this.clearIdleRecheck();
|
|
507
|
+
this.idleRecheckTimer = setTimeout(() => {
|
|
508
|
+
this.idleRecheckTimer = null;
|
|
509
|
+
this.recheckPendingIdle(token);
|
|
510
|
+
}, PENDING_IDLE_RECHECK_MS);
|
|
511
|
+
}
|
|
512
|
+
clearIdleRecheck() {
|
|
513
|
+
if (this.idleRecheckTimer) {
|
|
514
|
+
clearTimeout(this.idleRecheckTimer);
|
|
515
|
+
this.idleRecheckTimer = null;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Run a check now when the gate that held the pending update has opened,
|
|
520
|
+
* else re-read it again later. A check already in flight re-arms this
|
|
521
|
+
* itself when it ends `pending-idle`.
|
|
522
|
+
*/
|
|
523
|
+
recheckPendingIdle(token) {
|
|
524
|
+
if (this.running || this.stopped || token !== this.runToken)
|
|
525
|
+
return;
|
|
526
|
+
const pending = this.state.pendingUpdate;
|
|
527
|
+
if (!pending)
|
|
528
|
+
return;
|
|
529
|
+
const now = this.now();
|
|
530
|
+
const gate = this.applyGate(now, Date.parse(pending.eligibleAt));
|
|
531
|
+
if (!gate.open) {
|
|
532
|
+
// Still waiting; a blocker set that changed since the last poll is a new deferred.
|
|
533
|
+
this.noteDeferred(pending, pending.version, gate.blockers);
|
|
534
|
+
this.scheduleIdleRecheck(token);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
this.options.logger.info(`[senpi-auto-update] pending v${pending.version}: apply gate open; checking now`);
|
|
538
|
+
void this.tick(token);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* The 10.x apply gate: wait for `eligibleAt` (blocker `spread`), then for an
|
|
542
|
+
* idle box, unless {@link mustForce} applies and every blocker is one the
|
|
543
|
+
* forced restart may override.
|
|
544
|
+
*/
|
|
545
|
+
applyGate(now, eligibleMs) {
|
|
546
|
+
if (Number.isFinite(eligibleMs) && now < eligibleMs) {
|
|
547
|
+
return { open: false, forced: false, blockers: ["spread"] };
|
|
548
|
+
}
|
|
549
|
+
const decision = this.readIdle(now);
|
|
550
|
+
// Forcing overrides a busy user, never money in motion (FORCE_MAY_OVERRIDE).
|
|
551
|
+
const forced = !decision.idle &&
|
|
552
|
+
onlyOverridable(decision.blockers) &&
|
|
553
|
+
mustForce(now, Number.isFinite(eligibleMs) ? eligibleMs : now);
|
|
554
|
+
return { open: decision.idle || forced, forced, blockers: [...decision.blockers] };
|
|
555
|
+
}
|
|
252
556
|
/**
|
|
253
557
|
* Emit a catalogued `auto_update.*` event, guarded so neither a throwing sink nor a bad
|
|
254
558
|
* attribute shape can disturb the tick — telemetry must never break auto-update itself.
|
|
@@ -274,6 +578,7 @@ export class AutoUpdateCoordinator {
|
|
|
274
578
|
/** Core update logic: fetch latest version, classify, and take action. */
|
|
275
579
|
async runTick(state) {
|
|
276
580
|
const { logger } = this.options;
|
|
581
|
+
this.pendingBlockers = [];
|
|
277
582
|
// Stamped before the tag is resolved so a rejected tag reads as "checked
|
|
278
583
|
// just now and failed", not as "stopped polling a while ago".
|
|
279
584
|
state.lastCheckedAt = new Date().toISOString();
|
|
@@ -324,29 +629,311 @@ export class AutoUpdateCoordinator {
|
|
|
324
629
|
"senpi.auto_update.will_update": willUpdate,
|
|
325
630
|
}),
|
|
326
631
|
});
|
|
632
|
+
// A pending 10.x update lives only while an applicable update is seen, so
|
|
633
|
+
// a dist-tag rollback or a mode change does not leave a stale eligibility
|
|
634
|
+
// time that would let the next release skip the spread.
|
|
327
635
|
if (updateType === "none") {
|
|
636
|
+
state.pendingUpdate = undefined;
|
|
328
637
|
this.snapshot = this.buildSnapshot("healthy", `no-update:${latest}`);
|
|
329
638
|
return;
|
|
330
639
|
}
|
|
331
640
|
if (updateType === "major") {
|
|
641
|
+
state.pendingUpdate = undefined;
|
|
332
642
|
await this.handleMajorAvailable(state, latest, spec);
|
|
333
643
|
return;
|
|
334
644
|
}
|
|
335
645
|
if (!this.isAllowedByPolicy(updateType)) {
|
|
646
|
+
state.pendingUpdate = undefined;
|
|
336
647
|
logger.info(`[senpi-auto-update] update blocked by policy | updateType=${updateType} remoteVersion=${latest}`);
|
|
337
648
|
this.snapshot = this.buildSnapshot("healthy", `policy-blocked:${updateType}:${latest}`);
|
|
338
649
|
return;
|
|
339
650
|
}
|
|
340
651
|
if (state.effectiveMode !== "auto-apply") {
|
|
652
|
+
state.pendingUpdate = undefined;
|
|
341
653
|
await this.logUpdateAvailable(state, latest, updateType, spec);
|
|
342
654
|
return;
|
|
343
655
|
}
|
|
344
|
-
if (state.
|
|
345
|
-
|
|
656
|
+
if (state.applyFailures && state.applyFailures.version !== latest) {
|
|
657
|
+
// A newer release after failed applies of an older one starts a new
|
|
658
|
+
// episode with a fresh spread wait. Keeping the old `eligibleAt` (long
|
|
659
|
+
// past) would restart every such box on its first idle tick, and a
|
|
660
|
+
// fleet-wide apply failure makes that the whole fleet at once.
|
|
661
|
+
state.applyFailures = undefined;
|
|
662
|
+
state.pendingUpdate = undefined;
|
|
663
|
+
}
|
|
664
|
+
// The latch fix: a persisted target equal to `latest` means a restart was
|
|
665
|
+
// requested for it and the loaded version is still behind (classifyUpdate
|
|
666
|
+
// above said so). That is trusted only while the request is recent.
|
|
667
|
+
const target = state.pendingRestart?.toVersion ?? state.lastAppliedVersion;
|
|
668
|
+
if (target === latest) {
|
|
669
|
+
if (this.restartStillPending(state) || this.awaitingManualRestart(state)) {
|
|
670
|
+
this.handleAlreadyAppliedAwaitingRestart(state, latest);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
await this.expireUnloadedUpdate(state, latest);
|
|
674
|
+
}
|
|
675
|
+
if ((state.applyFailures?.count ?? 0) >= MAX_APPLY_ATTEMPTS) {
|
|
676
|
+
this.handleParkedUpdate(state, latest);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (this.options.modernHost) {
|
|
680
|
+
await this.runRestartToApply(state, latest, spec);
|
|
346
681
|
return;
|
|
347
682
|
}
|
|
348
683
|
await this.runUpdateTransaction(state, latest, updateType, spec);
|
|
349
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* Whether the restart requested for the persisted target may still land:
|
|
687
|
+
* its request time (the pending restart's, else the last restart trigger,
|
|
688
|
+
* else the last install) is under {@link RESTART_LOAD_TIMEOUT_MS} old. With
|
|
689
|
+
* no time on record the persisted version is not trusted at all.
|
|
690
|
+
*/
|
|
691
|
+
restartStillPending(state) {
|
|
692
|
+
// The latest of the recorded times: an older trigger for a previous
|
|
693
|
+
// version must not expire a fresh install.
|
|
694
|
+
const times = [state.pendingRestart?.requestedAt, state.lastRestartTriggerAt, state.lastSuccessAt]
|
|
695
|
+
.map((iso) => (iso ? Date.parse(iso) : Number.NaN))
|
|
696
|
+
.filter((ms) => Number.isFinite(ms));
|
|
697
|
+
if (times.length === 0)
|
|
698
|
+
return false;
|
|
699
|
+
return this.now() - Math.max(...times) < RESTART_LOAD_TIMEOUT_MS;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* The 5.x latch, as before R2: once this flow installed `latest` (an
|
|
703
|
+
* install or restart time is on record), the box waits for a restart and
|
|
704
|
+
* never runs the in-process install and config-bump restart for that
|
|
705
|
+
* version again, however long it waits. 5.x has no idle gate, so a retry
|
|
706
|
+
* would restart the gateway over an order call or an open approval. A
|
|
707
|
+
* restart trigger that threw is latched too (the warning asks for a manual
|
|
708
|
+
* restart). A persisted applied version with no time at all is still not
|
|
709
|
+
* trusted. 10.x expires its wait after 30 min instead: its retries are
|
|
710
|
+
* idle-gated.
|
|
711
|
+
*/
|
|
712
|
+
awaitingManualRestart(state) {
|
|
713
|
+
if (this.options.modernHost)
|
|
714
|
+
return false;
|
|
715
|
+
return [state.pendingRestart?.requestedAt, state.lastRestartTriggerAt, state.lastSuccessAt].some((iso) => typeof iso === "string" && Number.isFinite(Date.parse(iso)));
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* {@link MAX_APPLY_ATTEMPTS} restarts for `latest` did not load it. Stop
|
|
719
|
+
* trying this version: no install and no restart until a newer one ships
|
|
720
|
+
* (or a restart loads it some other way). Reported, not thrown, so the poll
|
|
721
|
+
* keeps its normal cadence.
|
|
722
|
+
*/
|
|
723
|
+
handleParkedUpdate(state, latest) {
|
|
724
|
+
const count = state.applyFailures?.count ?? 0;
|
|
725
|
+
const message = `update to v${latest} did not load after ${count} restarts; not retrying this version. ` +
|
|
726
|
+
`The gateway stays on v${this.loadedVersion} until a newer release ships or an operator installs it`;
|
|
727
|
+
if (state.lastLoggedAwaitingRestartVersion !== `parked:${latest}`) {
|
|
728
|
+
this.options.logger.warn(`[senpi-auto-update] ${message}`);
|
|
729
|
+
state.lastLoggedAwaitingRestartVersion = `parked:${latest}`;
|
|
730
|
+
}
|
|
731
|
+
this.snapshot = this.buildSnapshot("degraded", `update-not-loaded:${latest}`, message);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* The requested update did not load. Drop the awaiting-restart state and the
|
|
735
|
+
* marker, report `auto_update.apply_failed`, and fail this tick (so the
|
|
736
|
+
* backoff applies). The next tick starts over: a new install and restart on
|
|
737
|
+
* 5.x, a new idle-gated restart request on 10.x. The pending update keeps
|
|
738
|
+
* its eligibility time, so the box does not wait out its spread again.
|
|
739
|
+
*
|
|
740
|
+
* @throws Always.
|
|
741
|
+
*/
|
|
742
|
+
async expireUnloadedUpdate(state, target) {
|
|
743
|
+
const loaded = this.loadedVersion;
|
|
744
|
+
const requestedAt = state.pendingRestart?.requestedAt ?? state.lastRestartTriggerAt ?? "never";
|
|
745
|
+
const message = `update to v${target} did not load: the gateway still runs v${loaded} ` +
|
|
746
|
+
`(restart requested ${requestedAt}); retrying from the start`;
|
|
747
|
+
this.emitEvent(EVENT_NAME.autoUpdateApplyFailed, "warn", {
|
|
748
|
+
body: `Update to v${target} did not load; loaded v${loaded}`,
|
|
749
|
+
attributes: eventAttrs({
|
|
750
|
+
"senpi.auto_update.to_version": target,
|
|
751
|
+
"senpi.auto_update.loaded_version": loaded,
|
|
752
|
+
}),
|
|
753
|
+
});
|
|
754
|
+
this.options.logger.warn(`[senpi-auto-update] ${message}`);
|
|
755
|
+
state.lastAppliedVersion = undefined;
|
|
756
|
+
state.pendingRestart = undefined;
|
|
757
|
+
state.lastLoggedAwaitingRestartVersion = undefined;
|
|
758
|
+
state.applyFailures = {
|
|
759
|
+
version: target,
|
|
760
|
+
count: (state.applyFailures?.version === target ? state.applyFailures.count : 0) + 1,
|
|
761
|
+
};
|
|
762
|
+
await this.clearMarkerQuietly();
|
|
763
|
+
this.snapshot = this.buildSnapshot("degraded", `update-not-loaded:${target}`, message);
|
|
764
|
+
throw new Error(message);
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* The OpenClaw 10.x apply path (restart-to-apply). Nothing is installed in
|
|
768
|
+
* this process: 10.10 refuses `plugins update` while the gateway runs.
|
|
769
|
+
*
|
|
770
|
+
* 1. Record the pending update once per episode (`firstSeenAt`,
|
|
771
|
+
* `eligibleAt` = first seen + spread offset + jitter).
|
|
772
|
+
* 2. Before `eligibleAt`, wait (`pending-idle:<v>`, blocker `spread`). After
|
|
773
|
+
* it, continue only when the idle gate says idle, or when
|
|
774
|
+
* {@link mustForce} (24 h past eligibility, inside 03:00–05:00 UTC).
|
|
775
|
+
* 3. Build the exact `<name>@<version>` spec, check restart capability,
|
|
776
|
+
* write the marker, and request the restart through `mutateConfigFile`.
|
|
777
|
+
* 4. A failure at any step clears the marker, reports
|
|
778
|
+
* `auto_update.restart_failed` and fails the tick (backoff; `checkNow`
|
|
779
|
+
* answers `failed`). Waiting for the spread or an idle box is not a failure.
|
|
780
|
+
*/
|
|
781
|
+
async runRestartToApply(state, latest, spec) {
|
|
782
|
+
const { logger } = this.options;
|
|
783
|
+
const now = this.now();
|
|
784
|
+
if (!state.pendingUpdate) {
|
|
785
|
+
const firstSeenAt = new Date(now).toISOString();
|
|
786
|
+
const eligible = computeEligibleAt(now, this.options.mid ?? "", this.rand);
|
|
787
|
+
state.pendingUpdate = { version: latest, firstSeenAt, eligibleAt: new Date(eligible).toISOString() };
|
|
788
|
+
logger.info(`[senpi-auto-update] v${latest} seen; restart-to-apply eligible at ${state.pendingUpdate.eligibleAt}`);
|
|
789
|
+
}
|
|
790
|
+
else if (state.pendingUpdate.version !== latest) {
|
|
791
|
+
// A newer release during the wait keeps the box's place in the spread.
|
|
792
|
+
state.pendingUpdate = { ...state.pendingUpdate, version: latest };
|
|
793
|
+
}
|
|
794
|
+
const gate = this.applyGate(now, Date.parse(state.pendingUpdate.eligibleAt));
|
|
795
|
+
const forced = gate.forced;
|
|
796
|
+
if (!gate.open) {
|
|
797
|
+
this.pendingBlockers = gate.blockers;
|
|
798
|
+
this.snapshot = this.buildSnapshot("healthy", `pending-idle:${latest}`);
|
|
799
|
+
this.noteDeferred(state.pendingUpdate, latest, gate.blockers);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
let exactSpec;
|
|
803
|
+
try {
|
|
804
|
+
exactSpec = buildExactUpdateSpec(spec, latest);
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
await this.reportModernRestartFailure(latest, formatErr(error), `restart-unavailable:${latest}`);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const capability = evaluateModernRestartCapability({
|
|
811
|
+
readLiveConfig: this.options.readLiveConfig,
|
|
812
|
+
mutateConfigFile: this.options.mutateConfigFile,
|
|
813
|
+
pluginId: this.options.pluginId,
|
|
814
|
+
});
|
|
815
|
+
state.restartCapability = capability.capable ? "capable" : "not-capable";
|
|
816
|
+
state.restartCapabilityReasons = capability.reasons;
|
|
817
|
+
state.restartCapabilityMode = capability.mode;
|
|
818
|
+
const mutateConfigFile = this.options.mutateConfigFile;
|
|
819
|
+
const markerPath = this.options.updateMarkerPath;
|
|
820
|
+
if (!capability.capable || !mutateConfigFile) {
|
|
821
|
+
await this.reportModernRestartFailure(latest, capability.reasons.join(", ") || "unknown", `restart-unavailable:${latest}`);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (!markerPath) {
|
|
825
|
+
state.restartCapability = "not-capable";
|
|
826
|
+
state.restartCapabilityReasons = [...capability.reasons, "update-marker-path-unavailable"];
|
|
827
|
+
await this.reportModernRestartFailure(latest, "update-marker-path-unavailable", `restart-unavailable:${latest}`);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const requestedAt = new Date(now).toISOString();
|
|
831
|
+
state.lastAttemptAt = requestedAt;
|
|
832
|
+
// The pending restart is saved before the restart is requested: the
|
|
833
|
+
// gateway may stop this process right after, and a lost pending restart
|
|
834
|
+
// would never count toward the retry cap. Rolled back if the request fails.
|
|
835
|
+
const before = {
|
|
836
|
+
pendingRestart: state.pendingRestart,
|
|
837
|
+
lastRestartTriggerAt: state.lastRestartTriggerAt,
|
|
838
|
+
lastRestartTriggerMode: state.lastRestartTriggerMode,
|
|
839
|
+
};
|
|
840
|
+
state.pendingRestart = {
|
|
841
|
+
fromVersion: this.loadedVersion,
|
|
842
|
+
toVersion: latest,
|
|
843
|
+
requestedAt,
|
|
844
|
+
mode: "restart-to-apply",
|
|
845
|
+
};
|
|
846
|
+
state.lastRestartTriggerAt = requestedAt;
|
|
847
|
+
state.lastRestartTriggerMode = "restart-to-apply";
|
|
848
|
+
const rollBack = () => {
|
|
849
|
+
state.pendingRestart = before.pendingRestart;
|
|
850
|
+
state.lastRestartTriggerAt = before.lastRestartTriggerAt;
|
|
851
|
+
state.lastRestartTriggerMode = before.lastRestartTriggerMode;
|
|
852
|
+
};
|
|
853
|
+
try {
|
|
854
|
+
await this.store.save(state);
|
|
855
|
+
}
|
|
856
|
+
catch (error) {
|
|
857
|
+
// Not a restart-capability problem: the state dir is not writable. No
|
|
858
|
+
// restart without a saved pending restart, or the retry cap is lost.
|
|
859
|
+
rollBack();
|
|
860
|
+
await this.reportModernRestartFailure(latest, `state-save-failed:${formatErr(error)}`, `state-save-failed:${latest}`, error);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
try {
|
|
864
|
+
await writeUpdateMarker(markerPath, { spec: exactSpec, targetVersion: latest, requestedAt });
|
|
865
|
+
// Read the gate again after the awaits above: a money call or a user
|
|
866
|
+
// turn that started meanwhile holds the restart, as at the first read.
|
|
867
|
+
const again = this.readIdle(this.now());
|
|
868
|
+
if (!again.idle && !(forced && onlyOverridable(again.blockers))) {
|
|
869
|
+
rollBack();
|
|
870
|
+
await this.clearMarkerQuietly();
|
|
871
|
+
this.pendingBlockers = [...again.blockers];
|
|
872
|
+
this.snapshot = this.buildSnapshot("healthy", `pending-idle:${latest}`);
|
|
873
|
+
if (state.pendingUpdate)
|
|
874
|
+
this.noteDeferred(state.pendingUpdate, latest, again.blockers);
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
await triggerGatewayRestartViaMutate({ mutateConfigFile, pluginId: this.options.pluginId, triggerValue: requestedAt });
|
|
878
|
+
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
rollBack();
|
|
881
|
+
const reason = formatErr(error);
|
|
882
|
+
state.restartCapability = "not-capable";
|
|
883
|
+
state.restartCapabilityReasons = [...state.restartCapabilityReasons, `restart-trigger-failed:${reason}`];
|
|
884
|
+
await this.reportModernRestartFailure(latest, reason, `restart-request-failed:${latest}`, error);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
this.emitEvent(EVENT_NAME.autoUpdateRestartRequested, "info", {
|
|
888
|
+
body: `Restart requested to apply v${latest} (from v${this.loadedVersion}${forced ? ", forced after 24 h busy" : ""})`,
|
|
889
|
+
attributes: eventAttrs({
|
|
890
|
+
"senpi.auto_update.from_version": this.loadedVersion,
|
|
891
|
+
"senpi.auto_update.to_version": latest,
|
|
892
|
+
"senpi.auto_update.restart_mode": "restart-to-apply",
|
|
893
|
+
"senpi.auto_update.forced": forced,
|
|
894
|
+
"senpi.auto_update.blockers": forced ? blockerList(gate.blockers) : undefined,
|
|
895
|
+
}),
|
|
896
|
+
});
|
|
897
|
+
logger.info(`[senpi-auto-update] marker written (${exactSpec}); gateway restart requested to apply v${latest}` +
|
|
898
|
+
(forced ? ` (forced: busy for 24 h past eligibility; blockers ${gate.blockers.join(",")})` : ""));
|
|
899
|
+
this.snapshot = this.buildSnapshot("healthy", `updated-restart-requested:${latest}`);
|
|
900
|
+
}
|
|
901
|
+
/** The idle gate's answer; a throwing gate counts as busy. */
|
|
902
|
+
readIdle(now) {
|
|
903
|
+
const gate = this.options.idleGate;
|
|
904
|
+
if (!gate)
|
|
905
|
+
return { idle: true, blockers: [] };
|
|
906
|
+
try {
|
|
907
|
+
return gate.isIdle(now);
|
|
908
|
+
}
|
|
909
|
+
catch (error) {
|
|
910
|
+
this.options.logger.warn(`[senpi-auto-update] idle gate threw; treating the box as busy | err=${formatErr(error)}`);
|
|
911
|
+
return { idle: false, blockers: ["idle-gate-error"] };
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Clear the marker, report `auto_update.restart_failed` for the 10.x path
|
|
916
|
+
* (snapshot degraded), and fail the tick: no restart was requested, so the
|
|
917
|
+
* tick must not count as a success. The throw makes `tick()` back off and
|
|
918
|
+
* `checkNow()` answer `failed`.
|
|
919
|
+
*
|
|
920
|
+
* @throws Always, with `reason`.
|
|
921
|
+
*/
|
|
922
|
+
async reportModernRestartFailure(latest, reason, lastAction, error) {
|
|
923
|
+
await this.clearMarkerQuietly();
|
|
924
|
+
this.emitEvent(EVENT_NAME.autoUpdateRestartFailed, "error", {
|
|
925
|
+
body: `Restart to apply v${latest} was not requested: ${reason}`,
|
|
926
|
+
attributes: eventAttrs({
|
|
927
|
+
"senpi.auto_update.to_version": latest,
|
|
928
|
+
"senpi.auto_update.reason": reason,
|
|
929
|
+
}),
|
|
930
|
+
...(error === undefined ? {} : { error }),
|
|
931
|
+
});
|
|
932
|
+
this.options.logger.warn(`[senpi-auto-update] v${latest} is available but the gateway restart that applies it was not requested. ` +
|
|
933
|
+
`Nothing was installed; the loaded version stays v${this.loadedVersion}. Reason(s): ${reason}`);
|
|
934
|
+
this.snapshot = this.buildSnapshot("degraded", lastAction, reason);
|
|
935
|
+
throw new Error(`restart to apply v${latest} was not requested: ${reason}`);
|
|
936
|
+
}
|
|
350
937
|
/**
|
|
351
938
|
* `currentVersion` is a compile-time constant frozen for the life of the
|
|
352
939
|
* process, so it cannot reflect an install that already happened this
|
|
@@ -359,19 +946,30 @@ export class AutoUpdateCoordinator {
|
|
|
359
946
|
* so this branch stops being reached on its own.
|
|
360
947
|
*/
|
|
361
948
|
handleAlreadyAppliedAwaitingRestart(state, latest) {
|
|
949
|
+
const modern = this.options.modernHost === true;
|
|
362
950
|
if (state.lastLoggedAwaitingRestartVersion !== latest) {
|
|
363
|
-
this.options.logger.info(
|
|
951
|
+
this.options.logger.info(modern
|
|
952
|
+
? `[senpi-auto-update] restart to apply v${latest} requested, awaiting gateway restart | not requesting again`
|
|
953
|
+
: `[senpi-auto-update] update to v${latest} already applied, awaiting gateway restart to take effect | skipping reinstall`);
|
|
364
954
|
state.lastLoggedAwaitingRestartVersion = latest;
|
|
365
955
|
}
|
|
366
956
|
// Fired on every guard hit (not deduped like the log line above) — an operator watching for
|
|
367
957
|
// "how long has this box been stuck awaiting restart" needs every occurrence, not just the first.
|
|
368
958
|
this.emitEvent(EVENT_NAME.autoUpdateAwaitingRestart, "warn", {
|
|
369
|
-
body:
|
|
959
|
+
body: modern
|
|
960
|
+
? `Restart to apply v${latest} requested, still awaiting gateway restart`
|
|
961
|
+
: `Update to v${latest} already applied, still awaiting gateway restart`,
|
|
370
962
|
attributes: eventAttrs({
|
|
371
963
|
"senpi.auto_update.to_version": latest,
|
|
372
964
|
}),
|
|
373
965
|
});
|
|
374
|
-
|
|
966
|
+
// On 10.x nothing is installed until the wrapper consumes the marker, so
|
|
967
|
+
// "already applied" would be false there.
|
|
968
|
+
this.snapshot = this.buildSnapshot("degraded", modern ? `awaiting-restart:${latest}` : `already-applied-awaiting-restart:${latest}`);
|
|
969
|
+
}
|
|
970
|
+
/** OpenClaw 10.x refuses `plugins update` while the gateway runs, so a manual instruction says so. */
|
|
971
|
+
manualUpdateSuffix() {
|
|
972
|
+
return this.options.modernHost ? " with the gateway stopped" : "";
|
|
375
973
|
}
|
|
376
974
|
isAllowedByPolicy(kind) {
|
|
377
975
|
if (kind === "patch")
|
|
@@ -394,7 +992,7 @@ export class AutoUpdateCoordinator {
|
|
|
394
992
|
});
|
|
395
993
|
this.options.logger.info(`[senpi-auto-update] Senpi update available: v${latest} (major). ` +
|
|
396
994
|
`Auto-update will not apply major versions. ` +
|
|
397
|
-
`Review changelog and run: openclaw plugins update ${spec}`);
|
|
995
|
+
`Review changelog and run: openclaw plugins update ${spec}${this.manualUpdateSuffix()}`);
|
|
398
996
|
state.lastNotifiedMajorVersion = latest;
|
|
399
997
|
this.snapshot = this.buildSnapshot("healthy", `major-notified:${latest}`);
|
|
400
998
|
}
|
|
@@ -412,7 +1010,7 @@ export class AutoUpdateCoordinator {
|
|
|
412
1010
|
});
|
|
413
1011
|
this.options.logger.info(`[senpi-auto-update] Senpi update available: v${latest} (${kind}). ` +
|
|
414
1012
|
`Auto-update mode is notify-only. ` +
|
|
415
|
-
`Run: openclaw plugins update ${spec}`);
|
|
1013
|
+
`Run: openclaw plugins update ${spec}${this.manualUpdateSuffix()}`);
|
|
416
1014
|
state.lastNotifiedUpdateVersion = latest;
|
|
417
1015
|
this.snapshot = this.buildSnapshot("healthy", `update-notified:${kind}:${latest}`);
|
|
418
1016
|
}
|
|
@@ -495,6 +1093,12 @@ export class AutoUpdateCoordinator {
|
|
|
495
1093
|
});
|
|
496
1094
|
state.lastRestartTriggerAt = nowIso;
|
|
497
1095
|
state.lastRestartTriggerMode = capability.mode ?? "unknown";
|
|
1096
|
+
state.pendingRestart = {
|
|
1097
|
+
fromVersion: this.loadedVersion,
|
|
1098
|
+
toVersion: latest,
|
|
1099
|
+
requestedAt: nowIso,
|
|
1100
|
+
mode: state.lastRestartTriggerMode,
|
|
1101
|
+
};
|
|
498
1102
|
this.emitEvent(EVENT_NAME.autoUpdateApplied, "info", {
|
|
499
1103
|
body: `Update applied: ${this.options.currentVersion} -> v${latest}; restart requested (${state.lastRestartTriggerMode})`,
|
|
500
1104
|
attributes: eventAttrs({
|
|
@@ -553,6 +1157,7 @@ export class AutoUpdateCoordinator {
|
|
|
553
1157
|
restartCapabilityReasons: state.restartCapabilityReasons,
|
|
554
1158
|
lastRestartTriggerAt: state.lastRestartTriggerAt ?? null,
|
|
555
1159
|
lastRestartTriggerMode: state.lastRestartTriggerMode ?? null,
|
|
1160
|
+
pendingBlockers: this.pendingBlockers,
|
|
556
1161
|
};
|
|
557
1162
|
}
|
|
558
1163
|
async persistState(state) {
|
|
@@ -573,6 +1178,10 @@ async function defaultRunUpdateCommand(packageName) {
|
|
|
573
1178
|
throw new Error(result.stderr || `openclaw plugins update failed with code ${result.code}`);
|
|
574
1179
|
}
|
|
575
1180
|
}
|
|
1181
|
+
/** A blocker set as one stable value: de-duplicated, sorted, comma-joined. */
|
|
1182
|
+
function blockerList(blockers) {
|
|
1183
|
+
return [...new Set(blockers)].sort().join(",");
|
|
1184
|
+
}
|
|
576
1185
|
function formatErr(error) {
|
|
577
1186
|
return error instanceof Error ? error.message : String(error);
|
|
578
1187
|
}
|