import type { Insight, InsightSearchResult, MemoryAdapter } from './types.js'; const STARTUP_CONTEXT_CANDIDATE_LIMIT = 64; /** Immutable server-derived boundary; callers never choose an adapter namespace. */ export interface OperatorMemoryScope { readonly tenantId: string; readonly ownerId: string; readonly sessionId: string; } export interface OperatorMemoryConfig { /** Adapter injection is deployment/lifecycle configuration, never caller input. */ readonly adapter: MemoryAdapter; /** Configured agent identity; it is metadata rather than a storage key default. */ readonly instanceId: string; /** Configured storage partition; callers cannot select a namespace. */ readonly namespace: string; readonly maxStartupContext?: number; redact(content: string): string; } export interface OperatorMemoryResult extends InsightSearchResult { provenance: { instanceId: string; namespace: string; source: string }; } export interface OperatorMemoryPlugin { capture( scope: OperatorMemoryScope, input: { content: string; source: string; category: string }, ): Promise; search( scope: OperatorMemoryScope, query: string, limit?: number, ): Promise; recent(scope: OperatorMemoryScope, limit?: number): Promise; stats(scope: OperatorMemoryScope): Promise<{ namespace: string; resultCount: number }>; startupContext(scope: OperatorMemoryScope): Promise; } function scopedUserId(scope: OperatorMemoryScope, namespace: string): string { const normalizedScope = normalizeScope(scope); // JSON tuple encoding avoids delimiter collisions between independently scoped IDs. return JSON.stringify([ namespace, normalizedScope.tenantId, normalizedScope.ownerId, normalizedScope.sessionId, ]); } function normalizeScope(scope: OperatorMemoryScope): OperatorMemoryScope { if (typeof scope !== 'object' || scope === null) { throw new Error('Operator memory scope is required'); } return Object.freeze({ tenantId: requiredScopeId(scope.tenantId, 'tenant ID'), ownerId: requiredScopeId(scope.ownerId, 'owner ID'), sessionId: requiredScopeId(scope.sessionId, 'session ID'), }); } function requiredScopeId(value: unknown, field: string): string { if (typeof value !== 'string' || value.trim().length === 0) { throw new Error(`Operator memory ${field} is required`); } return value.trim(); } function compareStartupContext(left: OperatorMemoryResult, right: OperatorMemoryResult): number { return ( startupSourcePriority(left.provenance.source) - startupSourcePriority(right.provenance.source) ); } function startupSourcePriority(source: string): number { if (source === 'project') return 0; if (source === 'flat-file') return 1; return 2; } function normalizeConfig(config: OperatorMemoryConfig): OperatorMemoryConfig { const instanceId = config.instanceId.trim(); const namespace = config.namespace.trim(); const maxStartupContext = config.maxStartupContext ?? 8; if (instanceId.length === 0 || namespace.length === 0) { throw new Error('Operator memory instance ID and namespace must be configured'); } if (!Number.isSafeInteger(maxStartupContext) || maxStartupContext < 1) { throw new Error('Operator memory startup context limit must be a positive integer'); } return Object.freeze({ ...config, instanceId, namespace, maxStartupContext }); } /** Creates a leaf-package, replaceable memory adapter facade. */ export function createOperatorMemoryPlugin(config: OperatorMemoryConfig): OperatorMemoryPlugin { const pluginConfig = normalizeConfig(config); const mapResult = (result: InsightSearchResult): OperatorMemoryResult => ({ ...result, provenance: { instanceId: pluginConfig.instanceId, namespace: pluginConfig.namespace, source: String(result.metadata?.['source'] ?? 'retrieval'), }, }); const search = async ( scope: OperatorMemoryScope, query: string, limit = 10, ): Promise => ( await pluginConfig.adapter.searchInsights( scopedUserId(scope, pluginConfig.namespace), query, { limit, }, ) ).map(mapResult); return { async capture(scope, input) { return pluginConfig.adapter.storeInsight({ userId: scopedUserId(scope, pluginConfig.namespace), content: pluginConfig.redact(input.content), source: input.source, category: input.category, relevanceScore: 1, metadata: { namespace: pluginConfig.namespace, instanceId: pluginConfig.instanceId, source: input.source, }, }); }, search, async recent(scope, limit = 10) { return search(scope, '*', limit); }, async stats(scope) { return { namespace: pluginConfig.namespace, resultCount: (await search(scope, '*', 100)).length, }; }, async startupContext(scope) { const maxStartupContext = pluginConfig.maxStartupContext ?? 8; // Prioritize authoritative sources within a bounded candidate window. const candidateLimit = Math.max(maxStartupContext, STARTUP_CONTEXT_CANDIDATE_LIMIT); const context = await search(scope, '*', candidateLimit); return [...context].sort(compareStartupContext).slice(0, maxStartupContext); }, }; }