fix: preserve gateway agent session scope

This commit is contained in:
Frank Yang
2026-04-24 16:27:30 +08:00
parent 59b25bae9d
commit 52ef97d375
2 changed files with 83 additions and 4 deletions

View File

@@ -25,6 +25,7 @@ const mocks = vi.hoisted(() => ({
replaceSubagentRunAfterSteer: vi.fn(),
resolveExplicitAgentSessionKey: vi.fn(),
resolveBareResetBootstrapFileAccess: vi.fn(() => true),
listAgentIds: vi.fn(() => ["main"]),
loadConfigReturn: {} as Record<string, unknown>,
}));
@@ -71,7 +72,8 @@ vi.mock("../../config/config.js", async () => {
});
vi.mock("../../agents/agent-scope.js", () => ({
listAgentIds: () => ["main"],
listAgentIds: mocks.listAgentIds,
resolveDefaultAgentId: () => "main",
resolveAgentWorkspaceDir: (cfg: { agents?: { defaults?: { workspace?: string } } }) =>
cfg?.agents?.defaults?.workspace ?? "/tmp/workspace",
resolveAgentEffectiveModelPrimary: () => undefined,
@@ -337,6 +339,7 @@ describe("gateway agent handler", () => {
resetTaskRegistryForTests();
mocks.resolveExplicitAgentSessionKey.mockReset().mockReturnValue(undefined);
mocks.resolveBareResetBootstrapFileAccess.mockReset().mockReturnValue(true);
mocks.listAgentIds.mockReset().mockReturnValue(["main"]);
});
it("preserves ACP metadata from the current stored session entry", async () => {
@@ -1027,6 +1030,76 @@ describe("gateway agent handler", () => {
expect(call?.sessionKey).toBeUndefined();
});
it("treats whitespace sessionId as absent before resolving the agent session key", async () => {
mocks.resolveExplicitAgentSessionKey.mockReturnValue("agent:main:main");
mockMainSessionEntry({ sessionId: "existing-session-id" });
mocks.agentCommand.mockResolvedValue({
payloads: [{ text: "ok" }],
meta: { durationMs: 100 },
});
await invokeAgent(
{
message: "resume main",
agentId: "main",
sessionId: " ",
idempotencyKey: "blank-session-id-agent-resume",
},
{ reqId: "blank-session-id-agent-resume" },
);
await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled());
const call = mocks.agentCommand.mock.calls.at(-1)?.[0] as {
agentId?: string;
sessionId?: string;
sessionKey?: string;
};
expect(call?.agentId).toBe("main");
expect(call?.sessionId).toBe("existing-session-id");
expect(call?.sessionKey).toBe("agent:main:main");
});
it("does not forward a non-main agent id with canonical global session keys", async () => {
mocks.listAgentIds.mockReturnValue(["main", "ops"]);
mocks.resolveExplicitAgentSessionKey.mockReturnValue("agent:ops:main");
mocks.loadSessionEntry.mockReturnValue({
cfg: { session: { scope: "global" } },
storePath: "/tmp/sessions.json",
entry: {
sessionId: "global-session-id",
updatedAt: Date.now(),
},
canonicalKey: "global",
});
mocks.updateSessionStore.mockImplementation(async (_path, updater) => {
const store: Record<string, unknown> = {
global: { sessionId: "global-session-id", updatedAt: Date.now() },
};
return await updater(store);
});
mocks.agentCommand.mockResolvedValue({
payloads: [{ text: "ok" }],
meta: { durationMs: 100 },
});
await invokeAgent(
{
message: "global session",
agentId: "ops",
idempotencyKey: "global-session-agent-id",
},
{ reqId: "global-session-agent-id" },
);
await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled());
const call = mocks.agentCommand.mock.calls.at(-1)?.[0] as {
agentId?: string;
sessionKey?: string;
};
expect(call?.agentId).toBeUndefined();
expect(call?.sessionKey).toBe("global");
});
it("dispatches async gateway agent task creation through the detached task runtime seam", async () => {
await withTempDir({ prefix: "openclaw-gateway-agent-seam-" }, async (root) => {
process.env.OPENCLAW_STATE_DIR = root;

View File

@@ -500,9 +500,10 @@ export const agentHandlers: GatewayRequestHandlers = {
);
return;
}
const requestedSessionId = normalizeOptionalString(request.sessionId);
let requestedSessionKey =
requestedSessionKeyRaw ??
(!request.sessionId
(!requestedSessionId
? resolveExplicitAgentSessionKey({
cfg,
agentId,
@@ -522,7 +523,7 @@ export const agentHandlers: GatewayRequestHandlers = {
return;
}
}
let resolvedSessionId = normalizeOptionalString(request.sessionId);
let resolvedSessionId = requestedSessionId;
let sessionEntry: SessionEntry | undefined;
let bestEffortDeliver = requestedBestEffortDeliver ?? false;
let cfgForAgent: OpenClawConfig | undefined;
@@ -915,13 +916,18 @@ export const agentHandlers: GatewayRequestHandlers = {
}
const resolvedThreadId = explicitThreadId ?? deliveryPlan.resolvedThreadId;
const ingressAgentId =
agentId &&
(!resolvedSessionKey || resolveAgentIdFromSessionKey(resolvedSessionKey) === agentId)
? agentId
: undefined;
dispatchAgentRunFromGateway({
ingressOpts: {
message,
images,
imageOrder,
agentId,
agentId: ingressAgentId,
provider: providerOverride,
model: modelOverride,
to: resolvedTo,