mirror of https://github.com/openclaw/openclaw.git
CLI: add per-session cleanup action table for dry-runs
This commit is contained in:
parent
382bdce4b3
commit
ff076fc358
|
|
@ -29,6 +29,7 @@ openclaw sessions cleanup --json
|
|||
`openclaw sessions cleanup` uses `session.maintenance` settings from config:
|
||||
|
||||
- `--dry-run`: preview how many entries would be pruned/capped without writing.
|
||||
- In text mode, dry-run prints a per-session action table (`Action`, `Key`, `Age`, `Model`, `Flags`) so you can see what would be kept vs removed.
|
||||
- `--enforce`: apply maintenance even when `session.maintenance.mode` is `warn`.
|
||||
- `--active-key <key>`: protect a specific active key from disk-budget eviction.
|
||||
- `--store <path>`: run against a specific `sessions.json` file.
|
||||
|
|
|
|||
|
|
@ -61,13 +61,20 @@ describe("sessionsCleanupCommand", () => {
|
|||
maxDiskBytes: null,
|
||||
highWaterBytes: null,
|
||||
});
|
||||
mocks.pruneStaleEntries.mockImplementation((store: Record<string, SessionEntry>) => {
|
||||
if (store.stale) {
|
||||
delete store.stale;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
mocks.pruneStaleEntries.mockImplementation(
|
||||
(
|
||||
store: Record<string, SessionEntry>,
|
||||
_maxAgeMs: number,
|
||||
opts?: { onPruned?: (params: { key: string; entry: SessionEntry }) => void },
|
||||
) => {
|
||||
if (store.stale) {
|
||||
opts?.onPruned?.({ key: "stale", entry: store.stale });
|
||||
delete store.stale;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
);
|
||||
mocks.capEntryCount.mockImplementation(() => 0);
|
||||
mocks.updateSessionStore.mockResolvedValue(undefined);
|
||||
mocks.enforceSessionDiskBudget.mockResolvedValue({
|
||||
|
|
@ -151,4 +158,25 @@ describe("sessionsCleanupCommand", () => {
|
|||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("renders a dry-run action table with keep/prune actions", async () => {
|
||||
mocks.enforceSessionDiskBudget.mockResolvedValue(null);
|
||||
mocks.loadSessionStore.mockReturnValue({
|
||||
stale: { sessionId: "stale", updatedAt: 1, model: "pi:opus" },
|
||||
fresh: { sessionId: "fresh", updatedAt: 2, model: "pi:opus" },
|
||||
});
|
||||
|
||||
const { runtime, logs } = makeRuntime();
|
||||
await sessionsCleanupCommand(
|
||||
{
|
||||
dryRun: true,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(logs.some((line) => line.includes("Planned session actions:"))).toBe(true);
|
||||
expect(logs.some((line) => line.includes("Action") && line.includes("Key"))).toBe(true);
|
||||
expect(logs.some((line) => line.includes("fresh") && line.includes("keep"))).toBe(true);
|
||||
expect(logs.some((line) => line.includes("stale") && line.includes("prune-stale"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,19 @@ import {
|
|||
updateSessionStore,
|
||||
} from "../config/sessions.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { isRich, theme } from "../terminal/theme.js";
|
||||
import {
|
||||
formatSessionAgeCell,
|
||||
formatSessionFlagsCell,
|
||||
formatSessionKeyCell,
|
||||
formatSessionModelCell,
|
||||
resolveSessionDisplayDefaults,
|
||||
resolveSessionDisplayModel,
|
||||
SESSION_AGE_PAD,
|
||||
SESSION_KEY_PAD,
|
||||
SESSION_MODEL_PAD,
|
||||
toSessionDisplayRows,
|
||||
} from "./sessions-table.js";
|
||||
|
||||
export type SessionsCleanupOptions = {
|
||||
store?: string;
|
||||
|
|
@ -19,8 +32,48 @@ export type SessionsCleanupOptions = {
|
|||
json?: boolean;
|
||||
};
|
||||
|
||||
type SessionCleanupAction = "keep" | "prune-stale" | "cap-overflow" | "evict-budget";
|
||||
|
||||
const ACTION_PAD = 12;
|
||||
|
||||
function resolveSessionCleanupAction(params: {
|
||||
key: string;
|
||||
staleKeys: Set<string>;
|
||||
cappedKeys: Set<string>;
|
||||
budgetEvictedKeys: Set<string>;
|
||||
}): SessionCleanupAction {
|
||||
if (params.staleKeys.has(params.key)) {
|
||||
return "prune-stale";
|
||||
}
|
||||
if (params.cappedKeys.has(params.key)) {
|
||||
return "cap-overflow";
|
||||
}
|
||||
if (params.budgetEvictedKeys.has(params.key)) {
|
||||
return "evict-budget";
|
||||
}
|
||||
return "keep";
|
||||
}
|
||||
|
||||
function formatCleanupActionCell(action: SessionCleanupAction, rich: boolean): string {
|
||||
const label = action.padEnd(ACTION_PAD);
|
||||
if (!rich) {
|
||||
return label;
|
||||
}
|
||||
if (action === "keep") {
|
||||
return theme.muted(label);
|
||||
}
|
||||
if (action === "prune-stale") {
|
||||
return theme.warn(label);
|
||||
}
|
||||
if (action === "cap-overflow") {
|
||||
return theme.accentBright(label);
|
||||
}
|
||||
return theme.error(label);
|
||||
}
|
||||
|
||||
export async function sessionsCleanupCommand(opts: SessionsCleanupOptions, runtime: RuntimeEnv) {
|
||||
const cfg = loadConfig();
|
||||
const displayDefaults = resolveSessionDisplayDefaults(cfg);
|
||||
const defaultAgentId = resolveDefaultAgentId(cfg);
|
||||
const storePath = resolveStorePath(opts.store ?? cfg.session?.store, { agentId: defaultAgentId });
|
||||
const maintenance = resolveMaintenanceConfig();
|
||||
|
|
@ -28,8 +81,21 @@ export async function sessionsCleanupCommand(opts: SessionsCleanupOptions, runti
|
|||
|
||||
const beforeStore = loadSessionStore(storePath, { skipCache: true });
|
||||
const previewStore = structuredClone(beforeStore);
|
||||
const pruned = pruneStaleEntries(previewStore, maintenance.pruneAfterMs, { log: false });
|
||||
const capped = capEntryCount(previewStore, maintenance.maxEntries, { log: false });
|
||||
const staleKeys = new Set<string>();
|
||||
const cappedKeys = new Set<string>();
|
||||
const pruned = pruneStaleEntries(previewStore, maintenance.pruneAfterMs, {
|
||||
log: false,
|
||||
onPruned: ({ key }) => {
|
||||
staleKeys.add(key);
|
||||
},
|
||||
});
|
||||
const capped = capEntryCount(previewStore, maintenance.maxEntries, {
|
||||
log: false,
|
||||
onCapped: ({ key }) => {
|
||||
cappedKeys.add(key);
|
||||
},
|
||||
});
|
||||
const beforeBudgetStore = structuredClone(previewStore);
|
||||
const diskBudget = await enforceSessionDiskBudget({
|
||||
store: previewStore,
|
||||
storePath,
|
||||
|
|
@ -38,6 +104,13 @@ export async function sessionsCleanupCommand(opts: SessionsCleanupOptions, runti
|
|||
warnOnly: false,
|
||||
dryRun: true,
|
||||
});
|
||||
const budgetEvictedKeys = new Set<string>();
|
||||
for (const key of Object.keys(beforeBudgetStore)) {
|
||||
if (Object.hasOwn(previewStore, key)) {
|
||||
continue;
|
||||
}
|
||||
budgetEvictedKeys.add(key);
|
||||
}
|
||||
const beforeCount = Object.keys(beforeStore).length;
|
||||
const afterPreviewCount = Object.keys(previewStore).length;
|
||||
const wouldMutate =
|
||||
|
|
@ -57,6 +130,16 @@ export async function sessionsCleanupCommand(opts: SessionsCleanupOptions, runti
|
|||
wouldMutate,
|
||||
};
|
||||
|
||||
const actionRows = toSessionDisplayRows(beforeStore).map((row) => ({
|
||||
row,
|
||||
action: resolveSessionCleanupAction({
|
||||
key: row.key,
|
||||
staleKeys,
|
||||
cappedKeys,
|
||||
budgetEvictedKeys,
|
||||
}),
|
||||
}));
|
||||
|
||||
if (opts.json && opts.dryRun) {
|
||||
runtime.log(JSON.stringify(summary, null, 2));
|
||||
return;
|
||||
|
|
@ -65,7 +148,9 @@ export async function sessionsCleanupCommand(opts: SessionsCleanupOptions, runti
|
|||
if (!opts.json) {
|
||||
runtime.log(`Session store: ${storePath}`);
|
||||
runtime.log(`Maintenance mode: ${effectiveMode}`);
|
||||
runtime.log(`Entries: ${beforeCount} -> ${afterPreviewCount}`);
|
||||
runtime.log(
|
||||
`Entries: ${beforeCount} -> ${afterPreviewCount} (remove ${beforeCount - afterPreviewCount})`,
|
||||
);
|
||||
runtime.log(`Would prune stale: ${pruned}`);
|
||||
runtime.log(`Would cap overflow: ${capped}`);
|
||||
if (diskBudget) {
|
||||
|
|
@ -73,6 +158,30 @@ export async function sessionsCleanupCommand(opts: SessionsCleanupOptions, runti
|
|||
`Would enforce disk budget: ${diskBudget.totalBytesBefore} -> ${diskBudget.totalBytesAfter} bytes (files ${diskBudget.removedFiles}, entries ${diskBudget.removedEntries})`,
|
||||
);
|
||||
}
|
||||
if (opts.dryRun && actionRows.length > 0) {
|
||||
const rich = isRich();
|
||||
runtime.log("");
|
||||
runtime.log("Planned session actions:");
|
||||
const header = [
|
||||
"Action".padEnd(ACTION_PAD),
|
||||
"Key".padEnd(SESSION_KEY_PAD),
|
||||
"Age".padEnd(SESSION_AGE_PAD),
|
||||
"Model".padEnd(SESSION_MODEL_PAD),
|
||||
"Flags",
|
||||
].join(" ");
|
||||
runtime.log(rich ? theme.heading(header) : header);
|
||||
for (const actionRow of actionRows) {
|
||||
const model = resolveSessionDisplayModel(cfg, actionRow.row, displayDefaults);
|
||||
const line = [
|
||||
formatCleanupActionCell(actionRow.action, rich),
|
||||
formatSessionKeyCell(actionRow.row.key, rich),
|
||||
formatSessionAgeCell(actionRow.row.updatedAt, rich),
|
||||
formatSessionModelCell(model, rich),
|
||||
formatSessionFlagsCell(actionRow.row, rich),
|
||||
].join(" ");
|
||||
runtime.log(line.trimEnd());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.dryRun) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveSessionModelRef } from "../gateway/session-utils.js";
|
||||
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
|
||||
import { parseAgentSessionKey } from "../routing/session-key.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
|
||||
export type SessionDisplayRow = {
|
||||
key: string;
|
||||
updatedAt: number | null;
|
||||
ageMs: number | null;
|
||||
sessionId?: string;
|
||||
systemSent?: boolean;
|
||||
abortedLastRun?: boolean;
|
||||
thinkingLevel?: string;
|
||||
verboseLevel?: string;
|
||||
reasoningLevel?: string;
|
||||
elevatedLevel?: string;
|
||||
responseUsage?: string;
|
||||
groupActivation?: string;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
totalTokens?: number;
|
||||
totalTokensFresh?: boolean;
|
||||
model?: string;
|
||||
modelProvider?: string;
|
||||
providerOverride?: string;
|
||||
modelOverride?: string;
|
||||
contextTokens?: number;
|
||||
};
|
||||
|
||||
export type SessionDisplayDefaults = {
|
||||
model: string;
|
||||
};
|
||||
|
||||
export const SESSION_KEY_PAD = 26;
|
||||
export const SESSION_AGE_PAD = 9;
|
||||
export const SESSION_MODEL_PAD = 14;
|
||||
|
||||
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,
|
||||
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;
|
||||
})
|
||||
.toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
||||
}
|
||||
|
||||
export function resolveSessionDisplayDefaults(cfg: OpenClawConfig): SessionDisplayDefaults {
|
||||
const resolved = resolveConfiguredModelRef({
|
||||
cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
return {
|
||||
model: resolved.model ?? DEFAULT_MODEL,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveSessionDisplayModel(
|
||||
cfg: OpenClawConfig,
|
||||
row: Pick<
|
||||
SessionDisplayRow,
|
||||
"key" | "model" | "modelProvider" | "modelOverride" | "providerOverride"
|
||||
>,
|
||||
defaults: SessionDisplayDefaults,
|
||||
): string {
|
||||
const resolved = resolveSessionModelRef(cfg, row, parseAgentSessionKey(row.key)?.agentId);
|
||||
return resolved.model ?? defaults.model;
|
||||
}
|
||||
|
||||
function truncateSessionKey(key: string): string {
|
||||
if (key.length <= SESSION_KEY_PAD) {
|
||||
return key;
|
||||
}
|
||||
const head = Math.max(4, SESSION_KEY_PAD - 10);
|
||||
return `${key.slice(0, head)}...${key.slice(-6)}`;
|
||||
}
|
||||
|
||||
export function formatSessionKeyCell(key: string, rich: boolean): string {
|
||||
const label = truncateSessionKey(key).padEnd(SESSION_KEY_PAD);
|
||||
return rich ? theme.accent(label) : label;
|
||||
}
|
||||
|
||||
export function formatSessionAgeCell(updatedAt: number | null | undefined, rich: boolean): string {
|
||||
const ageLabel = updatedAt ? formatTimeAgo(Date.now() - updatedAt) : "unknown";
|
||||
const padded = ageLabel.padEnd(SESSION_AGE_PAD);
|
||||
return rich ? theme.muted(padded) : padded;
|
||||
}
|
||||
|
||||
export function formatSessionModelCell(model: string | null | undefined, rich: boolean): string {
|
||||
const label = (model ?? "unknown").padEnd(SESSION_MODEL_PAD);
|
||||
return rich ? theme.info(label) : label;
|
||||
}
|
||||
|
||||
export function formatSessionFlagsCell(
|
||||
row: Pick<
|
||||
SessionDisplayRow,
|
||||
| "thinkingLevel"
|
||||
| "verboseLevel"
|
||||
| "reasoningLevel"
|
||||
| "elevatedLevel"
|
||||
| "responseUsage"
|
||||
| "groupActivation"
|
||||
| "systemSent"
|
||||
| "abortedLastRun"
|
||||
| "sessionId"
|
||||
>,
|
||||
rich: boolean,
|
||||
): string {
|
||||
const flags = [
|
||||
row.thinkingLevel ? `think:${row.thinkingLevel}` : null,
|
||||
row.verboseLevel ? `verbose:${row.verboseLevel}` : null,
|
||||
row.reasoningLevel ? `reasoning:${row.reasoningLevel}` : null,
|
||||
row.elevatedLevel ? `elev:${row.elevatedLevel}` : null,
|
||||
row.responseUsage ? `usage:${row.responseUsage}` : null,
|
||||
row.groupActivation ? `activation:${row.groupActivation}` : null,
|
||||
row.systemSent ? "system" : null,
|
||||
row.abortedLastRun ? "aborted" : null,
|
||||
row.sessionId ? `id:${row.sessionId}` : null,
|
||||
].filter(Boolean);
|
||||
const label = flags.join(" ");
|
||||
return label.length === 0 ? "" : rich ? theme.muted(label) : label;
|
||||
}
|
||||
|
|
@ -1,62 +1,39 @@
|
|||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { lookupContextTokens } from "../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveFreshSessionTotalTokens,
|
||||
resolveStorePath,
|
||||
type SessionEntry,
|
||||
} from "../config/sessions.js";
|
||||
import { classifySessionKey, resolveSessionModelRef } from "../gateway/session-utils.js";
|
||||
import { classifySessionKey } from "../gateway/session-utils.js";
|
||||
import { info } from "../globals.js";
|
||||
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
|
||||
import { parseAgentSessionKey } from "../routing/session-key.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { isRich, theme } from "../terminal/theme.js";
|
||||
import {
|
||||
formatSessionAgeCell,
|
||||
formatSessionFlagsCell,
|
||||
formatSessionKeyCell,
|
||||
formatSessionModelCell,
|
||||
resolveSessionDisplayDefaults,
|
||||
resolveSessionDisplayModel,
|
||||
SESSION_AGE_PAD,
|
||||
SESSION_KEY_PAD,
|
||||
SESSION_MODEL_PAD,
|
||||
type SessionDisplayRow,
|
||||
toSessionDisplayRows,
|
||||
} from "./sessions-table.js";
|
||||
|
||||
type SessionRow = {
|
||||
key: string;
|
||||
type SessionRow = SessionDisplayRow & {
|
||||
kind: "direct" | "group" | "global" | "unknown";
|
||||
updatedAt: number | null;
|
||||
ageMs: number | null;
|
||||
sessionId?: string;
|
||||
systemSent?: boolean;
|
||||
abortedLastRun?: boolean;
|
||||
thinkingLevel?: string;
|
||||
verboseLevel?: string;
|
||||
reasoningLevel?: string;
|
||||
elevatedLevel?: string;
|
||||
responseUsage?: string;
|
||||
groupActivation?: string;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
totalTokens?: number;
|
||||
totalTokensFresh?: boolean;
|
||||
model?: string;
|
||||
modelProvider?: string;
|
||||
providerOverride?: string;
|
||||
modelOverride?: string;
|
||||
contextTokens?: number;
|
||||
};
|
||||
|
||||
const KIND_PAD = 6;
|
||||
const KEY_PAD = 26;
|
||||
const AGE_PAD = 9;
|
||||
const MODEL_PAD = 14;
|
||||
const TOKENS_PAD = 20;
|
||||
|
||||
const formatKTokens = (value: number) => `${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k`;
|
||||
|
||||
const truncateKey = (key: string) => {
|
||||
if (key.length <= KEY_PAD) {
|
||||
return key;
|
||||
}
|
||||
const head = Math.max(4, KEY_PAD - 10);
|
||||
return `${key.slice(0, head)}...${key.slice(-6)}`;
|
||||
};
|
||||
|
||||
const colorByPct = (label: string, pct: number | null, rich: boolean) => {
|
||||
if (!rich || pct === null) {
|
||||
return label;
|
||||
|
|
@ -108,80 +85,17 @@ const formatKindCell = (kind: SessionRow["kind"], rich: boolean) => {
|
|||
return theme.muted(label);
|
||||
};
|
||||
|
||||
const formatAgeCell = (updatedAt: number | null | undefined, rich: boolean) => {
|
||||
const ageLabel = updatedAt ? formatTimeAgo(Date.now() - updatedAt) : "unknown";
|
||||
const padded = ageLabel.padEnd(AGE_PAD);
|
||||
return rich ? theme.muted(padded) : padded;
|
||||
};
|
||||
|
||||
const formatModelCell = (model: string | null | undefined, rich: boolean) => {
|
||||
const label = (model ?? "unknown").padEnd(MODEL_PAD);
|
||||
return rich ? theme.info(label) : label;
|
||||
};
|
||||
|
||||
const formatFlagsCell = (row: SessionRow, rich: boolean) => {
|
||||
const flags = [
|
||||
row.thinkingLevel ? `think:${row.thinkingLevel}` : null,
|
||||
row.verboseLevel ? `verbose:${row.verboseLevel}` : null,
|
||||
row.reasoningLevel ? `reasoning:${row.reasoningLevel}` : null,
|
||||
row.elevatedLevel ? `elev:${row.elevatedLevel}` : null,
|
||||
row.responseUsage ? `usage:${row.responseUsage}` : null,
|
||||
row.groupActivation ? `activation:${row.groupActivation}` : null,
|
||||
row.systemSent ? "system" : null,
|
||||
row.abortedLastRun ? "aborted" : null,
|
||||
row.sessionId ? `id:${row.sessionId}` : null,
|
||||
].filter(Boolean);
|
||||
const label = flags.join(" ");
|
||||
return label.length === 0 ? "" : rich ? theme.muted(label) : label;
|
||||
};
|
||||
|
||||
function toRows(store: Record<string, SessionEntry>): SessionRow[] {
|
||||
return Object.entries(store)
|
||||
.map(([key, entry]) => {
|
||||
const updatedAt = entry?.updatedAt ?? null;
|
||||
return {
|
||||
key,
|
||||
kind: classifySessionKey(key, entry),
|
||||
updatedAt,
|
||||
ageMs: updatedAt ? Date.now() - updatedAt : null,
|
||||
sessionId: entry?.sessionId,
|
||||
systemSent: entry?.systemSent,
|
||||
abortedLastRun: entry?.abortedLastRun,
|
||||
thinkingLevel: entry?.thinkingLevel,
|
||||
verboseLevel: entry?.verboseLevel,
|
||||
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 SessionRow;
|
||||
})
|
||||
.toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
||||
}
|
||||
|
||||
export async function sessionsCommand(
|
||||
opts: { json?: boolean; store?: string; active?: string },
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const cfg = loadConfig();
|
||||
const resolved = resolveConfiguredModelRef({
|
||||
cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
const displayDefaults = resolveSessionDisplayDefaults(cfg);
|
||||
const configContextTokens =
|
||||
cfg.agents?.defaults?.contextTokens ??
|
||||
lookupContextTokens(resolved.model) ??
|
||||
lookupContextTokens(displayDefaults.model) ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
const configModel = resolved.model ?? DEFAULT_MODEL;
|
||||
const configModel = displayDefaults.model;
|
||||
const defaultAgentId = resolveDefaultAgentId(cfg);
|
||||
const storePath = resolveStorePath(opts.store ?? cfg.session?.store, { agentId: defaultAgentId });
|
||||
const store = loadSessionStore(storePath);
|
||||
|
|
@ -197,15 +111,20 @@ export async function sessionsCommand(
|
|||
activeMinutes = parsed;
|
||||
}
|
||||
|
||||
const rows = toRows(store).filter((row) => {
|
||||
if (activeMinutes === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (!row.updatedAt) {
|
||||
return false;
|
||||
}
|
||||
return Date.now() - row.updatedAt <= activeMinutes * 60_000;
|
||||
});
|
||||
const rows = toSessionDisplayRows(store)
|
||||
.map((row) => ({
|
||||
...row,
|
||||
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;
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
runtime.log(
|
||||
|
|
@ -215,12 +134,7 @@ export async function sessionsCommand(
|
|||
count: rows.length,
|
||||
activeMinutes: activeMinutes ?? null,
|
||||
sessions: rows.map((r) => {
|
||||
const resolvedModel = resolveSessionModelRef(
|
||||
cfg,
|
||||
r,
|
||||
parseAgentSessionKey(r.key)?.agentId,
|
||||
);
|
||||
const model = resolvedModel.model ?? configModel;
|
||||
const model = resolveSessionDisplayModel(cfg, r, displayDefaults);
|
||||
return {
|
||||
...r,
|
||||
totalTokens: resolveFreshSessionTotalTokens(r) ?? null,
|
||||
|
|
@ -252,9 +166,9 @@ export async function sessionsCommand(
|
|||
const rich = isRich();
|
||||
const header = [
|
||||
"Kind".padEnd(KIND_PAD),
|
||||
"Key".padEnd(KEY_PAD),
|
||||
"Age".padEnd(AGE_PAD),
|
||||
"Model".padEnd(MODEL_PAD),
|
||||
"Key".padEnd(SESSION_KEY_PAD),
|
||||
"Age".padEnd(SESSION_AGE_PAD),
|
||||
"Model".padEnd(SESSION_MODEL_PAD),
|
||||
"Tokens (ctx %)".padEnd(TOKENS_PAD),
|
||||
"Flags",
|
||||
].join(" ");
|
||||
|
|
@ -262,21 +176,17 @@ export async function sessionsCommand(
|
|||
runtime.log(rich ? theme.heading(header) : header);
|
||||
|
||||
for (const row of rows) {
|
||||
const resolvedModel = resolveSessionModelRef(cfg, row, parseAgentSessionKey(row.key)?.agentId);
|
||||
const model = resolvedModel.model ?? configModel;
|
||||
const model = resolveSessionDisplayModel(cfg, row, displayDefaults) ?? configModel;
|
||||
const contextTokens = row.contextTokens ?? lookupContextTokens(model) ?? configContextTokens;
|
||||
const total = resolveFreshSessionTotalTokens(row);
|
||||
|
||||
const keyLabel = truncateKey(row.key).padEnd(KEY_PAD);
|
||||
const keyCell = rich ? theme.accent(keyLabel) : keyLabel;
|
||||
|
||||
const line = [
|
||||
formatKindCell(row.kind, rich),
|
||||
keyCell,
|
||||
formatAgeCell(row.updatedAt, rich),
|
||||
formatModelCell(model, rich),
|
||||
formatSessionKeyCell(row.key, rich),
|
||||
formatSessionAgeCell(row.updatedAt, rich),
|
||||
formatSessionModelCell(model, rich),
|
||||
formatTokensCell(total, contextTokens ?? null, rich),
|
||||
formatFlagsCell(row, rich),
|
||||
formatSessionFlagsCell(row, rich),
|
||||
].join(" ");
|
||||
|
||||
runtime.log(line.trimEnd());
|
||||
|
|
|
|||
Loading…
Reference in New Issue