mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
fix: continue update runs after restart (#74362) (thanks @100menotu001)
This commit is contained in:
@@ -157,6 +157,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers/Ollama: restore catalog context-window forwarding as `num_ctx` for native `/api/chat` requests; fixes tool selection and context truncation regressions on models with catalog entries (qwen3, llama3, gemma3, …) when no explicit `params.num_ctx` was configured. Fixes #76117. (#76181) Thanks @openperf.
|
||||
|
||||
- Plugins/providers: preserve scoped cold-load fallback for enabled external manifest-contract capability providers missing from the startup registry, so providers such as Fish Audio can resolve on request without requiring `activation.onStartup` for correctness. (#76536) Thanks @Conan-Scott.
|
||||
- Gateway/update: carry `continuationMessage` from `update.run` into successful restart sentinels so session-scoped self-updates can resume one follow-up turn after the Gateway restarts. Refs #71178. (#74362) Thanks @100menotu001, @HeilbronAILabs, and @artnking.
|
||||
|
||||
## 2026.5.2
|
||||
|
||||
|
||||
@@ -609,7 +609,7 @@ For tooling that writes config over the gateway API, prefer this flow:
|
||||
- `config.patch` for partial updates (JSON merge patch: objects merge, `null`
|
||||
deletes, arrays replace)
|
||||
- `config.apply` only when you intend to replace the entire config
|
||||
- `update.run` for explicit self-update plus restart
|
||||
- `update.run` for explicit self-update plus restart; include `continuationMessage` when the post-restart session should run one follow-up turn
|
||||
- `update.status` to inspect the latest update restart sentinel and verify the running version after a restart
|
||||
|
||||
Agents should treat `config.schema.lookup` as the first stop for exact
|
||||
|
||||
@@ -381,7 +381,7 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
||||
- `config.apply` validates + replaces the full config payload.
|
||||
- `config.schema` returns the live config schema payload used by Control UI and CLI tooling: schema, `uiHints`, version, and generation metadata, including plugin + channel schema metadata when the runtime can load it. The schema includes field `title` / `description` metadata derived from the same labels and help text used by the UI, including nested object, wildcard, array-item, and `anyOf` / `oneOf` / `allOf` composition branches when matching field documentation exists.
|
||||
- `config.schema.lookup` returns a path-scoped lookup payload for one config path: normalized path, a shallow schema node, matched hint + `hintPath`, and immediate child summaries for UI/CLI drill-down. Lookup schema nodes keep the user-facing docs and common validation fields (`title`, `description`, `type`, `enum`, `const`, `format`, `pattern`, numeric/string/array/object bounds, and flags like `additionalProperties`, `deprecated`, `readOnly`, `writeOnly`). Child summaries expose `key`, normalized `path`, `type`, `required`, `hasChildren`, plus the matched `hint` / `hintPath`.
|
||||
- `update.run` runs the gateway update flow and schedules a restart only when the update itself succeeded. Package-manager updates force a non-deferred, no-cooldown update restart after the package swap so the old Gateway process does not keep lazy-loading from a replaced `dist` tree.
|
||||
- `update.run` runs the gateway update flow and schedules a restart only when the update itself succeeded; callers with a session can include `continuationMessage` so startup resumes one follow-up agent turn through the restart continuation queue. Package-manager updates force a non-deferred, no-cooldown update restart after the package swap so the old Gateway process does not keep lazy-loading from a replaced `dist` tree.
|
||||
- `update.status` returns the latest cached update restart sentinel, including the post-restart running version when available.
|
||||
- `wizard.start`, `wizard.next`, `wizard.status`, and `wizard.cancel` expose the onboarding wizard over WS RPC.
|
||||
|
||||
|
||||
@@ -708,12 +708,14 @@ describe("gateway tool", () => {
|
||||
await tool.execute("call3", {
|
||||
action: "update.run",
|
||||
note: "test update",
|
||||
continuationMessage: "Report the update result after restart.",
|
||||
});
|
||||
|
||||
expect(callGatewayTool).toHaveBeenCalledWith(
|
||||
"update.run",
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
continuationMessage: "Report the update result after restart.",
|
||||
note: "test update",
|
||||
sessionKey,
|
||||
}),
|
||||
|
||||
@@ -519,6 +519,7 @@ export function createGatewayTool(opts?: {
|
||||
}
|
||||
if (action === "update.run") {
|
||||
const { sessionKey, note, restartDelayMs } = resolveGatewayWriteMeta();
|
||||
const continuationMessage = normalizeOptionalString(params.continuationMessage);
|
||||
const updateTimeoutMs = gatewayOpts.timeoutMs ?? DEFAULT_UPDATE_TIMEOUT_MS;
|
||||
const updateGatewayOpts = {
|
||||
...gatewayOpts,
|
||||
@@ -527,6 +528,7 @@ export function createGatewayTool(opts?: {
|
||||
const result = await callGatewayTool("update.run", updateGatewayOpts, {
|
||||
sessionKey,
|
||||
note,
|
||||
continuationMessage,
|
||||
restartDelayMs,
|
||||
timeoutMs: updateTimeoutMs,
|
||||
});
|
||||
|
||||
@@ -58,6 +58,7 @@ export const UpdateRunParamsSchema = Type.Object(
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
deliveryContext: Type.Optional(ConfigDeliveryContextSchema),
|
||||
note: Type.Optional(Type.String()),
|
||||
continuationMessage: Type.Optional(Type.String()),
|
||||
restartDelayMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
},
|
||||
|
||||
@@ -39,15 +39,19 @@ export function parseRestartRequestParams(params: unknown): {
|
||||
deliveryContext: RestartDeliveryContext | undefined;
|
||||
threadId: string | undefined;
|
||||
note: string | undefined;
|
||||
continuationMessage: string | undefined;
|
||||
restartDelayMs: number | undefined;
|
||||
} {
|
||||
const sessionKey = normalizeOptionalString((params as { sessionKey?: unknown }).sessionKey);
|
||||
const { deliveryContext, threadId } = parseRestartDeliveryContext(params);
|
||||
const note = normalizeOptionalString((params as { note?: unknown }).note);
|
||||
const continuationMessage = normalizeOptionalString(
|
||||
(params as { continuationMessage?: unknown }).continuationMessage,
|
||||
);
|
||||
const restartDelayMsRaw = (params as { restartDelayMs?: unknown }).restartDelayMs;
|
||||
const restartDelayMs =
|
||||
typeof restartDelayMsRaw === "number" && Number.isFinite(restartDelayMsRaw)
|
||||
? Math.max(0, Math.floor(restartDelayMsRaw))
|
||||
: undefined;
|
||||
return { sessionKey, deliveryContext, threadId, note, restartDelayMs };
|
||||
return { sessionKey, deliveryContext, threadId, note, continuationMessage, restartDelayMs };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { RestartSentinelPayload } from "../../infra/restart-sentinel.js";
|
||||
import {
|
||||
DEFAULT_RESTART_SUCCESS_CONTINUATION_MESSAGE,
|
||||
type RestartSentinelPayload,
|
||||
} from "../../infra/restart-sentinel.js";
|
||||
import type { UpdateInstallSurface, UpdateRunResult } from "../../infra/update-runner.js";
|
||||
|
||||
// Capture the sentinel payload written during update.run
|
||||
@@ -102,6 +105,7 @@ vi.mock("./restart-request.js", () => ({
|
||||
parseRestartRequestParams: (params: Record<string, unknown>) => ({
|
||||
sessionKey: params.sessionKey,
|
||||
note: params.note,
|
||||
continuationMessage: params.continuationMessage,
|
||||
restartDelayMs: undefined,
|
||||
}),
|
||||
}));
|
||||
@@ -167,7 +171,10 @@ describe("update.run sentinel deliveryContext", () => {
|
||||
to: "webchat:user-123",
|
||||
accountId: "default",
|
||||
});
|
||||
expect(capturedPayload!.continuation).toBeUndefined();
|
||||
expect(capturedPayload!.continuation).toEqual({
|
||||
kind: "agentTurn",
|
||||
message: DEFAULT_RESTART_SUCCESS_CONTINUATION_MESSAGE,
|
||||
});
|
||||
});
|
||||
|
||||
it("omits deliveryContext when no sessionKey is provided", async () => {
|
||||
@@ -193,7 +200,25 @@ describe("update.run sentinel deliveryContext", () => {
|
||||
accountId: "workspace-1",
|
||||
});
|
||||
expect(capturedPayload!.threadId).toBe("1234567890.123456");
|
||||
expect(capturedPayload!.continuation).toBeUndefined();
|
||||
expect(capturedPayload!.continuation).toEqual({
|
||||
kind: "agentTurn",
|
||||
message: DEFAULT_RESTART_SUCCESS_CONTINUATION_MESSAGE,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses an explicit continuationMessage in successful update sentinels", async () => {
|
||||
capturedPayload = undefined;
|
||||
|
||||
await invokeUpdateRun({
|
||||
sessionKey: "agent:main:webchat:dm:user-123",
|
||||
continuationMessage: "Check the running version and finish the update report.",
|
||||
});
|
||||
|
||||
expect(capturedPayload).toBeDefined();
|
||||
expect(capturedPayload!.continuation).toEqual({
|
||||
kind: "agentTurn",
|
||||
message: "Check the running version and finish the update report.",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -234,10 +259,16 @@ describe("update.run restart scheduling", () => {
|
||||
|
||||
let payload: { ok: boolean; restart: unknown } | undefined;
|
||||
|
||||
await invokeUpdateRun({}, (_ok: boolean, response: unknown) => {
|
||||
const typed = response as { ok: boolean; restart: unknown };
|
||||
payload = typed;
|
||||
});
|
||||
await invokeUpdateRun(
|
||||
{
|
||||
sessionKey: "agent:main:webchat:dm:user-123",
|
||||
continuationMessage: "This should not run after a failed update.",
|
||||
},
|
||||
(_ok: boolean, response: unknown) => {
|
||||
const typed = response as { ok: boolean; restart: unknown };
|
||||
payload = typed;
|
||||
},
|
||||
);
|
||||
|
||||
expect(scheduleGatewaySigusr1RestartMock).not.toHaveBeenCalled();
|
||||
expect(payload?.ok).toBe(false);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { extractDeliveryInfo } from "../../config/sessions.js";
|
||||
import { resolveOpenClawPackageRoot } from "../../infra/openclaw-root.js";
|
||||
import { readPackageVersion } from "../../infra/package-json.js";
|
||||
import {
|
||||
buildRestartSuccessContinuation,
|
||||
formatDoctorNonInteractiveHint,
|
||||
type RestartSentinelPayload,
|
||||
writeRestartSentinel,
|
||||
@@ -40,6 +41,7 @@ export const updateHandlers: GatewayRequestHandlers = {
|
||||
deliveryContext: requestedDeliveryContext,
|
||||
threadId: requestedThreadId,
|
||||
note,
|
||||
continuationMessage,
|
||||
restartDelayMs,
|
||||
} = parseRestartRequestParams(params);
|
||||
const { deliveryContext: sessionDeliveryContext, threadId: sessionThreadId } =
|
||||
@@ -99,6 +101,10 @@ export const updateHandlers: GatewayRequestHandlers = {
|
||||
};
|
||||
}
|
||||
|
||||
const continuation =
|
||||
result.status === "ok"
|
||||
? buildRestartSuccessContinuation({ sessionKey, continuationMessage })
|
||||
: null;
|
||||
const payload: RestartSentinelPayload = {
|
||||
kind: "update",
|
||||
status: result.status,
|
||||
@@ -107,6 +113,7 @@ export const updateHandlers: GatewayRequestHandlers = {
|
||||
deliveryContext,
|
||||
threadId,
|
||||
message: note ?? null,
|
||||
...(continuation ? { continuation } : {}),
|
||||
doctorHint: formatDoctorNonInteractiveHint(),
|
||||
stats: {
|
||||
mode: result.mode,
|
||||
|
||||
Reference in New Issue
Block a user