From 784e86433c62dccf0e8a755d780c7748288de960 Mon Sep 17 00:00:00 2001 From: Shakker Date: Tue, 9 Jun 2026 18:57:57 +0100 Subject: [PATCH] fix: preserve ACP metadata key during deleted-agent checks --- src/gateway/server-methods/chat.ts | 6 ++-- src/gateway/server-methods/sessions.ts | 6 ++-- src/gateway/session-utils.ts | 10 +++++- src/gateway/sessions-resolve-store.test.ts | 37 ++++++++++++++++++++++ src/gateway/sessions-resolve.ts | 5 ++- 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index 31fd5f18e943..78f7606494a9 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -3068,7 +3068,7 @@ export const chatHandlers: GatewayRequestHandlers = { }, ); const sessionLoadMs = roundedChatSendTimingMs(performance.now() - sessionLoadStartedAtMs); - const { cfg, entry, canonicalKey: sessionKey } = sessionLoadResult; + const { cfg, entry, canonicalKey: sessionKey, legacyKey } = sessionLoadResult; const selectedAgent = validateChatSelectedAgent({ cfg, requestedSessionKey: rawSessionKey, @@ -3080,7 +3080,9 @@ export const chatHandlers: GatewayRequestHandlers = { } const requestedSessionId = normalizeOptionalText(p.sessionId); const backingSessionId = entry?.sessionId ?? requestedSessionId; - const deletedAgentId = resolveDeletedAgentIdFromSessionKey(cfg, sessionKey, entry); + const deletedAgentId = resolveDeletedAgentIdFromSessionKey(cfg, sessionKey, entry, { + acpMetadataSessionKey: legacyKey ?? sessionKey, + }); if (deletedAgentId !== null) { respond( false, diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index 43532846cf0e..3a32d6d65fa2 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -785,9 +785,11 @@ async function handleSessionSend(params: { } const requestedAgentId = requestedAgent.agentId; const loaded = loadSessionEntry(key, { agentId: requestedAgentId }); - let { entry, canonicalKey, storePath } = loaded; + let { entry, canonicalKey, storePath, legacyKey } = loaded; // Reject sends/steers targeting sessions whose owning agent was deleted (#65524). - const deletedAgentId = resolveDeletedAgentIdFromSessionKey(cfg, canonicalKey, entry); + const deletedAgentId = resolveDeletedAgentIdFromSessionKey(cfg, canonicalKey, entry, { + acpMetadataSessionKey: legacyKey ?? canonicalKey, + }); if (deletedAgentId !== null) { params.respond( false, diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 4dbd5bab5075..e1008a9219b9 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -923,6 +923,7 @@ export function resolveDeletedAgentIdFromSessionKey( cfg: OpenClawConfig, sessionKey: string, entry?: SessionEntry | null, + options?: { acpMetadataSessionKey?: string | null }, ): string | null { const parsed = parseAgentSessionKey(sessionKey); if (!parsed) { @@ -932,8 +933,15 @@ export function resolveDeletedAgentIdFromSessionKey( // Free ACP runtime keys use agent::acp:, but key shape is // not proof: ACP bridge sessions can use ACP-shaped keys without SessionAcpMeta. // Configured acp:binding keys stay owner-scoped even when ACP metadata exists. + const acpMetadataSessionKey = normalizeOptionalString(options?.acpMetadataSessionKey); const acpMeta = - entry?.acp ?? readAcpSessionMetaForEntry({ sessionKey, entry: entry ?? undefined }); + entry?.acp ?? + (acpMetadataSessionKey + ? readAcpSessionMetaForEntry({ + sessionKey: acpMetadataSessionKey, + entry: entry ?? undefined, + }) + : readAcpSessionMeta({ sessionKey, cfg })); if (acpMeta) { return null; } diff --git a/src/gateway/sessions-resolve-store.test.ts b/src/gateway/sessions-resolve-store.test.ts index ca8f878e97b8..325e0728ef04 100644 --- a/src/gateway/sessions-resolve-store.test.ts +++ b/src/gateway/sessions-resolve-store.test.ts @@ -261,6 +261,43 @@ describe("resolveSessionKeyFromResolveParams store canonicalization", () => { }); }); + it("resolves migrated ACP harness keys when metadata remains under the matched store key", async () => { + await withStateDirEnv("openclaw-sessions-resolve-acp-harness-legacy-", async () => { + const cfg: OpenClawConfig = { + agents: { list: [{ id: "main", default: true }] }, + }; + const acpKey = "agent:claude:acp:33333333-3333-4333-8333-333333333333"; + const legacyAcpKey = "agent:CLAUDE:acp:33333333-3333-4333-8333-333333333333"; + const claudeStorePath = resolveStorePath(cfg.session?.store, { agentId: "claude" }); + await saveSessionStore(claudeStorePath, { + [legacyAcpKey]: { + sessionId: "sess-acp-harness-legacy", + label: "claude-delegate-legacy", + updatedAt: freshUpdatedAt(), + }, + }); + writeAcpSessionMetaForMigration({ + sessionKey: legacyAcpKey, + sessionId: "sess-acp-harness-legacy", + meta: { + backend: "acpx", + agent: "claude", + runtimeSessionName: legacyAcpKey, + mode: "oneshot", + state: "idle", + lastActivityAt: freshUpdatedAt(), + }, + }); + + await expect( + resolveSessionKeyFromResolveParams({ + cfg, + p: { key: acpKey }, + }), + ).resolves.toEqual({ ok: true, key: acpKey }); + }); + }); + it("rejects ACP-shaped bridge sessions without ACP runtime metadata under deleted agents", async () => { await withStateDirEnv("openclaw-sessions-resolve-acp-bridge-deleted-", async () => { const cfg: OpenClawConfig = { diff --git a/src/gateway/sessions-resolve.ts b/src/gateway/sessions-resolve.ts index 14a51c1eaa3c..cea38b55a75a 100644 --- a/src/gateway/sessions-resolve.ts +++ b/src/gateway/sessions-resolve.ts @@ -43,8 +43,9 @@ function validateSessionAgentExists( cfg: OpenClawConfig, key: string, entry?: SessionEntry | null, + options?: { acpMetadataSessionKey?: string | null }, ): SessionsResolveResult | null { - const deletedAgentId = resolveDeletedAgentIdFromSessionKey(cfg, key, entry); + const deletedAgentId = resolveDeletedAgentIdFromSessionKey(cfg, key, entry, options); if (deletedAgentId === null) { return null; } @@ -142,6 +143,7 @@ export async function resolveSessionKeyFromResolveParams(params: { cfg, target.canonicalKey, store[target.canonicalKey], + { acpMetadataSessionKey: target.canonicalKey }, ); if (agentCheck) { return agentCheck; @@ -174,6 +176,7 @@ export async function resolveSessionKeyFromResolveParams(params: { cfg, target.canonicalKey, migratedStore[target.canonicalKey], + { acpMetadataSessionKey: legacyKey }, ); if (agentCheckLegacy) { return agentCheckLegacy;