UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

367 lines (366 loc) 12.8 kB
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 };