Skip to content

Commit 9402bca

Browse files
committed
fix: limit session list enrichment
1 parent 72f3c84 commit 9402bca

4 files changed

Lines changed: 113 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
1919
- CLI/message: load only the selected channel plugin for targeted `openclaw message` actions, and fall back to configured channel plugins when the channel must be inferred, so scripted sends avoid full bundled plugin registry scans. Fixes #73006. Thanks @jasonftl.
2020
- CLI/models: keep route-first `models status --json` stdout reserved for the JSON payload by routing auth-profile and startup diagnostics to stderr. Fixes #72962. Thanks @vishutdhar.
2121
- Sessions: ignore future-dated session activity timestamps during reset freshness checks and cap future `updatedAt` values at the merge boundary so clock-skewed messages cannot keep stale sessions alive forever. Fixes #72989. Thanks @martingarramon.
22+
- Sessions: apply search, activity filters, and limits before gateway row enrichment so bounded session lists avoid scanning discarded transcripts. Carries forward #72978. Thanks @yeager.
2223
- Plugins/CLI: allow managed plugin installs when the active extensions root is a symlink to a real state directory, while keeping nested target symlinks blocked and suppressing misleading hook-pack fallback errors for install-boundary failures. Fixes #72946. Thanks @mayank6136.
2324
- Providers/Ollama: mark discovered Ollama catalog models as supporting streaming usage metadata so token accounting stays enabled for local models. (#72976) Thanks @sdeyang.
2425
- Gateway/startup: keep hot Gateway boot paths on leaf config imports and add max-RSS reporting to the gateway startup bench so low-memory startup regressions are visible before release. Thanks @vincentkoc.

src/config/sessions/combined-store-gateway.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function loadCombinedSessionStoreForGateway(cfg: OpenClawConfig): {
5151
if (storeConfig && !isStorePathTemplate(storeConfig)) {
5252
const storePath = resolveStorePath(storeConfig);
5353
const defaultAgentId = normalizeAgentId(resolveDefaultAgentId(cfg));
54-
const store = loadSessionStore(storePath);
54+
const store = loadSessionStore(storePath, { clone: false });
5555
const combined: Record<string, SessionEntry> = {};
5656
for (const [key, entry] of Object.entries(store)) {
5757
const canonicalKey = resolveStoredSessionKeyForAgentStore({
@@ -75,7 +75,7 @@ export function loadCombinedSessionStoreForGateway(cfg: OpenClawConfig): {
7575
for (const target of targets) {
7676
const agentId = target.agentId;
7777
const storePath = target.storePath;
78-
const store = loadSessionStore(storePath);
78+
const store = loadSessionStore(storePath, { clone: false });
7979
for (const [key, entry] of Object.entries(store)) {
8080
const canonicalKey = resolveStoredSessionKeyForAgentStore({
8181
cfg,

src/gateway/session-utils.subagent.test.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from "node:fs";
22
import os from "node:os";
33
import path from "node:path";
4-
import { afterEach, beforeEach, describe, expect, test } from "vitest";
4+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
55
import {
66
addSubagentRunForTests,
77
resetSubagentRegistryForTests,
@@ -32,6 +32,68 @@ describe("listSessionsFromStore subagent metadata", () => {
3232
agents: { list: [{ id: "main", default: true }] },
3333
} as OpenClawConfig;
3434

35+
test("searches channel-derived display names before row enrichment", () => {
36+
const result = listSessionsFromStore({
37+
cfg,
38+
storePath: "/tmp/sessions.json",
39+
store: {
40+
"agent:main:slack:group:general": {
41+
sessionId: "slack-general-session",
42+
updatedAt: 2,
43+
channel: "slack",
44+
} as SessionEntry,
45+
"agent:main:discord:group:random": {
46+
sessionId: "discord-random-session",
47+
updatedAt: 1,
48+
channel: "discord",
49+
} as SessionEntry,
50+
},
51+
opts: { search: "slack:g-general" },
52+
});
53+
54+
expect(result.sessions.map((session) => session.key)).toEqual([
55+
"agent:main:slack:group:general",
56+
]);
57+
expect(result.sessions[0]?.displayName).toBe("slack:g-general");
58+
});
59+
60+
test("applies limit before transcript enrichment", () => {
61+
const store: Record<string, SessionEntry> = {
62+
"agent:main:newest": {
63+
sessionId: "newest-session",
64+
sessionFile: "/tmp/newest-session.jsonl",
65+
updatedAt: 300,
66+
} as SessionEntry,
67+
"agent:main:middle": {
68+
sessionId: "middle-session",
69+
sessionFile: "/tmp/middle-session.jsonl",
70+
updatedAt: 200,
71+
} as SessionEntry,
72+
"agent:main:oldest": {
73+
sessionId: "old-session",
74+
sessionFile: "/tmp/old-session.jsonl",
75+
updatedAt: 100,
76+
} as SessionEntry,
77+
};
78+
const existsSpy = vi.spyOn(fs, "existsSync").mockReturnValue(false);
79+
try {
80+
const result = listSessionsFromStore({
81+
cfg,
82+
storePath: "/tmp/sessions.json",
83+
store,
84+
opts: { limit: 2 },
85+
});
86+
87+
expect(result.sessions.map((session) => session.sessionId)).toEqual([
88+
"newest-session",
89+
"middle-session",
90+
]);
91+
expect(existsSpy.mock.calls.flat().join("\n")).not.toContain("old-session");
92+
} finally {
93+
existsSpy.mockRestore();
94+
}
95+
});
96+
3597
test("includes subagent status timing and direct child session keys", () => {
3698
const now = Date.now();
3799
const store: Record<string, SessionEntry> = {

src/gateway/session-utils.ts

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,6 +1487,28 @@ export function buildGatewaySessionRow(params: {
14871487
};
14881488
}
14891489

1490+
function resolveSessionListSearchDisplayName(
1491+
key: string,
1492+
entry?: SessionEntry,
1493+
): string | undefined {
1494+
if (entry?.displayName) {
1495+
return entry.displayName;
1496+
}
1497+
const parsed = parseGroupKey(key);
1498+
const channel = entry?.channel ?? parsed?.channel;
1499+
if (!channel) {
1500+
return undefined;
1501+
}
1502+
return buildGroupDisplayName({
1503+
provider: channel,
1504+
subject: entry?.subject,
1505+
groupChannel: entry?.groupChannel,
1506+
space: entry?.space,
1507+
id: parsed?.id,
1508+
key,
1509+
});
1510+
}
1511+
14901512
export function loadGatewaySessionRow(
14911513
sessionKey: string,
14921514
options?: { includeDerivedTitles?: boolean; includeLastMessage?: boolean; now?: number },
@@ -1529,7 +1551,7 @@ export function listSessionsFromStore(params: {
15291551
? Math.max(1, Math.floor(opts.activeMinutes))
15301552
: undefined;
15311553

1532-
let sessions = Object.entries(store)
1554+
let entries = Object.entries(store)
15331555
.filter(([key]) => {
15341556
if (isCronRunSessionKey(key)) {
15351557
return false;
@@ -1583,23 +1605,17 @@ export function listSessionsFromStore(params: {
15831605
}
15841606
return entry?.label === label;
15851607
})
1586-
.map(([key, entry]) =>
1587-
buildGatewaySessionRow({
1588-
cfg,
1589-
storePath,
1590-
store,
1591-
key,
1592-
entry,
1593-
now,
1594-
includeDerivedTitles,
1595-
includeLastMessage,
1596-
}),
1597-
)
1598-
.toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
1608+
.toSorted((a, b) => (b[1]?.updatedAt ?? 0) - (a[1]?.updatedAt ?? 0));
15991609

16001610
if (search) {
1601-
sessions = sessions.filter((s) => {
1602-
const fields = [s.displayName, s.label, s.subject, s.sessionId, s.key];
1611+
entries = entries.filter(([key, entry]) => {
1612+
const fields = [
1613+
resolveSessionListSearchDisplayName(key, entry),
1614+
entry?.label,
1615+
entry?.subject,
1616+
entry?.sessionId,
1617+
key,
1618+
];
16031619
return fields.some(
16041620
(f) => typeof f === "string" && normalizeLowercaseStringOrEmpty(f).includes(search),
16051621
);
@@ -1608,14 +1624,27 @@ export function listSessionsFromStore(params: {
16081624

16091625
if (activeMinutes !== undefined) {
16101626
const cutoff = now - activeMinutes * 60_000;
1611-
sessions = sessions.filter((s) => (s.updatedAt ?? 0) >= cutoff);
1627+
entries = entries.filter(([, entry]) => (entry?.updatedAt ?? 0) >= cutoff);
16121628
}
16131629

16141630
if (typeof opts.limit === "number" && Number.isFinite(opts.limit)) {
16151631
const limit = Math.max(1, Math.floor(opts.limit));
1616-
sessions = sessions.slice(0, limit);
1632+
entries = entries.slice(0, limit);
16171633
}
16181634

1635+
const sessions = entries.map(([key, entry]) =>
1636+
buildGatewaySessionRow({
1637+
cfg,
1638+
storePath,
1639+
store,
1640+
key,
1641+
entry,
1642+
now,
1643+
includeDerivedTitles,
1644+
includeLastMessage,
1645+
}),
1646+
);
1647+
16191648
return {
16201649
ts: now,
16211650
path: storePath,

0 commit comments

Comments
 (0)