mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
fix(gateway): bound sessions list responses
Bound default Gateway sessions.list responses to 100 rows when callers omit limit, with response metadata for totalCount, limitApplied, and hasMore.\n\nFixes #77062.
This commit is contained in:
committed by
GitHub
parent
1df6226d90
commit
a224810a7f
@@ -94,6 +94,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Release validation: let Windows packaged-upgrade checks continue after the shipped 2026.5.2 updater hits its native-module swap cleanup fallback, verifying the fallback-installed candidate through package metadata and downstream smoke instead of crashing on the immediate update-status probe. Thanks @vincentkoc.
|
||||
- Doctor/plugins: skip channel-derived official plugin installs when another configured plugin is the effective owner for the same channel, so `doctor --repair` does not reinstall `feishu` while `openclaw-lark` handles `channels.feishu`. Fixes #76623. Thanks @fuyizheng3120.
|
||||
- Gateway/sessions: memoize repeated thinking-option enrichment and skip unused cost fallback checks while listing sessions, reducing per-row work on large multi-agent stores. Fixes #76931.
|
||||
- Gateway/sessions: bound default `sessions.list` RPC responses and report truncation metadata, preventing Slack-heavy long-lived stores from forcing unbounded Gateway row construction. Fixes #77062.
|
||||
- Agents/tools: use config-only runtime snapshots for plugin tool registration and live runtime config getters, avoiding expensive full secrets snapshot clones on the core-plugin-tools prep path. Fixes #76295.
|
||||
- Agents/tools: honor the effective tool denylist before constructing optional PDF/media tool factories, so `tools.deny: ["pdf"]` skips PDF setup before later policy filtering. Fixes #76997.
|
||||
- MCP/plugin tools: apply global `tools.profile`, `tools.alsoAllow`, and `tools.deny` policy while exposing plugin tools over the standalone MCP bridge, so ACP clients do not see policy-hidden plugin tools or miss opt-in optional tools. Thanks @vincentkoc.
|
||||
|
||||
@@ -16,6 +16,12 @@ until a message is processed. Use `openclaw channels status --probe`,
|
||||
`openclaw status --deep`, or `openclaw health --verbose` when you need live
|
||||
channel connectivity.
|
||||
|
||||
Gateway `sessions.list` responses are bounded by default so large long-lived
|
||||
stores cannot monopolize the Gateway event loop. Pass an explicit positive
|
||||
`limit` from RPC clients when a different result window is needed; responses
|
||||
include `totalCount`, `limitApplied`, and `hasMore` when callers need to show
|
||||
that more rows exist.
|
||||
|
||||
```bash
|
||||
openclaw sessions
|
||||
openclaw sessions --agent work
|
||||
|
||||
@@ -38,6 +38,10 @@ export const SessionCompactionCheckpointSchema = Type.Object(
|
||||
|
||||
export const SessionsListParamsSchema = Type.Object(
|
||||
{
|
||||
/**
|
||||
* Maximum rows to return. Omitted Gateway RPC calls use a bounded default
|
||||
* to keep large session stores from monopolizing the event loop.
|
||||
*/
|
||||
limit: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
activeMinutes: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
includeGlobal: Type.Optional(Type.Boolean()),
|
||||
|
||||
@@ -83,6 +83,64 @@ describe("gateway session utils", () => {
|
||||
expect(res.items).toEqual(["b", "c"]);
|
||||
});
|
||||
|
||||
test("session lists apply a bounded default and expose truncation metadata", async () => {
|
||||
const cfg = createModelDefaultsConfig({ primary: "openai/gpt-5.4" });
|
||||
const store = Object.fromEntries(
|
||||
Array.from({ length: 105 }, (_value, index) => [
|
||||
`session-${index}`,
|
||||
{
|
||||
sessionId: `session-${index}`,
|
||||
updatedAt: 1_000 - index,
|
||||
} satisfies SessionEntry,
|
||||
]),
|
||||
);
|
||||
|
||||
const listed = await listSessionsFromStoreAsync({
|
||||
cfg,
|
||||
storePath: "",
|
||||
store,
|
||||
opts: {},
|
||||
});
|
||||
|
||||
expect(listed.sessions).toHaveLength(100);
|
||||
expect(listed.count).toBe(100);
|
||||
expect(listed.totalCount).toBe(105);
|
||||
expect(listed.limitApplied).toBe(100);
|
||||
expect(listed.hasMore).toBe(true);
|
||||
expect(listed.sessions[0]?.key).toBe("session-0");
|
||||
expect(listed.sessions.at(-1)?.key).toBe("session-99");
|
||||
});
|
||||
|
||||
test("session lists honor explicit caller limits", () => {
|
||||
const cfg = createModelDefaultsConfig({ primary: "openai/gpt-5.4" });
|
||||
const store = Object.fromEntries(
|
||||
Array.from({ length: 5 }, (_value, index) => [
|
||||
`session-${index}`,
|
||||
{
|
||||
sessionId: `session-${index}`,
|
||||
updatedAt: 1_000 - index,
|
||||
} satisfies SessionEntry,
|
||||
]),
|
||||
);
|
||||
|
||||
const listed = listSessionsFromStore({
|
||||
cfg,
|
||||
storePath: "",
|
||||
store,
|
||||
opts: { limit: 3 },
|
||||
});
|
||||
|
||||
expect(listed.sessions.map((session) => session.key)).toEqual([
|
||||
"session-0",
|
||||
"session-1",
|
||||
"session-2",
|
||||
]);
|
||||
expect(listed.count).toBe(3);
|
||||
expect(listed.totalCount).toBe(5);
|
||||
expect(listed.limitApplied).toBe(3);
|
||||
expect(listed.hasMore).toBe(true);
|
||||
});
|
||||
|
||||
test("parseGroupKey handles group keys", () => {
|
||||
expect(parseGroupKey("discord:group:dev")).toEqual({
|
||||
channel: "discord",
|
||||
|
||||
@@ -1795,8 +1795,14 @@ export function loadGatewaySessionRow(
|
||||
*/
|
||||
const SESSIONS_LIST_YIELD_BATCH_SIZE = 10;
|
||||
const SESSIONS_LIST_TOP_N_LIMIT = 200;
|
||||
const SESSIONS_LIST_DEFAULT_LIMIT = 100;
|
||||
|
||||
type SessionEntryPair = [string, SessionEntry];
|
||||
type SessionEntrySelection = {
|
||||
entries: SessionEntryPair[];
|
||||
totalCount: number;
|
||||
limitApplied?: number;
|
||||
};
|
||||
|
||||
function compareSessionEntryPairsByUpdatedAt(a: SessionEntryPair, b: SessionEntryPair): number {
|
||||
return (b[1]?.updatedAt ?? 0) - (a[1]?.updatedAt ?? 0);
|
||||
@@ -1804,9 +1810,10 @@ function compareSessionEntryPairsByUpdatedAt(a: SessionEntryPair, b: SessionEntr
|
||||
|
||||
function resolveSessionsListLimit(
|
||||
opts: import("./protocol/index.js").SessionsListParams,
|
||||
defaultLimit?: number,
|
||||
): number | undefined {
|
||||
if (typeof opts.limit !== "number" || !Number.isFinite(opts.limit)) {
|
||||
return undefined;
|
||||
return defaultLimit;
|
||||
}
|
||||
return Math.max(1, Math.floor(opts.limit));
|
||||
}
|
||||
@@ -1843,12 +1850,12 @@ function sortAndLimitSessionEntries(
|
||||
return limit === undefined ? sorted : sorted.slice(0, limit);
|
||||
}
|
||||
|
||||
export function filterAndSortSessionEntries(params: {
|
||||
function filterSessionEntries(params: {
|
||||
store: Record<string, SessionEntry>;
|
||||
opts: import("./protocol/index.js").SessionsListParams;
|
||||
now: number;
|
||||
rowContext?: SessionListRowContext;
|
||||
}): [string, SessionEntry][] {
|
||||
}): SessionEntryPair[] {
|
||||
const { store, opts, now } = params;
|
||||
const rowContext = params.rowContext;
|
||||
const includeGlobal = opts.includeGlobal === true;
|
||||
@@ -1941,7 +1948,33 @@ export function filterAndSortSessionEntries(params: {
|
||||
entries = entries.filter(([, entry]) => (entry?.updatedAt ?? 0) >= cutoff);
|
||||
}
|
||||
|
||||
return sortAndLimitSessionEntries(entries, resolveSessionsListLimit(opts));
|
||||
return entries;
|
||||
}
|
||||
|
||||
function selectSessionEntries(params: {
|
||||
store: Record<string, SessionEntry>;
|
||||
opts: import("./protocol/index.js").SessionsListParams;
|
||||
now: number;
|
||||
rowContext?: SessionListRowContext;
|
||||
defaultLimit?: number;
|
||||
}): SessionEntrySelection {
|
||||
const filtered = filterSessionEntries(params);
|
||||
const limit = resolveSessionsListLimit(params.opts, params.defaultLimit);
|
||||
const entries = sortAndLimitSessionEntries(filtered, limit);
|
||||
return {
|
||||
entries,
|
||||
totalCount: filtered.length,
|
||||
limitApplied: limit,
|
||||
};
|
||||
}
|
||||
|
||||
export function filterAndSortSessionEntries(params: {
|
||||
store: Record<string, SessionEntry>;
|
||||
opts: import("./protocol/index.js").SessionsListParams;
|
||||
now: number;
|
||||
rowContext?: SessionListRowContext;
|
||||
}): [string, SessionEntry][] {
|
||||
return selectSessionEntries(params).entries;
|
||||
}
|
||||
|
||||
export function listSessionsFromStore(params: {
|
||||
@@ -1964,12 +1997,14 @@ export function listSessionsFromStore(params: {
|
||||
const includeLastMessage = opts.includeLastMessage === true;
|
||||
const hasSpawnedByFilter = typeof opts.spawnedBy === "string" && opts.spawnedBy.length > 0;
|
||||
|
||||
const entries = filterAndSortSessionEntries({
|
||||
const selection = selectSessionEntries({
|
||||
store,
|
||||
opts,
|
||||
now,
|
||||
rowContext: hasSpawnedByFilter ? getRowContext() : undefined,
|
||||
defaultLimit: SESSIONS_LIST_DEFAULT_LIMIT,
|
||||
});
|
||||
const { entries, totalCount, limitApplied } = selection;
|
||||
|
||||
const sessions = entries.map(([key, entry], index) => {
|
||||
const includeTranscriptFields = index < sessionListTranscriptFieldRows;
|
||||
@@ -1993,6 +2028,9 @@ export function listSessionsFromStore(params: {
|
||||
ts: now,
|
||||
path: storePath,
|
||||
count: sessions.length,
|
||||
totalCount,
|
||||
limitApplied,
|
||||
hasMore: sessions.length < totalCount,
|
||||
defaults: getSessionDefaults(cfg, params.modelCatalog),
|
||||
sessions,
|
||||
};
|
||||
@@ -2028,12 +2066,14 @@ export async function listSessionsFromStoreAsync(params: {
|
||||
const includeLastMessage = opts.includeLastMessage === true;
|
||||
const hasSpawnedByFilter = typeof opts.spawnedBy === "string" && opts.spawnedBy.length > 0;
|
||||
|
||||
const entries = filterAndSortSessionEntries({
|
||||
const selection = selectSessionEntries({
|
||||
store,
|
||||
opts,
|
||||
now,
|
||||
rowContext: hasSpawnedByFilter ? getRowContext() : undefined,
|
||||
defaultLimit: SESSIONS_LIST_DEFAULT_LIMIT,
|
||||
});
|
||||
const { entries, totalCount, limitApplied } = selection;
|
||||
|
||||
const sessions: GatewaySessionRow[] = [];
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
@@ -2089,6 +2129,9 @@ export async function listSessionsFromStoreAsync(params: {
|
||||
ts: now,
|
||||
path: storePath,
|
||||
count: sessions.length,
|
||||
totalCount,
|
||||
limitApplied,
|
||||
hasMore: sessions.length < totalCount,
|
||||
defaults: getSessionDefaults(cfg, params.modelCatalog),
|
||||
sessions,
|
||||
};
|
||||
|
||||
@@ -30,6 +30,9 @@ export type SessionsListResultBase<TDefaults, TRow> = {
|
||||
ts: number;
|
||||
path: string;
|
||||
count: number;
|
||||
totalCount?: number;
|
||||
limitApplied?: number;
|
||||
hasMore?: boolean;
|
||||
defaults: TDefaults;
|
||||
sessions: TRow[];
|
||||
};
|
||||
|
||||
@@ -25,6 +25,9 @@ export type TuiSessionList = {
|
||||
ts: number;
|
||||
path: string;
|
||||
count: number;
|
||||
totalCount?: number;
|
||||
limitApplied?: number;
|
||||
hasMore?: boolean;
|
||||
defaults?: {
|
||||
model?: string | null;
|
||||
modelProvider?: string | null;
|
||||
|
||||
Reference in New Issue
Block a user