openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
367 lines (366 loc) • 12.8 kB
JavaScript
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { f as resolveAgentIdFromSessionKey } from "./session-key-B_NoIfpX.js";
import { i as getRuntimeConfig } from "./io-Gi7-pyU-.js";
import "./config-C9RxTsn1.js";
import { a as resolveStoredSessionKeyForAgentStore } from "./session-store-key-BsydjA1h.js";
import { i as resolveSessionFilePath } from "./paths-NEwU8m3X.js";
import { t as loadSessionStore } from "./store-load-C4uq6Lrq.js";
import { i as resolveTrajectoryFilePath } from "./paths-Biq9XkB5.js";
import "./sessions-D6zZvoxX.js";
import { r as readAcpSessionMeta } from "./session-meta-BuCj-TpW.js";
import { n as resolveTrajectoryRuntimeFile } from "./runtime-file-BxBUlhIQ.js";
import { t as shortenText } from "./text-format-_51_xEcm.js";
import { t as resolveSessionStoreTargetsOrExit } from "./session-store-targets-BGF93pOS.js";
import fs from "node:fs";
import path from "node:path";
//#region src/commands/sessions-tail.ts
/**
* Session trajectory tail command.
*
* It selects active or requested sessions, renders recent trajectory events,
* and can follow append-only trajectory files across rotation/truncation.
*/
const DEFAULT_TAIL_COUNT = 80;
const SESSION_KEY_PAD = 30;
const EVENT_TYPE_PAD = 16;
const FOLLOW_INTERVAL_MS = 1e3;
function resolveFollowIntervalMs() {
return FOLLOW_INTERVAL_MS;
}
function parseTailCount(value) {
if (value === void 0) return DEFAULT_TAIL_COUNT;
if (typeof value === "number") return Number.isInteger(value) && value >= 0 ? value : null;
const trimmed = value.trim();
if (!/^\d+$/.test(trimmed)) return null;
return Number.parseInt(trimmed, 10);
}
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function toOptionalString(value) {
return typeof value === "string" && value.trim() ? value.trim() : void 0;
}
function isTrajectoryEvent(value) {
return isRecord(value) && value.traceSchema === "openclaw-trajectory" && value.schemaVersion === 1 && typeof value.type === "string" && typeof value.ts === "string" && typeof value.sessionId === "string";
}
function parseTrajectoryEventLine(line) {
const trimmed = line.trim();
if (!trimmed) return null;
try {
const parsed = JSON.parse(trimmed);
return isTrajectoryEvent(parsed) ? parsed : null;
} catch {
return null;
}
}
function parseTrajectoryEventLines(lines) {
return lines.flatMap((line) => {
const event = parseTrajectoryEventLine(line);
return event ? [event] : [];
});
}
function eventSequence(event) {
const seq = event.sourceSeq ?? event.seq;
return Number.isFinite(seq) ? seq : null;
}
function eventTimestampMs(event) {
const parsed = Date.parse(event.ts);
return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
}
function eventCursor(event) {
return {
seq: eventSequence(event),
tsMs: eventTimestampMs(event)
};
}
function compareCursors(left, right) {
if (left.seq !== null && right.seq !== null && left.seq !== right.seq) return left.seq - right.seq;
const byTimestamp = left.tsMs - right.tsMs;
if (byTimestamp !== 0) return byTimestamp;
if (left.seq !== null && right.seq !== null) return left.seq - right.seq;
return 0;
}
function maxCursorValue(current, candidate) {
return !current || compareCursors(candidate, current) > 0 ? candidate : current;
}
function maxCursor(current, event) {
return maxCursorValue(current, eventCursor(event));
}
function maxCursorFromEvents(events) {
return events.reduce((cursor, event) => maxCursor(cursor, event), null);
}
function eventsAfterCursor(events, cursor) {
if (!cursor) return events;
return events.filter((event) => compareCursors(eventCursor(event), cursor) > 0);
}
function formatTimestamp(ts) {
const date = new Date(ts);
if (Number.isNaN(date.getTime())) return "--:--:--";
return date.toISOString().slice(11, 19);
}
function modelLabel(event) {
const provider = event.provider?.trim();
const model = event.modelId?.trim();
if (provider && model) return `${provider}/${model}`;
return model || provider || void 0;
}
function toolName(data) {
return toOptionalString(data?.name) ?? toOptionalString(data?.toolName) ?? "tool";
}
function resultStatus(data) {
if (data?.success === true) return "ok";
if (data?.success === false || data?.isError === true) return "error";
return toOptionalString(data?.status) ?? "done";
}
function modelCompletionStatus(data) {
if (data?.timedOut === true) return "timeout";
if (data?.aborted === true) return "aborted";
if (toOptionalString(data?.promptError)) return "error";
return "done";
}
function safePreview(event) {
const data = event.data;
switch (event.type) {
case "session.started": return "session started";
case "context.compiled": {
const tools = Array.isArray(data?.tools) ? data.tools.length : void 0;
return tools === void 0 ? "context compiled" : `context compiled (${tools} tools)`;
}
case "prompt.submitted": return "prompt submitted";
case "prompt.skipped": {
const reason = toOptionalString(data?.reason);
return `prompt skipped${reason ? `: ${reason}` : ""}`;
}
case "tool.call": return `${toolName(data)} {...redacted...}`;
case "tool.timeout": return `${toolName(data)} timeout`;
case "tool.result": return `${toolName(data)} ${resultStatus(data)}`;
case "model.completed": {
const model = modelLabel(event);
const status = modelCompletionStatus(data);
return model ? `${model} ${status}` : status;
}
case "session.ended": return toOptionalString(data?.status) ?? "ended";
case "trace.truncated": return "trajectory truncated";
default: return toOptionalString(data?.status) ?? toOptionalString(data?.name) ?? "";
}
}
function formatProgressLine(event) {
const sessionLabel = shortenText(event.sessionKey ?? event.sessionId, SESSION_KEY_PAD).padEnd(SESSION_KEY_PAD);
const typeLabel = shortenText(event.type, EVENT_TYPE_PAD).padEnd(EVENT_TYPE_PAD);
const preview = safePreview(event);
return [
formatTimestamp(event.ts),
typeLabel,
sessionLabel,
preview
].join(" ").trimEnd();
}
function readTrajectorySnapshot(filePath) {
try {
const stat = fs.statSync(filePath);
const text = fs.readFileSync(filePath, "utf8");
return {
events: parseTrajectoryEventLines(text.split(/\r?\n/u)),
fileState: fileStateFromStat(stat),
offset: Buffer.byteLength(text, "utf8")
};
} catch (error) {
if (error.code === "ENOENT") return {
events: [],
fileState: null,
offset: 0
};
throw error;
}
}
function renderEvents(events, runtime) {
let cursor = null;
for (const event of events) {
runtime.log(formatProgressLine(event));
cursor = maxCursor(cursor, event);
}
return cursor;
}
function fileStateFromStat(stat) {
return {
dev: stat.dev,
ino: stat.ino,
mtimeMs: stat.mtimeMs,
size: stat.size
};
}
function sameFileIdentity(left, right) {
return Boolean(left && left.dev === right.dev && left.ino === right.ino);
}
function readFollowFileState(filePath) {
try {
return fileStateFromStat(fs.statSync(filePath));
} catch (error) {
if (error.code === "ENOENT") return null;
throw error;
}
}
function isRunningSession(selection) {
const acpMeta = readAcpSessionMeta({ sessionKey: resolveStoredSessionKeyForAgentStore({
cfg: getRuntimeConfig(),
agentId: selection.agentId,
sessionKey: selection.key
}) });
return selection.entry.status === "running" || acpMeta?.state === "running";
}
function compareSelectionsByUpdatedAt(a, b) {
return (b.entry.updatedAt ?? 0) - (a.entry.updatedAt ?? 0);
}
async function buildTailSelection(params) {
const sessionsDir = path.dirname(params.storePath);
const sessionFile = resolveSessionFilePath(params.entry.sessionId, params.entry, {
agentId: params.agentId,
sessionsDir
});
const trajectoryPath = await resolveTrajectoryRuntimeFile({
sessionFile,
sessionId: params.entry.sessionId
}) ?? resolveTrajectoryFilePath({
sessionFile,
sessionId: params.entry.sessionId
});
return {
agentId: params.agentId,
entry: params.entry,
key: params.key,
storePath: params.storePath,
trajectoryPath
};
}
function selectSessionsToTail(selections, sessionKey) {
const requested = sessionKey?.trim();
if (requested) return selections.filter((selection) => selection.key === requested);
const running = selections.filter((selection) => isRunningSession(selection));
if (running.length > 0) return running.toSorted(compareSelectionsByUpdatedAt);
const latest = selections.toSorted(compareSelectionsByUpdatedAt)[0];
return latest ? [latest] : [];
}
function statFileSize(filePath) {
try {
return fs.statSync(filePath).size;
} catch (error) {
if (error.code === "ENOENT") return 0;
throw error;
}
}
function readNewFollowEvents(state) {
const fileState = readFollowFileState(state.selection.trajectoryPath);
if (!fileState) {
state.fileState = null;
state.offset = 0;
state.pending = "";
return [];
}
const replaced = !sameFileIdentity(state.fileState, fileState);
const truncated = fileState.size < state.offset;
const possiblyRewrittenSameSize = fileState.size === state.offset && state.fileState?.mtimeMs !== fileState.mtimeMs;
if (replaced || truncated || possiblyRewrittenSameSize) {
const snapshot = readTrajectorySnapshot(state.selection.trajectoryPath);
state.fileState = snapshot.fileState;
state.offset = snapshot.offset;
state.pending = "";
return eventsAfterCursor(snapshot.events, state.cursor);
}
if (fileState.size === state.offset) {
state.fileState = fileState;
return [];
}
const fd = fs.openSync(state.selection.trajectoryPath, "r");
try {
const buffer = Buffer.alloc(fileState.size - state.offset);
fs.readSync(fd, buffer, 0, buffer.length, state.offset);
state.offset = fileState.size;
state.fileState = fileState;
const lines = `${state.pending}${buffer.toString("utf8")}`.split(/\r?\n/u);
state.pending = lines.pop() ?? "";
return parseTrajectoryEventLines(lines);
} finally {
fs.closeSync(fd);
}
}
function renderFollowEvents(events, state, runtime) {
const cursor = renderEvents(events, runtime);
if (cursor) state.cursor = maxCursorValue(state.cursor, cursor);
}
async function followSelections(selections, runtime, initialSnapshots) {
const states = selections.map((selection) => {
const snapshot = initialSnapshots.get(selection.trajectoryPath);
return {
cursor: snapshot ? maxCursorFromEvents(snapshot.events) : null,
fileState: snapshot?.fileState ?? readFollowFileState(selection.trajectoryPath),
offset: snapshot?.offset ?? statFileSize(selection.trajectoryPath),
pending: "",
selection
};
});
await new Promise((resolve) => {
const interval = setInterval(() => {
for (const state of states) try {
renderFollowEvents(readNewFollowEvents(state), state, runtime);
} catch (error) {
runtime.error(`Failed to read trajectory progress for ${state.selection.key}: ${formatErrorMessage(error)}`);
}
}, resolveFollowIntervalMs());
const stop = () => {
clearInterval(interval);
process.off("SIGINT", stop);
process.off("SIGTERM", stop);
resolve();
};
process.once("SIGINT", stop);
process.once("SIGTERM", stop);
});
}
function resolveTailTargetAgent(opts) {
if (opts.agent?.trim() || opts.store?.trim() || opts.allAgents === true) return opts.agent;
return opts.sessionKey?.trim() ? resolveAgentIdFromSessionKey(opts.sessionKey) : void 0;
}
/** Tails recent trajectory events for the selected session(s). */
async function sessionsTailCommand(opts, runtime) {
const tailCount = parseTailCount(opts.tail);
if (tailCount === null) {
runtime.error("--tail must be a non-negative integer, for example --tail 25.");
runtime.exit(1);
return;
}
const targets = resolveSessionStoreTargetsOrExit({
cfg: getRuntimeConfig(),
opts: {
store: opts.store,
agent: resolveTailTargetAgent(opts),
allAgents: opts.allAgents
},
runtime
});
if (!targets) return;
const selections = [];
for (const target of targets) {
const store = loadSessionStore(target.storePath);
for (const [key, entry] of Object.entries(store)) selections.push(await buildTailSelection({
agentId: target.agentId,
entry,
key,
storePath: target.storePath
}));
}
const selected = selectSessionsToTail(selections, opts.sessionKey);
if (selected.length === 0) {
const suffix = opts.sessionKey ? ` for ${opts.sessionKey}` : "";
runtime.log(`No sessions found${suffix}.`);
return;
}
const followSnapshots = /* @__PURE__ */ new Map();
for (const selection of selected) {
const snapshot = readTrajectorySnapshot(selection.trajectoryPath);
followSnapshots.set(selection.trajectoryPath, snapshot);
renderEvents(tailCount > 0 ? snapshot.events.slice(-tailCount) : [], runtime);
}
if (opts.follow) await followSelections(selected, runtime, followSnapshots);
}
//#endregion
export { sessionsTailCommand };