fix(agents): prevent invalid session filters from listing every session (#129198)

* fix(agents): preserve explicit session kind filtering

* test(agents): refresh session kind prompt snapshots
This commit is contained in:
Peter Steinberger
2026-08-25 02:53:07 -07:00
committed by GitHub
parent 8160c6e90f
commit 79722d2353
7 changed files with 67 additions and 33 deletions
+2 -1
View File
@@ -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<Record<string, SessionKind>> = {
main: "main",
@@ -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({
+12 -20
View File
@@ -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");
@@ -1119,6 +1119,7 @@
},
"kinds": {
"items": {
"enum": ["main", "group", "cron", "hook", "node", "other"],
"type": "string"
},
"type": "array"
@@ -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,
@@ -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,
@@ -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,