fix(agents): backfill missing sessionKey in embedded PI runner — prevents undefined key in model selection and live-switch (#60555)

Merged via squash.

Prepared head SHA: 8081345f1c
Co-authored-by: 100yenadmin <239388517+100yenadmin@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
EVA
2026-04-06 23:51:05 +07:00
committed by GitHub
parent b4e1747391
commit 594ea6e1b9
6 changed files with 337 additions and 16 deletions

View File

@@ -25,7 +25,8 @@ vi.mock("../agent-scope.js", () => ({
listAgentIds: () => hoisted.listAgentIdsMock(),
}));
const { resolveSessionKeyForRequest } = await import("./session.js");
const { resolveSessionKeyForRequest, resolveStoredSessionKeyForSessionId } =
await import("./session.js");
describe("resolveSessionKeyForRequest", () => {
beforeEach(() => {
@@ -95,4 +96,32 @@ describe("resolveSessionKeyForRequest", () => {
expect(result.sessionStore).toBe(otherStore);
expect(result.storePath).toBe("/stores/other.json");
});
it("scopes stored session-key lookup to the requested agent store", () => {
const embeddedAgentStore = {
"agent:embedded-agent:main": { sessionId: "other-session", updatedAt: 2 },
"agent:embedded-agent:work": { sessionId: "resume-agent-1", updatedAt: 1 },
} satisfies Record<string, SessionEntry>;
hoisted.loadSessionStoreMock.mockImplementation((storePath) => {
if (storePath === "/stores/embedded-agent.json") {
return embeddedAgentStore;
}
return {};
});
const result = resolveStoredSessionKeyForSessionId({
cfg: {
session: {
store: "/stores/{agentId}.json",
},
} satisfies OpenClawConfig,
sessionId: "resume-agent-1",
agentId: "embedded-agent",
});
expect(result.sessionKey).toBe("agent:embedded-agent:work");
expect(result.sessionStore).toBe(embeddedAgentStore);
expect(result.storePath).toBe("/stores/embedded-agent.json");
expect(hoisted.loadSessionStoreMock).toHaveBeenCalledTimes(1);
});
});

View File

@@ -95,6 +95,37 @@ function collectSessionIdMatchesForRequest(opts: {
return { matches, primaryStoreMatches, storeByKey };
}
/**
* Resolve an existing stored session key for a session id from a specific agent store.
* This scopes the lookup to the target store without implicitly converting `agentId`
* into that agent's main session key.
*/
export function resolveStoredSessionKeyForSessionId(opts: {
cfg: OpenClawConfig;
sessionId: string;
agentId?: string;
}): SessionKeyResolution {
const sessionId = opts.sessionId.trim();
const storeAgentId = opts.agentId?.trim() ? normalizeAgentId(opts.agentId) : undefined;
const storePath = resolveStorePath(opts.cfg.session?.store, {
agentId: storeAgentId,
});
const sessionStore = loadSessionStore(storePath);
if (!sessionId) {
return { sessionKey: undefined, sessionStore, storePath };
}
const selection = resolveSessionIdMatchSelection(
Object.entries(sessionStore).filter(([, entry]) => entry?.sessionId === sessionId),
sessionId,
);
return {
sessionKey: selection.kind === "selected" ? selection.sessionKey : undefined,
sessionStore,
storePath,
};
}
export function resolveSessionKeyForRequest(opts: {
cfg: OpenClawConfig;
to?: string;
@@ -121,9 +152,10 @@ export function resolveSessionKeyForRequest(opts: {
let sessionKey: string | undefined =
explicitSessionKey ?? (ctx ? resolveSessionKey(scope, ctx, mainKey) : undefined);
// If a session id was provided, prefer to re-use its entry (by id) even when no key was derived.
// When duplicates exist across agent stores, pick the same deterministic best match used by the
// shared gateway/session resolver helpers instead of whichever store happens to be scanned first.
// If a session id was provided, prefer to re-use its existing entry (by id) even when no key was
// derived. When duplicates exist across agent stores, pick the same deterministic best match used
// by the shared gateway/session resolver helpers instead of whichever store happens to be scanned
// first.
if (
opts.sessionId &&
!explicitSessionKey &&