@gamaze/hicortex 0.20.7 → 0.20.9
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 +10 -41
- package/dist/calibration.d.ts +174 -0
- package/dist/calibration.js +231 -0
- package/dist/capture.d.ts +15 -3
- package/dist/capture.js +10 -1
- package/dist/classify-domains.d.ts +6 -0
- package/dist/classify-domains.js +7 -1
- package/dist/cli.js +2 -3
- package/dist/config-read.d.ts +1 -1
- package/dist/config-read.js +96 -9
- package/dist/consolidate.d.ts +79 -68
- package/dist/consolidate.js +218 -174
- package/dist/dashboard.d.ts +4 -3
- 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/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/index.js +4 -3
- package/dist/init.d.ts +9 -3
- package/dist/init.js +52 -9
- package/dist/llm.d.ts +43 -58
- package/dist/llm.js +87 -101
- package/dist/mcp-server.js +29 -29
- package/dist/nightly.js +105 -103
- package/dist/nofit.d.ts +4 -11
- package/dist/nofit.js +6 -23
- 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 +124 -72
- package/dist/reconsolidation.js +359 -148
- package/dist/relink.js +3 -4
- package/dist/retrieval.d.ts +68 -35
- package/dist/retrieval.js +292 -104
- 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/state.d.ts +2 -3
- package/dist/storage.d.ts +16 -16
- package/dist/storage.js +62 -24
- 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 +95 -155
- package/domains.example.json +4 -5
- package/hermes-plugin/hicortex/README.md +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/pi-extension/hicortex/README.md +1 -1
- package/server.json +3 -3
package/dist/consolidate.js
CHANGED
|
@@ -38,8 +38,10 @@ 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.isStaleTokenPeriod = isStaleTokenPeriod;
|
|
43
45
|
exports.shouldThrottleTokens = shouldThrottleTokens;
|
|
44
46
|
exports.parseJsonLenient = parseJsonLenient;
|
|
45
47
|
exports.rebuildContentModuleIndex = rebuildContentModuleIndex;
|
|
@@ -53,8 +55,6 @@ exports.stageDecayPrune = stageDecayPrune;
|
|
|
53
55
|
exports.resolveMemorySoftCap = resolveMemorySoftCap;
|
|
54
56
|
exports.stageMemoryCapEviction = stageMemoryCapEviction;
|
|
55
57
|
exports.runConsolidation = runConsolidation;
|
|
56
|
-
exports.msUntilHour = msUntilHour;
|
|
57
|
-
exports.scheduleConsolidation = scheduleConsolidation;
|
|
58
58
|
const retrieval_js_1 = require("./retrieval.js");
|
|
59
59
|
const storage = __importStar(require("./storage.js"));
|
|
60
60
|
const config_read_js_1 = require("./config-read.js");
|
|
@@ -68,19 +68,41 @@ const schema_prototypes_js_1 = require("./schema-prototypes.js");
|
|
|
68
68
|
const nofit_js_1 = require("./nofit.js");
|
|
69
69
|
const reconsolidation_js_1 = require("./reconsolidation.js");
|
|
70
70
|
const dedup_js_1 = require("./dedup.js");
|
|
71
|
+
const CALIBRATION = __importStar(require("./calibration.js"));
|
|
71
72
|
// Default config constants (matching Python config.py)
|
|
72
73
|
/**
|
|
73
|
-
* Default ceiling on
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
74
|
+
* Default ceiling on LLM calls across the WHOLE nightly pipeline (#405; the
|
|
75
|
+
* #241 consolidateMaxLlmCalls mechanism, renamed and widened). A runaway
|
|
76
|
+
* BACKSTOP that bounds money/load INDEPENDENT OF LATENCY — a fast metered or
|
|
77
|
+
* capacity-limited endpoint permits thousands of calls inside the wall-clock
|
|
78
|
+
* budget, so time alone cannot protect it (owner ruling 2026-09-12). Consumed
|
|
79
|
+
* in run order: a stage that exhausts it defers its remainder via its cursor.
|
|
80
|
+
* 5000 clears a one-time classification backlog (a ~2000-memory batch drains
|
|
81
|
+
* in ~1-2 runs) with margin. Config: `nightlyLlmCallBudget` (#405); the old
|
|
82
|
+
* `consolidateMaxLlmCalls` key is a deprecated alias honored one release.
|
|
82
83
|
*/
|
|
83
|
-
exports.
|
|
84
|
+
exports.DEFAULT_NIGHTLY_LLM_CALL_BUDGET = 5000;
|
|
85
|
+
/**
|
|
86
|
+
* Resolve the per-run LLM call budget from config (#405):
|
|
87
|
+
* - `nightlyLlmCallBudget` present (positive finite) → it wins;
|
|
88
|
+
* - else `consolidateMaxLlmCalls` present → used as a DEPRECATED ALIAS with
|
|
89
|
+
* a warn naming the replacement (honored one release);
|
|
90
|
+
* - absent/invalid → the 5000 default.
|
|
91
|
+
*/
|
|
92
|
+
function resolveNightlyLlmCallBudget(config) {
|
|
93
|
+
const c = config ?? {};
|
|
94
|
+
if (c.nightlyLlmCallBudget !== undefined) {
|
|
95
|
+
return (0, config_read_js_1.readPositiveConfig)(c, "nightlyLlmCallBudget", exports.DEFAULT_NIGHTLY_LLM_CALL_BUDGET);
|
|
96
|
+
}
|
|
97
|
+
if (c.consolidateMaxLlmCalls !== undefined) {
|
|
98
|
+
const legacy = (0, config_read_js_1.readPositiveConfig)(c, "consolidateMaxLlmCalls", exports.DEFAULT_NIGHTLY_LLM_CALL_BUDGET);
|
|
99
|
+
console.warn(`[hicortex] config key "consolidateMaxLlmCalls" is deprecated — renamed ` +
|
|
100
|
+
`"nightlyLlmCallBudget" (same meaning, now the ONE per-run LLM call ceiling). ` +
|
|
101
|
+
`The old key is honored for one release; rename it to clear this warning.`);
|
|
102
|
+
return legacy;
|
|
103
|
+
}
|
|
104
|
+
return exports.DEFAULT_NIGHTLY_LLM_CALL_BUDGET;
|
|
105
|
+
}
|
|
84
106
|
const CONSOLIDATE_PRUNE_MIN_AGE_DAYS = 90;
|
|
85
107
|
/**
|
|
86
108
|
* Minimum COSINE similarity for a link candidate.
|
|
@@ -133,14 +155,12 @@ class BudgetTracker {
|
|
|
133
155
|
callsByStage = {};
|
|
134
156
|
/**
|
|
135
157
|
* 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.
|
|
158
|
+
* exhausted (#255). Keys are the same stage labels passed to `use()`; each
|
|
159
|
+
* refused call adds 1 (the dead batch `count` param is gone — #405 —
|
|
160
|
+
* production always passed 1 anyway). Stages break on the first refusal,
|
|
161
|
+
* so a stage's value is the count of requests that crossed the boundary.
|
|
162
|
+
* For item-level skip counts (how many memories or pairs were left
|
|
163
|
+
* unprocessed), see the per-stage reports — e.g.
|
|
144
164
|
* `stages.importance.skipped_budget` — which count MEMORIES, not call
|
|
145
165
|
* requests. Surfaced in summary() and ConsolidationReport as
|
|
146
166
|
* `deferred_by_stage`.
|
|
@@ -168,21 +188,21 @@ class BudgetTracker {
|
|
|
168
188
|
get remaining() {
|
|
169
189
|
return Math.max(0, this.maxCalls - this.callsUsed);
|
|
170
190
|
}
|
|
171
|
-
use(stage
|
|
172
|
-
if (this.callsUsed
|
|
191
|
+
use(stage) {
|
|
192
|
+
if (this.callsUsed >= this.maxCalls) {
|
|
173
193
|
// #255: emit as a STRUCTURED event (not a bare prose warn) so a monitor
|
|
174
194
|
// can grep/parse `event=budget_exhausted` from journald. The line stays
|
|
175
195
|
// human-readable (key=value tokens after the [hicortex] prefix). Deferred
|
|
176
196
|
// counts are accrued BEFORE the log so the line reflects the up-to-date
|
|
177
197
|
// per-stage toll — the refused count is added to this stage's slot.
|
|
178
|
-
this.deferredByStage[stage] = (this.deferredByStage[stage] ?? 0) +
|
|
198
|
+
this.deferredByStage[stage] = (this.deferredByStage[stage] ?? 0) + 1;
|
|
179
199
|
console.warn(`[hicortex] event=budget_exhausted stage=${stage} ` +
|
|
180
200
|
`calls_used=${this.callsUsed} max_calls=${this.maxCalls} ` +
|
|
181
201
|
`deferred_by_stage=${JSON.stringify(this.deferredByStage)}`);
|
|
182
202
|
return false;
|
|
183
203
|
}
|
|
184
|
-
this.callsUsed +=
|
|
185
|
-
this.callsByStage[stage] = (this.callsByStage[stage] ?? 0) +
|
|
204
|
+
this.callsUsed += 1;
|
|
205
|
+
this.callsByStage[stage] = (this.callsByStage[stage] ?? 0) + 1;
|
|
186
206
|
return true;
|
|
187
207
|
}
|
|
188
208
|
/**
|
|
@@ -223,6 +243,20 @@ exports.BudgetTracker = BudgetTracker;
|
|
|
223
243
|
// ---------------------------------------------------------------------------
|
|
224
244
|
// Token fair-use throttle decision (#246)
|
|
225
245
|
// ---------------------------------------------------------------------------
|
|
246
|
+
/**
|
|
247
|
+
* True when a token-period start stamp is ABSENT or sits in a previous UTC
|
|
248
|
+
* calendar month than `now` — the monthly-reset staleness check. #405: ONE
|
|
249
|
+
* shared helper — the check was triplicated (the nightly's throttle branch,
|
|
250
|
+
* the nightly's accrual write, token-budget.ts recordDistillUsage) and each
|
|
251
|
+
* copy re-derived the year+month comparison by hand.
|
|
252
|
+
*/
|
|
253
|
+
function isStaleTokenPeriod(periodStart, now = new Date()) {
|
|
254
|
+
if (!periodStart)
|
|
255
|
+
return true;
|
|
256
|
+
const start = new Date(periodStart);
|
|
257
|
+
return start.getUTCFullYear() !== now.getUTCFullYear() ||
|
|
258
|
+
start.getUTCMonth() !== now.getUTCMonth();
|
|
259
|
+
}
|
|
226
260
|
/**
|
|
227
261
|
* Decide whether consolidation should be throttled this run based on the
|
|
228
262
|
* `llmTokensPerMonth` fair-use cap. Pure (no I/O) so it can be unit-tested
|
|
@@ -235,22 +269,14 @@ exports.BudgetTracker = BudgetTracker;
|
|
|
235
269
|
*
|
|
236
270
|
* `cap = 0` (the self-hosted default) → never throttle (unlimited).
|
|
237
271
|
* `periodStart` in a previous calendar month → period resets to 0 first
|
|
238
|
-
* (
|
|
239
|
-
* the same state + clock).
|
|
272
|
+
* (isStaleTokenPeriod — the same helper every monthly-reset site uses, so
|
|
273
|
+
* the sides agree because they read the same state + clock).
|
|
240
274
|
*/
|
|
241
275
|
function shouldThrottleTokens(cap, period, lastRunTokens, now = new Date()) {
|
|
242
276
|
if (cap <= 0)
|
|
243
277
|
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
|
-
}
|
|
278
|
+
// Stale period → reset accrual to 0 before the check.
|
|
279
|
+
const periodTotal = isStaleTokenPeriod(period?.periodStart, now) ? 0 : (period?.total ?? 0);
|
|
254
280
|
if (periodTotal + lastRunTokens > cap) {
|
|
255
281
|
return { throttle: true, used: periodTotal, cap };
|
|
256
282
|
}
|
|
@@ -325,7 +351,7 @@ function stagePrecheck(db, stateDir) {
|
|
|
325
351
|
// ---------------------------------------------------------------------------
|
|
326
352
|
// Stage 2: Importance Scoring
|
|
327
353
|
// ---------------------------------------------------------------------------
|
|
328
|
-
async function stageImportance(db, memories, llm, budget, dryRun) {
|
|
354
|
+
async function stageImportance(db, memories, llm, budget, dryRun, deadline) {
|
|
329
355
|
const batchSize = 10;
|
|
330
356
|
let scored = 0;
|
|
331
357
|
let failed = 0;
|
|
@@ -335,6 +361,11 @@ async function stageImportance(db, memories, llm, budget, dryRun) {
|
|
|
335
361
|
skippedBudget += memories.length - i;
|
|
336
362
|
break;
|
|
337
363
|
}
|
|
364
|
+
// #405: the run deadline bounds the batch loop too — a large unscored
|
|
365
|
+
// backlog must not blow the whole run's wall-clock inside one stage.
|
|
366
|
+
// Same stage label as the boundary check, so the defer log fires once.
|
|
367
|
+
if (deadline?.hit("importance"))
|
|
368
|
+
break;
|
|
338
369
|
const batch = memories.slice(i, i + batchSize);
|
|
339
370
|
const lines = batch.map((mem, idx) => `[${idx}] ${mem.content.slice(0, 500)}`);
|
|
340
371
|
const memoriesBlock = lines.join("\n\n");
|
|
@@ -346,7 +377,7 @@ async function stageImportance(db, memories, llm, budget, dryRun) {
|
|
|
346
377
|
break;
|
|
347
378
|
}
|
|
348
379
|
try {
|
|
349
|
-
const r = await llm.
|
|
380
|
+
const r = await llm.complete(prompt);
|
|
350
381
|
budget.recordUsage("importance", r.usage);
|
|
351
382
|
let scores = parseJsonLenient(r.text, null);
|
|
352
383
|
if (!Array.isArray(scores)) {
|
|
@@ -408,7 +439,7 @@ async function stageReflection(db, memories, llm, budget, embedFn, dryRun) {
|
|
|
408
439
|
return { lessons_generated: 0, skipped: true, reason: "budget_exhausted" };
|
|
409
440
|
}
|
|
410
441
|
try {
|
|
411
|
-
const r = await llm.
|
|
442
|
+
const r = await llm.complete(prompt);
|
|
412
443
|
budget.recordUsage("reflection", r.usage);
|
|
413
444
|
const lessons = parseJsonLenient(r.text, []);
|
|
414
445
|
if (!Array.isArray(lessons)) {
|
|
@@ -458,9 +489,9 @@ async function stageReflection(db, memories, llm, budget, embedFn, dryRun) {
|
|
|
458
489
|
const existingText = similarLessons[0].content.slice(0, 300);
|
|
459
490
|
const newText = content.slice(0, 300);
|
|
460
491
|
try {
|
|
461
|
-
const verdictR = await llm.
|
|
492
|
+
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
493
|
`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"
|
|
494
|
+
`Answer ONLY "yes" or "no". If the new lesson updates/refines the existing one (not contradicts), answer "no".`);
|
|
464
495
|
// Stage label "contradiction_check" matches the budget.use() call
|
|
465
496
|
// above (separate counter from the reflection call proper). Token
|
|
466
497
|
// accounting follows the same stage partition as the call counter.
|
|
@@ -543,7 +574,7 @@ function rebuildContentModuleIndex(db, domains, stateDir) {
|
|
|
543
574
|
(0, state_js_1.updateState)((s) => { s.moduleIndex = moduleIndex; }, stateDir);
|
|
544
575
|
return { domains: moduleDomains.length };
|
|
545
576
|
}
|
|
546
|
-
async function stageContentDomains(db, domains, llm, budget, embedFn, dryRun, stateDir, weakPrimaryFloor = nofit_js_1.DEFAULT_WEAK_PRIMARY_FLOOR) {
|
|
577
|
+
async function stageContentDomains(db, domains, llm, budget, embedFn, dryRun, stateDir, weakPrimaryFloor = nofit_js_1.DEFAULT_WEAK_PRIMARY_FLOOR, deadline) {
|
|
547
578
|
// Rows needing (re)classification:
|
|
548
579
|
// - domain IS NULL (never classified), OR
|
|
549
580
|
// - domain NOT IN the current vocabulary (a rename/removal re-files), OR
|
|
@@ -569,6 +600,11 @@ async function stageContentDomains(db, domains, llm, budget, embedFn, dryRun, st
|
|
|
569
600
|
// refreshes everything from the updated tag sets anyway).
|
|
570
601
|
const { prototypes: startPrototypes } = await (0, schema_prototypes_js_1.computeDomainPrototypes)(db, domains, getEmbedFn);
|
|
571
602
|
for (const row of rows) {
|
|
603
|
+
// #405: the run deadline bounds the classification row loop (a large
|
|
604
|
+
// backlog must defer, not blow the wall-clock). Same stage label as the
|
|
605
|
+
// boundary check, so the defer log fires once.
|
|
606
|
+
if (deadline?.hit("domain_curation"))
|
|
607
|
+
break;
|
|
572
608
|
if (budget.exhausted || !budget.use("content_domain")) {
|
|
573
609
|
console.warn(`[hicortex] content-domain: budget exhausted after ${classified} classified`);
|
|
574
610
|
break;
|
|
@@ -716,7 +752,7 @@ async function stageDomainCuration(db, llm, budget, dryRun, stateDir) {
|
|
|
716
752
|
.map((r) => `${r.project}: ${r.cnt} / ${lessonsByProject.get(r.project) ?? 0}`)
|
|
717
753
|
.join("\n");
|
|
718
754
|
try {
|
|
719
|
-
const r = await llm.
|
|
755
|
+
const r = await llm.complete((0, prompts_js_1.domainCuration)(projectLines));
|
|
720
756
|
budget.recordUsage("domain_curation", r.usage);
|
|
721
757
|
const parsed = parseJsonLenient(r.text, []);
|
|
722
758
|
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
@@ -847,19 +883,16 @@ function discoverLinkCandidates(db, mem, embedding) {
|
|
|
847
883
|
* 672-link audit (see the Stage 3 header) found the LLM-classified UPPERCASE
|
|
848
884
|
* types near-useless (CONTRADICTS 4% acceptable). Every candidate now takes its
|
|
849
885
|
* 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.
|
|
886
|
+
* classifyRelationship). No LLM call is made. #405: the ignored `llm`/`budget`
|
|
887
|
+
* params are deleted — the signature now tells the truth.
|
|
888
|
+
* The return shape is unchanged; `llmClassified` is always 0 and
|
|
889
|
+
* `heuristicFallback` counts every candidate. Do NOT re-add an LLM path here
|
|
890
|
+
* without a classifier that passes the audit harness at >= 70% acceptable.
|
|
858
891
|
*
|
|
859
892
|
* Shared between the nightly `stageLinks` and `hicortex relink`.
|
|
860
893
|
* Returns one relationship type per candidate (same order as input).
|
|
861
894
|
*/
|
|
862
|
-
async function classifyLinkCandidates(candidates
|
|
895
|
+
async function classifyLinkCandidates(candidates) {
|
|
863
896
|
const types = candidates.map((c) => c.heuristicType);
|
|
864
897
|
return { types, llmClassified: 0, heuristicFallback: candidates.length };
|
|
865
898
|
}
|
|
@@ -880,8 +913,9 @@ async function stageLinks(db, memories, embedFn, dryRun, llm, budget) {
|
|
|
880
913
|
if (candidates.length === 0) {
|
|
881
914
|
return { auto_linked: 0, llm_classified: 0, heuristic_fallback: 0, failed };
|
|
882
915
|
}
|
|
883
|
-
// Phase B:
|
|
884
|
-
|
|
916
|
+
// Phase B: heuristic-only classification (LLM retired; #405 dropped the
|
|
917
|
+
// dead llm/budget params)
|
|
918
|
+
const { types: classifiedTypes, llmClassified, heuristicFallback } = await classifyLinkCandidates(candidates);
|
|
885
919
|
// Phase C: Store all classified links
|
|
886
920
|
for (let i = 0; i < candidates.length; i++) {
|
|
887
921
|
const c = candidates[i];
|
|
@@ -968,26 +1002,17 @@ function stageHubBoost(db, dryRun) {
|
|
|
968
1002
|
// this is a judgment call about content, not a duplicate).
|
|
969
1003
|
//
|
|
970
1004
|
// 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;
|
|
1005
|
+
// the corpus is back-processed gradually) whose shape suggests a
|
|
1006
|
+
// decision/correction. For each, KNN top-5 OLDER same-shape neighbors
|
|
1007
|
+
// at/above the release-managed similarity floor (calibration.ts); one constrained classify-tier LLM
|
|
1008
|
+
// call per pair decides `superseded: true| false`. A parse/infra error skips
|
|
1009
|
+
// just that PAIR (retried naturally next night since the cursor still
|
|
1010
|
+
// advances past the memory — see the cursor note below); it never mis-links.
|
|
1011
|
+
// #405: no per-stage call cap — the ONE run budget (nightlyLlmCallBudget)
|
|
1012
|
+
// and the run deadline are the only bounds, like every other stage.
|
|
1013
|
+
/** Default minimum COSINE similarity for a supersession candidate pair —
|
|
1014
|
+
* RELEASE-MANAGED since #408 (calibration.ts SUPERSESSION_MIN_SIMILARITY). */
|
|
1015
|
+
exports.DEFAULT_SUPERSESSION_MIN_SIMILARITY = CALIBRATION.SUPERSESSION_MIN_SIMILARITY;
|
|
991
1016
|
/** Default multiplier applied to a superseded memory's base_strength. */
|
|
992
1017
|
/** Floor under which a superseded memory's base_strength never drops. */
|
|
993
1018
|
/** Neighbor pool size before shape/older/similarity filtering narrows to top 5. */
|
|
@@ -1067,7 +1092,7 @@ function parseSupersessionReply(reply) {
|
|
|
1067
1092
|
*/
|
|
1068
1093
|
async function classifySupersession(llm, oldContent, newContent) {
|
|
1069
1094
|
try {
|
|
1070
|
-
const r = await llm.
|
|
1095
|
+
const r = await llm.complete(buildSupersessionPrompt(oldContent, newContent));
|
|
1071
1096
|
return { verdict: parseSupersessionReply(r.text), usage: r.usage };
|
|
1072
1097
|
}
|
|
1073
1098
|
catch {
|
|
@@ -1107,6 +1132,13 @@ async function findOlderNeighbors(db, candidate, embedFn, minSimilarity) {
|
|
|
1107
1132
|
* candidacy. It only stops SHORT of a candidate when the budget is already
|
|
1108
1133
|
* exhausted before that candidate starts, so the cursor never skips a
|
|
1109
1134
|
* candidate that was never looked at.
|
|
1135
|
+
*
|
|
1136
|
+
* #405: the cursor persists after EVERY fully-considered candidate (the
|
|
1137
|
+
* post-#404 reconsolidation pattern), not at stage end — a run killed or
|
|
1138
|
+
* deadline-deferred mid-stage loses at most the candidate in flight. No
|
|
1139
|
+
* orphan clamp is needed (unlike reconsolidation): supersession applies each
|
|
1140
|
+
* verdict's link immediately, so `cursor = candidate.__rowid` always sits
|
|
1141
|
+
* after all of that candidate's writes.
|
|
1110
1142
|
*/
|
|
1111
1143
|
async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, options = {}) {
|
|
1112
1144
|
// Config values pass through `unknown`-typed JSON — validate rather than
|
|
@@ -1116,7 +1148,6 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
1116
1148
|
return Number.isFinite(n) && ok(n) ? n : fallback;
|
|
1117
1149
|
};
|
|
1118
1150
|
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
1151
|
const startCursor = (0, state_js_1.loadState)(stateDir).supersessionCursor ?? 0;
|
|
1121
1152
|
const rows = db
|
|
1122
1153
|
.prepare(
|
|
@@ -1136,10 +1167,26 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
1136
1167
|
let superseded = 0;
|
|
1137
1168
|
let skippedInfra = 0;
|
|
1138
1169
|
let skippedIdempotent = 0;
|
|
1139
|
-
let callsUsed = 0;
|
|
1140
1170
|
let cursor = startCursor;
|
|
1171
|
+
// #405: per-candidate checkpoint — persists the cursor after every fully
|
|
1172
|
+
// considered candidate (updateState is an atomic temp-rename of a small
|
|
1173
|
+
// file; the loop cadence is seconds per candidate, so the cost is
|
|
1174
|
+
// negligible). The end-of-stage write below stays the authoritative final
|
|
1175
|
+
// write.
|
|
1176
|
+
const persistCursor = () => {
|
|
1177
|
+
if (dryRun)
|
|
1178
|
+
return;
|
|
1179
|
+
(0, state_js_1.updateState)((s) => {
|
|
1180
|
+
s.supersessionCursor = cursor;
|
|
1181
|
+
}, stateDir);
|
|
1182
|
+
};
|
|
1141
1183
|
for (const candidate of rows) {
|
|
1142
|
-
|
|
1184
|
+
// #405: the ONE run budget is the only call cap; the deadline stops the
|
|
1185
|
+
// scan at the candidate boundary — the cursor holds at the last
|
|
1186
|
+
// fully-considered candidate (persisted below).
|
|
1187
|
+
if (!dryRun && budget.exhausted)
|
|
1188
|
+
break;
|
|
1189
|
+
if (!dryRun && options.deadline?.hit("supersession"))
|
|
1143
1190
|
break;
|
|
1144
1191
|
scanned++;
|
|
1145
1192
|
let neighbors;
|
|
@@ -1149,6 +1196,7 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
1149
1196
|
catch (err) {
|
|
1150
1197
|
console.warn(`[hicortex] supersession: discovery failed for ${candidate.id.slice(0, 8)} — ${err instanceof Error ? err.message : String(err)}`);
|
|
1151
1198
|
cursor = candidate.__rowid;
|
|
1199
|
+
persistCursor(); // #405: every exit path persists
|
|
1152
1200
|
continue;
|
|
1153
1201
|
}
|
|
1154
1202
|
for (const neighbor of neighbors) {
|
|
@@ -1158,9 +1206,8 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
1158
1206
|
}
|
|
1159
1207
|
if (dryRun)
|
|
1160
1208
|
continue; // preview only — no LLM call, no write
|
|
1161
|
-
if (
|
|
1162
|
-
break;
|
|
1163
|
-
callsUsed++;
|
|
1209
|
+
if (!budget.use("supersession"))
|
|
1210
|
+
break; // #405: the ONE run budget
|
|
1164
1211
|
const { verdict, usage } = await classifySupersession(llm, neighbor.content, candidate.content);
|
|
1165
1212
|
// Meter every round-tripped attempt (#246) — even a null verdict spent
|
|
1166
1213
|
// real tokens. The stage label matches the budget.use() above.
|
|
@@ -1184,6 +1231,10 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
1184
1231
|
}
|
|
1185
1232
|
}
|
|
1186
1233
|
cursor = candidate.__rowid;
|
|
1234
|
+
// #405: checkpoint after every fully-considered candidate (post-#404
|
|
1235
|
+
// reconsolidation pattern) — a killed or deadline-deferred run loses at
|
|
1236
|
+
// most the candidate in flight.
|
|
1237
|
+
persistCursor();
|
|
1187
1238
|
}
|
|
1188
1239
|
if (!dryRun) {
|
|
1189
1240
|
(0, state_js_1.updateState)((s) => {
|
|
@@ -1376,21 +1427,23 @@ async function skippedRunResolutionReport(db, dryRun, stateDir, options = {}) {
|
|
|
1376
1427
|
return Number.isFinite(n) && ok(n) ? n : fallback;
|
|
1377
1428
|
};
|
|
1378
1429
|
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
1430
|
const merges = await (0, dedup_js_1.runDeterministicMergeZone)(db, {
|
|
1381
1431
|
stateDir,
|
|
1382
1432
|
threshold: autoMergeThreshold,
|
|
1383
|
-
maxMerges,
|
|
1384
1433
|
dryRun,
|
|
1385
1434
|
acquireLock: options.acquireLock,
|
|
1435
|
+
deadline: options.deadline,
|
|
1386
1436
|
});
|
|
1387
1437
|
const bandStats = {};
|
|
1388
|
-
|
|
1438
|
+
{
|
|
1439
|
+
// #405: recorded whenever the zone ran (the old max_merges>0 gate was a
|
|
1440
|
+
// 0=disabled switch — the switch is gone).
|
|
1389
1441
|
bandStats[`>=${autoMergeThreshold}`] = {
|
|
1390
1442
|
pairs: merges.losers_merged,
|
|
1391
1443
|
merge: merges.losers_merged,
|
|
1392
1444
|
corrects: 0,
|
|
1393
1445
|
supersedes: 0,
|
|
1446
|
+
conflicts: 0,
|
|
1394
1447
|
none: 0,
|
|
1395
1448
|
merge_below_gate: 0,
|
|
1396
1449
|
conf_sum: merges.losers_merged,
|
|
@@ -1421,24 +1474,40 @@ async function skippedRunResolutionReport(db, dryRun, stateDir, options = {}) {
|
|
|
1421
1474
|
merge_below_gate: 0,
|
|
1422
1475
|
skipped_above_ceiling: 0,
|
|
1423
1476
|
skipped_metadata_mismatch: 0,
|
|
1477
|
+
conflict_flagged: 0, // guard-C: no scan on a quiet night — nothing flagged
|
|
1478
|
+
conflict_skipped: merges.skipped_conflict, // guard-C: the zone's guard still counts
|
|
1479
|
+
scout_scanned: 0, // #393 B: the scan (and its shape calls) doesn't run on a quiet night
|
|
1480
|
+
scout_correction_shaped: 0,
|
|
1481
|
+
scout_candidates_found: 0,
|
|
1424
1482
|
band_stats: bandStats,
|
|
1425
1483
|
};
|
|
1426
1484
|
}
|
|
1427
1485
|
async function runConsolidation(db, llm, embedFn, dryRun = false, skipReflection = false, stateDir, domainOptions, supersessionOptions,
|
|
1428
|
-
/**
|
|
1429
|
-
*
|
|
1430
|
-
*
|
|
1486
|
+
/** The ONE per-run LLM-call ceiling (#405/#241). The caller resolves
|
|
1487
|
+
* `nightlyLlmCallBudget` from config (consolidateMaxLlmCalls is a
|
|
1488
|
+
* deprecated alias — resolveNightlyLlmCallBudget) and passes it; unset →
|
|
1489
|
+
* the exported DEFAULT_NIGHTLY_LLM_CALL_BUDGET (5000). */
|
|
1431
1490
|
budgetMaxCalls,
|
|
1432
1491
|
/** Soft cap on the corpus (#245). Nightly.ts reads `memorySoftCap` from
|
|
1433
1492
|
* config and passes it; unset → `DEFAULT_MEMORY_SOFT_CAP` (10000). `0`
|
|
1434
1493
|
* disables eviction (indefinite growth). */
|
|
1435
1494
|
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
|
-
|
|
1495
|
+
/** Reconsolidation-stage knobs (#384) — eval/test seams since #408 (the
|
|
1496
|
+
* values are release-managed calibration constants; nightly.ts threads
|
|
1497
|
+
* NOTHING), exactly like supersessionOptions above; unset fields → the
|
|
1498
|
+
* stage's calibration defaults. Appended AFTER the pre-#384 params so
|
|
1499
|
+
* every existing positional caller (tests, hosted nightly) keeps its
|
|
1500
|
+
* argument meaning. */
|
|
1501
|
+
reconsolidationOptions,
|
|
1502
|
+
/**
|
|
1503
|
+
* The run-wide pipeline deadline (#405), created at nightly start and
|
|
1504
|
+
* shared by capture + every consolidation stage. Absent = no deadline
|
|
1505
|
+
* (tests, evict-only paths, pre-#405 callers). When it fires, every
|
|
1506
|
+
* not-yet-run stage defers (logs event=deadline_deferred stage=<name>) and
|
|
1507
|
+
* the report status becomes "deferred" — which keeps lastConsolidated
|
|
1508
|
+
* un-advanced so the next run re-finds the pending work.
|
|
1509
|
+
*/
|
|
1510
|
+
deadline) {
|
|
1442
1511
|
const start = new Date();
|
|
1443
1512
|
const report = {
|
|
1444
1513
|
started_at: start.toISOString(),
|
|
@@ -1483,20 +1552,31 @@ reconsolidationOptions) {
|
|
|
1483
1552
|
// stage report (telemetry's "skipped = zero LLM work" stays true), and
|
|
1484
1553
|
// the zone never runs twice: the main path runs it INSIDE the stage, this
|
|
1485
1554
|
// skip path returns before that.
|
|
1486
|
-
report.stages.reconsolidation = await skippedRunResolutionReport(db, dryRun, stateDir, reconsolidationOptions);
|
|
1555
|
+
report.stages.reconsolidation = await skippedRunResolutionReport(db, dryRun, stateDir, { ...reconsolidationOptions, deadline });
|
|
1487
1556
|
report.status = "skipped";
|
|
1488
1557
|
report.completed_at = new Date().toISOString();
|
|
1489
1558
|
return report;
|
|
1490
1559
|
}
|
|
1491
|
-
//
|
|
1492
|
-
//
|
|
1493
|
-
//
|
|
1494
|
-
const budget = new BudgetTracker(budgetMaxCalls ?? exports.
|
|
1560
|
+
// #405: the ONE per-run LLM-call ceiling (default 5000). The caller
|
|
1561
|
+
// resolves `nightlyLlmCallBudget` from config (consolidateMaxLlmCalls is a
|
|
1562
|
+
// deprecated alias — see resolveNightlyLlmCallBudget) and passes it here.
|
|
1563
|
+
const budget = new BudgetTracker(budgetMaxCalls ?? exports.DEFAULT_NIGHTLY_LLM_CALL_BUDGET);
|
|
1564
|
+
console.log(`[hicortex] Consolidation LLM call budget: ${budget.maxCalls} calls`);
|
|
1495
1565
|
try {
|
|
1566
|
+
// #405 stage gating: each boundary checks the run deadline; a hit defers
|
|
1567
|
+
// that stage (absent from the report — it did not run) and logs
|
|
1568
|
+
// event=deadline_deferred stage=<name> once. Later boundaries check
|
|
1569
|
+
// independently, so a mid-run deadline reports every remaining stage as
|
|
1570
|
+
// deferred. Deferred stages drain next run (cursors hold below them).
|
|
1496
1571
|
// Stage 2: Importance Scoring
|
|
1497
|
-
|
|
1572
|
+
if (!deadline?.hit("importance")) {
|
|
1573
|
+
report.stages.importance = await stageImportance(db, scoreMemories, llm, budget, dryRun, deadline);
|
|
1574
|
+
}
|
|
1498
1575
|
// Stage 2.5: Reflection
|
|
1499
|
-
if (
|
|
1576
|
+
if (deadline?.hit("reflection")) {
|
|
1577
|
+
// deferred — stage absent from the report
|
|
1578
|
+
}
|
|
1579
|
+
else if (skipReflection) {
|
|
1500
1580
|
report.stages.reflection = {
|
|
1501
1581
|
lessons_generated: 0,
|
|
1502
1582
|
skipped: true,
|
|
@@ -1511,40 +1591,60 @@ reconsolidationOptions) {
|
|
|
1511
1591
|
// domain list is configured. The single model serves all phases; if it's
|
|
1512
1592
|
// down, the phase skips and retries on the next nightly run (no fallback).
|
|
1513
1593
|
const cfgDomains = domainOptions?.domains;
|
|
1514
|
-
if (
|
|
1515
|
-
if (
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1594
|
+
if (!deadline?.hit("domain_curation")) {
|
|
1595
|
+
if (cfgDomains && cfgDomains.length > 0) {
|
|
1596
|
+
if (domainOptions?.contentDomainsReady === false) {
|
|
1597
|
+
report.stages.domain_curation = {
|
|
1598
|
+
curated: false,
|
|
1599
|
+
domains: cfgDomains.length,
|
|
1600
|
+
reason: "reflect_endpoint_offline",
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
else {
|
|
1604
|
+
report.stages.domain_curation = await stageContentDomains(db, cfgDomains, llm, budget, embedFn, dryRun, stateDir, domainOptions?.weakPrimaryFloor ?? nofit_js_1.DEFAULT_WEAK_PRIMARY_FLOOR, deadline);
|
|
1605
|
+
}
|
|
1521
1606
|
}
|
|
1522
1607
|
else {
|
|
1523
|
-
report.stages.domain_curation = await
|
|
1608
|
+
report.stages.domain_curation = await stageDomainCuration(db, llm, budget, dryRun, stateDir);
|
|
1524
1609
|
}
|
|
1525
1610
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1611
|
+
// Stage 3: Link Discovery (heuristic edge classification — local work,
|
|
1612
|
+
// but bounded by the same deadline as every other stage).
|
|
1613
|
+
if (!deadline?.hit("links")) {
|
|
1614
|
+
report.stages.links = await stageLinks(db, precheck.newMemories, embedFn, dryRun, llm, budget);
|
|
1528
1615
|
}
|
|
1529
|
-
// Stage 3: Link Discovery (with LLM-assisted edge classification)
|
|
1530
|
-
report.stages.links = await stageLinks(db, precheck.newMemories, embedFn, dryRun, llm, budget);
|
|
1531
1616
|
// Stage 3.5: Hub Detection — boost highly-connected memories
|
|
1532
|
-
|
|
1617
|
+
if (!deadline?.hit("hub_boost")) {
|
|
1618
|
+
report.stages.hub_boost = stageHubBoost(db, dryRun);
|
|
1619
|
+
}
|
|
1533
1620
|
// Stage 3.7: Supersession Detection (#191 Phase B)
|
|
1534
|
-
|
|
1621
|
+
if (!deadline?.hit("supersession")) {
|
|
1622
|
+
report.stages.supersession = await stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, { ...supersessionOptions, deadline });
|
|
1623
|
+
}
|
|
1535
1624
|
// Stage 3.8: Reconsolidation (#384) — resolve corrections: rewrite
|
|
1536
1625
|
// fact-shaped targets in place (absorbing transition-only triggers),
|
|
1537
1626
|
// mark everything else. Rides the same shared budget under its own stage
|
|
1538
1627
|
// label + cursor (supersession-stage pattern).
|
|
1539
|
-
|
|
1628
|
+
if (!deadline?.hit("reconsolidation")) {
|
|
1629
|
+
report.stages.reconsolidation = await (0, reconsolidation_js_1.stageReconsolidation)(db, llm, budget, embedFn, dryRun, stateDir, { ...reconsolidationOptions, deadline });
|
|
1630
|
+
}
|
|
1540
1631
|
// Stage 4: Decay & Prune
|
|
1541
|
-
|
|
1632
|
+
if (!deadline?.hit("decay_prune")) {
|
|
1633
|
+
report.stages.decay_prune = stageDecayPrune(db, dryRun);
|
|
1634
|
+
}
|
|
1542
1635
|
// (Memory cap eviction moved before the precheck skip — see above.)
|
|
1543
1636
|
}
|
|
1544
1637
|
catch (err) {
|
|
1545
1638
|
report.status = "failed";
|
|
1546
1639
|
console.error("[hicortex] Consolidation pipeline error:", err);
|
|
1547
1640
|
}
|
|
1641
|
+
// #405: any deferred stage ⇒ the run is "deferred", not "completed" — the
|
|
1642
|
+
// lastConsolidated gate below then holds the watermark so the next run's
|
|
1643
|
+
// pending-set queries re-find the deferred work (mirrors endpoint_down).
|
|
1644
|
+
// A thrown error still wins ("failed" is the more specific outcome).
|
|
1645
|
+
if (report.status === "completed" && deadline && deadline.deferredStages().length > 0) {
|
|
1646
|
+
report.status = "deferred";
|
|
1647
|
+
}
|
|
1548
1648
|
// Update last-consolidated timestamp. #357: the stages fail SOFT, so a run
|
|
1549
1649
|
// against an endpoint that died mid-way still reports status "completed"
|
|
1550
1650
|
// here — advancing the timestamp made state.json disagree with the
|
|
@@ -1568,59 +1668,3 @@ reconsolidationOptions) {
|
|
|
1568
1668
|
Math.round((Date.now() - start.getTime()) / 100) / 10;
|
|
1569
1669
|
return report;
|
|
1570
1670
|
}
|
|
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
|
-
}
|