UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

215 lines (214 loc) 7.88 kB
import "./fs-safe-aqmM_n6V.js"; import { r as readRegularFile } from "./regular-file-BD2zl6_l.js"; import { a as resolveSessionFilePathOptions, i as resolveSessionFilePath } from "./paths-NEwU8m3X.js"; import { s as resolveFreshSessionTotalTokens } from "./types-D8S_uNvu.js"; import "./transcript-jsonl-CD0KPCG0.js"; import { c as parseSessionEntries, s as migrateSessionEntries } from "./session-manager-_IGt1AS6.js"; import { r as readLatestRecentSessionUsageFromTranscriptAsync } from "./session-utils.fs-D_8-X4jl.js"; import { n as derivePromptTokens } from "./usage-C67Kbb7n.js"; import path from "node:path"; import fs from "node:fs/promises"; import crypto from "node:crypto"; //#region src/auto-reply/reply/session-fork.runtime.ts /** Runtime implementation for forking sessions from parent transcripts. */ const FALLBACK_TRANSCRIPT_BYTES_PER_TOKEN = 4; function resolvePositiveTokenCount(value) { return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : void 0; } function maxPositiveTokenCount(...values) { let max; for (const value of values) { const normalized = resolvePositiveTokenCount(value); if (typeof normalized === "number" && (max === void 0 || normalized > max)) max = normalized; } return max; } async function estimateParentTranscriptTokensFromBytes(params) { try { const filePath = resolveSessionFilePath(params.parentEntry.sessionId, params.parentEntry, resolveSessionFilePathOptions({ storePath: params.storePath })); const stat = await fs.stat(filePath); return resolvePositiveTokenCount(Math.ceil(stat.size / FALLBACK_TRANSCRIPT_BYTES_PER_TOKEN)); } catch { return; } } /** Resolves the best available token count for a parent session before forking. */ async function resolveParentForkTokenCountRuntime(params) { const freshPersistedTokens = resolveFreshSessionTotalTokens(params.parentEntry); if (typeof freshPersistedTokens === "number") return freshPersistedTokens; const cachedTokens = resolvePositiveTokenCount(params.parentEntry.totalTokens); const byteEstimateTokens = await estimateParentTranscriptTokensFromBytes(params); try { const usage = await readLatestRecentSessionUsageFromTranscriptAsync(params.parentEntry.sessionId, params.storePath, params.parentEntry.sessionFile, void 0, 1024 * 1024); const promptTokens = resolvePositiveTokenCount(derivePromptTokens({ input: usage?.inputTokens, cacheRead: usage?.cacheRead, cacheWrite: usage?.cacheWrite })); const outputTokens = resolvePositiveTokenCount(usage?.outputTokens); if (typeof promptTokens === "number") return maxPositiveTokenCount(promptTokens + (outputTokens ?? 0), cachedTokens, byteEstimateTokens); } catch {} return maxPositiveTokenCount(cachedTokens, byteEstimateTokens); } function isSessionEntry(entry) { return entry.type !== "session" && typeof entry.id === "string" && (typeof entry.timestamp === "string" || typeof entry.timestamp === "number"); } function buildEntryIndex(entries) { return new Map(entries.map((entry) => [entry.id, entry])); } function readBranch(params) { const branchEntries = []; let current = params.leafId ? params.byId.get(params.leafId) : void 0; while (current) { branchEntries.unshift(current); current = current.parentId ? params.byId.get(current.parentId) : void 0; } return branchEntries; } function generateEntryId(existingIds) { for (let attempt = 0; attempt < 100; attempt += 1) { const id = crypto.randomUUID().slice(0, 8); if (!existingIds.has(id)) { existingIds.add(id); return id; } } const id = crypto.randomUUID(); existingIds.add(id); return id; } function collectBranchLabels(params) { const labelsToWrite = []; for (const entry of params.allEntries) if (entry.type === "label" && entry.label && params.pathEntryIds.has(entry.targetId) && typeof entry.timestamp === "string") labelsToWrite.push({ targetId: entry.targetId, label: entry.label, timestamp: entry.timestamp }); return labelsToWrite; } async function readForkSourceTranscript(parentSessionFile) { const fileEntries = parseSessionEntries((await readRegularFile({ filePath: parentSessionFile })).buffer.toString("utf-8")); migrateSessionEntries(fileEntries); const header = fileEntries.find((entry) => entry.type === "session") ?? null; const entries = fileEntries.filter(isSessionEntry); const byId = buildEntryIndex(entries); const leafId = entries.at(-1)?.id ?? null; const branchEntries = readBranch({ byId, leafId }); const pathEntryIds = new Set(branchEntries.filter((entry) => entry.type !== "label").map((entry) => entry.id)); return { cwd: header?.cwd ?? process.cwd(), sessionDir: path.dirname(parentSessionFile), leafId, branchEntries, labelsToWrite: collectBranchLabels({ allEntries: entries, pathEntryIds }) }; } function buildBranchLabelEntries(params) { let parentId = params.lastEntryId; const labelEntries = []; for (const { targetId, label, timestamp } of params.labelsToWrite) { const labelEntry = { type: "label", id: generateEntryId(params.pathEntryIds), parentId, timestamp, targetId, label }; params.pathEntryIds.add(labelEntry.id); labelEntries.push(labelEntry); parentId = labelEntry.id; } return labelEntries; } async function writeForkHeaderOnly(params) { const sessionId = crypto.randomUUID(); const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const fileTimestamp = timestamp.replace(/[:.]/g, "-"); const sessionFile = path.join(params.sessionDir, `${fileTimestamp}_${sessionId}.jsonl`); const header = { type: "session", version: 3, id: sessionId, timestamp, cwd: params.cwd, parentSession: params.parentSessionFile }; await fs.mkdir(path.dirname(sessionFile), { recursive: true }); await fs.writeFile(sessionFile, `${JSON.stringify(header)}\n`, { encoding: "utf-8", mode: 384, flag: "wx" }); return { sessionId, sessionFile }; } async function writeBranchedSession(params) { const sessionId = crypto.randomUUID(); const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const fileTimestamp = timestamp.replace(/[:.]/g, "-"); const sessionFile = path.join(params.source.sessionDir, `${fileTimestamp}_${sessionId}.jsonl`); const pathWithoutLabels = params.source.branchEntries.filter((entry) => entry.type !== "label"); const pathEntryIds = new Set(pathWithoutLabels.map((entry) => entry.id)); const labelEntries = buildBranchLabelEntries({ labelsToWrite: params.source.labelsToWrite, pathEntryIds, lastEntryId: pathWithoutLabels.at(-1)?.id ?? null }); const entries = [ { type: "session", version: 3, id: sessionId, timestamp, cwd: params.source.cwd, parentSession: params.parentSessionFile }, ...pathWithoutLabels, ...labelEntries ]; if (entries.some((entry) => entry.type === "message" && entry.message.role === "assistant")) { await fs.mkdir(path.dirname(sessionFile), { recursive: true }); await fs.writeFile(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, { encoding: "utf-8", mode: 384, flag: "wx" }); } return { sessionId, sessionFile }; } /** Creates a child session transcript from a parent session branch. */ async function forkSessionFromParentRuntime(params) { const parentSessionFile = resolveSessionFilePath(params.parentEntry.sessionId, params.parentEntry, { agentId: params.agentId, sessionsDir: params.sessionsDir }); if (!parentSessionFile) return null; try { const source = await readForkSourceTranscript(parentSessionFile); if (!source) return null; return source.leafId ? await writeBranchedSession({ parentSessionFile, source }) : await writeForkHeaderOnly({ parentSessionFile, sessionDir: source.sessionDir, cwd: source.cwd }); } catch { return null; } } //#endregion export { forkSessionFromParentRuntime, resolveParentForkTokenCountRuntime };