mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:30:42 +00:00
fix(cli): bound sessions list output
This commit is contained in:
@@ -58,6 +58,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Active Memory: give timeout partial transcript recovery enough abort-settle headroom so temporary recall summaries are returned before cleanup. Thanks @vincentkoc.
|
||||
- Gateway/chat: clear the active reply-run guard before draining queued same-session follow-up turns, so sequential `chat.send` calls no longer trip `ReplyRunAlreadyActiveError` every other request. Fixes #77485. Thanks @bws14email.
|
||||
- Agents/media: avoid sending generated image, video, and music attachments twice when streamed reply text arrives before the final `MEDIA:` directive.
|
||||
- CLI/sessions: cap `openclaw sessions` output to the newest 100 rows by default and add `--limit <n|all>` plus JSON pagination metadata, so repeated machine polling of large session stores cannot fan out into unbounded per-row enrichment/output work. Fixes #77500. Thanks @Kaotic3.
|
||||
- Doctor/config: restore legacy group chat config migrations for `routing.allowFrom`, `routing.groupChat.*`, and `channels.telegram.requireMention` so upgrades keep WhatsApp, Telegram, and iMessage group mention gates and history settings instead of leaving configs invalid or silently blocked. Thanks @scoootscooob.
|
||||
- CLI/update: make package-update follow-up processes write completion results and exit explicitly, so Windows packaged upgrades do not hang after the new package finishes post-core plugin work. Thanks @vincentkoc.
|
||||
- Release validation: skip Slack live QA unless Slack credentials are explicitly configured, so release gates can keep proving non-Slack surfaces while Slack is still local and credential-gated. Thanks @vincentkoc.
|
||||
|
||||
@@ -16,17 +16,19 @@ 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.
|
||||
`openclaw sessions` and Gateway `sessions.list` responses are bounded by
|
||||
default so large long-lived stores cannot monopolize the CLI process or Gateway
|
||||
event loop. The CLI returns the newest 100 sessions by default; pass
|
||||
`--limit <n>` for a smaller/larger window or `--limit all` when you intentionally
|
||||
need the full store. JSON responses include `totalCount`, `limitApplied`, and
|
||||
`hasMore` when callers need to show that more rows exist.
|
||||
|
||||
```bash
|
||||
openclaw sessions
|
||||
openclaw sessions --agent work
|
||||
openclaw sessions --all-agents
|
||||
openclaw sessions --active 120
|
||||
openclaw sessions --limit 25
|
||||
openclaw sessions --verbose
|
||||
openclaw sessions --json
|
||||
```
|
||||
@@ -38,6 +40,7 @@ Scope selection:
|
||||
- `--agent <id>`: one configured agent store
|
||||
- `--all-agents`: aggregate all configured agent stores
|
||||
- `--store <path>`: explicit store path (cannot be combined with `--agent` or `--all-agents`)
|
||||
- `--limit <n|all>`: max rows to output (default `100`; `all` restores full output)
|
||||
|
||||
Export a trajectory bundle for a stored session:
|
||||
|
||||
@@ -69,6 +72,9 @@ JSON examples:
|
||||
],
|
||||
"allAgents": true,
|
||||
"count": 2,
|
||||
"totalCount": 2,
|
||||
"limitApplied": 100,
|
||||
"hasMore": false,
|
||||
"activeMinutes": null,
|
||||
"sessions": [
|
||||
{ "agentId": "main", "key": "agent:main:main", "model": "gpt-5" },
|
||||
|
||||
@@ -191,6 +191,8 @@ describe("registerStatusHealthSessionsCommands", () => {
|
||||
"/tmp/sessions.json",
|
||||
"--active",
|
||||
"120",
|
||||
"--limit",
|
||||
"25",
|
||||
]);
|
||||
|
||||
expect(setVerbose).toHaveBeenCalledWith(true);
|
||||
@@ -199,6 +201,7 @@ describe("registerStatusHealthSessionsCommands", () => {
|
||||
json: true,
|
||||
store: "/tmp/sessions.json",
|
||||
active: "120",
|
||||
limit: "25",
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
|
||||
@@ -132,6 +132,7 @@ export function registerStatusHealthSessionsCommands(program: Command) {
|
||||
.option("--agent <id>", "Agent id to inspect (default: configured default agent)")
|
||||
.option("--all-agents", "Aggregate sessions across all configured agents", false)
|
||||
.option("--active <minutes>", "Only show sessions updated within the past N minutes")
|
||||
.option("--limit <count>", 'Max sessions to show (default: 100; use "all" for full output)')
|
||||
.addHelpText(
|
||||
"after",
|
||||
() =>
|
||||
@@ -140,6 +141,7 @@ export function registerStatusHealthSessionsCommands(program: Command) {
|
||||
["openclaw sessions --agent work", "List sessions for one agent."],
|
||||
["openclaw sessions --all-agents", "Aggregate sessions across agents."],
|
||||
["openclaw sessions --active 120", "Only last 2 hours."],
|
||||
["openclaw sessions --limit 25", "Show the newest 25 sessions."],
|
||||
["openclaw sessions --json", "Machine-readable output."],
|
||||
["openclaw sessions --store ./tmp/sessions.json", "Use a specific session store."],
|
||||
])}\n\n${theme.muted(
|
||||
@@ -160,6 +162,7 @@ export function registerStatusHealthSessionsCommands(program: Command) {
|
||||
agent: opts.agent as string | undefined,
|
||||
allAgents: Boolean(opts.allAgents),
|
||||
active: opts.active as string | undefined,
|
||||
limit: opts.limit as string | undefined,
|
||||
},
|
||||
defaultRuntime,
|
||||
);
|
||||
|
||||
@@ -96,6 +96,8 @@ describe("route-args", () => {
|
||||
"sqlite",
|
||||
"--active",
|
||||
"true",
|
||||
"--limit",
|
||||
"25",
|
||||
]),
|
||||
).toEqual({
|
||||
json: true,
|
||||
@@ -103,8 +105,10 @@ describe("route-args", () => {
|
||||
agent: "default",
|
||||
store: "sqlite",
|
||||
active: "true",
|
||||
limit: "25",
|
||||
});
|
||||
expect(parseSessionsRouteArgs(["node", "openclaw", "sessions", "--agent"])).toBeNull();
|
||||
expect(parseSessionsRouteArgs(["node", "openclaw", "sessions", "--limit"])).toBeNull();
|
||||
expect(
|
||||
parseAgentsListRouteArgs(["node", "openclaw", "agents", "list", "--json", "--bindings"]),
|
||||
).toEqual({
|
||||
|
||||
@@ -144,12 +144,17 @@ export function parseSessionsRouteArgs(argv: string[]) {
|
||||
if (!active.ok) {
|
||||
return null;
|
||||
}
|
||||
const limit = parseOptionalFlagValue(argv, "--limit");
|
||||
if (!limit.ok) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
json: hasFlag(argv, "--json"),
|
||||
allAgents: hasFlag(argv, "--all-agents"),
|
||||
agent: agent.value,
|
||||
store: store.value,
|
||||
active: active.value,
|
||||
limit: limit.value,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -31,35 +31,37 @@ export const SESSION_KEY_PAD = 26;
|
||||
export const SESSION_AGE_PAD = 9;
|
||||
export const SESSION_MODEL_PAD = 14;
|
||||
|
||||
export function toSessionDisplayRow(key: string, entry: SessionEntry): SessionDisplayRow {
|
||||
const updatedAt = entry?.updatedAt ?? null;
|
||||
return {
|
||||
key,
|
||||
updatedAt,
|
||||
ageMs: updatedAt ? Date.now() - updatedAt : null,
|
||||
sessionId: entry?.sessionId,
|
||||
systemSent: entry?.systemSent,
|
||||
abortedLastRun: entry?.abortedLastRun,
|
||||
thinkingLevel: entry?.thinkingLevel,
|
||||
verboseLevel: entry?.verboseLevel,
|
||||
traceLevel: entry?.traceLevel,
|
||||
reasoningLevel: entry?.reasoningLevel,
|
||||
elevatedLevel: entry?.elevatedLevel,
|
||||
responseUsage: entry?.responseUsage,
|
||||
groupActivation: entry?.groupActivation,
|
||||
inputTokens: entry?.inputTokens,
|
||||
outputTokens: entry?.outputTokens,
|
||||
totalTokens: entry?.totalTokens,
|
||||
totalTokensFresh: entry?.totalTokensFresh,
|
||||
model: entry?.model,
|
||||
modelProvider: entry?.modelProvider,
|
||||
providerOverride: entry?.providerOverride,
|
||||
modelOverride: entry?.modelOverride,
|
||||
contextTokens: entry?.contextTokens,
|
||||
};
|
||||
}
|
||||
|
||||
export function toSessionDisplayRows(store: Record<string, SessionEntry>): SessionDisplayRow[] {
|
||||
return Object.entries(store)
|
||||
.map(([key, entry]) => {
|
||||
const updatedAt = entry?.updatedAt ?? null;
|
||||
return {
|
||||
key,
|
||||
updatedAt,
|
||||
ageMs: updatedAt ? Date.now() - updatedAt : null,
|
||||
sessionId: entry?.sessionId,
|
||||
systemSent: entry?.systemSent,
|
||||
abortedLastRun: entry?.abortedLastRun,
|
||||
thinkingLevel: entry?.thinkingLevel,
|
||||
verboseLevel: entry?.verboseLevel,
|
||||
traceLevel: entry?.traceLevel,
|
||||
reasoningLevel: entry?.reasoningLevel,
|
||||
elevatedLevel: entry?.elevatedLevel,
|
||||
responseUsage: entry?.responseUsage,
|
||||
groupActivation: entry?.groupActivation,
|
||||
inputTokens: entry?.inputTokens,
|
||||
outputTokens: entry?.outputTokens,
|
||||
totalTokens: entry?.totalTokens,
|
||||
totalTokensFresh: entry?.totalTokensFresh,
|
||||
model: entry?.model,
|
||||
modelProvider: entry?.modelProvider,
|
||||
providerOverride: entry?.providerOverride,
|
||||
modelOverride: entry?.modelOverride,
|
||||
contextTokens: entry?.contextTokens,
|
||||
} satisfies SessionDisplayRow;
|
||||
})
|
||||
.map(([key, entry]) => toSessionDisplayRow(key, entry))
|
||||
.toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
||||
}
|
||||
|
||||
|
||||
@@ -73,12 +73,13 @@ export function writeStore(data: unknown, prefix = "sessions"): string {
|
||||
|
||||
export async function runSessionsJson<T>(
|
||||
run: (
|
||||
opts: { json?: boolean; store?: string; active?: string },
|
||||
opts: { json?: boolean; store?: string; active?: string; limit?: string | number },
|
||||
runtime: RuntimeEnv,
|
||||
) => Promise<void>,
|
||||
store: string,
|
||||
options?: {
|
||||
active?: string;
|
||||
limit?: string | number;
|
||||
},
|
||||
): Promise<T> {
|
||||
const { runtime, logs } = makeRuntime();
|
||||
@@ -88,6 +89,7 @@ export async function runSessionsJson<T>(
|
||||
store,
|
||||
json: true,
|
||||
active: options?.active,
|
||||
limit: options?.limit,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
@@ -154,6 +154,106 @@ describe("sessionsCommand", () => {
|
||||
expect(payload.sessions?.map((row) => row.key)).toEqual(["recent"]);
|
||||
});
|
||||
|
||||
it("limits JSON output to the newest 100 sessions by default", async () => {
|
||||
const entries: Record<string, { sessionId: string; updatedAt: number; model: string }> = {};
|
||||
for (let i = 0; i < 105; i += 1) {
|
||||
entries[`session-${String(i).padStart(3, "0")}`] = {
|
||||
sessionId: `session-${i}`,
|
||||
updatedAt: Date.now() - i * 60_000,
|
||||
model: "pi:opus",
|
||||
};
|
||||
}
|
||||
const store = writeStore(entries, "sessions-default-limit");
|
||||
|
||||
const payload = await runSessionsJson<{
|
||||
count?: number;
|
||||
totalCount?: number;
|
||||
limitApplied?: number | null;
|
||||
hasMore?: boolean;
|
||||
sessions?: Array<{ key: string }>;
|
||||
}>(sessionsCommand, store);
|
||||
|
||||
expect(payload.count).toBe(100);
|
||||
expect(payload.totalCount).toBe(105);
|
||||
expect(payload.limitApplied).toBe(100);
|
||||
expect(payload.hasMore).toBe(true);
|
||||
expect(payload.sessions?.at(0)?.key).toBe("session-000");
|
||||
expect(payload.sessions?.some((row) => row.key === "session-104")).toBe(false);
|
||||
});
|
||||
|
||||
it("honors explicit JSON output limits", async () => {
|
||||
const store = writeStore(
|
||||
{
|
||||
newest: { sessionId: "newest", updatedAt: Date.now(), model: "pi:opus" },
|
||||
middle: { sessionId: "middle", updatedAt: Date.now() - 60_000, model: "pi:opus" },
|
||||
oldest: { sessionId: "oldest", updatedAt: Date.now() - 120_000, model: "pi:opus" },
|
||||
},
|
||||
"sessions-explicit-limit",
|
||||
);
|
||||
|
||||
const payload = await runSessionsJson<{
|
||||
count?: number;
|
||||
totalCount?: number;
|
||||
limitApplied?: number | null;
|
||||
hasMore?: boolean;
|
||||
sessions?: Array<{ key: string }>;
|
||||
}>(sessionsCommand, store, { limit: "2" });
|
||||
|
||||
expect(payload.count).toBe(2);
|
||||
expect(payload.totalCount).toBe(3);
|
||||
expect(payload.limitApplied).toBe(2);
|
||||
expect(payload.hasMore).toBe(true);
|
||||
expect(payload.sessions?.map((row) => row.key)).toEqual(["newest", "middle"]);
|
||||
});
|
||||
|
||||
it("allows full JSON output with --limit all", async () => {
|
||||
const store = writeStore(
|
||||
{
|
||||
newest: { sessionId: "newest", updatedAt: Date.now(), model: "pi:opus" },
|
||||
oldest: { sessionId: "oldest", updatedAt: Date.now() - 120_000, model: "pi:opus" },
|
||||
},
|
||||
"sessions-limit-all",
|
||||
);
|
||||
|
||||
const payload = await runSessionsJson<{
|
||||
count?: number;
|
||||
totalCount?: number;
|
||||
limitApplied?: number | null;
|
||||
hasMore?: boolean;
|
||||
sessions?: Array<{ key: string }>;
|
||||
}>(sessionsCommand, store, { limit: "all" });
|
||||
|
||||
expect(payload.count).toBe(2);
|
||||
expect(payload.totalCount).toBe(2);
|
||||
expect(payload.limitApplied).toBeNull();
|
||||
expect(payload.hasMore).toBe(false);
|
||||
expect(payload.sessions?.map((row) => row.key)).toEqual(["newest", "oldest"]);
|
||||
});
|
||||
|
||||
it("sorts and slices large explicit limits instead of using top-N insertion", async () => {
|
||||
const store = writeStore(
|
||||
{
|
||||
newest: { sessionId: "newest", updatedAt: Date.now(), model: "pi:opus" },
|
||||
oldest: { sessionId: "oldest", updatedAt: Date.now() - 120_000, model: "pi:opus" },
|
||||
},
|
||||
"sessions-large-limit",
|
||||
);
|
||||
|
||||
const payload = await runSessionsJson<{
|
||||
count?: number;
|
||||
totalCount?: number;
|
||||
limitApplied?: number | null;
|
||||
hasMore?: boolean;
|
||||
sessions?: Array<{ key: string }>;
|
||||
}>(sessionsCommand, store, { limit: "100000" });
|
||||
|
||||
expect(payload.count).toBe(2);
|
||||
expect(payload.totalCount).toBe(2);
|
||||
expect(payload.limitApplied).toBe(100000);
|
||||
expect(payload.hasMore).toBe(false);
|
||||
expect(payload.sessions?.map((row) => row.key)).toEqual(["newest", "oldest"]);
|
||||
});
|
||||
|
||||
it("rejects invalid --active values", async () => {
|
||||
const store = writeStore(
|
||||
{
|
||||
@@ -171,4 +271,22 @@ describe("sessionsCommand", () => {
|
||||
|
||||
fs.rmSync(store);
|
||||
});
|
||||
|
||||
it("rejects invalid --limit values", async () => {
|
||||
const store = writeStore(
|
||||
{
|
||||
demo: {
|
||||
sessionId: "demo",
|
||||
updatedAt: Date.now() - 5 * 60_000,
|
||||
},
|
||||
},
|
||||
"sessions-limit-invalid",
|
||||
);
|
||||
const { runtime, errors } = makeRuntime();
|
||||
|
||||
await expect(sessionsCommand({ store, limit: "0" }, runtime)).rejects.toThrow("exit 1");
|
||||
expect(errors[0]).toContain('--limit must be a positive integer or "all"');
|
||||
|
||||
fs.rmSync(store);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
SESSION_KEY_PAD,
|
||||
SESSION_MODEL_PAD,
|
||||
type SessionDisplayRow,
|
||||
toSessionDisplayRows,
|
||||
toSessionDisplayRow,
|
||||
} from "./sessions-table.js";
|
||||
|
||||
type SessionRow = SessionDisplayRow & {
|
||||
@@ -35,10 +35,58 @@ type SessionRow = SessionDisplayRow & {
|
||||
const AGENT_PAD = 10;
|
||||
const KIND_PAD = 6;
|
||||
const TOKENS_PAD = 20;
|
||||
const DEFAULT_SESSIONS_LIMIT = 100;
|
||||
const TOP_N_SELECTION_LIMIT = 200;
|
||||
const contextLookupRuntimeLoader = createLazyImportLoader(() => import("../agents/context.js"));
|
||||
|
||||
const formatKTokens = (value: number) => `${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k`;
|
||||
|
||||
function compareSessionRowsByUpdatedAt(a: SessionRow, b: SessionRow): number {
|
||||
return (b.updatedAt ?? 0) - (a.updatedAt ?? 0);
|
||||
}
|
||||
|
||||
function selectNewestSessionRows(rows: SessionRow[], limit: number | undefined): SessionRow[] {
|
||||
if (limit === undefined) {
|
||||
return rows.toSorted(compareSessionRowsByUpdatedAt);
|
||||
}
|
||||
if (limit > TOP_N_SELECTION_LIMIT) {
|
||||
return rows.toSorted(compareSessionRowsByUpdatedAt).slice(0, limit);
|
||||
}
|
||||
const selected: SessionRow[] = [];
|
||||
for (const row of rows) {
|
||||
const insertAt = selected.findIndex(
|
||||
(candidate) => compareSessionRowsByUpdatedAt(row, candidate) < 0,
|
||||
);
|
||||
if (insertAt >= 0) {
|
||||
selected.splice(insertAt, 0, row);
|
||||
if (selected.length > limit) {
|
||||
selected.pop();
|
||||
}
|
||||
} else if (selected.length < limit) {
|
||||
selected.push(row);
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
function parseSessionsLimit(value: string | number | undefined): number | undefined | null {
|
||||
if (value === undefined) {
|
||||
return DEFAULT_SESSIONS_LIMIT;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.toLowerCase() === "all") {
|
||||
return undefined;
|
||||
}
|
||||
if (!/^\d+$/.test(trimmed)) {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number.parseInt(trimmed, 10);
|
||||
return parsed > 0 ? parsed : null;
|
||||
}
|
||||
return Number.isInteger(value) && value > 0 ? value : null;
|
||||
}
|
||||
|
||||
const colorByPct = (label: string, pct: number | null, rich: boolean) => {
|
||||
if (!rich || pct === null) {
|
||||
return label;
|
||||
@@ -115,7 +163,14 @@ const formatKindCell = (kind: SessionRow["kind"], rich: boolean) => {
|
||||
};
|
||||
|
||||
export async function sessionsCommand(
|
||||
opts: { json?: boolean; store?: string; active?: string; agent?: string; allAgents?: boolean },
|
||||
opts: {
|
||||
json?: boolean;
|
||||
store?: string;
|
||||
active?: string;
|
||||
agent?: string;
|
||||
allAgents?: boolean;
|
||||
limit?: string | number;
|
||||
},
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const aggregateAgents = opts.allAgents === true;
|
||||
@@ -150,10 +205,25 @@ export async function sessionsCommand(
|
||||
activeMinutes = parsed;
|
||||
}
|
||||
|
||||
const rows = targets
|
||||
.flatMap((target) => {
|
||||
const store = loadSessionStore(target.storePath);
|
||||
return toSessionDisplayRows(store).map((row) => {
|
||||
const limit = parseSessionsLimit(opts.limit);
|
||||
if (limit === null) {
|
||||
runtime.error('--limit must be a positive integer or "all"');
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const allRows = targets.flatMap((target) => {
|
||||
const store = loadSessionStore(target.storePath);
|
||||
return Object.entries(store)
|
||||
.filter(([, entry]) => {
|
||||
if (activeMinutes === undefined) {
|
||||
return true;
|
||||
}
|
||||
const updatedAt = entry?.updatedAt;
|
||||
return typeof updatedAt === "number" && Date.now() - updatedAt <= activeMinutes * 60_000;
|
||||
})
|
||||
.map(([key, entry]) => {
|
||||
const row = toSessionDisplayRow(key, entry);
|
||||
const agentId = parseAgentSessionKey(row.key)?.agentId ?? target.agentId;
|
||||
return Object.assign({}, row, {
|
||||
agentId,
|
||||
@@ -161,17 +231,10 @@ export async function sessionsCommand(
|
||||
kind: classifySessionKey(row.key, store[row.key]),
|
||||
});
|
||||
});
|
||||
})
|
||||
.filter((row) => {
|
||||
if (activeMinutes === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (!row.updatedAt) {
|
||||
return false;
|
||||
}
|
||||
return Date.now() - row.updatedAt <= activeMinutes * 60_000;
|
||||
})
|
||||
.toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
||||
});
|
||||
const totalCount = allRows.length;
|
||||
const rows = selectNewestSessionRows(allRows, limit);
|
||||
const hasMore = rows.length < totalCount;
|
||||
|
||||
if (opts.json) {
|
||||
const multi = targets.length > 1;
|
||||
@@ -186,6 +249,9 @@ export async function sessionsCommand(
|
||||
: undefined,
|
||||
allAgents: aggregateAgents ? true : undefined,
|
||||
count: rows.length,
|
||||
totalCount,
|
||||
limitApplied: limit ?? null,
|
||||
hasMore,
|
||||
activeMinutes: activeMinutes ?? null,
|
||||
sessions: await Promise.all(
|
||||
rows.map(async (r) => {
|
||||
@@ -217,7 +283,13 @@ export async function sessionsCommand(
|
||||
info(`Session stores: ${targets.length} (${targets.map((t) => t.agentId).join(", ")})`),
|
||||
);
|
||||
}
|
||||
runtime.log(info(`Sessions listed: ${rows.length}`));
|
||||
runtime.log(
|
||||
info(
|
||||
hasMore && limit !== undefined
|
||||
? `Sessions listed: ${rows.length} of ${totalCount} (limit ${limit})`
|
||||
: `Sessions listed: ${rows.length}`,
|
||||
),
|
||||
);
|
||||
if (activeMinutes) {
|
||||
runtime.log(info(`Filtered to last ${activeMinutes} minute(s)`));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user