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

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ChristmasFox / Arthur Xu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,55 @@
1
+ /** One earlier turn of the conversation, in the host's own words. */
2
+ export interface ConversationTurn {
3
+ role: 'user' | 'assistant';
4
+ content: string;
5
+ }
6
+ /** A rolling, low-trust excerpt of what is older than the live window (see checkpoint.ts). */
7
+ export interface ConversationCheckpointView {
8
+ text: string;
9
+ generation: number;
10
+ coverageFrom: string;
11
+ coverageThrough: string;
12
+ coveredMessageCount: number;
13
+ sourceEventIds: string[];
14
+ }
15
+ export interface ConversationWindow {
16
+ turns: ConversationTurn[];
17
+ /** Host label of the current speaker; not used for budgeting. */
18
+ currentSpeakerLabel?: string;
19
+ currentQuoteContext: string | null;
20
+ rollingCheckpoint?: ConversationCheckpointView | null;
21
+ }
22
+ export interface ContextBudgetPolicy {
23
+ recentTurnsTokens: number;
24
+ quotedMessageTokens: number;
25
+ episodicSummaryTokens: number;
26
+ longTermRecallTokens: number;
27
+ estimatedProviderContextTokens: number;
28
+ systemAndToolSchemaReserveTokens: number;
29
+ toolResultReserveTokens: number;
30
+ finalAnswerReserveTokens: number;
31
+ }
32
+ export declare const DEFAULT_CONTEXT_BUDGET_POLICY: Readonly<ContextBudgetPolicy>;
33
+ export interface BuiltWorkingContext {
34
+ turns: ConversationTurn[];
35
+ currentQuoteContext: string | null;
36
+ episodicSummaryContext: string | null;
37
+ episodicSummarySourceEventIds: string[];
38
+ longTermMemoryContext: string | null;
39
+ currentMessage: string;
40
+ estimateMethod: 'conservative_unicode_char_v1';
41
+ estimatedRecentTokens: number;
42
+ estimatedQuoteTokens: number;
43
+ estimatedEpisodicSummaryTokens: number;
44
+ estimatedLongTermMemoryTokens: number;
45
+ estimatedCurrentMessageTokens: number;
46
+ reservedProviderTokens: number;
47
+ estimatedTotalContextTokens: number;
48
+ }
49
+ /**
50
+ * Conservative planning estimate: ASCII code points cost one third of a token;
51
+ * every non-ASCII code point costs one. This is deliberately labeled as an
52
+ * estimate because the configured provider tokenizer is not available here.
53
+ */
54
+ export declare function estimateContextTokens(text: string): number;
55
+ export declare function buildWorkingContext(window: ConversationWindow, currentMessage: string, policy?: Readonly<ContextBudgetPolicy>, longTermMemoryContext?: string | null): BuiltWorkingContext;
@@ -0,0 +1,103 @@
1
+ export const DEFAULT_CONTEXT_BUDGET_POLICY = Object.freeze({
2
+ recentTurnsTokens: 4_000,
3
+ quotedMessageTokens: 1_000,
4
+ episodicSummaryTokens: 1_000,
5
+ longTermRecallTokens: 1_000,
6
+ estimatedProviderContextTokens: 24_000,
7
+ systemAndToolSchemaReserveTokens: 4_000,
8
+ toolResultReserveTokens: 5_000,
9
+ finalAnswerReserveTokens: 2_000,
10
+ });
11
+ /**
12
+ * Conservative planning estimate: ASCII code points cost one third of a token;
13
+ * every non-ASCII code point costs one. This is deliberately labeled as an
14
+ * estimate because the configured provider tokenizer is not available here.
15
+ */
16
+ export function estimateContextTokens(text) {
17
+ let units = 0;
18
+ for (const character of text)
19
+ units += character.codePointAt(0) <= 0x7f ? 1 / 3 : 1;
20
+ return Math.ceil(units);
21
+ }
22
+ function fitText(text, budget) {
23
+ if (estimateContextTokens(text) <= budget)
24
+ return text;
25
+ const marker = ' …[正文按上下文预算截断]';
26
+ const markerCost = estimateContextTokens(marker);
27
+ const suffix = budget >= markerCost ? marker : '';
28
+ const prefixBudget = suffix ? budget - markerCost : budget;
29
+ let prefix = '';
30
+ let usedUnits = 0;
31
+ for (const character of text) {
32
+ const cost = character.codePointAt(0) <= 0x7f ? 1 / 3 : 1;
33
+ if (Math.ceil(usedUnits + cost) > prefixBudget)
34
+ break;
35
+ prefix += character;
36
+ usedUnits += cost;
37
+ }
38
+ let fitted = `${prefix.trimEnd()}${suffix}`;
39
+ while (estimateContextTokens(fitted) > budget && prefix.length > 0) {
40
+ prefix = Array.from(prefix).slice(0, -1).join('');
41
+ fitted = `${prefix.trimEnd()}${suffix}`;
42
+ }
43
+ return fitted;
44
+ }
45
+ export function buildWorkingContext(window, currentMessage, policy = DEFAULT_CONTEXT_BUDGET_POLICY, longTermMemoryContext = null) {
46
+ for (const [name, limit] of Object.entries(policy)) {
47
+ if (!Number.isSafeInteger(limit) || limit < 0)
48
+ throw new Error(`agent_context_budget_${name}_invalid`);
49
+ }
50
+ const currentTokens = estimateContextTokens(currentMessage);
51
+ const quote = window.currentQuoteContext === null
52
+ ? null
53
+ : fitText(window.currentQuoteContext, policy.quotedMessageTokens);
54
+ const quoteTokens = quote === null ? 0 : estimateContextTokens(quote);
55
+ const memoryContext = longTermMemoryContext === null
56
+ ? null
57
+ : fitText(longTermMemoryContext, policy.longTermRecallTokens);
58
+ const memoryTokens = memoryContext === null ? 0 : estimateContextTokens(memoryContext);
59
+ const reservedProviderTokens = policy.systemAndToolSchemaReserveTokens +
60
+ policy.toolResultReserveTokens +
61
+ policy.finalAnswerReserveTokens +
62
+ policy.longTermRecallTokens;
63
+ const availableHistoryTokens = Math.max(0, policy.estimatedProviderContextTokens - reservedProviderTokens - currentTokens - quoteTokens);
64
+ const selected = [];
65
+ let recentTokens = 0;
66
+ for (let index = window.turns.length - 1; index >= 0; index -= 1) {
67
+ const turn = window.turns[index];
68
+ const cost = estimateContextTokens(turn.content);
69
+ if (cost > policy.recentTurnsTokens ||
70
+ recentTokens + cost > policy.recentTurnsTokens ||
71
+ recentTokens + cost > availableHistoryTokens)
72
+ continue;
73
+ selected.push(turn);
74
+ recentTokens += cost;
75
+ }
76
+ selected.reverse();
77
+ const checkpoint = window.rollingCheckpoint;
78
+ const checkpointHeader = checkpoint
79
+ ? `此前对话滚动摘录(低信任原文片段;覆盖 ${checkpoint.coverageFrom} 至 ${checkpoint.coverageThrough};第 ${checkpoint.generation} 代;${checkpoint.coveredMessageCount} 条源消息;${checkpoint.sourceEventIds.length} 个来源事件):\n`
80
+ : '';
81
+ const summaryBudget = Math.min(policy.episodicSummaryTokens, Math.max(0, availableHistoryTokens - recentTokens));
82
+ const episodicSummaryContext = checkpoint && checkpoint.text && summaryBudget > 0
83
+ ? fitText(checkpointHeader + checkpoint.text, summaryBudget)
84
+ : null;
85
+ const summaryTokens = episodicSummaryContext === null ? 0 : estimateContextTokens(episodicSummaryContext);
86
+ const estimatedTotalContextTokens = reservedProviderTokens + currentTokens + quoteTokens + recentTokens + summaryTokens;
87
+ return {
88
+ turns: selected,
89
+ currentQuoteContext: quote,
90
+ episodicSummaryContext,
91
+ episodicSummarySourceEventIds: checkpoint?.sourceEventIds ?? [],
92
+ longTermMemoryContext: memoryContext,
93
+ currentMessage,
94
+ estimateMethod: 'conservative_unicode_char_v1',
95
+ estimatedRecentTokens: recentTokens,
96
+ estimatedQuoteTokens: quoteTokens,
97
+ estimatedEpisodicSummaryTokens: summaryTokens,
98
+ estimatedLongTermMemoryTokens: memoryTokens,
99
+ estimatedCurrentMessageTokens: currentTokens,
100
+ reservedProviderTokens,
101
+ estimatedTotalContextTokens,
102
+ };
103
+ }
@@ -0,0 +1,24 @@
1
+ export interface CheckpointSourceRow {
2
+ sourceEventId: string;
3
+ speakerKind: 'human' | 'assistant' | string;
4
+ /** ISO time. */
5
+ createdAt: string;
6
+ content: string;
7
+ }
8
+ export interface ExtractiveCheckpoint {
9
+ text: string;
10
+ coverageFrom: string;
11
+ coverageThrough: string;
12
+ coveredMessageCount: number;
13
+ sourceEventIds: string[];
14
+ /** Hash of the covered rows; changes exactly when the checkpoint needs a new generation. */
15
+ sourceDigest: string;
16
+ }
17
+ export declare const MAX_CHECKPOINT_SOURCE_ROWS = 100;
18
+ export declare const MAX_CHECKPOINT_TOKENS = 1000;
19
+ /**
20
+ * Builds a small extractive checkpoint (no model involved) from the rows older than the live window,
21
+ * newest first as the host reads them. The 100-row page and the token cap keep the work bounded; the
22
+ * result is safe to rebuild at any time.
23
+ */
24
+ export declare function buildExtractiveCheckpoint(rowsNewestFirst: readonly CheckpointSourceRow[]): ExtractiveCheckpoint | null;
@@ -0,0 +1,67 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { estimateContextTokens } from './budget.js';
3
+ export const MAX_CHECKPOINT_SOURCE_ROWS = 100;
4
+ export const MAX_CHECKPOINT_TOKENS = 1_000;
5
+ const CHECKPOINT_SOURCE_LIMIT = 48;
6
+ function contentWithBudget(value, budget) {
7
+ if (estimateContextTokens(value) <= budget)
8
+ return value;
9
+ const marker = ' …[对话摘录截断]';
10
+ const markerTokens = estimateContextTokens(marker);
11
+ const textBudget = Math.max(0, budget - markerTokens);
12
+ let text = '';
13
+ let tokens = 0;
14
+ for (const character of value) {
15
+ const cost = estimateContextTokens(character);
16
+ if (tokens + cost > textBudget)
17
+ break;
18
+ text += character;
19
+ tokens += cost;
20
+ }
21
+ return `${text.trimEnd()}${budget >= markerTokens ? marker : ''}`;
22
+ }
23
+ /**
24
+ * Builds a small extractive checkpoint (no model involved) from the rows older than the live window,
25
+ * newest first as the host reads them. The 100-row page and the token cap keep the work bounded; the
26
+ * result is safe to rebuild at any time.
27
+ */
28
+ export function buildExtractiveCheckpoint(rowsNewestFirst) {
29
+ const rows = rowsNewestFirst.slice(0, MAX_CHECKPOINT_SOURCE_ROWS);
30
+ if (!rows.length)
31
+ return null;
32
+ const chronologicalRows = [...rows].reverse();
33
+ const digestMaterial = chronologicalRows.map((row) => [
34
+ row.sourceEventId,
35
+ row.speakerKind,
36
+ row.createdAt,
37
+ row.content,
38
+ ]);
39
+ const sourceDigest = createHash('sha256').update(JSON.stringify(digestMaterial)).digest('hex');
40
+ const selected = [];
41
+ let usedTokens = 0;
42
+ for (const row of rows) {
43
+ if (!row.sourceEventId || (row.speakerKind !== 'human' && row.speakerKind !== 'assistant'))
44
+ continue;
45
+ if (selected.length >= CHECKPOINT_SOURCE_LIMIT || usedTokens >= MAX_CHECKPOINT_TOKENS)
46
+ break;
47
+ const prefix = row.speakerKind === 'assistant' ? '助手说:' : '成员说:';
48
+ const remaining = MAX_CHECKPOINT_TOKENS - usedTokens - estimateContextTokens(prefix);
49
+ if (remaining <= 0)
50
+ break;
51
+ const line = `${prefix}${contentWithBudget(row.content, remaining)}`;
52
+ const cost = estimateContextTokens(line);
53
+ if (cost <= 0 || cost > remaining)
54
+ continue;
55
+ selected.push({ sourceEventId: row.sourceEventId, line });
56
+ usedTokens += cost;
57
+ }
58
+ selected.reverse();
59
+ return {
60
+ text: selected.map((item) => item.line).join('\n'),
61
+ coverageFrom: chronologicalRows[0].createdAt,
62
+ coverageThrough: chronologicalRows.at(-1).createdAt,
63
+ coveredMessageCount: chronologicalRows.length,
64
+ sourceEventIds: [...new Set(selected.map((item) => item.sourceEventId))],
65
+ sourceDigest,
66
+ };
67
+ }
@@ -0,0 +1,160 @@
1
+ export type MemorySourceKind = 'trusted_ingress' | 'user_confirmed' | 'system_verified' | 'unknown';
2
+ export interface MemoryEntry {
3
+ key: string;
4
+ content: string;
5
+ revision: number;
6
+ updatedAt: string;
7
+ sourceEventId: string;
8
+ sourceKind: MemorySourceKind;
9
+ sourceReliability: number;
10
+ validUntil: string | null;
11
+ semanticSimilarity?: number;
12
+ }
13
+ export interface MemoryDraft {
14
+ key: string;
15
+ content: string;
16
+ }
17
+ export interface MemoryWriteMetadata {
18
+ sourceKind?: MemorySourceKind;
19
+ sourceReliability?: number;
20
+ validUntil?: string | null;
21
+ }
22
+ export interface MemoryWriteState extends MemoryEntry {
23
+ revoked: boolean;
24
+ }
25
+ export type MemorySearchMode = 'semantic' | 'lexical_only';
26
+ export type MemorySearchReason = 'semantic_unconfigured' | 'vector_index_unavailable' | 'semantic_unavailable' | 'semantic_timeout' | 'embedding_budget_exhausted' | 'embedding_budget_unavailable' | null;
27
+ export interface MemorySearchResult {
28
+ entries: MemoryEntry[];
29
+ mode: MemorySearchMode;
30
+ reason: MemorySearchReason;
31
+ }
32
+ export interface MemorySearchMetric {
33
+ durationMs: number;
34
+ mode: MemorySearchMode;
35
+ reason: MemorySearchReason;
36
+ }
37
+ export interface MemorySemanticSearchResult {
38
+ matches: MemoryEntry[];
39
+ reason: MemorySearchReason;
40
+ }
41
+ export type MemoryRetrievalMode = 'hybrid' | 'lexical_only' | 'disabled';
42
+ export type MemoryRetrievalReason = MemorySearchReason | 'memory_scope_unavailable' | 'memory_store_unavailable' | 'memory_candidate_source_unavailable' | 'disabled_by_operator';
43
+ export interface MemoryRetrievedCandidate {
44
+ canonicalKey: string;
45
+ key: string;
46
+ content: string;
47
+ revision: number;
48
+ updatedAt: string;
49
+ sourceEventId: string;
50
+ sourceKind: MemorySourceKind;
51
+ score: number;
52
+ sources: Array<'exact' | 'lexical' | 'semantic'>;
53
+ reasons: string[];
54
+ }
55
+ export interface MemoryRetrievalMetric {
56
+ mode: MemoryRetrievalMode;
57
+ reason: MemoryRetrievalReason;
58
+ durationMs: number;
59
+ candidateCount: number;
60
+ exactCount: number;
61
+ lexicalCount: number;
62
+ semanticCount: number;
63
+ }
64
+ export interface MemoryRecallResult {
65
+ mode: MemoryRetrievalMode;
66
+ reason: MemoryRetrievalReason;
67
+ candidates: MemoryRetrievedCandidate[];
68
+ context: string | null;
69
+ }
70
+ /** Content-free signal linking a trusted recalled entry to one request. */
71
+ export interface MemoryUsageSignal {
72
+ key: string;
73
+ revision: number;
74
+ sourceEventId: string;
75
+ queryFingerprint: string;
76
+ }
77
+ export type MemoryUsageSignalKind = 'recalled' | 'used';
78
+ export interface MemoryRecallPolicyOptions {
79
+ topK: number;
80
+ semanticSimilarityThreshold: number;
81
+ minimumScore: number;
82
+ }
83
+ export interface MemoryEvidence {
84
+ id: string;
85
+ observedAt: string;
86
+ source: 'conversation' | 'user_confirmed' | 'system_verified';
87
+ }
88
+ export interface MemoryObservation {
89
+ content: string;
90
+ observedAt: string;
91
+ evidence: readonly MemoryEvidence[];
92
+ }
93
+ export interface MemoryRecallRequest {
94
+ query: string;
95
+ limit?: number;
96
+ }
97
+ export interface ExplicitMemoryWrite extends MemoryDraft {
98
+ operation: 'remember' | 'correct';
99
+ expectedRevision?: number;
100
+ validUntil?: string | null;
101
+ }
102
+ export interface MemoryForgetRequest {
103
+ key: string;
104
+ expectedRevision?: number;
105
+ }
106
+ export type MemoryWriteResult = {
107
+ status: 'created' | 'revised' | 'forgotten' | 'already_present' | 'not_found';
108
+ };
109
+ export type DreamingStage = 'light' | 'rem' | 'deep';
110
+ export type MemoryCandidateSubject = 'self' | 'third_party' | 'unknown';
111
+ export type MemoryCandidateKind = 'stable_preference' | 'verifiable_fact' | 'episodic_checkpoint';
112
+ export type MemoryCandidateValidity = 'stable' | '7_days' | '30_days' | '90_days' | 'unknown';
113
+ export interface MemoryCandidateDraft extends MemoryDraft {
114
+ confidence: 'high' | 'medium';
115
+ subject: MemoryCandidateSubject;
116
+ kind: MemoryCandidateKind;
117
+ evidence: string;
118
+ validity: MemoryCandidateValidity;
119
+ }
120
+ export interface MemoryPlanningRequest {
121
+ stage: DreamingStage;
122
+ messages: readonly {
123
+ content: string;
124
+ createdAt: string;
125
+ }[];
126
+ }
127
+ export interface DreamingStageResult {
128
+ stage: DreamingStage;
129
+ status: 'completed' | 'skipped' | 'failed';
130
+ processed: number;
131
+ }
132
+ export interface DreamingReport {
133
+ startedAt: string;
134
+ completedAt: string | null;
135
+ stages: readonly DreamingStageResult[];
136
+ }
137
+ export interface MemoryHealth {
138
+ status: 'ready' | 'degraded';
139
+ lastDreamingAt: string | null;
140
+ reason: string | null;
141
+ }
142
+ export interface MemoryFeatureFlags {
143
+ read: boolean;
144
+ write: boolean;
145
+ autoLearn: boolean;
146
+ }
147
+ export declare const MEMORY_FEATURE_DEFAULTS: Readonly<MemoryFeatureFlags>;
148
+ export interface MemoryEmbeddingContract {
149
+ providerId: string;
150
+ modelId: string;
151
+ dimensions: number;
152
+ normalization: 'l2_unit';
153
+ indexVersion: number;
154
+ }
155
+ export interface MemoryEmbeddingBatch {
156
+ contract: MemoryEmbeddingContract;
157
+ vectors: number[][];
158
+ contentHashes: string[];
159
+ inputTokens: number | null;
160
+ }
@@ -0,0 +1,5 @@
1
+ export const MEMORY_FEATURE_DEFAULTS = {
2
+ read: true,
3
+ write: true,
4
+ autoLearn: true,
5
+ };
@@ -0,0 +1,26 @@
1
+ import type { MemoryCandidateDraft, MemoryCandidateKind } from '../contracts.js';
2
+ export interface MemoryCandidateSource {
3
+ content: string;
4
+ sourceEventId: string;
5
+ }
6
+ export interface NormalizedMemoryCandidate {
7
+ key: string;
8
+ content: string;
9
+ kind: MemoryCandidateKind;
10
+ validUntil: string | null;
11
+ evidenceEventIds: string[];
12
+ idempotencyKey: string;
13
+ }
14
+ export interface NormalizeMemoryCandidatesOptions {
15
+ maxCandidates: number;
16
+ now: () => number;
17
+ canonicalizeKey: (key: string) => string;
18
+ isProhibited: (text: string) => boolean;
19
+ createIdempotencyKey: (input: {
20
+ key: string;
21
+ content: string;
22
+ evidenceEventIds: readonly string[];
23
+ }) => string;
24
+ }
25
+ /** Normalize bounded Light/REM drafts against their source evidence before persistence. */
26
+ export declare function normalizeMemoryCandidates(drafts: readonly MemoryCandidateDraft[], sources: readonly MemoryCandidateSource[], options: NormalizeMemoryCandidatesOptions): NormalizedMemoryCandidate[];
@@ -0,0 +1,53 @@
1
+ /** Normalize bounded Light/REM drafts against their source evidence before persistence. */
2
+ export function normalizeMemoryCandidates(drafts, sources, options) {
3
+ const accepted = [];
4
+ const seenKeys = new Set();
5
+ for (const draft of drafts.slice(0, Math.max(0, options.maxCandidates))) {
6
+ if (draft.subject !== 'self' || draft.confidence !== 'high')
7
+ continue;
8
+ if (!validCandidateKindAndValidity(draft.kind, draft.validity))
9
+ continue;
10
+ const evidence = draft.evidence.trim().normalize('NFKC');
11
+ const content = draft.content.trim().normalize('NFKC');
12
+ if (!content || !evidence.includes(content) || options.isProhibited(`${evidence}\n${content}`))
13
+ continue;
14
+ const evidenceSources = sources.filter((source) => source.content.normalize('NFKC').includes(evidence));
15
+ if (!evidenceSources.length)
16
+ continue;
17
+ let key;
18
+ try {
19
+ key = options.canonicalizeKey(draft.key);
20
+ }
21
+ catch {
22
+ continue;
23
+ }
24
+ if (seenKeys.has(key))
25
+ continue;
26
+ const evidenceEventIds = [
27
+ ...new Set(evidenceSources.map((source) => source.sourceEventId)),
28
+ ].slice(0, 8);
29
+ seenKeys.add(key);
30
+ accepted.push({
31
+ key,
32
+ content,
33
+ kind: draft.kind,
34
+ validUntil: candidateExpiry(draft.kind, draft.validity, options.now()),
35
+ evidenceEventIds,
36
+ idempotencyKey: options.createIdempotencyKey({ key, content, evidenceEventIds }),
37
+ });
38
+ }
39
+ return accepted;
40
+ }
41
+ function validCandidateKindAndValidity(kind, validity) {
42
+ if (kind === 'stable_preference')
43
+ return validity === 'stable';
44
+ if (kind === 'verifiable_fact')
45
+ return validity === '7_days' || validity === '30_days' || validity === '90_days';
46
+ return validity === '7_days' || validity === '30_days';
47
+ }
48
+ function candidateExpiry(kind, validity, now) {
49
+ if (kind === 'stable_preference' && validity === 'stable')
50
+ return null;
51
+ const days = validity === '7_days' ? 7 : validity === '30_days' ? 30 : validity === '90_days' ? 90 : 0;
52
+ return days === 0 ? null : new Date(now + days * 86_400_000).toISOString();
53
+ }
@@ -0,0 +1,27 @@
1
+ import type { MemoryCandidateKind } from '../contracts.js';
2
+ /** Evidence collected by the host adapter for one normalized candidate value. */
3
+ export interface DeepPromotionEvidence {
4
+ candidateKind: MemoryCandidateKind;
5
+ sourceReliability: number;
6
+ supportingObservations: number;
7
+ distinctObservationDays: number;
8
+ recallCount: number;
9
+ distinctQueries: number;
10
+ usedCount: number;
11
+ ageDays: number;
12
+ conflictsWithCurrent: boolean;
13
+ competingValues: number;
14
+ prohibited: boolean;
15
+ }
16
+ export type DeepPromotionReason = 'promotion_evidence_sufficient' | 'invalid_evidence' | 'sensitive_or_prohibited' | 'source_unreliable' | 'single_observation' | 'insufficient_repetition_or_usage' | 'ambiguous_conflict' | 'stale_evidence' | 'score_below_threshold';
17
+ export interface DeepPromotionDecision {
18
+ promote: boolean;
19
+ score: number;
20
+ threshold: number;
21
+ reason: DeepPromotionReason;
22
+ }
23
+ /**
24
+ * Deterministic Deep policy. High model confidence is intentionally absent:
25
+ * repeated, attributable evidence and useful recall are what can promote.
26
+ */
27
+ export declare function evaluateDeepPromotion(evidence: DeepPromotionEvidence): DeepPromotionDecision;
@@ -0,0 +1,63 @@
1
+ const SCORE_THRESHOLDS = {
2
+ stable_preference: 70,
3
+ verifiable_fact: 75,
4
+ episodic_checkpoint: 78,
5
+ };
6
+ /**
7
+ * Deterministic Deep policy. High model confidence is intentionally absent:
8
+ * repeated, attributable evidence and useful recall are what can promote.
9
+ */
10
+ export function evaluateDeepPromotion(evidence) {
11
+ const threshold = SCORE_THRESHOLDS[evidence.candidateKind];
12
+ const integerFields = [
13
+ evidence.sourceReliability,
14
+ evidence.supportingObservations,
15
+ evidence.distinctObservationDays,
16
+ evidence.recallCount,
17
+ evidence.distinctQueries,
18
+ evidence.usedCount,
19
+ evidence.competingValues,
20
+ ];
21
+ if (!threshold ||
22
+ integerFields.some((value) => !Number.isSafeInteger(value) || value < 0) ||
23
+ evidence.sourceReliability > 100 ||
24
+ !Number.isFinite(evidence.ageDays) ||
25
+ evidence.ageDays < 0 ||
26
+ typeof evidence.conflictsWithCurrent !== 'boolean' ||
27
+ typeof evidence.prohibited !== 'boolean') {
28
+ return { promote: false, score: 0, threshold: threshold ?? 0, reason: 'invalid_evidence' };
29
+ }
30
+ const rawScore = evidence.sourceReliability * 0.55 +
31
+ Math.min(evidence.supportingObservations, 4) * 8 +
32
+ Math.min(evidence.distinctObservationDays, 4) * 6 +
33
+ Math.min(evidence.recallCount, 5) +
34
+ Math.min(evidence.distinctQueries, 5) * 2 +
35
+ Math.min(evidence.usedCount, 3) * 4 -
36
+ Math.max(0, evidence.ageDays - 2) * 2 -
37
+ (evidence.conflictsWithCurrent ? 5 : 0) -
38
+ Math.min(evidence.competingValues, 3) * 25;
39
+ const score = Math.max(0, Math.min(100, Math.round(rawScore)));
40
+ const decision = (reason) => ({
41
+ promote: false,
42
+ score,
43
+ threshold,
44
+ reason,
45
+ });
46
+ if (evidence.prohibited)
47
+ return decision('sensitive_or_prohibited');
48
+ if (evidence.sourceReliability < 80)
49
+ return decision('source_unreliable');
50
+ if (evidence.supportingObservations < 2)
51
+ return decision('single_observation');
52
+ if (evidence.ageDays > 30)
53
+ return decision('stale_evidence');
54
+ if (evidence.competingValues > 0)
55
+ return decision('ambiguous_conflict');
56
+ const repeatedAcrossDays = evidence.distinctObservationDays >= 2;
57
+ const recalledAndUseful = evidence.recallCount >= 2 && evidence.distinctQueries >= 2 && evidence.usedCount >= 1;
58
+ if (!repeatedAcrossDays && !recalledAndUseful)
59
+ return decision('insufficient_repetition_or_usage');
60
+ if (score < threshold)
61
+ return decision('score_below_threshold');
62
+ return { promote: true, score, threshold, reason: 'promotion_evidence_sufficient' };
63
+ }
@@ -0,0 +1,16 @@
1
+ import type { DreamingReport } from '../contracts.js';
2
+ export type MemoryDreamingWorkStatus = 'processed' | 'skipped' | 'retry' | 'dead' | 'timed_out';
3
+ export interface MemoryDreamingStageWorker {
4
+ processNext(stage: 'light' | 'rem', signal: AbortSignal): Promise<MemoryDreamingWorkStatus>;
5
+ processDeep(signal: AbortSignal): Promise<{
6
+ status: MemoryDreamingWorkStatus;
7
+ processed: number;
8
+ }>;
9
+ }
10
+ export interface MemoryDreamingRunOptions {
11
+ maxLightBatches?: number;
12
+ maxRemBatches?: number;
13
+ now?: () => Date;
14
+ }
15
+ /** Generic bounded Light -> REM -> Deep orchestration over host-owned workers. */
16
+ export declare function runMemoryDreamingStages(worker: MemoryDreamingStageWorker, signal: AbortSignal, options?: MemoryDreamingRunOptions): Promise<DreamingReport>;