import { listPinAssets, type PinAsset } from "./memory-assets.js"; import * as envConfig from "./env-config.js"; import { buildStableContextSections } from "./context-composer-stable.js"; import { buildAssociativeNestEntityFallbackQuery, buildScopedEntityFallbackQuery, buildStableQuery, buildStylePreferenceFallbackQuery, buildTaskQuery, formatLatestCheckpointHeadline, } from "./context-composer-queries.js"; import { cleanText, dedupeText } from "./context-composer-text.js"; import { buildTaskResultSections, } from "./context-composer-task-results.js"; import type { RetrievalContext, RetrievalResult } from "./retriever.js"; import type { EssentialContext, ResumeContextResponse, SessionCheckpointRecord } from "./session-schema.js"; import { ResumeContextRequestSchema, ResumeContextResponseSchema } from "./session-schema.js"; import { formatCheckpointRecallSummary } from "./session-output.js"; import { STRONG_WORKFLOW_CUE_TERMS, looksLikeContinuityTask, looksLikeRecallOnlyTask, looksLikeStyleTask, } from "./term-registry.js"; import { synthesizeSection } from "./result-synthesizer.js"; import type { LLMClient } from "./llm-client.js"; import { collapseResults, estimateTokens, type CollapseInput } from "./context-collapse-renderer.js"; import { filterByRelevance } from "./post-retrieval-filter.js"; import { reconstruct as runReconstruction } from "./context-reconstructor.js"; import { parseNarrative, isNarrativeModeEnabled } from "./narrative-schema.js"; type ResumeCategory = "profile" | "preferences" | "entities" | "patterns" | "cases"; interface ResumeRetriever { retrieve(context: RetrievalContext): Promise; /** Optional: 影子记录 resume reconstruction 引用的记忆(injection-vs-use)。未实现则 no-op。 */ recordReconstructionUsage?(citedIds: string[]): Promise; } interface CheckpointLookup { getLatest(query?: { sessionId?: string; scope?: string }): Promise; } export interface ResumeContextDeps { retriever: ResumeRetriever; checkpointStore: CheckpointLookup; listPins?: (limit?: number) => Array; /** Optional LLM client for Tier 3.5 result synthesis. */ llm?: LLMClient | null; } async function retrieveCandidates( retriever: ResumeRetriever, params: { category?: ResumeCategory; query: string; limit: number; scope?: string; }, ): Promise { const { category, query, limit, scope } = params; if (!scope) { return retriever.retrieve({ query, limit, ...(category ? { category } : {}), source: "auto-recall", }); } const [scoped, global] = await Promise.all([ retriever.retrieve({ query, limit, ...(category ? { category } : {}), scopeFilter: [scope], source: "auto-recall", }), retriever.retrieve({ query, limit: Math.min(10, limit * 2), ...(category ? { category } : {}), source: "auto-recall", }), ]); const seen = new Set(); const merged: RetrievalResult[] = []; for (const result of [...scoped, ...global]) { if (seen.has(result.entry.id)) continue; seen.add(result.entry.id); merged.push(result); if (merged.length >= Math.min(10, limit * 2)) break; } return merged; } function mergeRetrievalResults(resultSets: RetrievalResult[][], limit: number): RetrievalResult[] { const seen = new Set(); const merged: RetrievalResult[] = []; for (const set of resultSets) { for (const result of set) { if (seen.has(result.entry.id)) continue; seen.add(result.entry.id); merged.push(result); if (merged.length >= limit) return merged; } } return merged; } async function resolveLatestCheckpoint( checkpointStore: CheckpointLookup, params: { includeLatestCheckpoint: boolean; sessionId?: string; scope?: string; }, ): Promise { if (!params.includeLatestCheckpoint) return null; if (params.sessionId) { const bySession = await checkpointStore.getLatest({ sessionId: params.sessionId }); if (bySession) return bySession; } if (params.scope) { return checkpointStore.getLatest({ scope: params.scope }); } return null; } function buildSummary(params: { stableContext: string[]; relevantPatterns: string[]; recentCases: string[]; latestCheckpoint: SessionCheckpointRecord | null; }): string { const parts: string[] = []; if (params.latestCheckpoint) { parts.push( formatLatestCheckpointHeadline( params.latestCheckpoint.sessionId, params.latestCheckpoint.updatedAt, formatCheckpointRecallSummary(params.latestCheckpoint), ), ); } if (params.stableContext.length > 0) { parts.push(`Stable context: ${params.stableContext.slice(0, 2).map((item) => cleanText(item, 120)).join(" | ")}`); } parts.push( `Loaded ${params.stableContext.length} stable context item(s), ${params.relevantPatterns.length} pattern(s), and ${params.recentCases.length} case(s).`, ); return cleanText(parts.join(" "), 800); } // --------------------------------------------------------------------------- // CC-8: Post-Compact Reconstruction — assemble essential context // --------------------------------------------------------------------------- const ESSENTIAL_CONTEXT_TOKEN_BUDGET = 2000; function buildEssentialContext(params: { pinAssets: Array; patternResults: RetrievalResult[]; latestCheckpoint: SessionCheckpointRecord | null; }): EssentialContext | undefined { let tokensUsed = 0; // 1. Pinned memories: take up to 3 most recent pins' summaries const pinnedMemories: string[] = []; for (const pin of params.pinAssets.slice(0, 3)) { const text = pin.summary || pin.title; const cost = estimateTokens(text); if (tokensUsed + cost > ESSENTIAL_CONTEXT_TOKEN_BUDGET) break; pinnedMemories.push(text); tokensUsed += cost; } // 2. Active patterns: take top 1-2 by score const activePatterns: string[] = []; const sortedPatterns = [...params.patternResults].sort((a, b) => b.score - a.score); for (const pattern of sortedPatterns.slice(0, 2)) { const text = cleanText(pattern.entry.text, 200); const cost = estimateTokens(text); if (tokensUsed + cost > ESSENTIAL_CONTEXT_TOKEN_BUDGET) break; activePatterns.push(text); tokensUsed += cost; } // 3. Open loops from latest checkpoint const openLoops: string[] = []; if (params.latestCheckpoint?.openLoops) { for (const loop of params.latestCheckpoint.openLoops.slice(0, 3)) { const cost = estimateTokens(loop); if (tokensUsed + cost > ESSENTIAL_CONTEXT_TOKEN_BUDGET) break; openLoops.push(loop); tokensUsed += cost; } } // Return undefined if nothing was collected if (pinnedMemories.length === 0 && activePatterns.length === 0 && openLoops.length === 0) { return undefined; } return { ...(pinnedMemories.length > 0 ? { pinnedMemories } : {}), ...(activePatterns.length > 0 ? { activePatterns } : {}), ...(openLoops.length > 0 ? { openLoops } : {}), }; } // --------------------------------------------------------------------------- // MP-3: Ultra-Light Wake-up — <300 token resume for low-budget terminals // --------------------------------------------------------------------------- const LIGHT_TOKEN_BUDGET = 300; const LIGHT_MEMORY_CHAR_LIMIT = 200; // ~50 tokens export interface LightResumeResult { text: string; resolvedScope?: string; generatedAt: string; } export async function composeLightResumeContext( deps: ResumeContextDeps, rawInput: unknown, ): Promise { const input = ResumeContextRequestSchema.parse(rawInput); const latestCheckpoint = await resolveLatestCheckpoint(deps.checkpointStore, { includeLatestCheckpoint: true, sessionId: input.sessionId, scope: input.scope, }); const resolvedScope = input.scope || latestCheckpoint?.resolvedScope; // Retrieve top 3 stable memories (profile + preferences + entities combined) const stableResults = await retrieveCandidates(deps.retriever, { query: input.task || "identity key facts and preferences", limit: 3, scope: resolvedScope, }); // Pin assets provide high-signal stable context const pinAssets = (deps.listPins || listPinAssets)(3); const parts: string[] = []; let tokensUsed = 0; // 1. Checkpoint summary (1 sentence, ~30 tokens) if (latestCheckpoint) { const cpLine = `Last session (${latestCheckpoint.sessionId}): ${cleanText(latestCheckpoint.summary, 120)}`; parts.push(cpLine); tokensUsed += estimateTokens(cpLine); } // 2. Top pinned memories (most reliable stable context) if (pinAssets.length > 0) { for (const pin of pinAssets) { if (tokensUsed >= LIGHT_TOKEN_BUDGET - 40) break; const line = `- ${cleanText(pin.summary || pin.title, LIGHT_MEMORY_CHAR_LIMIT)}`; parts.push(line); tokensUsed += estimateTokens(line); } } // 3. Top stable memories (fill remaining budget) if (stableResults.length > 0 && tokensUsed < LIGHT_TOKEN_BUDGET - 40) { const pinTexts = new Set(pinAssets.map(p => (p.summary || p.title).toLowerCase())); for (const r of stableResults) { if (tokensUsed >= LIGHT_TOKEN_BUDGET - 40) break; const textLower = r.entry.text.toLowerCase().slice(0, 100); if (pinTexts.has(textLower)) continue; const line = `- ${cleanText(r.entry.text, LIGHT_MEMORY_CHAR_LIMIT)}`; parts.push(line); tokensUsed += estimateTokens(line); } } // 4. Upgrade hint parts.push("\nFor complete context, call resume_context(mode='full')."); return { text: parts.join("\n"), resolvedScope, generatedAt: new Date().toISOString(), }; } export async function composeResumeContext( deps: ResumeContextDeps, rawInput: unknown, ): Promise { const input = ResumeContextRequestSchema.parse(rawInput); const latestCheckpoint = await resolveLatestCheckpoint(deps.checkpointStore, { includeLatestCheckpoint: input.includeLatestCheckpoint, sessionId: input.sessionId, scope: input.scope, }); const resolvedScope = input.scope || latestCheckpoint?.resolvedScope; const taskSeed = input.task || latestCheckpoint?.task || latestCheckpoint?.summary; const stableLimit = input.limitPerSection; const taskLimit = input.limitPerSection; const styleFocusedTask = looksLikeStyleTask(taskSeed) || input.profile === "writing"; const recallOnlyTask = looksLikeRecallOnlyTask(taskSeed) && styleFocusedTask; const preferenceQueries = dedupeText([ buildStableQuery("preferences", taskSeed), ...(styleFocusedTask ? [buildStylePreferenceFallbackQuery(taskSeed)] : []), ], 2); const entityQueries = dedupeText([ buildStableQuery("entities", taskSeed), ...(resolvedScope ? [buildScopedEntityFallbackQuery(resolvedScope, taskSeed)] : []), ...(!resolvedScope ? [buildAssociativeNestEntityFallbackQuery(taskSeed)] : []), ], 3); const [profileResults, preferenceResultSets, entityResultSets, patternResults, caseResults] = await Promise.all([ retrieveCandidates(deps.retriever, { category: "profile", query: buildStableQuery("profile", taskSeed), limit: Math.max(2, stableLimit), scope: resolvedScope, }), Promise.all(preferenceQueries.map((query) => retrieveCandidates(deps.retriever, { category: "preferences", query, limit: Math.max(2, stableLimit), scope: resolvedScope, }) )), Promise.all(entityQueries.map((query) => retrieveCandidates(deps.retriever, { category: "entities", query, limit: Math.max(2, stableLimit), scope: resolvedScope, }) )), retrieveCandidates(deps.retriever, { category: "patterns", query: buildTaskQuery("patterns", taskSeed), limit: taskLimit, scope: resolvedScope, }), retrieveCandidates(deps.retriever, { category: "cases", query: buildTaskQuery("cases", taskSeed), limit: taskLimit, scope: resolvedScope, }), ]); const preferenceResults = mergeRetrievalResults(preferenceResultSets, Math.max(4, stableLimit * 3)); const entityResults = mergeRetrievalResults(entityResultSets, Math.max(4, stableLimit * 3)); // E-2: Post-retrieval LLM relevance filter for pattern/case results. // Only fires when LLM is available; stable context (profile/preferences/entities) skipped // because those are identity-level and already curated by the selection pipeline. const llm = deps.llm ?? null; const [filteredPatterns, filteredCases] = llm && taskSeed ? await Promise.all([ filterByRelevance(patternResults, taskSeed, llm), filterByRelevance(caseResults, taskSeed, llm), ]) : [patternResults, caseResults]; const continuityTask = looksLikeContinuityTask(taskSeed); const pinAssets = (deps.listPins || listPinAssets)(Math.max(4, stableLimit * 2)); const { preferenceContext, stableContext, } = buildStableContextSections({ profileResults, preferenceResults, entityResults, pinAssets, latestCheckpoint, taskSeed, scope: resolvedScope, stableLimit, styleFocusedTask, }); const { relevantPatterns, recentCases } = await buildTaskResultSections({ retrieveCandidates: ({ category, query, limit, scope }) => retrieveCandidates(deps.retriever, { ...(category ? { category } : {}), query, limit, scope, }), patternResults: filteredPatterns, caseResults: filteredCases, continuityTask, hasLatestCheckpoint: Boolean(latestCheckpoint), taskLimit, taskSeed, scope: resolvedScope, strongWorkflowCueTerms: STRONG_WORKFLOW_CUE_TERMS, }); // Tier 3.5: Optionally synthesize sections into coherent narratives via LLM. // Only activates when RECALLNEST_SYNTHESIZE=true and llm is provided. const queryHint = taskSeed || resolvedScope || "general"; const [synthStable, synthPatterns, synthCases] = await Promise.all([ synthesizeSection(stableContext, queryHint, llm), synthesizeSection(relevantPatterns, queryHint, llm), synthesizeSection(recentCases, queryHint, llm), ]); // HP-narrative: Group recalled items by life period for narrative context let narrativeGroups: Array<{ period: string; items: string[] }> | undefined; if (isNarrativeModeEnabled()) { const allNarrativeResults = [...profileResults, ...preferenceResults, ...entityResults, ...filteredPatterns, ...filteredCases]; const periodMap = new Map(); for (const r of allNarrativeResults) { const narrative = parseNarrative(r.entry.metadata); if (!narrative) continue; const items = periodMap.get(narrative.lifePeriodLabel) ?? []; items.push(cleanText(r.entry.text, 120)); periodMap.set(narrative.lifePeriodLabel, items); } if (periodMap.size > 0) { narrativeGroups = [...periodMap.entries()] .map(([period, items]) => ({ period, items: items.slice(0, 5) })); } } // CC-7: Collapse rendering — build mixed-granularity view of all recalled items. // Gathers all retrieval results, deduplicates, and renders at L0/L1/L2 based on score. const allResults: RetrievalResult[] = []; const seenIds = new Set(); for (const r of [...profileResults, ...preferenceResults, ...entityResults, ...filteredPatterns, ...filteredCases]) { if (!seenIds.has(r.entry.id)) { seenIds.add(r.entry.id); allResults.push(r); } } const collapseInput: CollapseInput[] = allResults.map(r => ({ entryId: r.entry.id, text: r.entry.text, metadata: r.entry.metadata, score: r.score, timestamp: r.entry.timestamp, })); const collapsedItems = collapseInput.length > 0 ? collapseResults(collapseInput) : undefined; // CC-8: Build essential context from pinned memories, top patterns, and open loops. const essentialContext = buildEssentialContext({ pinAssets, patternResults: filteredPatterns, latestCheckpoint, }); // Phase 4: Constructive retrieval reconstruction for resume context // Pass checkpoint openLoops/nextActions/scope into reconstruction prompt let reconstructedContext: string | undefined; let reconstructionConfidence: number | undefined; let reconstructionContradictions: Array<{ memoryIds: [string, string]; description: string }> | undefined; const constructiveFlag = envConfig.constructiveRetrieval(); if (constructiveFlag && deps.llm?.isAvailable?.()) { const allReconResults = [...profileResults, ...preferenceResults, ...entityResults, ...filteredPatterns, ...filteredCases]; if (allReconResults.length >= 3) { try { const taskQuery = latestCheckpoint?.summary ?? taskSeed ?? "general context"; const checkpointContext = latestCheckpoint ? { openLoops: latestCheckpoint.openLoops, nextActions: latestCheckpoint.nextActions, scope: resolvedScope, } : resolvedScope ? { scope: resolvedScope } : undefined; const recon = await runReconstruction( { query: taskQuery, results: allReconResults, mode: "resume", maxTokens: 600, checkpointContext }, deps.llm, ); if (recon.reconstructed) { reconstructedContext = recon.reconstructed; reconstructionConfidence = recon.confidence; } if (recon.contradictions.length > 0) { reconstructionContradictions = recon.contradictions; } // Shadow usage:resume reconstruction 真正引用的记忆记 use 计数(不动 ranking/forget)。 if (recon.sources.length > 0) { await deps.retriever.recordReconstructionUsage?.(recon.sources.map(s => s.id)); } } catch { /* silent fallback */ } } } const response = { summary: buildSummary({ stableContext: synthStable, relevantPatterns: synthPatterns, recentCases: synthCases, latestCheckpoint, }), resolvedScope, stableContext: synthStable, relevantPatterns: synthPatterns, recentCases: synthCases, collapsedItems: collapsedItems && collapsedItems.length > 0 ? collapsedItems : undefined, essentialContext, latestCheckpoint: latestCheckpoint ? { sessionId: latestCheckpoint.sessionId, resolvedScope: latestCheckpoint.resolvedScope, summary: formatCheckpointRecallSummary(latestCheckpoint), updatedAt: latestCheckpoint.updatedAt, } : undefined, injectionHint: "user_attachment" as const, ephemeral: true, responseMode: recallOnlyTask ? "recall-only" as const : "default" as const, responseGuidance: recallOnlyTask ? ( stableContext.length <= 1 ? "Recall-only mode: answer from the recalled stable context item only. Restate it briefly and do not expand into extra rules, examples, or local writing docs unless the user explicitly asks." : "Recall-only mode: answer only from the recalled stable context items. Keep the reply brief and do not expand into extra rules, examples, or local writing docs unless the user explicitly asks." ) : undefined, reconstructedContext, reconstructionConfidence, reconstructionContradictions, narrativeGroups, generatedAt: new Date().toISOString(), }; return ResumeContextResponseSchema.parse(response); }