fix: ACP model requests fail for aliases and provider references (#152850)

Resolve ACP model references against the connected harness catalog before useful turns, preserving exact model IDs, accepted settings, and conversation history.

Refs #124843.

Reviewed-by: @shakkernerd
Co-authored-by: Shakker <165377636+shakkernerd@users.noreply.github.com>
This commit is contained in:
Bobby Bones 2026-09-20 18:09:54 -04:00 • committed by GitHub
parent 2c6626c618
commit cc522878b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 518 additions and 54 deletions

View file

@ -5,7 +5,7 @@ extensions/acpx/src/codex-auth-bridge.ts 7
extensions/acpx/src/codex-trust-config.ts 1
extensions/acpx/src/pi-session-catalog-runtime.ts 1
extensions/acpx/src/process-lease.ts 3
extensions/acpx/src/runtime.ts 4
extensions/acpx/src/runtime.ts 2
extensions/acpx/src/service.ts 1
extensions/acpx/src/state.ts 1
extensions/active-memory/config.ts 2

View file

@ -95,6 +95,12 @@ thinking level in sync with that response. A model switch may lower the level or
remove thinking support; subsequent turns and reconnects use the accepted
selection instead of replaying the old level. Backend defaults do not become new
session overrides, and the model reference keeps its OpenClaw provider prefix.
With Cursor, model requests can use exact advertised IDs, selectors with one
advertised variant, or OpenClaw `provider/model` references to either. Unknown or
ambiguous requests fail visibly. An exact advertised ID that contains `/` takes
precedence over interpreting it as a provider-qualified reference.
Model overrides are validated before prompt submission, including after reconnect.
Unsupported inherited defaults dropped during new session initialization are not
saved as overrides.

View file

@ -10,7 +10,7 @@
"dependencies": {
"@agentclientprotocol/claude-agent-acp": "0.76.0",
"@agentclientprotocol/codex-acp": "1.10.0",
"acpx": "0.17.1",
"acpx": "0.18.0",
"smol-toml": "1.8.0",
"zod": "4.6.4"
},

View file

@ -0,0 +1,64 @@
import {
isRequestedModelUnsupportedError,
type AcpxRuntime as BaseAcpxRuntime,
} from "acpx/runtime";
import type { AcpRuntime } from "../runtime-api.js";
type DelegateEnsureInput = Parameters<BaseAcpxRuntime["ensureSession"]>[0];
type EnsureInput = Parameters<AcpRuntime["ensureSession"]>[0] &
Pick<DelegateEnsureInput, "sessionOptions">;
export function withAcpxSessionOptions(input: EnsureInput): DelegateEnsureInput {
const model = input.model?.trim() || input.sessionOptions?.model;
const sessionOptions = model ? { ...input.sessionOptions, model } : input.sessionOptions;
const { modelExplicit: _modelExplicit, thinkingExplicit: _thinkingExplicit, ...rest } = input;
return { ...rest, ...(sessionOptions ? { sessionOptions } : {}) };
}
// Try the exact harness id first. ACPX owns live catalog validation and vendor aliases;
// only its typed rejection allows retrying an OpenClaw provider/model reference.
export async function withOpenClawModelRef<T>(
requested: string,
apply: (model: string) => Promise<T>,
): Promise<T> {
try {
return await apply(requested);
} catch (error) {
const model = requested.trim();
const slash = model.indexOf("/");
if (
!isRequestedModelUnsupportedError(error) ||
error.reason !== "unadvertised-model" ||
error.ambiguous === true ||
slash <= 0 ||
slash === model.length - 1
) {
throw error;
}
return await apply(model.slice(slash + 1));
}
}
export async function ensureSessionWithModelRef(
ensureSession: BaseAcpxRuntime["ensureSession"],
input: EnsureInput,
): Promise<Awaited<ReturnType<AcpRuntime["ensureSession"]>>> {
const ensure = (model: string | undefined) =>
ensureSession(withAcpxSessionOptions({ ...input, model }));
const requested = input.model?.trim();
try {
return requested
? await withOpenClawModelRef(requested, ensure)
: await ensureSession(withAcpxSessionOptions(input));
} catch (error) {
if (
!requested ||
input.modelExplicit ||
!isRequestedModelUnsupportedError(error) ||
error.reason !== "missing-capability"
) {
throw error;
}
return { ...(await ensure(undefined)), appliedModel: { kind: "dropped" } };
}
}

View file

@ -0,0 +1,257 @@
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createAgentRegistry, createFileSessionStore, type AcpProcessStarted } from "acpx/runtime";
import { withOpenClawTestState } from "openclaw/plugin-sdk/test-state";
import { expect, it } from "vitest";
import { AcpxRuntime } from "./runtime.js";
const script = fileURLToPath(new URL("../test/fixtures/model-catalog-agent.mjs", import.meta.url));
function isAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
async function withRuntime(
run: (
runtime: AcpxRuntime,
spawned: AcpProcessStarted[],
restart: () => AcpxRuntime,
) => Promise<void>,
cursor = true,
) {
await withOpenClawTestState({ label: "acpx-advertised-model" }, async (state) => {
let executable = process.execPath;
if (cursor) {
executable = path.join(
state.root,
process.platform === "win32" ? "cursor-agent.exe" : "cursor-agent",
);
if (process.platform === "win32") {
await fs.copyFile(process.execPath, executable);
} else {
// Preserve Node's executable-relative libraries while exercising ACPX's Cursor contract.
await fs.symlink(process.execPath, executable);
}
}
const spawned: AcpProcessStarted[] = [];
const runtimes: AcpxRuntime[] = [];
const create = () => {
const created = new AcpxRuntime({
cwd: state.root,
sessionStore: createFileSessionStore({ stateDir: state.root }),
agentRegistry: createAgentRegistry({ overrides: { catalog: [executable, script] } }),
permissionMode: "deny-all",
timeoutMs: 10_000,
processLifecycle: { onSpawned: (started) => void spawned.push(started) },
});
runtimes.push(created);
return created;
};
try {
await run(create(), spawned, create);
} finally {
for (const runtime of runtimes) {
await runtime.shutdown();
}
}
});
}
async function prompt(
runtime: AcpxRuntime,
handle: Awaited<ReturnType<AcpxRuntime["ensureSession"]>>,
text: string,
) {
const turn = runtime.startTurn({ handle, text, mode: "prompt", requestId: text });
const chunks: string[] = [];
for await (const event of turn.events) {
if (event.type === "text_delta") {
chunks.push(event.text);
}
}
expect(await turn.result).toMatchObject({ status: "completed" });
return JSON.parse(chunks.join(""));
}
it("selects the unique advertised id for an explicit model ref", async () => {
await withRuntime(async (runtime) => {
const handle = await runtime.ensureSession({
sessionKey: "agent:main:acp:catalog-explicit",
agent: "catalog",
mode: "persistent",
model: "cursor/composer-2.5",
modelExplicit: true,
});
// Session metadata keeps the OpenClaw ref; the harness reports the advertised id.
expect(handle.appliedModel).toBeUndefined();
expect(await runtime.getStatus({ handle })).toMatchObject({
models: { currentModelId: "composer-2.5[fast=true]" },
});
// Replay the original OpenClaw ref before the first useful turn.
await runtime.setConfigOption({ handle, key: "model", value: "cursor/composer-2.5" });
expect(await prompt(runtime, handle, "first")).toMatchObject({
model: "composer-2.5[fast=true]",
});
const accepted = await runtime.setConfigOption({
handle,
key: "model",
value: "xai/grok-4.5",
});
expect(accepted).toMatchObject({
configOptions: expect.arrayContaining([
expect.objectContaining({
id: "model",
currentValue: "grok-4.5[effort=high,fast=true]",
}),
]),
});
expect(await prompt(runtime, handle, "changed")).toMatchObject({
model: "grok-4.5[effort=high,fast=true]",
});
});
});
it("matches a derived provider-prefixed model ref to the advertised id", async () => {
await withRuntime(async (runtime) => {
const handle = await runtime.ensureSession({
sessionKey: "agent:main:acp:catalog-derived",
agent: "catalog",
mode: "persistent",
model: "xai/grok-4.5",
});
expect(handle.appliedModel).toBeUndefined();
expect(await runtime.getStatus({ handle })).toMatchObject({
models: { currentModelId: "grok-4.5[effort=high,fast=true]" },
});
});
});
it.each(["gpt-5.5", "missing-model", "vendor/ambiguous"])(
"rejects an ambiguous or unknown model %s",
async (model) => {
await withRuntime(async (runtime) => {
await expect(
runtime.ensureSession({
sessionKey: "agent:main:acp:catalog-missing",
agent: "catalog",
mode: "persistent",
model,
modelExplicit: true,
}),
).rejects.toMatchObject({ code: "ACP_MODEL_UNSUPPORTED" });
});
},
);
it("rejects a model that becomes ambiguous on reconnect and preserves the conversation", async () => {
await withRuntime(async (runtime, _spawned, restart) => {
const input = {
sessionKey: "agent:main:acp:catalog-reconnect",
agent: "catalog",
mode: "persistent" as const,
};
const original = await runtime.ensureSession(input);
expect(await prompt(runtime, original, "before")).toMatchObject({ history: ["before"] });
await runtime.shutdown();
const resumed = restart();
const handle = await resumed.ensureSession(input);
expect(handle.backendSessionId).toBe(original.backendSessionId);
// session/new has one variant; session/load advertises a second on the same conversation.
await expect(
resumed.setConfigOption({ handle, key: "model", value: "composer-2.5" }),
).rejects.toMatchObject({ code: "ACP_MODEL_UNSUPPORTED" });
await resumed.setConfigOption({ handle, key: "model", value: "composer-2.5[fast=false]" });
expect(await prompt(resumed, handle, "after")).toEqual({
model: "composer-2.5[fast=false]",
history: ["before", "after"],
});
});
});
it("releases rejected startup attempts when an advertised model cannot be selected", async () => {
await withRuntime(async (runtime, spawned) => {
await expect(
runtime.ensureSession({
sessionKey: "agent:main:acp:catalog-locked",
agent: "catalog",
mode: "persistent",
model: "provider/locked-1",
modelExplicit: true,
}),
).rejects.toThrow(/not available on this plan/);
// The original reference and stripped retry both failed before session publication.
expect(spawned).toHaveLength(2);
await expect.poll(() => spawned.filter(({ pid }) => isAlive(pid))).toEqual([]);
});
});
it("keeps rejecting a failed selection on a same-key retry or after restart", async () => {
await withRuntime(async (runtime, _spawned, restart) => {
const input = {
sessionKey: "agent:main:acp:catalog-retry",
agent: "catalog",
mode: "persistent" as const,
model: "provider/locked-1",
modelExplicit: true,
};
await expect(runtime.ensureSession(input)).rejects.toThrow(/not available on this plan/);
// A published incomplete record would make these succeed without the requested model.
await expect(runtime.ensureSession(input)).rejects.toThrow(/not available on this plan/);
await runtime.shutdown();
await expect(restart().ensureSession(input)).rejects.toThrow(/not available on this plan/);
});
});
it("preserves an advertised slash id before considering a provider-stripped reference", async () => {
await withRuntime(async (runtime) => {
const handle = await runtime.ensureSession({
sessionKey: "agent:main:acp:catalog-native-id",
agent: "catalog",
mode: "persistent",
model: "vendor/native-model",
modelExplicit: true,
});
expect(await prompt(runtime, handle, "startup")).toMatchObject({
model: "vendor/native-model",
});
await runtime.setConfigOption({ handle, key: "model", value: "native-model" });
await runtime.setConfigOption({ handle, key: "model", value: "vendor/native-model" });
expect(await prompt(runtime, handle, "control")).toMatchObject({
model: "vendor/native-model",
});
});
});
it("does not interpret another harness's opaque ids as Cursor aliases", async () => {
await withRuntime(async (runtime) => {
await expect(
runtime.ensureSession({
sessionKey: "agent:main:acp:generic-alias",
agent: "catalog",
mode: "persistent",
model: "composer-2.5",
modelExplicit: true,
}),
).rejects.toMatchObject({ reason: "unadvertised-model" });
const handle = await runtime.ensureSession({
sessionKey: "agent:main:acp:generic-exact",
agent: "catalog",
mode: "persistent",
model: "composer-2.5[fast=true]",
modelExplicit: true,
});
await expect(
runtime.setConfigOption({ handle, key: "model", value: "grok-4.5" }),
).rejects.toMatchObject({ reason: "unadvertised-model" });
expect(await prompt(runtime, handle, "exact")).toMatchObject({
model: "composer-2.5[fast=true]",
});
}, false);
});

