Gateway: harden custom session-store discovery (#44176)
Merged via squash. Prepared head SHA: 52ebbf5188b47386f2a78ac4715993bc082e911b Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Reviewed-by: @gumadeiras
This commit is contained in:
committed by
GitHub
parent
dc3bb1890b
commit
46f0bfc55b
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from "vitest";
|
||||
const loadSessionStoreMock = vi.fn();
|
||||
const updateSessionStoreMock = vi.fn();
|
||||
const callGatewayMock = vi.fn();
|
||||
const loadCombinedSessionStoreForGatewayMock = vi.fn();
|
||||
|
||||
const createMockConfig = () => ({
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
@@ -42,6 +43,15 @@ vi.mock("../gateway/call.js", () => ({
|
||||
callGateway: (opts: unknown) => callGatewayMock(opts),
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/session-utils.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../gateway/session-utils.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadCombinedSessionStoreForGateway: (cfg: unknown) =>
|
||||
loadCombinedSessionStoreForGatewayMock(cfg),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||
return {
|
||||
@@ -95,7 +105,12 @@ function resetSessionStore(store: Record<string, unknown>) {
|
||||
loadSessionStoreMock.mockClear();
|
||||
updateSessionStoreMock.mockClear();
|
||||
callGatewayMock.mockClear();
|
||||
loadCombinedSessionStoreForGatewayMock.mockClear();
|
||||
loadSessionStoreMock.mockReturnValue(store);
|
||||
loadCombinedSessionStoreForGatewayMock.mockReturnValue({
|
||||
storePath: "(multiple)",
|
||||
store,
|
||||
});
|
||||
callGatewayMock.mockResolvedValue({});
|
||||
mockConfig = createMockConfig();
|
||||
}
|
||||
@@ -161,6 +176,30 @@ describe("session_status tool", () => {
|
||||
expect(details.sessionKey).toBe("agent:main:main");
|
||||
});
|
||||
|
||||
it("resolves duplicate sessionId inputs deterministically", async () => {
|
||||
resetSessionStore({
|
||||
"agent:main:main": {
|
||||
sessionId: "current",
|
||||
updatedAt: 10,
|
||||
},
|
||||
"agent:main:other": {
|
||||
sessionId: "run-dup",
|
||||
updatedAt: 999,
|
||||
},
|
||||
"agent:main:acp:run-dup": {
|
||||
sessionId: "run-dup",
|
||||
updatedAt: 100,
|
||||
},
|
||||
});
|
||||
|
||||
const tool = getSessionStatusTool();
|
||||
|
||||
const result = await tool.execute("call-dup", { sessionKey: "run-dup" });
|
||||
const details = result.details as { ok?: boolean; sessionKey?: string };
|
||||
expect(details.ok).toBe(true);
|
||||
expect(details.sessionKey).toBe("agent:main:acp:run-dup");
|
||||
});
|
||||
|
||||
it("uses non-standard session keys without sessionId resolution", async () => {
|
||||
resetSessionStore({
|
||||
"temp:slug-generator": {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import type { Dirent } from "node:fs";
|
||||
import fsSync, { type Dirent } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
export async function resolveAgentSessionDirs(stateDir: string): Promise<string[]> {
|
||||
const agentsDir = path.join(stateDir, "agents");
|
||||
function mapAgentSessionDirs(agentsDir: string, entries: Dirent[]): string[] {
|
||||
return entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => path.join(agentsDir, entry.name, "sessions"))
|
||||
.toSorted((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
export async function resolveAgentSessionDirsFromAgentsDir(agentsDir: string): Promise<string[]> {
|
||||
let entries: Dirent[] = [];
|
||||
try {
|
||||
entries = await fs.readdir(agentsDir, { withFileTypes: true });
|
||||
@@ -15,8 +21,24 @@ export async function resolveAgentSessionDirs(stateDir: string): Promise<string[
|
||||
throw err;
|
||||
}
|
||||
|
||||
return entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => path.join(agentsDir, entry.name, "sessions"))
|
||||
.toSorted((a, b) => a.localeCompare(b));
|
||||
return mapAgentSessionDirs(agentsDir, entries);
|
||||
}
|
||||
|
||||
export function resolveAgentSessionDirsFromAgentsDirSync(agentsDir: string): string[] {
|
||||
let entries: Dirent[] = [];
|
||||
try {
|
||||
entries = fsSync.readdirSync(agentsDir, { withFileTypes: true });
|
||||
} catch (err) {
|
||||
const code = (err as { code?: string }).code;
|
||||
if (code === "ENOENT") {
|
||||
return [];
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
return mapAgentSessionDirs(agentsDir, entries);
|
||||
}
|
||||
|
||||
export async function resolveAgentSessionDirs(stateDir: string): Promise<string[]> {
|
||||
return await resolveAgentSessionDirsFromAgentsDir(path.join(stateDir, "agents"));
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
resolveAgentIdFromSessionKey,
|
||||
} from "../../routing/session-key.js";
|
||||
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
|
||||
import { resolvePreferredSessionKeyForSessionIdMatches } from "../../sessions/session-id-resolution.js";
|
||||
import { resolveAgentDir } from "../agent-scope.js";
|
||||
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../date-time.js";
|
||||
import { resolveModelAuthLabel } from "../model-auth-label.js";
|
||||
@@ -100,16 +101,12 @@ function resolveSessionKeyFromSessionId(params: {
|
||||
return null;
|
||||
}
|
||||
const { store } = loadCombinedSessionStoreForGateway(params.cfg);
|
||||
const match = Object.entries(store).find(([key, entry]) => {
|
||||
if (entry?.sessionId !== trimmed) {
|
||||
return false;
|
||||
}
|
||||
if (!params.agentId) {
|
||||
return true;
|
||||
}
|
||||
return resolveAgentIdFromSessionKey(key) === params.agentId;
|
||||
});
|
||||
return match?.[0] ?? null;
|
||||
const matches = Object.entries(store).filter(
|
||||
(entry): entry is [string, SessionEntry] =>
|
||||
entry[1]?.sessionId === trimmed &&
|
||||
(!params.agentId || resolveAgentIdFromSessionKey(entry[0]) === params.agentId),
|
||||
);
|
||||
return resolvePreferredSessionKeyForSessionIdMatches(matches, trimmed) ?? null;
|
||||
}
|
||||
|
||||
async function resolveModelOverride(params: {
|
||||
|
||||
Reference in New Issue
Block a user