@gamaze/hicortex 0.20.7 → 0.20.10
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/README.md +18 -41
- package/assets/dashboard.html +3989 -836
- package/dist/calibration.d.ts +293 -0
- package/dist/calibration.js +379 -0
- package/dist/capture-health.d.ts +87 -0
- package/dist/capture-health.js +106 -0
- package/dist/capture-pause.d.ts +86 -0
- package/dist/capture-pause.js +127 -0
- package/dist/capture.d.ts +24 -3
- package/dist/capture.js +11 -1
- package/dist/classify-domains.d.ts +6 -0
- package/dist/classify-domains.js +7 -1
- package/dist/cli.js +38 -3
- package/dist/config-read.d.ts +1 -1
- package/dist/config-read.js +96 -9
- package/dist/consolidate.d.ts +114 -68
- package/dist/consolidate.js +302 -182
- package/dist/dashboard.d.ts +326 -6
- package/dist/dashboard.js +592 -7
- package/dist/db.js +105 -0
- package/dist/dedup.d.ts +34 -26
- package/dist/dedup.js +91 -57
- package/dist/distiller.js +1 -1
- package/dist/domain-classify.d.ts +7 -6
- package/dist/domain-classify.js +12 -10
- package/dist/eval/decay-eval.d.ts +3 -3
- package/dist/eval/decay-eval.js +4 -4
- package/dist/eval/importance-eval.d.ts +85 -0
- package/dist/eval/importance-eval.js +286 -0
- package/dist/eval/planted-eval.d.ts +26 -0
- package/dist/eval/planted-eval.js +97 -0
- package/dist/eval/planted-fixtures.d.ts +107 -0
- package/dist/eval/planted-fixtures.js +283 -0
- package/dist/eval/planted-harness.d.ts +176 -0
- package/dist/eval/planted-harness.js +649 -0
- package/dist/eval/ranking-battery.d.ts +78 -0
- package/dist/eval/ranking-battery.js +181 -0
- package/dist/eval/ranking-eval.d.ts +41 -0
- package/dist/eval/ranking-eval.js +391 -0
- package/dist/eval/ranking-fixtures.d.ts +77 -0
- package/dist/eval/ranking-fixtures.js +226 -0
- package/dist/identity-store.d.ts +21 -0
- package/dist/identity-store.js +49 -0
- package/dist/index.js +4 -3
- package/dist/init.d.ts +23 -3
- package/dist/init.js +84 -9
- package/dist/llm.d.ts +43 -58
- package/dist/llm.js +87 -101
- package/dist/mcp-server.d.ts +12 -0
- package/dist/mcp-server.js +213 -32
- package/dist/nightly.d.ts +9 -1
- package/dist/nightly.js +164 -110
- package/dist/nofit.d.ts +4 -11
- package/dist/nofit.js +6 -23
- package/dist/prompts.d.ts +10 -0
- package/dist/prompts.js +28 -5
- package/dist/recall-index.d.ts +30 -28
- package/dist/recall-index.js +21 -18
- package/dist/recall-registry.d.ts +2 -1
- package/dist/recall-registry.js +35 -1
- package/dist/reconsolidation.d.ts +168 -87
- package/dist/reconsolidation.js +818 -377
- package/dist/relink.js +3 -4
- package/dist/rescore-importance.d.ts +80 -0
- package/dist/rescore-importance.js +236 -0
- package/dist/retrieval.d.ts +80 -35
- package/dist/retrieval.js +322 -105
- package/dist/run-deadline.d.ts +62 -0
- package/dist/run-deadline.js +73 -0
- package/dist/schema-prototypes.d.ts +3 -3
- package/dist/schema-prototypes.js +3 -3
- package/dist/stages.d.ts +37 -0
- package/dist/stages.js +51 -0
- package/dist/state.d.ts +34 -9
- package/dist/storage.d.ts +50 -18
- package/dist/storage.js +125 -30
- package/dist/telemetry.d.ts +8 -7
- package/dist/token-budget.js +3 -4
- package/dist/type-classify.js +4 -4
- package/dist/types.d.ts +143 -155
- package/domains.example.json +4 -5
- package/hermes-plugin/hicortex/README.md +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -1
- package/pi-extension/hicortex/README.md +1 -1
- package/server.json +3 -3
package/dist/consolidate.js
CHANGED
|
@@ -38,10 +38,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
};
|
|
39
39
|
})();
|
|
40
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
-
exports.DEFAULT_MEMORY_SOFT_CAP = exports.
|
|
41
|
+
exports.DEFAULT_MEMORY_SOFT_CAP = exports.DEFAULT_SUPERSESSION_MIN_SIMILARITY = exports.BudgetTracker = exports.REFLECTION_CONTRADICTION_MIN_COSINE = exports.l2ToCosine = exports.CROSS_PROJECT_LINK_THRESHOLD = exports.CONSOLIDATE_LINK_TOP_K = exports.CONSOLIDATE_LINK_THRESHOLD = exports.DEFAULT_NIGHTLY_LLM_CALL_BUDGET = void 0;
|
|
42
|
+
exports.resolveNightlyLlmCallBudget = resolveNightlyLlmCallBudget;
|
|
42
43
|
exports.isContradictionCandidate = isContradictionCandidate;
|
|
44
|
+
exports.warnUnmeteredTokensRun = warnUnmeteredTokensRun;
|
|
45
|
+
exports.isStaleTokenPeriod = isStaleTokenPeriod;
|
|
43
46
|
exports.shouldThrottleTokens = shouldThrottleTokens;
|
|
44
47
|
exports.parseJsonLenient = parseJsonLenient;
|
|
48
|
+
exports.scoreMemoriesImportance = scoreMemoriesImportance;
|
|
45
49
|
exports.rebuildContentModuleIndex = rebuildContentModuleIndex;
|
|
46
50
|
exports.discoverLinkCandidates = discoverLinkCandidates;
|
|
47
51
|
exports.classifyLinkCandidates = classifyLinkCandidates;
|
|
@@ -53,8 +57,6 @@ exports.stageDecayPrune = stageDecayPrune;
|
|
|
53
57
|
exports.resolveMemorySoftCap = resolveMemorySoftCap;
|
|
54
58
|
exports.stageMemoryCapEviction = stageMemoryCapEviction;
|
|
55
59
|
exports.runConsolidation = runConsolidation;
|
|
56
|
-
exports.msUntilHour = msUntilHour;
|
|
57
|
-
exports.scheduleConsolidation = scheduleConsolidation;
|
|
58
60
|
const retrieval_js_1 = require("./retrieval.js");
|
|
59
61
|
const storage = __importStar(require("./storage.js"));
|
|
60
62
|
const config_read_js_1 = require("./config-read.js");
|
|
@@ -68,19 +70,41 @@ const schema_prototypes_js_1 = require("./schema-prototypes.js");
|
|
|
68
70
|
const nofit_js_1 = require("./nofit.js");
|
|
69
71
|
const reconsolidation_js_1 = require("./reconsolidation.js");
|
|
70
72
|
const dedup_js_1 = require("./dedup.js");
|
|
73
|
+
const CALIBRATION = __importStar(require("./calibration.js"));
|
|
71
74
|
// Default config constants (matching Python config.py)
|
|
72
75
|
/**
|
|
73
|
-
* Default ceiling on
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
76
|
+
* Default ceiling on LLM calls across the WHOLE nightly pipeline (#405; the
|
|
77
|
+
* #241 consolidateMaxLlmCalls mechanism, renamed and widened). A runaway
|
|
78
|
+
* BACKSTOP that bounds money/load INDEPENDENT OF LATENCY — a fast metered or
|
|
79
|
+
* capacity-limited endpoint permits thousands of calls inside the wall-clock
|
|
80
|
+
* budget, so time alone cannot protect it (owner ruling 2026-09-12). Consumed
|
|
81
|
+
* in run order: a stage that exhausts it defers its remainder via its cursor.
|
|
82
|
+
* 5000 clears a one-time classification backlog (a ~2000-memory batch drains
|
|
83
|
+
* in ~1-2 runs) with margin. Config: `nightlyLlmCallBudget` (#405); the old
|
|
84
|
+
* `consolidateMaxLlmCalls` key is a deprecated alias honored one release.
|
|
82
85
|
*/
|
|
83
|
-
exports.
|
|
86
|
+
exports.DEFAULT_NIGHTLY_LLM_CALL_BUDGET = 5000;
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the per-run LLM call budget from config (#405):
|
|
89
|
+
* - `nightlyLlmCallBudget` present (positive finite) → it wins;
|
|
90
|
+
* - else `consolidateMaxLlmCalls` present → used as a DEPRECATED ALIAS with
|
|
91
|
+
* a warn naming the replacement (honored one release);
|
|
92
|
+
* - absent/invalid → the 5000 default.
|
|
93
|
+
*/
|
|
94
|
+
function resolveNightlyLlmCallBudget(config) {
|
|
95
|
+
const c = config ?? {};
|
|
96
|
+
if (c.nightlyLlmCallBudget !== undefined) {
|
|
97
|
+
return (0, config_read_js_1.readPositiveConfig)(c, "nightlyLlmCallBudget", exports.DEFAULT_NIGHTLY_LLM_CALL_BUDGET);
|
|
98
|
+
}
|
|
99
|
+
if (c.consolidateMaxLlmCalls !== undefined) {
|
|
100
|
+
const legacy = (0, config_read_js_1.readPositiveConfig)(c, "consolidateMaxLlmCalls", exports.DEFAULT_NIGHTLY_LLM_CALL_BUDGET);
|
|
101
|
+
console.warn(`[hicortex] config key "consolidateMaxLlmCalls" is deprecated — renamed ` +
|
|
102
|
+
`"nightlyLlmCallBudget" (same meaning, now the ONE per-run LLM call ceiling). ` +
|
|
103
|
+
`The old key is honored for one release; rename it to clear this warning.`);
|
|
104
|
+
return legacy;
|
|
105
|
+
}
|
|
106
|
+
return exports.DEFAULT_NIGHTLY_LLM_CALL_BUDGET;
|
|
107
|
+
}
|
|
84
108
|
const CONSOLIDATE_PRUNE_MIN_AGE_DAYS = 90;
|
|
85
109
|
/**
|
|
86
110
|
* Minimum COSINE similarity for a link candidate.
|
|
@@ -133,14 +157,12 @@ class BudgetTracker {
|
|
|
133
157
|
callsByStage = {};
|
|
134
158
|
/**
|
|
135
159
|
* Per-stage count of LLM-call REQUESTS refused because the budget was
|
|
136
|
-
* exhausted (#255). Keys are the same stage labels passed to `use()
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* the boundary. For item-level skip counts (how many memories or pairs were
|
|
143
|
-
* left unprocessed), see the per-stage reports — e.g.
|
|
160
|
+
* exhausted (#255). Keys are the same stage labels passed to `use()`; each
|
|
161
|
+
* refused call adds 1 (the dead batch `count` param is gone — #405 —
|
|
162
|
+
* production always passed 1 anyway). Stages break on the first refusal,
|
|
163
|
+
* so a stage's value is the count of requests that crossed the boundary.
|
|
164
|
+
* For item-level skip counts (how many memories or pairs were left
|
|
165
|
+
* unprocessed), see the per-stage reports — e.g.
|
|
144
166
|
* `stages.importance.skipped_budget` — which count MEMORIES, not call
|
|
145
167
|
* requests. Surfaced in summary() and ConsolidationReport as
|
|
146
168
|
* `deferred_by_stage`.
|
|
@@ -168,21 +190,21 @@ class BudgetTracker {
|
|
|
168
190
|
get remaining() {
|
|
169
191
|
return Math.max(0, this.maxCalls - this.callsUsed);
|
|
170
192
|
}
|
|
171
|
-
use(stage
|
|
172
|
-
if (this.callsUsed
|
|
193
|
+
use(stage) {
|
|
194
|
+
if (this.callsUsed >= this.maxCalls) {
|
|
173
195
|
// #255: emit as a STRUCTURED event (not a bare prose warn) so a monitor
|
|
174
196
|
// can grep/parse `event=budget_exhausted` from journald. The line stays
|
|
175
197
|
// human-readable (key=value tokens after the [hicortex] prefix). Deferred
|
|
176
198
|
// counts are accrued BEFORE the log so the line reflects the up-to-date
|
|
177
199
|
// per-stage toll — the refused count is added to this stage's slot.
|
|
178
|
-
this.deferredByStage[stage] = (this.deferredByStage[stage] ?? 0) +
|
|
200
|
+
this.deferredByStage[stage] = (this.deferredByStage[stage] ?? 0) + 1;
|
|
179
201
|
console.warn(`[hicortex] event=budget_exhausted stage=${stage} ` +
|
|
180
202
|
`calls_used=${this.callsUsed} max_calls=${this.maxCalls} ` +
|
|
181
203
|
`deferred_by_stage=${JSON.stringify(this.deferredByStage)}`);
|
|
182
204
|
return false;
|
|
183
205
|
}
|
|
184
|
-
this.callsUsed +=
|
|
185
|
-
this.callsByStage[stage] = (this.callsByStage[stage] ?? 0) +
|
|
206
|
+
this.callsUsed += 1;
|
|
207
|
+
this.callsByStage[stage] = (this.callsByStage[stage] ?? 0) + 1;
|
|
186
208
|
return true;
|
|
187
209
|
}
|
|
188
210
|
/**
|
|
@@ -220,9 +242,44 @@ class BudgetTracker {
|
|
|
220
242
|
}
|
|
221
243
|
}
|
|
222
244
|
exports.BudgetTracker = BudgetTracker;
|
|
245
|
+
/**
|
|
246
|
+
* #427 observability: warn when a consolidation run made LLM CALLS but
|
|
247
|
+
* metered ZERO tokens — the endpoint returned no usage objects on its
|
|
248
|
+
* completions (recordUsage skips undefined by design, never fabricates a
|
|
249
|
+
* zero). Such a run still spends budget calls but its snapshot carries token
|
|
250
|
+
* nulls, which read as a mystery on the dashboard. The warn is a structured
|
|
251
|
+
* event in the same journald-greppable style as `event=budget_exhausted`
|
|
252
|
+
* (grep `event=tokens_unmetered`), so the blind spot is visible instead of
|
|
253
|
+
* silent. Returns true when it warned (for tests); no fabrication either
|
|
254
|
+
* way — the numbers stay exactly what the endpoint reported.
|
|
255
|
+
*/
|
|
256
|
+
function warnUnmeteredTokensRun(budget) {
|
|
257
|
+
const calls = budget.calls_used ?? 0;
|
|
258
|
+
const tokens = budget.tokens_total?.total ?? 0;
|
|
259
|
+
if (calls <= 0 || tokens > 0)
|
|
260
|
+
return false;
|
|
261
|
+
console.warn(`[hicortex] event=tokens_unmetered calls_used=${calls} — the LLM endpoint ` +
|
|
262
|
+
`returned no usage objects on its completions; this run's snapshot carries ` +
|
|
263
|
+
`no token metering (budget calls were still counted).`);
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
223
266
|
// ---------------------------------------------------------------------------
|
|
224
267
|
// Token fair-use throttle decision (#246)
|
|
225
268
|
// ---------------------------------------------------------------------------
|
|
269
|
+
/**
|
|
270
|
+
* True when a token-period start stamp is ABSENT or sits in a previous UTC
|
|
271
|
+
* calendar month than `now` — the monthly-reset staleness check. #405: ONE
|
|
272
|
+
* shared helper — the check was triplicated (the nightly's throttle branch,
|
|
273
|
+
* the nightly's accrual write, token-budget.ts recordDistillUsage) and each
|
|
274
|
+
* copy re-derived the year+month comparison by hand.
|
|
275
|
+
*/
|
|
276
|
+
function isStaleTokenPeriod(periodStart, now = new Date()) {
|
|
277
|
+
if (!periodStart)
|
|
278
|
+
return true;
|
|
279
|
+
const start = new Date(periodStart);
|
|
280
|
+
return start.getUTCFullYear() !== now.getUTCFullYear() ||
|
|
281
|
+
start.getUTCMonth() !== now.getUTCMonth();
|
|
282
|
+
}
|
|
226
283
|
/**
|
|
227
284
|
* Decide whether consolidation should be throttled this run based on the
|
|
228
285
|
* `llmTokensPerMonth` fair-use cap. Pure (no I/O) so it can be unit-tested
|
|
@@ -235,22 +292,14 @@ exports.BudgetTracker = BudgetTracker;
|
|
|
235
292
|
*
|
|
236
293
|
* `cap = 0` (the self-hosted default) → never throttle (unlimited).
|
|
237
294
|
* `periodStart` in a previous calendar month → period resets to 0 first
|
|
238
|
-
* (
|
|
239
|
-
* the same state + clock).
|
|
295
|
+
* (isStaleTokenPeriod — the same helper every monthly-reset site uses, so
|
|
296
|
+
* the sides agree because they read the same state + clock).
|
|
240
297
|
*/
|
|
241
298
|
function shouldThrottleTokens(cap, period, lastRunTokens, now = new Date()) {
|
|
242
299
|
if (cap <= 0)
|
|
243
300
|
return { throttle: false };
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
if (periodStart) {
|
|
247
|
-
const start = new Date(periodStart);
|
|
248
|
-
if (start.getUTCFullYear() !== now.getUTCFullYear() ||
|
|
249
|
-
start.getUTCMonth() !== now.getUTCMonth()) {
|
|
250
|
-
// Stale period → reset accrual to 0 before the check.
|
|
251
|
-
periodTotal = 0;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
301
|
+
// Stale period → reset accrual to 0 before the check.
|
|
302
|
+
const periodTotal = isStaleTokenPeriod(period?.periodStart, now) ? 0 : (period?.total ?? 0);
|
|
254
303
|
if (periodTotal + lastRunTokens > cap) {
|
|
255
304
|
return { throttle: true, used: periodTotal, cap };
|
|
256
305
|
}
|
|
@@ -325,29 +374,50 @@ function stagePrecheck(db, stateDir) {
|
|
|
325
374
|
// ---------------------------------------------------------------------------
|
|
326
375
|
// Stage 2: Importance Scoring
|
|
327
376
|
// ---------------------------------------------------------------------------
|
|
328
|
-
|
|
377
|
+
/**
|
|
378
|
+
* The shared importance-scoring loop (#425 extraction): one LLM call per
|
|
379
|
+
* 10-memory batch through the production `importanceScoring` prompt, each
|
|
380
|
+
* written score clamped at IMPORTANCE_CEILING and stamped with the
|
|
381
|
+
* importance_scored_at watermark. Used by the nightly's stageImportance AND
|
|
382
|
+
* `hicortex rescore-importance` — there is exactly one scoring code path
|
|
383
|
+
* (no forked backfill logic; cap + watermark write identically everywhere).
|
|
384
|
+
*
|
|
385
|
+
* Failure semantics: a batch whose LLM call THROWS writes nothing (counted
|
|
386
|
+
* in `failed` — retried naturally later); a batch whose reply parses to a
|
|
387
|
+
* non-array falls back to 0.5 per memory (written + watermarked — the
|
|
388
|
+
* endpoint answered, the answer was unusable).
|
|
389
|
+
*/
|
|
390
|
+
async function scoreMemoriesImportance(db, memories, llm, opts = {}) {
|
|
329
391
|
const batchSize = 10;
|
|
392
|
+
const budget = opts.budget;
|
|
393
|
+
const deadline = opts.deadline;
|
|
394
|
+
const dryRun = opts.dryRun ?? false;
|
|
330
395
|
let scored = 0;
|
|
331
396
|
let failed = 0;
|
|
332
397
|
let skippedBudget = 0;
|
|
333
398
|
for (let i = 0; i < memories.length; i += batchSize) {
|
|
334
|
-
if (budget
|
|
399
|
+
if (budget?.exhausted) {
|
|
335
400
|
skippedBudget += memories.length - i;
|
|
336
401
|
break;
|
|
337
402
|
}
|
|
403
|
+
// #405: the run deadline bounds the batch loop too — a large unscored
|
|
404
|
+
// backlog must not blow the whole run's wall-clock inside one stage.
|
|
405
|
+
// Same stage label as the boundary check, so the defer log fires once.
|
|
406
|
+
if (deadline?.hit("importance"))
|
|
407
|
+
break;
|
|
338
408
|
const batch = memories.slice(i, i + batchSize);
|
|
339
409
|
const lines = batch.map((mem, idx) => `[${idx}] ${mem.content.slice(0, 500)}`);
|
|
340
410
|
const memoriesBlock = lines.join("\n\n");
|
|
341
411
|
const prompt = (0, prompts_js_1.importanceScoring)(memoriesBlock);
|
|
342
412
|
if (dryRun)
|
|
343
413
|
continue;
|
|
344
|
-
if (!budget.use("importance")) {
|
|
414
|
+
if (budget && !budget.use("importance")) {
|
|
345
415
|
skippedBudget += memories.length - i;
|
|
346
416
|
break;
|
|
347
417
|
}
|
|
348
418
|
try {
|
|
349
|
-
const r = await llm.
|
|
350
|
-
budget
|
|
419
|
+
const r = await llm.complete(prompt);
|
|
420
|
+
budget?.recordUsage("importance", r.usage);
|
|
351
421
|
let scores = parseJsonLenient(r.text, null);
|
|
352
422
|
if (!Array.isArray(scores)) {
|
|
353
423
|
scores = new Array(batch.length).fill(0.5);
|
|
@@ -355,6 +425,8 @@ async function stageImportance(db, memories, llm, budget, dryRun) {
|
|
|
355
425
|
while (scores.length < batch.length)
|
|
356
426
|
scores.push(0.5);
|
|
357
427
|
scores = scores.slice(0, batch.length);
|
|
428
|
+
let batchWritten = 0;
|
|
429
|
+
let batchFailed = 0;
|
|
358
430
|
for (let j = 0; j < batch.length; j++) {
|
|
359
431
|
let scoreVal = 0.5;
|
|
360
432
|
try {
|
|
@@ -365,21 +437,37 @@ async function stageImportance(db, memories, llm, budget, dryRun) {
|
|
|
365
437
|
catch {
|
|
366
438
|
scoreVal = 0.5;
|
|
367
439
|
}
|
|
440
|
+
// #425: the write cap — importance exactly 1.0 has decay rate exactly
|
|
441
|
+
// 1.0 and never decays, so no row is ever born immortal. The scored-at
|
|
442
|
+
// watermark lands in the SAME update, taking the row out of the
|
|
443
|
+
// nightly's unscored pool however it scored (the pre-#425 0.5-sentinel
|
|
444
|
+
// re-rolled genuinely-0.5 rows every night).
|
|
445
|
+
scoreVal = Math.min(scoreVal, CALIBRATION.IMPORTANCE_CEILING);
|
|
368
446
|
try {
|
|
369
|
-
storage.updateMemory(db, batch[j].id, {
|
|
447
|
+
storage.updateMemory(db, batch[j].id, {
|
|
448
|
+
base_strength: scoreVal,
|
|
449
|
+
importance_scored_at: new Date().toISOString(),
|
|
450
|
+
});
|
|
370
451
|
scored++;
|
|
452
|
+
batchWritten++;
|
|
371
453
|
}
|
|
372
454
|
catch {
|
|
373
455
|
failed++;
|
|
456
|
+
batchFailed++;
|
|
374
457
|
}
|
|
375
458
|
}
|
|
459
|
+
opts.onBatch?.(batchWritten, batchFailed);
|
|
376
460
|
}
|
|
377
461
|
catch {
|
|
378
462
|
failed += batch.length;
|
|
463
|
+
opts.onBatch?.(0, batch.length);
|
|
379
464
|
}
|
|
380
465
|
}
|
|
381
466
|
return { scored, failed, skipped_budget: skippedBudget };
|
|
382
467
|
}
|
|
468
|
+
async function stageImportance(db, memories, llm, budget, dryRun, deadline) {
|
|
469
|
+
return scoreMemoriesImportance(db, memories, llm, { budget, deadline, dryRun });
|
|
470
|
+
}
|
|
383
471
|
// ---------------------------------------------------------------------------
|
|
384
472
|
// Stage 2.5: Reflection
|
|
385
473
|
// ---------------------------------------------------------------------------
|
|
@@ -408,7 +496,7 @@ async function stageReflection(db, memories, llm, budget, embedFn, dryRun) {
|
|
|
408
496
|
return { lessons_generated: 0, skipped: true, reason: "budget_exhausted" };
|
|
409
497
|
}
|
|
410
498
|
try {
|
|
411
|
-
const r = await llm.
|
|
499
|
+
const r = await llm.complete(prompt);
|
|
412
500
|
budget.recordUsage("reflection", r.usage);
|
|
413
501
|
const lessons = parseJsonLenient(r.text, []);
|
|
414
502
|
if (!Array.isArray(lessons)) {
|
|
@@ -458,9 +546,9 @@ async function stageReflection(db, memories, llm, budget, embedFn, dryRun) {
|
|
|
458
546
|
const existingText = similarLessons[0].content.slice(0, 300);
|
|
459
547
|
const newText = content.slice(0, 300);
|
|
460
548
|
try {
|
|
461
|
-
const verdictR = await llm.
|
|
549
|
+
const verdictR = await llm.complete(`Two lessons from an AI memory system. Do they CONTRADICT each other (opposite advice on the same topic)?\n\n` +
|
|
462
550
|
`EXISTING: ${existingText}\n\nNEW: ${newText}\n\n` +
|
|
463
|
-
`Answer ONLY "yes" or "no". If the new lesson updates/refines the existing one (not contradicts), answer "no"
|
|
551
|
+
`Answer ONLY "yes" or "no". If the new lesson updates/refines the existing one (not contradicts), answer "no".`);
|
|
464
552
|
// Stage label "contradiction_check" matches the budget.use() call
|
|
465
553
|
// above (separate counter from the reflection call proper). Token
|
|
466
554
|
// accounting follows the same stage partition as the call counter.
|
|
@@ -543,7 +631,7 @@ function rebuildContentModuleIndex(db, domains, stateDir) {
|
|
|
543
631
|
(0, state_js_1.updateState)((s) => { s.moduleIndex = moduleIndex; }, stateDir);
|
|
544
632
|
return { domains: moduleDomains.length };
|
|
545
633
|
}
|
|
546
|
-
async function stageContentDomains(db, domains, llm, budget, embedFn, dryRun, stateDir, weakPrimaryFloor = nofit_js_1.DEFAULT_WEAK_PRIMARY_FLOOR) {
|
|
634
|
+
async function stageContentDomains(db, domains, llm, budget, embedFn, dryRun, stateDir, weakPrimaryFloor = nofit_js_1.DEFAULT_WEAK_PRIMARY_FLOOR, deadline) {
|
|
547
635
|
// Rows needing (re)classification:
|
|
548
636
|
// - domain IS NULL (never classified), OR
|
|
549
637
|
// - domain NOT IN the current vocabulary (a rename/removal re-files), OR
|
|
@@ -569,6 +657,11 @@ async function stageContentDomains(db, domains, llm, budget, embedFn, dryRun, st
|
|
|
569
657
|
// refreshes everything from the updated tag sets anyway).
|
|
570
658
|
const { prototypes: startPrototypes } = await (0, schema_prototypes_js_1.computeDomainPrototypes)(db, domains, getEmbedFn);
|
|
571
659
|
for (const row of rows) {
|
|
660
|
+
// #405: the run deadline bounds the classification row loop (a large
|
|
661
|
+
// backlog must defer, not blow the wall-clock). Same stage label as the
|
|
662
|
+
// boundary check, so the defer log fires once.
|
|
663
|
+
if (deadline?.hit("domain_curation"))
|
|
664
|
+
break;
|
|
572
665
|
if (budget.exhausted || !budget.use("content_domain")) {
|
|
573
666
|
console.warn(`[hicortex] content-domain: budget exhausted after ${classified} classified`);
|
|
574
667
|
break;
|
|
@@ -716,7 +809,7 @@ async function stageDomainCuration(db, llm, budget, dryRun, stateDir) {
|
|
|
716
809
|
.map((r) => `${r.project}: ${r.cnt} / ${lessonsByProject.get(r.project) ?? 0}`)
|
|
717
810
|
.join("\n");
|
|
718
811
|
try {
|
|
719
|
-
const r = await llm.
|
|
812
|
+
const r = await llm.complete((0, prompts_js_1.domainCuration)(projectLines));
|
|
720
813
|
budget.recordUsage("domain_curation", r.usage);
|
|
721
814
|
const parsed = parseJsonLenient(r.text, []);
|
|
722
815
|
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
@@ -847,19 +940,16 @@ function discoverLinkCandidates(db, mem, embedding) {
|
|
|
847
940
|
* 672-link audit (see the Stage 3 header) found the LLM-classified UPPERCASE
|
|
848
941
|
* types near-useless (CONTRADICTS 4% acceptable). Every candidate now takes its
|
|
849
942
|
* pre-computed `heuristicType` (only `extends` or `relates_to` — see
|
|
850
|
-
* classifyRelationship). No LLM call is made.
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
* is unchanged; `llmClassified` is always 0 now and `heuristicFallback` counts
|
|
856
|
-
* every candidate. Do NOT re-add an LLM path here without a classifier that
|
|
857
|
-
* passes the audit harness at >= 70% acceptable.
|
|
943
|
+
* classifyRelationship). No LLM call is made. #405: the ignored `llm`/`budget`
|
|
944
|
+
* params are deleted — the signature now tells the truth.
|
|
945
|
+
* The return shape is unchanged; `llmClassified` is always 0 and
|
|
946
|
+
* `heuristicFallback` counts every candidate. Do NOT re-add an LLM path here
|
|
947
|
+
* without a classifier that passes the audit harness at >= 70% acceptable.
|
|
858
948
|
*
|
|
859
949
|
* Shared between the nightly `stageLinks` and `hicortex relink`.
|
|
860
950
|
* Returns one relationship type per candidate (same order as input).
|
|
861
951
|
*/
|
|
862
|
-
async function classifyLinkCandidates(candidates
|
|
952
|
+
async function classifyLinkCandidates(candidates) {
|
|
863
953
|
const types = candidates.map((c) => c.heuristicType);
|
|
864
954
|
return { types, llmClassified: 0, heuristicFallback: candidates.length };
|
|
865
955
|
}
|
|
@@ -880,8 +970,9 @@ async function stageLinks(db, memories, embedFn, dryRun, llm, budget) {
|
|
|
880
970
|
if (candidates.length === 0) {
|
|
881
971
|
return { auto_linked: 0, llm_classified: 0, heuristic_fallback: 0, failed };
|
|
882
972
|
}
|
|
883
|
-
// Phase B:
|
|
884
|
-
|
|
973
|
+
// Phase B: heuristic-only classification (LLM retired; #405 dropped the
|
|
974
|
+
// dead llm/budget params)
|
|
975
|
+
const { types: classifiedTypes, llmClassified, heuristicFallback } = await classifyLinkCandidates(candidates);
|
|
885
976
|
// Phase C: Store all classified links
|
|
886
977
|
for (let i = 0; i < candidates.length; i++) {
|
|
887
978
|
const c = candidates[i];
|
|
@@ -930,7 +1021,9 @@ function classifyRelationship(source, target, similarity) {
|
|
|
930
1021
|
// Stage 3.5: Hub Detection & Strength Boost
|
|
931
1022
|
// ---------------------------------------------------------------------------
|
|
932
1023
|
const HUB_BOOST = 0.1;
|
|
933
|
-
|
|
1024
|
+
// #425: the release-managed importance ceiling (calibration.ts) — hub boosts
|
|
1025
|
+
// can no longer push a row to base 1.0 (decay rate exactly 1.0 = immortal).
|
|
1026
|
+
const HUB_STRENGTH_CAP = CALIBRATION.IMPORTANCE_CEILING;
|
|
934
1027
|
function stageHubBoost(db, dryRun) {
|
|
935
1028
|
const hubs = (0, graph_js_1.detectHubs)(db);
|
|
936
1029
|
if (hubs.length === 0)
|
|
@@ -968,26 +1061,17 @@ function stageHubBoost(db, dryRun) {
|
|
|
968
1061
|
// this is a judgment call about content, not a duplicate).
|
|
969
1062
|
//
|
|
970
1063
|
// Scope: memories with `rowid > supersessionCursor` (state.json; starts 0 —
|
|
971
|
-
// the corpus is back-processed gradually
|
|
972
|
-
//
|
|
973
|
-
//
|
|
974
|
-
//
|
|
975
|
-
//
|
|
976
|
-
//
|
|
977
|
-
//
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
* 0 = no separate cap — supersession shares the consolidation budget
|
|
983
|
-
* (CONSOLIDATE_MAX_LLM_CALLS, default 5000) like every other stage. The old
|
|
984
|
-
* default of 30 was set when the corpus had 14 decisions; with the distiller
|
|
985
|
-
* now classifying types correctly (#216), decisions are common and the cap
|
|
986
|
-
* was throttling supersession to a crawl. On a local free model there is no
|
|
987
|
-
* per-call cost to defend against — the binding constraint is the wall-clock
|
|
988
|
-
* timeout (TimeoutStartSec), not call count.
|
|
989
|
-
*/
|
|
990
|
-
exports.DEFAULT_SUPERSESSION_MAX_CALLS = 0;
|
|
1064
|
+
// the corpus is back-processed gradually) whose shape suggests a
|
|
1065
|
+
// decision/correction. For each, KNN top-5 OLDER same-shape neighbors
|
|
1066
|
+
// at/above the release-managed similarity floor (calibration.ts); one constrained classify-tier LLM
|
|
1067
|
+
// call per pair decides `superseded: true| false`. A parse/infra error skips
|
|
1068
|
+
// just that PAIR (retried naturally next night since the cursor still
|
|
1069
|
+
// advances past the memory — see the cursor note below); it never mis-links.
|
|
1070
|
+
// #405: no per-stage call cap — the ONE run budget (nightlyLlmCallBudget)
|
|
1071
|
+
// and the run deadline are the only bounds, like every other stage.
|
|
1072
|
+
/** Default minimum COSINE similarity for a supersession candidate pair —
|
|
1073
|
+
* RELEASE-MANAGED since #408 (calibration.ts SUPERSESSION_MIN_SIMILARITY). */
|
|
1074
|
+
exports.DEFAULT_SUPERSESSION_MIN_SIMILARITY = CALIBRATION.SUPERSESSION_MIN_SIMILARITY;
|
|
991
1075
|
/** Default multiplier applied to a superseded memory's base_strength. */
|
|
992
1076
|
/** Floor under which a superseded memory's base_strength never drops. */
|
|
993
1077
|
/** Neighbor pool size before shape/older/similarity filtering narrows to top 5. */
|
|
@@ -1067,7 +1151,7 @@ function parseSupersessionReply(reply) {
|
|
|
1067
1151
|
*/
|
|
1068
1152
|
async function classifySupersession(llm, oldContent, newContent) {
|
|
1069
1153
|
try {
|
|
1070
|
-
const r = await llm.
|
|
1154
|
+
const r = await llm.complete(buildSupersessionPrompt(oldContent, newContent));
|
|
1071
1155
|
return { verdict: parseSupersessionReply(r.text), usage: r.usage };
|
|
1072
1156
|
}
|
|
1073
1157
|
catch {
|
|
@@ -1107,6 +1191,13 @@ async function findOlderNeighbors(db, candidate, embedFn, minSimilarity) {
|
|
|
1107
1191
|
* candidacy. It only stops SHORT of a candidate when the budget is already
|
|
1108
1192
|
* exhausted before that candidate starts, so the cursor never skips a
|
|
1109
1193
|
* candidate that was never looked at.
|
|
1194
|
+
*
|
|
1195
|
+
* #405: the cursor persists after EVERY fully-considered candidate (the
|
|
1196
|
+
* post-#404 reconsolidation pattern), not at stage end — a run killed or
|
|
1197
|
+
* deadline-deferred mid-stage loses at most the candidate in flight. No
|
|
1198
|
+
* orphan clamp is needed (unlike reconsolidation): supersession applies each
|
|
1199
|
+
* verdict's link immediately, so `cursor = candidate.__rowid` always sits
|
|
1200
|
+
* after all of that candidate's writes.
|
|
1110
1201
|
*/
|
|
1111
1202
|
async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, options = {}) {
|
|
1112
1203
|
// Config values pass through `unknown`-typed JSON — validate rather than
|
|
@@ -1116,7 +1207,6 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
1116
1207
|
return Number.isFinite(n) && ok(n) ? n : fallback;
|
|
1117
1208
|
};
|
|
1118
1209
|
const minSimilarity = validNumber(options.minSimilarity, exports.DEFAULT_SUPERSESSION_MIN_SIMILARITY, (n) => n > 0 && n <= 1);
|
|
1119
|
-
const maxCalls = validNumber(options.maxCalls, exports.DEFAULT_SUPERSESSION_MAX_CALLS, (n) => n >= 0);
|
|
1120
1210
|
const startCursor = (0, state_js_1.loadState)(stateDir).supersessionCursor ?? 0;
|
|
1121
1211
|
const rows = db
|
|
1122
1212
|
.prepare(
|
|
@@ -1136,10 +1226,26 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
1136
1226
|
let superseded = 0;
|
|
1137
1227
|
let skippedInfra = 0;
|
|
1138
1228
|
let skippedIdempotent = 0;
|
|
1139
|
-
let callsUsed = 0;
|
|
1140
1229
|
let cursor = startCursor;
|
|
1230
|
+
// #405: per-candidate checkpoint — persists the cursor after every fully
|
|
1231
|
+
// considered candidate (updateState is an atomic temp-rename of a small
|
|
1232
|
+
// file; the loop cadence is seconds per candidate, so the cost is
|
|
1233
|
+
// negligible). The end-of-stage write below stays the authoritative final
|
|
1234
|
+
// write.
|
|
1235
|
+
const persistCursor = () => {
|
|
1236
|
+
if (dryRun)
|
|
1237
|
+
return;
|
|
1238
|
+
(0, state_js_1.updateState)((s) => {
|
|
1239
|
+
s.supersessionCursor = cursor;
|
|
1240
|
+
}, stateDir);
|
|
1241
|
+
};
|
|
1141
1242
|
for (const candidate of rows) {
|
|
1142
|
-
|
|
1243
|
+
// #405: the ONE run budget is the only call cap; the deadline stops the
|
|
1244
|
+
// scan at the candidate boundary — the cursor holds at the last
|
|
1245
|
+
// fully-considered candidate (persisted below).
|
|
1246
|
+
if (!dryRun && budget.exhausted)
|
|
1247
|
+
break;
|
|
1248
|
+
if (!dryRun && options.deadline?.hit("supersession"))
|
|
1143
1249
|
break;
|
|
1144
1250
|
scanned++;
|
|
1145
1251
|
let neighbors;
|
|
@@ -1149,6 +1255,7 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
1149
1255
|
catch (err) {
|
|
1150
1256
|
console.warn(`[hicortex] supersession: discovery failed for ${candidate.id.slice(0, 8)} — ${err instanceof Error ? err.message : String(err)}`);
|
|
1151
1257
|
cursor = candidate.__rowid;
|
|
1258
|
+
persistCursor(); // #405: every exit path persists
|
|
1152
1259
|
continue;
|
|
1153
1260
|
}
|
|
1154
1261
|
for (const neighbor of neighbors) {
|
|
@@ -1158,9 +1265,8 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
1158
1265
|
}
|
|
1159
1266
|
if (dryRun)
|
|
1160
1267
|
continue; // preview only — no LLM call, no write
|
|
1161
|
-
if (
|
|
1162
|
-
break;
|
|
1163
|
-
callsUsed++;
|
|
1268
|
+
if (!budget.use("supersession"))
|
|
1269
|
+
break; // #405: the ONE run budget
|
|
1164
1270
|
const { verdict, usage } = await classifySupersession(llm, neighbor.content, candidate.content);
|
|
1165
1271
|
// Meter every round-tripped attempt (#246) — even a null verdict spent
|
|
1166
1272
|
// real tokens. The stage label matches the budget.use() above.
|
|
@@ -1184,6 +1290,10 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
1184
1290
|
}
|
|
1185
1291
|
}
|
|
1186
1292
|
cursor = candidate.__rowid;
|
|
1293
|
+
// #405: checkpoint after every fully-considered candidate (post-#404
|
|
1294
|
+
// reconsolidation pattern) — a killed or deadline-deferred run loses at
|
|
1295
|
+
// most the candidate in flight.
|
|
1296
|
+
persistCursor();
|
|
1187
1297
|
}
|
|
1188
1298
|
if (!dryRun) {
|
|
1189
1299
|
(0, state_js_1.updateState)((s) => {
|
|
@@ -1311,7 +1421,15 @@ function stageMemoryCapEviction(db, dryRun, cap) {
|
|
|
1311
1421
|
// rejects negatives at the boundary, but this stage is callable directly).
|
|
1312
1422
|
if (cap <= 0)
|
|
1313
1423
|
return { cap, evicted: 0 };
|
|
1314
|
-
|
|
1424
|
+
// #422 (#317 discipline): the cap keys off LIVE (non-absorbed) rows on BOTH
|
|
1425
|
+
// the count and the victim SELECT — absorbed rows are invisible evidence
|
|
1426
|
+
// (no vector, no FTS, recall never serves them); they must neither consume
|
|
1427
|
+
// cap headroom nor be picked as eviction victims. The DISPLAYED headroom
|
|
1428
|
+
// (dashboard.ts headline live_memories vs memory_soft_cap) reads the same
|
|
1429
|
+
// predicate, so the enforced and displayed caps cannot disagree.
|
|
1430
|
+
const count = db
|
|
1431
|
+
.prepare("SELECT COUNT(*) AS c FROM memories WHERE COALESCE(status, '') != 'absorbed'")
|
|
1432
|
+
.get().c;
|
|
1315
1433
|
if (count <= cap)
|
|
1316
1434
|
return { cap, evicted: 0 };
|
|
1317
1435
|
const surplus = count - cap;
|
|
@@ -1319,10 +1437,12 @@ function stageMemoryCapEviction(db, dryRun, cap) {
|
|
|
1319
1437
|
// NOT NULL after scoring; the `?? 0.5` mirrors stageDecayPrune's defensive
|
|
1320
1438
|
// default for unscored rows (inserts at 0.5). last_accessed is NULL until
|
|
1321
1439
|
// first /recall-index exposure — COALESCE to created_at for the tiebreak so
|
|
1322
|
-
// never-shown memories sort by when they entered the corpus.
|
|
1440
|
+
// never-shown memories sort by when they entered the corpus. Same
|
|
1441
|
+
// non-absorbed predicate as the count above.
|
|
1323
1442
|
const rows = db
|
|
1324
1443
|
.prepare(`SELECT id, base_strength, last_accessed, access_count, created_at
|
|
1325
|
-
FROM memories
|
|
1444
|
+
FROM memories
|
|
1445
|
+
WHERE COALESCE(status, '') != 'absorbed'`)
|
|
1326
1446
|
.all();
|
|
1327
1447
|
const linkCounts = storage.getAllLinkCounts(db);
|
|
1328
1448
|
const now = new Date();
|
|
@@ -1376,21 +1496,23 @@ async function skippedRunResolutionReport(db, dryRun, stateDir, options = {}) {
|
|
|
1376
1496
|
return Number.isFinite(n) && ok(n) ? n : fallback;
|
|
1377
1497
|
};
|
|
1378
1498
|
const autoMergeThreshold = validNumber(options.autoMergeThreshold, dedup_js_1.DEFAULT_DEDUP_MERGE_THRESHOLD, (n) => n > 0 && n <= 1);
|
|
1379
|
-
const maxMerges = validNumber(options.maxMerges, dedup_js_1.DEFAULT_DEDUP_NIGHTLY_MAX_MERGES, (n) => n >= 0);
|
|
1380
1499
|
const merges = await (0, dedup_js_1.runDeterministicMergeZone)(db, {
|
|
1381
1500
|
stateDir,
|
|
1382
1501
|
threshold: autoMergeThreshold,
|
|
1383
|
-
maxMerges,
|
|
1384
1502
|
dryRun,
|
|
1385
1503
|
acquireLock: options.acquireLock,
|
|
1504
|
+
deadline: options.deadline,
|
|
1386
1505
|
});
|
|
1387
1506
|
const bandStats = {};
|
|
1388
|
-
|
|
1507
|
+
{
|
|
1508
|
+
// #405: recorded whenever the zone ran (the old max_merges>0 gate was a
|
|
1509
|
+
// 0=disabled switch — the switch is gone).
|
|
1389
1510
|
bandStats[`>=${autoMergeThreshold}`] = {
|
|
1390
1511
|
pairs: merges.losers_merged,
|
|
1391
1512
|
merge: merges.losers_merged,
|
|
1392
1513
|
corrects: 0,
|
|
1393
1514
|
supersedes: 0,
|
|
1515
|
+
conflicts: 0,
|
|
1394
1516
|
none: 0,
|
|
1395
1517
|
merge_below_gate: 0,
|
|
1396
1518
|
conf_sum: merges.losers_merged,
|
|
@@ -1416,29 +1538,52 @@ async function skippedRunResolutionReport(db, dryRun, stateDir, options = {}) {
|
|
|
1416
1538
|
explicit_verified: 0,
|
|
1417
1539
|
explicit_divergent: 0,
|
|
1418
1540
|
cursor: (0, state_js_1.loadState)(stateDir).reconsolidationCursor ?? 0,
|
|
1541
|
+
// #439 fields: zeros on a quiet night (no scan ran — nothing re-judged,
|
|
1542
|
+
// new, skipped, or deferred; the type carries them so the report surface
|
|
1543
|
+
// stays uniform).
|
|
1544
|
+
pairs_reevaluated: 0,
|
|
1545
|
+
pairs_new: 0,
|
|
1546
|
+
skipped_absorbed: 0,
|
|
1547
|
+
merge_pairs_deferred: 0,
|
|
1419
1548
|
merges,
|
|
1420
1549
|
merge_pairs_applied: 0,
|
|
1421
1550
|
merge_below_gate: 0,
|
|
1422
1551
|
skipped_above_ceiling: 0,
|
|
1423
1552
|
skipped_metadata_mismatch: 0,
|
|
1553
|
+
conflict_flagged: 0, // guard-C: no scan on a quiet night — nothing flagged
|
|
1554
|
+
conflict_skipped: merges.skipped_conflict, // guard-C: the zone's guard still counts
|
|
1555
|
+
scout_scanned: 0, // #393 B: the scan (and its shape calls) doesn't run on a quiet night
|
|
1556
|
+
scout_correction_shaped: 0,
|
|
1557
|
+
scout_candidates_found: 0,
|
|
1424
1558
|
band_stats: bandStats,
|
|
1425
1559
|
};
|
|
1426
1560
|
}
|
|
1427
1561
|
async function runConsolidation(db, llm, embedFn, dryRun = false, skipReflection = false, stateDir, domainOptions, supersessionOptions,
|
|
1428
|
-
/**
|
|
1429
|
-
*
|
|
1430
|
-
*
|
|
1562
|
+
/** The ONE per-run LLM-call ceiling (#405/#241). The caller resolves
|
|
1563
|
+
* `nightlyLlmCallBudget` from config (consolidateMaxLlmCalls is a
|
|
1564
|
+
* deprecated alias — resolveNightlyLlmCallBudget) and passes it; unset →
|
|
1565
|
+
* the exported DEFAULT_NIGHTLY_LLM_CALL_BUDGET (5000). */
|
|
1431
1566
|
budgetMaxCalls,
|
|
1432
1567
|
/** Soft cap on the corpus (#245). Nightly.ts reads `memorySoftCap` from
|
|
1433
1568
|
* config and passes it; unset → `DEFAULT_MEMORY_SOFT_CAP` (10000). `0`
|
|
1434
1569
|
* disables eviction (indefinite growth). */
|
|
1435
1570
|
memorySoftCap,
|
|
1436
|
-
/** Reconsolidation-stage knobs (#384)
|
|
1437
|
-
*
|
|
1438
|
-
* supersessionOptions above; unset fields → the
|
|
1439
|
-
* Appended AFTER the pre-#384 params so
|
|
1440
|
-
* (tests, hosted nightly) keeps its
|
|
1441
|
-
|
|
1571
|
+
/** Reconsolidation-stage knobs (#384) — eval/test seams since #408 (the
|
|
1572
|
+
* values are release-managed calibration constants; nightly.ts threads
|
|
1573
|
+
* NOTHING), exactly like supersessionOptions above; unset fields → the
|
|
1574
|
+
* stage's calibration defaults. Appended AFTER the pre-#384 params so
|
|
1575
|
+
* every existing positional caller (tests, hosted nightly) keeps its
|
|
1576
|
+
* argument meaning. */
|
|
1577
|
+
reconsolidationOptions,
|
|
1578
|
+
/**
|
|
1579
|
+
* The run-wide pipeline deadline (#405), created at nightly start and
|
|
1580
|
+
* shared by capture + every consolidation stage. Absent = no deadline
|
|
1581
|
+
* (tests, evict-only paths, pre-#405 callers). When it fires, every
|
|
1582
|
+
* not-yet-run stage defers (logs event=deadline_deferred stage=<name>) and
|
|
1583
|
+
* the report status becomes "deferred" — which keeps lastConsolidated
|
|
1584
|
+
* un-advanced so the next run re-finds the pending work.
|
|
1585
|
+
*/
|
|
1586
|
+
deadline) {
|
|
1442
1587
|
const start = new Date();
|
|
1443
1588
|
const report = {
|
|
1444
1589
|
started_at: start.toISOString(),
|
|
@@ -1483,20 +1628,31 @@ reconsolidationOptions) {
|
|
|
1483
1628
|
// stage report (telemetry's "skipped = zero LLM work" stays true), and
|
|
1484
1629
|
// the zone never runs twice: the main path runs it INSIDE the stage, this
|
|
1485
1630
|
// skip path returns before that.
|
|
1486
|
-
report.stages.reconsolidation = await skippedRunResolutionReport(db, dryRun, stateDir, reconsolidationOptions);
|
|
1631
|
+
report.stages.reconsolidation = await skippedRunResolutionReport(db, dryRun, stateDir, { ...reconsolidationOptions, deadline });
|
|
1487
1632
|
report.status = "skipped";
|
|
1488
1633
|
report.completed_at = new Date().toISOString();
|
|
1489
1634
|
return report;
|
|
1490
1635
|
}
|
|
1491
|
-
//
|
|
1492
|
-
//
|
|
1493
|
-
//
|
|
1494
|
-
const budget = new BudgetTracker(budgetMaxCalls ?? exports.
|
|
1636
|
+
// #405: the ONE per-run LLM-call ceiling (default 5000). The caller
|
|
1637
|
+
// resolves `nightlyLlmCallBudget` from config (consolidateMaxLlmCalls is a
|
|
1638
|
+
// deprecated alias — see resolveNightlyLlmCallBudget) and passes it here.
|
|
1639
|
+
const budget = new BudgetTracker(budgetMaxCalls ?? exports.DEFAULT_NIGHTLY_LLM_CALL_BUDGET);
|
|
1640
|
+
console.log(`[hicortex] Consolidation LLM call budget: ${budget.maxCalls} calls`);
|
|
1495
1641
|
try {
|
|
1642
|
+
// #405 stage gating: each boundary checks the run deadline; a hit defers
|
|
1643
|
+
// that stage (absent from the report — it did not run) and logs
|
|
1644
|
+
// event=deadline_deferred stage=<name> once. Later boundaries check
|
|
1645
|
+
// independently, so a mid-run deadline reports every remaining stage as
|
|
1646
|
+
// deferred. Deferred stages drain next run (cursors hold below them).
|
|
1496
1647
|
// Stage 2: Importance Scoring
|
|
1497
|
-
|
|
1648
|
+
if (!deadline?.hit("importance")) {
|
|
1649
|
+
report.stages.importance = await stageImportance(db, scoreMemories, llm, budget, dryRun, deadline);
|
|
1650
|
+
}
|
|
1498
1651
|
// Stage 2.5: Reflection
|
|
1499
|
-
if (
|
|
1652
|
+
if (deadline?.hit("reflection")) {
|
|
1653
|
+
// deferred — stage absent from the report
|
|
1654
|
+
}
|
|
1655
|
+
else if (skipReflection) {
|
|
1500
1656
|
report.stages.reflection = {
|
|
1501
1657
|
lessons_generated: 0,
|
|
1502
1658
|
skipped: true,
|
|
@@ -1511,40 +1667,60 @@ reconsolidationOptions) {
|
|
|
1511
1667
|
// domain list is configured. The single model serves all phases; if it's
|
|
1512
1668
|
// down, the phase skips and retries on the next nightly run (no fallback).
|
|
1513
1669
|
const cfgDomains = domainOptions?.domains;
|
|
1514
|
-
if (
|
|
1515
|
-
if (
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1670
|
+
if (!deadline?.hit("domain_curation")) {
|
|
1671
|
+
if (cfgDomains && cfgDomains.length > 0) {
|
|
1672
|
+
if (domainOptions?.contentDomainsReady === false) {
|
|
1673
|
+
report.stages.domain_curation = {
|
|
1674
|
+
curated: false,
|
|
1675
|
+
domains: cfgDomains.length,
|
|
1676
|
+
reason: "reflect_endpoint_offline",
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
else {
|
|
1680
|
+
report.stages.domain_curation = await stageContentDomains(db, cfgDomains, llm, budget, embedFn, dryRun, stateDir, domainOptions?.weakPrimaryFloor ?? nofit_js_1.DEFAULT_WEAK_PRIMARY_FLOOR, deadline);
|
|
1681
|
+
}
|
|
1521
1682
|
}
|
|
1522
1683
|
else {
|
|
1523
|
-
report.stages.domain_curation = await
|
|
1684
|
+
report.stages.domain_curation = await stageDomainCuration(db, llm, budget, dryRun, stateDir);
|
|
1524
1685
|
}
|
|
1525
1686
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1687
|
+
// Stage 3: Link Discovery (heuristic edge classification — local work,
|
|
1688
|
+
// but bounded by the same deadline as every other stage).
|
|
1689
|
+
if (!deadline?.hit("links")) {
|
|
1690
|
+
report.stages.links = await stageLinks(db, precheck.newMemories, embedFn, dryRun, llm, budget);
|
|
1528
1691
|
}
|
|
1529
|
-
// Stage 3: Link Discovery (with LLM-assisted edge classification)
|
|
1530
|
-
report.stages.links = await stageLinks(db, precheck.newMemories, embedFn, dryRun, llm, budget);
|
|
1531
1692
|
// Stage 3.5: Hub Detection — boost highly-connected memories
|
|
1532
|
-
|
|
1693
|
+
if (!deadline?.hit("hub_boost")) {
|
|
1694
|
+
report.stages.hub_boost = stageHubBoost(db, dryRun);
|
|
1695
|
+
}
|
|
1533
1696
|
// Stage 3.7: Supersession Detection (#191 Phase B)
|
|
1534
|
-
|
|
1697
|
+
if (!deadline?.hit("supersession")) {
|
|
1698
|
+
report.stages.supersession = await stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, { ...supersessionOptions, deadline });
|
|
1699
|
+
}
|
|
1535
1700
|
// Stage 3.8: Reconsolidation (#384) — resolve corrections: rewrite
|
|
1536
1701
|
// fact-shaped targets in place (absorbing transition-only triggers),
|
|
1537
1702
|
// mark everything else. Rides the same shared budget under its own stage
|
|
1538
1703
|
// label + cursor (supersession-stage pattern).
|
|
1539
|
-
|
|
1704
|
+
if (!deadline?.hit("reconsolidation")) {
|
|
1705
|
+
report.stages.reconsolidation = await (0, reconsolidation_js_1.stageReconsolidation)(db, llm, budget, embedFn, dryRun, stateDir, { ...reconsolidationOptions, deadline });
|
|
1706
|
+
}
|
|
1540
1707
|
// Stage 4: Decay & Prune
|
|
1541
|
-
|
|
1708
|
+
if (!deadline?.hit("decay_prune")) {
|
|
1709
|
+
report.stages.decay_prune = stageDecayPrune(db, dryRun);
|
|
1710
|
+
}
|
|
1542
1711
|
// (Memory cap eviction moved before the precheck skip — see above.)
|
|
1543
1712
|
}
|
|
1544
1713
|
catch (err) {
|
|
1545
1714
|
report.status = "failed";
|
|
1546
1715
|
console.error("[hicortex] Consolidation pipeline error:", err);
|
|
1547
1716
|
}
|
|
1717
|
+
// #405: any deferred stage ⇒ the run is "deferred", not "completed" — the
|
|
1718
|
+
// lastConsolidated gate below then holds the watermark so the next run's
|
|
1719
|
+
// pending-set queries re-find the deferred work (mirrors endpoint_down).
|
|
1720
|
+
// A thrown error still wins ("failed" is the more specific outcome).
|
|
1721
|
+
if (report.status === "completed" && deadline && deadline.deferredStages().length > 0) {
|
|
1722
|
+
report.status = "deferred";
|
|
1723
|
+
}
|
|
1548
1724
|
// Update last-consolidated timestamp. #357: the stages fail SOFT, so a run
|
|
1549
1725
|
// against an endpoint that died mid-way still reports status "completed"
|
|
1550
1726
|
// here — advancing the timestamp made state.json disagree with the
|
|
@@ -1568,59 +1744,3 @@ reconsolidationOptions) {
|
|
|
1568
1744
|
Math.round((Date.now() - start.getTime()) / 100) / 10;
|
|
1569
1745
|
return report;
|
|
1570
1746
|
}
|
|
1571
|
-
// ---------------------------------------------------------------------------
|
|
1572
|
-
// Scheduling
|
|
1573
|
-
// ---------------------------------------------------------------------------
|
|
1574
|
-
/**
|
|
1575
|
-
* Calculate milliseconds until the next occurrence of a given hour (local time).
|
|
1576
|
-
*/
|
|
1577
|
-
function msUntilHour(hour) {
|
|
1578
|
-
const now = new Date();
|
|
1579
|
-
const target = new Date(now);
|
|
1580
|
-
target.setHours(hour, 30, 0, 0); // :30 past the hour
|
|
1581
|
-
if (target.getTime() <= now.getTime()) {
|
|
1582
|
-
// Already passed today, schedule for tomorrow
|
|
1583
|
-
target.setDate(target.getDate() + 1);
|
|
1584
|
-
}
|
|
1585
|
-
return target.getTime() - now.getTime();
|
|
1586
|
-
}
|
|
1587
|
-
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
1588
|
-
/**
|
|
1589
|
-
* Schedule the consolidation pipeline to run nightly.
|
|
1590
|
-
* Returns a cleanup function to cancel the timer.
|
|
1591
|
-
*
|
|
1592
|
-
* NOTE: currently unused (nightly.ts drives consolidation directly). Any future
|
|
1593
|
-
* caller MUST read config.domains and thread `domainOptions` into runConsolidation
|
|
1594
|
-
* when content domains are configured — otherwise it silently falls back to the
|
|
1595
|
-
* legacy project-grouping path even when a domain list is set.
|
|
1596
|
-
*/
|
|
1597
|
-
function scheduleConsolidation(db, llm, embedFn, hour = 2) {
|
|
1598
|
-
let timeout = null;
|
|
1599
|
-
let interval = null;
|
|
1600
|
-
const runAndScheduleInterval = () => {
|
|
1601
|
-
runConsolidation(db, llm, embedFn)
|
|
1602
|
-
.then((report) => {
|
|
1603
|
-
console.log(`[hicortex] Consolidation ${report.status} in ${report.elapsed_seconds}s`);
|
|
1604
|
-
})
|
|
1605
|
-
.catch((err) => {
|
|
1606
|
-
console.error("[hicortex] Consolidation failed:", err);
|
|
1607
|
-
});
|
|
1608
|
-
// Schedule recurring daily runs
|
|
1609
|
-
if (!interval) {
|
|
1610
|
-
interval = setInterval(() => {
|
|
1611
|
-
runConsolidation(db, llm, embedFn).catch((err) => {
|
|
1612
|
-
console.error("[hicortex] Consolidation failed:", err);
|
|
1613
|
-
});
|
|
1614
|
-
}, ONE_DAY_MS);
|
|
1615
|
-
}
|
|
1616
|
-
};
|
|
1617
|
-
const delay = msUntilHour(hour);
|
|
1618
|
-
console.log(`[hicortex] Consolidation scheduled in ${Math.round(delay / 60_000)} minutes`);
|
|
1619
|
-
timeout = setTimeout(runAndScheduleInterval, delay);
|
|
1620
|
-
return () => {
|
|
1621
|
-
if (timeout)
|
|
1622
|
-
clearTimeout(timeout);
|
|
1623
|
-
if (interval)
|
|
1624
|
-
clearInterval(interval);
|
|
1625
|
-
};
|
|
1626
|
-
}
|