diff --git a/src/agents/openclaw-tools.session-status.test.ts b/src/agents/openclaw-tools.session-status.test.ts index 0bc079d4ced..342c21b5536 100644 --- a/src/agents/openclaw-tools.session-status.test.ts +++ b/src/agents/openclaw-tools.session-status.test.ts @@ -203,6 +203,59 @@ describe("session_status tool", () => { expect(updateSessionStoreMock).not.toHaveBeenCalled(); }); + it("resolves sessionKey=current to the requester session", async () => { + resetSessionStore({ + main: { + sessionId: "s1", + updatedAt: 10, + }, + }); + + const tool = getSessionStatusTool(); + + const result = await tool.execute("call-current", { sessionKey: "current" }); + const details = result.details as { ok?: boolean; sessionKey?: string }; + expect(details.ok).toBe(true); + expect(details.sessionKey).toBe("main"); + }); + + it("resolves sessionKey=current to the requester agent session", async () => { + loadSessionStoreMock.mockClear(); + updateSessionStoreMock.mockClear(); + callGatewayMock.mockClear(); + loadCombinedSessionStoreForGatewayMock.mockClear(); + const stores = new Map>([ + [ + "/tmp/main/sessions.json", + { + "agent:main:main": { sessionId: "s-main", updatedAt: 10 }, + }, + ], + [ + "/tmp/support/sessions.json", + { + main: { sessionId: "s-support", updatedAt: 20 }, + }, + ], + ]); + loadSessionStoreMock.mockImplementation((storePath: string) => { + return stores.get(storePath) ?? {}; + }); + loadCombinedSessionStoreForGatewayMock.mockReturnValue({ + storePath: "(multiple)", + store: Object.fromEntries([...stores.values()].flatMap((s) => Object.entries(s))), + }); + + const tool = getSessionStatusTool("agent:support:main"); + + // "current" resolves to the support agent's own session via the "main" alias. + const result = await tool.execute("call-current-child", { sessionKey: "current" }); + const details = result.details as { ok?: boolean; sessionKey?: string }; + expect(details.ok).toBe(true); + // The resolved key is "main" within the support agent's store scope. + expect(details.sessionKey).toBe("main"); + }); + it("resolves sessionId inputs", async () => { const sessionId = "sess-main"; resetSessionStore({ diff --git a/src/agents/tools/session-status-tool.ts b/src/agents/tools/session-status-tool.ts index 132b470fd2f..8585bc37d3f 100644 --- a/src/agents/tools/session-status-tool.ts +++ b/src/agents/tools/session-status-tool.ts @@ -56,6 +56,7 @@ function resolveSessionEntry(params: { keyRaw: string; alias: string; mainKey: string; + requesterInternalKey?: string; }): { key: string; entry: SessionEntry } | null { const keyRaw = params.keyRaw.trim(); if (!keyRaw) { @@ -65,6 +66,7 @@ function resolveSessionEntry(params: { key: keyRaw, alias: params.alias, mainKey: params.mainKey, + requesterInternalKey: params.requesterInternalKey, }); const candidates = new Set([keyRaw, internal]); @@ -72,7 +74,7 @@ function resolveSessionEntry(params: { candidates.add(`agent:${DEFAULT_AGENT_ID}:${keyRaw}`); candidates.add(`agent:${DEFAULT_AGENT_ID}:${internal}`); } - if (keyRaw === "main") { + if (keyRaw === "main" || keyRaw === "current") { candidates.add( buildAgentMainSessionKey({ agentId: DEFAULT_AGENT_ID, @@ -251,6 +253,12 @@ export function createSessionStatusTool(opts?: { if (!requestedKeyRaw?.trim()) { throw new Error("sessionKey required"); } + // Normalize "current" to the requester's own session key. The "main" + // alias is already scoped to the requester agent's store, so this gives + // every caller their own session regardless of agent identity. + if (requestedKeyRaw.trim().toLowerCase() === "current") { + requestedKeyRaw = "main"; + } const ensureAgentAccess = (targetAgentId: string) => { if (targetAgentId === requesterAgentId) { return; @@ -290,6 +298,7 @@ export function createSessionStatusTool(opts?: { keyRaw: requestedKeyRaw, alias, mainKey, + requesterInternalKey: effectiveRequesterKey, }); if (!resolved && shouldResolveSessionIdInput(requestedKeyRaw)) { @@ -310,6 +319,7 @@ export function createSessionStatusTool(opts?: { keyRaw: requestedKeyRaw, alias, mainKey, + requesterInternalKey: effectiveRequesterKey, }); } } diff --git a/src/agents/tools/sessions-resolution.test.ts b/src/agents/tools/sessions-resolution.test.ts index 6b6c004e333..6883ae15446 100644 --- a/src/agents/tools/sessions-resolution.test.ts +++ b/src/agents/tools/sessions-resolution.test.ts @@ -67,6 +67,23 @@ describe("session key display/internal mapping", () => { resolveInternalSessionKey({ key: "agent:ops:main", alias: "global", mainKey: "main" }), ).toBe("agent:ops:main"); }); + + it("maps current to requester session key", () => { + expect( + resolveInternalSessionKey({ + key: "current", + alias: "global", + mainKey: "main", + requesterInternalKey: "agent:support:main", + }), + ).toBe("agent:support:main"); + }); + + it("maps current to alias when no requester key is provided", () => { + expect(resolveInternalSessionKey({ key: "current", alias: "global", mainKey: "main" })).toBe( + "global", + ); + }); }); describe("session reference shape detection", () => { @@ -77,6 +94,7 @@ describe("session reference shape detection", () => { it("detects canonical session key families", () => { expect(looksLikeSessionKey("main")).toBe(true); + expect(looksLikeSessionKey("current")).toBe(true); expect(looksLikeSessionKey("agent:main:main")).toBe(true); expect(looksLikeSessionKey("cron:daily-report")).toBe(true); expect(looksLikeSessionKey("node:macbook")).toBe(true); @@ -86,6 +104,7 @@ describe("session reference shape detection", () => { it("treats non-keys as session-id candidates", () => { expect(shouldResolveSessionIdInput("agent:main:main")).toBe(false); + expect(shouldResolveSessionIdInput("current")).toBe(false); expect(shouldResolveSessionIdInput("d4f5a5a1-9f75-42cf-83a6-8d170e6a1538")).toBe(true); expect(shouldResolveSessionIdInput("random-slug")).toBe(true); }); diff --git a/src/agents/tools/sessions-resolution.ts b/src/agents/tools/sessions-resolution.ts index c2ba83c3001..db63d147461 100644 --- a/src/agents/tools/sessions-resolution.ts +++ b/src/agents/tools/sessions-resolution.ts @@ -25,7 +25,16 @@ export function resolveDisplaySessionKey(params: { key: string; alias: string; m return params.key; } -export function resolveInternalSessionKey(params: { key: string; alias: string; mainKey: string }) { +export function resolveInternalSessionKey(params: { + key: string; + alias: string; + mainKey: string; + requesterInternalKey?: string; +}) { + if (params.key === "current") { + // "current" resolves to the requester's own session, falling back to alias. + return params.requesterInternalKey ?? params.alias; + } if (params.key === "main") { return params.alias; } @@ -121,7 +130,7 @@ export function looksLikeSessionKey(value: string): boolean { return false; } // These are canonical key shapes that should never be treated as sessionIds. - if (raw === "main" || raw === "global" || raw === "unknown") { + if (raw === "main" || raw === "global" || raw === "unknown" || raw === "current") { return true; } if (isAcpSessionKey(raw)) { @@ -264,7 +273,11 @@ export async function resolveSessionReference(params: { requesterInternalKey?: string; restrictToSpawned: boolean; }): Promise { - const raw = params.sessionKey.trim(); + const rawInput = params.sessionKey.trim(); + // Normalize "current" to the requester's own session key so every session + // tool resolves it consistently without per-tool special-casing. + const raw = + rawInput === "current" && params.requesterInternalKey ? params.requesterInternalKey : rawInput; if (shouldResolveSessionIdInput(raw)) { // Prefer key resolution to avoid misclassifying custom keys as sessionIds. const resolvedByKey = await resolveSessionKeyFromKey({ @@ -290,6 +303,7 @@ export async function resolveSessionReference(params: { key: raw, alias: params.alias, mainKey: params.mainKey, + requesterInternalKey: params.requesterInternalKey, }); const displayKey = resolveDisplaySessionKey({ key: resolvedKey,