mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:10:43 +00:00
fix: restore verbose command and ACP cleanup controls
This commit is contained in:
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- ACP/commands: accept forwarded ACP timeout config controls in the OpenClaw bridge, treat unsupported discard-close controls as recoverable cleanup, and restore native `/verbose full` plus no-arg status behavior, so Discord command menus and nested ACP turns no longer fail on supported session controls. Thanks @vincentkoc.
|
||||
- Channels/Discord: fail startup closed when Discord cannot resolve the bot's own identity and keep mention gating active when only configured mention patterns can detect mentions, so the provider no longer continues with a missing bot id. Fixes #42219; carries forward #46856 and #49218. Thanks @education-01 and @BenediktSchackenberg.
|
||||
- Channels/Discord: split long CJK replies at punctuation and code-point-safe fallback boundaries so Discord chunking stays readable without corrupting astral characters. Fixes #38597; repairs #71384. Thanks @p3nchan.
|
||||
- Browser/gateway: ignore Playwright dialog-close races from `Page.handleJavaScriptDialog` so browser automation no longer crashes the Gateway when a dialog disappears before Playwright accepts it. (#40067) Thanks @randyjtw.
|
||||
|
||||
@@ -1297,6 +1297,8 @@ export class AcpSessionManager {
|
||||
(acpError.code === "ACP_BACKEND_MISSING" ||
|
||||
acpError.code === "ACP_BACKEND_UNAVAILABLE" ||
|
||||
(input.discardPersistentState && acpError.code === "ACP_SESSION_INIT_FAILED") ||
|
||||
(input.discardPersistentState &&
|
||||
acpError.code === "ACP_BACKEND_UNSUPPORTED_CONTROL") ||
|
||||
this.isRecoverableAcpxExitError(acpError.message))
|
||||
) {
|
||||
if (input.discardPersistentState) {
|
||||
|
||||
@@ -1555,6 +1555,44 @@ describe("AcpSessionManager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("treats unsupported close controls as recoverable during discard cleanup", async () => {
|
||||
const runtimeState = createRuntime();
|
||||
runtimeState.close.mockRejectedValueOnce(
|
||||
new AcpRuntimeError(
|
||||
"ACP_BACKEND_UNSUPPORTED_CONTROL",
|
||||
'ACP backend "acpx" does not support session/close.',
|
||||
),
|
||||
);
|
||||
hoisted.requireAcpRuntimeBackendMock.mockReturnValue({
|
||||
id: "acpx",
|
||||
runtime: runtimeState.runtime,
|
||||
});
|
||||
hoisted.readAcpSessionEntryMock.mockReturnValue({
|
||||
sessionKey: "agent:openclaw:acp:session-1",
|
||||
storeSessionKey: "agent:openclaw:acp:session-1",
|
||||
acp: readySessionMeta({
|
||||
agent: "openclaw",
|
||||
}),
|
||||
});
|
||||
|
||||
const manager = new AcpSessionManager();
|
||||
const closeResult = await manager.closeSession({
|
||||
cfg: baseCfg,
|
||||
sessionKey: "agent:openclaw:acp:session-1",
|
||||
reason: "terminal-task-cleanup",
|
||||
allowBackendUnavailable: true,
|
||||
discardPersistentState: true,
|
||||
clearMeta: true,
|
||||
});
|
||||
|
||||
expect(closeResult.runtimeClosed).toBe(false);
|
||||
expect(closeResult.runtimeNotice).toContain("does not support session/close");
|
||||
expect(closeResult.metaCleared).toBe(true);
|
||||
expect(runtimeState.prepareFreshSession).toHaveBeenCalledWith({
|
||||
sessionKey: "agent:openclaw:acp:session-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("clears persisted resume identity when close discards persistent state", async () => {
|
||||
const runtimeState = createRuntime();
|
||||
const sessionKey = "agent:claude:acp:binding:discord:default:9373ab192b2317f4";
|
||||
|
||||
@@ -742,6 +742,56 @@ describe("acp setSessionConfigOption bridge behavior", () => {
|
||||
sessionStore.clearAllSessionsForTest();
|
||||
});
|
||||
|
||||
it("accepts forwarded timeout config options without failing OpenClaw ACP bridge turns", async () => {
|
||||
const sessionStore = createInMemorySessionStore();
|
||||
const connection = createAcpConnection();
|
||||
const request = vi.fn(async (method: string, params?: unknown) => {
|
||||
if (method === "sessions.list") {
|
||||
return {
|
||||
ts: Date.now(),
|
||||
path: "/tmp/sessions.json",
|
||||
count: 1,
|
||||
defaults: {
|
||||
modelProvider: null,
|
||||
model: null,
|
||||
contextTokens: null,
|
||||
},
|
||||
sessions: [
|
||||
{
|
||||
key: "timeout-session",
|
||||
kind: "direct",
|
||||
updatedAt: Date.now(),
|
||||
thinkingLevel: "minimal",
|
||||
modelProvider: "openai",
|
||||
model: "gpt-5.4",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
expect(method).not.toBe("sessions.patch");
|
||||
return { ok: true };
|
||||
}) as GatewayClient["request"];
|
||||
const agent = new AcpGatewayAgent(connection, createAcpGateway(request), {
|
||||
sessionStore,
|
||||
});
|
||||
|
||||
await agent.loadSession(createLoadSessionRequest("timeout-session"));
|
||||
|
||||
await expect(
|
||||
agent.setSessionConfigOption(
|
||||
createSetSessionConfigOptionRequest("timeout-session", "timeout", "180"),
|
||||
),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
configOptions: expect.any(Array),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(request).not.toHaveBeenCalledWith("sessions.patch", expect.anything());
|
||||
|
||||
sessionStore.clearAllSessionsForTest();
|
||||
});
|
||||
|
||||
it("rejects non-string ACP config option values", async () => {
|
||||
const sessionStore = createInMemorySessionStore();
|
||||
const connection = createAcpConnection();
|
||||
|
||||
@@ -58,6 +58,8 @@ const ACP_TRACE_LEVEL_CONFIG_ID = "trace_level";
|
||||
const ACP_REASONING_LEVEL_CONFIG_ID = "reasoning_level";
|
||||
const ACP_RESPONSE_USAGE_CONFIG_ID = "response_usage";
|
||||
const ACP_ELEVATED_LEVEL_CONFIG_ID = "elevated_level";
|
||||
const ACP_TIMEOUT_CONFIG_ID = "timeout";
|
||||
const ACP_TIMEOUT_SECONDS_CONFIG_ID = "timeout_seconds";
|
||||
const ACP_LOAD_SESSION_REPLAY_LIMIT = 1_000_000;
|
||||
const ACP_GATEWAY_DISCONNECT_GRACE_MS = 5_000;
|
||||
|
||||
@@ -664,10 +666,12 @@ export class AcpGatewayAgent implements Agent {
|
||||
const sessionPatch = this.resolveSessionConfigPatch(params.configId, params.value);
|
||||
|
||||
try {
|
||||
await this.gateway.request("sessions.patch", {
|
||||
key: session.sessionKey,
|
||||
...sessionPatch.patch,
|
||||
});
|
||||
if (sessionPatch.patch) {
|
||||
await this.gateway.request("sessions.patch", {
|
||||
key: session.sessionKey,
|
||||
...sessionPatch.patch,
|
||||
});
|
||||
}
|
||||
this.log(
|
||||
`setSessionConfigOption: ${session.sessionId} -> ${params.configId}=${params.value}`,
|
||||
);
|
||||
@@ -1291,7 +1295,7 @@ export class AcpGatewayAgent implements Agent {
|
||||
value: string | boolean,
|
||||
): {
|
||||
overrides: Partial<GatewaySessionPresentationRow>;
|
||||
patch: Record<string, string | boolean>;
|
||||
patch?: Record<string, string | boolean>;
|
||||
} {
|
||||
if (typeof value !== "string") {
|
||||
throw new Error(
|
||||
@@ -1334,6 +1338,11 @@ export class AcpGatewayAgent implements Agent {
|
||||
patch: { elevatedLevel: value },
|
||||
overrides: { elevatedLevel: value },
|
||||
};
|
||||
case ACP_TIMEOUT_CONFIG_ID:
|
||||
case ACP_TIMEOUT_SECONDS_CONFIG_ID:
|
||||
return {
|
||||
overrides: {},
|
||||
};
|
||||
default:
|
||||
throw new Error(`ACP bridge mode does not support session config option "${configId}".`);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export function buildHelpMessage(cfg?: OpenClawConfig): string {
|
||||
"/think <level>",
|
||||
"/model <id>",
|
||||
"/fast status|on|off",
|
||||
"/verbose on|off",
|
||||
"/verbose on|off|full",
|
||||
"/trace on|off|raw",
|
||||
];
|
||||
if (isCommandFlagEnabled(cfg, "config")) {
|
||||
|
||||
@@ -746,12 +746,11 @@ export function buildBuiltinChatCommands(): ChatCommandDefinition[] {
|
||||
args: [
|
||||
{
|
||||
name: "mode",
|
||||
description: "on or off",
|
||||
description: "on, off, or full",
|
||||
type: "string",
|
||||
choices: ["on", "off"],
|
||||
choices: ["on", "off", "full"],
|
||||
},
|
||||
],
|
||||
argsMenu: "auto",
|
||||
}),
|
||||
defineChatCommand({
|
||||
key: "trace",
|
||||
|
||||
@@ -461,6 +461,15 @@ describe("commands registry args", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps verbose full available while preserving no-arg status dispatch", () => {
|
||||
const verbose = listChatCommands().find((command) => command.key === "verbose");
|
||||
|
||||
expect(verbose?.args?.[0]?.choices).toEqual(["on", "off", "full"]);
|
||||
expect(
|
||||
resolveCommandArgMenu({ command: verbose!, args: undefined, cfg: {} as never }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("does not show menus when arg already provided", () => {
|
||||
const command = createUsageModeCommand();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user