chapterhouse 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/route-coverage.test.js +1 -3
- package/dist/api/server.js +0 -2
- package/dist/api/server.test.js +0 -281
- package/dist/config.js +3 -85
- package/dist/config.test.js +5 -123
- package/dist/copilot/agents.js +13 -10
- package/dist/copilot/agents.test.js +10 -11
- package/dist/copilot/memory-coordinator.js +12 -227
- package/dist/copilot/memory-coordinator.test.js +31 -250
- package/dist/copilot/orchestrator.js +8 -66
- package/dist/copilot/orchestrator.test.js +9 -467
- package/dist/copilot/skills.js +15 -1
- package/dist/copilot/system-message.js +9 -15
- package/dist/copilot/system-message.test.js +9 -22
- package/dist/copilot/tools/index.js +3 -3
- package/dist/copilot/tools-deps.js +1 -1
- package/dist/copilot/tools.agent.test.js +6 -0
- package/dist/copilot/tools.inventory.test.js +1 -14
- package/dist/daemon.js +7 -9
- package/dist/memory/assets.js +33 -0
- package/dist/memory/domains.js +58 -0
- package/dist/memory/domains.test.js +47 -0
- package/dist/memory/git.js +66 -0
- package/dist/memory/git.test.js +32 -0
- package/dist/memory/history.js +19 -0
- package/dist/memory/hottier.js +32 -0
- package/dist/memory/hottier.test.js +33 -0
- package/dist/memory/index.js +5 -13
- package/dist/memory/instructions.js +17 -0
- package/dist/memory/manager.js +84 -0
- package/dist/memory/markdown.js +78 -0
- package/dist/memory/markdown.test.js +42 -0
- package/dist/memory/mutex.js +18 -0
- package/dist/memory/path-guard.js +26 -0
- package/dist/memory/path-guard.test.js +27 -0
- package/dist/memory/paths.js +12 -0
- package/dist/memory/reconcile.js +75 -0
- package/dist/memory/reconcile.test.js +50 -0
- package/dist/memory/scaffold.js +37 -0
- package/dist/memory/scaffold.test.js +52 -0
- package/dist/memory/tools/commit-wrapper.js +32 -0
- package/dist/memory/tools/domains.js +73 -0
- package/dist/memory/tools/domains.test.js +66 -0
- package/dist/memory/tools/git.js +52 -0
- package/dist/memory/tools/index.js +25 -0
- package/dist/memory/tools/read.js +101 -0
- package/dist/memory/tools/read.test.js +69 -0
- package/dist/memory/tools/search.js +103 -0
- package/dist/memory/tools/search.test.js +63 -0
- package/dist/memory/tools/sessions.js +45 -0
- package/dist/memory/tools/sessions.test.js +74 -0
- package/dist/memory/tools/shared.js +7 -0
- package/dist/memory/tools/write.js +116 -0
- package/dist/memory/tools/write.test.js +107 -0
- package/dist/memory/walk.js +39 -0
- package/dist/store/repositories/sessions.js +40 -0
- package/dist/wiki/consolidation.js +3 -31
- package/dist/wiki/consolidation.test.js +0 -19
- package/package.json +1 -1
- package/skills/system/evolve/SKILL.md +131 -0
- package/skills/system/foresight/SKILL.md +116 -0
- package/skills/system/history/SKILL.md +58 -0
- package/skills/system/housekeeping/SKILL.md +185 -0
- package/skills/system/reflect/SKILL.md +214 -0
- package/skills/system/scenario/SKILL.md +198 -0
- package/skills/system/setup/SKILL.md +113 -0
- package/web/dist/assets/{WikiEdit-CGRxNazp.js → WikiEdit-BTsiBfbC.js} +2 -2
- package/web/dist/assets/{WikiEdit-CGRxNazp.js.map → WikiEdit-BTsiBfbC.js.map} +1 -1
- package/web/dist/assets/{WikiGraph-eVWNhZS3.js → WikiGraph-COOZbUeH.js} +2 -2
- package/web/dist/assets/{WikiGraph-eVWNhZS3.js.map → WikiGraph-COOZbUeH.js.map} +1 -1
- package/web/dist/assets/{index-gAvLNEvJ.js → index-aCcfpaLM.js} +101 -101
- package/web/dist/assets/index-aCcfpaLM.js.map +1 -0
- package/web/dist/index.html +1 -1
- package/dist/api/routes/memory.js +0 -475
- package/dist/api/routes/memory.test.js +0 -108
- package/dist/copilot/tools/memory.js +0 -678
- package/dist/copilot/tools.memory.test.js +0 -590
- package/dist/memory/action-items.js +0 -100
- package/dist/memory/action-items.test.js +0 -83
- package/dist/memory/active-scope.js +0 -78
- package/dist/memory/active-scope.test.js +0 -80
- package/dist/memory/checkpoint-prompt.js +0 -71
- package/dist/memory/checkpoint.js +0 -274
- package/dist/memory/checkpoint.test.js +0 -275
- package/dist/memory/decisions.js +0 -54
- package/dist/memory/decisions.test.js +0 -92
- package/dist/memory/entities.js +0 -70
- package/dist/memory/entities.test.js +0 -65
- package/dist/memory/eot.js +0 -459
- package/dist/memory/eot.test.js +0 -949
- package/dist/memory/hooks.js +0 -149
- package/dist/memory/hooks.test.js +0 -325
- package/dist/memory/hot-tier.js +0 -283
- package/dist/memory/hot-tier.test.js +0 -275
- package/dist/memory/housekeeping-scheduler.js +0 -187
- package/dist/memory/housekeeping-scheduler.test.js +0 -236
- package/dist/memory/housekeeping.js +0 -497
- package/dist/memory/housekeeping.test.js +0 -410
- package/dist/memory/inbox.js +0 -83
- package/dist/memory/inbox.test.js +0 -178
- package/dist/memory/migration.js +0 -244
- package/dist/memory/migration.test.js +0 -108
- package/dist/memory/observations.js +0 -46
- package/dist/memory/observations.test.js +0 -86
- package/dist/memory/recall.js +0 -269
- package/dist/memory/recall.test.js +0 -265
- package/dist/memory/reflect.js +0 -273
- package/dist/memory/reflect.test.js +0 -256
- package/dist/memory/scope-lock.js +0 -26
- package/dist/memory/scope-lock.test.js +0 -118
- package/dist/memory/scopes.js +0 -89
- package/dist/memory/scopes.test.js +0 -176
- package/dist/memory/tiering.js +0 -223
- package/dist/memory/tiering.test.js +0 -323
- package/dist/memory/types.js +0 -2
- package/web/dist/assets/index-gAvLNEvJ.js.map +0 -1
|
@@ -1,497 +0,0 @@
|
|
|
1
|
-
import { performance } from "node:perf_hooks";
|
|
2
|
-
import { config } from "../config.js";
|
|
3
|
-
import { getDb } from "../store/db.js";
|
|
4
|
-
import { childLogger } from "../util/logger.js";
|
|
5
|
-
import { getActiveScope } from "./active-scope.js";
|
|
6
|
-
import { releaseScopeWriteLocks, tryAcquireScopeWriteLocks } from "./scope-lock.js";
|
|
7
|
-
import { listScopes } from "./scopes.js";
|
|
8
|
-
import { tieringPass } from "./tiering.js";
|
|
9
|
-
export { tieringPass };
|
|
10
|
-
const log = childLogger("memory.housekeeping");
|
|
11
|
-
const GLOBAL_PASS_SCOPE = Symbol("global-housekeeping-pass-scope");
|
|
12
|
-
const inFlightScopesByPass = new Map();
|
|
13
|
-
const PASS_ORDER = [
|
|
14
|
-
"dedup_observations",
|
|
15
|
-
"dedup_decisions",
|
|
16
|
-
"compact_supersede_chains",
|
|
17
|
-
"orphan_cleanup",
|
|
18
|
-
"decay",
|
|
19
|
-
"compact_inbox",
|
|
20
|
-
"tiering",
|
|
21
|
-
];
|
|
22
|
-
function passSummary(pass, examined = 0, modified = 0, errors = []) {
|
|
23
|
-
return { pass, examined, modified, errors };
|
|
24
|
-
}
|
|
25
|
-
function tokens(value) {
|
|
26
|
-
const words = value
|
|
27
|
-
.toLowerCase()
|
|
28
|
-
.split(/[^a-z0-9]+/u)
|
|
29
|
-
.map((word) => word.replace(/s$/, ""))
|
|
30
|
-
.filter((word) => word.length > 1);
|
|
31
|
-
return new Set(words);
|
|
32
|
-
}
|
|
33
|
-
function jaccard(left, right) {
|
|
34
|
-
const leftTokens = tokens(left);
|
|
35
|
-
const rightTokens = tokens(right);
|
|
36
|
-
if (leftTokens.size === 0 || rightTokens.size === 0) {
|
|
37
|
-
return 0;
|
|
38
|
-
}
|
|
39
|
-
let intersection = 0;
|
|
40
|
-
for (const token of leftTokens) {
|
|
41
|
-
if (rightTokens.has(token)) {
|
|
42
|
-
intersection++;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
const union = leftTokens.size + rightTokens.size - intersection;
|
|
46
|
-
return union === 0 ? 0 : intersection / union;
|
|
47
|
-
}
|
|
48
|
-
function isSimilar(left, right) {
|
|
49
|
-
if (left.trim().toLowerCase() === right.trim().toLowerCase()) {
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
52
|
-
return jaccard(left, right) >= config.memoryHousekeepingSimilarityThreshold;
|
|
53
|
-
}
|
|
54
|
-
function compareObservationKeeper(left, right) {
|
|
55
|
-
if (right.confidence !== left.confidence) {
|
|
56
|
-
return right.confidence - left.confidence;
|
|
57
|
-
}
|
|
58
|
-
if (right.created_at !== left.created_at) {
|
|
59
|
-
return right.created_at.localeCompare(left.created_at);
|
|
60
|
-
}
|
|
61
|
-
return left.id - right.id;
|
|
62
|
-
}
|
|
63
|
-
function compareDecisionKeeper(left, right) {
|
|
64
|
-
if (right.decided_at !== left.decided_at) {
|
|
65
|
-
return right.decided_at.localeCompare(left.decided_at);
|
|
66
|
-
}
|
|
67
|
-
if (right.created_at !== left.created_at) {
|
|
68
|
-
return right.created_at.localeCompare(left.created_at);
|
|
69
|
-
}
|
|
70
|
-
return left.id - right.id;
|
|
71
|
-
}
|
|
72
|
-
function normalizePassName(pass) {
|
|
73
|
-
const normalized = pass.trim().toLowerCase().replace(/-/g, "_");
|
|
74
|
-
const aliases = {
|
|
75
|
-
dedup_observations: "dedup_observations",
|
|
76
|
-
dedupobservations: "dedup_observations",
|
|
77
|
-
dedupobservationspass: "dedup_observations",
|
|
78
|
-
observations: "dedup_observations",
|
|
79
|
-
dedup_decisions: "dedup_decisions",
|
|
80
|
-
dedupdecisions: "dedup_decisions",
|
|
81
|
-
dedupdecisionspass: "dedup_decisions",
|
|
82
|
-
decisions: "dedup_decisions",
|
|
83
|
-
compact_supersede_chains: "compact_supersede_chains",
|
|
84
|
-
compactsupersedechains: "compact_supersede_chains",
|
|
85
|
-
compactsupersedechainspass: "compact_supersede_chains",
|
|
86
|
-
supersede: "compact_supersede_chains",
|
|
87
|
-
supersedechains: "compact_supersede_chains",
|
|
88
|
-
orphan_cleanup: "orphan_cleanup",
|
|
89
|
-
orphancleanup: "orphan_cleanup",
|
|
90
|
-
orphancleanuppass: "orphan_cleanup",
|
|
91
|
-
orphans: "orphan_cleanup",
|
|
92
|
-
decay: "decay",
|
|
93
|
-
decaypass: "decay",
|
|
94
|
-
compact_inbox: "compact_inbox",
|
|
95
|
-
compactinbox: "compact_inbox",
|
|
96
|
-
compactinboxpass: "compact_inbox",
|
|
97
|
-
inbox: "compact_inbox",
|
|
98
|
-
tiering: "tiering",
|
|
99
|
-
tieringpass: "tiering",
|
|
100
|
-
tiers: "tiering",
|
|
101
|
-
};
|
|
102
|
-
const resolved = aliases[normalized];
|
|
103
|
-
if (!resolved) {
|
|
104
|
-
throw new Error(`Unknown housekeeping pass '${pass}'. Valid passes: ${PASS_ORDER.join(", ")}`);
|
|
105
|
-
}
|
|
106
|
-
return resolved;
|
|
107
|
-
}
|
|
108
|
-
export function dedupObservationsPass(scopeId) {
|
|
109
|
-
try {
|
|
110
|
-
const db = getDb();
|
|
111
|
-
const candidates = db.prepare(`
|
|
112
|
-
SELECT id, content, confidence, created_at
|
|
113
|
-
FROM mem_observations
|
|
114
|
-
WHERE scope_id = ?
|
|
115
|
-
AND superseded_by IS NULL
|
|
116
|
-
AND archived_at IS NULL
|
|
117
|
-
ORDER BY id ASC
|
|
118
|
-
`).all(scopeId);
|
|
119
|
-
let modified = 0;
|
|
120
|
-
const visited = new Set();
|
|
121
|
-
const update = db.prepare(`UPDATE mem_observations SET superseded_by = ? WHERE id = ? AND superseded_by IS NULL`);
|
|
122
|
-
const tx = db.transaction(() => {
|
|
123
|
-
for (const candidate of candidates) {
|
|
124
|
-
if (visited.has(candidate.id)) {
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
const cluster = candidates.filter((entry) => !visited.has(entry.id) && isSimilar(candidate.content, entry.content));
|
|
128
|
-
for (const entry of cluster) {
|
|
129
|
-
visited.add(entry.id);
|
|
130
|
-
}
|
|
131
|
-
if (cluster.length < 2) {
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
const [keeper] = cluster.sort(compareObservationKeeper);
|
|
135
|
-
if (!keeper) {
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
for (const entry of cluster) {
|
|
139
|
-
if (entry.id === keeper.id) {
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
modified += update.run(keeper.id, entry.id).changes;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
tx();
|
|
147
|
-
return passSummary("dedupObservationsPass", candidates.length, modified);
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
return passSummary("dedupObservationsPass", 0, 0, [error instanceof Error ? error.message : String(error)]);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
export function dedupDecisionsPass(scopeId) {
|
|
154
|
-
try {
|
|
155
|
-
const db = getDb();
|
|
156
|
-
const candidates = db.prepare(`
|
|
157
|
-
SELECT id, entity_id, title, decided_at, created_at
|
|
158
|
-
FROM mem_decisions
|
|
159
|
-
WHERE scope_id = ?
|
|
160
|
-
AND superseded_by IS NULL
|
|
161
|
-
AND archived_at IS NULL
|
|
162
|
-
ORDER BY id ASC
|
|
163
|
-
`).all(scopeId);
|
|
164
|
-
let modified = 0;
|
|
165
|
-
const update = db.prepare(`UPDATE mem_decisions SET superseded_by = ? WHERE id = ? AND superseded_by IS NULL`);
|
|
166
|
-
const tx = db.transaction(() => {
|
|
167
|
-
const visited = new Set();
|
|
168
|
-
for (const candidate of candidates) {
|
|
169
|
-
if (visited.has(candidate.id)) {
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
const cluster = candidates.filter((entry) => !visited.has(entry.id) && isSimilar(candidate.title, entry.title));
|
|
173
|
-
for (const entry of cluster) {
|
|
174
|
-
visited.add(entry.id);
|
|
175
|
-
}
|
|
176
|
-
if (cluster.length < 2) {
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
const [keeper] = cluster.sort(compareDecisionKeeper);
|
|
180
|
-
if (!keeper) {
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
for (const entry of cluster) {
|
|
184
|
-
if (entry.id === keeper.id) {
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
modified += update.run(keeper.id, entry.id).changes;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
tx();
|
|
192
|
-
return passSummary("dedupDecisionsPass", candidates.length, modified);
|
|
193
|
-
}
|
|
194
|
-
catch (error) {
|
|
195
|
-
return passSummary("dedupDecisionsPass", 0, 0, [error instanceof Error ? error.message : String(error)]);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
function resolveSupersedeTerminal(startId, firstTargetId, supersededBy) {
|
|
199
|
-
const seen = new Set([startId]);
|
|
200
|
-
let current = firstTargetId;
|
|
201
|
-
while (true) {
|
|
202
|
-
if (seen.has(current)) {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
seen.add(current);
|
|
206
|
-
const next = supersededBy.get(current);
|
|
207
|
-
if (next === undefined) {
|
|
208
|
-
return firstTargetId;
|
|
209
|
-
}
|
|
210
|
-
if (next === null) {
|
|
211
|
-
return current;
|
|
212
|
-
}
|
|
213
|
-
current = next;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
export function compactSupersedeChainsPass(scopeId) {
|
|
217
|
-
try {
|
|
218
|
-
const db = getDb();
|
|
219
|
-
const observationRows = db.prepare(`
|
|
220
|
-
SELECT id, superseded_by
|
|
221
|
-
FROM mem_observations
|
|
222
|
-
WHERE scope_id = ?
|
|
223
|
-
AND superseded_by IS NOT NULL
|
|
224
|
-
ORDER BY id ASC
|
|
225
|
-
`).all(scopeId);
|
|
226
|
-
const decisionRows = db.prepare(`
|
|
227
|
-
SELECT id, superseded_by
|
|
228
|
-
FROM mem_decisions
|
|
229
|
-
WHERE scope_id = ?
|
|
230
|
-
AND superseded_by IS NOT NULL
|
|
231
|
-
ORDER BY id ASC
|
|
232
|
-
`).all(scopeId);
|
|
233
|
-
let modified = 0;
|
|
234
|
-
const tx = db.transaction(() => {
|
|
235
|
-
for (const { table, rows } of [
|
|
236
|
-
{ table: "mem_observations", rows: observationRows },
|
|
237
|
-
{ table: "mem_decisions", rows: decisionRows },
|
|
238
|
-
]) {
|
|
239
|
-
const targetIds = [...new Set(rows.map((row) => row.superseded_by))];
|
|
240
|
-
const activeTargets = targetIds.length === 0
|
|
241
|
-
? undefined
|
|
242
|
-
: db.prepare(`
|
|
243
|
-
SELECT id, superseded_by
|
|
244
|
-
FROM ${table}
|
|
245
|
-
WHERE scope_id = ?
|
|
246
|
-
AND id IN (${targetIds.map(() => "?").join(",")})
|
|
247
|
-
`);
|
|
248
|
-
const targets = targetIds.length === 0
|
|
249
|
-
? []
|
|
250
|
-
: activeTargets.all(scopeId, ...targetIds);
|
|
251
|
-
const supersededBy = new Map();
|
|
252
|
-
for (const row of rows) {
|
|
253
|
-
supersededBy.set(row.id, row.superseded_by);
|
|
254
|
-
}
|
|
255
|
-
for (const target of targets) {
|
|
256
|
-
supersededBy.set(target.id, target.superseded_by);
|
|
257
|
-
}
|
|
258
|
-
const update = db.prepare(`UPDATE ${table} SET superseded_by = ? WHERE id = ? AND scope_id = ?`);
|
|
259
|
-
for (const row of rows) {
|
|
260
|
-
const terminal = resolveSupersedeTerminal(row.id, row.superseded_by, supersededBy);
|
|
261
|
-
if (terminal !== null && terminal !== row.superseded_by) {
|
|
262
|
-
modified += update.run(terminal, row.id, scopeId).changes;
|
|
263
|
-
supersededBy.set(row.id, terminal);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
tx();
|
|
269
|
-
return passSummary("compactSupersedeChainsPass", observationRows.length + decisionRows.length, modified);
|
|
270
|
-
}
|
|
271
|
-
catch (error) {
|
|
272
|
-
return passSummary("compactSupersedeChainsPass", 0, 0, [error instanceof Error ? error.message : String(error)]);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
export function orphanCleanupPass(scopeId) {
|
|
276
|
-
try {
|
|
277
|
-
const db = getDb();
|
|
278
|
-
const orphanIds = db.prepare(`
|
|
279
|
-
SELECT o.id
|
|
280
|
-
FROM mem_observations o
|
|
281
|
-
LEFT JOIN mem_entities e ON e.id = o.entity_id
|
|
282
|
-
WHERE o.scope_id = ?
|
|
283
|
-
AND o.entity_id IS NOT NULL
|
|
284
|
-
AND e.id IS NULL
|
|
285
|
-
ORDER BY o.id ASC
|
|
286
|
-
`).all(scopeId);
|
|
287
|
-
const tx = db.transaction(() => db.prepare(`
|
|
288
|
-
UPDATE mem_observations
|
|
289
|
-
SET entity_id = NULL
|
|
290
|
-
WHERE scope_id = ?
|
|
291
|
-
AND entity_id IS NOT NULL
|
|
292
|
-
AND NOT EXISTS (SELECT 1 FROM mem_entities e WHERE e.id = mem_observations.entity_id)
|
|
293
|
-
`).run(scopeId).changes);
|
|
294
|
-
const modified = tx();
|
|
295
|
-
if (modified > 0) {
|
|
296
|
-
log.info({ scope_id: scopeId, count: modified }, "memory.housekeeping.orphan_cleanup");
|
|
297
|
-
}
|
|
298
|
-
return passSummary("orphanCleanupPass", orphanIds.length, modified);
|
|
299
|
-
}
|
|
300
|
-
catch (error) {
|
|
301
|
-
return passSummary("orphanCleanupPass", 0, 0, [error instanceof Error ? error.message : String(error)]);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
export function decayPass(scopeId) {
|
|
305
|
-
try {
|
|
306
|
-
const db = getDb();
|
|
307
|
-
const candidateIds = db.prepare(`
|
|
308
|
-
SELECT id
|
|
309
|
-
FROM mem_observations
|
|
310
|
-
WHERE scope_id = ?
|
|
311
|
-
AND superseded_by IS NULL
|
|
312
|
-
AND archived_at IS NULL
|
|
313
|
-
AND confidence < 0.3
|
|
314
|
-
AND datetime(created_at) < datetime('now', ?)
|
|
315
|
-
ORDER BY id ASC
|
|
316
|
-
`).all(scopeId, `-${config.memoryDecayDays} days`);
|
|
317
|
-
const tx = db.transaction(() => db.prepare(`
|
|
318
|
-
UPDATE mem_observations
|
|
319
|
-
SET archived_at = CURRENT_TIMESTAMP
|
|
320
|
-
WHERE scope_id = ?
|
|
321
|
-
AND superseded_by IS NULL
|
|
322
|
-
AND archived_at IS NULL
|
|
323
|
-
AND confidence < 0.3
|
|
324
|
-
AND datetime(created_at) < datetime('now', ?)
|
|
325
|
-
`).run(scopeId, `-${config.memoryDecayDays} days`).changes);
|
|
326
|
-
const modified = tx();
|
|
327
|
-
return passSummary("decayPass", candidateIds.length, modified);
|
|
328
|
-
}
|
|
329
|
-
catch (error) {
|
|
330
|
-
return passSummary("decayPass", 0, 0, [error instanceof Error ? error.message : String(error)]);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
export function compactInboxPass() {
|
|
334
|
-
try {
|
|
335
|
-
const db = getDb();
|
|
336
|
-
const candidateIds = db.prepare(`
|
|
337
|
-
SELECT id
|
|
338
|
-
FROM mem_inbox
|
|
339
|
-
WHERE status IN ('accepted', 'rejected')
|
|
340
|
-
AND resolved_at IS NOT NULL
|
|
341
|
-
AND datetime(resolved_at) < datetime('now', ?)
|
|
342
|
-
ORDER BY id ASC
|
|
343
|
-
`).all(`-${config.memoryInboxRetentionDays} days`);
|
|
344
|
-
const tx = db.transaction(() => db.prepare(`
|
|
345
|
-
DELETE FROM mem_inbox
|
|
346
|
-
WHERE status IN ('accepted', 'rejected')
|
|
347
|
-
AND resolved_at IS NOT NULL
|
|
348
|
-
AND datetime(resolved_at) < datetime('now', ?)
|
|
349
|
-
`).run(`-${config.memoryInboxRetentionDays} days`).changes);
|
|
350
|
-
const modified = tx();
|
|
351
|
-
return passSummary("compactInboxPass", candidateIds.length, modified);
|
|
352
|
-
}
|
|
353
|
-
catch (error) {
|
|
354
|
-
return passSummary("compactInboxPass", 0, 0, [error instanceof Error ? error.message : String(error)]);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
function resolveScopeIds(input) {
|
|
358
|
-
if (input?.scopeIds && input.scopeIds.length > 0) {
|
|
359
|
-
return [...new Set(input.scopeIds)];
|
|
360
|
-
}
|
|
361
|
-
if (input?.allScopes) {
|
|
362
|
-
return listScopes().filter((scope) => scope.active).map((scope) => scope.id);
|
|
363
|
-
}
|
|
364
|
-
const activeScope = getActiveScope();
|
|
365
|
-
return activeScope ? [activeScope.id] : [];
|
|
366
|
-
}
|
|
367
|
-
function getInFlightScopes(pass) {
|
|
368
|
-
let scopes = inFlightScopesByPass.get(pass);
|
|
369
|
-
if (!scopes) {
|
|
370
|
-
scopes = new Set();
|
|
371
|
-
inFlightScopesByPass.set(pass, scopes);
|
|
372
|
-
}
|
|
373
|
-
return scopes;
|
|
374
|
-
}
|
|
375
|
-
function getReservedPassScopes(scopeIds, passes) {
|
|
376
|
-
const reserved = [];
|
|
377
|
-
for (const pass of passes) {
|
|
378
|
-
if (pass === "compact_inbox") {
|
|
379
|
-
reserved.push({ pass, scope: GLOBAL_PASS_SCOPE });
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
for (const scopeId of scopeIds) {
|
|
383
|
-
reserved.push({ pass, scope: scopeId });
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return reserved;
|
|
387
|
-
}
|
|
388
|
-
function reservePassScopes(reserved) {
|
|
389
|
-
if (reserved.some(({ pass, scope }) => getInFlightScopes(pass).has(scope))) {
|
|
390
|
-
return false;
|
|
391
|
-
}
|
|
392
|
-
for (const { pass, scope } of reserved) {
|
|
393
|
-
getInFlightScopes(pass).add(scope);
|
|
394
|
-
}
|
|
395
|
-
return true;
|
|
396
|
-
}
|
|
397
|
-
function releasePassScopes(reserved) {
|
|
398
|
-
for (const { pass, scope } of reserved) {
|
|
399
|
-
const scopes = inFlightScopesByPass.get(pass);
|
|
400
|
-
scopes?.delete(scope);
|
|
401
|
-
if (scopes && scopes.size === 0) {
|
|
402
|
-
inFlightScopesByPass.delete(pass);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
async function runPass(pass, scopeId) {
|
|
407
|
-
switch (pass) {
|
|
408
|
-
case "dedup_observations":
|
|
409
|
-
return await Promise.resolve(dedupObservationsPass(scopeId));
|
|
410
|
-
case "dedup_decisions":
|
|
411
|
-
return await Promise.resolve(dedupDecisionsPass(scopeId));
|
|
412
|
-
case "compact_supersede_chains":
|
|
413
|
-
return await Promise.resolve(compactSupersedeChainsPass(scopeId));
|
|
414
|
-
case "orphan_cleanup":
|
|
415
|
-
return await Promise.resolve(orphanCleanupPass(scopeId));
|
|
416
|
-
case "decay":
|
|
417
|
-
return await Promise.resolve(decayPass(scopeId));
|
|
418
|
-
case "compact_inbox":
|
|
419
|
-
return await Promise.resolve(compactInboxPass());
|
|
420
|
-
case "tiering":
|
|
421
|
-
return await Promise.resolve(tieringPass(scopeId));
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
async function runScopePasses(scopeId, passes) {
|
|
425
|
-
const summaries = [];
|
|
426
|
-
for (const pass of passes) {
|
|
427
|
-
summaries.push(await runPass(pass, scopeId));
|
|
428
|
-
}
|
|
429
|
-
return summaries;
|
|
430
|
-
}
|
|
431
|
-
export function isHousekeepingInFlight(scopeIds, passes) {
|
|
432
|
-
const normalizedPasses = passes?.map(normalizePassName) ?? PASS_ORDER;
|
|
433
|
-
if (!scopeIds || scopeIds.length === 0) {
|
|
434
|
-
return normalizedPasses.some((pass) => (inFlightScopesByPass.get(pass)?.size ?? 0) > 0);
|
|
435
|
-
}
|
|
436
|
-
const uniqueScopeIds = [...new Set(scopeIds)].sort((a, b) => a - b);
|
|
437
|
-
return normalizedPasses.some((pass) => {
|
|
438
|
-
const scopes = inFlightScopesByPass.get(pass);
|
|
439
|
-
if (!scopes) {
|
|
440
|
-
return false;
|
|
441
|
-
}
|
|
442
|
-
if (pass === "compact_inbox") {
|
|
443
|
-
return scopes.has(GLOBAL_PASS_SCOPE);
|
|
444
|
-
}
|
|
445
|
-
return uniqueScopeIds.some((scopeId) => scopes.has(scopeId));
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
export async function runHousekeeping(opts = {}) {
|
|
449
|
-
const started = performance.now();
|
|
450
|
-
const scopeIds = resolveScopeIds(opts).sort((a, b) => a - b);
|
|
451
|
-
const passes = opts.passes?.length ? opts.passes.map(normalizePassName) : PASS_ORDER;
|
|
452
|
-
const reservedPassScopes = getReservedPassScopes(scopeIds, passes);
|
|
453
|
-
if (!reservePassScopes(reservedPassScopes)) {
|
|
454
|
-
return {
|
|
455
|
-
scopeIds,
|
|
456
|
-
summaries: [passSummary("runHousekeeping", 0, 0, ["Housekeeping is already in flight for this scope/pass set."])],
|
|
457
|
-
totalExamined: 0,
|
|
458
|
-
totalModified: 0,
|
|
459
|
-
durationMs: 0,
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
const scopedPasses = passes.filter((pass) => pass !== "compact_inbox");
|
|
463
|
-
const lockedScopeIds = scopedPasses.length > 0 ? scopeIds : [];
|
|
464
|
-
if (lockedScopeIds.length > 0 && !tryAcquireScopeWriteLocks(lockedScopeIds)) {
|
|
465
|
-
releasePassScopes(reservedPassScopes);
|
|
466
|
-
return {
|
|
467
|
-
scopeIds,
|
|
468
|
-
summaries: [passSummary("runHousekeeping", 0, 0, ["Memory writes are already in flight for this scope."])],
|
|
469
|
-
totalExamined: 0,
|
|
470
|
-
totalModified: 0,
|
|
471
|
-
durationMs: 0,
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
try {
|
|
475
|
-
const hasCompactInbox = passes.includes("compact_inbox");
|
|
476
|
-
const summaries = (await Promise.all(scopeIds.map((scopeId) => runScopePasses(scopeId, scopedPasses)))).flat();
|
|
477
|
-
if (hasCompactInbox) {
|
|
478
|
-
summaries.push(await runPass("compact_inbox", undefined));
|
|
479
|
-
}
|
|
480
|
-
const totalExamined = summaries.reduce((sum, summary) => sum + summary.examined, 0);
|
|
481
|
-
const totalModified = summaries.reduce((sum, summary) => sum + summary.modified, 0);
|
|
482
|
-
const durationMs = Math.round(performance.now() - started);
|
|
483
|
-
log.info({
|
|
484
|
-
passes_run: summaries.map((summary) => summary.pass),
|
|
485
|
-
total_examined: totalExamined,
|
|
486
|
-
total_modified: totalModified,
|
|
487
|
-
duration_ms: durationMs,
|
|
488
|
-
scope_ids: scopeIds,
|
|
489
|
-
}, "memory.housekeeping.run");
|
|
490
|
-
return { scopeIds, summaries, totalExamined, totalModified, durationMs };
|
|
491
|
-
}
|
|
492
|
-
finally {
|
|
493
|
-
releaseScopeWriteLocks(lockedScopeIds);
|
|
494
|
-
releasePassScopes(reservedPassScopes);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
//# sourceMappingURL=housekeeping.js.map
|