fix(acp): separate bridge session namespace

This commit is contained in:
Vincent Koc
2026-06-16 17:44:21 +08:00
parent a6dd20ae9d
commit f26140a4b4
5 changed files with 34 additions and 5 deletions
+6 -4
View File
@@ -63,7 +63,7 @@ Quick rule:
fallback and do not reconstruct historic tool calls or system notices.
- If multiple ACP clients share the same Gateway session key, event and cancel
routing are best-effort rather than strictly isolated per client. Prefer the
default isolated `acp:<uuid>` sessions when you need clean editor-local
default isolated `acp-bridge:<uuid>` sessions when you need clean editor-local
turns.
- Gateway stop states are translated into ACP stop reasons, but that mapping is
less expressive than a fully ACP-native runtime.
@@ -206,7 +206,7 @@ openclaw acp --session agent:qa:bug-123
```
Each ACP session maps to a single Gateway session key. One agent can have many
sessions; ACP defaults to an isolated `acp:<uuid>` session unless you override
sessions; ACP defaults to an isolated `acp-bridge:<uuid>` session unless you override
the key or label.
Per-session `mcpServers` are not supported in bridge mode. If an ACP client
@@ -309,8 +309,10 @@ In Zed, open the Agent panel and select "OpenClaw ACP" to start a thread.
## Session mapping
By default, ACP sessions get an isolated Gateway session key with an `acp:` prefix.
To reuse a known session, pass a session key or label:
By default, ACP bridge sessions get an isolated Gateway session key with an
`acp-bridge:` prefix. These normal-model bridge sessions are synthetic and
subject to stale-entry pruning and entry-count caps. To reuse a known session,
pass a session key or label:
- `--session <key>`: use a specific Gateway session key.
- `--session-label <label>`: resolve an existing session by label.
+16
View File
@@ -2,6 +2,7 @@
import { createInMemorySessionStore } from "@openclaw/acp-core/session";
import { describe, expect, it, vi } from "vitest";
import type { GatewayClient } from "../gateway/client.js";
import { isAcpSessionKey } from "../sessions/session-key-utils.js";
import {
createNewSessionRequest,
createLoadSessionRequest,
@@ -59,6 +60,21 @@ describe("acp unsupported bridge session setup", () => {
});
describe("acp session UX bridge behavior", () => {
it("uses a non-runtime namespace for generated bridge sessions", async () => {
const sessionStore = createInMemorySessionStore();
const agent = new AcpGatewayAgent(createAcpConnection(), createAcpGateway(), {
sessionStore,
});
const result = await agent.newSession(createNewSessionRequest());
const sessionKey = sessionStore.getSession(result.sessionId)?.sessionKey;
expect(sessionKey).toMatch(/^acp-bridge:/);
expect(isAcpSessionKey(sessionKey)).toBe(false);
sessionStore.clearAllSessionsForTest();
});
it("returns initial modes and thought-level config options for new sessions", async () => {
const sessionStore = createInMemorySessionStore();
const agent = new AcpGatewayAgent(createAcpConnection(), createAcpGateway(), {
+1 -1
View File
@@ -374,7 +374,7 @@ export class AcpGatewayAgent implements Agent {
const meta = parseSessionMeta(params["_meta"]);
const sessionKey = await this.resolveSessionKeyFromMeta({
meta,
fallbackKey: `acp:${sessionId}`,
fallbackKey: `acp-bridge:${sessionId}`,
});
const session = this.sessionStore.createSession({
+2
View File
@@ -308,10 +308,12 @@ function getEntryUpdatedAt(entry?: SessionEntry): number {
function isSyntheticSessionMaintenanceKey(sessionKey: string): boolean {
const parsed = parseAgentSessionKey(sessionKey);
const rest = normalizeLowercaseStringOrEmpty(parsed?.rest ?? sessionKey);
// ACP bridge sessions use normal model dispatch, but remain synthetic and disposable.
return (
isSubagentSessionKey(sessionKey) ||
isAcpSessionKey(sessionKey) ||
isCronSessionKey(sessionKey) ||
rest.startsWith("acp-bridge:") ||
rest.startsWith("hook:") ||
rest.startsWith("node:") ||
rest === "heartbeat" ||
@@ -266,6 +266,15 @@ describe("capEntryCount", () => {
});
describe("isProtectedSessionMaintenanceEntry", () => {
it("treats generated ACP bridge sessions as disposable", () => {
expect(
isProtectedSessionMaintenanceEntry("agent:main:acp-bridge:session-1", {
...makeEntry(Date.now()),
chatType: "group",
}),
).toBe(false);
});
it("does not protect synthetic sessions just because they carry group metadata", () => {
expect(
isProtectedSessionMaintenanceEntry("agent:main:subagent:worker", {