diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ed936692b9..5f10bf3e9e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Docs: https://docs.openclaw.ai - Agents/Bedrock: stop heartbeat runs from persisting blank user transcript turns and repair existing blank user text messages before replay, preventing AWS Bedrock `ContentBlock` blank-text validation failures. Fixes #72640 and #72622. Thanks @goldzulu. - Agents/LM Studio: promote standalone bracketed local-model tool requests into registered tool calls and hide unsupported bracket blocks from visible replies, so MemPalace MCP lookups do not print raw `[tool]` JSON scaffolding in chat. Fixes #66178. Thanks @detroit357. - Local models: warn when an assistant reply looks like a tool call but the provider emitted plain text instead of a structured tool invocation, making fake/non-executed tool calls visible in logs. Fixes #51332. Thanks @emilclaw. +- TUI/local models: treat visible gateway client labels such as `openclaw-tui` as the current requester session for session-aware tools, so Ollama tool calls no longer fail by resolving the UI label as a session id. Fixes #66391. Thanks @kickingzebra. - Local models: route self-hosted OpenAI-compatible model discovery through the guarded fetch path pinned to the configured host, covering vLLM and SGLang setup without reopening local/LAN SSRF probes. Supersedes #46359. Thanks @cdxiaodong. - Local models: classify terminated, reset, closed, timeout, and aborted model-call failures and attach a process memory snapshot to the diagnostic event, making LM Studio/Ollama RAM-pressure failures easier to prove from stability bundles. Refs #65551. Thanks @BigWiLLi111. - Local models: pass configured provider request timeouts through OpenAI SDK transports and the model idle watchdog so long-running local or custom OpenAI-compatible streams use one timeout knob instead of hitting the SDK's 10-minute default or the 120s idle default. Fixes #63663. Thanks @aidiffuser. diff --git a/docs/concepts/session-tool.md b/docs/concepts/session-tool.md index 5747466030f..ac74ff9fb65 100644 --- a/docs/concepts/session-tool.md +++ b/docs/concepts/session-tool.md @@ -82,7 +82,9 @@ agents alternate messages (up to 5 turns). The target agent can reply or another visible session. It reports usage, time, model/runtime state, and linked background-task context when present. Like `/status`, it can backfill sparse token/cache counters from the latest transcript usage entry, and -`model=default` clears a per-session override. +`model=default` clears a per-session override. Use `sessionKey="current"` for +the caller's current session; visible client labels such as `openclaw-tui` are +not session keys. `sessions_yield` intentionally ends the current turn so the next message can be the follow-up event you are waiting for. Use it after spawning sub-agents when diff --git a/src/agents/openclaw-tools.session-status.test.ts b/src/agents/openclaw-tools.session-status.test.ts index 0dc44a84a9a..5427cdd1dae 100644 --- a/src/agents/openclaw-tools.session-status.test.ts +++ b/src/agents/openclaw-tools.session-status.test.ts @@ -468,6 +468,22 @@ describe("session_status tool", () => { expect(details.sessionKey).toBe("main"); }); + it("treats the TUI client label as the current requester session", async () => { + resetSessionStore({ + "agent:main:main": { + sessionId: "s-main", + updatedAt: 10, + }, + }); + + const tool = getSessionStatusTool("agent:main:main"); + + const result = await tool.execute("call-tui-label", { sessionKey: "openclaw-tui" }); + const details = result.details as { ok?: boolean; sessionKey?: string }; + expect(details.ok).toBe(true); + expect(details.sessionKey).toBe("agent:main:main"); + }); + it("falls back from implicit default-account direct policy keys to persisted direct sessions", async () => { resetSessionStore({ "agent:main:telegram:direct:1053274893": { diff --git a/src/agents/tool-description-presets.ts b/src/agents/tool-description-presets.ts index f17eac45de4..1da6ac64faa 100644 --- a/src/agents/tool-description-presets.ts +++ b/src/agents/tool-description-presets.ts @@ -60,6 +60,7 @@ export function describeSessionsSpawnTool(options?: { acpAvailable?: boolean }): export function describeSessionStatusTool(): string { return [ "Show a /status-equivalent session status card for the current or another visible session, including usage, time, cost when available, and linked background task context.", + 'Use `sessionKey="current"` for the current session; do not use UI/client labels such as `openclaw-tui` as session keys.', "Optional `model` sets a per-session model override; `model=default` resets overrides.", "Use this for questions like what model is active or how a session is configured.", ].join(" "); diff --git a/src/agents/tools/session-status-tool.ts b/src/agents/tools/session-status-tool.ts index a76ca41dbb9..49c9036d851 100644 --- a/src/agents/tools/session-status-tool.ts +++ b/src/agents/tools/session-status-tool.ts @@ -45,6 +45,7 @@ import { createSessionVisibilityGuard, shouldResolveSessionIdInput, createAgentToAgentPolicy, + resolveCurrentSessionClientAlias, resolveEffectiveSessionToolsVisibility, resolveInternalSessionKey, resolveSessionReference, @@ -322,6 +323,13 @@ export function createSessionStatusTool(opts?: { const requestedKeyParam = readStringParam(params, "sessionKey"); let requestedKeyRaw = requestedKeyParam ?? opts?.agentSessionKey; + const currentSessionAlias = resolveCurrentSessionClientAlias({ + key: requestedKeyRaw ?? "", + requesterInternalKey: effectiveRequesterKey, + }); + if (currentSessionAlias) { + requestedKeyRaw = currentSessionAlias; + } const requestedKeyInput = requestedKeyRaw?.trim() ?? ""; let resolvedViaSessionId = false; let resolvedViaImplicitCurrentFallback = false; diff --git a/src/agents/tools/sessions-helpers.ts b/src/agents/tools/sessions-helpers.ts index 09116686a09..2e486ff5a88 100644 --- a/src/agents/tools/sessions-helpers.ts +++ b/src/agents/tools/sessions-helpers.ts @@ -20,6 +20,7 @@ export { listSpawnedSessionKeys, looksLikeSessionId, looksLikeSessionKey, + resolveCurrentSessionClientAlias, resolveDisplaySessionKey, resolveInternalSessionKey, resolveMainSessionAlias, diff --git a/src/agents/tools/sessions-resolution.test.ts b/src/agents/tools/sessions-resolution.test.ts index 202f9925299..e0154a8511d 100644 --- a/src/agents/tools/sessions-resolution.test.ts +++ b/src/agents/tools/sessions-resolution.test.ts @@ -7,6 +7,7 @@ vi.mock("../../gateway/call.js", () => ({ let isResolvedSessionVisibleToRequester: typeof import("./sessions-resolution.js").isResolvedSessionVisibleToRequester; let looksLikeSessionId: typeof import("./sessions-resolution.js").looksLikeSessionId; let looksLikeSessionKey: typeof import("./sessions-resolution.js").looksLikeSessionKey; +let resolveCurrentSessionClientAlias: typeof import("./sessions-resolution.js").resolveCurrentSessionClientAlias; let resolveDisplaySessionKey: typeof import("./sessions-resolution.js").resolveDisplaySessionKey; let resolveInternalSessionKey: typeof import("./sessions-resolution.js").resolveInternalSessionKey; let resolveMainSessionAlias: typeof import("./sessions-resolution.js").resolveMainSessionAlias; @@ -19,6 +20,7 @@ beforeAll(async () => { isResolvedSessionVisibleToRequester, looksLikeSessionId, looksLikeSessionKey, + resolveCurrentSessionClientAlias, resolveDisplaySessionKey, resolveInternalSessionKey, resolveMainSessionAlias, @@ -105,6 +107,22 @@ describe("session key display/internal mapping", () => { "current", ); }); + + it("maps interactive client ids to the requester session", () => { + expect( + resolveCurrentSessionClientAlias({ + key: "openclaw-tui", + requesterInternalKey: "agent:main:main", + }), + ).toBe("agent:main:main"); + expect(resolveCurrentSessionClientAlias({ key: "openclaw-tui" })).toBeUndefined(); + expect( + resolveCurrentSessionClientAlias({ + key: "node-host", + requesterInternalKey: "agent:main:main", + }), + ).toBeUndefined(); + }); }); describe("session reference shape detection", () => { @@ -303,4 +321,22 @@ describe("resolveSessionReference", () => { }); expect(callGatewayMock).toHaveBeenCalledTimes(1); }); + + it("treats the TUI client label as the requester session", async () => { + await expect( + resolveSessionReference({ + sessionKey: "openclaw-tui", + alias: "main", + mainKey: "main", + requesterInternalKey: "agent:main:main", + restrictToSpawned: false, + }), + ).resolves.toMatchObject({ + ok: true, + key: "agent:main:main", + displayKey: "agent:main:main", + resolvedViaSessionId: false, + }); + expect(callGatewayMock).not.toHaveBeenCalled(); + }); }); diff --git a/src/agents/tools/sessions-resolution.ts b/src/agents/tools/sessions-resolution.ts index 562d7affccd..9efe29d5ef4 100644 --- a/src/agents/tools/sessions-resolution.ts +++ b/src/agents/tools/sessions-resolution.ts @@ -1,5 +1,9 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { callGateway } from "../../gateway/call.js"; +import { + GATEWAY_CLIENT_IDS, + normalizeGatewayClientId, +} from "../../gateway/protocol/client-info.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { listSpawnedSessionKeys, @@ -15,6 +19,16 @@ const defaultSessionsResolutionDeps = { callGateway, }; +const CURRENT_SESSION_CLIENT_ALIAS_IDS = new Set([ + GATEWAY_CLIENT_IDS.TUI, + GATEWAY_CLIENT_IDS.CLI, + GATEWAY_CLIENT_IDS.WEBCHAT_UI, + GATEWAY_CLIENT_IDS.CONTROL_UI, + GATEWAY_CLIENT_IDS.MACOS_APP, + GATEWAY_CLIENT_IDS.IOS_APP, + GATEWAY_CLIENT_IDS.ANDROID_APP, +]); + let sessionsResolutionDeps: { callGateway: GatewayCaller; } = defaultSessionsResolutionDeps; @@ -51,6 +65,23 @@ export function resolveInternalSessionKey(params: { return params.key; } +export function resolveCurrentSessionClientAlias(params: { + key: string; + requesterInternalKey?: string; +}): string | undefined { + const requesterKey = normalizeOptionalString(params.requesterInternalKey); + if (!requesterKey) { + return undefined; + } + const clientId = normalizeGatewayClientId(params.key); + if (!clientId || !CURRENT_SESSION_CLIENT_ALIAS_IDS.has(clientId)) { + return undefined; + } + // UI/client labels can appear next to the real session key in status text. + // Treat them as the current requester instead of probing them as sessionIds. + return requesterKey; +} + export { listSpawnedSessionKeys }; export async function isRequesterSpawnedSessionVisible(params: { @@ -361,7 +392,11 @@ export async function resolveSessionReference(params: { requesterInternalKey?: string; restrictToSpawned: boolean; }): Promise { - const rawInput = params.sessionKey.trim(); + const rawInput = + resolveCurrentSessionClientAlias({ + key: params.sessionKey, + requesterInternalKey: params.requesterInternalKey, + }) ?? params.sessionKey.trim(); if (rawInput === "current") { const resolvedCurrent = await resolveSessionReferenceByKeyOrSessionId({ raw: rawInput,