diff --git a/src/agents/tools/sessions-helpers.ts b/src/agents/tools/sessions-helpers.ts index 3135dccd4532..9d618c8490c9 100644 --- a/src/agents/tools/sessions-helpers.ts +++ b/src/agents/tools/sessions-helpers.ts @@ -32,7 +32,8 @@ export { } from "./sessions-resolution.js"; /** Coarse session category used by session list/status tools. */ -type SessionKind = "main" | "group" | "cron" | "hook" | "node" | "other"; +export const SESSION_LIST_KINDS = ["main", "group", "cron", "hook", "node", "other"] as const; +type SessionKind = (typeof SESSION_LIST_KINDS)[number]; const SESSION_KIND_BY_CLASSIFICATION: Readonly> = { main: "main", diff --git a/src/agents/tools/sessions-list-tool.test.ts b/src/agents/tools/sessions-list-tool.test.ts index ccfbefd2f532..bcc1b3dc7e06 100644 --- a/src/agents/tools/sessions-list-tool.test.ts +++ b/src/agents/tools/sessions-list-tool.test.ts @@ -182,6 +182,46 @@ describe("sessions-list-tool", () => { }); }); + it("limits session kind arguments to the documented classification values", () => { + const tool = createSessionsListTool({ config: VALID_CONFIG }); + + expect( + Value.Check(tool.parameters, { kinds: ["main", "group", "cron", "hook", "node", "other"] }), + ).toBe(true); + expect(Value.Check(tool.parameters, { kinds: ["unknown"] })).toBe(false); + expect(Value.Check(tool.parameters, { kinds: [" "] })).toBe(false); + }); + + it.each([ + { name: "unknown-only", kinds: ["unknown"], expected: [] }, + { name: "whitespace-only", kinds: [" "], expected: [] }, + { name: "unknown scalar", kinds: "unknown", expected: [] }, + { name: "whitespace scalar", kinds: " ", expected: [] }, + { name: "known scalar", kinds: "MAIN", expected: ["agent:main:main"] }, + { name: "mixed known and unknown", kinds: ["unknown", "MAIN"], expected: ["agent:main:main"] }, + { + name: "empty", + kinds: [], + expected: ["agent:main:main", "agent:main:slack:channel:team-room"], + }, + ])("never broadens an explicit $name session kind filter", async ({ kinds, expected }) => { + mocks.gatewayCall.mockResolvedValue({ + sessions: [ + sessionRow("agent:main:main", "main"), + sessionRow("agent:main:slack:channel:team-room", "channel"), + sessionRow("agent:other:main", "main", "other"), + ], + }); + + const result = await createSessionsListTool({ config: VALID_CONFIG }).execute("filter-kinds", { + kinds, + }); + + expect(getSessionsListDetails(result).sessions?.map((session) => session.key)).toEqual( + expected, + ); + }); + it("lists unspawned same-agent sessions from the canonical main session under tree visibility", async () => { mocks.resolveEffectiveSessionToolsVisibility.mockReturnValue("tree"); mocks.resolveSandboxedSessionToolContext.mockReturnValue({ diff --git a/src/agents/tools/sessions-list-tool.ts b/src/agents/tools/sessions-list-tool.ts index 3f67d8b4356f..8694c49ead4c 100644 --- a/src/agents/tools/sessions-list-tool.ts +++ b/src/agents/tools/sessions-list-tool.ts @@ -3,10 +3,7 @@ * * Lists visible sessions and optionally hydrates titles, last messages, and transcript-derived metadata. */ -import { - normalizeOptionalLowercaseString, - readStringValue, -} from "@openclaw/normalization-core/string-coerce"; +import { readStringValue } from "@openclaw/normalization-core/string-coerce"; import pMap from "p-map"; import { Type } from "typebox"; import type { SessionRunStatus } from "../../../packages/gateway-protocol/src/schema/sessions-row.js"; @@ -21,6 +18,7 @@ import { resolveSessionAgentIds } from "../agent-scope.js"; import { optionalNonNegativeIntegerSchema, optionalPositiveIntegerSchema, + stringEnum, } from "../schema/typebox.js"; import { describeSessionLinkRule, @@ -51,12 +49,13 @@ import { resolveEffectiveSessionToolsVisibility, resolveInternalSessionKey, resolveSandboxedSessionToolContext, + SESSION_LIST_KINDS, type GatewaySessionListRow, type SessionListRow, } from "./sessions-helpers.js"; const SessionsListToolSchema = Type.Object({ - kinds: Type.Optional(Type.Array(Type.String())), + kinds: Type.Optional(Type.Array(stringEnum(SESSION_LIST_KINDS))), limit: optionalPositiveIntegerSchema(), activeMinutes: optionalPositiveIntegerSchema(), messageLimit: optionalNonNegativeIntegerSchema(), @@ -73,14 +72,7 @@ const SessionListRowOutputSchema = Type.Object( key: Type.String(), sessionId: Type.Optional(Type.String()), agentId: Type.String(), - kind: Type.Union([ - Type.Literal("main"), - Type.Literal("group"), - Type.Literal("cron"), - Type.Literal("hook"), - Type.Literal("node"), - Type.Literal("other"), - ]), + kind: stringEnum(SESSION_LIST_KINDS), channel: Type.String(), archived: Type.Boolean(), pinned: Type.Boolean(), @@ -185,13 +177,13 @@ export function createSessionsListTool(opts?: { sandboxed: opts?.sandboxed === true, }); - const kindsRaw = readStringArrayParam(params, "kinds") - ?.map((value) => normalizeOptionalLowercaseString(value)) - .filter((value): value is string => Boolean(value)); - const allowedKindsList = (kindsRaw ?? []).filter((value) => - ["main", "group", "cron", "hook", "node", "other"].includes(value), - ); - const allowedKinds = allowedKindsList.length ? new Set(allowedKindsList) : undefined; + const kindsRaw = readStringArrayParam(params, "kinds")?.map((value) => value.toLowerCase()); + const requestedKinds = params.kinds; + const allowedKinds = + (Array.isArray(requestedKinds) || typeof requestedKinds === "string") && + requestedKinds.length > 0 + ? new Set(kindsRaw) + : undefined; const limit = readPositiveIntegerParam(params, "limit"); const activeMinutes = readPositiveIntegerParam(params, "activeMinutes"); diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json index e27e39c7e410..c89dc9e681b0 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json @@ -1119,6 +1119,7 @@ }, "kinds": { "items": { + "enum": ["main", "group", "cron", "hook", "node", "other"], "type": "string" }, "type": "array" diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md index f0eed0e61210..b95d4f5ae4fc 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md @@ -231,8 +231,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 54999, - "roughTokens": 13750 + "chars": 55201, + "roughTokens": 13801 }, "openClawDeveloperInstructions": { "chars": 4499, @@ -243,8 +243,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 7221 }, "totalWithDynamicToolsJson": { - "chars": 83883, - "roughTokens": 20971 + "chars": 84085, + "roughTokens": 21022 }, "userInputText": { "chars": 1300, diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md index 48452daf3a5c..f7aa63deecdc 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md @@ -231,8 +231,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 54691, - "roughTokens": 13673 + "chars": 54893, + "roughTokens": 13724 }, "openClawDeveloperInstructions": { "chars": 3390, @@ -243,8 +243,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 6851 }, "totalWithDynamicToolsJson": { - "chars": 82095, - "roughTokens": 20524 + "chars": 82297, + "roughTokens": 20575 }, "userInputText": { "chars": 929, diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md index 453543ba5649..8199f59d6cce 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md @@ -226,8 +226,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 56248, - "roughTokens": 14062 + "chars": 56450, + "roughTokens": 14113 }, "openClawDeveloperInstructions": { "chars": 3390, @@ -238,8 +238,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 6955 }, "totalWithDynamicToolsJson": { - "chars": 84068, - "roughTokens": 21017 + "chars": 84270, + "roughTokens": 21068 }, "userInputText": { "chars": 1271,