fix(agents): treat TUI client label as current session

This commit is contained in:
Peter Steinberger
2026-04-27 10:45:07 +01:00
parent 27ee5c0098
commit dfe58a1b8e
8 changed files with 102 additions and 2 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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": {

View File

@@ -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(" ");

View File

@@ -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;

View File

@@ -20,6 +20,7 @@ export {
listSpawnedSessionKeys,
looksLikeSessionId,
looksLikeSessionKey,
resolveCurrentSessionClientAlias,
resolveDisplaySessionKey,
resolveInternalSessionKey,
resolveMainSessionAlias,

View File

@@ -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();
});
});

View File

@@ -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<string>([
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<SessionReferenceResolution> {
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,