@edihasaj/recall 0.5.2
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 +21 -0
- package/README.md +409 -0
- package/dist/chunk-4CV4JOE5.js +27 -0
- package/dist/chunk-4CV4JOE5.js.map +1 -0
- package/dist/chunk-A5UIRZU6.js +469 -0
- package/dist/chunk-A5UIRZU6.js.map +1 -0
- package/dist/chunk-AYHFPCGY.js +964 -0
- package/dist/chunk-AYHFPCGY.js.map +1 -0
- package/dist/chunk-DNFKAHS6.js +204 -0
- package/dist/chunk-DNFKAHS6.js.map +1 -0
- package/dist/chunk-GC5XMBG4.js +551 -0
- package/dist/chunk-GC5XMBG4.js.map +1 -0
- package/dist/chunk-IILLSHLM.js +3021 -0
- package/dist/chunk-IILLSHLM.js.map +1 -0
- package/dist/chunk-LVQW6WHK.js +146 -0
- package/dist/chunk-LVQW6WHK.js.map +1 -0
- package/dist/chunk-LZ6PMQRX.js +955 -0
- package/dist/chunk-LZ6PMQRX.js.map +1 -0
- package/dist/chunk-PC43MBX5.js +2960 -0
- package/dist/chunk-PC43MBX5.js.map +1 -0
- package/dist/chunk-VEPXEHRZ.js +1763 -0
- package/dist/chunk-VEPXEHRZ.js.map +1 -0
- package/dist/cleanup-TVOX2S2S.js +28 -0
- package/dist/cleanup-TVOX2S2S.js.map +1 -0
- package/dist/cli.js +3425 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon.js +1298 -0
- package/dist/daemon.js.map +1 -0
- package/dist/dispatcher-UGMU6THT.js +15 -0
- package/dist/dispatcher-UGMU6THT.js.map +1 -0
- package/dist/keychain-5QG52ANO.js +22 -0
- package/dist/keychain-5QG52ANO.js.map +1 -0
- package/dist/mcp.js +21 -0
- package/dist/mcp.js.map +1 -0
- package/dist/quality-Z7LPMMBC.js +17 -0
- package/dist/quality-Z7LPMMBC.js.map +1 -0
- package/dist/sync-server.js +225 -0
- package/dist/sync-server.js.map +1 -0
- package/dist/tasks-UOLSPXJQ.js +61 -0
- package/dist/tasks-UOLSPXJQ.js.map +1 -0
- package/dist/usage-CY3V72YN.js +101 -0
- package/dist/usage-CY3V72YN.js.map +1 -0
- package/drizzle/0000_initial_create.sql +240 -0
- package/drizzle/0001_rich_liz_osborn.sql +21 -0
- package/drizzle/0002_unknown_spot.sql +18 -0
- package/drizzle/0003_red_wendigo.sql +19 -0
- package/drizzle/0004_early_carlie_cooper.sql +1 -0
- package/drizzle/0005_simple_emma_frost.sql +96 -0
- package/drizzle/0006_keen_mongoose.sql +2 -0
- package/drizzle/0007_flawless_maximus.sql +15 -0
- package/drizzle/meta/0000_snapshot.json +1630 -0
- package/drizzle/meta/0001_snapshot.json +1773 -0
- package/drizzle/meta/0002_snapshot.json +1891 -0
- package/drizzle/meta/0003_snapshot.json +2014 -0
- package/drizzle/meta/0004_snapshot.json +2022 -0
- package/drizzle/meta/0005_snapshot.json +2064 -0
- package/drizzle/meta/0006_snapshot.json +2078 -0
- package/drizzle/meta/0007_snapshot.json +2183 -0
- package/drizzle/meta/_journal.json +62 -0
- package/package.json +64 -0
- package/scripts/recall-claude +7 -0
- package/scripts/recall-codex +7 -0
- package/scripts/recall-session +71 -0
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,1298 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computeQualityReport,
|
|
3
|
+
listQualitySnapshots,
|
|
4
|
+
recordQualitySnapshot
|
|
5
|
+
} from "./chunk-LVQW6WHK.js";
|
|
6
|
+
import {
|
|
7
|
+
ensureDailyBackup,
|
|
8
|
+
handlePromptHook,
|
|
9
|
+
handleSessionEndHook,
|
|
10
|
+
handleSessionStartHook,
|
|
11
|
+
handleToolHook
|
|
12
|
+
} from "./chunk-AYHFPCGY.js";
|
|
13
|
+
import {
|
|
14
|
+
createRecallMcpServer
|
|
15
|
+
} from "./chunk-LZ6PMQRX.js";
|
|
16
|
+
import {
|
|
17
|
+
bootstrapHistoryEmbeddings,
|
|
18
|
+
compileContext,
|
|
19
|
+
compileContextHybrid,
|
|
20
|
+
computeMetrics,
|
|
21
|
+
createActivityEvent,
|
|
22
|
+
createHistorySnippet,
|
|
23
|
+
createPolicy,
|
|
24
|
+
endEvalSession,
|
|
25
|
+
endSessionLifecycle,
|
|
26
|
+
ensureRepoBootstrapped,
|
|
27
|
+
evaluatePolicy,
|
|
28
|
+
evaluateScannedMemory,
|
|
29
|
+
findHistorySnippetByRepoKind,
|
|
30
|
+
findHistorySnippetBySession,
|
|
31
|
+
getSignalStats,
|
|
32
|
+
incrementEvalCounter,
|
|
33
|
+
inferRepoSlugFromPath,
|
|
34
|
+
initDb,
|
|
35
|
+
listActivityEvents,
|
|
36
|
+
listActivitySessions,
|
|
37
|
+
listHistorySnippets,
|
|
38
|
+
listPendingApprovals,
|
|
39
|
+
listPolicies,
|
|
40
|
+
pruneMemories,
|
|
41
|
+
recordSessionLifecycleEvent,
|
|
42
|
+
recordSignal,
|
|
43
|
+
recordTestSignals,
|
|
44
|
+
removeHistoryFtsRow,
|
|
45
|
+
removeHistoryVecRow,
|
|
46
|
+
requestApproval,
|
|
47
|
+
resolveApproval,
|
|
48
|
+
runTests,
|
|
49
|
+
scanAndStore,
|
|
50
|
+
startEvalSession,
|
|
51
|
+
startSessionLifecycle,
|
|
52
|
+
syncHistoryFtsIndex,
|
|
53
|
+
updateHistorySnippet,
|
|
54
|
+
verifyHistoryEmbeddings,
|
|
55
|
+
writeRepoContextArtifact
|
|
56
|
+
} from "./chunk-PC43MBX5.js";
|
|
57
|
+
import {
|
|
58
|
+
autoResolveContradictions,
|
|
59
|
+
computeAllHealthScores,
|
|
60
|
+
computeHealthScore,
|
|
61
|
+
detectContradictions,
|
|
62
|
+
detectCorrections,
|
|
63
|
+
getRepoQualityProfile,
|
|
64
|
+
listContradictions,
|
|
65
|
+
processCorrection,
|
|
66
|
+
processReviewFeedback,
|
|
67
|
+
resolveContradiction,
|
|
68
|
+
runDeterministicCleanup
|
|
69
|
+
} from "./chunk-VEPXEHRZ.js";
|
|
70
|
+
import {
|
|
71
|
+
dispatchPendingTasks
|
|
72
|
+
} from "./chunk-GC5XMBG4.js";
|
|
73
|
+
import {
|
|
74
|
+
DEFAULT_ENQUEUE_CONFIG,
|
|
75
|
+
bootstrapEmbeddings,
|
|
76
|
+
confirmMemory,
|
|
77
|
+
enqueueMaintenanceTasks,
|
|
78
|
+
ensureEmbeddingProviderReady,
|
|
79
|
+
getAuditTrail,
|
|
80
|
+
getEmbeddingModelInfo,
|
|
81
|
+
getMemory,
|
|
82
|
+
getRecentAudit,
|
|
83
|
+
loadEmbeddingConfigFromEnv,
|
|
84
|
+
promoteMemory,
|
|
85
|
+
queryMemories,
|
|
86
|
+
queueMemoryEmbeddingSync,
|
|
87
|
+
rebuildEmbeddingIndex,
|
|
88
|
+
recordAuditWithSnapshot,
|
|
89
|
+
recordFeedback,
|
|
90
|
+
rejectMemory,
|
|
91
|
+
rollbackMemory,
|
|
92
|
+
statusFromConfidence,
|
|
93
|
+
verifyEmbeddings
|
|
94
|
+
} from "./chunk-IILLSHLM.js";
|
|
95
|
+
import {
|
|
96
|
+
activityEvents,
|
|
97
|
+
feedbackEvents,
|
|
98
|
+
historySnippets,
|
|
99
|
+
implicitSignals,
|
|
100
|
+
memories
|
|
101
|
+
} from "./chunk-A5UIRZU6.js";
|
|
102
|
+
import {
|
|
103
|
+
hasProviderConfigured,
|
|
104
|
+
init_keychain
|
|
105
|
+
} from "./chunk-DNFKAHS6.js";
|
|
106
|
+
import "./chunk-4CV4JOE5.js";
|
|
107
|
+
|
|
108
|
+
// src/daemon.ts
|
|
109
|
+
import { createServer } from "http";
|
|
110
|
+
|
|
111
|
+
// src/maintenance/lifecycle.ts
|
|
112
|
+
import { eq, lt } from "drizzle-orm";
|
|
113
|
+
var DAY_MS = 864e5;
|
|
114
|
+
function loadMaintenanceConfigFromEnv() {
|
|
115
|
+
return {
|
|
116
|
+
enabled: process.env.RECALL_MAINTENANCE_ENABLED !== "false",
|
|
117
|
+
interval_seconds: parseInt(process.env.RECALL_MAINTENANCE_INTERVAL_SECONDS ?? "300", 10),
|
|
118
|
+
stale_days: parseInt(process.env.RECALL_MAINTENANCE_STALE_DAYS ?? "90", 10),
|
|
119
|
+
min_health_score: parseFloat(process.env.RECALL_MAINTENANCE_MIN_HEALTH_SCORE ?? "0.2"),
|
|
120
|
+
activity_retention_days: parseInt(process.env.RECALL_ACTIVITY_RETENTION_DAYS ?? "90", 10),
|
|
121
|
+
feedback_retention_days: parseInt(process.env.RECALL_FEEDBACK_RETENTION_DAYS ?? "180", 10),
|
|
122
|
+
signal_retention_days: parseInt(process.env.RECALL_SIGNAL_RETENTION_DAYS ?? "180", 10),
|
|
123
|
+
history_session_retention_days: parseInt(process.env.RECALL_HISTORY_SESSION_RETENTION_DAYS ?? "30", 10),
|
|
124
|
+
sqlite_analyze_enabled: process.env.RECALL_SQLITE_ANALYZE_ENABLED !== "false",
|
|
125
|
+
sqlite_optimize_enabled: process.env.RECALL_SQLITE_OPTIMIZE_ENABLED !== "false",
|
|
126
|
+
sqlite_wal_checkpoint_enabled: process.env.RECALL_SQLITE_CHECKPOINT_ENABLED !== "false",
|
|
127
|
+
sqlite_vacuum_enabled: process.env.RECALL_SQLITE_VACUUM_ENABLED === "true",
|
|
128
|
+
sqlite_vacuum_min_free_pages: parseInt(process.env.RECALL_SQLITE_VACUUM_MIN_FREE_PAGES ?? "100", 10),
|
|
129
|
+
sqlite_vacuum_min_free_ratio: parseFloat(process.env.RECALL_SQLITE_VACUUM_MIN_FREE_RATIO ?? "0.1"),
|
|
130
|
+
llm_tasks_enabled: process.env.RECALL_MAINTENANCE_LLM_DISABLED !== "true",
|
|
131
|
+
llm_task_config: {
|
|
132
|
+
max_pending: parseInt(process.env.RECALL_MAINTENANCE_MAX_PENDING ?? String(DEFAULT_ENQUEUE_CONFIG.max_pending), 10),
|
|
133
|
+
max_per_kind: parseInt(process.env.RECALL_MAINTENANCE_MAX_PER_KIND ?? String(DEFAULT_ENQUEUE_CONFIG.max_per_kind), 10),
|
|
134
|
+
refine_min_repetition: parseInt(process.env.RECALL_MAINTENANCE_REFINE_MIN_REPETITION ?? String(DEFAULT_ENQUEUE_CONFIG.refine_min_repetition), 10),
|
|
135
|
+
summary_max_age_days: parseInt(process.env.RECALL_MAINTENANCE_SUMMARY_MAX_AGE_DAYS ?? String(DEFAULT_ENQUEUE_CONFIG.summary_max_age_days), 10),
|
|
136
|
+
merge_similarity_threshold: parseFloat(process.env.RECALL_MAINTENANCE_MERGE_SIMILARITY_THRESHOLD ?? String(DEFAULT_ENQUEUE_CONFIG.merge_similarity_threshold)),
|
|
137
|
+
session_min_activity_events: parseInt(process.env.RECALL_MAINTENANCE_SESSION_MIN_EVENTS ?? String(DEFAULT_ENQUEUE_CONFIG.session_min_activity_events), 10),
|
|
138
|
+
repo_synthesis_min_memories: parseInt(process.env.RECALL_MAINTENANCE_REPO_SYNTHESIS_MIN_MEMORIES ?? String(DEFAULT_ENQUEUE_CONFIG.repo_synthesis_min_memories), 10),
|
|
139
|
+
repo_synthesis_refresh_days: parseInt(process.env.RECALL_MAINTENANCE_REPO_SYNTHESIS_REFRESH_DAYS ?? String(DEFAULT_ENQUEUE_CONFIG.repo_synthesis_refresh_days), 10)
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async function runMaintenanceCycle(db2, config = loadMaintenanceConfigFromEnv()) {
|
|
144
|
+
const prune = pruneMemories(db2, {
|
|
145
|
+
stale_days: config.stale_days,
|
|
146
|
+
min_health_score: config.min_health_score
|
|
147
|
+
});
|
|
148
|
+
const scannedMemoryCleanup = reconcileScannedMemories(db2);
|
|
149
|
+
const candidates_promoted = promoteRepetitionCandidates(db2);
|
|
150
|
+
const activity_pruned = pruneOldActivityEvents(db2, config.activity_retention_days);
|
|
151
|
+
const feedback_pruned = pruneOldFeedbackEvents(db2, config.feedback_retention_days);
|
|
152
|
+
const signals_pruned = pruneOldImplicitSignals(db2, config.signal_retention_days);
|
|
153
|
+
const sqliteMaintenance = runSqliteMaintenance(db2, config);
|
|
154
|
+
let embeddings_refreshed = 0;
|
|
155
|
+
let vector_rows_rebuilt = 0;
|
|
156
|
+
let lexical_rows_rebuilt = 0;
|
|
157
|
+
let embedding_stale = 0;
|
|
158
|
+
let vector_drift = 0;
|
|
159
|
+
let lexical_drift = 0;
|
|
160
|
+
const history_snippets_created = rollupSessionHistory(db2);
|
|
161
|
+
const history_summaries_created = summarizeHistorySnippets(db2);
|
|
162
|
+
const history_session_deleted = cleanupSessionHistory(db2, config.history_session_retention_days);
|
|
163
|
+
let history_embeddings_refreshed = 0;
|
|
164
|
+
let history_vector_drift = 0;
|
|
165
|
+
let history_lexical_drift = 0;
|
|
166
|
+
const embeddingConfig = loadEmbeddingConfigFromEnv();
|
|
167
|
+
if (embeddingConfig) {
|
|
168
|
+
const verify = verifyEmbeddings(db2, embeddingConfig);
|
|
169
|
+
embedding_stale = verify.stale;
|
|
170
|
+
vector_drift = verify.index_drift;
|
|
171
|
+
lexical_drift = verify.lexical_drift;
|
|
172
|
+
if (embedding_stale > 0) {
|
|
173
|
+
embeddings_refreshed = await bootstrapEmbeddings(db2, embeddingConfig);
|
|
174
|
+
}
|
|
175
|
+
if (vector_drift !== 0 || lexical_drift !== 0) {
|
|
176
|
+
const rebuilt = rebuildEmbeddingIndex(db2, embeddingConfig);
|
|
177
|
+
vector_rows_rebuilt = rebuilt.vector_rows;
|
|
178
|
+
lexical_rows_rebuilt = rebuilt.lexical_rows;
|
|
179
|
+
}
|
|
180
|
+
const historyVerify = verifyHistoryEmbeddings(db2, embeddingConfig);
|
|
181
|
+
history_vector_drift = historyVerify.index_drift;
|
|
182
|
+
history_lexical_drift = historyVerify.lexical_drift;
|
|
183
|
+
if (historyVerify.stale > 0 || history_snippets_created > 0 || history_summaries_created > 0 || history_session_deleted > 0) {
|
|
184
|
+
history_embeddings_refreshed = await bootstrapHistoryEmbeddings(db2, embeddingConfig);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const tasks = config.llm_tasks_enabled ? await enqueueMaintenanceTasks(db2, config.llm_task_config) : { tasks_enqueued: 0, per_kind: {}, expired_leases_swept: 0, dropped_over_cap: 0, expired_pending_tasks: 0 };
|
|
188
|
+
return {
|
|
189
|
+
prune_total: prune.total,
|
|
190
|
+
stale_rejected: prune.stale_rejected.length,
|
|
191
|
+
rejected_pruned: prune.rejected_pruned.length,
|
|
192
|
+
transient_pruned: prune.transient_pruned.length,
|
|
193
|
+
unhealthy_demoted: prune.unhealthy_demoted.length,
|
|
194
|
+
scanned_memories_normalized: scannedMemoryCleanup.normalized,
|
|
195
|
+
scanned_memories_demoted: scannedMemoryCleanup.demoted,
|
|
196
|
+
scanned_memories_rejected: scannedMemoryCleanup.rejected,
|
|
197
|
+
activity_pruned,
|
|
198
|
+
feedback_pruned,
|
|
199
|
+
signals_pruned,
|
|
200
|
+
embeddings_refreshed,
|
|
201
|
+
vector_rows_rebuilt,
|
|
202
|
+
lexical_rows_rebuilt,
|
|
203
|
+
embedding_stale,
|
|
204
|
+
vector_drift,
|
|
205
|
+
lexical_drift,
|
|
206
|
+
history_snippets_created,
|
|
207
|
+
history_summaries_created,
|
|
208
|
+
history_session_deleted,
|
|
209
|
+
history_embeddings_refreshed,
|
|
210
|
+
history_vector_drift,
|
|
211
|
+
history_lexical_drift,
|
|
212
|
+
candidates_promoted,
|
|
213
|
+
sqlite_analyze_ran: sqliteMaintenance.analyze_ran,
|
|
214
|
+
sqlite_optimize_ran: sqliteMaintenance.optimize_ran,
|
|
215
|
+
sqlite_checkpoint_ran: sqliteMaintenance.checkpoint_ran,
|
|
216
|
+
sqlite_vacuum_ran: sqliteMaintenance.vacuum_ran,
|
|
217
|
+
sqlite_page_count: sqliteMaintenance.page_count,
|
|
218
|
+
sqlite_freelist_count: sqliteMaintenance.freelist_count,
|
|
219
|
+
maintenance_tasks_enqueued: tasks.tasks_enqueued,
|
|
220
|
+
maintenance_leases_swept: tasks.expired_leases_swept,
|
|
221
|
+
maintenance_tasks_dropped: tasks.dropped_over_cap,
|
|
222
|
+
maintenance_tasks_expired: tasks.expired_pending_tasks
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function promoteRepetitionCandidates(db2) {
|
|
226
|
+
const candidates = queryMemories(db2, { status: "candidate" });
|
|
227
|
+
let promoted = 0;
|
|
228
|
+
for (const candidate of candidates) {
|
|
229
|
+
if (!candidate.repo) continue;
|
|
230
|
+
const profile = getRepoQualityProfile(db2, candidate.repo);
|
|
231
|
+
if (candidate.repetition_count < profile.repeat_sessions_required) continue;
|
|
232
|
+
const before = getMemory(db2, candidate.id);
|
|
233
|
+
if (!before || before.status !== "candidate") continue;
|
|
234
|
+
const ok = promoteMemory(db2, candidate.id, "repeat_correction");
|
|
235
|
+
if (!ok) continue;
|
|
236
|
+
const after = getMemory(db2, candidate.id);
|
|
237
|
+
recordAuditWithSnapshot(
|
|
238
|
+
db2,
|
|
239
|
+
candidate.id,
|
|
240
|
+
"promoted",
|
|
241
|
+
"system",
|
|
242
|
+
`repetition:${candidate.repetition_count}`,
|
|
243
|
+
before,
|
|
244
|
+
after ?? null
|
|
245
|
+
);
|
|
246
|
+
promoted += 1;
|
|
247
|
+
}
|
|
248
|
+
return promoted;
|
|
249
|
+
}
|
|
250
|
+
function reconcileScannedMemories(db2) {
|
|
251
|
+
const scanned = queryMemories(db2, {}).filter(
|
|
252
|
+
(memory) => memory.status !== "rejected" && (memory.source === "repo_scan" || memory.source === "config_parse")
|
|
253
|
+
);
|
|
254
|
+
let normalized = 0;
|
|
255
|
+
let demoted = 0;
|
|
256
|
+
let rejected = 0;
|
|
257
|
+
for (const memory of scanned) {
|
|
258
|
+
const evaluated = evaluateScannedMemory({
|
|
259
|
+
text: memory.text,
|
|
260
|
+
type: memory.type,
|
|
261
|
+
source: memory.source,
|
|
262
|
+
confidence: memory.confidence
|
|
263
|
+
});
|
|
264
|
+
if (evaluated.action === "reject") {
|
|
265
|
+
if (memory.status !== "rejected") {
|
|
266
|
+
rejectMemory(db2, memory.id);
|
|
267
|
+
rejected += 1;
|
|
268
|
+
}
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const nextStatus = statusFromConfidence(evaluated.confidence);
|
|
272
|
+
const updates = {};
|
|
273
|
+
if (memory.text !== evaluated.text) {
|
|
274
|
+
updates.text = evaluated.text;
|
|
275
|
+
normalized += 1;
|
|
276
|
+
}
|
|
277
|
+
if (memory.confidence !== evaluated.confidence) {
|
|
278
|
+
updates.confidence = evaluated.confidence;
|
|
279
|
+
}
|
|
280
|
+
if (memory.status !== nextStatus) {
|
|
281
|
+
updates.status = nextStatus;
|
|
282
|
+
if (memory.status === "active" && nextStatus === "candidate") {
|
|
283
|
+
demoted += 1;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (Object.keys(updates).length === 0) continue;
|
|
287
|
+
updates.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
288
|
+
db2.update(memories).set(updates).where(eq(memories.id, memory.id)).run();
|
|
289
|
+
queueMemoryEmbeddingSync(db2, memory.id);
|
|
290
|
+
}
|
|
291
|
+
return { normalized, demoted, rejected };
|
|
292
|
+
}
|
|
293
|
+
function runSqliteMaintenance(db2, config) {
|
|
294
|
+
const sqlite = db2.$client;
|
|
295
|
+
const pageCount = Number(sqlite.pragma("page_count", { simple: true }) ?? 0);
|
|
296
|
+
const freelistCount = Number(sqlite.pragma("freelist_count", { simple: true }) ?? 0);
|
|
297
|
+
const freeRatio = pageCount > 0 ? freelistCount / pageCount : 0;
|
|
298
|
+
let analyzeRan = false;
|
|
299
|
+
let optimizeRan = false;
|
|
300
|
+
let checkpointRan = false;
|
|
301
|
+
let vacuumRan = false;
|
|
302
|
+
if (config.sqlite_analyze_enabled) {
|
|
303
|
+
sqlite.exec("ANALYZE;");
|
|
304
|
+
analyzeRan = true;
|
|
305
|
+
}
|
|
306
|
+
if (config.sqlite_wal_checkpoint_enabled) {
|
|
307
|
+
sqlite.pragma("wal_checkpoint(PASSIVE)");
|
|
308
|
+
checkpointRan = true;
|
|
309
|
+
}
|
|
310
|
+
if (config.sqlite_optimize_enabled) {
|
|
311
|
+
sqlite.pragma("optimize");
|
|
312
|
+
optimizeRan = true;
|
|
313
|
+
}
|
|
314
|
+
if (config.sqlite_vacuum_enabled && freelistCount >= config.sqlite_vacuum_min_free_pages && freeRatio >= config.sqlite_vacuum_min_free_ratio) {
|
|
315
|
+
sqlite.exec("VACUUM;");
|
|
316
|
+
vacuumRan = true;
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
analyze_ran: analyzeRan,
|
|
320
|
+
optimize_ran: optimizeRan,
|
|
321
|
+
checkpoint_ran: checkpointRan,
|
|
322
|
+
vacuum_ran: vacuumRan,
|
|
323
|
+
page_count: pageCount,
|
|
324
|
+
freelist_count: freelistCount
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function pruneOldActivityEvents(db2, retentionDays) {
|
|
328
|
+
const cutoff = new Date(Date.now() - retentionDays * DAY_MS).toISOString();
|
|
329
|
+
return db2.delete(activityEvents).where(lt(activityEvents.created_at, cutoff)).run().changes;
|
|
330
|
+
}
|
|
331
|
+
function pruneOldFeedbackEvents(db2, retentionDays) {
|
|
332
|
+
const cutoff = new Date(Date.now() - retentionDays * DAY_MS).toISOString();
|
|
333
|
+
return db2.delete(feedbackEvents).where(lt(feedbackEvents.timestamp, cutoff)).run().changes;
|
|
334
|
+
}
|
|
335
|
+
function pruneOldImplicitSignals(db2, retentionDays) {
|
|
336
|
+
const cutoff = new Date(Date.now() - retentionDays * DAY_MS).toISOString();
|
|
337
|
+
return db2.delete(implicitSignals).where(lt(implicitSignals.timestamp, cutoff)).run().changes;
|
|
338
|
+
}
|
|
339
|
+
function rollupSessionHistory(db2) {
|
|
340
|
+
const sessionEnds = listActivityEvents(db2, { event_type: "session_end", limit: 500 });
|
|
341
|
+
let createdOrUpdated = 0;
|
|
342
|
+
for (const end of sessionEnds) {
|
|
343
|
+
if (!end.session_id) continue;
|
|
344
|
+
const existing = findHistorySnippetBySession(db2, end.session_id, "session_summary");
|
|
345
|
+
const events = listActivityEvents(db2, { session_id: end.session_id }).sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
346
|
+
if (events.length === 0) continue;
|
|
347
|
+
const repo = end.repo ?? events.find((event) => event.repo)?.repo ?? null;
|
|
348
|
+
const summary = summarizeSessionEvents(events);
|
|
349
|
+
const sourceActivityIds = events.map((event) => event.id);
|
|
350
|
+
if (existing) {
|
|
351
|
+
if (existing.text !== summary) {
|
|
352
|
+
updateHistorySnippet(db2, existing.id, {
|
|
353
|
+
text: summary,
|
|
354
|
+
source_activity_ids: sourceActivityIds
|
|
355
|
+
});
|
|
356
|
+
syncHistoryFtsIndex(db2, existing.id);
|
|
357
|
+
createdOrUpdated++;
|
|
358
|
+
}
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const id = createHistorySnippet(db2, {
|
|
362
|
+
repo,
|
|
363
|
+
session_id: end.session_id,
|
|
364
|
+
kind: "session_summary",
|
|
365
|
+
text: summary,
|
|
366
|
+
source_activity_ids: sourceActivityIds
|
|
367
|
+
});
|
|
368
|
+
syncHistoryFtsIndex(db2, id);
|
|
369
|
+
createdOrUpdated++;
|
|
370
|
+
}
|
|
371
|
+
return createdOrUpdated;
|
|
372
|
+
}
|
|
373
|
+
function summarizeHistorySnippets(db2) {
|
|
374
|
+
const sessionSnippets = listHistorySnippets(db2, {
|
|
375
|
+
kind: "session_summary",
|
|
376
|
+
limit: 1e3
|
|
377
|
+
});
|
|
378
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
379
|
+
for (const snippet of sessionSnippets) {
|
|
380
|
+
if (!snippet.repo) continue;
|
|
381
|
+
const bucket = byRepo.get(snippet.repo) ?? [];
|
|
382
|
+
bucket.push(snippet);
|
|
383
|
+
byRepo.set(snippet.repo, bucket);
|
|
384
|
+
}
|
|
385
|
+
let createdOrUpdated = 0;
|
|
386
|
+
for (const [repo, snippets] of byRepo.entries()) {
|
|
387
|
+
const aggregated = aggregateRepoHistory(repo, snippets);
|
|
388
|
+
for (const item of aggregated) {
|
|
389
|
+
if (!item.text) continue;
|
|
390
|
+
const existing = findHistorySnippetByRepoKind(db2, repo, item.kind);
|
|
391
|
+
if (existing) {
|
|
392
|
+
if (existing.text !== item.text) {
|
|
393
|
+
updateHistorySnippet(db2, existing.id, {
|
|
394
|
+
text: item.text,
|
|
395
|
+
source_activity_ids: item.source_activity_ids
|
|
396
|
+
});
|
|
397
|
+
syncHistoryFtsIndex(db2, existing.id);
|
|
398
|
+
createdOrUpdated++;
|
|
399
|
+
}
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const id = createHistorySnippet(db2, {
|
|
403
|
+
repo,
|
|
404
|
+
kind: item.kind,
|
|
405
|
+
text: item.text,
|
|
406
|
+
source_activity_ids: item.source_activity_ids
|
|
407
|
+
});
|
|
408
|
+
syncHistoryFtsIndex(db2, id);
|
|
409
|
+
createdOrUpdated++;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return createdOrUpdated;
|
|
413
|
+
}
|
|
414
|
+
function cleanupSessionHistory(db2, retentionDays) {
|
|
415
|
+
const cutoff = new Date(Date.now() - retentionDays * DAY_MS).toISOString();
|
|
416
|
+
const sessionSnippets = listHistorySnippets(db2, {
|
|
417
|
+
kind: "session_summary",
|
|
418
|
+
limit: 1e3
|
|
419
|
+
});
|
|
420
|
+
let deleted = 0;
|
|
421
|
+
for (const snippet of sessionSnippets) {
|
|
422
|
+
if (!snippet.repo) continue;
|
|
423
|
+
if (snippet.created_at >= cutoff) continue;
|
|
424
|
+
const hasRepoSummary = findHistorySnippetByRepoKind(db2, snippet.repo, "correction_summary") || findHistorySnippetByRepoKind(db2, snippet.repo, "decision_summary") || findHistorySnippetByRepoKind(db2, snippet.repo, "review_summary") || findHistorySnippetByRepoKind(db2, snippet.repo, "compile_summary");
|
|
425
|
+
if (!hasRepoSummary) continue;
|
|
426
|
+
removeHistoryFtsRow(db2, snippet.id);
|
|
427
|
+
removeHistoryVecRow(db2, snippet.id);
|
|
428
|
+
db2.delete(historySnippets).where(eq(historySnippets.id, snippet.id)).run();
|
|
429
|
+
deleted++;
|
|
430
|
+
}
|
|
431
|
+
return deleted;
|
|
432
|
+
}
|
|
433
|
+
function summarizeSessionEvents(events) {
|
|
434
|
+
const repo = events.find((event) => event.repo)?.repo ?? "unknown";
|
|
435
|
+
const eventTypes = [...new Set(events.map((event) => event.event_type))];
|
|
436
|
+
const corrections = events.filter((event) => event.event_type === "correction").map((event) => String(event.request.text ?? "")).filter(Boolean);
|
|
437
|
+
const reviews = events.filter((event) => event.event_type === "review").map((event) => String(event.request.feedback ?? "")).filter(Boolean);
|
|
438
|
+
const decisions = extractPromptDecisions(events);
|
|
439
|
+
const compileEvents = events.filter((event) => event.event_type === "compile");
|
|
440
|
+
const lines = [
|
|
441
|
+
`Repo: ${repo}`,
|
|
442
|
+
`Event types: ${eventTypes.join(", ")}`
|
|
443
|
+
];
|
|
444
|
+
if (compileEvents.length > 0) {
|
|
445
|
+
const latestCompile = compileEvents.at(-1);
|
|
446
|
+
const included = Array.isArray(latestCompile?.result.included) ? latestCompile.result.included.length : 0;
|
|
447
|
+
lines.push(`Latest compile included ${included} memories.`);
|
|
448
|
+
}
|
|
449
|
+
if (corrections.length > 0) {
|
|
450
|
+
lines.push(`Corrections: ${corrections.slice(0, 3).join(" | ")}`);
|
|
451
|
+
}
|
|
452
|
+
if (reviews.length > 0) {
|
|
453
|
+
lines.push(`Reviews: ${reviews.slice(0, 3).join(" | ")}`);
|
|
454
|
+
}
|
|
455
|
+
if (decisions.length > 0) {
|
|
456
|
+
lines.push(`Decisions: ${decisions.slice(0, 5).join(" | ")}`);
|
|
457
|
+
}
|
|
458
|
+
return lines.join("\n");
|
|
459
|
+
}
|
|
460
|
+
function aggregateRepoHistory(repo, snippets) {
|
|
461
|
+
const corrections = /* @__PURE__ */ new Map();
|
|
462
|
+
const decisions = /* @__PURE__ */ new Map();
|
|
463
|
+
const reviews = /* @__PURE__ */ new Map();
|
|
464
|
+
let compileObservations = 0;
|
|
465
|
+
let compileIncludedTotal = 0;
|
|
466
|
+
const sourceActivityIds = /* @__PURE__ */ new Set();
|
|
467
|
+
for (const snippet of snippets) {
|
|
468
|
+
for (const id of snippet.source_activity_ids) {
|
|
469
|
+
sourceActivityIds.add(id);
|
|
470
|
+
}
|
|
471
|
+
const lines = snippet.text.split("\n");
|
|
472
|
+
const correctionsLine = lines.find((line) => line.startsWith("Corrections: "));
|
|
473
|
+
if (correctionsLine) {
|
|
474
|
+
for (const item of correctionsLine.replace("Corrections: ", "").split(" | ").filter(Boolean)) {
|
|
475
|
+
corrections.set(item, (corrections.get(item) ?? 0) + 1);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const reviewsLine = lines.find((line) => line.startsWith("Reviews: "));
|
|
479
|
+
if (reviewsLine) {
|
|
480
|
+
for (const item of reviewsLine.replace("Reviews: ", "").split(" | ").filter(Boolean)) {
|
|
481
|
+
reviews.set(item, (reviews.get(item) ?? 0) + 1);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const decisionsLine = lines.find((line) => line.startsWith("Decisions: "));
|
|
485
|
+
if (decisionsLine) {
|
|
486
|
+
for (const item of decisionsLine.replace("Decisions: ", "").split(" | ").filter(Boolean)) {
|
|
487
|
+
decisions.set(item, (decisions.get(item) ?? 0) + 1);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const compileLine = lines.find((line) => line.startsWith("Latest compile included "));
|
|
491
|
+
if (compileLine) {
|
|
492
|
+
compileObservations++;
|
|
493
|
+
const match = compileLine.match(/included (\d+) memories/);
|
|
494
|
+
if (match) compileIncludedTotal += parseInt(match[1], 10);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return [
|
|
498
|
+
{
|
|
499
|
+
kind: "correction_summary",
|
|
500
|
+
text: renderSummary(repo, "Frequent corrections", corrections),
|
|
501
|
+
source_activity_ids: [...sourceActivityIds]
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
kind: "review_summary",
|
|
505
|
+
text: renderSummary(repo, "Frequent review guidance", reviews),
|
|
506
|
+
source_activity_ids: [...sourceActivityIds]
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
kind: "decision_summary",
|
|
510
|
+
text: renderSummary(repo, "Frequent user decisions", decisions),
|
|
511
|
+
source_activity_ids: [...sourceActivityIds]
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
kind: "compile_summary",
|
|
515
|
+
text: compileObservations > 0 ? [
|
|
516
|
+
`Repo: ${repo}`,
|
|
517
|
+
`Compile observations: ${compileObservations}`,
|
|
518
|
+
`Average included memories: ${(compileIncludedTotal / compileObservations).toFixed(1)}`
|
|
519
|
+
].join("\n") : "",
|
|
520
|
+
source_activity_ids: [...sourceActivityIds]
|
|
521
|
+
}
|
|
522
|
+
];
|
|
523
|
+
}
|
|
524
|
+
function renderSummary(repo, heading, counts) {
|
|
525
|
+
if (counts.size === 0) return "";
|
|
526
|
+
const top = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([text, count]) => `- (${count}) ${text}`);
|
|
527
|
+
return [
|
|
528
|
+
`Repo: ${repo}`,
|
|
529
|
+
`${heading}:`,
|
|
530
|
+
...top
|
|
531
|
+
].join("\n");
|
|
532
|
+
}
|
|
533
|
+
function extractPromptDecisions(events) {
|
|
534
|
+
const seen = /* @__PURE__ */ new Set();
|
|
535
|
+
const decisions = [];
|
|
536
|
+
for (const event of events) {
|
|
537
|
+
if (event.event_type !== "session_event") continue;
|
|
538
|
+
if (event.request.name !== "prompt_submitted") continue;
|
|
539
|
+
const text = String(event.result.text ?? "").trim();
|
|
540
|
+
if (!text) continue;
|
|
541
|
+
const durable = detectCorrections(text).filter((match) => match.type === "decision").map((match) => match.text);
|
|
542
|
+
for (const item of durable) {
|
|
543
|
+
addUniqueDecision(seen, decisions, item);
|
|
544
|
+
}
|
|
545
|
+
const directive = extractDurablePromptDirective(text);
|
|
546
|
+
if (directive) {
|
|
547
|
+
addUniqueDecision(seen, decisions, directive);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return decisions;
|
|
551
|
+
}
|
|
552
|
+
function addUniqueDecision(seen, decisions, text) {
|
|
553
|
+
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
554
|
+
if (!normalized || seen.has(normalized)) return;
|
|
555
|
+
seen.add(normalized);
|
|
556
|
+
decisions.push(text);
|
|
557
|
+
}
|
|
558
|
+
function extractDurablePromptDirective(text) {
|
|
559
|
+
const compact = text.replace(/\s+/g, " ").replace(/^[-*]\s+/, "").trim();
|
|
560
|
+
const phaseDirective = /\b(?:do\s+phase|phase\s+\d+)\b/i.test(compact);
|
|
561
|
+
if (!phaseDirective && compact.length < 14 || compact.length > 240) return null;
|
|
562
|
+
if (!DURABLE_PROMPT_DIRECTIVE.test(compact)) return null;
|
|
563
|
+
if (!DURABLE_PROMPT_DOMAIN.test(compact)) return null;
|
|
564
|
+
if (/^(?:can|could|would|should|why|what|how)\b.*\?$/i.test(compact)) return null;
|
|
565
|
+
return `User direction: ${compact.replace(/[.!?]+$/u, "")}.`;
|
|
566
|
+
}
|
|
567
|
+
var DURABLE_PROMPT_DIRECTIVE = /\b(?:let's|lets|let us|we should|we need to|make|improve|add|change|implement|ship|do phase|phase\s+\d+|production ready|open source|self[- ]healing|self healing)\b/i;
|
|
568
|
+
var DURABLE_PROMPT_DOMAIN = /\b(?:recall|memory|memories|dedupe|duplicate|question|prompt|capture|history|summary|quality|maintenance|cleanup|doctor|daemon|dispatcher|migration|test|phase|production|open source|self[- ]healing|self healing)\b/i;
|
|
569
|
+
|
|
570
|
+
// src/maintenance/logging.ts
|
|
571
|
+
function maintenanceChangeCount(result) {
|
|
572
|
+
return result.prune_total + result.scanned_memories_normalized + result.scanned_memories_demoted + result.scanned_memories_rejected + result.activity_pruned + result.feedback_pruned + result.signals_pruned + result.embeddings_refreshed + result.vector_rows_rebuilt + result.lexical_rows_rebuilt + result.history_snippets_created + result.history_embeddings_refreshed;
|
|
573
|
+
}
|
|
574
|
+
function shouldLogMaintenance(result) {
|
|
575
|
+
return maintenanceChangeCount(result) > 0 || result.vector_drift !== 0 || result.lexical_drift !== 0 || result.embedding_stale > 0 || result.history_vector_drift !== 0 || result.history_lexical_drift !== 0;
|
|
576
|
+
}
|
|
577
|
+
function formatMaintenanceSummary(result) {
|
|
578
|
+
return `[recall] maintenance prune=${result.prune_total} scanned(normalized=${result.scanned_memories_normalized},demoted=${result.scanned_memories_demoted},rejected=${result.scanned_memories_rejected}) activity=${result.activity_pruned} feedback=${result.feedback_pruned} signals=${result.signals_pruned} refreshed=${result.embeddings_refreshed} rebuilt(vec=${result.vector_rows_rebuilt},fts=${result.lexical_rows_rebuilt}) drift(vec=${result.vector_drift},fts=${result.lexical_drift}) stale=${result.embedding_stale} history(created=${result.history_snippets_created},refreshed=${result.history_embeddings_refreshed},drift_vec=${result.history_vector_drift},drift_fts=${result.history_lexical_drift})`;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/daemon.ts
|
|
582
|
+
init_keychain();
|
|
583
|
+
|
|
584
|
+
// src/mcp/http.ts
|
|
585
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
586
|
+
async function handleRecallMcpHttpRequest(req, res, db2) {
|
|
587
|
+
if (req.method !== "POST") {
|
|
588
|
+
return sendJsonRpcError(res, 405, -32e3, "Method not allowed");
|
|
589
|
+
}
|
|
590
|
+
const mcpServer = createRecallMcpServer(db2);
|
|
591
|
+
const transport = new StreamableHTTPServerTransport({
|
|
592
|
+
sessionIdGenerator: void 0,
|
|
593
|
+
enableJsonResponse: true
|
|
594
|
+
});
|
|
595
|
+
try {
|
|
596
|
+
await mcpServer.connect(transport);
|
|
597
|
+
await transport.handleRequest(req, res);
|
|
598
|
+
} catch (error) {
|
|
599
|
+
if (!res.headersSent) {
|
|
600
|
+
sendJsonRpcError(
|
|
601
|
+
res,
|
|
602
|
+
500,
|
|
603
|
+
-32603,
|
|
604
|
+
error instanceof Error ? error.message : "Internal server error"
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
} finally {
|
|
608
|
+
await transport.close().catch(() => {
|
|
609
|
+
});
|
|
610
|
+
await mcpServer.close().catch(() => {
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function sendJsonRpcError(res, status, code, message) {
|
|
615
|
+
res.statusCode = status;
|
|
616
|
+
res.setHeader("Content-Type", "application/json");
|
|
617
|
+
res.end(JSON.stringify({
|
|
618
|
+
jsonrpc: "2.0",
|
|
619
|
+
error: { code, message },
|
|
620
|
+
id: null
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/daemon.ts
|
|
625
|
+
var db;
|
|
626
|
+
var PORT = parseInt(process.env.RECALL_PORT ?? "7890", 10);
|
|
627
|
+
var maintenanceConfig = loadMaintenanceConfigFromEnv();
|
|
628
|
+
var maintenanceRunning = false;
|
|
629
|
+
var dispatcherConfig = {
|
|
630
|
+
enabled: process.env.RECALL_DISPATCHER_ENABLED !== "false",
|
|
631
|
+
intervalSeconds: parseInt(process.env.RECALL_DISPATCHER_INTERVAL_SECONDS ?? "86400", 10),
|
|
632
|
+
maxTasksPerRun: parseInt(process.env.RECALL_DISPATCHER_MAX_TASKS_PER_RUN ?? "5", 10)
|
|
633
|
+
};
|
|
634
|
+
var dispatcherRunning = false;
|
|
635
|
+
var cleanupConfig = {
|
|
636
|
+
enabled: process.env.RECALL_CLEANUP_ENABLED !== "false",
|
|
637
|
+
intervalSeconds: parseInt(process.env.RECALL_CLEANUP_INTERVAL_SECONDS ?? "86400", 10)
|
|
638
|
+
};
|
|
639
|
+
var cleanupRunning = false;
|
|
640
|
+
var qualitySnapshotConfig = {
|
|
641
|
+
enabled: process.env.RECALL_QUALITY_SNAPSHOT_ENABLED !== "false",
|
|
642
|
+
intervalSeconds: parseInt(process.env.RECALL_QUALITY_SNAPSHOT_INTERVAL_SECONDS ?? "604800", 10)
|
|
643
|
+
};
|
|
644
|
+
var qualitySnapshotRunning = false;
|
|
645
|
+
function parseBody(req) {
|
|
646
|
+
return new Promise((resolve, reject) => {
|
|
647
|
+
const chunks = [];
|
|
648
|
+
req.on("data", (c) => chunks.push(c));
|
|
649
|
+
req.on("end", () => {
|
|
650
|
+
try {
|
|
651
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString()));
|
|
652
|
+
} catch {
|
|
653
|
+
resolve({});
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
req.on("error", reject);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
function resolveRepo(body) {
|
|
660
|
+
return body.repo ?? inferRepoSlugFromPath(body.repo_path) ?? void 0;
|
|
661
|
+
}
|
|
662
|
+
function scheduleMaintenanceLoop() {
|
|
663
|
+
if (!maintenanceConfig.enabled) return;
|
|
664
|
+
const run = async () => {
|
|
665
|
+
if (maintenanceRunning) return;
|
|
666
|
+
maintenanceRunning = true;
|
|
667
|
+
try {
|
|
668
|
+
const result = await runMaintenanceCycle(db, maintenanceConfig);
|
|
669
|
+
if (shouldLogMaintenance(result)) {
|
|
670
|
+
console.log(formatMaintenanceSummary(result));
|
|
671
|
+
}
|
|
672
|
+
} catch (error) {
|
|
673
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
674
|
+
console.error(`[recall] maintenance failed: ${message}`);
|
|
675
|
+
} finally {
|
|
676
|
+
maintenanceRunning = false;
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
const intervalMs = Math.max(30, maintenanceConfig.interval_seconds) * 1e3;
|
|
680
|
+
setTimeout(() => void run(), intervalMs).unref?.();
|
|
681
|
+
const timer = setInterval(() => {
|
|
682
|
+
void run();
|
|
683
|
+
}, intervalMs);
|
|
684
|
+
timer.unref?.();
|
|
685
|
+
}
|
|
686
|
+
var dispatcherDormantLogged = false;
|
|
687
|
+
function scheduleDispatcherLoop() {
|
|
688
|
+
if (!dispatcherConfig.enabled) return;
|
|
689
|
+
const run = async () => {
|
|
690
|
+
if (dispatcherRunning) return;
|
|
691
|
+
const hasKey = hasProviderConfigured("anthropic") || hasProviderConfigured("azure-openai") || hasProviderConfigured("openai");
|
|
692
|
+
if (!hasKey) {
|
|
693
|
+
if (!dispatcherDormantLogged) {
|
|
694
|
+
console.log("[recall] dispatcher dormant: no LLM provider configured (set one via 'recall maintenance credentials set <provider> <key>'; preview prompts via 'recall maintenance dispatch --preview')");
|
|
695
|
+
dispatcherDormantLogged = true;
|
|
696
|
+
}
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
dispatcherDormantLogged = false;
|
|
700
|
+
dispatcherRunning = true;
|
|
701
|
+
try {
|
|
702
|
+
const report = await dispatchPendingTasks(db, {
|
|
703
|
+
maxTasks: dispatcherConfig.maxTasksPerRun
|
|
704
|
+
});
|
|
705
|
+
if (report.attempted > 0 || report.applied > 0) {
|
|
706
|
+
console.log(
|
|
707
|
+
`[recall] dispatcher ${report.provider}: attempted=${report.attempted} applied=${report.applied} rejected=${report.rejected} released=${report.released}`
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
} catch (error) {
|
|
711
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
712
|
+
console.error(`[recall] dispatcher failed: ${message}`);
|
|
713
|
+
} finally {
|
|
714
|
+
dispatcherRunning = false;
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
const timer = setInterval(() => {
|
|
718
|
+
void run();
|
|
719
|
+
}, Math.max(60, dispatcherConfig.intervalSeconds) * 1e3);
|
|
720
|
+
timer.unref?.();
|
|
721
|
+
}
|
|
722
|
+
function scheduleCleanupLoop() {
|
|
723
|
+
if (!cleanupConfig.enabled) return;
|
|
724
|
+
const run = async () => {
|
|
725
|
+
if (cleanupRunning) return;
|
|
726
|
+
cleanupRunning = true;
|
|
727
|
+
try {
|
|
728
|
+
const report = runDeterministicCleanup(db, { dryRun: false });
|
|
729
|
+
const c = report.counts;
|
|
730
|
+
const total = c.dedupe_clusters + c.fragment_rejections + c.repeat_promotions + c.command_suppressions + c.globalizations;
|
|
731
|
+
if (total > 0) {
|
|
732
|
+
console.log(
|
|
733
|
+
`[recall] cleanup run=${report.run_id.slice(0, 8)} merges=${c.dedupe_clusters}/${c.dedupe_losers} fragments=${c.fragment_rejections} promotions=${c.repeat_promotions} suppress=${c.command_suppressions} globalize=${c.globalizations}/${c.globalize_losers}`
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
const newContradictions = detectContradictions(db);
|
|
737
|
+
if (newContradictions.length > 0) {
|
|
738
|
+
console.log(
|
|
739
|
+
`[recall] contradictions detected: ${newContradictions.length} new pair(s)`
|
|
740
|
+
);
|
|
741
|
+
for (const c2 of newContradictions.slice(0, 5)) {
|
|
742
|
+
console.log(` [${c2.severity}] ${c2.contradiction_type}: ${c2.description.slice(0, 120)}`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
} catch (error) {
|
|
746
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
747
|
+
console.error(`[recall] cleanup failed: ${message}`);
|
|
748
|
+
} finally {
|
|
749
|
+
cleanupRunning = false;
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
setTimeout(() => void run(), 3e4).unref?.();
|
|
753
|
+
const timer = setInterval(() => {
|
|
754
|
+
void run();
|
|
755
|
+
}, Math.max(60, cleanupConfig.intervalSeconds) * 1e3);
|
|
756
|
+
timer.unref?.();
|
|
757
|
+
}
|
|
758
|
+
function scheduleQualitySnapshotLoop() {
|
|
759
|
+
if (!qualitySnapshotConfig.enabled) return;
|
|
760
|
+
const intervalMs = Math.max(60, qualitySnapshotConfig.intervalSeconds) * 1e3;
|
|
761
|
+
const run = () => {
|
|
762
|
+
if (qualitySnapshotRunning) return;
|
|
763
|
+
qualitySnapshotRunning = true;
|
|
764
|
+
try {
|
|
765
|
+
const last = listQualitySnapshots(db, 1)[0];
|
|
766
|
+
if (last) {
|
|
767
|
+
const ageMs = Date.now() - new Date(last.taken_at).getTime();
|
|
768
|
+
if (ageMs < intervalMs) return;
|
|
769
|
+
}
|
|
770
|
+
const report = computeQualityReport(db);
|
|
771
|
+
const row = recordQualitySnapshot(db, report, "auto");
|
|
772
|
+
console.log(
|
|
773
|
+
`[recall] quality snapshot ${row.id.slice(0, 8)} followed=${row.followed_rate_resolved != null ? (row.followed_rate_resolved * 100).toFixed(1) + "%" : "n/a"} resolved=${row.injections_resolved} history=${row.history_injections_total} rules=${row.active_rule_count} cand=${row.candidate_correction_count}`
|
|
774
|
+
);
|
|
775
|
+
} catch (error) {
|
|
776
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
777
|
+
console.error(`[recall] quality snapshot failed: ${message}`);
|
|
778
|
+
} finally {
|
|
779
|
+
qualitySnapshotRunning = false;
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
setTimeout(run, 6e4).unref?.();
|
|
783
|
+
const timer = setInterval(run, 3600 * 1e3);
|
|
784
|
+
timer.unref?.();
|
|
785
|
+
}
|
|
786
|
+
var server = createServer(async (req, res) => {
|
|
787
|
+
const url = new URL(req.url ?? "/", `http://localhost:${PORT}`);
|
|
788
|
+
const path = url.pathname;
|
|
789
|
+
const method = req.method ?? "GET";
|
|
790
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
791
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
792
|
+
res.setHeader(
|
|
793
|
+
"Access-Control-Allow-Headers",
|
|
794
|
+
"Content-Type, Authorization, MCP-Protocol-Version, MCP-Session-Id, Last-Event-ID"
|
|
795
|
+
);
|
|
796
|
+
if (method === "OPTIONS") {
|
|
797
|
+
res.statusCode = 204;
|
|
798
|
+
res.end();
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
if (path === "/mcp") {
|
|
803
|
+
return await handleRecallMcpHttpRequest(req, res, db);
|
|
804
|
+
}
|
|
805
|
+
res.setHeader("Content-Type", "application/json");
|
|
806
|
+
if (path === "/health" && method === "GET") {
|
|
807
|
+
return send(res, 200, {
|
|
808
|
+
status: "ok",
|
|
809
|
+
version: "0.5.0",
|
|
810
|
+
embeddings: getEmbeddingModelInfo()
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
if (path === "/compile" && method === "POST") {
|
|
814
|
+
const body = await parseBody(req);
|
|
815
|
+
const repo = resolveRepo(body);
|
|
816
|
+
if (!repo) return send(res, 400, { error: "repo or repo_path required" });
|
|
817
|
+
const bootstrap = ensureRepoBootstrapped(db, {
|
|
818
|
+
repo,
|
|
819
|
+
repoPathHint: body.repo_path
|
|
820
|
+
});
|
|
821
|
+
if (bootstrap.status === "bootstrapped" || bootstrap.status === "scanned_empty") {
|
|
822
|
+
createActivityEvent(db, {
|
|
823
|
+
session_id: body.session_id ?? null,
|
|
824
|
+
repo,
|
|
825
|
+
source: "daemon",
|
|
826
|
+
event_type: "scan",
|
|
827
|
+
memory_ids: bootstrap.created_ids,
|
|
828
|
+
request: {
|
|
829
|
+
repo_path: bootstrap.repo_path,
|
|
830
|
+
trigger: "compile_auto_bootstrap"
|
|
831
|
+
},
|
|
832
|
+
result: {
|
|
833
|
+
created: bootstrap.created_ids.length,
|
|
834
|
+
status: bootstrap.status
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
const result = body.query_text || body.config?.include_candidates ? await compileContextHybrid(db, {
|
|
839
|
+
repo,
|
|
840
|
+
path: body.path,
|
|
841
|
+
session_id: body.session_id,
|
|
842
|
+
query_text: body.query_text,
|
|
843
|
+
config: body.config
|
|
844
|
+
}) : compileContext(db, {
|
|
845
|
+
repo,
|
|
846
|
+
path: body.path,
|
|
847
|
+
session_id: body.session_id,
|
|
848
|
+
config: body.config
|
|
849
|
+
});
|
|
850
|
+
createActivityEvent(db, {
|
|
851
|
+
session_id: body.session_id ?? null,
|
|
852
|
+
repo,
|
|
853
|
+
path: body.path ?? null,
|
|
854
|
+
source: "daemon",
|
|
855
|
+
event_type: "compile",
|
|
856
|
+
memory_ids: result.memories_included,
|
|
857
|
+
request: {
|
|
858
|
+
config: body.config ?? {},
|
|
859
|
+
query_text: body.query_text ?? null,
|
|
860
|
+
bootstrap_status: bootstrap.status
|
|
861
|
+
},
|
|
862
|
+
result: {
|
|
863
|
+
included: result.memories_included,
|
|
864
|
+
dropped: result.memories_dropped,
|
|
865
|
+
history_included: result.history_included,
|
|
866
|
+
token_estimate: result.token_estimate,
|
|
867
|
+
repo_path: bootstrap.repo_path
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
return send(res, 200, {
|
|
871
|
+
...result,
|
|
872
|
+
repo,
|
|
873
|
+
repo_path: bootstrap.repo_path ?? body.repo_path ?? null,
|
|
874
|
+
bootstrap_status: bootstrap.status
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
if (path === "/session/start" && method === "POST") {
|
|
878
|
+
const body = await parseBody(req);
|
|
879
|
+
if (!body.session_id) {
|
|
880
|
+
return send(res, 400, { error: "session_id required" });
|
|
881
|
+
}
|
|
882
|
+
const result = startSessionLifecycle(db, {
|
|
883
|
+
session_id: body.session_id,
|
|
884
|
+
client: body.client ?? null,
|
|
885
|
+
repo: body.repo ?? null,
|
|
886
|
+
repo_path: body.repo_path ?? null,
|
|
887
|
+
path: body.path ?? null,
|
|
888
|
+
meta: body.meta ?? {}
|
|
889
|
+
});
|
|
890
|
+
return send(res, 200, result);
|
|
891
|
+
}
|
|
892
|
+
if (path === "/session/event" && method === "POST") {
|
|
893
|
+
const body = await parseBody(req);
|
|
894
|
+
if (!body.session_id || !body.name) {
|
|
895
|
+
return send(res, 400, { error: "session_id and name required" });
|
|
896
|
+
}
|
|
897
|
+
const result = recordSessionLifecycleEvent(db, {
|
|
898
|
+
session_id: body.session_id,
|
|
899
|
+
client: body.client ?? null,
|
|
900
|
+
repo: body.repo ?? null,
|
|
901
|
+
repo_path: body.repo_path ?? null,
|
|
902
|
+
path: body.path ?? null,
|
|
903
|
+
meta: body.meta ?? {},
|
|
904
|
+
name: body.name,
|
|
905
|
+
payload: body.payload ?? {}
|
|
906
|
+
});
|
|
907
|
+
return send(res, 200, result);
|
|
908
|
+
}
|
|
909
|
+
if (path === "/session/end" && method === "POST") {
|
|
910
|
+
const body = await parseBody(req);
|
|
911
|
+
if (!body.session_id) {
|
|
912
|
+
return send(res, 400, { error: "session_id required" });
|
|
913
|
+
}
|
|
914
|
+
const result = endSessionLifecycle(db, {
|
|
915
|
+
session_id: body.session_id,
|
|
916
|
+
client: body.client ?? null,
|
|
917
|
+
repo: body.repo ?? null,
|
|
918
|
+
repo_path: body.repo_path ?? null,
|
|
919
|
+
path: body.path ?? null,
|
|
920
|
+
meta: body.meta ?? {},
|
|
921
|
+
payload: body.payload ?? {}
|
|
922
|
+
});
|
|
923
|
+
return send(res, 200, result);
|
|
924
|
+
}
|
|
925
|
+
if (path === "/hook/prompt" && method === "POST") {
|
|
926
|
+
const body = await parseBody(req);
|
|
927
|
+
if (!body.text) {
|
|
928
|
+
return send(res, 400, { error: "text required" });
|
|
929
|
+
}
|
|
930
|
+
const result = await handlePromptHook(body, {
|
|
931
|
+
db,
|
|
932
|
+
source: "daemon"
|
|
933
|
+
});
|
|
934
|
+
return send(res, 200, { ...result, transport: "daemon" });
|
|
935
|
+
}
|
|
936
|
+
if (path === "/hook/tool" && method === "POST") {
|
|
937
|
+
const body = await parseBody(req);
|
|
938
|
+
if (!body.name || typeof body.exit_code !== "number") {
|
|
939
|
+
return send(res, 400, { error: "name and numeric exit_code required" });
|
|
940
|
+
}
|
|
941
|
+
const result = await handleToolHook(body, {
|
|
942
|
+
db,
|
|
943
|
+
source: "daemon"
|
|
944
|
+
});
|
|
945
|
+
return send(res, 200, { ...result, transport: "daemon" });
|
|
946
|
+
}
|
|
947
|
+
if (path === "/hook/session-start" && method === "POST") {
|
|
948
|
+
const body = await parseBody(req);
|
|
949
|
+
if (!body.session_id || !body.agent) {
|
|
950
|
+
return send(res, 400, { error: "session_id and agent required" });
|
|
951
|
+
}
|
|
952
|
+
const result = await handleSessionStartHook(body, {
|
|
953
|
+
db,
|
|
954
|
+
source: "daemon"
|
|
955
|
+
});
|
|
956
|
+
return send(res, 200, { ...result, transport: "daemon" });
|
|
957
|
+
}
|
|
958
|
+
if (path === "/hook/session-end" && method === "POST") {
|
|
959
|
+
const body = await parseBody(req);
|
|
960
|
+
if (!body.session_id) {
|
|
961
|
+
return send(res, 400, { error: "session_id required" });
|
|
962
|
+
}
|
|
963
|
+
const result = await handleSessionEndHook(body, {
|
|
964
|
+
db,
|
|
965
|
+
source: "daemon"
|
|
966
|
+
});
|
|
967
|
+
return send(res, 200, { ...result, transport: "daemon" });
|
|
968
|
+
}
|
|
969
|
+
if (path === "/correct" && method === "POST") {
|
|
970
|
+
const body = await parseBody(req);
|
|
971
|
+
const repo = resolveRepo(body);
|
|
972
|
+
const ids = await processCorrection(db, body.text, {
|
|
973
|
+
sessionId: body.session_id ?? "hook",
|
|
974
|
+
repo,
|
|
975
|
+
path: body.path
|
|
976
|
+
});
|
|
977
|
+
createActivityEvent(db, {
|
|
978
|
+
session_id: body.session_id ?? "hook",
|
|
979
|
+
repo: repo ?? null,
|
|
980
|
+
path: body.path ?? null,
|
|
981
|
+
source: "daemon",
|
|
982
|
+
event_type: "correction",
|
|
983
|
+
memory_ids: ids,
|
|
984
|
+
request: { text: body.text },
|
|
985
|
+
result: { created: ids }
|
|
986
|
+
});
|
|
987
|
+
return send(res, 200, { created: ids });
|
|
988
|
+
}
|
|
989
|
+
if (path === "/review" && method === "POST") {
|
|
990
|
+
const body = await parseBody(req);
|
|
991
|
+
const repo = resolveRepo(body);
|
|
992
|
+
const ids = await processReviewFeedback(db, body.feedback, {
|
|
993
|
+
sessionId: body.session_id ?? "hook-review",
|
|
994
|
+
repo,
|
|
995
|
+
path: body.path,
|
|
996
|
+
reviewer: body.reviewer
|
|
997
|
+
});
|
|
998
|
+
createActivityEvent(db, {
|
|
999
|
+
session_id: body.session_id ?? "hook-review",
|
|
1000
|
+
repo: repo ?? null,
|
|
1001
|
+
path: body.path ?? null,
|
|
1002
|
+
source: "daemon",
|
|
1003
|
+
event_type: "review",
|
|
1004
|
+
memory_ids: ids,
|
|
1005
|
+
request: { feedback: body.feedback, reviewer: body.reviewer ?? null },
|
|
1006
|
+
result: { created: ids }
|
|
1007
|
+
});
|
|
1008
|
+
return send(res, 200, { created: ids });
|
|
1009
|
+
}
|
|
1010
|
+
if (path === "/confirm" && method === "POST") {
|
|
1011
|
+
const body = await parseBody(req);
|
|
1012
|
+
const ok = confirmMemory(db, body.memory_id);
|
|
1013
|
+
return send(res, ok ? 200 : 404, { success: ok });
|
|
1014
|
+
}
|
|
1015
|
+
if (path === "/reject" && method === "POST") {
|
|
1016
|
+
const body = await parseBody(req);
|
|
1017
|
+
const ok = rejectMemory(db, body.memory_id);
|
|
1018
|
+
return send(res, ok ? 200 : 404, { success: ok });
|
|
1019
|
+
}
|
|
1020
|
+
if (path === "/feedback" && method === "POST") {
|
|
1021
|
+
const body = await parseBody(req);
|
|
1022
|
+
const id = recordFeedback(
|
|
1023
|
+
db,
|
|
1024
|
+
body.memory_id,
|
|
1025
|
+
body.session_id,
|
|
1026
|
+
body.injected,
|
|
1027
|
+
body.outcome
|
|
1028
|
+
);
|
|
1029
|
+
createActivityEvent(db, {
|
|
1030
|
+
session_id: body.session_id,
|
|
1031
|
+
source: "daemon",
|
|
1032
|
+
event_type: "feedback",
|
|
1033
|
+
memory_ids: [body.memory_id],
|
|
1034
|
+
request: { injected: body.injected, outcome: body.outcome },
|
|
1035
|
+
result: { feedback_id: id }
|
|
1036
|
+
});
|
|
1037
|
+
return send(res, 200, { feedback_id: id });
|
|
1038
|
+
}
|
|
1039
|
+
if (path === "/memories" && method === "GET") {
|
|
1040
|
+
const repo = url.searchParams.get("repo") ?? void 0;
|
|
1041
|
+
const status = url.searchParams.get("status");
|
|
1042
|
+
const limit = url.searchParams.get("limit");
|
|
1043
|
+
const offset = url.searchParams.get("offset");
|
|
1044
|
+
const items = queryMemories(db, {
|
|
1045
|
+
repo,
|
|
1046
|
+
status,
|
|
1047
|
+
limit: limit ? parseInt(limit, 10) : void 0,
|
|
1048
|
+
offset: offset ? parseInt(offset, 10) : void 0
|
|
1049
|
+
});
|
|
1050
|
+
return send(res, 200, { memories: items });
|
|
1051
|
+
}
|
|
1052
|
+
if (path.startsWith("/memory/") && method === "GET") {
|
|
1053
|
+
const id = path.slice("/memory/".length);
|
|
1054
|
+
const mem = getMemory(db, id);
|
|
1055
|
+
if (!mem) return send(res, 404, { error: "not found" });
|
|
1056
|
+
return send(res, 200, mem);
|
|
1057
|
+
}
|
|
1058
|
+
if (path === "/scan" && method === "POST") {
|
|
1059
|
+
const body = await parseBody(req);
|
|
1060
|
+
const ids = scanAndStore(db, body.repo_path);
|
|
1061
|
+
const mem = ids[0] ? getMemory(db, ids[0]) : void 0;
|
|
1062
|
+
const artifact = writeRepoContextArtifact(db, {
|
|
1063
|
+
repo: mem?.repo ?? null,
|
|
1064
|
+
repo_path: body.repo_path
|
|
1065
|
+
});
|
|
1066
|
+
createActivityEvent(db, {
|
|
1067
|
+
session_id: body.session_id ?? null,
|
|
1068
|
+
repo: mem?.repo ?? null,
|
|
1069
|
+
source: "daemon",
|
|
1070
|
+
event_type: "scan",
|
|
1071
|
+
memory_ids: ids,
|
|
1072
|
+
request: { repo_path: body.repo_path },
|
|
1073
|
+
result: {
|
|
1074
|
+
created: ids.length,
|
|
1075
|
+
artifact_path: artifact.output_path,
|
|
1076
|
+
artifact_written: artifact.written
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
return send(res, 200, {
|
|
1080
|
+
created: ids,
|
|
1081
|
+
count: ids.length,
|
|
1082
|
+
artifact_path: artifact.output_path,
|
|
1083
|
+
artifact_written: artifact.written
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
if (path === "/eval/metrics" && method === "GET") {
|
|
1087
|
+
const repo = url.searchParams.get("repo") ?? void 0;
|
|
1088
|
+
const since = url.searchParams.get("since") ?? void 0;
|
|
1089
|
+
const metrics = computeMetrics(db, { repo, since });
|
|
1090
|
+
return send(res, 200, metrics);
|
|
1091
|
+
}
|
|
1092
|
+
if (path === "/eval/start" && method === "POST") {
|
|
1093
|
+
const body = await parseBody(req);
|
|
1094
|
+
const id = startEvalSession(db, body.repo);
|
|
1095
|
+
return send(res, 200, { session_id: id });
|
|
1096
|
+
}
|
|
1097
|
+
if (path === "/eval/end" && method === "POST") {
|
|
1098
|
+
const body = await parseBody(req);
|
|
1099
|
+
endEvalSession(db, body.session_id);
|
|
1100
|
+
return send(res, 200, { success: true });
|
|
1101
|
+
}
|
|
1102
|
+
if (path === "/eval/increment" && method === "POST") {
|
|
1103
|
+
const body = await parseBody(req);
|
|
1104
|
+
incrementEvalCounter(db, body.session_id, body.field, body.amount ?? 1);
|
|
1105
|
+
return send(res, 200, { success: true });
|
|
1106
|
+
}
|
|
1107
|
+
if (path === "/signal" && method === "POST") {
|
|
1108
|
+
const body = await parseBody(req);
|
|
1109
|
+
const id = recordSignal(
|
|
1110
|
+
db,
|
|
1111
|
+
body.memory_id,
|
|
1112
|
+
body.session_id ?? "daemon",
|
|
1113
|
+
body.signal_type,
|
|
1114
|
+
body.context
|
|
1115
|
+
);
|
|
1116
|
+
const mem = getMemory(db, body.memory_id);
|
|
1117
|
+
createActivityEvent(db, {
|
|
1118
|
+
session_id: body.session_id ?? "daemon",
|
|
1119
|
+
repo: mem?.repo ?? null,
|
|
1120
|
+
path: mem?.path_scope ?? null,
|
|
1121
|
+
source: "daemon",
|
|
1122
|
+
event_type: "signal",
|
|
1123
|
+
memory_ids: [body.memory_id],
|
|
1124
|
+
request: { signal_type: body.signal_type, context: body.context ?? null },
|
|
1125
|
+
result: { signal_id: id }
|
|
1126
|
+
});
|
|
1127
|
+
return send(res, 200, { signal_id: id });
|
|
1128
|
+
}
|
|
1129
|
+
if (path.startsWith("/signal/stats/") && method === "GET") {
|
|
1130
|
+
const memId = path.slice("/signal/stats/".length);
|
|
1131
|
+
const stats = getSignalStats(db, memId);
|
|
1132
|
+
return send(res, 200, stats);
|
|
1133
|
+
}
|
|
1134
|
+
if (path === "/test" && method === "POST") {
|
|
1135
|
+
const body = await parseBody(req);
|
|
1136
|
+
const testResult = runTests(body.repo_path, body.command);
|
|
1137
|
+
const signalIds = recordTestSignals(
|
|
1138
|
+
db,
|
|
1139
|
+
body.session_id ?? "daemon",
|
|
1140
|
+
body.memory_ids ?? [],
|
|
1141
|
+
testResult
|
|
1142
|
+
);
|
|
1143
|
+
return send(res, 200, {
|
|
1144
|
+
passed: testResult.passed,
|
|
1145
|
+
signals: signalIds,
|
|
1146
|
+
output: testResult.output?.slice(0, 1e3)
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
if (path === "/quality" && method === "GET") {
|
|
1150
|
+
const repo = url.searchParams.get("repo") ?? void 0;
|
|
1151
|
+
return send(res, 200, getRepoQualityProfile(db, repo));
|
|
1152
|
+
}
|
|
1153
|
+
if (path === "/activity" && method === "GET") {
|
|
1154
|
+
const repo = url.searchParams.get("repo") ?? void 0;
|
|
1155
|
+
const session_id = url.searchParams.get("session_id") ?? void 0;
|
|
1156
|
+
const source = url.searchParams.get("source");
|
|
1157
|
+
const event_type = url.searchParams.get("event_type");
|
|
1158
|
+
const since = url.searchParams.get("since") ?? void 0;
|
|
1159
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "20", 10);
|
|
1160
|
+
return send(res, 200, {
|
|
1161
|
+
events: listActivityEvents(db, { repo, session_id, source, event_type, since, limit })
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
if (path === "/sessions" && method === "GET") {
|
|
1165
|
+
const repo = url.searchParams.get("repo") ?? void 0;
|
|
1166
|
+
const source = url.searchParams.get("source");
|
|
1167
|
+
const event_type = url.searchParams.get("event_type");
|
|
1168
|
+
const since = url.searchParams.get("since") ?? void 0;
|
|
1169
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "20", 10);
|
|
1170
|
+
return send(res, 200, {
|
|
1171
|
+
sessions: listActivitySessions(db, { repo, source, event_type, since, limit })
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
if (path === "/policy/list" && method === "GET") {
|
|
1175
|
+
const orgId = url.searchParams.get("org_id") ?? "";
|
|
1176
|
+
return send(res, 200, { policies: listPolicies(db, orgId) });
|
|
1177
|
+
}
|
|
1178
|
+
if (path === "/policy" && method === "POST") {
|
|
1179
|
+
const body = await parseBody(req);
|
|
1180
|
+
const id = createPolicy(db, body.org_id, body.rule_type, body.config);
|
|
1181
|
+
return send(res, 200, { policy_id: id });
|
|
1182
|
+
}
|
|
1183
|
+
if (path === "/policy/check" && method === "POST") {
|
|
1184
|
+
const body = await parseBody(req);
|
|
1185
|
+
const mem = getMemory(db, body.memory_id);
|
|
1186
|
+
if (!mem) return send(res, 404, { error: "memory not found" });
|
|
1187
|
+
const violations = evaluatePolicy(db, body.org_id, mem);
|
|
1188
|
+
return send(res, 200, { violations });
|
|
1189
|
+
}
|
|
1190
|
+
if (path === "/approval/request" && method === "POST") {
|
|
1191
|
+
const body = await parseBody(req);
|
|
1192
|
+
const id = requestApproval(db, body.memory_id, body.org_id, body.requested_by ?? "daemon");
|
|
1193
|
+
return send(res, 200, { approval_id: id });
|
|
1194
|
+
}
|
|
1195
|
+
if (path === "/approval/pending" && method === "GET") {
|
|
1196
|
+
const orgId = url.searchParams.get("org_id") ?? "";
|
|
1197
|
+
return send(res, 200, { approvals: listPendingApprovals(db, orgId) });
|
|
1198
|
+
}
|
|
1199
|
+
if (path === "/approval/resolve" && method === "POST") {
|
|
1200
|
+
const body = await parseBody(req);
|
|
1201
|
+
const ok = resolveApproval(db, body.approval_id, body.status, body.reviewed_by ?? "daemon", body.reason);
|
|
1202
|
+
return send(res, ok ? 200 : 404, { success: ok });
|
|
1203
|
+
}
|
|
1204
|
+
if (path.startsWith("/health/") && method === "GET") {
|
|
1205
|
+
const memId = path.slice("/health/".length);
|
|
1206
|
+
const score = computeHealthScore(db, memId);
|
|
1207
|
+
if (!score) return send(res, 404, { error: "not found" });
|
|
1208
|
+
return send(res, 200, score);
|
|
1209
|
+
}
|
|
1210
|
+
if (path === "/health" && method === "GET") {
|
|
1211
|
+
const repo = url.searchParams.get("repo") ?? void 0;
|
|
1212
|
+
const scores = computeAllHealthScores(db, repo);
|
|
1213
|
+
return send(res, 200, { scores });
|
|
1214
|
+
}
|
|
1215
|
+
if (path === "/contradictions/detect" && method === "POST") {
|
|
1216
|
+
const body = await parseBody(req);
|
|
1217
|
+
const found = detectContradictions(db, body.repo);
|
|
1218
|
+
return send(res, 200, { contradictions: found });
|
|
1219
|
+
}
|
|
1220
|
+
if (path === "/contradictions" && method === "GET") {
|
|
1221
|
+
const resolved = url.searchParams.get("resolved");
|
|
1222
|
+
const items = listContradictions(db, {
|
|
1223
|
+
resolved: resolved === "true" ? true : resolved === "false" ? false : void 0
|
|
1224
|
+
});
|
|
1225
|
+
return send(res, 200, { contradictions: items });
|
|
1226
|
+
}
|
|
1227
|
+
if (path === "/contradictions/resolve" && method === "POST") {
|
|
1228
|
+
const body = await parseBody(req);
|
|
1229
|
+
const ok = resolveContradiction(db, body.contradiction_id, body.keep_memory_id, body.actor ?? "daemon", body.resolution);
|
|
1230
|
+
return send(res, ok ? 200 : 404, { success: ok });
|
|
1231
|
+
}
|
|
1232
|
+
if (path === "/contradictions/auto-resolve" && method === "POST") {
|
|
1233
|
+
const body = await parseBody(req);
|
|
1234
|
+
const count = autoResolveContradictions(db, body.repo);
|
|
1235
|
+
return send(res, 200, { resolved: count });
|
|
1236
|
+
}
|
|
1237
|
+
if (path === "/prune" && method === "POST") {
|
|
1238
|
+
const body = await parseBody(req);
|
|
1239
|
+
const result = pruneMemories(db, body.config);
|
|
1240
|
+
return send(res, 200, result);
|
|
1241
|
+
}
|
|
1242
|
+
if (path.startsWith("/audit/memory/") && method === "GET") {
|
|
1243
|
+
const memId = path.slice("/audit/memory/".length);
|
|
1244
|
+
const entries = getAuditTrail(db, memId);
|
|
1245
|
+
return send(res, 200, { entries });
|
|
1246
|
+
}
|
|
1247
|
+
if (path === "/audit/recent" && method === "GET") {
|
|
1248
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "50");
|
|
1249
|
+
const entries = getRecentAudit(db, limit);
|
|
1250
|
+
return send(res, 200, { entries });
|
|
1251
|
+
}
|
|
1252
|
+
if (path === "/audit/rollback" && method === "POST") {
|
|
1253
|
+
const body = await parseBody(req);
|
|
1254
|
+
const ok = rollbackMemory(db, body.memory_id, body.audit_entry_id, body.actor ?? "daemon");
|
|
1255
|
+
return send(res, ok ? 200 : 404, { success: ok });
|
|
1256
|
+
}
|
|
1257
|
+
send(res, 404, { error: "not found" });
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
send(res, 500, { error: err.message });
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
function send(res, status, data) {
|
|
1263
|
+
res.statusCode = status;
|
|
1264
|
+
res.end(JSON.stringify(data));
|
|
1265
|
+
}
|
|
1266
|
+
async function startDaemon() {
|
|
1267
|
+
const backup = ensureDailyBackup();
|
|
1268
|
+
if (backup.created) {
|
|
1269
|
+
console.log(`[recall] backup created ${backup.created} (retained ${backup.retained.length})`);
|
|
1270
|
+
}
|
|
1271
|
+
db = initDb();
|
|
1272
|
+
server.listen(PORT, () => {
|
|
1273
|
+
console.log(`Recall daemon listening on http://localhost:${PORT}`);
|
|
1274
|
+
scheduleMaintenanceLoop();
|
|
1275
|
+
scheduleDispatcherLoop();
|
|
1276
|
+
scheduleCleanupLoop();
|
|
1277
|
+
scheduleQualitySnapshotLoop();
|
|
1278
|
+
setTimeout(() => {
|
|
1279
|
+
const embeddingConfig = loadEmbeddingConfigFromEnv();
|
|
1280
|
+
if (!embeddingConfig) return;
|
|
1281
|
+
const info = getEmbeddingModelInfo(embeddingConfig);
|
|
1282
|
+
if (info && !info.cached) {
|
|
1283
|
+
const approx = info.estimated_size_mb ? `~${info.estimated_size_mb}MB` : "download";
|
|
1284
|
+
console.log(`[recall] Fetching embedding model (one-time, ${approx}) -> ${info.cache_path}`);
|
|
1285
|
+
}
|
|
1286
|
+
void ensureEmbeddingProviderReady(embeddingConfig).catch((error) => {
|
|
1287
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
1288
|
+
console.error(`[recall] embedding provider warmup failed: ${message}`);
|
|
1289
|
+
});
|
|
1290
|
+
}, 6e4).unref?.();
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
void startDaemon().catch((error) => {
|
|
1294
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
1295
|
+
console.error(`[recall] daemon startup failed: ${message}`);
|
|
1296
|
+
process.exit(1);
|
|
1297
|
+
});
|
|
1298
|
+
//# sourceMappingURL=daemon.js.map
|