From f9fbbe50166b1621472696c59afba9565f4e5e49 Mon Sep 17 00:00:00 2001 From: Josh Lehman Date: Wed, 15 Apr 2026 10:41:55 -0700 Subject: [PATCH] memory-core: precompute dreaming transcript lookup --- extensions/memory-core/src/dreaming-phases.ts | 20 ++++++++- src/memory-host-sdk/engine-qmd.ts | 3 ++ src/memory-host-sdk/host/session-files.ts | 45 +++++++++++++++---- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/extensions/memory-core/src/dreaming-phases.ts b/extensions/memory-core/src/dreaming-phases.ts index d8f7f7295fd..993129c7a70 100644 --- a/extensions/memory-core/src/dreaming-phases.ts +++ b/extensions/memory-core/src/dreaming-phases.ts @@ -6,6 +6,8 @@ import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/memo import { buildSessionEntry, listSessionFilesForAgent, + loadDreamingNarrativeTranscriptPathSetForAgent, + normalizeSessionTranscriptPathForComparison, parseUsageCountedSessionIdFromFileName, sessionPathForFile, } from "openclaw/plugin-sdk/memory-core-host-engine-qmd"; @@ -688,13 +690,25 @@ async function collectSessionIngestionBatches(params: { const nextSeenMessages: Record = { ...params.state.seenMessages }; let changed = false; - const sessionFiles: Array<{ agentId: string; absolutePath: string; sessionPath: string }> = []; + const sessionFiles: Array<{ + agentId: string; + absolutePath: string; + generatedByDreamingNarrative: boolean; + sessionPath: string; + }> = []; for (const agentId of agentIds) { const files = await listSessionFilesForAgent(agentId); + const dreamingTranscriptPaths = + files.length > 0 + ? loadDreamingNarrativeTranscriptPathSetForAgent(agentId) + : new Set(); for (const absolutePath of files) { sessionFiles.push({ agentId, absolutePath, + generatedByDreamingNarrative: dreamingTranscriptPaths.has( + normalizeSessionTranscriptPathForComparison(absolutePath), + ), sessionPath: sessionPathForFile(absolutePath), }); } @@ -751,7 +765,9 @@ async function collectSessionIngestionBatches(params: { continue; } - const entry = await buildSessionEntry(file.absolutePath); + const entry = await buildSessionEntry(file.absolutePath, { + generatedByDreamingNarrative: file.generatedByDreamingNarrative, + }); if (!entry) { continue; } diff --git a/src/memory-host-sdk/engine-qmd.ts b/src/memory-host-sdk/engine-qmd.ts index 1c4ac361c91..8ef479b3e4d 100644 --- a/src/memory-host-sdk/engine-qmd.ts +++ b/src/memory-host-sdk/engine-qmd.ts @@ -4,7 +4,10 @@ export { extractKeywords, isQueryStopWordToken } from "./host/query-expansion.js export { buildSessionEntry, listSessionFilesForAgent, + loadDreamingNarrativeTranscriptPathSetForAgent, + normalizeSessionTranscriptPathForComparison, sessionPathForFile, + type BuildSessionEntryOptions, type SessionFileEntry, } from "./host/session-files.js"; export { parseUsageCountedSessionIdFromFileName } from "../config/sessions/artifacts.js"; diff --git a/src/memory-host-sdk/host/session-files.ts b/src/memory-host-sdk/host/session-files.ts index 61dc8fe5d51..5865cf9bbb3 100644 --- a/src/memory-host-sdk/host/session-files.ts +++ b/src/memory-host-sdk/host/session-files.ts @@ -25,6 +25,11 @@ export type SessionFileEntry = { generatedByDreamingNarrative?: boolean; }; +export type BuildSessionEntryOptions = { + /** Optional preclassification from a caller-managed dreaming transcript lookup. */ + generatedByDreamingNarrative?: boolean; +}; + function isDreamingNarrativeBootstrapRecord(record: unknown): boolean { if (!record || typeof record !== "object" || Array.isArray(record)) { return false; @@ -98,6 +103,10 @@ function normalizeComparablePath(pathname: string): string { return process.platform === "win32" ? resolved.toLowerCase() : resolved; } +export function normalizeSessionTranscriptPathForComparison(pathname: string): string { + return normalizeComparablePath(pathname); +} + function resolveSessionStoreTranscriptPath( sessionsDir: string, entry: { sessionFile?: unknown; sessionId?: unknown } | undefined, @@ -115,21 +124,37 @@ function resolveSessionStoreTranscriptPath( return null; } -function isDreamingNarrativeTranscriptFromSessionStore(absPath: string): boolean { - const sessionsDir = path.dirname(absPath); +export function loadDreamingNarrativeTranscriptPathSetForSessionsDir( + sessionsDir: string, +): ReadonlySet { const storePath = path.join(sessionsDir, "sessions.json"); - const normalizedAbsPath = normalizeComparablePath(absPath); const store = loadSessionStore(storePath); + const dreamingTranscriptPaths = new Set(); for (const [sessionKey, entry] of Object.entries(store)) { if (!isDreamingNarrativeSessionStoreKey(sessionKey)) { continue; } const transcriptPath = resolveSessionStoreTranscriptPath(sessionsDir, entry); - if (transcriptPath === normalizedAbsPath) { - return true; + if (transcriptPath) { + dreamingTranscriptPaths.add(transcriptPath); } } - return false; + return dreamingTranscriptPaths; +} + +export function loadDreamingNarrativeTranscriptPathSetForAgent( + agentId: string, +): ReadonlySet { + return loadDreamingNarrativeTranscriptPathSetForSessionsDir( + resolveSessionTranscriptsDirForAgent(agentId), + ); +} + +function isDreamingNarrativeTranscriptFromSessionStore(absPath: string): boolean { + const sessionsDir = path.dirname(absPath); + const normalizedAbsPath = normalizeComparablePath(absPath); + const dreamingTranscriptPaths = loadDreamingNarrativeTranscriptPathSetForSessionsDir(sessionsDir); + return dreamingTranscriptPaths.has(normalizedAbsPath); } export async function listSessionFilesForAgent(agentId: string): Promise { @@ -207,7 +232,10 @@ function parseSessionTimestampMs( return 0; } -export async function buildSessionEntry(absPath: string): Promise { +export async function buildSessionEntry( + absPath: string, + opts: BuildSessionEntryOptions = {}, +): Promise { try { const stat = await fs.stat(absPath); const raw = await fs.readFile(absPath, "utf-8"); @@ -215,7 +243,8 @@ export async function buildSessionEntry(absPath: string): Promise