diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index 2bb78aff421f..c6209c194db2 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -47,7 +47,6 @@ import { compactEmbeddedAgentSession } from "../../agents/embedded-agent.js"; import { clearSessionQueues } from "../../auto-reply/reply/queue/cleanup.js"; import { normalizeReasoningLevel, normalizeThinkLevel } from "../../auto-reply/thinking.js"; import { - loadSessionStore, runSessionsCleanup, serializeSessionCleanupResult, resolveMainSessionKey, @@ -259,6 +258,22 @@ function resolveGatewaySessionTargetFromKey( return { cfg, target, storePath: target.storePath }; } +function loadSessionEntriesForTarget(params: { + key: string; + cfg: OpenClawConfig; + agentId?: string; +}) { + const target = resolveGatewaySessionStoreTargetWithStore({ + cfg: params.cfg, + key: params.key, + clone: false, + ...(params.agentId ? { agentId: params.agentId } : {}), + }); + const store = target.store; + const entry = resolveFreshestSessionEntryFromStoreKeys(store, target.storeKeys); + return { target, storePath: target.storePath, store, entry }; +} + function resolveOptionalInitialSessionMessage(params: { task?: unknown; message?: unknown; @@ -1307,9 +1322,7 @@ export const sessionsHandlers: GatewayRequestHandlers = { return; } const cfg = context.getRuntimeConfig(); - const { target, storePath } = resolveGatewaySessionTargetFromKey(key, cfg); - const store = loadSessionStore(storePath); - const entry = resolveFreshestSessionEntryFromStoreKeys(store, target.storeKeys); + const { target, storePath, store, entry } = loadSessionEntriesForTarget({ key, cfg }); if (!entry) { respond(true, { session: null }, undefined); return; @@ -2512,11 +2525,11 @@ export const sessionsHandlers: GatewayRequestHandlers = { respond(false, undefined, requestedAgent.error); return; } - const { target, storePath } = resolveGatewaySessionTargetFromKey(key, cfg, { + const { storePath, entry } = loadSessionEntriesForTarget({ + key, + cfg, agentId: requestedAgent.agentId, }); - const store = loadSessionStore(storePath); - const entry = resolveFreshestSessionEntryFromStoreKeys(store, target.storeKeys); if (!entry?.sessionId) { respond(true, { messages: [] }, undefined); return; diff --git a/src/gateway/sessions-resolve.test.ts b/src/gateway/sessions-resolve.test.ts index 6aa4e48bdc5c..ab3941746bc2 100644 --- a/src/gateway/sessions-resolve.test.ts +++ b/src/gateway/sessions-resolve.test.ts @@ -3,11 +3,10 @@ import { ErrorCodes } from "../../packages/gateway-protocol/src/index.js"; import type { SessionEntry } from "../config/sessions/types.js"; const hoisted = vi.hoisted(() => ({ - loadSessionStoreMock: vi.fn(), updateSessionStoreMock: vi.fn(), listSessionsFromStoreMock: vi.fn(), migrateAndPruneGatewaySessionStoreKeyMock: vi.fn(), - resolveGatewaySessionStoreTargetMock: vi.fn(), + resolveGatewaySessionStoreTargetWithStoreMock: vi.fn(), loadCombinedSessionStoreForGatewayMock: vi.fn(), listAgentIdsMock: vi.fn(), })); @@ -27,7 +26,6 @@ vi.mock("../config/sessions.js", async () => { await vi.importActual("../config/sessions.js"); return { ...actual, - loadSessionStore: hoisted.loadSessionStoreMock, updateSessionStore: hoisted.updateSessionStoreMock, }; }); @@ -38,7 +36,8 @@ vi.mock("./session-utils.js", async () => { ...actual, listSessionsFromStore: hoisted.listSessionsFromStoreMock, migrateAndPruneGatewaySessionStoreKey: hoisted.migrateAndPruneGatewaySessionStoreKeyMock, - resolveGatewaySessionStoreTarget: hoisted.resolveGatewaySessionStoreTargetMock, + resolveGatewaySessionStoreTargetWithStore: + hoisted.resolveGatewaySessionStoreTargetWithStoreMock, loadCombinedSessionStoreForGateway: hoisted.loadCombinedSessionStoreForGatewayMock, }; }); @@ -49,6 +48,7 @@ describe("resolveSessionKeyFromResolveParams", () => { const canonicalKey = "agent:main:canon"; const legacyKey = "agent:main:legacy"; const storePath = "/tmp/sessions.json"; + let targetStore: Record; const expectResolveToCanonicalKey = async ( p: Parameters[0]["p"], @@ -66,37 +66,33 @@ describe("resolveSessionKeyFromResolveParams", () => { }; beforeEach(() => { - hoisted.loadSessionStoreMock.mockReset(); hoisted.updateSessionStoreMock.mockReset(); hoisted.listSessionsFromStoreMock.mockReset(); hoisted.migrateAndPruneGatewaySessionStoreKeyMock.mockReset(); - hoisted.resolveGatewaySessionStoreTargetMock.mockReset(); + hoisted.resolveGatewaySessionStoreTargetWithStoreMock.mockReset(); hoisted.loadCombinedSessionStoreForGatewayMock.mockReset(); hoisted.listAgentIdsMock.mockReset(); + targetStore = {}; // Default: all agents are known (main is always present). hoisted.listAgentIdsMock.mockReturnValue(["main"]); - hoisted.resolveGatewaySessionStoreTargetMock.mockReturnValue({ + hoisted.resolveGatewaySessionStoreTargetWithStoreMock.mockImplementation(() => ({ canonicalKey, storeKeys: [canonicalKey, legacyKey], storePath, - }); + store: targetStore, + })); hoisted.migrateAndPruneGatewaySessionStoreKeyMock.mockReturnValue({ primaryKey: canonicalKey }); hoisted.updateSessionStoreMock.mockImplementation( async (_path: string, updater: (store: Record) => void) => { - const store = hoisted.loadSessionStoreMock.mock.results[0]?.value as - | Record - | undefined; - if (store) { - updater(store); - } + updater(targetStore); }, ); }); it("hides canonical keys that fail the spawnedBy visibility filter", async () => { - hoisted.loadSessionStoreMock.mockReturnValue({ + targetStore = { [canonicalKey]: { sessionId: "sess-1", updatedAt: 1 }, - }); + }; hoisted.listSessionsFromStoreMock.mockReturnValue({ sessions: [] }); await expect( @@ -129,7 +125,7 @@ describe("resolveSessionKeyFromResolveParams", () => { updatedAt: now - i, }; } - hoisted.loadSessionStoreMock.mockReturnValue(store); + targetStore = store; await expectResolveToCanonicalKey({ key: canonicalKey, spawnedBy: "controller-1" }); }); @@ -138,7 +134,7 @@ describe("resolveSessionKeyFromResolveParams", () => { const store = { [legacyKey]: { sessionId: "sess-legacy", spawnedBy: "controller-1", updatedAt: Date.now() }, } satisfies Record; - hoisted.loadSessionStoreMock.mockImplementation(() => store); + targetStore = store; await expectResolveToCanonicalKey({ key: canonicalKey, spawnedBy: "controller-1" }); @@ -150,13 +146,14 @@ describe("resolveSessionKeyFromResolveParams", () => { it("rejects sessions belonging to a deleted agent (key-based lookup)", async () => { const deletedAgentKey = "agent:deleted-agent:main"; - hoisted.resolveGatewaySessionStoreTargetMock.mockReturnValue({ + targetStore = { + [deletedAgentKey]: { sessionId: "sess-orphan", updatedAt: 1 }, + }; + hoisted.resolveGatewaySessionStoreTargetWithStoreMock.mockReturnValue({ canonicalKey: deletedAgentKey, storeKeys: [deletedAgentKey], storePath, - }); - hoisted.loadSessionStoreMock.mockReturnValue({ - [deletedAgentKey]: { sessionId: "sess-orphan", updatedAt: 1 }, + store: targetStore, }); // "deleted-agent" is not in the known agents list. hoisted.listAgentIdsMock.mockReturnValue(["main"]); @@ -177,13 +174,14 @@ describe("resolveSessionKeyFromResolveParams", () => { it("rejects non-alias agent:main sessions when main is no longer configured", async () => { const staleMainKey = "agent:main:guildchat:direct:u1"; - hoisted.resolveGatewaySessionStoreTargetMock.mockReturnValue({ + targetStore = { + [staleMainKey]: { sessionId: "sess-stale-main", updatedAt: 1 }, + }; + hoisted.resolveGatewaySessionStoreTargetWithStoreMock.mockReturnValue({ canonicalKey: staleMainKey, storeKeys: [staleMainKey], storePath, - }); - hoisted.loadSessionStoreMock.mockReturnValue({ - [staleMainKey]: { sessionId: "sess-stale-main", updatedAt: 1 }, + store: targetStore, }); hoisted.listAgentIdsMock.mockReturnValue(["ops"]); diff --git a/src/gateway/sessions-resolve.ts b/src/gateway/sessions-resolve.ts index 6644cb9c3660..c867ed23b29e 100644 --- a/src/gateway/sessions-resolve.ts +++ b/src/gateway/sessions-resolve.ts @@ -5,7 +5,7 @@ import { errorShape, type SessionsResolveParams, } from "../../packages/gateway-protocol/src/index.js"; -import { loadSessionStore, updateSessionStore, type SessionEntry } from "../config/sessions.js"; +import { updateSessionStore, type SessionEntry } from "../config/sessions.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveSessionIdMatchSelection } from "../sessions/session-id-resolution.js"; import { parseSessionLabel } from "../sessions/session-label.js"; @@ -15,7 +15,7 @@ import { loadCombinedSessionStoreForGateway, migrateAndPruneGatewaySessionStoreKey, resolveDeletedAgentIdFromSessionKey, - resolveGatewaySessionStoreTarget, + resolveGatewaySessionStoreTargetWithStore, } from "./session-utils.js"; export type SessionsResolveResult = { ok: true; key: string } | { ok: false; error: ErrorShape }; @@ -57,8 +57,7 @@ function validateSessionAgentExists( function isResolvedSessionKeyVisible(params: { cfg: OpenClawConfig; p: SessionsResolveParams; - storePath: string; - store: ReturnType; + store: Record; key: string; }) { if (typeof params.p.spawnedBy !== "string" || params.p.spawnedBy.trim().length === 0) { @@ -119,14 +118,13 @@ export async function resolveSessionKeyFromResolveParams(params: { } if (hasKey) { - const target = resolveGatewaySessionStoreTarget({ cfg, key }); - const store = loadSessionStore(target.storePath); + const target = resolveGatewaySessionStoreTargetWithStore({ cfg, key, clone: false }); + const store = target.store; if (store[target.canonicalKey]) { if ( !isResolvedSessionKeyVisible({ cfg, p, - storePath: target.storePath, store, key: target.canonicalKey, }) @@ -149,22 +147,26 @@ export async function resolveSessionKeyFromResolveParams(params: { s[primaryKey] = s[legacyKey]; } }); + const refreshedTarget = resolveGatewaySessionStoreTargetWithStore({ + cfg, + key: target.canonicalKey, + clone: false, + }); if ( !isResolvedSessionKeyVisible({ cfg, p, - storePath: target.storePath, - store: loadSessionStore(target.storePath), - key: target.canonicalKey, + store: refreshedTarget.store, + key: refreshedTarget.canonicalKey, }) ) { return noSessionFoundResult(key); } - const agentCheckLegacy = validateSessionAgentExists(cfg, target.canonicalKey); + const agentCheckLegacy = validateSessionAgentExists(cfg, refreshedTarget.canonicalKey); if (agentCheckLegacy) { return agentCheckLegacy; } - return { ok: true, key: target.canonicalKey }; + return { ok: true, key: refreshedTarget.canonicalKey }; } if (hasSessionId) {