openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
244 lines (243 loc) • 9.27 kB
JavaScript
import { y as resolveStateDir } from "./paths-mvMm5bYV.js";
import { g as shortenHomePath } from "./utils-CCC-BEJH.js";
import { t as resolveAgentSessionDirs } from "./session-dirs-COV6A9cq.js";
import { d as stripInternalRuntimeContext, l as hasInternalRuntimeContext } from "./internal-runtime-context-BH_40W4f.js";
import { t as note } from "./note-BRSJp0UF.js";
import path from "node:path";
import fs from "node:fs/promises";
//#region src/commands/doctor-session-transcripts.ts
const LEGACY_OPENAI_CODEX_PROVIDER_ID = "openai-codex";
const OPENAI_PROVIDER_ID = "openai";
const LEGACY_OPENAI_CODEX_RESPONSES_API = "openai-codex-responses";
const OPENAI_CHATGPT_RESPONSES_API = "openai-chatgpt-responses";
function parseTranscriptEntries(raw) {
const entries = [];
for (const line of raw.split(/\r?\n/)) {
if (!line.trim()) continue;
try {
const parsed = JSON.parse(line);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) entries.push(parsed);
} catch {
return [];
}
}
return entries;
}
function getEntryId(entry) {
return typeof entry.id === "string" && entry.id.trim() ? entry.id : null;
}
function getParentId(entry) {
return typeof entry.parentId === "string" && entry.parentId.trim() ? entry.parentId : null;
}
function getMessage(entry) {
return entry.message && typeof entry.message === "object" && !Array.isArray(entry.message) ? entry.message : null;
}
function normalizeLegacyOpenAICodexTranscriptMetadata(entries) {
let changed = 0;
for (const entry of entries) {
const message = getMessage(entry);
if (!message) continue;
let touched = false;
if (message.provider === LEGACY_OPENAI_CODEX_PROVIDER_ID) {
message.provider = OPENAI_PROVIDER_ID;
touched = true;
}
if (message.api === LEGACY_OPENAI_CODEX_RESPONSES_API) {
message.api = OPENAI_CHATGPT_RESPONSES_API;
touched = true;
}
if (touched) changed += 1;
}
return changed;
}
function textFromContent(content) {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return null;
return content.map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").join("") || null;
}
function selectActivePath(entries) {
const sessionEntries = entries.filter((entry) => entry.type !== "session");
const leaf = sessionEntries.at(-1);
const leafId = leaf ? getEntryId(leaf) : null;
if (!leaf || !leafId) return null;
const byId = /* @__PURE__ */ new Map();
for (const entry of sessionEntries) {
const id = getEntryId(entry);
if (id) byId.set(id, entry);
}
const active = [];
const seen = /* @__PURE__ */ new Set();
let current = leaf;
while (current) {
const id = getEntryId(current);
if (!id || seen.has(id)) return null;
seen.add(id);
active.unshift(current);
const parentId = getParentId(current);
current = parentId ? byId.get(parentId) : void 0;
}
return active;
}
function hasBrokenPromptRewriteBranch(entries, activePath) {
const activeIds = new Set(activePath.map(getEntryId).filter((id) => Boolean(id)));
const activeUserByParentAndText = /* @__PURE__ */ new Set();
for (const entry of activePath) {
const id = getEntryId(entry);
const message = getMessage(entry);
if (!id || message?.role !== "user") continue;
const text = textFromContent(message.content);
if (text !== null) activeUserByParentAndText.add(`${getParentId(entry) ?? ""}\0${text.trim()}`);
}
for (const entry of entries) {
const id = getEntryId(entry);
if (!id || activeIds.has(id)) continue;
const message = getMessage(entry);
if (message?.role !== "user") continue;
const text = textFromContent(message.content);
if (!text || !hasInternalRuntimeContext(text)) continue;
const visibleText = stripInternalRuntimeContext(text).trim();
if (visibleText && activeUserByParentAndText.has(`${getParentId(entry) ?? ""}\0${visibleText}`)) return true;
}
return false;
}
async function writeActiveTranscript(params) {
const header = params.entries.find((entry) => entry.type === "session");
if (!header) throw new Error("missing session header");
const backupPath = `${params.filePath}.pre-doctor-branch-repair-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
await fs.copyFile(params.filePath, backupPath);
const next = [header, ...params.activePath].map((entry) => JSON.stringify(entry)).join("\n");
await fs.writeFile(params.filePath, `${next}\n`, "utf-8");
return backupPath;
}
async function writeTranscriptEntries(params) {
const backupPath = `${params.filePath}.pre-doctor-openai-codex-repair-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.bak`;
await fs.copyFile(params.filePath, backupPath);
const next = params.entries.map((entry) => JSON.stringify(entry)).join("\n");
await fs.writeFile(params.filePath, `${next}\n`, "utf-8");
return backupPath;
}
/** Repairs one transcript file by keeping the active branch and backing up the original file. */
async function repairBrokenSessionTranscriptFile(params) {
try {
const entries = parseTranscriptEntries(await fs.readFile(params.filePath, "utf-8"));
const legacyOpenAICodexEntries = normalizeLegacyOpenAICodexTranscriptMetadata(entries);
const activePath = selectActivePath(entries);
if (!activePath) {
if (legacyOpenAICodexEntries > 0 && params.shouldRepair) {
const backupPath = await writeTranscriptEntries({
filePath: params.filePath,
entries
});
return {
filePath: params.filePath,
broken: true,
repaired: true,
originalEntries: entries.length,
activeEntries: 0,
legacyOpenAICodexEntries,
backupPath,
reason: "no active branch"
};
}
return {
filePath: params.filePath,
broken: legacyOpenAICodexEntries > 0,
repaired: false,
originalEntries: entries.length,
activeEntries: 0,
legacyOpenAICodexEntries,
reason: "no active branch"
};
}
const broken = hasBrokenPromptRewriteBranch(entries, activePath);
if (!broken && legacyOpenAICodexEntries === 0) return {
filePath: params.filePath,
broken: false,
repaired: false,
originalEntries: entries.length,
activeEntries: activePath.length,
legacyOpenAICodexEntries
};
if (!params.shouldRepair) return {
filePath: params.filePath,
broken: true,
repaired: false,
originalEntries: entries.length,
activeEntries: activePath.length,
legacyOpenAICodexEntries
};
const backupPath = broken ? await writeActiveTranscript({
filePath: params.filePath,
entries,
activePath
}) : await writeTranscriptEntries({
filePath: params.filePath,
entries
});
return {
filePath: params.filePath,
broken: true,
repaired: true,
originalEntries: entries.length,
activeEntries: activePath.length,
legacyOpenAICodexEntries,
backupPath
};
} catch (err) {
return {
filePath: params.filePath,
broken: false,
repaired: false,
originalEntries: 0,
activeEntries: 0,
legacyOpenAICodexEntries: 0,
reason: String(err)
};
}
}
async function listSessionTranscriptFiles(sessionDirs) {
const files = [];
for (const sessionsDir of sessionDirs) {
let entries;
try {
entries = await fs.readdir(sessionsDir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) if (entry.isFile() && entry.name.endsWith(".jsonl")) files.push(path.join(sessionsDir, entry.name));
}
return files.toSorted((a, b) => a.localeCompare(b));
}
/** Scans session transcript files and reports or repairs legacy/broken transcript state. */
async function noteSessionTranscriptHealth(params) {
const shouldRepair = params?.shouldRepair === true;
let sessionDirs = params?.sessionDirs;
try {
sessionDirs ??= await resolveAgentSessionDirs(resolveStateDir(process.env));
} catch (err) {
note(`- Failed to inspect session transcripts: ${String(err)}`, "Session transcripts");
return;
}
const files = await listSessionTranscriptFiles(sessionDirs);
if (files.length === 0) return;
const results = [];
for (const filePath of files) results.push(await repairBrokenSessionTranscriptFile({
filePath,
shouldRepair
}));
const broken = results.filter((result) => result.broken);
if (broken.length === 0) return;
const repairedCount = broken.filter((result) => result.repaired).length;
const lines = [`- Found ${broken.length} transcript file${broken.length === 1 ? "" : "s"} with legacy state.`, ...broken.slice(0, 20).map((result) => {
const backup = result.backupPath ? ` backup=${shortenHomePath(result.backupPath)}` : "";
const status = result.repaired ? "repaired" : "needs repair";
const metadata = result.legacyOpenAICodexEntries > 0 ? ` openai-codex=${result.legacyOpenAICodexEntries}` : "";
return `- ${shortenHomePath(result.filePath)} ${status} entries=${result.originalEntries}->${result.activeEntries + 1}${metadata}${backup}`;
})];
if (broken.length > 20) lines.push(`- ...and ${broken.length - 20} more.`);
if (!shouldRepair) lines.push("- Run \"openclaw doctor --fix\" to rewrite affected files to their active branch.");
else if (repairedCount > 0) lines.push(`- Repaired ${repairedCount} transcript file${repairedCount === 1 ? "" : "s"}.`);
note(lines.join("\n"), "Session transcripts");
}
//#endregion
export { noteSessionTranscriptHealth };