/**
* Memory recall tool - Token-aware semantic search
* v3.0: Dual-response pattern (index + details) for skill-like progressive loading
*/
import type { DbDriver } from '../database/db-driver.js';
import type {
SearchOptions,
SearchOptionsInternal,
RecallResponse,
Memory,
MinimalMemory,
FormattedMemory,
Entity,
Provenance,
} from '../types/index.js';
import { semanticSearch } from '../search/semantic-search.js';
import { formatMemory, formatMemoryList, getMemoryTokenCount } from './response-formatter.js';
import { now } from '../database/connection.js';
import { estimateTokens } from '../utils/token-estimator.js';
/**
* Recall memories using semantic search with intelligent token budgeting
* Returns: index (all matches as summaries) + details (top matches with full content)
*/
export async function memoryRecall(
db: DbDriver,
options: SearchOptions
): Promise<RecallResponse> {
try {
console.error('[memoryRecall] Starting recall with options:', JSON.stringify(options));
// Set defaults
const limit = Math.min(options.limit || 20, 50); // Max 50
const maxTokens = options.max_tokens || 1000; // Default 1k token budget
console.error(`[memoryRecall] Processed options - limit: ${limit}, maxTokens: ${maxTokens}`);
// Perform semantic search (get all matches up to limit)
const searchOptions: SearchOptionsInternal = {
query: options.query,
limit,
offset: 0,
includeExpired: false,
};
if (options.type) searchOptions.type = options.type;
if (options.entities) searchOptions.entities = options.entities;
console.error('[memoryRecall] Calling semanticSearch...');
const { results, totalCount } = semanticSearch(db, searchOptions);
console.error(`[memoryRecall] Search returned ${results.length} results, total count: ${totalCount}`);
// Track access for frequency tracking
const currentTime = now();
for (const result of results) {
// Increment access_count and update last_accessed
db.prepare(
`UPDATE memories
SET access_count = access_count + 1, last_accessed = ?
WHERE id = ?`
).run(currentTime, result.id);
}
// Build options map for formatting (includes entities and provenance)
const optionsMap = new Map<string, { entities?: Entity[]; provenance?: Provenance[] }>();
for (const result of results) {
optionsMap.set(result.id, {
entities: result.entities,
provenance: result.provenance,
});
}
// Convert MemorySearchResult[] to Memory[] for formatting
const memories: Memory[] = results.map((result) => ({
id: result.id,
content: result.content,
summary: result.summary,
type: result.type,
importance: result.importance,
created_at: result.created_at,
last_accessed: result.last_accessed,
access_count: result.access_count,
expires_at: result.expires_at,
metadata: result.metadata,
is_deleted: result.is_deleted,
}));
// PHASE 1: Create index (all matches as minimal summaries)
const index: MinimalMemory[] = formatMemoryList(memories, 'minimal') as MinimalMemory[];
const indexTokens = estimateTokens(index);
// PHASE 2: Fill remaining budget with detailed content
const details: FormattedMemory[] = [];
let tokensUsed = indexTokens;
for (const memory of memories) {
// Format with standard detail (content + entities + timestamps)
const options = optionsMap.get(memory.id) || {};
const formatted = formatMemory(memory, 'standard', options);
const memoryTokens = getMemoryTokenCount(formatted);
// Check if it fits in remaining budget
if (tokensUsed + memoryTokens <= maxTokens) {
details.push(formatted);
tokensUsed += memoryTokens;
} else {
// Budget exhausted, stop adding details
break;
}
}
// Build response with dual structure
const response: RecallResponse = {
index,
details,
total_count: totalCount,
has_more: totalCount > limit,
tokens_used: tokensUsed,
query: options.query,
};
console.error(`[memoryRecall] Built response - index: ${index.length} items, details: ${details.length} items, tokens: ${tokensUsed}`);
console.error('[memoryRecall] Recall completed successfully');
return response;
} catch (error) {
console.error('[memoryRecall] ERROR:', error);
if (error instanceof Error) {
console.error('[memoryRecall] Error stack:', error.stack);
}
throw error;
}
}