sensorium-mcp 2.17.28 → 3.0.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.
- package/Install-Sensorium.ps1 +351 -0
- package/README.md +14 -0
- package/dist/config.d.ts +16 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +39 -2
- package/dist/config.js.map +1 -1
- package/dist/daily-session.d.ts +2 -1
- package/dist/daily-session.d.ts.map +1 -1
- package/dist/daily-session.js +23 -26
- package/dist/daily-session.js.map +1 -1
- package/dist/dashboard/routes/settings.d.ts +4 -0
- package/dist/dashboard/routes/settings.d.ts.map +1 -1
- package/dist/dashboard/routes/settings.js +57 -1
- package/dist/dashboard/routes/settings.js.map +1 -1
- package/dist/dashboard/routes/threads.d.ts +1 -0
- package/dist/dashboard/routes/threads.d.ts.map +1 -1
- package/dist/dashboard/routes/threads.js +23 -25
- package/dist/dashboard/routes/threads.js.map +1 -1
- package/dist/dashboard/routes.d.ts.map +1 -1
- package/dist/dashboard/routes.js +7 -2
- package/dist/dashboard/routes.js.map +1 -1
- package/dist/dashboard/spa.html +11 -11
- package/dist/data/interfaces.d.ts +36 -0
- package/dist/data/interfaces.d.ts.map +1 -0
- package/dist/data/interfaces.js +2 -0
- package/dist/data/interfaces.js.map +1 -0
- package/dist/data/memory/bootstrap.d.ts +36 -16
- package/dist/data/memory/bootstrap.d.ts.map +1 -1
- package/dist/data/memory/bootstrap.js +71 -217
- package/dist/data/memory/bootstrap.js.map +1 -1
- package/dist/data/memory/consolidation.d.ts +35 -34
- package/dist/data/memory/consolidation.d.ts.map +1 -1
- package/dist/data/memory/consolidation.js +43 -554
- package/dist/data/memory/consolidation.js.map +1 -1
- package/dist/data/memory/migration-runner.d.ts +5 -0
- package/dist/data/memory/migration-runner.d.ts.map +1 -0
- package/dist/data/memory/migration-runner.js +403 -0
- package/dist/data/memory/migration-runner.js.map +1 -0
- package/dist/data/memory/reflection.js +1 -1
- package/dist/data/memory/schema-ddl.d.ts +4 -0
- package/dist/data/memory/schema-ddl.d.ts.map +1 -0
- package/dist/data/memory/schema-ddl.js +194 -0
- package/dist/data/memory/schema-ddl.js.map +1 -0
- package/dist/data/memory/schema-guard.d.ts +3 -0
- package/dist/data/memory/schema-guard.d.ts.map +1 -0
- package/dist/data/memory/schema-guard.js +184 -0
- package/dist/data/memory/schema-guard.js.map +1 -0
- package/dist/data/memory/schema.d.ts +2 -5
- package/dist/data/memory/schema.d.ts.map +1 -1
- package/dist/data/memory/schema.js +6 -834
- package/dist/data/memory/schema.js.map +1 -1
- package/dist/data/memory/synthesis.js +2 -2
- package/dist/data/memory/synthesis.js.map +1 -1
- package/dist/data/memory/thread-registry.d.ts +18 -4
- package/dist/data/memory/thread-registry.d.ts.map +1 -1
- package/dist/data/memory/thread-registry.js +25 -0
- package/dist/data/memory/thread-registry.js.map +1 -1
- package/dist/data/sent-message.repository.d.ts +12 -0
- package/dist/data/sent-message.repository.d.ts.map +1 -0
- package/dist/data/sent-message.repository.js +31 -0
- package/dist/data/sent-message.repository.js.map +1 -0
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +23 -2
- package/dist/http-server.js.map +1 -1
- package/dist/index.js +27 -48
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +7 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +89 -12
- package/dist/logger.js.map +1 -1
- package/dist/scheduler.d.ts +8 -0
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +15 -0
- package/dist/scheduler.js.map +1 -1
- package/dist/server/factory.d.ts +2 -1
- package/dist/server/factory.d.ts.map +1 -1
- package/dist/server/factory.js +11 -4
- package/dist/server/factory.js.map +1 -1
- package/dist/services/agent-spawn.service.d.ts +39 -0
- package/dist/services/agent-spawn.service.d.ts.map +1 -0
- package/dist/services/agent-spawn.service.js +348 -0
- package/dist/services/agent-spawn.service.js.map +1 -0
- package/dist/services/background-runner.d.ts +26 -0
- package/dist/services/background-runner.d.ts.map +1 -0
- package/dist/services/background-runner.js +71 -0
- package/dist/services/background-runner.js.map +1 -0
- package/dist/services/consolidation.service.d.ts +16 -0
- package/dist/services/consolidation.service.d.ts.map +1 -0
- package/dist/services/consolidation.service.js +508 -0
- package/dist/services/consolidation.service.js.map +1 -0
- package/dist/services/dispatcher/broker.d.ts +2 -0
- package/dist/services/dispatcher/broker.d.ts.map +1 -1
- package/dist/services/dispatcher/broker.js +5 -10
- package/dist/services/dispatcher/broker.js.map +1 -1
- package/dist/services/dispatcher/index.d.ts +1 -1
- package/dist/services/dispatcher/index.d.ts.map +1 -1
- package/dist/services/dispatcher/index.js +1 -1
- package/dist/services/dispatcher/index.js.map +1 -1
- package/dist/services/dispatcher/lock.d.ts.map +1 -1
- package/dist/services/dispatcher/lock.js +7 -11
- package/dist/services/dispatcher/lock.js.map +1 -1
- package/dist/services/maintenance-signal.d.ts +18 -0
- package/dist/services/maintenance-signal.d.ts.map +1 -0
- package/dist/services/maintenance-signal.js +48 -0
- package/dist/services/maintenance-signal.js.map +1 -0
- package/dist/services/memory-briefing.service.d.ts +4 -0
- package/dist/services/memory-briefing.service.d.ts.map +1 -0
- package/dist/services/memory-briefing.service.js +143 -0
- package/dist/services/memory-briefing.service.js.map +1 -0
- package/dist/services/process.service.d.ts +31 -0
- package/dist/services/process.service.d.ts.map +1 -0
- package/dist/services/process.service.js +100 -0
- package/dist/services/process.service.js.map +1 -0
- package/dist/services/thread-health.service.d.ts +18 -0
- package/dist/services/thread-health.service.d.ts.map +1 -0
- package/dist/services/thread-health.service.js +118 -0
- package/dist/services/thread-health.service.js.map +1 -0
- package/dist/services/thread-lifecycle.service.d.ts +52 -0
- package/dist/services/thread-lifecycle.service.d.ts.map +1 -0
- package/dist/services/thread-lifecycle.service.js +174 -0
- package/dist/services/thread-lifecycle.service.js.map +1 -0
- package/dist/services/topic.service.d.ts +25 -0
- package/dist/services/topic.service.d.ts.map +1 -0
- package/dist/services/topic.service.js +65 -0
- package/dist/services/topic.service.js.map +1 -0
- package/dist/services/worker-cleanup.service.d.ts +8 -0
- package/dist/services/worker-cleanup.service.d.ts.map +1 -0
- package/dist/services/worker-cleanup.service.js +82 -0
- package/dist/services/worker-cleanup.service.js.map +1 -0
- package/dist/sessions.d.ts +14 -0
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +55 -0
- package/dist/sessions.js.map +1 -1
- package/dist/telegram.d.ts +13 -6
- package/dist/telegram.d.ts.map +1 -1
- package/dist/telegram.js +43 -14
- package/dist/telegram.js.map +1 -1
- package/dist/tools/delegate-tool.d.ts +4 -0
- package/dist/tools/delegate-tool.d.ts.map +1 -1
- package/dist/tools/delegate-tool.js +48 -109
- package/dist/tools/delegate-tool.js.map +1 -1
- package/dist/tools/memory-tools.d.ts.map +1 -1
- package/dist/tools/memory-tools.js +1 -1
- package/dist/tools/memory-tools.js.map +1 -1
- package/dist/tools/shared-agent-utils.d.ts +9 -1
- package/dist/tools/shared-agent-utils.d.ts.map +1 -1
- package/dist/tools/shared-agent-utils.js +21 -38
- package/dist/tools/shared-agent-utils.js.map +1 -1
- package/dist/tools/start-session-tool.d.ts +2 -0
- package/dist/tools/start-session-tool.d.ts.map +1 -1
- package/dist/tools/start-session-tool.js +68 -118
- package/dist/tools/start-session-tool.js.map +1 -1
- package/dist/tools/thread-lifecycle.d.ts +5 -127
- package/dist/tools/thread-lifecycle.d.ts.map +1 -1
- package/dist/tools/thread-lifecycle.js +5 -1167
- package/dist/tools/thread-lifecycle.js.map +1 -1
- package/dist/tools/utility-tools.js +5 -2
- package/dist/tools/utility-tools.js.map +1 -1
- package/dist/tools/wait/drive-handler.d.ts +0 -1
- package/dist/tools/wait/drive-handler.d.ts.map +1 -1
- package/dist/tools/wait/drive-handler.js +5 -22
- package/dist/tools/wait/drive-handler.js.map +1 -1
- package/dist/tools/wait/message-delivery.js +1 -1
- package/dist/tools/wait/message-delivery.js.map +1 -1
- package/dist/tools/wait/message-processing.d.ts.map +1 -1
- package/dist/tools/wait/message-processing.js +9 -8
- package/dist/tools/wait/message-processing.js.map +1 -1
- package/dist/tools/wait/poll-loop.d.ts +2 -0
- package/dist/tools/wait/poll-loop.d.ts.map +1 -1
- package/dist/tools/wait/poll-loop.js +27 -29
- package/dist/tools/wait/poll-loop.js.map +1 -1
- package/dist/tools/wait/task-handler.d.ts +0 -3
- package/dist/tools/wait/task-handler.d.ts.map +1 -1
- package/dist/tools/wait/task-handler.js +3 -2
- package/dist/tools/wait/task-handler.js.map +1 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -8
- package/supervisor/config.go +182 -69
- package/supervisor/config_test.go +78 -0
- package/supervisor/go.mod +12 -0
- package/supervisor/go.sum +20 -0
- package/supervisor/health.go +24 -6
- package/supervisor/keeper.go +15 -10
- package/supervisor/log.go +109 -28
- package/supervisor/log_test.go +86 -6
- package/supervisor/main.go +146 -19
- package/supervisor/main_test.go +130 -0
- package/supervisor/process.go +47 -4
- package/supervisor/process_test.go +14 -0
- package/supervisor/secrets.go +95 -0
- package/supervisor/secrets_securevault_test.go +98 -0
- package/supervisor/secrets_test.go +119 -0
- package/supervisor/self_update.go +282 -0
- package/supervisor/self_update_test.go +177 -0
- package/supervisor/service_restart_stub.go +9 -0
- package/supervisor/service_restart_windows.go +63 -0
- package/supervisor/service_stub.go +15 -0
- package/supervisor/service_windows.go +216 -0
- package/supervisor/update_state.go +264 -0
- package/supervisor/update_state_test.go +306 -0
- package/supervisor/updater.go +341 -10
- package/supervisor/updater_test.go +64 -0
- package/scripts/install-supervisor.ps1 +0 -67
- package/scripts/install-supervisor.sh +0 -43
- package/scripts/start-supervisor.ps1 +0 -46
- package/scripts/start-supervisor.sh +0 -20
|
@@ -1,57 +1,62 @@
|
|
|
1
1
|
import { cleanupOldSentMessages } from "./schema.js";
|
|
2
|
-
import {
|
|
3
|
-
import { saveSemanticNote, searchSemanticNotesRanked, supersedeNote, archiveNotesForThread, getThreadIdsWithActiveNotes, searchByEmbedding, saveNoteEmbedding, } from "./semantic.js";
|
|
2
|
+
import { archiveNotesForThread, getThreadIdsWithActiveNotes } from "./semantic.js";
|
|
4
3
|
import { log } from "../../logger.js";
|
|
5
|
-
import {
|
|
6
|
-
import { nowISO, repairAndParseJSON } from "./utils.js";
|
|
7
|
-
import { chatCompletion, generateEmbedding } from "../../integrations/openai/chat.js";
|
|
8
|
-
import { errorMessage } from "../../utils.js";
|
|
4
|
+
import { nowISO } from "./utils.js";
|
|
9
5
|
import { getAllThreads } from "./thread-registry.js";
|
|
10
|
-
|
|
11
|
-
function
|
|
6
|
+
const TERMINAL_THREAD_STATUSES = new Set(["archived", "expired", "exited"]);
|
|
7
|
+
export function getUnconsolidatedThreadIds(db) {
|
|
8
|
+
const rows = db
|
|
9
|
+
.prepare(`SELECT DISTINCT thread_id FROM episodes WHERE consolidated = 0`)
|
|
10
|
+
.all();
|
|
11
|
+
return rows.map((row) => row.thread_id);
|
|
12
|
+
}
|
|
13
|
+
export function logConsolidation(db, entry) {
|
|
12
14
|
db.prepare(`INSERT INTO meta_consolidation_log
|
|
13
15
|
(run_at, episodes_processed, notes_created, duration_ms)
|
|
14
16
|
VALUES (?, ?, ?, ?)`).run(nowISO(), entry.episodesProcessed, entry.notesCreated, entry.durationMs);
|
|
15
17
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
18
|
+
export function cleanupConsolidationHousekeeping(db) {
|
|
19
|
+
cleanupOldSentMessages(db);
|
|
20
|
+
}
|
|
21
|
+
export function getCandidateNotesForPruning(db, maxNotes) {
|
|
22
|
+
return db.prepare(`
|
|
23
|
+
SELECT * FROM semantic_notes
|
|
24
|
+
WHERE valid_to IS NULL
|
|
25
|
+
AND superseded_by IS NULL
|
|
26
|
+
AND is_guardrail = 0
|
|
27
|
+
AND pinned = 0
|
|
28
|
+
ORDER BY access_count ASC, created_at ASC
|
|
29
|
+
LIMIT ?
|
|
30
|
+
`).all(maxNotes);
|
|
31
|
+
}
|
|
32
|
+
export function hasActiveNote(db, noteId) {
|
|
33
|
+
const row = db.prepare(`SELECT note_id FROM semantic_notes WHERE note_id = ? AND valid_to IS NULL AND superseded_by IS NULL`).get(noteId);
|
|
34
|
+
return Boolean(row);
|
|
35
|
+
}
|
|
36
|
+
export function getActiveNoteContent(db, noteId) {
|
|
37
|
+
const row = db.prepare(`SELECT note_id, content FROM semantic_notes WHERE note_id = ? AND valid_to IS NULL AND superseded_by IS NULL`).get(noteId);
|
|
38
|
+
if (!row)
|
|
39
|
+
return null;
|
|
40
|
+
return { noteId: row.note_id, content: row.content };
|
|
41
|
+
}
|
|
42
|
+
export function expireNote(db, noteId, now) {
|
|
43
|
+
db.prepare(`UPDATE semantic_notes SET valid_to = ?, updated_at = ? WHERE note_id = ?`).run(now, now, noteId);
|
|
44
|
+
}
|
|
45
|
+
export function mergeDuplicateNote(db, keepId, expireId, now, mergedContent) {
|
|
46
|
+
if (mergedContent) {
|
|
47
|
+
db.prepare(`UPDATE semantic_notes SET content = ?, updated_at = ? WHERE note_id = ?`).run(mergedContent, now, keepId);
|
|
35
48
|
}
|
|
49
|
+
db.prepare(`UPDATE semantic_notes SET valid_to = ?, superseded_by = ?, updated_at = ? WHERE note_id = ?`).run(now, keepId, now, expireId);
|
|
36
50
|
}
|
|
37
|
-
|
|
38
|
-
/** Terminal thread statuses that indicate notes should be expired. */
|
|
39
|
-
const TERMINAL_THREAD_STATUSES = new Set(['archived', 'expired', 'exited']);
|
|
40
|
-
/**
|
|
41
|
-
* Find threads with active semantic notes whose registry status is terminal
|
|
42
|
-
* (archived/expired/exited) and expire those notes.
|
|
43
|
-
* Returns total number of notes archived.
|
|
44
|
-
*/
|
|
45
|
-
function sweepOrphanedNotes(db) {
|
|
51
|
+
export function sweepOrphanedNotes(db) {
|
|
46
52
|
const threadIdsWithNotes = getThreadIdsWithActiveNotes(db);
|
|
47
53
|
if (threadIdsWithNotes.length === 0)
|
|
48
54
|
return 0;
|
|
49
55
|
const allThreads = getAllThreads(db);
|
|
50
|
-
const threadStatusMap = new Map(allThreads.map(
|
|
56
|
+
const threadStatusMap = new Map(allThreads.map((thread) => [thread.threadId, thread.status]));
|
|
51
57
|
let totalArchived = 0;
|
|
52
58
|
for (const threadId of threadIdsWithNotes) {
|
|
53
59
|
const status = threadStatusMap.get(threadId);
|
|
54
|
-
// Archive notes for terminal threads AND threads missing from registry
|
|
55
60
|
if (!status || TERMINAL_THREAD_STATUSES.has(status)) {
|
|
56
61
|
totalArchived += archiveNotesForThread(db, threadId);
|
|
57
62
|
}
|
|
@@ -61,520 +66,4 @@ function sweepOrphanedNotes(db) {
|
|
|
61
66
|
}
|
|
62
67
|
return totalArchived;
|
|
63
68
|
}
|
|
64
|
-
// ─── Intelligent Consolidation ───────────────────────────────────────────────
|
|
65
|
-
// PRIVACY NOTE: This function sends conversation episode excerpts to OpenAI's
|
|
66
|
-
// API for knowledge extraction and consolidation. Operators can disable this
|
|
67
|
-
// by setting the environment variable CONSOLIDATION_ENABLED=false (or "0").
|
|
68
|
-
let consolidationInProgress = false;
|
|
69
|
-
/**
|
|
70
|
-
* Consolidate ALL threads that have unconsolidated episodes.
|
|
71
|
-
* Iterates distinct thread_ids and runs per-thread consolidation for each.
|
|
72
|
-
* Returns an aggregated report.
|
|
73
|
-
*
|
|
74
|
-
* Owns the global `consolidationInProgress` lock so that per-thread calls
|
|
75
|
-
* within the loop don't block each other and concurrent invocations are
|
|
76
|
-
* properly serialized.
|
|
77
|
-
*/
|
|
78
|
-
export async function runConsolidationAllThreads(db, options) {
|
|
79
|
-
if (consolidationInProgress) {
|
|
80
|
-
log.info("Consolidation already in progress — skipping (all-threads)");
|
|
81
|
-
return {
|
|
82
|
-
episodesProcessed: 0,
|
|
83
|
-
notesCreated: 0,
|
|
84
|
-
durationMs: 0,
|
|
85
|
-
details: ["Skipped — consolidation already in progress."],
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
consolidationInProgress = true;
|
|
89
|
-
try {
|
|
90
|
-
const startMs = Date.now();
|
|
91
|
-
const threadRows = db
|
|
92
|
-
.prepare(`SELECT DISTINCT thread_id FROM episodes WHERE consolidated = 0`)
|
|
93
|
-
.all();
|
|
94
|
-
if (threadRows.length === 0) {
|
|
95
|
-
return {
|
|
96
|
-
episodesProcessed: 0,
|
|
97
|
-
notesCreated: 0,
|
|
98
|
-
durationMs: Date.now() - startMs,
|
|
99
|
-
details: ["Nothing to consolidate across any thread."],
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
let totalProcessed = 0;
|
|
103
|
-
let totalNotes = 0;
|
|
104
|
-
const allDetails = [];
|
|
105
|
-
for (const { thread_id } of threadRows) {
|
|
106
|
-
const report = await runIntelligentConsolidation(db, thread_id, {
|
|
107
|
-
maxEpisodes: options?.maxEpisodesPerThread ?? 30,
|
|
108
|
-
dryRun: options?.dryRun,
|
|
109
|
-
_skipLock: true, // lock is held by this function
|
|
110
|
-
});
|
|
111
|
-
totalProcessed += report.episodesProcessed;
|
|
112
|
-
totalNotes += report.notesCreated;
|
|
113
|
-
allDetails.push(`Thread ${thread_id}: ${report.episodesProcessed} eps → ${report.notesCreated} notes`);
|
|
114
|
-
}
|
|
115
|
-
// Phase 2: Memory pruning — scan for outdated, duplicate, or low-quality notes
|
|
116
|
-
try {
|
|
117
|
-
const pruneReport = await runMemoryPruning(db);
|
|
118
|
-
if (pruneReport.notesExpired + pruneReport.notesMerged > 0) {
|
|
119
|
-
allDetails.push(`Pruning: scanned ${pruneReport.notesScanned}, expired ${pruneReport.notesExpired}, merged ${pruneReport.notesMerged}`);
|
|
120
|
-
allDetails.push(...pruneReport.details);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
log.warn(`[memory] Pruning phase failed: ${errorMessage(err)}`);
|
|
125
|
-
}
|
|
126
|
-
// Phase 3: Orphaned notes sweep — expire notes for dead/archived threads
|
|
127
|
-
try {
|
|
128
|
-
const orphanedCount = sweepOrphanedNotes(db);
|
|
129
|
-
if (orphanedCount > 0) {
|
|
130
|
-
allDetails.push(`Orphan sweep: archived ${orphanedCount} notes from dead threads`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
catch (err) {
|
|
134
|
-
log.warn(`[memory] Orphan notes sweep failed: ${errorMessage(err)}`);
|
|
135
|
-
}
|
|
136
|
-
return {
|
|
137
|
-
episodesProcessed: totalProcessed,
|
|
138
|
-
notesCreated: totalNotes,
|
|
139
|
-
durationMs: Date.now() - startMs,
|
|
140
|
-
details: allDetails,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
finally {
|
|
144
|
-
consolidationInProgress = false;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
export async function runIntelligentConsolidation(db, threadId, options) {
|
|
148
|
-
// Opt-out: allow operators to disable consolidation for privacy reasons
|
|
149
|
-
const consolidationEnabled = process.env.CONSOLIDATION_ENABLED;
|
|
150
|
-
if (consolidationEnabled === "false" || consolidationEnabled === "0") {
|
|
151
|
-
return {
|
|
152
|
-
episodesProcessed: 0,
|
|
153
|
-
notesCreated: 0,
|
|
154
|
-
durationMs: 0,
|
|
155
|
-
details: ["Consolidation disabled via CONSOLIDATION_ENABLED env var."],
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
const skipLock = options?._skipLock ?? false;
|
|
159
|
-
if (!skipLock && consolidationInProgress) {
|
|
160
|
-
log.info("Consolidation already in progress — skipping");
|
|
161
|
-
return {
|
|
162
|
-
episodesProcessed: 0,
|
|
163
|
-
notesCreated: 0,
|
|
164
|
-
durationMs: 0,
|
|
165
|
-
details: ["Skipped — consolidation already in progress."],
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
if (!skipLock)
|
|
169
|
-
consolidationInProgress = true;
|
|
170
|
-
try {
|
|
171
|
-
const startMs = Date.now();
|
|
172
|
-
const maxEpisodes = options?.maxEpisodes ?? 30;
|
|
173
|
-
const dryRun = options?.dryRun ?? false;
|
|
174
|
-
const episodes = getUnconsolidatedEpisodes(db, threadId, maxEpisodes);
|
|
175
|
-
if (episodes.length === 0) {
|
|
176
|
-
return {
|
|
177
|
-
episodesProcessed: 0,
|
|
178
|
-
notesCreated: 0,
|
|
179
|
-
durationMs: Date.now() - startMs,
|
|
180
|
-
details: ["Nothing to consolidate."],
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
// Format episodes for the prompt
|
|
184
|
-
const episodesText = episodes
|
|
185
|
-
.map((ep, i) => {
|
|
186
|
-
const c = ep.content;
|
|
187
|
-
let content;
|
|
188
|
-
if (c && typeof c === "object") {
|
|
189
|
-
content = c.text ?? c.caption ?? JSON.stringify(c);
|
|
190
|
-
}
|
|
191
|
-
else {
|
|
192
|
-
content = JSON.stringify(ep.content);
|
|
193
|
-
}
|
|
194
|
-
return `[${i + 1}] (${ep.type}/${ep.modality}, ${ep.timestamp}) ${content}`;
|
|
195
|
-
})
|
|
196
|
-
.join("\n");
|
|
197
|
-
// ── Contradiction detection: find existing notes related to these episodes ──
|
|
198
|
-
// Extract keywords from episodes to search for potentially conflicting notes
|
|
199
|
-
const episodeWords = episodesText.toLowerCase()
|
|
200
|
-
.replace(/[^a-z0-9\s]/g, " ")
|
|
201
|
-
.split(/\s+/)
|
|
202
|
-
.filter(w => w.length > 3);
|
|
203
|
-
const wordFreq = new Map();
|
|
204
|
-
const stopWords = new Set(["this", "that", "with", "from", "have", "been", "will", "would", "could", "should", "about", "there", "their", "which", "when", "what", "were", "they", "than", "then", "also", "just", "more", "some", "into", "over", "after", "before", "other", "very", "your", "here"]);
|
|
205
|
-
for (const w of episodeWords) {
|
|
206
|
-
if (!stopWords.has(w))
|
|
207
|
-
wordFreq.set(w, (wordFreq.get(w) ?? 0) + 1);
|
|
208
|
-
}
|
|
209
|
-
const topKeywords = [...wordFreq.entries()]
|
|
210
|
-
.sort((a, b) => b[1] - a[1])
|
|
211
|
-
.slice(0, 12)
|
|
212
|
-
.map(([w]) => w);
|
|
213
|
-
let existingNotesSection = "";
|
|
214
|
-
if (topKeywords.length > 0) {
|
|
215
|
-
try {
|
|
216
|
-
const related = searchSemanticNotesRanked(db, topKeywords.join(" "), {
|
|
217
|
-
maxResults: 15,
|
|
218
|
-
skipAccessTracking: true,
|
|
219
|
-
minMatchRatio: 0.2, // broader recall for contradiction scan
|
|
220
|
-
});
|
|
221
|
-
if (related.length > 0) {
|
|
222
|
-
existingNotesSection = `\n\nExisting memory notes (potentially related):
|
|
223
|
-
${related.map(n => `[${n.noteId}] (${n.type}, conf: ${n.confidence}) ${n.content}`).join("\n")}`;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
catch (err) {
|
|
227
|
-
log.warn(`[consolidation] searchSemanticNotesRanked failed during contradiction scan: ${errorMessage(err)}`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
const systemPrompt = `You are a memory consolidation agent. Analyze these conversation episodes and extract knowledge that should be remembered across sessions.
|
|
231
|
-
|
|
232
|
-
Episodes:
|
|
233
|
-
${episodesText}${existingNotesSection}
|
|
234
|
-
|
|
235
|
-
Output a JSON object with:
|
|
236
|
-
{
|
|
237
|
-
"notes": [
|
|
238
|
-
{
|
|
239
|
-
"type": "fact" | "preference" | "pattern" | "entity" | "relationship",
|
|
240
|
-
"content": "One clear sentence describing the knowledge",
|
|
241
|
-
"keywords": ["keyword1", "keyword2", "keyword3"],
|
|
242
|
-
"confidence": 0.0-1.0,
|
|
243
|
-
"priority": 0 | 1 | 2
|
|
244
|
-
}
|
|
245
|
-
],
|
|
246
|
-
"supersede": [
|
|
247
|
-
{
|
|
248
|
-
"oldNoteId": "sn_xxx",
|
|
249
|
-
"reason": "Why the old note is outdated/contradicted",
|
|
250
|
-
"newContent": "Updated version of the knowledge",
|
|
251
|
-
"type": "fact",
|
|
252
|
-
"keywords": ["keyword1", "keyword2"],
|
|
253
|
-
"confidence": 0.8,
|
|
254
|
-
"priority": 0 | 1 | 2
|
|
255
|
-
}
|
|
256
|
-
]
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
Rules:
|
|
260
|
-
- Only extract information that would be useful in future sessions
|
|
261
|
-
- Preferences are stronger signals than facts (confidence: 0.9)
|
|
262
|
-
- Do not extract trivial/transient information
|
|
263
|
-
- If the operator corrected the agent, extract the correction as a preference
|
|
264
|
-
- Focus on: operator name, preferences, communication style, technical choices, project context
|
|
265
|
-
- CRITICAL: Check existing notes for CONTRADICTIONS. If a new episode contradicts or updates an existing note, add a "supersede" entry. The new episodes represent MORE RECENT information.
|
|
266
|
-
- Common contradictions: decisions changed, projects completed/abandoned, preferences updated, tools/tech switched
|
|
267
|
-
- PRIORITY DETECTION: Infer priority from the operator's language and emotional investment:
|
|
268
|
-
- priority 2 (high importance): operator says "important", "crucial", "I really need", "don't forget", shows strong emotional investment, repeated emphasis
|
|
269
|
-
- priority 1 (notable): operator says "would be nice", "I'd like", "should", mentions something multiple times across conversations
|
|
270
|
-
- priority 0 (normal): default for routine facts, observations, patterns
|
|
271
|
-
- Return {"notes": [], "supersede": []} if nothing notable`;
|
|
272
|
-
let notesCreated = 0;
|
|
273
|
-
const details = [];
|
|
274
|
-
try {
|
|
275
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
276
|
-
if (!apiKey) {
|
|
277
|
-
throw new Error("OPENAI_API_KEY not set");
|
|
278
|
-
}
|
|
279
|
-
const messages = [
|
|
280
|
-
{ role: "system", content: systemPrompt },
|
|
281
|
-
{ role: "user", content: "Extract knowledge from the episodes above." },
|
|
282
|
-
];
|
|
283
|
-
const raw = await chatCompletion(messages, apiKey, {
|
|
284
|
-
model: process.env.CONSOLIDATION_MODEL ?? "gpt-4o-mini",
|
|
285
|
-
maxTokens: 4096,
|
|
286
|
-
responseFormat: { type: "json_object" },
|
|
287
|
-
timeoutMs: 60_000,
|
|
288
|
-
});
|
|
289
|
-
const parsed = repairAndParseJSON(raw);
|
|
290
|
-
const extractedNotes = parsed.notes ?? [];
|
|
291
|
-
const supersedeActions = parsed.supersede ?? [];
|
|
292
|
-
const episodeIds = episodes.map((ep) => ep.episodeId);
|
|
293
|
-
if (!dryRun) {
|
|
294
|
-
const knowledgeThreadId = resolveKnowledgeThreadId(threadId);
|
|
295
|
-
for (const note of extractedNotes) {
|
|
296
|
-
if (typeof note.content !== 'string' || note.content.length === 0 || note.content.length >= 2000) {
|
|
297
|
-
log.warn(`[consolidation] Skipping note with invalid content (type=${typeof note.content}, len=${typeof note.content === 'string' ? note.content.length : 'N/A'})`);
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
const validTypes = ["fact", "preference", "pattern", "entity", "relationship"];
|
|
301
|
-
const noteType = validTypes.includes(note.type)
|
|
302
|
-
? note.type
|
|
303
|
-
: "fact";
|
|
304
|
-
// Write-time dedup: skip notes that are too similar to existing ones
|
|
305
|
-
const dedup = await checkConsolidationDuplicate(db, note.content, apiKey, knowledgeThreadId);
|
|
306
|
-
if (dedup.isDuplicate) {
|
|
307
|
-
log.debug(`[consolidation] Dedup: skipping note similar to ${dedup.matchId} at ${dedup.similarity?.toFixed(3)}`);
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
const noteId = saveSemanticNote(db, {
|
|
311
|
-
type: noteType,
|
|
312
|
-
content: note.content,
|
|
313
|
-
keywords: Array.isArray(note.keywords) ? note.keywords : [],
|
|
314
|
-
confidence: Math.max(0, Math.min(1, note.confidence ?? 0.5)),
|
|
315
|
-
priority: Math.max(0, Math.min(2, note.priority ?? 0)),
|
|
316
|
-
threadId: knowledgeThreadId,
|
|
317
|
-
sourceEpisodes: episodeIds,
|
|
318
|
-
});
|
|
319
|
-
// Persist the embedding computed during dedup check
|
|
320
|
-
if (dedup.embedding) {
|
|
321
|
-
saveNoteEmbedding(db, noteId, dedup.embedding);
|
|
322
|
-
}
|
|
323
|
-
notesCreated++;
|
|
324
|
-
details.push(`[${noteType}] ${note.content}`);
|
|
325
|
-
}
|
|
326
|
-
// Execute supersede actions — resolve contradictions with existing notes
|
|
327
|
-
let supersededCount = 0;
|
|
328
|
-
for (const action of supersedeActions) {
|
|
329
|
-
if (!action.oldNoteId || !action.newContent)
|
|
330
|
-
continue;
|
|
331
|
-
// Verify old note exists and is still active
|
|
332
|
-
const oldNote = db.prepare(`SELECT note_id FROM semantic_notes WHERE note_id = ? AND valid_to IS NULL AND superseded_by IS NULL`).get(action.oldNoteId);
|
|
333
|
-
if (!oldNote) {
|
|
334
|
-
details.push(`[skip-supersede] ${action.oldNoteId} not found or already superseded`);
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
try {
|
|
338
|
-
const validTypes = ["fact", "preference", "pattern", "entity", "relationship"];
|
|
339
|
-
const noteType = validTypes.includes(action.type) ? action.type : "fact";
|
|
340
|
-
const newId = supersedeNote(db, action.oldNoteId, {
|
|
341
|
-
type: noteType,
|
|
342
|
-
content: action.newContent,
|
|
343
|
-
keywords: Array.isArray(action.keywords) ? action.keywords : [],
|
|
344
|
-
confidence: Math.max(0, Math.min(1, action.confidence ?? 0.8)),
|
|
345
|
-
priority: Math.max(0, Math.min(2, action.priority ?? 0)),
|
|
346
|
-
sourceEpisodes: episodeIds,
|
|
347
|
-
});
|
|
348
|
-
supersededCount++;
|
|
349
|
-
details.push(`[supersede] ${action.oldNoteId} → ${newId}: ${action.reason}`);
|
|
350
|
-
}
|
|
351
|
-
catch (err) {
|
|
352
|
-
details.push(`[supersede-error] ${action.oldNoteId}: ${errorMessage(err)}`);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
if (supersededCount > 0) {
|
|
356
|
-
log.info(`[memory] Contradiction resolution: superseded ${supersededCount} outdated note(s)`);
|
|
357
|
-
}
|
|
358
|
-
// Mark episodes as consolidated
|
|
359
|
-
markConsolidated(db, episodeIds);
|
|
360
|
-
// Log the consolidation
|
|
361
|
-
logConsolidation(db, {
|
|
362
|
-
episodesProcessed: episodes.length,
|
|
363
|
-
notesCreated: notesCreated + supersededCount,
|
|
364
|
-
durationMs: Date.now() - startMs,
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
for (const note of extractedNotes) {
|
|
369
|
-
details.push(`[dry-run] [${note.type}] ${note.content}`);
|
|
370
|
-
notesCreated++;
|
|
371
|
-
}
|
|
372
|
-
for (const action of supersedeActions) {
|
|
373
|
-
details.push(`[dry-run] [supersede] ${action.oldNoteId} → ${action.reason}`);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
catch (err) {
|
|
378
|
-
// Do NOT mark episodes as consolidated on failure — they should be
|
|
379
|
-
// retried on the next consolidation run. Previously this was a silent
|
|
380
|
-
// data-loss bug: a transient OpenAI outage would permanently lose the
|
|
381
|
-
// episodes' knowledge without extracting anything.
|
|
382
|
-
const msg = errorMessage(err);
|
|
383
|
-
log.error(`[memory] Intelligent consolidation failed (episodes NOT marked): ${msg}`);
|
|
384
|
-
details.push(`Consolidation failed (will retry): ${msg}`);
|
|
385
|
-
}
|
|
386
|
-
// Housekeeping: clean up old sent_messages entries (>7 days)
|
|
387
|
-
cleanupOldSentMessages(db);
|
|
388
|
-
return {
|
|
389
|
-
episodesProcessed: episodes.length,
|
|
390
|
-
notesCreated,
|
|
391
|
-
durationMs: Date.now() - startMs,
|
|
392
|
-
details,
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
finally {
|
|
396
|
-
if (!skipLock)
|
|
397
|
-
consolidationInProgress = false;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* Scan a batch of existing semantic notes for quality issues:
|
|
402
|
-
* - Outdated facts that no longer reflect reality
|
|
403
|
-
* - Duplicate notes conveying the same information
|
|
404
|
-
* - Low-quality notes that are vague or non-actionable
|
|
405
|
-
*
|
|
406
|
-
* Designed to run as a post-consolidation phase, gradually cleaning
|
|
407
|
-
* the memory DB over successive runs.
|
|
408
|
-
*/
|
|
409
|
-
export async function runMemoryPruning(db, options) {
|
|
410
|
-
const pruningEnabled = process.env.PRUNING_ENABLED;
|
|
411
|
-
if (pruningEnabled === "false" || pruningEnabled === "0") {
|
|
412
|
-
return {
|
|
413
|
-
notesScanned: 0, notesExpired: 0, notesMerged: 0, durationMs: 0,
|
|
414
|
-
details: ["Pruning disabled via PRUNING_ENABLED env var."],
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
418
|
-
if (!apiKey) {
|
|
419
|
-
return {
|
|
420
|
-
notesScanned: 0, notesExpired: 0, notesMerged: 0, durationMs: 0,
|
|
421
|
-
details: ["Pruning skipped — OPENAI_API_KEY not set."],
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
const startMs = Date.now();
|
|
425
|
-
const envSampleSize = parseInt(process.env.PRUNING_SAMPLE_SIZE ?? "", 10);
|
|
426
|
-
const maxNotes = options?.maxNotes ?? (Number.isFinite(envSampleSize) && envSampleSize > 0 ? envSampleSize : 30);
|
|
427
|
-
const dryRun = options?.dryRun ?? false;
|
|
428
|
-
const details = [];
|
|
429
|
-
// Sample candidate notes: lowest access count first, then oldest.
|
|
430
|
-
// Guardrails and pinned notes are excluded — they're intentionally permanent.
|
|
431
|
-
const candidates = db.prepare(`
|
|
432
|
-
SELECT * FROM semantic_notes
|
|
433
|
-
WHERE valid_to IS NULL
|
|
434
|
-
AND superseded_by IS NULL
|
|
435
|
-
AND is_guardrail = 0
|
|
436
|
-
AND pinned = 0
|
|
437
|
-
ORDER BY access_count ASC, created_at ASC
|
|
438
|
-
LIMIT ?
|
|
439
|
-
`).all(maxNotes);
|
|
440
|
-
if (candidates.length < 5) {
|
|
441
|
-
return {
|
|
442
|
-
notesScanned: 0, notesExpired: 0, notesMerged: 0,
|
|
443
|
-
durationMs: Date.now() - startMs,
|
|
444
|
-
details: ["Too few candidate notes for pruning (< 5)."],
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
const now = new Date();
|
|
448
|
-
const notesText = candidates.map((row) => {
|
|
449
|
-
const ageMs = now.getTime() - new Date(row.created_at).getTime();
|
|
450
|
-
const ageDays = Math.round(ageMs / (1000 * 60 * 60 * 24));
|
|
451
|
-
return `[${row.note_id}] (${row.type}, conf: ${row.confidence}, accesses: ${row.access_count}, age: ${ageDays}d) ${row.content}`;
|
|
452
|
-
}).join("\n");
|
|
453
|
-
const systemPrompt = `You are a memory quality manager. Review these semantic notes and identify problems.
|
|
454
|
-
|
|
455
|
-
Notes to review:
|
|
456
|
-
${notesText}
|
|
457
|
-
|
|
458
|
-
Output a JSON object:
|
|
459
|
-
{
|
|
460
|
-
"expire": [
|
|
461
|
-
{ "noteId": "sn_xxx", "reason": "Brief explanation" }
|
|
462
|
-
],
|
|
463
|
-
"merge": [
|
|
464
|
-
{ "keepId": "sn_xxx", "expireId": "sn_yyy", "mergedContent": "Best combined version", "reason": "Brief explanation" }
|
|
465
|
-
]
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
Decision criteria:
|
|
469
|
-
- EXPIRE if: the note is vague/non-actionable ("the system has issues"), trivially obvious, or clearly outdated (references old versions, completed tasks still marked pending)
|
|
470
|
-
- MERGE if: two or more notes convey the same information — keep the more specific/accurate one. Provide mergedContent combining the best of both when useful.
|
|
471
|
-
- KEEP (omit from output) if: the note contains unique, specific, actionable information
|
|
472
|
-
|
|
473
|
-
Be conservative — when in doubt, keep the note. Only expire notes you are confident are low-value.
|
|
474
|
-
Do NOT expire preferences or guardrails unless they clearly contradict each other.
|
|
475
|
-
Return {"expire": [], "merge": []} if nothing needs pruning.`;
|
|
476
|
-
try {
|
|
477
|
-
const messages = [
|
|
478
|
-
{ role: "system", content: systemPrompt },
|
|
479
|
-
{ role: "user", content: "Review the notes above and identify any that should be expired or merged." },
|
|
480
|
-
];
|
|
481
|
-
const raw = await chatCompletion(messages, apiKey, {
|
|
482
|
-
model: process.env.CONSOLIDATION_MODEL ?? "gpt-4o-mini",
|
|
483
|
-
maxTokens: 4096,
|
|
484
|
-
responseFormat: { type: "json_object" },
|
|
485
|
-
timeoutMs: 60_000,
|
|
486
|
-
});
|
|
487
|
-
const parsed = repairAndParseJSON(raw);
|
|
488
|
-
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
489
|
-
throw new Error(`LLM returned non-object response: ${typeof parsed}`);
|
|
490
|
-
}
|
|
491
|
-
const result = parsed;
|
|
492
|
-
const expireActions = (Array.isArray(result.expire) ? result.expire : []);
|
|
493
|
-
const mergeActions = (Array.isArray(result.merge) ? result.merge : []);
|
|
494
|
-
// Only allow actions targeting notes in the candidate set — prevents
|
|
495
|
-
// LLM hallucinations from expiring pinned/guardrail notes outside the batch.
|
|
496
|
-
const candidateIds = new Set(candidates.map((c) => c.note_id));
|
|
497
|
-
let expired = 0;
|
|
498
|
-
let merged = 0;
|
|
499
|
-
const nowStr = nowISO();
|
|
500
|
-
if (!dryRun) {
|
|
501
|
-
// Wrap all mutations in a transaction so partial failures roll back.
|
|
502
|
-
db.transaction(() => {
|
|
503
|
-
for (const action of expireActions) {
|
|
504
|
-
if (!action.noteId)
|
|
505
|
-
continue;
|
|
506
|
-
if (!candidateIds.has(action.noteId)) {
|
|
507
|
-
details.push(`[skip-expire] ${action.noteId} not in candidate set`);
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
const exists = db.prepare(`SELECT note_id FROM semantic_notes WHERE note_id = ? AND valid_to IS NULL`).get(action.noteId);
|
|
511
|
-
if (!exists) {
|
|
512
|
-
details.push(`[skip-expire] ${action.noteId} not found or already expired`);
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
db.prepare(`UPDATE semantic_notes SET valid_to = ?, updated_at = ? WHERE note_id = ?`).run(nowStr, nowStr, action.noteId);
|
|
516
|
-
expired++;
|
|
517
|
-
details.push(`[pruned] ${action.noteId}: ${action.reason}`);
|
|
518
|
-
}
|
|
519
|
-
for (const action of mergeActions) {
|
|
520
|
-
if (!action.keepId || !action.expireId)
|
|
521
|
-
continue;
|
|
522
|
-
if (action.keepId === action.expireId) {
|
|
523
|
-
details.push(`[skip-merge] ${action.keepId}: keepId === expireId`);
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
if (!candidateIds.has(action.keepId) || !candidateIds.has(action.expireId)) {
|
|
527
|
-
details.push(`[skip-merge] ${action.keepId}/${action.expireId} not in candidate set`);
|
|
528
|
-
continue;
|
|
529
|
-
}
|
|
530
|
-
const keepNote = db.prepare(`SELECT note_id, content FROM semantic_notes WHERE note_id = ? AND valid_to IS NULL`).get(action.keepId);
|
|
531
|
-
const expireNote = db.prepare(`SELECT note_id FROM semantic_notes WHERE note_id = ? AND valid_to IS NULL`).get(action.expireId);
|
|
532
|
-
if (!keepNote || !expireNote) {
|
|
533
|
-
details.push(`[skip-merge] ${action.keepId}/${action.expireId} not found`);
|
|
534
|
-
continue;
|
|
535
|
-
}
|
|
536
|
-
// Update kept note with merged content when provided
|
|
537
|
-
if (action.mergedContent && action.mergedContent !== keepNote.content) {
|
|
538
|
-
db.prepare(`UPDATE semantic_notes SET content = ?, updated_at = ? WHERE note_id = ?`).run(action.mergedContent, nowStr, keepNote.note_id);
|
|
539
|
-
}
|
|
540
|
-
// Expire the duplicate, linking to the kept note
|
|
541
|
-
db.prepare(`UPDATE semantic_notes SET valid_to = ?, superseded_by = ?, updated_at = ? WHERE note_id = ?`).run(nowStr, keepNote.note_id, nowStr, action.expireId);
|
|
542
|
-
merged++;
|
|
543
|
-
details.push(`[merged] ${action.expireId} → ${action.keepId}: ${action.reason}`);
|
|
544
|
-
}
|
|
545
|
-
})();
|
|
546
|
-
}
|
|
547
|
-
else {
|
|
548
|
-
for (const action of expireActions) {
|
|
549
|
-
details.push(`[dry-run] [prune] ${action.noteId}: ${action.reason}`);
|
|
550
|
-
expired++;
|
|
551
|
-
}
|
|
552
|
-
for (const action of mergeActions) {
|
|
553
|
-
details.push(`[dry-run] [merge] ${action.expireId} → ${action.keepId}: ${action.reason}`);
|
|
554
|
-
merged++;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
if (expired + merged > 0) {
|
|
558
|
-
log.info(`[memory] Pruning: expired ${expired}, merged ${merged} note(s)`);
|
|
559
|
-
}
|
|
560
|
-
return {
|
|
561
|
-
notesScanned: candidates.length,
|
|
562
|
-
notesExpired: expired,
|
|
563
|
-
notesMerged: merged,
|
|
564
|
-
durationMs: Date.now() - startMs,
|
|
565
|
-
details,
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
catch (err) {
|
|
569
|
-
const msg = errorMessage(err);
|
|
570
|
-
log.error(`[memory] Pruning failed: ${msg}`);
|
|
571
|
-
return {
|
|
572
|
-
notesScanned: candidates.length,
|
|
573
|
-
notesExpired: 0,
|
|
574
|
-
notesMerged: 0,
|
|
575
|
-
durationMs: Date.now() - startMs,
|
|
576
|
-
details: [`Pruning failed: ${msg}`],
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
69
|
//# sourceMappingURL=consolidation.js.map
|