@memory-river/core 0.2.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 +201 -0
- package/README.md +222 -0
- package/README.zh-TW.md +186 -0
- package/dist/api.d.ts +100 -0
- package/dist/api.js +156 -0
- package/dist/cognition/causal-attribution.d.ts +36 -0
- package/dist/cognition/causal-attribution.js +239 -0
- package/dist/cognition/causal-engine.d.ts +105 -0
- package/dist/cognition/causal-engine.js +150 -0
- package/dist/cognition/conflict-detector.d.ts +39 -0
- package/dist/cognition/conflict-detector.js +193 -0
- package/dist/cognition/global-working-memory.d.ts +53 -0
- package/dist/cognition/global-working-memory.js +211 -0
- package/dist/cognition/hooks-engine.d.ts +99 -0
- package/dist/cognition/hooks-engine.js +672 -0
- package/dist/cognition/ralph-core.d.ts +28 -0
- package/dist/cognition/ralph-core.js +104 -0
- package/dist/distill/concentrator-adapter.d.ts +167 -0
- package/dist/distill/concentrator-adapter.js +1876 -0
- package/dist/engine.d.ts +402 -0
- package/dist/engine.js +2254 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/lifecycle/cleanup-engine.d.ts +80 -0
- package/dist/lifecycle/cleanup-engine.js +162 -0
- package/dist/lifecycle/cleanup-state.d.ts +34 -0
- package/dist/lifecycle/cleanup-state.js +50 -0
- package/dist/lifecycle/night-consolidation.d.ts +102 -0
- package/dist/lifecycle/night-consolidation.js +640 -0
- package/dist/lifecycle/night-recovery.d.ts +40 -0
- package/dist/lifecycle/night-recovery.js +107 -0
- package/dist/paths.d.ts +17 -0
- package/dist/paths.js +16 -0
- package/dist/pipeline/capsule-bridge.d.ts +35 -0
- package/dist/pipeline/capsule-bridge.js +86 -0
- package/dist/pipeline/compact-request.d.ts +30 -0
- package/dist/pipeline/compact-request.js +66 -0
- package/dist/pipeline/inbox-watcher.d.ts +112 -0
- package/dist/pipeline/inbox-watcher.js +1039 -0
- package/dist/ports.d.ts +29 -0
- package/dist/ports.js +1 -0
- package/dist/providers/embedder-v5.d.ts +46 -0
- package/dist/providers/embedder-v5.js +155 -0
- package/dist/providers/ollama-embedding.d.ts +25 -0
- package/dist/providers/ollama-embedding.js +166 -0
- package/dist/retrieval/abstractness-judge.d.ts +14 -0
- package/dist/retrieval/abstractness-judge.js +87 -0
- package/dist/retrieval/coverage-selection.d.ts +3 -0
- package/dist/retrieval/coverage-selection.js +53 -0
- package/dist/retrieval/cross-encoder-gate.d.ts +40 -0
- package/dist/retrieval/cross-encoder-gate.js +239 -0
- package/dist/retrieval/retriever-v4.d.ts +78 -0
- package/dist/retrieval/retriever-v4.js +1200 -0
- package/dist/skills/validate.d.ts +6 -0
- package/dist/skills/validate.js +69 -0
- package/dist/storage.d.ts +19 -0
- package/dist/storage.js +54 -0
- package/dist/store/aux-table-maintenance.d.ts +5 -0
- package/dist/store/aux-table-maintenance.js +64 -0
- package/dist/store/graph-enumerator.d.ts +21 -0
- package/dist/store/graph-enumerator.js +185 -0
- package/dist/store/graph-store.d.ts +107 -0
- package/dist/store/graph-store.js +478 -0
- package/dist/store/status-manager.d.ts +44 -0
- package/dist/store/status-manager.js +235 -0
- package/dist/store/store-v4.d.ts +339 -0
- package/dist/store/store-v4.js +2871 -0
- package/dist/transcript/keyword-search.d.ts +9 -0
- package/dist/transcript/keyword-search.js +67 -0
- package/dist/transcript/rehydrate-keyword.d.ts +6 -0
- package/dist/transcript/rehydrate-keyword.js +29 -0
- package/dist/transcript/rehydrate.d.ts +33 -0
- package/dist/transcript/rehydrate.js +285 -0
- package/dist/transcript/transcript-archive.d.ts +46 -0
- package/dist/transcript/transcript-archive.js +516 -0
- package/dist/types.d.ts +409 -0
- package/dist/types.js +104 -0
- package/dist/util/bounded-map.d.ts +1 -0
- package/dist/util/bounded-map.js +8 -0
- package/dist/util/rate-limiter.d.ts +12 -0
- package/dist/util/rate-limiter.js +54 -0
- package/dist/util/session-identity.d.ts +65 -0
- package/dist/util/session-identity.js +227 -0
- package/dist/util/util-hash.d.ts +1 -0
- package/dist/util/util-hash.js +4 -0
- package/package.json +59 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createMemoryRiver, type MemoryRiver, type MemoryRiverConfig, type MemoryRiverDeps, type MemoryUpdate, type RehydrateRequest, type SessionHint, } from './api.js';
|
|
2
|
+
export type { EmbeddingProvider, LlmClient, Logger, Notifier, SessionFileAccess, } from './ports.js';
|
|
3
|
+
export type { ContextMessage, EnumerationPlan, MemoryEntry, MemorySearchResult, MemoryCategory, MemoryStatus, SkillCapsuleV2, SkillDef, SkillIndexEntry, StatusChangeRequest, StatusChangeResult, } from './types.js';
|
|
4
|
+
export { Embedder as OllamaEmbedding } from './providers/embedder-v5.js';
|
|
5
|
+
export { getDevShmFreeBytes, resolveRamDbPath, MIN_RAM_DB_BYTES } from './storage.js';
|
|
6
|
+
export type { StorageMode } from './storage.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cleanup - 記憶代謝 (V4 升級版)
|
|
3
|
+
* memory-lance-v4
|
|
4
|
+
* * 核心原則:
|
|
5
|
+
* - 廢棄舊版扣減 importance 的邏輯
|
|
6
|
+
* - 統一轉交由 store-v4.ts 內的 decayMemories 執行 (基於 Health 系統)
|
|
7
|
+
* - 管理員指令直接與 V4 健康度生態系掛鉤
|
|
8
|
+
* - 刪除的記憶進入注入的回收桶,N 天後自動清除
|
|
9
|
+
*/
|
|
10
|
+
import { MemoryStore } from "../store/store-v4.js";
|
|
11
|
+
export interface CleanupEngineConfig {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
decayDays: number;
|
|
14
|
+
deleteBelow: number;
|
|
15
|
+
coreCategories?: string[];
|
|
16
|
+
coreImportanceThreshold?: number;
|
|
17
|
+
skillCapsuleProtection?: boolean;
|
|
18
|
+
useTrash?: boolean;
|
|
19
|
+
dryRun?: boolean;
|
|
20
|
+
/** 回收桶路徑 */
|
|
21
|
+
trashPath: string;
|
|
22
|
+
/** 回收桶保留天數(預設 7 天) */
|
|
23
|
+
trashRetentionDays?: number;
|
|
24
|
+
/** 是否啟用回收桶自動清除(預設 true) */
|
|
25
|
+
enableTrashAutoPurge?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface CleanupRunOptions {
|
|
28
|
+
dryRunOverride?: boolean;
|
|
29
|
+
maxDelete?: number;
|
|
30
|
+
maxDecay?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface CleanupRunResult {
|
|
33
|
+
deleted: number;
|
|
34
|
+
updated: number;
|
|
35
|
+
wouldDelete: number;
|
|
36
|
+
wouldDecay: number;
|
|
37
|
+
deferredDelete: number;
|
|
38
|
+
deferredDecay: number;
|
|
39
|
+
deleteCandidateSummary: {
|
|
40
|
+
count: number;
|
|
41
|
+
firstId: string | null;
|
|
42
|
+
lastId: string | null;
|
|
43
|
+
minCreatedAt: number | null;
|
|
44
|
+
maxCreatedAt: number | null;
|
|
45
|
+
createdAtByDay: Record<string, number>;
|
|
46
|
+
};
|
|
47
|
+
dryRun: boolean;
|
|
48
|
+
}
|
|
49
|
+
export declare class CleanupEngine {
|
|
50
|
+
private store;
|
|
51
|
+
private config;
|
|
52
|
+
private static _instance;
|
|
53
|
+
private readonly trashPath;
|
|
54
|
+
private readonly trashRetentionDays;
|
|
55
|
+
private readonly enableTrashAutoPurge;
|
|
56
|
+
private readonly useTrash;
|
|
57
|
+
private readonly dryRun;
|
|
58
|
+
constructor(store: MemoryStore, config: CleanupEngineConfig);
|
|
59
|
+
static getInstance(): CleanupEngine;
|
|
60
|
+
static setInstance(inst: CleanupEngine): void;
|
|
61
|
+
/**
|
|
62
|
+
* Session 結束鉤子 — 由 index.ts 的 session:end hook 觸發。
|
|
63
|
+
* 非同步、不阻塞:以 fire-and-forget 方式驅動健康度衰退。
|
|
64
|
+
*/
|
|
65
|
+
onSessionEnd(sessionId: string, _messages: unknown[], source?: string): void;
|
|
66
|
+
/**
|
|
67
|
+
* 執行智能清理 (調用 V4 核心代謝引擎)
|
|
68
|
+
*/
|
|
69
|
+
runSmartCleanup(source?: string, options?: CleanupRunOptions): Promise<CleanupRunResult>;
|
|
70
|
+
/**
|
|
71
|
+
* 刪除記憶:先搬到回收桶,再從 LanceDB 移除。
|
|
72
|
+
* 檔名格式:{id}_{timestamp}.json(含完整 entry 內容)
|
|
73
|
+
*/
|
|
74
|
+
delete(id: string): Promise<boolean>;
|
|
75
|
+
/**
|
|
76
|
+
* 掃描回收桶,自動清除超過 retention 天數的檔案。
|
|
77
|
+
* Idempotent:多次呼叫不會重複處理。
|
|
78
|
+
*/
|
|
79
|
+
purgeExpiredTrash(): Promise<number>;
|
|
80
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cleanup - 記憶代謝 (V4 升級版)
|
|
3
|
+
* memory-lance-v4
|
|
4
|
+
* * 核心原則:
|
|
5
|
+
* - 廢棄舊版扣減 importance 的邏輯
|
|
6
|
+
* - 統一轉交由 store-v4.ts 內的 decayMemories 執行 (基於 Health 系統)
|
|
7
|
+
* - 管理員指令直接與 V4 健康度生態系掛鉤
|
|
8
|
+
* - 刪除的記憶進入注入的回收桶,N 天後自動清除
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
export class CleanupEngine {
|
|
13
|
+
store;
|
|
14
|
+
config;
|
|
15
|
+
static _instance = null;
|
|
16
|
+
trashPath;
|
|
17
|
+
trashRetentionDays;
|
|
18
|
+
enableTrashAutoPurge;
|
|
19
|
+
useTrash;
|
|
20
|
+
dryRun;
|
|
21
|
+
constructor(store, config) {
|
|
22
|
+
this.store = store;
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.trashPath = config.trashPath;
|
|
25
|
+
this.trashRetentionDays = config.trashRetentionDays ?? 7;
|
|
26
|
+
this.enableTrashAutoPurge = config.enableTrashAutoPurge ?? true;
|
|
27
|
+
this.useTrash = config.useTrash ?? true;
|
|
28
|
+
this.dryRun = config.dryRun ?? false;
|
|
29
|
+
// 確保 trash 目錄存在(idempotent)
|
|
30
|
+
if (!fs.existsSync(this.trashPath)) {
|
|
31
|
+
fs.mkdirSync(this.trashPath, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
static getInstance() {
|
|
35
|
+
if (!CleanupEngine._instance) {
|
|
36
|
+
throw new Error('[CleanupEngine] getInstance() called before initialize(). Call CleanupEngine.getInstance() only after register() has instantiated it.');
|
|
37
|
+
}
|
|
38
|
+
return CleanupEngine._instance;
|
|
39
|
+
}
|
|
40
|
+
static setInstance(inst) {
|
|
41
|
+
CleanupEngine._instance = inst;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Session 結束鉤子 — 由 index.ts 的 session:end hook 觸發。
|
|
45
|
+
* 非同步、不阻塞:以 fire-and-forget 方式驅動健康度衰退。
|
|
46
|
+
*/
|
|
47
|
+
onSessionEnd(sessionId, _messages, source = 'session-end') {
|
|
48
|
+
console.log(`[CleanupEngine] session_end triggered, sessionId=${sessionId}, source=${source}`);
|
|
49
|
+
void this.runSmartCleanup(source).catch((err) => console.warn(`[CleanupEngine] onSessionEnd cleanup failed for ${sessionId}:`, err));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 執行智能清理 (調用 V4 核心代謝引擎)
|
|
53
|
+
*/
|
|
54
|
+
async runSmartCleanup(source = 'manual', options = {}) {
|
|
55
|
+
if (!this.config.enabled) {
|
|
56
|
+
return {
|
|
57
|
+
deleted: 0,
|
|
58
|
+
updated: 0,
|
|
59
|
+
wouldDelete: 0,
|
|
60
|
+
wouldDecay: 0,
|
|
61
|
+
deferredDelete: 0,
|
|
62
|
+
deferredDecay: 0,
|
|
63
|
+
deleteCandidateSummary: { count: 0, firstId: null, lastId: null, minCreatedAt: null, maxCreatedAt: null, createdAtByDay: {} },
|
|
64
|
+
dryRun: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const effectiveDryRun = options.dryRunOverride ?? this.dryRun;
|
|
68
|
+
const result = await this.store.decayMemories(5, this.config.deleteBelow, {
|
|
69
|
+
coreCategories: this.config.coreCategories,
|
|
70
|
+
coreImportanceThreshold: this.config.coreImportanceThreshold,
|
|
71
|
+
skillCapsuleProtection: this.config.skillCapsuleProtection ?? true,
|
|
72
|
+
dryRun: effectiveDryRun,
|
|
73
|
+
deleteWith: this.useTrash ? this.delete.bind(this) : undefined,
|
|
74
|
+
maxDelete: options.maxDelete,
|
|
75
|
+
maxDecay: options.maxDecay,
|
|
76
|
+
});
|
|
77
|
+
console.log(`[CleanupEngine] decayMemories: protected=${result.coreProtected} wouldDecay=${result.wouldDecay} wouldDelete=${result.wouldDelete}, source=${source}`);
|
|
78
|
+
if (effectiveDryRun) {
|
|
79
|
+
console.log(`[CleanupEngine] dry-run summary: would decay ${result.wouldDecay}, would delete ${result.wouldDelete}, source=${source}`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(`[CleanupEngine] decayed ${result.decayed} memories, deleted ${result.deleted} memories (dryRun=false), source=${source}`);
|
|
83
|
+
if (result.deferredDelete > 0 || result.deferredDecay > 0) {
|
|
84
|
+
console.log(`[CleanupEngine] ${source}: hit limit, deferred ${result.deferredDelete} delete candidates and ${result.deferredDecay} decay candidates to next run`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// 順便執行回收桶 auto-purge(idempotent)
|
|
88
|
+
if (!effectiveDryRun && this.enableTrashAutoPurge) {
|
|
89
|
+
await this.purgeExpiredTrash();
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
deleted: result.deleted,
|
|
93
|
+
updated: result.decayed,
|
|
94
|
+
wouldDelete: result.wouldDelete,
|
|
95
|
+
wouldDecay: result.wouldDecay,
|
|
96
|
+
deferredDelete: result.deferredDelete,
|
|
97
|
+
deferredDecay: result.deferredDecay,
|
|
98
|
+
deleteCandidateSummary: result.deleteCandidateSummary,
|
|
99
|
+
dryRun: effectiveDryRun,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// ═══════════════════════════════════════════════════════════
|
|
103
|
+
// 回收桶(Trash)管理
|
|
104
|
+
// ═══════════════════════════════════════════════════════════
|
|
105
|
+
/**
|
|
106
|
+
* 刪除記憶:先搬到回收桶,再從 LanceDB 移除。
|
|
107
|
+
* 檔名格式:{id}_{timestamp}.json(含完整 entry 內容)
|
|
108
|
+
*/
|
|
109
|
+
async delete(id) {
|
|
110
|
+
const entry = await this.store.getById(id, true);
|
|
111
|
+
if (!entry)
|
|
112
|
+
return false;
|
|
113
|
+
// 把完整 entry 寫入 trash
|
|
114
|
+
const timestamp = Date.now();
|
|
115
|
+
const trashFilename = `${id}_${timestamp}.json`;
|
|
116
|
+
const trashFilePath = path.join(this.trashPath, trashFilename);
|
|
117
|
+
fs.writeFileSync(trashFilePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
118
|
+
console.log(`[Cleanup] Memory ${id.slice(0, 8)} moved to trash: ${trashFilename} (retained for ${this.trashRetentionDays} days)`);
|
|
119
|
+
// 從 LanceDB 真正刪除
|
|
120
|
+
await this.store.delete(id);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 掃描回收桶,自動清除超過 retention 天數的檔案。
|
|
125
|
+
* Idempotent:多次呼叫不會重複處理。
|
|
126
|
+
*/
|
|
127
|
+
async purgeExpiredTrash() {
|
|
128
|
+
if (!fs.existsSync(this.trashPath)) {
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const retentionMs = this.trashRetentionDays * 24 * 60 * 60 * 1000;
|
|
133
|
+
const cutoff = now - retentionMs;
|
|
134
|
+
let purgedCount = 0;
|
|
135
|
+
let processedFiles = new Set();
|
|
136
|
+
const files = fs.readdirSync(this.trashPath);
|
|
137
|
+
for (const file of files) {
|
|
138
|
+
// 只處理格式正確的 trash 檔案
|
|
139
|
+
if (!file.endsWith(".json"))
|
|
140
|
+
continue;
|
|
141
|
+
const filePath = path.join(this.trashPath, file);
|
|
142
|
+
const stat = fs.statSync(filePath);
|
|
143
|
+
// 只刪除超期檔案
|
|
144
|
+
if (stat.mtimeMs < cutoff) {
|
|
145
|
+
// Idempotent check:已處理過同樣時間戳的檔案則跳過(不重複)
|
|
146
|
+
const timestampFromName = file.split("_").pop()?.replace(".json", "");
|
|
147
|
+
if (timestampFromName && processedFiles.has(timestampFromName)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (timestampFromName)
|
|
151
|
+
processedFiles.add(timestampFromName);
|
|
152
|
+
fs.unlinkSync(filePath);
|
|
153
|
+
console.log(`[Cleanup] Automatically purged trash file: ${file} (retained for more than ${this.trashRetentionDays} days)`);
|
|
154
|
+
purgedCount++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (purgedCount > 0) {
|
|
158
|
+
console.log(`[Cleanup] Trash purge complete: ${purgedCount} expired files permanently deleted`);
|
|
159
|
+
}
|
|
160
|
+
return purgedCount;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare const CLEANUP_RECOVERY_INTERVAL_MS: number;
|
|
2
|
+
export interface CleanupState {
|
|
3
|
+
lastSuccessfulRunAt: number;
|
|
4
|
+
lastDeleteCount: number;
|
|
5
|
+
lastDecayCount: number;
|
|
6
|
+
}
|
|
7
|
+
export interface StartupRecoveryLimits {
|
|
8
|
+
maxStartupDelete: number;
|
|
9
|
+
maxStartupDecay: number;
|
|
10
|
+
}
|
|
11
|
+
export type StartupRecoveryDecision = {
|
|
12
|
+
shouldRun: true;
|
|
13
|
+
reason: 'missing-state' | 'stale-state';
|
|
14
|
+
hoursSinceLastSuccess: number | null;
|
|
15
|
+
} | {
|
|
16
|
+
shouldRun: false;
|
|
17
|
+
reason: 'recent-success';
|
|
18
|
+
hoursSinceLastSuccess: number;
|
|
19
|
+
};
|
|
20
|
+
export type StartupRecoveryMode = {
|
|
21
|
+
dryRunOnly: true;
|
|
22
|
+
maxDelete: number;
|
|
23
|
+
maxDecay: number;
|
|
24
|
+
reason: 'backlog-too-large';
|
|
25
|
+
} | {
|
|
26
|
+
dryRunOnly: false;
|
|
27
|
+
maxDelete: number;
|
|
28
|
+
maxDecay: number;
|
|
29
|
+
reason: 'normal' | 'capped';
|
|
30
|
+
};
|
|
31
|
+
export declare function readCleanupState(filePath: string): CleanupState | null;
|
|
32
|
+
export declare function writeCleanupState(state: CleanupState, filePath: string): void;
|
|
33
|
+
export declare function shouldRunStartupRecovery(state: CleanupState | null, nowMs?: number): StartupRecoveryDecision;
|
|
34
|
+
export declare function chooseStartupRecoveryMode(estimatedDelete: number, limits: StartupRecoveryLimits): StartupRecoveryMode;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export const CLEANUP_RECOVERY_INTERVAL_MS = 25 * 60 * 60 * 1000;
|
|
4
|
+
export function readCleanupState(filePath) {
|
|
5
|
+
try {
|
|
6
|
+
if (!fs.existsSync(filePath))
|
|
7
|
+
return null;
|
|
8
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
9
|
+
if (typeof parsed.lastSuccessfulRunAt !== 'number')
|
|
10
|
+
return null;
|
|
11
|
+
return {
|
|
12
|
+
lastSuccessfulRunAt: parsed.lastSuccessfulRunAt,
|
|
13
|
+
lastDeleteCount: typeof parsed.lastDeleteCount === 'number' ? parsed.lastDeleteCount : 0,
|
|
14
|
+
lastDecayCount: typeof parsed.lastDecayCount === 'number' ? parsed.lastDecayCount : 0,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
console.warn('[CleanupEngine] cleanup-state read failed:', err?.message ?? err);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function writeCleanupState(state, filePath) {
|
|
23
|
+
const dir = path.dirname(filePath);
|
|
24
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
25
|
+
fs.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}\n`, 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
export function shouldRunStartupRecovery(state, nowMs = Date.now()) {
|
|
28
|
+
if (!state) {
|
|
29
|
+
return { shouldRun: true, reason: 'missing-state', hoursSinceLastSuccess: null };
|
|
30
|
+
}
|
|
31
|
+
const elapsedMs = nowMs - state.lastSuccessfulRunAt;
|
|
32
|
+
const hoursSinceLastSuccess = Math.max(0, elapsedMs / (60 * 60 * 1000));
|
|
33
|
+
if (elapsedMs < CLEANUP_RECOVERY_INTERVAL_MS) {
|
|
34
|
+
return { shouldRun: false, reason: 'recent-success', hoursSinceLastSuccess };
|
|
35
|
+
}
|
|
36
|
+
return { shouldRun: true, reason: 'stale-state', hoursSinceLastSuccess };
|
|
37
|
+
}
|
|
38
|
+
export function chooseStartupRecoveryMode(estimatedDelete, limits) {
|
|
39
|
+
const maxDelete = Math.max(0, Math.floor(limits.maxStartupDelete));
|
|
40
|
+
const maxDecay = Math.max(0, Math.floor(limits.maxStartupDecay));
|
|
41
|
+
if (estimatedDelete > maxDelete * 2) {
|
|
42
|
+
return { dryRunOnly: true, maxDelete, maxDecay, reason: 'backlog-too-large' };
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
dryRunOnly: false,
|
|
46
|
+
maxDelete,
|
|
47
|
+
maxDecay,
|
|
48
|
+
reason: estimatedDelete > maxDelete ? 'capped' : 'normal',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Night Consolidation — 記憶夜間整理模組
|
|
3
|
+
*
|
|
4
|
+
* 每天凌晨(或手動觸發)自動執行:
|
|
5
|
+
* 1. 取出當日所有新寫入的記憶
|
|
6
|
+
* 2. 依 slotKey 分組,找出重覆/碎片記憶
|
|
7
|
+
* 3. 丟 MiniMax M2.7 (thinking=high) 做品質把關決策
|
|
8
|
+
* 4. 執行:合併、刪除、更新 confidence/category
|
|
9
|
+
* 5. 寫入 consolidation 日誌
|
|
10
|
+
*/
|
|
11
|
+
import type { MemoryEntry, MemoryCategory } from '../types.js';
|
|
12
|
+
import type { StatusManager } from '../store/status-manager.js';
|
|
13
|
+
import { type NightRecoverySource } from './night-recovery.js';
|
|
14
|
+
import type { LlmClient, Notifier } from '../ports.js';
|
|
15
|
+
export interface ConsolidationDecision {
|
|
16
|
+
action: 'merge' | 'delete' | 'update' | 'keep' | 'deprecated';
|
|
17
|
+
memoryId: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
mergeIntoId?: string;
|
|
20
|
+
newCategory?: MemoryCategory;
|
|
21
|
+
newConfidence?: number;
|
|
22
|
+
newSlotKey?: string;
|
|
23
|
+
newText?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ConsolidationPlan {
|
|
26
|
+
decisions: ConsolidationDecision[];
|
|
27
|
+
summary: string;
|
|
28
|
+
processedCount: number;
|
|
29
|
+
mergedCount: number;
|
|
30
|
+
deletedCount: number;
|
|
31
|
+
updatedCount: number;
|
|
32
|
+
keptCount: number;
|
|
33
|
+
}
|
|
34
|
+
export interface ConsolidationResult {
|
|
35
|
+
plan: ConsolidationPlan;
|
|
36
|
+
executedAt: number;
|
|
37
|
+
durationMs: number;
|
|
38
|
+
errors: string[];
|
|
39
|
+
}
|
|
40
|
+
export declare class NightConsolidator {
|
|
41
|
+
private store;
|
|
42
|
+
private _options;
|
|
43
|
+
private logPath;
|
|
44
|
+
constructor(store: {
|
|
45
|
+
queryAll(limit?: number): Promise<MemoryEntry[]>;
|
|
46
|
+
getById(id: string, includeAllStatus?: boolean): Promise<MemoryEntry | null>;
|
|
47
|
+
update(id: string, updates: Partial<MemoryEntry>): Promise<boolean>;
|
|
48
|
+
delete(id: string): Promise<boolean>;
|
|
49
|
+
searchBySlotKey(slotKey: string): Promise<MemoryEntry[]>;
|
|
50
|
+
recordNightConsolidationStat?(record: {
|
|
51
|
+
runId: string;
|
|
52
|
+
phase: string;
|
|
53
|
+
ts?: number;
|
|
54
|
+
outcome?: string | null;
|
|
55
|
+
durationMs?: number | null;
|
|
56
|
+
candidateCount?: number | null;
|
|
57
|
+
scannedCount?: number | null;
|
|
58
|
+
decisionCount?: number | null;
|
|
59
|
+
mergeCount?: number | null;
|
|
60
|
+
deleteCount?: number | null;
|
|
61
|
+
deprecatedCount?: number | null;
|
|
62
|
+
updateCount?: number | null;
|
|
63
|
+
keepCount?: number | null;
|
|
64
|
+
attemptedCount?: number | null;
|
|
65
|
+
failedCount?: number | null;
|
|
66
|
+
batchIndex?: number | null;
|
|
67
|
+
batchSize?: number | null;
|
|
68
|
+
driftMs?: number | null;
|
|
69
|
+
scheduledFor?: number | null;
|
|
70
|
+
errorMessage?: string | null;
|
|
71
|
+
metadata?: string | Record<string, unknown> | null;
|
|
72
|
+
}): Promise<void>;
|
|
73
|
+
}, _options: {
|
|
74
|
+
concentrator?: LlmClient;
|
|
75
|
+
statusManager: StatusManager;
|
|
76
|
+
notifier?: Notifier;
|
|
77
|
+
}, consolidationLog: string);
|
|
78
|
+
private recordStat;
|
|
79
|
+
private statMetadata;
|
|
80
|
+
private _broadcast;
|
|
81
|
+
/**
|
|
82
|
+
* 執行夜間整理(當日記憶)
|
|
83
|
+
*/
|
|
84
|
+
consolidateToday(runId?: `${string}-${string}-${string}-${string}-${string}`, source?: NightRecoverySource): Promise<ConsolidationResult>;
|
|
85
|
+
/**
|
|
86
|
+
* 執行夜間整理(指定時間範圍)
|
|
87
|
+
* @param range 'today' | 'yesterday' | number (days ago)
|
|
88
|
+
*/
|
|
89
|
+
consolidateRange(range: 'today' | 'yesterday' | number, runId?: `${string}-${string}-${string}-${string}-${string}`, source?: NightRecoverySource): Promise<ConsolidationResult>;
|
|
90
|
+
private llmDecide;
|
|
91
|
+
private _llmDecideBatch;
|
|
92
|
+
private callAgent;
|
|
93
|
+
private executePlan;
|
|
94
|
+
private parseMeta;
|
|
95
|
+
/**
|
|
96
|
+
* checkSupersedes — 查詢同 slotKey 的所有 active 舊版本
|
|
97
|
+
* @returns 被取代的舊 entry id 清單
|
|
98
|
+
*/
|
|
99
|
+
private checkSupersedes;
|
|
100
|
+
private extractJson;
|
|
101
|
+
private writeLog;
|
|
102
|
+
}
|