openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
504 lines (503 loc) • 21.2 kB
JavaScript
import { c as listSessionTranscriptCorpusEntriesForAgent } from "./memory-core-host-engine-sessions-Cn5pEjBS.js";
import { a as SESSION_BACKFILL_REWIND_NAMESPACE, m as deleteMemoryCoreWorkspaceEntry, x as writeMemoryCoreWorkspaceEntry, y as readMemoryCoreWorkspaceEntries } from "./dreaming-state-B0qd2W7q.js";
import { t as normalizeSessionBackfillSelection } from "./session-backfill-selection-CjIn4YJO.js";
import { m as withMemoryWorkspaceLock, s as removeBackfillDiaryEntries, u as writeBackfillDiaryEntries } from "./dreaming-dreams-file-DQfkc_tk.js";
import { t as previewGroundedRemForFile } from "./rem-evidence-DwmnIkoF.js";
import { d as recordGroundedShortTermCandidates, r as removeGroundedShortTermCandidates, u as readShortTermRecallEntries } from "./short-term-promotion-BWqNnYfM.js";
import { i as listMemorySessionTombstones, o as recordMemoryEntryOrigins, p as hashMemoryContent, u as buildPromotionMarker } from "./memory-entry-origins-DGcbM97X.js";
import { a as mergeTrackedMessageHashes, c as scanSessionIngestionSource, f as trimTrackedSessionScopes, i as foreignSessionIngestionSource, l as sessionExclusionReason, n as SESSION_INGESTION_SCORE, o as readSessionIngestionState, p as writeSessionIngestionState, r as appendSessionCorpusLines, s as resolveAdmissionPolicy, u as sessionIngestionSourceFromCorpus } from "./session-ingestion-Cx5hXbYJ.js";
import path from "node:path";
import { createHash } from "node:crypto";
//#region extensions/memory-core/src/session-backfill-lifecycle.ts
const SESSION_BACKFILL_BASELINE_KEY_PREFIX = "complete-baseline:";
async function recordSessionBackfillRewindBatch(params) {
if (params.candidates.length === 0) return;
const key = createHash("sha256").update(JSON.stringify(params.candidates)).digest("hex");
await writeMemoryCoreWorkspaceEntry({
namespace: SESSION_BACKFILL_REWIND_NAMESPACE,
workspaceDir: params.workspaceDir,
key,
value: {
version: 1,
candidates: params.candidates
}
});
}
async function markSessionBackfillRewindBaseline(params) {
await writeMemoryCoreWorkspaceEntry({
namespace: SESSION_BACKFILL_REWIND_NAMESPACE,
workspaceDir: params.workspaceDir,
key: `${SESSION_BACKFILL_BASELINE_KEY_PREFIX}${params.agentId}`,
value: {
version: 1,
complete: true,
agentId: params.agentId
}
});
}
function isSessionBackfillRewindCandidate(value) {
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
const candidate = value;
return Number.isInteger(candidate.contentIndex) && candidate.contentIndex >= 0 && typeof candidate.hash === "string" && candidate.hash.length > 0 && typeof candidate.scope === "string" && candidate.scope.length > 0 && typeof candidate.stateKey === "string" && candidate.stateKey.length > 0;
}
function isSessionBackfillRewindBatch(value) {
return value.version === 1 && "candidates" in value && Array.isArray(value.candidates);
}
async function rewindSessionBackfillIngestionState(params) {
const entries = await readMemoryCoreWorkspaceEntries({
namespace: SESSION_BACKFILL_REWIND_NAMESPACE,
workspaceDir: params.workspaceDir
});
const completeCoverage = entries.some((entry) => entry.key === `${SESSION_BACKFILL_BASELINE_KEY_PREFIX}${params.agentId}` && "complete" in entry.value && entry.value.agentId === params.agentId);
const batchEntries = entries.filter((entry) => isSessionBackfillRewindBatch(entry.value));
const candidates = batchEntries.flatMap((entry) => isSessionBackfillRewindBatch(entry.value) ? entry.value.candidates.filter(isSessionBackfillRewindCandidate) : []);
if (candidates.length === 0) {
await deleteSessionBackfillRewindBatches(params.workspaceDir, batchEntries);
return {
completeCoverage,
rewoundCandidates: 0
};
}
const state = await readSessionIngestionState(params.workspaceDir);
const removedHashesByScope = /* @__PURE__ */ new Map();
const rewindLineByStateKey = /* @__PURE__ */ new Map();
for (const candidate of candidates) {
const hashes = removedHashesByScope.get(candidate.scope) ?? /* @__PURE__ */ new Set();
hashes.add(candidate.hash);
removedHashesByScope.set(candidate.scope, hashes);
rewindLineByStateKey.set(candidate.stateKey, Math.min(rewindLineByStateKey.get(candidate.stateKey) ?? candidate.contentIndex, candidate.contentIndex));
}
const seenMessages = { ...state.seenMessages };
for (const [scope, removedHashes] of removedHashesByScope) {
const remaining = (seenMessages[scope] ?? []).filter((hash) => !removedHashes.has(hash));
if (remaining.length > 0) seenMessages[scope] = remaining;
else delete seenMessages[scope];
}
const files = { ...state.files };
for (const [stateKey, lastContentLine] of rewindLineByStateKey) {
const current = files[stateKey];
if (current) files[stateKey] = {
...current,
lastContentLine: Math.min(current.lastContentLine, lastContentLine)
};
}
await writeSessionIngestionState(params.workspaceDir, {
...state,
files,
seenMessages
});
await deleteSessionBackfillRewindBatches(params.workspaceDir, batchEntries);
return {
completeCoverage,
rewoundCandidates: candidates.length
};
}
async function deleteSessionBackfillRewindBatches(workspaceDir, entries) {
await Promise.all(entries.map((entry) => deleteMemoryCoreWorkspaceEntry({
namespace: SESSION_BACKFILL_REWIND_NAMESPACE,
workspaceDir,
key: entry.key
})));
}
function belongsToAgentFileState(key, agentId) {
return key.startsWith(`${agentId}:`);
}
function belongsToAgentSeenState(key, agentId) {
if (!key.startsWith("archive:")) return key.startsWith(`${agentId}:`);
const archiveAgentEnd = key.indexOf(":", 8);
if (archiveAgentEnd === -1) return agentId === "archive";
return key.slice(8, archiveAgentEnd) === agentId;
}
async function resetSessionBackfillIngestionState(params) {
const state = await readSessionIngestionState(params.workspaceDir);
await writeSessionIngestionState(params.workspaceDir, {
...state,
files: Object.fromEntries(Object.entries(state.files).filter(([key]) => !belongsToAgentFileState(key, params.agentId))),
seenMessages: Object.fromEntries(Object.entries(state.seenMessages).filter(([key]) => !belongsToAgentSeenState(key, params.agentId)))
});
}
async function drainSessionBackfill(params) {
const batches = [];
for (let batch = 1; batch <= params.maxBatches; batch += 1) {
const execution = await params.executeBatch();
batches.push(execution);
if (!execution.continuation.hasMore) return aggregateSessionBackfillBatches(batches, params.topCandidateLimit);
if (!execution.continuation.advanced) throw new Error(`Memory session-backfill stopped after ${batch} batches because the ingestion cursor did not advance.`);
}
throw new Error(`Memory session-backfill exceeded the ${params.maxBatches}-batch safety limit.`);
}
function aggregateSessionBackfillBatches(executions, topCandidateLimit) {
const first = executions[0]?.result;
if (!first) throw new Error("Memory session-backfill completed without executing a batch.");
const days = /* @__PURE__ */ new Map();
for (const execution of executions) for (const day of execution.result.days) {
const current = days.get(day.day);
days.set(day.day, {
day: day.day,
candidateCount: (current?.candidateCount ?? 0) + day.candidateCount,
topCandidates: [...current?.topCandidates ?? [], ...day.topCandidates].slice(0, topCandidateLimit)
});
}
return {
...first,
days: [...days.values()].toSorted((a, b) => a.day.localeCompare(b.day)),
candidateCount: executions.reduce((sum, execution) => sum + execution.result.candidateCount, 0),
stagedEntries: executions.reduce((sum, execution) => sum + execution.result.stagedEntries, 0),
writtenDiaryEntries: executions.reduce((sum, execution) => sum + execution.result.writtenDiaryEntries, 0),
replacedDiaryEntries: executions.reduce((sum, execution) => sum + execution.result.replacedDiaryEntries, 0),
batchCount: executions.length,
batches: executions.map((execution, index) => ({
batch: index + 1,
days: execution.result.days.length,
candidates: execution.result.candidateCount,
stagedEntries: execution.result.stagedEntries
}))
};
}
//#endregion
//#region extensions/memory-core/src/session-backfill.ts
const SESSION_BACKFILL_QUERY_PREFIX = "__dreaming_session_backfill__";
const TOP_CANDIDATE_LIMIT = 5;
const MAX_SESSION_BACKFILL_APPLY_BATCHES = 1e4;
async function listSessionBackfillSources(params) {
const corpus = await listSessionTranscriptCorpusEntriesForAgent(params.agentId, { includeRetainedSqlite: true });
const forgottenSessionIds = new Set(listMemorySessionTombstones({ agentId: params.agentId }).map((entry) => entry.sessionId));
const sources = corpus.map(sessionIngestionSourceFromCorpus).filter((entry) => entry !== null && !entry.buildOptions.generatedByDreamingNarrative && !entry.buildOptions.generatedByCronRun && !sessionExclusionReason(entry, params.admissionPolicy, forgottenSessionIds));
const canonicalPaths = new Set(sources.map((entry) => path.resolve(entry.absolutePath)));
for (const archiveFile of params.archiveFiles) {
const source = foreignSessionIngestionSource(params.agentId, archiveFile);
if (!canonicalPaths.has(source.absolutePath)) {
sources.push(source);
canonicalPaths.add(source.absolutePath);
}
}
return sources.toSorted((a, b) => a.sessionPath === b.sessionPath ? a.absolutePath.localeCompare(b.absolutePath) : a.sessionPath.localeCompare(b.sessionPath));
}
function compareSessionBackfillCandidates(a, b) {
if (a.day !== b.day) return a.day.localeCompare(b.day);
if (a.provenance.observedAt !== b.provenance.observedAt) return a.provenance.observedAt - b.provenance.observedAt;
if (a.scope !== b.scope) return a.scope.localeCompare(b.scope);
return a.lineNumber - b.lineNumber;
}
async function collectSessionBackfillCandidates(params) {
const candidates = [];
const scans = [];
const perFileCap = Math.min(80, Math.max(12, Math.ceil(240 / Math.max(1, params.sources.length))));
for (const source of params.sources) {
const scan = await scanSessionIngestionSource({
source,
previous: params.files[source.stateKey],
seenMessages: params.seenMessages,
timezone: params.timezone,
verifyContent: true,
classifyDay: (day) => (params.from === void 0 || day >= params.from) && (params.to === void 0 || day <= params.to) ? "include" : "block",
acceptProvenance: (provenance) => provenance.originClass === "owner" || provenance.originClass === "agent"
});
if (scan.status !== "scanned" || !scan.fileState) continue;
candidates.push(...scan.candidates.toSorted(compareSessionBackfillCandidates).slice(0, perFileCap));
scans.push({
candidates: scan.candidates,
contentHash: scan.fileState.contentHash,
lineCount: scan.fileState.lineCount,
mtimeMs: scan.fileState.mtimeMs,
...scan.progressBlockIndex !== void 0 ? { progressBlockIndex: scan.progressBlockIndex } : {},
scannedEndIndex: scan.scannedEndIndex,
size: scan.fileState.size,
stateKey: source.stateKey
});
}
const selected = candidates.toSorted(compareSessionBackfillCandidates).slice(0, 240);
const byDay = /* @__PURE__ */ new Map();
for (const candidate of selected) {
const bucket = byDay.get(candidate.day) ?? [];
bucket.push(candidate);
byDay.set(candidate.day, bucket);
}
return {
byDay,
scans
};
}
function mergeSessionBackfillFileProgress(params) {
const selectedHashes = new Set(params.selectedDays.flatMap((day) => day.candidates.map((candidate) => candidate.hash)));
const files = { ...params.current };
for (const scan of params.scans) {
const firstUnselected = scan.candidates.find((candidate) => !selectedHashes.has(candidate.hash));
const progressStops = [
scan.scannedEndIndex,
...firstUnselected ? [firstUnselected.contentIndex] : [],
...scan.progressBlockIndex !== void 0 ? [scan.progressBlockIndex] : []
];
files[scan.stateKey] = {
mtimeMs: scan.mtimeMs,
size: scan.size,
contentHash: scan.contentHash,
lineCount: scan.lineCount,
lastContentLine: Math.min(...progressStops)
};
}
return files;
}
function summarizeDay(day, candidates) {
return {
day,
candidateCount: candidates.length,
topCandidates: candidates.slice(0, TOP_CANDIDATE_LIMIT).map((entry) => entry.snippet)
};
}
function buildSessionBackfillDiaryEntries(params) {
const origins = [];
const markLine = (day, line, sources) => {
const entryKey = `memory:session-backfill:${hashMemoryContent(JSON.stringify([day, line]))}`;
for (const source of sources) {
const origin = source.sessionOrigin;
if (origin) origins.push({
entryKey,
...origin,
sessionKey: origin.sessionKey ?? null,
originClass: source.provenance.originClass,
observedAt: source.provenance.observedAt
});
}
return `${buildPromotionMarker(entryKey)}\n${line}`;
};
const entries = params.days.map(({ day, candidates }) => {
let bodyLines;
if (params.rem) {
const relPath = `memory/${day}.md`;
const file = previewGroundedRemForFile({
relPath,
content: `## Session transcript\n\n${candidates.map((candidate) => candidate.rendered).join("\n")}\n`,
formatItem: (line, refs) => markLine(day, line, candidates.filter((_, index) => refs.some((ref) => {
const [start, end = start] = ref.slice(relPath.length + 1).split("-").map(Number);
return index + 3 >= start && index + 3 <= end;
})))
});
if (file.facts.length || file.reflections.length || file.memoryImplications.length || file.candidates.length) bodyLines = file.renderedMarkdown.replace(/^##\s+/gm, "").split("\n");
}
bodyLines ??= [`Session backfill found ${candidates.length} trusted candidate${candidates.length === 1 ? "" : "s"}.`, ...candidates.slice(0, TOP_CANDIDATE_LIMIT).map((candidate) => markLine(day, `- ${candidate.snippet}`, [candidate]))];
return {
isoDay: day,
sourcePath: `memory/.dreams/session-corpus/${day}.txt`,
bodyLines
};
});
recordMemoryEntryOrigins({
agentId: params.agentId,
origins
});
return entries;
}
function coalesceBackfillClaims(results) {
const claims = /* @__PURE__ */ new Map();
return results.flatMap((result) => {
const snippet = result.snippet.replace(/^(?:Assistant|User):\s*/i, "").trim();
const key = snippet.replace(/\s+/g, " ").toLowerCase();
if (!key) return [];
const claim = claims.get(key) ?? {
path: result.path,
startLine: result.startLine,
endLine: result.endLine,
snippet
};
claims.set(key, claim);
return [{
...result,
...claim
}];
});
}
async function applySessionBackfillDays(params) {
const before = await readShortTermRecallEntries({
workspaceDir: params.workspaceDir,
nowMs: params.nowMs
});
for (const day of params.days) {
const grounded = coalesceBackfillClaims(await appendSessionCorpusLines({
workspaceDir: params.workspaceDir,
day: day.day,
lines: day.candidates
}));
if (grounded.length === 0) continue;
await recordGroundedShortTermCandidates({
workspaceDir: params.workspaceDir,
query: `${SESSION_BACKFILL_QUERY_PREFIX}:${day.day}`,
items: grounded.map((result) => ({
path: result.path,
startLine: result.startLine,
endLine: result.endLine,
snippet: result.snippet,
score: SESSION_INGESTION_SCORE,
dayBucket: day.day,
provenance: result.provenance,
sessionOrigin: result.sessionOrigin
})),
dedupeByQueryPerDay: true,
nowMs: params.nowMs,
...params.timezone !== void 0 ? { timezone: params.timezone } : {}
});
}
const after = await readShortTermRecallEntries({
workspaceDir: params.workspaceDir,
nowMs: params.nowMs
});
return Math.max(0, after.length - before.length);
}
async function executeSessionBackfillCore(params) {
const workspaceDir = params.workspaceDir.trim();
if (!workspaceDir) throw new Error("Memory session-backfill requires a resolvable workspace directory.");
if (params.rem && params.apply) throw new Error("Memory session-backfill --rem cannot be combined with --apply.");
const execute = () => executeSessionBackfillBatchCore({
...params,
workspaceDir
});
return params.apply || params.rem || params.rollback ? withMemoryWorkspaceLock(workspaceDir, execute) : execute();
}
async function executeSessionBackfillBatchCore(params) {
const workspaceDir = params.workspaceDir;
const nowMs = Number.isFinite(params.nowMs) ? params.nowMs : Date.now();
if (params.rollback) {
const [diary, staged] = await Promise.all([removeBackfillDiaryEntries({ workspaceDir }), removeGroundedShortTermCandidates({ workspaceDir })]);
if (!(await rewindSessionBackfillIngestionState({
workspaceDir,
agentId: params.agentId
})).completeCoverage && (diary.removed > 0 || staged.removed > 0)) await resetSessionBackfillIngestionState({
workspaceDir,
agentId: params.agentId
});
await markSessionBackfillRewindBaseline({
workspaceDir,
agentId: params.agentId
});
return {
result: {
agentId: params.agentId,
workspaceDir,
applied: false,
rem: false,
days: [],
candidateCount: 0,
stagedEntries: 0,
writtenDiaryEntries: 0,
replacedDiaryEntries: 0,
rollback: {
removedDiaryEntries: diary.removed,
removedStagedEntries: staged.removed
}
},
continuation: {
advanced: false,
hasMore: false
}
};
}
const { from, to, limitDays } = normalizeSessionBackfillSelection(params);
const state = await readSessionIngestionState(workspaceDir);
const collected = await collectSessionBackfillCandidates({
sources: await listSessionBackfillSources({
agentId: params.agentId,
archiveFiles: params.archiveFiles ?? [],
admissionPolicy: resolveAdmissionPolicy(params.pluginConfig)
}),
files: state.files,
seenMessages: state.seenMessages,
...from !== void 0 ? { from } : {},
...to !== void 0 ? { to } : {},
...params.timezone !== void 0 ? { timezone: params.timezone } : {}
});
const selectedDays = [...collected.byDay.keys()].toSorted().slice(0, limitDays).map((day) => ({
day,
candidates: collected.byDay.get(day) ?? []
}));
const days = selectedDays.map((entry) => summarizeDay(entry.day, entry.candidates));
const candidateCount = days.reduce((sum, day) => sum + day.candidateCount, 0);
const selectedHashes = new Set(selectedDays.flatMap((day) => day.candidates.map((candidate) => candidate.hash)));
const continuation = {
advanced: Boolean(params.apply) && candidateCount > 0,
hasMore: collected.scans.some((scan) => scan.candidates.some((candidate) => !selectedHashes.has(candidate.hash)))
};
let writtenDiaryEntries = 0;
let replacedDiaryEntries = 0;
let stagedEntries = 0;
if (selectedDays.length > 0 && (params.rem || params.apply)) {
const diaryEntries = buildSessionBackfillDiaryEntries({
agentId: params.agentId,
days: selectedDays,
rem: params.rem
});
const diary = await writeBackfillDiaryEntries({
workspaceDir,
entries: diaryEntries,
preserveExisting: true,
...params.timezone !== void 0 ? { timezone: params.timezone } : {}
});
writtenDiaryEntries = diary.written;
replacedDiaryEntries = diary.replaced;
}
if (params.apply) {
await recordSessionBackfillRewindBatch({
workspaceDir,
candidates: selectedDays.flatMap((day) => day.candidates.map((candidate) => ({
contentIndex: candidate.contentIndex,
hash: candidate.hash,
scope: candidate.scope,
stateKey: candidate.stateKey
})))
});
if (selectedDays.length > 0) stagedEntries = await applySessionBackfillDays({
workspaceDir,
days: selectedDays,
nowMs,
...params.timezone !== void 0 ? { timezone: params.timezone } : {}
});
const nextSeenMessages = { ...state.seenMessages };
for (const { candidates } of selectedDays) {
const hashesByScope = /* @__PURE__ */ new Map();
for (const candidate of candidates) {
const hashes = hashesByScope.get(candidate.scope) ?? [];
hashes.push(candidate.hash);
hashesByScope.set(candidate.scope, hashes);
}
for (const [scope, hashes] of hashesByScope) nextSeenMessages[scope] = mergeTrackedMessageHashes(nextSeenMessages[scope] ?? [], hashes);
}
await writeSessionIngestionState(workspaceDir, {
...state,
files: mergeSessionBackfillFileProgress({
current: state.files,
scans: collected.scans,
selectedDays
}),
seenMessages: trimTrackedSessionScopes(nextSeenMessages)
});
}
return {
result: {
agentId: params.agentId,
workspaceDir,
applied: Boolean(params.apply),
rem: Boolean(params.rem),
days,
candidateCount,
stagedEntries,
writtenDiaryEntries,
replacedDiaryEntries
},
continuation
};
}
async function executeSessionBackfill(params) {
return (await executeSessionBackfillCore(params)).result;
}
async function runSessionBackfill(params) {
if (!params.apply || params.rollback) return (await executeSessionBackfillCore(params)).result;
return await drainSessionBackfill({
executeBatch: () => executeSessionBackfillCore(params),
maxBatches: MAX_SESSION_BACKFILL_APPLY_BATCHES,
topCandidateLimit: TOP_CANDIDATE_LIMIT
});
}
async function executeSessionBackfillBatch(params) {
return await executeSessionBackfillCore(params);
}
//#endregion
export { executeSessionBackfillBatch as n, runSessionBackfill as r, executeSessionBackfill as t };