fix(acpx): validate runtime session mode at wrapper boundary (#73071) (#73548)

This commit is contained in:
Alex Knight
2026-04-28 22:35:25 +10:00
committed by GitHub
parent 7a23b2d945
commit 7a23c18830
3 changed files with 60 additions and 1 deletions

View File

@@ -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

View File

@@ -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),

View File

@@ -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<AcpRuntime["ensureSession"]>[0],
): Promise<AcpRuntimeHandle> {
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,