mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:10:43 +00:00
fix: limit session list enrichment
This commit is contained in:
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- Sessions: apply search, activity filters, and limits before gateway row enrichment so bounded session lists avoid scanning discarded transcripts. Carries forward #72978. Thanks @yeager.
|
||||
- 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.
|
||||
- Providers/Ollama: mark discovered Ollama catalog models as supporting streaming usage metadata so token accounting stays enabled for local models. (#72976) Thanks @sdeyang.
|
||||
- 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.
|
||||
|
||||
@@ -51,7 +51,7 @@ export function loadCombinedSessionStoreForGateway(cfg: OpenClawConfig): {
|
||||
if (storeConfig && !isStorePathTemplate(storeConfig)) {
|
||||
const storePath = resolveStorePath(storeConfig);
|
||||
const defaultAgentId = normalizeAgentId(resolveDefaultAgentId(cfg));
|
||||
const store = loadSessionStore(storePath);
|
||||
const store = loadSessionStore(storePath, { clone: false });
|
||||
const combined: Record<string, SessionEntry> = {};
|
||||
for (const [key, entry] of Object.entries(store)) {
|
||||
const canonicalKey = resolveStoredSessionKeyForAgentStore({
|
||||
@@ -75,7 +75,7 @@ export function loadCombinedSessionStoreForGateway(cfg: OpenClawConfig): {
|
||||
for (const target of targets) {
|
||||
const agentId = target.agentId;
|
||||
const storePath = target.storePath;
|
||||
const store = loadSessionStore(storePath);
|
||||
const store = loadSessionStore(storePath, { clone: false });
|
||||
for (const [key, entry] of Object.entries(store)) {
|
||||
const canonicalKey = resolveStoredSessionKeyForAgentStore({
|
||||
cfg,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
addSubagentRunForTests,
|
||||
resetSubagentRegistryForTests,
|
||||
@@ -32,6 +32,68 @@ describe("listSessionsFromStore subagent metadata", () => {
|
||||
agents: { list: [{ id: "main", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
test("searches channel-derived display names before row enrichment", () => {
|
||||
const result = listSessionsFromStore({
|
||||
cfg,
|
||||
storePath: "/tmp/sessions.json",
|
||||
store: {
|
||||
"agent:main:slack:group:general": {
|
||||
sessionId: "slack-general-session",
|
||||
updatedAt: 2,
|
||||
channel: "slack",
|
||||
} as SessionEntry,
|
||||
"agent:main:discord:group:random": {
|
||||
sessionId: "discord-random-session",
|
||||
updatedAt: 1,
|
||||
channel: "discord",
|
||||
} as SessionEntry,
|
||||
},
|
||||
opts: { search: "slack:g-general" },
|
||||
});
|
||||
|
||||
expect(result.sessions.map((session) => session.key)).toEqual([
|
||||
"agent:main:slack:group:general",
|
||||
]);
|
||||
expect(result.sessions[0]?.displayName).toBe("slack:g-general");
|
||||
});
|
||||
|
||||
test("applies limit before transcript enrichment", () => {
|
||||
const store: Record<string, SessionEntry> = {
|
||||
"agent:main:newest": {
|
||||
sessionId: "newest-session",
|
||||
sessionFile: "/tmp/newest-session.jsonl",
|
||||
updatedAt: 300,
|
||||
} as SessionEntry,
|
||||
"agent:main:middle": {
|
||||
sessionId: "middle-session",
|
||||
sessionFile: "/tmp/middle-session.jsonl",
|
||||
updatedAt: 200,
|
||||
} as SessionEntry,
|
||||
"agent:main:oldest": {
|
||||
sessionId: "old-session",
|
||||
sessionFile: "/tmp/old-session.jsonl",
|
||||
updatedAt: 100,
|
||||
} as SessionEntry,
|
||||
};
|
||||
const existsSpy = vi.spyOn(fs, "existsSync").mockReturnValue(false);
|
||||
try {
|
||||
const result = listSessionsFromStore({
|
||||
cfg,
|
||||
storePath: "/tmp/sessions.json",
|
||||
store,
|
||||
opts: { limit: 2 },
|
||||
});
|
||||
|
||||
expect(result.sessions.map((session) => session.sessionId)).toEqual([
|
||||
"newest-session",
|
||||
"middle-session",
|
||||
]);
|
||||
expect(existsSpy.mock.calls.flat().join("\n")).not.toContain("old-session");
|
||||
} finally {
|
||||
existsSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
test("includes subagent status timing and direct child session keys", () => {
|
||||
const now = Date.now();
|
||||
const store: Record<string, SessionEntry> = {
|
||||
|
||||
@@ -1487,6 +1487,28 @@ export function buildGatewaySessionRow(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSessionListSearchDisplayName(
|
||||
key: string,
|
||||
entry?: SessionEntry,
|
||||
): string | undefined {
|
||||
if (entry?.displayName) {
|
||||
return entry.displayName;
|
||||
}
|
||||
const parsed = parseGroupKey(key);
|
||||
const channel = entry?.channel ?? parsed?.channel;
|
||||
if (!channel) {
|
||||
return undefined;
|
||||
}
|
||||
return buildGroupDisplayName({
|
||||
provider: channel,
|
||||
subject: entry?.subject,
|
||||
groupChannel: entry?.groupChannel,
|
||||
space: entry?.space,
|
||||
id: parsed?.id,
|
||||
key,
|
||||
});
|
||||
}
|
||||
|
||||
export function loadGatewaySessionRow(
|
||||
sessionKey: string,
|
||||
options?: { includeDerivedTitles?: boolean; includeLastMessage?: boolean; now?: number },
|
||||
@@ -1529,7 +1551,7 @@ export function listSessionsFromStore(params: {
|
||||
? Math.max(1, Math.floor(opts.activeMinutes))
|
||||
: undefined;
|
||||
|
||||
let sessions = Object.entries(store)
|
||||
let entries = Object.entries(store)
|
||||
.filter(([key]) => {
|
||||
if (isCronRunSessionKey(key)) {
|
||||
return false;
|
||||
@@ -1583,23 +1605,17 @@ export function listSessionsFromStore(params: {
|
||||
}
|
||||
return entry?.label === label;
|
||||
})
|
||||
.map(([key, entry]) =>
|
||||
buildGatewaySessionRow({
|
||||
cfg,
|
||||
storePath,
|
||||
store,
|
||||
key,
|
||||
entry,
|
||||
now,
|
||||
includeDerivedTitles,
|
||||
includeLastMessage,
|
||||
}),
|
||||
)
|
||||
.toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
||||
.toSorted((a, b) => (b[1]?.updatedAt ?? 0) - (a[1]?.updatedAt ?? 0));
|
||||
|
||||
if (search) {
|
||||
sessions = sessions.filter((s) => {
|
||||
const fields = [s.displayName, s.label, s.subject, s.sessionId, s.key];
|
||||
entries = entries.filter(([key, entry]) => {
|
||||
const fields = [
|
||||
resolveSessionListSearchDisplayName(key, entry),
|
||||
entry?.label,
|
||||
entry?.subject,
|
||||
entry?.sessionId,
|
||||
key,
|
||||
];
|
||||
return fields.some(
|
||||
(f) => typeof f === "string" && normalizeLowercaseStringOrEmpty(f).includes(search),
|
||||
);
|
||||
@@ -1608,14 +1624,27 @@ export function listSessionsFromStore(params: {
|
||||
|
||||
if (activeMinutes !== undefined) {
|
||||
const cutoff = now - activeMinutes * 60_000;
|
||||
sessions = sessions.filter((s) => (s.updatedAt ?? 0) >= cutoff);
|
||||
entries = entries.filter(([, entry]) => (entry?.updatedAt ?? 0) >= cutoff);
|
||||
}
|
||||
|
||||
if (typeof opts.limit === "number" && Number.isFinite(opts.limit)) {
|
||||
const limit = Math.max(1, Math.floor(opts.limit));
|
||||
sessions = sessions.slice(0, limit);
|
||||
entries = entries.slice(0, limit);
|
||||
}
|
||||
|
||||
const sessions = entries.map(([key, entry]) =>
|
||||
buildGatewaySessionRow({
|
||||
cfg,
|
||||
storePath,
|
||||
store,
|
||||
key,
|
||||
entry,
|
||||
now,
|
||||
includeDerivedTitles,
|
||||
includeLastMessage,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
ts: now,
|
||||
path: storePath,
|
||||
|
||||
Reference in New Issue
Block a user