openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
344 lines (343 loc) • 14.3 kB
JavaScript
import { c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { b as parseStrictPositiveInteger } from "./number-coercion-CJQ8TR--.js";
import "./parse-finite-number-Z7n6tXLk.js";
import { n as isRich, r as theme } from "./theme-vjDs9tao.js";
import { t as createLazyImportLoader } from "./lazy-promise-BONnzNfb.js";
import { r as writeRuntimeJson } from "./runtime-B4lgFmsS.js";
import { c as parseAgentSessionKey, n as isAcpSessionKey } from "./session-key-utils-Bx3apsJ3.js";
import { n as info } from "./globals-GTrXU4s9.js";
import { i as getRuntimeConfig } from "./io-Gi7-pyU-.js";
import "./defaults-mDjiWzE5.js";
import "./config-C9RxTsn1.js";
import { a as resolveStoredSessionKeyForAgentStore } from "./session-store-key-BsydjA1h.js";
import { t as loadSessionStore } from "./store-load-C4uq6Lrq.js";
import { t as normalizeChatType } from "./chat-type-CxEvM45e.js";
import { u as resolveSessionTotalTokens } from "./types-D8S_uNvu.js";
import "./sessions-D6zZvoxX.js";
import { i as readAcpSessionMetaForEntry } from "./session-meta-BuCj-TpW.js";
import { t as resolveModelAgentRuntimeMetadata } from "./agent-runtime-metadata-BIkYs5im.js";
import { t as classifySessionKind } from "./classify-session-kind-eHlwmGkk.js";
import { t as resolveAgentRuntimeLabel } from "./agent-runtime-label-DQQuDVe3.js";
import { t as resolveRuntimePolicySessionKey } from "./runtime-policy-session-key-1G2-vCcs.js";
import { t as resolveSessionStoreTargetsOrExit } from "./session-store-targets-BGF93pOS.js";
import { a as toSessionDisplayRow, i as formatSessionModelCell, l as resolveSessionDisplayModelRef, n as formatSessionFlagsCell, r as formatSessionKeyCell, s as resolveSessionDisplayDefaults, t as formatSessionAgeCell } from "./sessions-table-tqTiOISJ.js";
//#region src/commands/sessions.ts
/**
* Session listing command.
*
* It loads one or more agent session stores, enriches rows with model/runtime
* metadata, and emits JSON or fixed-width terminal tables.
*/
const AGENT_PAD = 10;
const KIND_PAD = 11;
const RUNTIME_PAD = 18;
const TOKENS_PAD = 20;
const DEFAULT_SESSIONS_LIMIT = 100;
const TOP_N_SELECTION_LIMIT = 200;
const contextLookupRuntimeLoader = createLazyImportLoader(() => import("./context-C0bALrvk.js"));
const formatKTokens = (value) => `${(value / 1e3).toFixed(value >= 1e4 ? 0 : 1)}k`;
/**
* Inline ACP model overlay — catalog #20.
*
* When a session ran via the ACP control plane (e.g. key =
* `agent:copilot:acp:<uuid>` AND ACP metadata is persisted), the agent's
* configured model is irrelevant: the actual model is selected inside the ACP
* child process. We overlay a sentinel `{ provider: "acpx",
* model: "<agentId>-acp" }` so the listing clearly signals "ACP runtime" and
* does not mislead operators into thinking the configured model ran.
*
* Key-shape alone is not sufficient: ACP bridge sessions (translator.ts) also
* use ACP-shaped keys but never persist `SessionAcpMeta` — they run the
* normal configured model and must not receive the sentinel. The `acpRuntime`
* flag is set at row-construction time from SQLite metadata.
*
* The resolver (`resolveSessionDisplayModelRef`) stays pure; this overlay
* applies only at the emit sites in this file.
*
* NOTE: Will be replaced by a shared `applyAcpModelOverlay` helper from
* `src/agents/acp-runtime-overlay.ts` once PR 2 lands.
*/
function applyAcpModelOverlayIfNeeded(modelRef, sessionKey, acpRuntime) {
if (!acpRuntime || !isAcpSessionKey(sessionKey)) return modelRef;
return {
provider: "acpx",
model: `${parseAgentSessionKey(sessionKey)?.agentId ?? "acp"}-acp`
};
}
function compareSessionRowsByUpdatedAt(a, b) {
return (b.updatedAt ?? 0) - (a.updatedAt ?? 0);
}
function selectNewestSessionRows(rows, limit) {
if (limit === void 0) return rows.toSorted(compareSessionRowsByUpdatedAt);
if (limit > TOP_N_SELECTION_LIMIT) return rows.toSorted(compareSessionRowsByUpdatedAt).slice(0, limit);
const selected = [];
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) {
if (value === void 0) return DEFAULT_SESSIONS_LIMIT;
if (typeof value === "string") {
const trimmed = value.trim();
if (trimmed.toLowerCase() === "all") return;
if (!/^\d+$/.test(trimmed)) return null;
return parseStrictPositiveInteger(trimmed) ?? null;
}
return Number.isInteger(value) && value > 0 ? value : null;
}
const colorByPct = (label, pct, rich) => {
if (!rich || pct === null) return label;
if (pct >= 95) return theme.error(label);
if (pct >= 80) return theme.warn(label);
if (pct >= 60) return theme.success(label);
return theme.muted(label);
};
const formatTokensCell = (total, contextTokens, rich) => {
if (total === void 0) {
const label = `unknown/${contextTokens ? formatKTokens(contextTokens) : "?"} (?%)`;
return rich ? theme.muted(label.padEnd(TOKENS_PAD)) : label.padEnd(TOKENS_PAD);
}
const totalLabel = formatKTokens(total);
const ctxLabel = contextTokens ? formatKTokens(contextTokens) : "?";
const pct = contextTokens ? Math.min(999, Math.round(total / contextTokens * 100)) : null;
return colorByPct(`${totalLabel}/${ctxLabel} (${pct ?? "?"}%)`.padEnd(TOKENS_PAD), pct, rich);
};
async function lookupContextTokensForDisplay(model) {
const { lookupContextTokens } = await contextLookupRuntimeLoader.load();
return lookupContextTokens(model, { allowAsyncLoad: false });
}
const formatKindCell = (kind, rich) => {
const label = kind.padEnd(KIND_PAD);
if (!rich) return label;
if (kind === "group") return theme.accentBright(label);
if (kind === "global") return theme.warn(label);
if (kind === "direct") return theme.accent(label);
return theme.muted(label);
};
function resolveSessionRuntimeLabel(params) {
const id = normalizeOptionalLowercaseString(params.agentRuntime.id);
const resolvedHarness = id && id !== "openclaw" && id !== "auto" ? id : void 0;
return resolveAgentRuntimeLabel({
config: params.cfg,
sessionEntry: params.entry,
resolvedHarness,
fallbackProvider: params.modelProvider
});
}
function formatRuntimeCell(runtimeLabel, rich) {
const label = runtimeLabel.padEnd(RUNTIME_PAD);
return rich ? theme.info(label) : label;
}
function toJsonSessionRow(row) {
const { runtimeLabel, ...jsonRow } = row;
return jsonRow;
}
function stripChannelRecipientPrefix(value, channel) {
const raw = normalizeOptionalString(value);
const normalizedChannel = normalizeOptionalLowercaseString(channel);
if (!raw || !normalizedChannel) return raw;
const prefix = `${normalizedChannel}:`;
if (!raw.toLowerCase().startsWith(prefix)) return raw;
const stripped = raw.slice(prefix.length);
const topicMarkerIndex = stripped.toLowerCase().indexOf(":topic:");
return topicMarkerIndex >= 0 ? stripped.slice(0, topicMarkerIndex) : stripped;
}
function resolveDisplayRuntimePolicySessionKey(params) {
const { cfg, entry, key } = params;
const origin = entry.origin;
const deliveryContext = entry.deliveryContext;
const chatType = normalizeChatType(origin?.chatType ?? entry.chatType);
if (chatType !== "direct") return;
const channel = normalizeOptionalString(origin?.provider ?? deliveryContext?.channel ?? entry.lastChannel ?? entry.channel ?? origin?.surface);
const to = normalizeOptionalString(origin?.to ?? deliveryContext?.to ?? entry.lastTo);
const from = normalizeOptionalString(origin?.from);
const nativeDirectUserId = normalizeOptionalString(origin?.nativeDirectUserId);
const peerId = nativeDirectUserId ?? stripChannelRecipientPrefix(to, channel) ?? stripChannelRecipientPrefix(from, channel);
const runtimePolicySessionKey = resolveRuntimePolicySessionKey({
cfg,
sessionKey: key,
ctx: {
SessionKey: key,
Provider: channel,
Surface: normalizeOptionalString(origin?.surface),
AccountId: normalizeOptionalString(origin?.accountId ?? deliveryContext?.accountId ?? entry.lastAccountId),
ChatType: chatType,
NativeDirectUserId: nativeDirectUserId,
SenderId: peerId,
OriginatingTo: to,
From: from,
To: to
}
});
return runtimePolicySessionKey && runtimePolicySessionKey !== key ? runtimePolicySessionKey : void 0;
}
/** Lists sessions across selected stores with optional JSON output. */
async function sessionsCommand(opts, runtime) {
const aggregateAgents = opts.allAgents === true;
const cfg = getRuntimeConfig();
const displayDefaults = resolveSessionDisplayDefaults(cfg);
const configuredContextTokens = cfg.agents?.defaults?.contextTokens;
const configContextTokens = configuredContextTokens ?? await lookupContextTokensForDisplay(displayDefaults.model) ?? 2e5;
const targets = resolveSessionStoreTargetsOrExit({
cfg,
opts: {
store: opts.store,
agent: opts.agent,
allAgents: opts.allAgents
},
runtime
});
if (!targets) return;
let activeMinutes;
if (opts.active !== void 0) {
const parsed = parseStrictPositiveInteger(opts.active);
if (parsed === void 0) {
runtime.error("--active must be a positive number of minutes, for example --active 30.");
runtime.exit(1);
return;
}
activeMinutes = parsed;
}
const limit = parseSessionsLimit(opts.limit);
if (limit === null) {
runtime.error("--limit must be a positive integer or \"all\", for example --limit 25.");
runtime.exit(1);
return;
}
const allRows = targets.flatMap((target) => {
const store = loadSessionStore(target.storePath);
return Object.entries(store).filter(([, entry]) => {
if (activeMinutes === void 0) return true;
const updatedAt = entry?.updatedAt;
return typeof updatedAt === "number" && Date.now() - updatedAt <= activeMinutes * 6e4;
}).map(([key, entry]) => {
const row = toSessionDisplayRow(key, entry);
const agentId = parseAgentSessionKey(row.key)?.agentId ?? target.agentId;
const acpSessionKey = resolveStoredSessionKeyForAgentStore({
cfg,
agentId,
sessionKey: row.key
});
const acpMeta = readAcpSessionMetaForEntry({
sessionKey: acpSessionKey,
entry
});
const acpRuntime = acpMeta != null;
const modelRef = applyAcpModelOverlayIfNeeded(resolveSessionDisplayModelRef(cfg, row), acpSessionKey, acpRuntime);
const agentRuntime = resolveModelAgentRuntimeMetadata({
cfg,
agentId,
provider: modelRef.provider,
model: modelRef.model,
sessionKey: acpSessionKey,
acpRuntime,
acpBackend: acpMeta?.backend
});
return Object.assign({}, row, {
agentId,
acpRuntime,
agentRuntime,
kind: classifySessionKind(row.key, store[row.key]),
runtimePolicySessionKey: resolveDisplayRuntimePolicySessionKey({
cfg,
key: row.key,
entry
}),
runtimeLabel: resolveSessionRuntimeLabel({
cfg,
entry,
agentRuntime,
modelProvider: modelRef.provider,
model: modelRef.model,
agentId,
sessionKey: row.key
})
});
});
});
const totalCount = allRows.length;
const rows = selectNewestSessionRows(allRows, limit);
const hasMore = rows.length < totalCount;
if (opts.json) {
const multi = targets.length > 1;
const aggregate = aggregateAgents || multi;
writeRuntimeJson(runtime, {
path: aggregate ? null : targets[0]?.storePath ?? null,
stores: aggregate ? targets.map((target) => ({
agentId: target.agentId,
path: target.storePath
})) : void 0,
allAgents: aggregateAgents ? true : void 0,
count: rows.length,
totalCount,
limitApplied: limit ?? null,
hasMore,
activeMinutes: activeMinutes ?? null,
sessions: await Promise.all(rows.map(async (row) => {
const r = toJsonSessionRow(row);
const modelRef = applyAcpModelOverlayIfNeeded(resolveSessionDisplayModelRef(cfg, r), resolveStoredSessionKeyForAgentStore({
cfg,
agentId: row.agentId,
sessionKey: r.key
}), row.acpRuntime);
return {
...r,
totalTokens: resolveSessionTotalTokens(r) ?? null,
totalTokensFresh: typeof r.totalTokens === "number" ? r.totalTokensFresh !== false : false,
contextTokens: r.contextTokens ?? configuredContextTokens ?? await lookupContextTokensForDisplay(modelRef.model) ?? configContextTokens ?? null,
modelProvider: modelRef.provider,
model: modelRef.model
};
}))
});
return;
}
if (targets.length === 1 && !aggregateAgents) runtime.log(info(`Session store: ${targets[0]?.storePath}`));
else runtime.log(info(`Session stores: ${targets.length} (${targets.map((t) => t.agentId).join(", ")})`));
runtime.log(info(hasMore && limit !== void 0 ? `Sessions listed: ${rows.length} of ${totalCount} (limit ${limit})` : `Sessions listed: ${rows.length}`));
if (activeMinutes) runtime.log(info(`Filtered to last ${activeMinutes} minute(s)`));
if (rows.length === 0) {
runtime.log("No sessions found.");
return;
}
const rich = isRich();
const showAgentColumn = aggregateAgents || targets.length > 1;
const header = [
...showAgentColumn ? ["Agent".padEnd(AGENT_PAD)] : [],
"Kind".padEnd(KIND_PAD),
"Key".padEnd(26),
"Age".padEnd(9),
"Model".padEnd(14),
"Runtime".padEnd(RUNTIME_PAD),
"Tokens (ctx %)".padEnd(TOKENS_PAD),
"Flags"
].join(" ");
runtime.log(rich ? theme.heading(header) : header);
for (const row of rows) {
const model = applyAcpModelOverlayIfNeeded(resolveSessionDisplayModelRef(cfg, row), resolveStoredSessionKeyForAgentStore({
cfg,
agentId: row.agentId,
sessionKey: row.key
}), row.acpRuntime).model;
const contextTokens = row.contextTokens ?? configuredContextTokens ?? await lookupContextTokensForDisplay(model) ?? configContextTokens;
const total = resolveSessionTotalTokens(row);
const line = [
...showAgentColumn ? [rich ? theme.accentBright(row.agentId.padEnd(AGENT_PAD)) : row.agentId.padEnd(AGENT_PAD)] : [],
formatKindCell(row.kind, rich),
formatSessionKeyCell(row.key, rich),
formatSessionAgeCell(row.updatedAt, rich),
formatSessionModelCell(model, rich),
formatRuntimeCell(row.runtimeLabel, rich),
formatTokensCell(total, contextTokens ?? null, rich),
formatSessionFlagsCell(row, rich)
].join(" ");
runtime.log(line.trimEnd());
}
}
//#endregion
export { sessionsCommand };