mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-23 08:58:09 +00:00
docs: document session display commands
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Model display resolution for session listings.
|
||||
*
|
||||
* Session rows may carry persisted model/provider overrides or CLI-runtime
|
||||
* model strings; this module normalizes them into display-ready model refs.
|
||||
*/
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import {
|
||||
inferUniqueProviderFromConfiguredModels,
|
||||
@@ -57,6 +63,8 @@ function normalizeStoredOverrideModel(params: {
|
||||
}
|
||||
|
||||
const providerPrefix = `${providerOverride.toLowerCase()}/`;
|
||||
// Older stores sometimes persisted both providerOverride and a
|
||||
// provider/model modelOverride; trim the duplicate provider for display.
|
||||
return {
|
||||
providerOverride,
|
||||
modelOverride: modelOverride.toLowerCase().startsWith(providerPrefix)
|
||||
@@ -73,6 +81,7 @@ function resolveDefaultModelRef(cfg: OpenClawConfig, agentId?: string): SessionD
|
||||
return parseModelRef(primary, DEFAULT_PROVIDER);
|
||||
}
|
||||
|
||||
/** Resolves default display values for a session table scoped to an agent. */
|
||||
export function resolveSessionDisplayDefaults(
|
||||
cfg: OpenClawConfig,
|
||||
agentId?: string,
|
||||
@@ -91,6 +100,8 @@ function normalizeCliRuntimeDisplayRef(
|
||||
return ref;
|
||||
}
|
||||
if (ref.model.includes("/")) {
|
||||
// CLI runtimes can store the real provider/model inside the model field;
|
||||
// prefer that embedded provider when it is not another CLI runtime alias.
|
||||
const parsed = parseModelRef(ref.model, defaultRef.provider);
|
||||
if (!isCliProvider(parsed.provider, cfg)) {
|
||||
return parsed;
|
||||
@@ -103,6 +114,8 @@ function normalizeCliRuntimeDisplayRef(
|
||||
if (inferredProvider && !isCliProvider(inferredProvider, cfg)) {
|
||||
return { provider: inferredProvider, model: ref.model };
|
||||
}
|
||||
// If the CLI runtime model cannot be mapped to a concrete provider, fall
|
||||
// back to the configured default provider so rows stay comparable.
|
||||
const parsed = parseModelRef(ref.model, defaultRef.provider);
|
||||
if (!isCliProvider(parsed.provider, cfg)) {
|
||||
return parsed;
|
||||
@@ -113,6 +126,7 @@ function normalizeCliRuntimeDisplayRef(
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolves only the model id to show for a session row. */
|
||||
export function resolveSessionDisplayModel(
|
||||
cfg: OpenClawConfig,
|
||||
row: SessionDisplayModelRow,
|
||||
@@ -120,6 +134,7 @@ export function resolveSessionDisplayModel(
|
||||
return resolveSessionDisplayModelRef(cfg, row).model;
|
||||
}
|
||||
|
||||
/** Resolves provider/model display metadata for a session row. */
|
||||
export function resolveSessionDisplayModelRef(
|
||||
cfg: OpenClawConfig,
|
||||
row: SessionDisplayModelRow,
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
/**
|
||||
* Shared table formatting helpers for session commands.
|
||||
*
|
||||
* Cleanup and listing commands use the same row shape and fixed-width cells so
|
||||
* terminal output stays aligned across commands.
|
||||
*/
|
||||
import { theme } from "../../packages/terminal-core/src/theme.js";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
|
||||
|
||||
/** Display row derived from a persisted session entry. */
|
||||
export type SessionDisplayRow = {
|
||||
key: string;
|
||||
updatedAt: number | null;
|
||||
@@ -32,6 +39,7 @@ export const SESSION_KEY_PAD = 26;
|
||||
export const SESSION_AGE_PAD = 9;
|
||||
export const SESSION_MODEL_PAD = 14;
|
||||
|
||||
/** Converts a persisted session entry into the shared display row shape. */
|
||||
export function toSessionDisplayRow(key: string, entry: SessionEntry): SessionDisplayRow {
|
||||
const updatedAt = entry?.updatedAt ?? null;
|
||||
return {
|
||||
@@ -60,6 +68,7 @@ export function toSessionDisplayRow(key: string, entry: SessionEntry): SessionDi
|
||||
};
|
||||
}
|
||||
|
||||
/** Converts and sorts a session store by most recent activity first. */
|
||||
export function toSessionDisplayRows(store: Record<string, SessionEntry>): SessionDisplayRow[] {
|
||||
return Object.entries(store)
|
||||
.map(([key, entry]) => toSessionDisplayRow(key, entry))
|
||||
@@ -70,26 +79,32 @@ function truncateSessionKey(key: string): string {
|
||||
if (key.length <= SESSION_KEY_PAD) {
|
||||
return key;
|
||||
}
|
||||
// Keep both the stable prefix and suffix; the tail often contains direct
|
||||
// recipient or runtime identifiers that distinguish otherwise similar keys.
|
||||
const head = Math.max(4, SESSION_KEY_PAD - 10);
|
||||
return `${key.slice(0, head)}...${key.slice(-6)}`;
|
||||
}
|
||||
|
||||
/** Formats a session key cell for table output. */
|
||||
export function formatSessionKeyCell(key: string, rich: boolean): string {
|
||||
const label = truncateSessionKey(key).padEnd(SESSION_KEY_PAD);
|
||||
return rich ? theme.accent(label) : label;
|
||||
}
|
||||
|
||||
/** Formats a relative session age cell for table output. */
|
||||
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;
|
||||
}
|
||||
|
||||
/** Formats a model cell for table output. */
|
||||
export function formatSessionModelCell(model: string | null | undefined, rich: boolean): string {
|
||||
const label = (model ?? "unknown").padEnd(SESSION_MODEL_PAD);
|
||||
return rich ? theme.info(label) : label;
|
||||
}
|
||||
|
||||
/** Formats compact per-session flags for table output. */
|
||||
export function formatSessionFlagsCell(
|
||||
row: Pick<
|
||||
SessionDisplayRow,
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Session trajectory tail command.
|
||||
*
|
||||
* It selects active or requested sessions, renders recent trajectory events,
|
||||
* and can follow append-only trajectory files across rotation/truncation.
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { readAcpSessionMeta } from "../acp/runtime/session-meta.js";
|
||||
@@ -64,6 +70,7 @@ const EVENT_TYPE_PAD = 16;
|
||||
const FOLLOW_INTERVAL_MS = 1_000;
|
||||
let followIntervalMsForTests: number | undefined;
|
||||
|
||||
/** Overrides the follow polling interval for tests. */
|
||||
export function setSessionsTailFollowIntervalMsForTests(intervalMs?: number): void {
|
||||
followIntervalMsForTests = intervalMs;
|
||||
}
|
||||
@@ -146,6 +153,8 @@ function compareCursors(left: TrajectoryCursor, right: TrajectoryCursor): number
|
||||
if (left.seq !== null && right.seq !== null && left.seq !== right.seq) {
|
||||
return left.seq - right.seq;
|
||||
}
|
||||
// Some trajectory events lack sequence numbers; timestamp fallback keeps
|
||||
// follow mode from replaying already-rendered events after file rewrites.
|
||||
const byTimestamp = left.tsMs - right.tsMs;
|
||||
if (byTimestamp !== 0) {
|
||||
return byTimestamp;
|
||||
@@ -241,6 +250,8 @@ function safePreview(event: TrajectoryEvent): string {
|
||||
return `prompt skipped${reason ? `: ${reason}` : ""}`;
|
||||
}
|
||||
case "tool.call":
|
||||
// Tool arguments may contain secrets or user text; tail output shows only
|
||||
// the tool name and a redacted placeholder.
|
||||
return `${toolName(data)} {...redacted...}`;
|
||||
case "tool.timeout":
|
||||
return `${toolName(data)} timeout`;
|
||||
@@ -372,6 +383,8 @@ function selectSessionsToTail(selections: TailSelection[], sessionKey?: string):
|
||||
|
||||
const running = selections.filter((selection) => isRunningSession(selection));
|
||||
if (running.length > 0) {
|
||||
// Without an explicit key, prefer all running sessions so follow mode shows
|
||||
// concurrent active work instead of only the newest store entry.
|
||||
return running.toSorted(compareSelectionsByUpdatedAt);
|
||||
}
|
||||
|
||||
@@ -405,6 +418,8 @@ function readNewFollowEvents(state: FollowState): TrajectoryEvent[] {
|
||||
fileState.size === state.offset && state.fileState?.mtimeMs !== fileState.mtimeMs;
|
||||
|
||||
if (replaced || truncated || possiblyRewrittenSameSize) {
|
||||
// Log rotation, truncation, and same-size rewrites all require a full
|
||||
// rescan; cursor filtering prevents duplicate event output.
|
||||
const snapshot = readTrajectorySnapshot(state.selection.trajectoryPath);
|
||||
state.fileState = snapshot.fileState;
|
||||
state.offset = snapshot.offset;
|
||||
@@ -424,6 +439,8 @@ function readNewFollowEvents(state: FollowState): TrajectoryEvent[] {
|
||||
state.offset = fileState.size;
|
||||
state.fileState = fileState;
|
||||
const combined = `${state.pending}${buffer.toString("utf8")}`;
|
||||
// Keep an incomplete trailing JSON line until the next poll, matching
|
||||
// append-only writers that flush in chunks.
|
||||
const lines = combined.split(/\r?\n/u);
|
||||
state.pending = lines.pop() ?? "";
|
||||
return parseTrajectoryEventLines(lines);
|
||||
@@ -492,6 +509,7 @@ function resolveTailTargetAgent(opts: SessionsTailOptions): string | undefined {
|
||||
return opts.sessionKey?.trim() ? resolveAgentIdFromSessionKey(opts.sessionKey) : undefined;
|
||||
}
|
||||
|
||||
/** Tails recent trajectory events for the selected session(s). */
|
||||
export async function sessionsTailCommand(
|
||||
opts: SessionsTailOptions,
|
||||
runtime: RuntimeEnv,
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
@@ -106,6 +112,8 @@ function selectNewestSessionRows(rows: SessionRow[], limit: number | undefined):
|
||||
if (limit > TOP_N_SELECTION_LIMIT) {
|
||||
return rows.toSorted(compareSessionRowsByUpdatedAt).slice(0, limit);
|
||||
}
|
||||
// For small limits, keep only the top N rows without sorting the full store;
|
||||
// large limits use the simpler full sort above.
|
||||
const selected: SessionRow[] = [];
|
||||
for (const row of rows) {
|
||||
const insertAt = selected.findIndex(
|
||||
@@ -241,6 +249,8 @@ function stripChannelRecipientPrefix(
|
||||
}
|
||||
const stripped = raw.slice(prefix.length);
|
||||
const topicMarkerIndex = stripped.toLowerCase().indexOf(":topic:");
|
||||
// Topic suffixes are routing detail, not the peer id used by runtime-policy
|
||||
// session-key display.
|
||||
return topicMarkerIndex >= 0 ? stripped.slice(0, topicMarkerIndex) : stripped;
|
||||
}
|
||||
|
||||
@@ -272,6 +282,8 @@ function resolveDisplayRuntimePolicySessionKey(params: {
|
||||
stripChannelRecipientPrefix(to, channel) ??
|
||||
stripChannelRecipientPrefix(from, channel);
|
||||
|
||||
// Direct-message runtime policy can route by native user id, stripped
|
||||
// recipient, or sender; expose the derived key when it differs from the row.
|
||||
const runtimePolicySessionKey = resolveRuntimePolicySessionKey({
|
||||
cfg,
|
||||
sessionKey: key,
|
||||
@@ -296,6 +308,7 @@ function resolveDisplayRuntimePolicySessionKey(params: {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Lists sessions across selected stores with optional JSON output. */
|
||||
export async function sessionsCommand(
|
||||
opts: {
|
||||
json?: boolean;
|
||||
@@ -369,6 +382,8 @@ export async function sessionsCommand(
|
||||
entry,
|
||||
});
|
||||
const acpRuntime = acpMeta != null;
|
||||
// ACP rows need stored-key metadata before model/runtime resolution so
|
||||
// bridge sessions and true ACP runtime sessions display differently.
|
||||
const modelRef = applyAcpModelOverlayIfNeeded(
|
||||
resolveSessionDisplayModelRef(cfg, row),
|
||||
acpSessionKey,
|
||||
@@ -443,6 +458,8 @@ export async function sessionsCommand(
|
||||
totalTokens: resolveSessionTotalTokens(r) ?? null,
|
||||
totalTokensFresh:
|
||||
typeof r.totalTokens === "number" ? r.totalTokensFresh !== false : false,
|
||||
// Prefer row-level context tokens, then config/model lookup, so JSON
|
||||
// mirrors the terminal percentage calculation.
|
||||
contextTokens:
|
||||
r.contextTokens ??
|
||||
configuredContextTokens ??
|
||||
|
||||
Reference in New Issue
Block a user