View file

@ -1,4 +1,4 @@
import { AcpxRuntime as BaseAcpxRuntime } from "acpx/runtime";
import { AcpxRuntime as BaseAcpxRuntime, RequestedModelUnsupportedError } from "acpx/runtime";
import type { AcpSessionStore } from "acpx/runtime";
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
@ -78,6 +78,52 @@ describe("AcpxRuntime reset generation custody", () => {
expect(persisted).toMatchObject({ acpSessionId: "fresh-session" });
});
it.each(["startup", "control"])(
"does not retry a model reference after reset during %s rejection",
async (operation) => {
const sessionKey = "agent:catalog:acp:model-reset";
const { runtime, delegate } = makeRuntime({
load: vi.fn(async () => undefined),
save: vi.fn(async () => {}),
});
const started = createDeferred<void>();
const release = createDeferred<void>();
const rejectModel = async (): Promise<never> => {
started.resolve();
await release.promise;
throw new RequestedModelUnsupportedError("Model is not advertised", "unadvertised-model");
};
const ensure = vi.spyOn(delegate, "ensureSession").mockImplementation(rejectModel);
const control = vi.spyOn(delegate, "setConfigOption").mockImplementation(rejectModel);
const pending =
operation === "startup"
? runtime.ensureSession({
sessionKey,
agent: "catalog",
mode: "persistent",
model: "provider/model",
modelExplicit: true,
})
: runtime.setConfigOption({
handle: { sessionKey, backend: "acpx", runtimeSessionName: sessionKey },
key: "model",
value: "provider/model",
});
const rejected = expect(pending).rejects.toThrow("superseded by reset");
try {
await started.promise;
await runtime.prepareFreshSession({ sessionKey });
release.resolve();
await rejected;
expect(operation === "startup" ? ensure : control).toHaveBeenCalledOnce();
} finally {
release.resolve();
await Promise.allSettled([pending]);
await runtime.shutdown();
}
},
);
it("keeps a fresh generation owned when an older discard close finishes late", async () => {
const sessionKey = "agent:codex:acp:binding:test";
const oldRecord: Record<string, unknown> = {

View file

@ -1107,7 +1107,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
},
);
it("does not retry when ACPX rejects an explicitly unsupported model id", async () => {
it("keeps rejecting an unsupported model after retrying its OpenClaw reference", async () => {
const baseStore: TestSessionStore = makeEmptySessionStore();
const { runtime, delegate } = makeRuntime(baseStore, {
agentRegistry: {
@ -1117,7 +1117,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
});
const ensure = vi
.spyOn(delegate, "ensureSession")
.mockRejectedValueOnce(
.mockRejectedValue(
new RequestedModelUnsupportedError(
"Cannot apply --model: the ACP agent did not advertise that model",
"unadvertised-model",
@ -1132,7 +1132,8 @@ describe("AcpxRuntime fresh reset wrapper", () => {
model: "unknown/model",
}),
).rejects.toThrow("did not advertise that model");
expect(ensure).toHaveBeenCalledTimes(1);
// Both attempts carry a model; failed startup never publishes a model-less session.
expect(ensure.mock.calls.map(([input]) => input.model)).toEqual(["unknown/model", "model"]);
});
it("does not retry an unrelated error with similar wording", async () => {

View file

@ -10,7 +10,6 @@ import { isDeepStrictEqual } from "node:util";
import {
AcpxRuntime as BaseAcpxRuntime,
decodeAcpxRuntimeHandleState,
isRequestedModelUnsupportedError,
type AcpAgentRegistry,
type AcpRuntimeDoctorReport,
type AcpRuntimeEvent,
@ -19,7 +18,6 @@ import {
type AcpProcessStarted,
type AcpRuntimeStatus,
type AcpRuntimeTurnResult,
type SessionAgentOptions,
} from "acpx/runtime";
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
import { redactSensitiveText } from "openclaw/plugin-sdk/security-runtime";
@ -40,6 +38,11 @@ import {
splitCommandParts,
type AcpxAgentCommand,
} from "./command-line.js";
import {
ensureSessionWithModelRef,
withAcpxSessionOptions,
withOpenClawModelRef,
} from "./model-ref.js";
import {
ACPX_PROBE_LEASE_SESSION_KEY,
hashAcpxProcessCommand,
@ -99,7 +102,6 @@ type OpenClawRuntimeEnsureInput = Parameters<AcpRuntime["ensureSession"]>[0] & {
type OpenClawRuntimeHandle = Awaited<ReturnType<AcpRuntime["ensureSession"]>> & {
bridgeSession?: BridgeSession | null;
};
type AcpxDelegateEnsureInput = Parameters<BaseAcpxRuntime["ensureSession"]>[0];
type AcpxMcpServers = Extract<NonNullable<AcpRuntimeOptions["mcpServers"]>, unknown[]>;
type AcpxMcpServer = AcpxMcpServers[number];
@ -316,40 +318,6 @@ function normalizeClaudeAcpModelOverride(rawModel: string | undefined): string |
return raw.slice(prefix[0].length).trim() || undefined;
}
function withAcpxSessionOptions(input: OpenClawRuntimeEnsureInput): AcpxDelegateEnsureInput {
const existingOptions = (input as { sessionOptions?: SessionAgentOptions }).sessionOptions;
const model = input.model?.trim() || existingOptions?.model;
const sessionOptions = model ? { ...existingOptions, model } : existingOptions;
const { modelExplicit: _modelExplicit, thinkingExplicit: _thinkingExplicit, ...rest } = input;
return {
...rest,
...(sessionOptions ? { sessionOptions } : {}),
} as AcpxDelegateEnsureInput;
}
function isAcpModelCapabilityMissingError(error: unknown): boolean {
return isRequestedModelUnsupportedError(error) && error.reason === "missing-capability";
}
// Only inherited defaults may be dropped when a harness has no model control;
// explicit selections and invalid model ids must remain visible failures.
async function ensureDelegateSessionWithModelFallback(
delegate: BaseAcpxRuntime,
input: OpenClawRuntimeEnsureInput,
): Promise<OpenClawRuntimeHandle> {
try {
return await delegate.ensureSession(withAcpxSessionOptions(input));
} catch (error) {
if (input.modelExplicit || !input.model || !isAcpModelCapabilityMissingError(error)) {
throw error;
}
return {
...(await delegate.ensureSession(withAcpxSessionOptions({ ...input, model: undefined }))),
appliedModel: { kind: "dropped" },
};
}
}
function appendCodexAcpConfigOverrides(
command: AcpxAgentCommand,
override: CodexAcpModelOverride,
@ -1037,7 +1005,10 @@ export class AcpxRuntime implements CompleteAcpRuntime {
run: () =>
codexModelOverride
? delegate.ensureSession(withAcpxSessionOptions(ensureInput))
: ensureDelegateSessionWithModelFallback(delegate, ensureInput),
: ensureSessionWithModelRef((request) => {
this.generationRegistry.assertCurrentGeneration(generation);
return delegate.ensureSession(request);
}, ensureInput),
}),
});
return {
@ -1262,6 +1233,12 @@ export class AcpxRuntime implements CompleteAcpRuntime {
value: normalizeClaudeAcpModelOverride(input.value) ?? input.value,
});
}
if (key === "model") {
return await withOpenClawModelRef(input.value, (value) => {
this.generationRegistry.assertCurrentGeneration(snapshot.generation);
return delegate.setConfigOption({ ...input, value });
});
}
return await delegate.setConfigOption(input);
}

View file

@ -0,0 +1,20 @@
{
"currentModelId": "grok-4.6[effort=high,fast=true]",
"availableModelIds": [
"default[]",
"grok-4.6[effort=high,fast=true]",
"composer-2.5[fast=true]",
"grok-4.5[effort=high,fast=true]",
"gpt-5.5[context=272k,reasoning=medium,fast=false]",
"gpt-5.5[context=272k,reasoning=high,fast=false]",
"locked-1[tier=pro]",
"vendor/native-model",
"native-model",
"vendor/ambiguous[fast=true]",
"vendor/ambiguous[fast=false]",
"ambiguous[fast=true]"
],
"unselectableModelIds": [
"locked-1[tier=pro]"
]
}

View file

@ -0,0 +1,93 @@
#!/usr/bin/env node
// Synthetic ACP peer shaped like Cursor's `agent acp`: it advertises only `mode` and a
// `category: "model"` select whose values are opaque parameterized IDs, and rejects any
// value it did not advertise with -32602, as Cursor does.
import { randomUUID } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { Readable, Writable } from "node:stream";
import {
AgentSideConnection,
ndJsonStream,
PROTOCOL_VERSION,
RequestError,
} from "@agentclientprotocol/sdk";
const catalog = JSON.parse(
fs.readFileSync(new URL("./cursor-model-catalog.json", import.meta.url), "utf8"),
);
const sessions = new Map();
let availableModelIds = catalog.availableModelIds;
const sessionPath = (sessionId) => path.join(process.cwd(), `catalog-${sessionId}.json`);
const save = (sessionId, state) => fs.writeFileSync(sessionPath(sessionId), JSON.stringify(state));
const configOptions = (state) => [
{
id: "mode",
name: "Mode",
category: "mode",
type: "select",
currentValue: "agent",
options: [{ value: "agent", name: "Agent" }],
},
{
id: "model",
name: "Model",
category: "model",
type: "select",
currentValue: state.model,
options: availableModelIds.map((value) => ({ value, name: value })),
},
];
const connection = new AgentSideConnection(
(client) => ({
async initialize() {
return {
protocolVersion: PROTOCOL_VERSION,
agentCapabilities: { loadSession: true },
authMethods: [],
};
},
async newSession() {
const sessionId = randomUUID();
const state = { model: catalog.currentModelId, history: [] };
sessions.set(sessionId, state);
save(sessionId, state);
return { sessionId, configOptions: configOptions(state) };
},
async loadSession({ sessionId }) {
const state = JSON.parse(fs.readFileSync(sessionPath(sessionId), "utf8"));
sessions.set(sessionId, state);
availableModelIds = [...catalog.availableModelIds, "composer-2.5[fast=false]"];
return { configOptions: configOptions(state) };
},
async setSessionConfigOption({ sessionId, configId, value }) {
const state = sessions.get(sessionId);
if (configId !== "model" || !availableModelIds.includes(value)) {
throw RequestError.invalidParams(undefined, `unsupported ${configId} value`);
}
if (catalog.unselectableModelIds.includes(value)) {
// Advertised but not selectable for this account (for example a plan limit).
throw RequestError.internalError(undefined, `${value} is not available on this plan`);
}
state.model = value;
save(sessionId, state);
return { configOptions: configOptions(state) };
},
async prompt({ sessionId, prompt }) {
const state = sessions.get(sessionId);
state.history.push(...prompt.filter((part) => part.type === "text").map((part) => part.text));
save(sessionId, state);
await client.sessionUpdate({
sessionId,
update: {
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: JSON.stringify(state) },
},
});
return { stopReason: "end_turn" };
},
async cancel() {},
}),
ndJsonStream(Writable.toWeb(process.stdout), Readable.toWeb(process.stdin)),
);
void connection;

View file

@ -2299,7 +2299,7 @@
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
"@vitest/coverage-v8": "5.0.0",
"acpx": "0.17.1",
"acpx": "0.18.0",
"apache-arrow": "21.2.0",
"baileys": "7.0.0-rc14",
"esbuild": "0.28.2",

14
pnpm-lock.yaml generated
View file

@ -495,8 +495,8 @@ importers:
specifier: 5.0.0
version: 5.0.0(@vitest/browser@5.0.0)(vitest@5.0.0)
acpx:
specifier: 0.17.1
version: 0.17.1
specifier: 0.18.0
version: 0.18.0
apache-arrow:
specifier: 21.2.0
version: 21.2.0
@ -635,8 +635,8 @@ importers:
specifier: 1.10.0
version: 1.10.0
acpx:
specifier: 0.17.1
version: 0.17.1
specifier: 0.18.0
version: 0.18.0
smol-toml:
specifier: 1.8.0
version: 1.8.0
@ -6168,8 +6168,8 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
acpx@0.17.1:
resolution: {integrity: sha512-VafoVDYO8xE/Pd/8hTwAR6eHoSJf3K119knfFfAjCVqGy8ilGtWoVRI8z7fm+Q1o+R1vKDo58MJoxDx6YIt1tQ==}
acpx@0.18.0:
resolution: {integrity: sha512-O5L2ldiSijytxsot9IIuViHCeZGNZpexlWKTD6za/flqrswotfl7RJ5Ch0NaU6ceSxFqAnLT/KVPwUF9KGVZlQ==}
engines: {node: '>=22.13.0'}
hasBin: true
@ -13560,7 +13560,7 @@ snapshots:
acorn@8.18.0: {}
acpx@0.17.1:
acpx@0.18.0:
dependencies:
'@agentclientprotocol/sdk': 1.4.0(zod@4.6.4)
'@openclaw/fs-safe': 0.12.0

View file

@ -10,8 +10,8 @@ minimumReleaseAgeStrict: true
# Trusted Codex runtimes are outside the dependency cooldown.
minimumReleaseAgeExclude:
# Reviewed ACPX release; remove after 2026-09-27T04:05:14Z.
- acpx@0.17.1
# Reviewed ACPX release; remove after 2026-09-27T20:56:00Z.
- acpx@0.18.0
# Published ACPX runtime dependencies; remove after 2026-09-22T12:38:43Z.
- "@openclaw/fs-safe@0.12.0"
- "@openclaw/fs-safe-darwin-arm64@0.12.0"