clawdbot-d02.1.9.1.1: route gateway session entry reads through seam

This commit is contained in:
Josh Lehman
2026-05-31 18:48:29 -07:00
parent 49e6db1201
commit 1c31222d71
3 changed files with 58 additions and 45 deletions

View File

@@ -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;

View File

@@ -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<typeof import("../config/sessions.js")>("../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<string, SessionEntry>;
const expectResolveToCanonicalKey = async (
p: Parameters<typeof resolveSessionKeyFromResolveParams>[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<string, SessionEntry>) => void) => {
const store = hoisted.loadSessionStoreMock.mock.results[0]?.value as
| Record<string, SessionEntry>
| 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<string, SessionEntry>;
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"]);

View File

@@ -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<typeof loadSessionStore>;
store: Record<string, SessionEntry>;
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) {