diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c5233f9142..c61e93db7b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai - Channels/Discord: bound message read/search REST calls, route those actions through Gateway execution, and fall back to `CommandTargetSessionKey` for inbound hook session keys so Discord reads do not hang and hooks still fire when `SessionKey` is empty. Fixes #73431. (#73521) Thanks @amknight. - Plugins/media: auto-enable provider plugins referenced by `agents.defaults.imageGenerationModel`, `videoGenerationModel`, and `musicGenerationModel` primary/fallback refs, so configured Google and MiniMax media providers do not stay disabled behind a restrictive plugin allowlist. Thanks @vincentkoc. - Memory-core/dreaming: retry managed dreaming cron registration after startup when the cron service is not reachable yet, so the scheduled Memory Dreaming Promotion sweep recovers without waiting for heartbeat traffic. Fixes #72841. Thanks @amknight. +- Acpx/runtime: validate the runtime session mode at the `AcpxRuntime.ensureSession` wrapper boundary so callers that pass anything other than `persistent` or `oneshot` get a clear `ACP_INVALID_RUNTIME_OPTION` error instead of silently round-tripping through the encoded handle as a default `persistent` mode and later throwing `SessionResumeRequiredError`. Investigation context: #73071. (#73548) Thanks @amknight. ## 2026.4.27 diff --git a/extensions/acpx/src/runtime.test.ts b/extensions/acpx/src/runtime.test.ts index 97e2ab4a0b9..3d74c9216b3 100644 --- a/extensions/acpx/src/runtime.test.ts +++ b/extensions/acpx/src/runtime.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { AcpRuntime } from "../runtime-api.js"; +import { AcpRuntimeError, type AcpRuntime } from "../runtime-api.js"; import { AcpxRuntime, __testing } from "./runtime.js"; type TestSessionStore = { @@ -85,6 +85,43 @@ describe("AcpxRuntime fresh reset wrapper", () => { vi.restoreAllMocks(); }); + it("rejects unsupported runtime session modes with a clear AcpRuntimeError (issue #73071)", async () => { + const baseStore: TestSessionStore = { + load: vi.fn(async () => undefined), + save: vi.fn(async () => {}), + }; + const { runtime, delegate } = makeRuntime(baseStore); + const ensureSpy = vi.spyOn(delegate, "ensureSession").mockResolvedValue({ + sessionKey: "agent:claude:acp:test", + backend: "acpx", + runtimeSessionName: "claude", + }); + + for (const badMode of ["run", "session", "", undefined, null, 0]) { + await expect( + runtime.ensureSession({ + sessionKey: "agent:claude:acp:test", + agent: "claude", + mode: badMode as never, + }), + ).rejects.toMatchObject({ + name: "AcpRuntimeError", + code: "ACP_INVALID_RUNTIME_OPTION", + message: expect.stringContaining("Unsupported ACP runtime session mode"), + }); + } + + expect(ensureSpy).not.toHaveBeenCalled(); + }); + + it("exposes assertSupportedRuntimeSessionMode as a typed guard", () => { + expect(() => __testing.assertSupportedRuntimeSessionMode("persistent")).not.toThrow(); + expect(() => __testing.assertSupportedRuntimeSessionMode("oneshot")).not.toThrow(); + expect(() => __testing.assertSupportedRuntimeSessionMode("run" as never)).toThrow( + AcpRuntimeError, + ); + }); + it("normalizes OpenClaw Codex model ids for ACP startup", async () => { const baseStore: TestSessionStore = { load: vi.fn(async () => undefined), diff --git a/extensions/acpx/src/runtime.ts b/extensions/acpx/src/runtime.ts index 4bc2dcc0287..252eef132ea 100644 --- a/extensions/acpx/src/runtime.ts +++ b/extensions/acpx/src/runtime.ts @@ -231,6 +231,25 @@ function failUnsupportedCodexAcpModel(rawModel: string, detail?: string): never ); } +// acpx's `decodeAcpxRuntimeHandleState` only accepts `persistent` and `oneshot`; any other +// value silently round-trips through the encoded handle as `persistent` and later throws +// `SessionResumeRequiredError` on agent restart. Fail fast at this boundary instead. +// See openclaw/openclaw#73071. +const SUPPORTED_RUNTIME_SESSION_MODES = new Set(["persistent", "oneshot"] as const); + +function assertSupportedRuntimeSessionMode( + mode: unknown, +): asserts mode is "persistent" | "oneshot" { + if (typeof mode === "string" && SUPPORTED_RUNTIME_SESSION_MODES.has(mode as never)) { + return; + } + const supported = Array.from(SUPPORTED_RUNTIME_SESSION_MODES).join(", "); + throw new AcpRuntimeError( + "ACP_INVALID_RUNTIME_OPTION", + `Unsupported ACP runtime session mode ${JSON.stringify(mode)}. Expected one of: ${supported}.`, + ); +} + function failUnsupportedCodexAcpThinking(rawThinking: string): never { throw new AcpRuntimeError( "ACP_INVALID_RUNTIME_OPTION", @@ -460,6 +479,7 @@ export class AcpxRuntime implements AcpRuntime { async ensureSession( input: Parameters[0], ): Promise { + assertSupportedRuntimeSessionMode(input.mode); const command = resolveAgentCommandForName({ agentName: input.agent, agentRegistry: this.agentRegistry, @@ -584,6 +604,7 @@ export { export const __testing = { appendCodexAcpConfigOverrides, + assertSupportedRuntimeSessionMode, codexAcpSessionModelId, isCodexAcpCommand, normalizeCodexAcpModelOverride,