@rine-kit/agent-memory 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,54 @@
1
+ function failed(status) {
2
+ return status === 'retry' || status === 'dead' || status === 'timed_out';
3
+ }
4
+ async function drain(worker, stage, maxBatches, signal) {
5
+ let processed = 0;
6
+ for (let batch = 0; batch < maxBatches && !signal.aborted; batch += 1) {
7
+ const status = await worker.processNext(stage, signal);
8
+ if (status === 'skipped')
9
+ break;
10
+ if (failed(status))
11
+ throw new Error(`memory_dreaming_${stage}_failed`);
12
+ processed += 1;
13
+ }
14
+ if (signal.aborted)
15
+ throw new Error('memory_dreaming_aborted');
16
+ return processed;
17
+ }
18
+ /** Generic bounded Light -> REM -> Deep orchestration over host-owned workers. */
19
+ export async function runMemoryDreamingStages(worker, signal, options = {}) {
20
+ const maxLightBatches = options.maxLightBatches ?? 8;
21
+ const maxRemBatches = options.maxRemBatches ?? 8;
22
+ if (!Number.isSafeInteger(maxLightBatches) ||
23
+ maxLightBatches < 1 ||
24
+ maxLightBatches > 32 ||
25
+ !Number.isSafeInteger(maxRemBatches) ||
26
+ maxRemBatches < 1 ||
27
+ maxRemBatches > 32) {
28
+ throw new Error('memory_dreaming_limits_invalid');
29
+ }
30
+ const now = options.now ?? (() => new Date());
31
+ const startedAt = now().toISOString();
32
+ const light = await drain(worker, 'light', maxLightBatches, signal);
33
+ const rem = await drain(worker, 'rem', maxRemBatches, signal);
34
+ if (signal.aborted)
35
+ throw new Error('memory_dreaming_aborted');
36
+ const deep = await worker.processDeep(signal);
37
+ if (signal.aborted)
38
+ throw new Error('memory_dreaming_aborted');
39
+ if (failed(deep.status))
40
+ throw new Error('memory_dreaming_deep_failed');
41
+ return {
42
+ startedAt,
43
+ completedAt: now().toISOString(),
44
+ stages: [
45
+ { stage: 'light', status: light ? 'completed' : 'skipped', processed: light },
46
+ { stage: 'rem', status: rem ? 'completed' : 'skipped', processed: rem },
47
+ {
48
+ stage: 'deep',
49
+ status: deep.status === 'processed' ? 'completed' : 'skipped',
50
+ processed: deep.status === 'processed' ? deep.processed : 0,
51
+ },
52
+ ],
53
+ };
54
+ }
@@ -0,0 +1,3 @@
1
+ /** Returns the next strictly future 03:00 wall-clock instant; startup never catches up a missed run. */
2
+ export declare function nextMemoryDreamingAt(now: number, timezone: string): number;
3
+ export declare function memoryDreamingLocalDateAt(timestamp: number, timezone: string): string;
@@ -0,0 +1,72 @@
1
+ function localParts(timestamp, timezone) {
2
+ const formatter = new Intl.DateTimeFormat('en-CA', {
3
+ timeZone: timezone,
4
+ calendar: 'iso8601',
5
+ numberingSystem: 'latn',
6
+ year: 'numeric',
7
+ month: '2-digit',
8
+ day: '2-digit',
9
+ hour: '2-digit',
10
+ minute: '2-digit',
11
+ second: '2-digit',
12
+ hourCycle: 'h23',
13
+ });
14
+ const values = new Map(formatter
15
+ .formatToParts(new Date(timestamp))
16
+ .filter((part) => part.type !== 'literal')
17
+ .map((part) => [part.type, Number(part.value)]));
18
+ const result = {
19
+ year: values.get('year'),
20
+ month: values.get('month'),
21
+ day: values.get('day'),
22
+ hour: values.get('hour'),
23
+ minute: values.get('minute'),
24
+ second: values.get('second'),
25
+ };
26
+ if (Object.values(result).some((value) => value === undefined || !Number.isSafeInteger(value))) {
27
+ throw new Error('memory_dreaming_timezone_invalid');
28
+ }
29
+ return result;
30
+ }
31
+ function wallClockToTimestamp(value, timezone) {
32
+ const target = Date.UTC(value.year, value.month - 1, value.day, value.hour, value.minute, value.second);
33
+ let timestamp = target;
34
+ for (let attempt = 0; attempt < 4; attempt += 1) {
35
+ const actual = localParts(timestamp, timezone);
36
+ const represented = Date.UTC(actual.year, actual.month - 1, actual.day, actual.hour, actual.minute, actual.second);
37
+ const delta = target - represented;
38
+ if (delta === 0)
39
+ return timestamp;
40
+ timestamp += delta;
41
+ }
42
+ throw new Error('memory_dreaming_local_time_unresolvable');
43
+ }
44
+ function addLocalDays(value, days) {
45
+ const date = new Date(Date.UTC(value.year, value.month - 1, value.day + days));
46
+ return {
47
+ year: date.getUTCFullYear(),
48
+ month: date.getUTCMonth() + 1,
49
+ day: date.getUTCDate(),
50
+ hour: value.hour,
51
+ minute: value.minute,
52
+ second: value.second,
53
+ };
54
+ }
55
+ function dateKey(value) {
56
+ return `${String(value.year).padStart(4, '0')}-${String(value.month).padStart(2, '0')}-${String(value.day).padStart(2, '0')}`;
57
+ }
58
+ /** Returns the next strictly future 03:00 wall-clock instant; startup never catches up a missed run. */
59
+ export function nextMemoryDreamingAt(now, timezone) {
60
+ if (!Number.isFinite(now))
61
+ throw new Error('memory_dreaming_clock_invalid');
62
+ const localNow = localParts(now, timezone);
63
+ const todayAtThree = wallClockToTimestamp({ ...localNow, hour: 3, minute: 0, second: 0 }, timezone);
64
+ if (todayAtThree > now)
65
+ return todayAtThree;
66
+ return wallClockToTimestamp({ ...addLocalDays(localNow, 1), hour: 3, minute: 0, second: 0 }, timezone);
67
+ }
68
+ export function memoryDreamingLocalDateAt(timestamp, timezone) {
69
+ if (!Number.isFinite(timestamp))
70
+ throw new Error('memory_dreaming_clock_invalid');
71
+ return dateKey(localParts(timestamp, timezone));
72
+ }
@@ -0,0 +1,22 @@
1
+ import type { DreamingReport } from '../contracts.js';
2
+ export interface MemoryDreamingSchedulePoint {
3
+ timezone: string;
4
+ localDate: string;
5
+ scheduledAt: Date;
6
+ }
7
+ export interface MemoryDreamingRunStore<TIdentity> {
8
+ claim(identity: TIdentity): Promise<string | null>;
9
+ finish(identity: TIdentity, runId: string, status: 'succeeded' | 'failed'): Promise<void>;
10
+ }
11
+ export interface MemoryDreamingSchedulerOptions<TIdentity> {
12
+ timezone?: string;
13
+ fallbackTimezone: string;
14
+ identity(point: MemoryDreamingSchedulePoint): TIdentity;
15
+ now?: () => number;
16
+ sleepUntil?: (timestamp: number, signal: AbortSignal) => Promise<void>;
17
+ }
18
+ export interface MemoryDreamingRunner {
19
+ runDreaming(signal?: AbortSignal): Promise<DreamingReport>;
20
+ }
21
+ /** Single host-process scheduler: one local 03:00 run, durable claim, and next-date retry. */
22
+ export declare function runMemoryDreamingScheduler<TIdentity>(runner: MemoryDreamingRunner, store: MemoryDreamingRunStore<TIdentity>, signal: AbortSignal, onRun: (value: Record<string, unknown>) => void, options: MemoryDreamingSchedulerOptions<TIdentity>): Promise<void>;
@@ -0,0 +1,96 @@
1
+ import { memoryDreamingLocalDateAt, nextMemoryDreamingAt } from './schedule.js';
2
+ async function defaultSleepUntil(timestamp, signal) {
3
+ if (signal.aborted)
4
+ return;
5
+ const delay = Math.max(0, timestamp - Date.now());
6
+ await new Promise((resolve) => {
7
+ const finish = () => {
8
+ clearTimeout(timer);
9
+ signal.removeEventListener('abort', finish);
10
+ resolve();
11
+ };
12
+ const timer = setTimeout(finish, delay);
13
+ signal.addEventListener('abort', finish, { once: true });
14
+ });
15
+ }
16
+ /** Single host-process scheduler: one local 03:00 run, durable claim, and next-date retry. */
17
+ export async function runMemoryDreamingScheduler(runner, store, signal, onRun, options) {
18
+ const now = options.now ?? Date.now;
19
+ const sleepUntil = options.sleepUntil ?? defaultSleepUntil;
20
+ const requestedTimezone = options.timezone?.trim() || options.fallbackTimezone;
21
+ try {
22
+ nextMemoryDreamingAt(now(), requestedTimezone);
23
+ }
24
+ catch {
25
+ onRun({ event: 'dreaming_timezone_fallback', timezone: options.fallbackTimezone });
26
+ }
27
+ const effectiveTimezone = (() => {
28
+ try {
29
+ nextMemoryDreamingAt(now(), requestedTimezone);
30
+ return requestedTimezone;
31
+ }
32
+ catch {
33
+ return options.fallbackTimezone;
34
+ }
35
+ })();
36
+ while (!signal.aborted) {
37
+ let scheduledAt;
38
+ try {
39
+ scheduledAt = nextMemoryDreamingAt(now(), effectiveTimezone);
40
+ }
41
+ catch {
42
+ onRun({ event: 'dreaming_schedule_failed', status: 'failed' });
43
+ return;
44
+ }
45
+ try {
46
+ while (!signal.aborted && now() < scheduledAt)
47
+ await sleepUntil(scheduledAt, signal);
48
+ }
49
+ catch {
50
+ onRun({ event: 'dreaming_schedule_failed', status: 'failed' });
51
+ return;
52
+ }
53
+ if (signal.aborted)
54
+ return;
55
+ const identity = options.identity({
56
+ timezone: effectiveTimezone,
57
+ localDate: memoryDreamingLocalDateAt(scheduledAt, effectiveTimezone),
58
+ scheduledAt: new Date(scheduledAt),
59
+ });
60
+ let runId;
61
+ try {
62
+ runId = await store.claim(identity);
63
+ }
64
+ catch {
65
+ onRun({ event: 'dreaming_run_failed', status: 'failed', errorCode: 'dreaming_claim_failed' });
66
+ continue;
67
+ }
68
+ if (!runId) {
69
+ onRun({
70
+ event: 'dreaming_run_skipped',
71
+ status: 'duplicate',
72
+ localDate: memoryDreamingLocalDateAt(scheduledAt, effectiveTimezone),
73
+ });
74
+ continue;
75
+ }
76
+ try {
77
+ const report = await runner.runDreaming(signal);
78
+ await store.finish(identity, runId, 'succeeded');
79
+ onRun({
80
+ event: 'dreaming_run_completed',
81
+ status: 'succeeded',
82
+ localDate: memoryDreamingLocalDateAt(scheduledAt, effectiveTimezone),
83
+ report,
84
+ });
85
+ }
86
+ catch {
87
+ await store.finish(identity, runId, 'failed').catch(() => undefined);
88
+ onRun({
89
+ event: 'dreaming_run_failed',
90
+ status: 'failed',
91
+ localDate: memoryDreamingLocalDateAt(scheduledAt, effectiveTimezone),
92
+ errorCode: 'dreaming_run_failed',
93
+ });
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,17 @@
1
+ export * from './contracts.js';
2
+ export * from './plugin.js';
3
+ export * from './runtime.js';
4
+ export * from './ports/index.js';
5
+ export * from './signals.js';
6
+ export * from './dreaming/deep.js';
7
+ export * from './dreaming/candidates.js';
8
+ export * from './dreaming/schedule.js';
9
+ export * from './dreaming/runner.js';
10
+ export * from './dreaming/scheduler.js';
11
+ export * from './recall/search.js';
12
+ export * from './recall/fusion.js';
13
+ export * from './recall/service.js';
14
+ export * from './write/service.js';
15
+ export * from './write/conflicts.js';
16
+ export * from './context/budget.js';
17
+ export * from './context/checkpoint.js';
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ export * from './contracts.js';
2
+ export * from './plugin.js';
3
+ export * from './runtime.js';
4
+ export * from './ports/index.js';
5
+ export * from './signals.js';
6
+ export * from './dreaming/deep.js';
7
+ export * from './dreaming/candidates.js';
8
+ export * from './dreaming/schedule.js';
9
+ export * from './dreaming/runner.js';
10
+ export * from './dreaming/scheduler.js';
11
+ export * from './recall/search.js';
12
+ export * from './recall/fusion.js';
13
+ export * from './recall/service.js';
14
+ export * from './write/service.js';
15
+ export * from './write/conflicts.js';
16
+ export * from './context/budget.js';
17
+ export * from './context/checkpoint.js';
@@ -0,0 +1,11 @@
1
+ import type { DreamingReport, ExplicitMemoryWrite, MemoryForgetRequest, MemoryHealth, MemoryObservation, MemoryRecallRequest, MemoryRecallResult, MemoryRetrievedCandidate, MemoryWriteResult } from './contracts.js';
2
+ /** Authorization stays opaque to the generic package and is supplied by the host for each request. */
3
+ export interface AgentMemoryPlugin<TAuthorization = unknown> {
4
+ observe(authorization: TAuthorization, turn: MemoryObservation): Promise<void>;
5
+ recall(authorization: TAuthorization, request: MemoryRecallRequest): Promise<MemoryRecallResult>;
6
+ remember(authorization: TAuthorization, write: ExplicitMemoryWrite): Promise<MemoryWriteResult>;
7
+ forget(authorization: TAuthorization, request: MemoryForgetRequest): Promise<MemoryWriteResult>;
8
+ recordUsed(authorization: TAuthorization, query: string, candidates: readonly MemoryRetrievedCandidate[]): Promise<void>;
9
+ runDreaming(signal?: AbortSignal): Promise<DreamingReport>;
10
+ health(): Promise<MemoryHealth>;
11
+ }
package/dist/plugin.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import type { ExplicitMemoryWrite, MemoryEmbeddingBatch, MemoryEmbeddingContract, MemoryEntry, MemoryForgetRequest, MemoryObservation, MemoryPlanningRequest, MemorySearchReason, MemoryUsageSignal, MemoryUsageSignalKind, MemoryWriteResult } from '../contracts.js';
2
+ export interface MemorySearchSources {
3
+ exact(query: string, limit?: number): Promise<MemoryEntry[]>;
4
+ lexical(query: string, limit?: number): Promise<MemoryEntry[]>;
5
+ semantic(query: string, limit?: number): Promise<{
6
+ matches: MemoryEntry[];
7
+ reason: MemorySearchReason;
8
+ }>;
9
+ }
10
+ /** The caller keeps its identity/authorization token opaque to this package. */
11
+ export interface MemoryStoragePort<TAuthorization = unknown> {
12
+ withAuthorizedSearch<T>(authorization: TAuthorization, work: (sources: MemorySearchSources) => Promise<T>): Promise<T>;
13
+ observe(authorization: TAuthorization, observation: MemoryObservation): Promise<void>;
14
+ recordUsageSignals(authorization: TAuthorization, kind: MemoryUsageSignalKind, signals: readonly MemoryUsageSignal[]): Promise<void>;
15
+ remember(authorization: TAuthorization, write: ExplicitMemoryWrite): Promise<MemoryWriteResult>;
16
+ forget(authorization: TAuthorization, request: MemoryForgetRequest): Promise<MemoryWriteResult>;
17
+ }
18
+ export interface MemoryModelPort {
19
+ plan(request: MemoryPlanningRequest, signal: AbortSignal): Promise<readonly unknown[]>;
20
+ }
21
+ export interface MemoryEmbeddingPort {
22
+ embedMany(values: readonly string[], signal?: AbortSignal): Promise<MemoryEmbeddingBatch>;
23
+ }
24
+ export interface MemoryEmbeddingClient extends MemoryEmbeddingPort {
25
+ readonly contract: MemoryEmbeddingContract;
26
+ readonly dailyTokenBudget: number;
27
+ readonly dailyRequestBudget: number;
28
+ }
29
+ export declare function estimateMemoryEmbeddingTokens(values: readonly string[]): number;
30
+ export interface MemoryClockPort {
31
+ now(): Date;
32
+ sleepUntil(deadline: Date, signal: AbortSignal): Promise<void>;
33
+ }
34
+ export interface MemoryLogEvent {
35
+ event: string;
36
+ fields?: Readonly<Record<string, string | number | boolean | null>>;
37
+ }
38
+ export interface MemoryLogPort {
39
+ info(event: MemoryLogEvent): void;
40
+ warn(event: MemoryLogEvent): void;
41
+ error(event: MemoryLogEvent): void;
42
+ }
@@ -0,0 +1,3 @@
1
+ export function estimateMemoryEmbeddingTokens(values) {
2
+ return values.reduce((total, value) => total + Buffer.byteLength(value, 'utf8'), 0);
3
+ }
@@ -0,0 +1,2 @@
1
+ import type { MemoryEntry, MemoryRecallPolicyOptions, MemoryRetrievedCandidate } from '../contracts.js';
2
+ export declare function fuseMemoryCandidates(query: string, exact: readonly MemoryEntry[], lexical: readonly MemoryEntry[], semantic: readonly MemoryEntry[], options: MemoryRecallPolicyOptions, now?: Date): MemoryRetrievedCandidate[];
@@ -0,0 +1,96 @@
1
+ import { memorySearchTokens, normalizeMemoryKey } from './search.js';
2
+ const RRF_K = 60;
3
+ function lexicalOverlap(entry, query) {
4
+ const normalized = query.normalize('NFC').trim().toLocaleLowerCase('en-US');
5
+ const haystack = `${entry.key} ${entry.content}`.normalize('NFC').toLocaleLowerCase('en-US');
6
+ if (haystack.includes(normalized))
7
+ return 1;
8
+ const tokens = memorySearchTokens(normalized);
9
+ if (!tokens.length)
10
+ return 0;
11
+ return tokens.filter((token) => haystack.includes(token)).length / tokens.length;
12
+ }
13
+ function sourceReliability(entry) {
14
+ const declared = Number.isFinite(entry.sourceReliability)
15
+ ? Math.min(1, Math.max(0, entry.sourceReliability / 100))
16
+ : 0;
17
+ const sourceCap = {
18
+ trusted_ingress: 0.85,
19
+ user_confirmed: 0.98,
20
+ system_verified: 1,
21
+ unknown: 0,
22
+ };
23
+ return Math.min(declared, sourceCap[entry.sourceKind]);
24
+ }
25
+ function freshness(entry, nowMs) {
26
+ const updatedAt = Date.parse(entry.updatedAt);
27
+ if (!Number.isFinite(updatedAt))
28
+ return 0.7;
29
+ const ageDays = Math.max(0, (nowMs - updatedAt) / 86_400_000);
30
+ return 0.7 + 0.3 * Math.exp(-ageDays / 1_825);
31
+ }
32
+ function addRanked(candidates, entry, source, rank, weight, reason, nowMs, quality = 1) {
33
+ if (entry.sourceKind === 'unknown' ||
34
+ (entry.validUntil && Date.parse(entry.validUntil) <= nowMs) ||
35
+ !entry.content ||
36
+ !Number.isSafeInteger(entry.revision) ||
37
+ entry.revision < 1)
38
+ return;
39
+ const normalizedKey = normalizeMemoryKey(entry.key);
40
+ if (!normalizedKey)
41
+ return;
42
+ const candidateKey = `${normalizedKey}\0${entry.revision}`;
43
+ let candidate = candidates.get(candidateKey);
44
+ if (!candidate) {
45
+ candidate = { entry, score: 0, sources: new Set(), reasons: new Set() };
46
+ candidates.set(candidateKey, candidate);
47
+ }
48
+ candidate.score +=
49
+ ((weight * quality) / (RRF_K + rank)) * sourceReliability(entry) * freshness(entry, nowMs);
50
+ candidate.sources.add(source);
51
+ candidate.reasons.add(reason);
52
+ }
53
+ export function fuseMemoryCandidates(query, exact, lexical, semantic, options, now = new Date()) {
54
+ const nowMs = now.getTime();
55
+ const ranked = new Map();
56
+ exact.forEach((entry, index) => addRanked(ranked, entry, 'exact', index + 1, 4, 'exact_key_or_content_match', nowMs));
57
+ lexical.forEach((entry, index) => {
58
+ const overlap = lexicalOverlap(entry, query);
59
+ if (overlap >= 0.2)
60
+ addRanked(ranked, entry, 'lexical', index + 1, 1, `lexical_overlap_${overlap.toFixed(2)}`, nowMs, overlap);
61
+ });
62
+ semantic.forEach((entry, index) => {
63
+ const similarity = entry.semanticSimilarity;
64
+ if (similarity !== undefined && similarity >= options.semanticSimilarityThreshold) {
65
+ addRanked(ranked, entry, 'semantic', index + 1, 1.5, `cosine_similarity_${similarity.toFixed(3)}`, nowMs, similarity);
66
+ }
67
+ });
68
+ const newestByKey = new Map();
69
+ for (const candidate of ranked.values()) {
70
+ const key = normalizeMemoryKey(candidate.entry.key);
71
+ const previous = newestByKey.get(key);
72
+ if (!previous ||
73
+ candidate.entry.revision > previous.entry.revision ||
74
+ (candidate.entry.revision === previous.entry.revision && candidate.score > previous.score)) {
75
+ newestByKey.set(key, candidate);
76
+ }
77
+ }
78
+ return [...newestByKey.values()]
79
+ .filter((candidate) => candidate.sources.has('exact') || candidate.score >= options.minimumScore)
80
+ .sort((left, right) => right.score - left.score ||
81
+ right.entry.revision - left.entry.revision ||
82
+ left.entry.key.localeCompare(right.entry.key))
83
+ .slice(0, options.topK)
84
+ .map((candidate) => ({
85
+ canonicalKey: normalizeMemoryKey(candidate.entry.key),
86
+ key: candidate.entry.key,
87
+ content: candidate.entry.content,
88
+ revision: candidate.entry.revision,
89
+ updatedAt: candidate.entry.updatedAt,
90
+ sourceEventId: candidate.entry.sourceEventId,
91
+ sourceKind: candidate.entry.sourceKind,
92
+ score: candidate.score,
93
+ sources: [...candidate.sources].sort(),
94
+ reasons: [...candidate.reasons].sort(),
95
+ }));
96
+ }
@@ -0,0 +1,12 @@
1
+ import type { MemoryDraft, MemoryEntry } from '../contracts.js';
2
+ export declare const MAX_MEMORY_RESULTS = 24;
3
+ export declare class MemoryPolicyError extends Error {
4
+ readonly code: 'input_invalid' | 'limit_invalid';
5
+ constructor(code: 'input_invalid' | 'limit_invalid');
6
+ }
7
+ export declare function normalizeMemoryKey(value: string): string;
8
+ export declare function canonicalizeMemoryKey(value: string): string;
9
+ export declare function validateMemoryDraft(input: MemoryDraft): MemoryDraft;
10
+ /** Bounded script-aware terms used by the lexical candidate and fallback policies. */
11
+ export declare function memorySearchTokens(query: string): string[];
12
+ export declare function rankMemoryEntries<T extends MemoryEntry>(entries: readonly T[], query: string, limit?: number): T[];
@@ -0,0 +1,81 @@
1
+ export const MAX_MEMORY_RESULTS = 24;
2
+ export class MemoryPolicyError extends Error {
3
+ code;
4
+ constructor(code) {
5
+ super(`memory_${code}`);
6
+ this.code = code;
7
+ this.name = 'MemoryPolicyError';
8
+ }
9
+ }
10
+ function boundedText(value, max, emojiSequence = false) {
11
+ if (typeof value !== 'string')
12
+ throw new MemoryPolicyError('input_invalid');
13
+ const text = value.normalize('NFC').trim();
14
+ if (!text || text.length > max || /[\p{Cc}\p{Cs}]/u.test(text))
15
+ throw new MemoryPolicyError('input_invalid');
16
+ for (let i = 0; i < text.length; i += 1) {
17
+ if (!/\p{Cf}/u.test(text[i]))
18
+ continue;
19
+ if (!emojiSequence ||
20
+ text[i] !== '\u200d' ||
21
+ !/\p{Extended_Pictographic}(?:\p{Emoji_Modifier}|\uFE0F)?$/u.test(text.slice(0, i)) ||
22
+ !/^\p{Extended_Pictographic}/u.test(text.slice(i + 1)))
23
+ throw new MemoryPolicyError('input_invalid');
24
+ }
25
+ return text;
26
+ }
27
+ export function normalizeMemoryKey(value) {
28
+ return value
29
+ .normalize('NFKC')
30
+ .trim()
31
+ .toLocaleLowerCase('en-US')
32
+ .replace(/[^\p{L}\p{N}]+/gu, '_')
33
+ .replace(/^_+|_+$/gu, '');
34
+ }
35
+ export function canonicalizeMemoryKey(value) {
36
+ const normalized = normalizeMemoryKey(boundedText(value, 96));
37
+ if (!normalized || normalized.length > 96)
38
+ throw new MemoryPolicyError('input_invalid');
39
+ return normalized;
40
+ }
41
+ export function validateMemoryDraft(input) {
42
+ return {
43
+ key: canonicalizeMemoryKey(input.key),
44
+ content: boundedText(input.content, 800, true),
45
+ };
46
+ }
47
+ /** Bounded script-aware terms used by the lexical candidate and fallback policies. */
48
+ export function memorySearchTokens(query) {
49
+ const normalized = boundedText(query, 240, true).toLocaleLowerCase('en-US');
50
+ const tokens = [];
51
+ for (const match of normalized.matchAll(/[\p{L}\p{N}]+/gu)) {
52
+ for (const chunk of match[0].match(/\p{Script=Han}+|[^\p{Script=Han}]+/gu) ?? []) {
53
+ if (/^\p{Script=Han}+$/u.test(chunk)) {
54
+ const chars = Array.from(chunk);
55
+ for (let index = 0; index + 1 < chars.length; index += 1)
56
+ tokens.push(chars[index] + chars[index + 1]);
57
+ }
58
+ else if (chunk.length > 1)
59
+ tokens.push(chunk);
60
+ }
61
+ }
62
+ return [...new Set(tokens)].slice(0, 240);
63
+ }
64
+ export function rankMemoryEntries(entries, query, limit = 8) {
65
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_MEMORY_RESULTS)
66
+ throw new MemoryPolicyError('limit_invalid');
67
+ const search = boundedText(query, 240, true).toLocaleLowerCase('en-US');
68
+ const words = memorySearchTokens(search);
69
+ return entries
70
+ .map((item, index) => {
71
+ const haystack = (item.key + ' ' + item.content).toLocaleLowerCase('en-US');
72
+ const score = haystack.includes(search)
73
+ ? 1000
74
+ : words.reduce((value, word) => value + (haystack.includes(word) ? 1 : 0), 0);
75
+ return { item, index, score };
76
+ })
77
+ .filter((match) => match.score > 0)
78
+ .sort((a, b) => b.score - a.score || a.index - b.index)
79
+ .slice(0, limit)
80
+ .map((match) => match.item);
81
+ }
@@ -0,0 +1,26 @@
1
+ import type { MemoryRecallPolicyOptions, MemoryRecallResult, MemoryRetrievedCandidate, MemoryRetrievalMetric, MemoryUsageSignal, MemoryUsageSignalKind } from '../contracts.js';
2
+ import type { MemorySearchSources } from '../ports/index.js';
3
+ export interface MemoryRecallStoragePort<TAuthorization> {
4
+ withAuthorizedSearch<T>(authorization: TAuthorization, work: (sources: MemorySearchSources) => Promise<T>): Promise<T>;
5
+ recordUsageSignals(authorization: TAuthorization, kind: MemoryUsageSignalKind, signals: readonly MemoryUsageSignal[]): Promise<void>;
6
+ }
7
+ export interface MemoryRecallServiceOptions extends Partial<MemoryRecallPolicyOptions> {
8
+ candidateLimit?: number;
9
+ now?: () => number;
10
+ renderContext?: (candidates: readonly MemoryRetrievedCandidate[]) => string | null;
11
+ onMetric?: (metric: MemoryRetrievalMetric) => void;
12
+ }
13
+ /** Generic authorization-fenced recall orchestration; the host supplies scoped storage and rendering. */
14
+ export declare class MemoryRecallService<TAuthorization> {
15
+ private readonly storage;
16
+ private readonly options;
17
+ private readonly topK;
18
+ private readonly candidateLimit;
19
+ private readonly semanticSimilarityThreshold;
20
+ private readonly minimumScore;
21
+ private readonly now;
22
+ constructor(storage: MemoryRecallStoragePort<TAuthorization>, options?: MemoryRecallServiceOptions);
23
+ private report;
24
+ recall(authorization: TAuthorization, query: string, limit?: number): Promise<MemoryRecallResult>;
25
+ recordUsed(authorization: TAuthorization, query: string, candidates: readonly MemoryRetrievedCandidate[]): Promise<void>;
26
+ }