fix(agents): filter session previews after visibility

This commit is contained in:
Peter Steinberger
2026-04-23 01:32:00 +01:00
parent 13882581b6
commit b53bce9f47
2 changed files with 119 additions and 6 deletions

View File

@@ -1,3 +1,5 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ChannelMessagingAdapter } from "../channels/plugins/types.js";
@@ -297,9 +299,7 @@ describe("sessions tools", () => {
params: {
activeMinutes: undefined,
agentId: "main",
includeDerivedTitles: true,
includeGlobal: true,
includeLastMessage: true,
includeUnknown: true,
label: "mailbox",
limit: undefined,
@@ -356,6 +356,93 @@ describe("sessions tools", () => {
expect(cronDetails.sessions?.[0]?.kind).toBe("cron");
});
it("derives mailbox previews only after agent visibility filtering", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sessions-list-preview-"));
const storePath = path.join(tmpDir, "sessions.json");
try {
fs.writeFileSync(
path.join(tmpDir, "visible.jsonl"),
[
JSON.stringify({ type: "session", id: "visible" }),
JSON.stringify({ message: { role: "user", content: "Visible project kickoff" } }),
JSON.stringify({ message: { role: "assistant", content: "Visible latest reply" } }),
].join("\n"),
"utf-8",
);
fs.writeFileSync(
path.join(tmpDir, "hidden.jsonl"),
[
JSON.stringify({ type: "session", id: "hidden" }),
JSON.stringify({ message: { role: "user", content: "Hidden cross-agent topic" } }),
JSON.stringify({ message: { role: "assistant", content: "Hidden latest reply" } }),
].join("\n"),
"utf-8",
);
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string; params?: Record<string, unknown> };
if (request.method === "sessions.list") {
expect(request.params?.includeDerivedTitles).toBeUndefined();
expect(request.params?.includeLastMessage).toBeUndefined();
return {
path: storePath,
sessions: [
{
key: "agent:main:main",
kind: "direct",
sessionId: "visible",
updatedAt: 20,
},
{
key: "agent:other:main",
kind: "direct",
sessionId: "hidden",
updatedAt: 21,
},
],
};
}
return {};
});
const tool = createOpenClawTools({
agentSessionKey: "agent:main:main",
config: {
...TEST_CONFIG,
tools: {
sessions: { visibility: "agent" },
agentToAgent: { enabled: false },
},
} as OpenClawConfig,
}).find((candidate) => candidate.name === "sessions_list");
expect(tool).toBeDefined();
if (!tool) {
throw new Error("missing sessions_list tool");
}
const result = await tool.execute("call-preview", {
includeDerivedTitles: true,
includeLastMessage: true,
});
const details = result.details as {
sessions?: Array<{
key?: string;
derivedTitle?: string;
lastMessagePreview?: string;
}>;
};
expect(details.sessions).toHaveLength(1);
expect(details.sessions?.[0]).toMatchObject({
key: "agent:main:main",
derivedTitle: "Visible project kickoff",
lastMessagePreview: "Visible latest reply",
});
expect(JSON.stringify(details.sessions)).not.toContain("Hidden");
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it("sessions_list resolves transcriptPath from agent state dir for multi-store listings", async () => {
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string };

View File

@@ -8,6 +8,8 @@ import {
} from "../../config/sessions.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { callGateway } from "../../gateway/call.js";
import { readSessionTitleFieldsFromTranscript } from "../../gateway/session-utils.fs.js";
import { deriveSessionTitle } from "../../gateway/session-utils.js";
import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
import { normalizeOptionalLowercaseString, readStringValue } from "../../shared/string-coerce.js";
import {
@@ -105,8 +107,8 @@ export function createSessionsListTool(opts?: {
const label = readStringParam(params, "label");
const agentId = readStringParam(params, "agentId");
const search = readStringParam(params, "search");
const includeDerivedTitles = params.includeDerivedTitles === true ? true : undefined;
const includeLastMessage = params.includeLastMessage === true ? true : undefined;
const includeDerivedTitles = params.includeDerivedTitles === true;
const includeLastMessage = params.includeLastMessage === true;
const gatewayCall = opts?.callGateway ?? callGateway;
const list = await gatewayCall<{ sessions: Array<SessionListRow>; path: string }>({
@@ -117,8 +119,6 @@ export function createSessionsListTool(opts?: {
label,
agentId,
search,
includeDerivedTitles,
includeLastMessage,
includeGlobal: !restrictToSpawned,
includeUnknown: !restrictToSpawned,
spawnedBy: restrictToSpawned ? effectiveRequesterKey : undefined,
@@ -309,6 +309,32 @@ export function createSessionsListTool(opts?: {
lastAccountId,
transcriptPath,
};
if (sessionId && (includeDerivedTitles || includeLastMessage)) {
const fields = readSessionTitleFieldsFromTranscript(
sessionId,
storePath,
sessionFile,
resolvedAgentId,
);
if (includeDerivedTitles && !row.derivedTitle) {
const derivedTitle = deriveSessionTitle(
{
sessionId,
displayName: row.displayName,
label: row.label,
subject: readStringValue((entry as { subject?: unknown }).subject),
updatedAt: typeof row.updatedAt === "number" ? row.updatedAt : 0,
},
fields.firstUserMessage,
);
if (derivedTitle) {
row.derivedTitle = derivedTitle;
}
}
if (includeLastMessage && !row.lastMessagePreview && fields.lastMessagePreview) {
row.lastMessagePreview = fields.lastMessagePreview;
}
}
if (messageLimit > 0) {
const resolvedKey = resolveInternalSessionKey({
key,