@gamaze/hicortex 0.20.5 → 0.20.7
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/consolidate.js +2 -0
- package/dist/nightly.js +6 -0
- package/dist/reconsolidation.d.ts +41 -2
- package/dist/reconsolidation.js +139 -6
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
package/dist/consolidate.js
CHANGED
|
@@ -1402,6 +1402,8 @@ async function skippedRunResolutionReport(db, dryRun, stateDir, options = {}) {
|
|
|
1402
1402
|
return {
|
|
1403
1403
|
scanned: 0,
|
|
1404
1404
|
pairs_evaluated: 0,
|
|
1405
|
+
pairs_discovered: 0, // #394: the scan doesn't run on a quiet night — nothing discovered
|
|
1406
|
+
pairs_discovered_unlinked: 0,
|
|
1405
1407
|
rewritten: 0,
|
|
1406
1408
|
absorbed: 0,
|
|
1407
1409
|
kept_linked: 0,
|
package/dist/nightly.js
CHANGED
|
@@ -748,6 +748,12 @@ async function runNightly(options = {}) {
|
|
|
748
748
|
autoMergeThreshold: (savedConfig?.dedupAutoMergeThreshold ??
|
|
749
749
|
savedConfig?.dedupMergeThreshold),
|
|
750
750
|
maxMerges: savedConfig?.dedupNightlyMaxMerges,
|
|
751
|
+
// #401 runtime bounds: the wall-clock deadline (default 120
|
|
752
|
+
// min, 0 disables) and the per-run classify-call ceiling
|
|
753
|
+
// (default 600, 0 disables). Same posture — the stage
|
|
754
|
+
// validates and falls back on invalid/absent values.
|
|
755
|
+
maxMinutes: savedConfig?.reconsolidationMaxMinutes,
|
|
756
|
+
maxCalls: savedConfig?.reconsolidationMaxCalls,
|
|
751
757
|
});
|
|
752
758
|
console.log(`[hicortex] Consolidation ${report.status} in ${report.elapsed_seconds}s` +
|
|
753
759
|
(report.stages.reflection ? ` (${report.stages.reflection.lessons_generated} lessons)` : ""));
|
|
@@ -57,6 +57,24 @@ export declare const DEFAULT_CORRECTION_MIN_SIMILARITY = 0.75;
|
|
|
57
57
|
* weak rewrite is corruption.
|
|
58
58
|
*/
|
|
59
59
|
export declare const DEFAULT_CORRECTION_REWRITE_MIN_CONFIDENCE = 0.8;
|
|
60
|
+
/**
|
|
61
|
+
* Default wall-clock bound for the stage, in minutes (#401). Checked at the
|
|
62
|
+
* top of the candidate scan loop (and before each rewrite contract call); on
|
|
63
|
+
* expiry the scan breaks cleanly at the last fully-considered candidate and
|
|
64
|
+
* the next run resumes from the persisted cursor. 120 sits safely under any
|
|
65
|
+
* sane process-level nightly timeout. 0 disables the bound. Invalid →
|
|
66
|
+
* default.
|
|
67
|
+
*/
|
|
68
|
+
export declare const DEFAULT_RECONSOLIDATION_MAX_MINUTES = 120;
|
|
69
|
+
/**
|
|
70
|
+
* Default per-run classify-call ceiling for the stage (#401) — the
|
|
71
|
+
* supersessionMaxCalls pattern with a NON-ZERO default ON PURPOSE: that
|
|
72
|
+
* knob's 0=unlimited default is what let the first full-corpus pass grow
|
|
73
|
+
* unbounded. Counts EVERY classify-tier call the stage makes (mark
|
|
74
|
+
* verifications, pair verdicts, rewrite contracts). 0 disables the cap.
|
|
75
|
+
* Invalid → default.
|
|
76
|
+
*/
|
|
77
|
+
export declare const DEFAULT_RECONSOLIDATION_MAX_CALLS = 600;
|
|
60
78
|
/** Head of the old content quoted in the provenance footer. */
|
|
61
79
|
export declare const FOOTER_HEAD_MAX_CHARS = 160;
|
|
62
80
|
/** The code-defined status vocabulary (see module doc). Not user-configurable. */
|
|
@@ -91,6 +109,24 @@ export interface ReconsolidationOptions {
|
|
|
91
109
|
* pair merges against ONE cap. Invalid → default.
|
|
92
110
|
*/
|
|
93
111
|
maxMerges?: number;
|
|
112
|
+
/**
|
|
113
|
+
* reconsolidationMaxMinutes (config; default 120; 0 disables) — wall-clock
|
|
114
|
+
* deadline for the stage (#401), measured from stage start. Checked at the
|
|
115
|
+
* top of the candidate scan loop and before each rewrite contract call; on
|
|
116
|
+
* expiry the scan breaks cleanly — the cursor already points at the last
|
|
117
|
+
* fully-considered candidate, so the run ends consistent and the next
|
|
118
|
+
* nightly resumes from it. Invalid → default.
|
|
119
|
+
*/
|
|
120
|
+
maxMinutes?: number;
|
|
121
|
+
/**
|
|
122
|
+
* reconsolidationMaxCalls (config; default 600; 0 disables) — per-run
|
|
123
|
+
* ceiling on classify-tier calls for the stage (#401), the
|
|
124
|
+
* supersessionMaxCalls pattern with a NON-ZERO default (the 0=unlimited
|
|
125
|
+
* default there is what removed the last per-stage bound). Exhaustion
|
|
126
|
+
* mid-neighbor-loop or mid-rewrite-phase stops/defers cleanly at the
|
|
127
|
+
* current candidate boundary. Invalid → default.
|
|
128
|
+
*/
|
|
129
|
+
maxCalls?: number;
|
|
94
130
|
/**
|
|
95
131
|
* Capture-lock acquirer override (tests) — the deterministic zone and the
|
|
96
132
|
* judged-merge phase each hold a short lock window. Defaults to the real
|
|
@@ -98,7 +134,7 @@ export interface ReconsolidationOptions {
|
|
|
98
134
|
*/
|
|
99
135
|
acquireLock?: typeof acquireCaptureLock;
|
|
100
136
|
}
|
|
101
|
-
/** The
|
|
137
|
+
/** The stage report (typed once, in ConsolidationReport — field list there). */
|
|
102
138
|
export type ReconsolidationStageResult = NonNullable<ConsolidationReport["stages"]["reconsolidation"]>;
|
|
103
139
|
/**
|
|
104
140
|
* True when a memory is REWRITE-ELIGIBLE — a fact-shaped target. The fork is
|
|
@@ -282,7 +318,10 @@ export declare function bandForCosine(bands: ResolutionBand[], cosine: number):
|
|
|
282
318
|
* silently dropped by the cursor passing it).
|
|
283
319
|
*
|
|
284
320
|
* Dry-run: the zone's discovery + the free idempotency check only — zero LLM
|
|
285
|
-
* calls, zero writes, no cursor or band-stats persistence.
|
|
321
|
+
* calls, zero writes, no cursor or band-stats persistence. Gate discovery is
|
|
322
|
+
* reported on every run (pairs_discovered / pairs_discovered_unlinked, #394) —
|
|
323
|
+
* on a dry-run they are the sizing numbers (pairs_evaluated stays 0: no calls
|
|
324
|
+
* are ever made).
|
|
286
325
|
*/
|
|
287
326
|
export declare function stageReconsolidation(db: Database.Database, llm: LlmClient, budget: StageBudget, embedFn: EmbedFn, dryRun: boolean, stateDir: string | undefined, options?: ReconsolidationOptions): Promise<ReconsolidationStageResult>;
|
|
288
327
|
export interface RollbackResult {
|
package/dist/reconsolidation.js
CHANGED
|
@@ -71,7 +71,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
71
71
|
};
|
|
72
72
|
})();
|
|
73
73
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
74
|
-
exports.absorbTrigger = exports.DEMOTED_STATUSES = exports.FOOTER_HEAD_MAX_CHARS = exports.DEFAULT_CORRECTION_REWRITE_MIN_CONFIDENCE = exports.DEFAULT_CORRECTION_MIN_SIMILARITY = exports.RECONSOLIDATION_STAGE_LABEL = void 0;
|
|
74
|
+
exports.absorbTrigger = exports.DEMOTED_STATUSES = exports.FOOTER_HEAD_MAX_CHARS = exports.DEFAULT_RECONSOLIDATION_MAX_CALLS = exports.DEFAULT_RECONSOLIDATION_MAX_MINUTES = exports.DEFAULT_CORRECTION_REWRITE_MIN_CONFIDENCE = exports.DEFAULT_CORRECTION_MIN_SIMILARITY = exports.RECONSOLIDATION_STAGE_LABEL = void 0;
|
|
75
75
|
exports.isFactShapedTarget = isFactShapedTarget;
|
|
76
76
|
exports.buildCorrectionVerdictPrompt = buildCorrectionVerdictPrompt;
|
|
77
77
|
exports.parseCorrectionVerdict = parseCorrectionVerdict;
|
|
@@ -115,6 +115,24 @@ exports.DEFAULT_CORRECTION_MIN_SIMILARITY = 0.75;
|
|
|
115
115
|
* weak rewrite is corruption.
|
|
116
116
|
*/
|
|
117
117
|
exports.DEFAULT_CORRECTION_REWRITE_MIN_CONFIDENCE = 0.8;
|
|
118
|
+
/**
|
|
119
|
+
* Default wall-clock bound for the stage, in minutes (#401). Checked at the
|
|
120
|
+
* top of the candidate scan loop (and before each rewrite contract call); on
|
|
121
|
+
* expiry the scan breaks cleanly at the last fully-considered candidate and
|
|
122
|
+
* the next run resumes from the persisted cursor. 120 sits safely under any
|
|
123
|
+
* sane process-level nightly timeout. 0 disables the bound. Invalid →
|
|
124
|
+
* default.
|
|
125
|
+
*/
|
|
126
|
+
exports.DEFAULT_RECONSOLIDATION_MAX_MINUTES = 120;
|
|
127
|
+
/**
|
|
128
|
+
* Default per-run classify-call ceiling for the stage (#401) — the
|
|
129
|
+
* supersessionMaxCalls pattern with a NON-ZERO default ON PURPOSE: that
|
|
130
|
+
* knob's 0=unlimited default is what let the first full-corpus pass grow
|
|
131
|
+
* unbounded. Counts EVERY classify-tier call the stage makes (mark
|
|
132
|
+
* verifications, pair verdicts, rewrite contracts). 0 disables the cap.
|
|
133
|
+
* Invalid → default.
|
|
134
|
+
*/
|
|
135
|
+
exports.DEFAULT_RECONSOLIDATION_MAX_CALLS = 600;
|
|
118
136
|
/** Neighbor pool size before older/similarity filtering narrows to top 5 (supersession mirror). */
|
|
119
137
|
const CORRECTION_NEIGHBOR_POOL = 15;
|
|
120
138
|
/** Older-neighbor pairs kept per candidate after filtering (supersession mirror). */
|
|
@@ -501,7 +519,10 @@ async function classifyPair(llm, oldContent, newContent) {
|
|
|
501
519
|
* silently dropped by the cursor passing it).
|
|
502
520
|
*
|
|
503
521
|
* Dry-run: the zone's discovery + the free idempotency check only — zero LLM
|
|
504
|
-
* calls, zero writes, no cursor or band-stats persistence.
|
|
522
|
+
* calls, zero writes, no cursor or band-stats persistence. Gate discovery is
|
|
523
|
+
* reported on every run (pairs_discovered / pairs_discovered_unlinked, #394) —
|
|
524
|
+
* on a dry-run they are the sizing numbers (pairs_evaluated stays 0: no calls
|
|
525
|
+
* are ever made).
|
|
505
526
|
*/
|
|
506
527
|
async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir, options = {}) {
|
|
507
528
|
// Config values pass through `unknown`-typed JSON — validate, never trust.
|
|
@@ -513,6 +534,13 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
|
|
|
513
534
|
const rewriteMinConfidence = validNumber(options.rewriteMinConfidence, exports.DEFAULT_CORRECTION_REWRITE_MIN_CONFIDENCE, (n) => n > 0 && n <= 1);
|
|
514
535
|
const autoMergeThreshold = validNumber(options.autoMergeThreshold, dedup_js_1.DEFAULT_DEDUP_MERGE_THRESHOLD, (n) => n > 0 && n <= 1);
|
|
515
536
|
const maxMerges = validNumber(options.maxMerges, dedup_js_1.DEFAULT_DEDUP_NIGHTLY_MAX_MERGES, (n) => n >= 0);
|
|
537
|
+
const maxMinutes = validNumber(options.maxMinutes, exports.DEFAULT_RECONSOLIDATION_MAX_MINUTES, (n) => n >= 0);
|
|
538
|
+
const maxCalls = validNumber(options.maxCalls, exports.DEFAULT_RECONSOLIDATION_MAX_CALLS, (n) => n >= 0);
|
|
539
|
+
// ---- #401 runtime bounds. The wall-clock deadline is measured from stage
|
|
540
|
+
// start (the deterministic zone's runtime counts against it — the binding
|
|
541
|
+
// constraint must be THIS knob, never the process-level backstop).
|
|
542
|
+
const deadlineAt = maxMinutes > 0 ? Date.now() + Math.round(maxMinutes * 60_000) : Infinity;
|
|
543
|
+
const deadlineHit = () => Date.now() >= deadlineAt;
|
|
516
544
|
// ---- #392 phase 0: the deterministic merge zone (pairs >= the ceiling),
|
|
517
545
|
// LLM-free and budget-free — an LLM-less night still drains duplicates. Its
|
|
518
546
|
// own short lock window, pre-merge backup, and pacing cap; fail-soft, never
|
|
@@ -548,6 +576,8 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
|
|
|
548
576
|
.all(startCursor);
|
|
549
577
|
let scanned = 0;
|
|
550
578
|
let pairsEvaluated = 0;
|
|
579
|
+
let pairsDiscovered = 0; // #394: gate discovery — counted before any skip/judgment
|
|
580
|
+
let pairsDiscoveredUnlinked = 0;
|
|
551
581
|
let rewritten = 0;
|
|
552
582
|
let absorbed = 0;
|
|
553
583
|
let keptLinked = 0;
|
|
@@ -576,6 +606,40 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
|
|
|
576
606
|
storage.addLink(db, oldId, newId, relationship, strength);
|
|
577
607
|
linksCreatedThisRun.add(`${oldId}|${newId}`);
|
|
578
608
|
};
|
|
609
|
+
// ---- #401: mid-scan cursor persistence. Called at EVERY scan-loop exit
|
|
610
|
+
// path (deadline, call/budget cap, mark-verify budget stop, discovery
|
|
611
|
+
// failure) AND after every fully-considered candidate, so a killed run
|
|
612
|
+
// loses at most the candidate in flight. The end-of-stage updateState
|
|
613
|
+
// below stays the authoritative final write (it also applies the
|
|
614
|
+
// pendingMinRowid hold — that variable is only ever set AFTER the scan
|
|
615
|
+
// loop, so it is null at every call site here). updateState is
|
|
616
|
+
// load→mutate→temp-rename atomic.
|
|
617
|
+
let callsUsed = 0;
|
|
618
|
+
let deadlineStopped = false;
|
|
619
|
+
let callCapStopped = false;
|
|
620
|
+
// #402 follow-up (reviewer note 1): the hard-kill orphan floor. Queued
|
|
621
|
+
// merges and open rewrite groups are applied only in the POST-scan
|
|
622
|
+
// phases — until then their verdicts exist only in memory, and a
|
|
623
|
+
// SIGKILL/OOM between two persists would strand them BEHIND the persisted
|
|
624
|
+
// cursor (the next run would skip them forever). This tracks the smallest
|
|
625
|
+
// candidate rowid contributing to queued-but-unapplied work;
|
|
626
|
+
// persistCursor clamps every checkpoint below it so a resumed run
|
|
627
|
+
// re-detects the pairs (dup-over-loss). Deliberately SEPARATE from the
|
|
628
|
+
// post-loop pendingMinRowid hold above — different lifetime, different
|
|
629
|
+
// writers.
|
|
630
|
+
let scanPendingMinRowid = null;
|
|
631
|
+
const notePendingRowid = (rowid) => {
|
|
632
|
+
scanPendingMinRowid =
|
|
633
|
+
scanPendingMinRowid === null ? rowid : Math.min(scanPendingMinRowid, rowid);
|
|
634
|
+
};
|
|
635
|
+
const persistCursor = () => {
|
|
636
|
+
if (dryRun)
|
|
637
|
+
return;
|
|
638
|
+
const checkpoint = scanPendingMinRowid !== null ? Math.min(cursor, scanPendingMinRowid - 1) : cursor;
|
|
639
|
+
(0, state_js_1.updateState)((s) => {
|
|
640
|
+
s.reconsolidationCursor = checkpoint;
|
|
641
|
+
}, stateDir);
|
|
642
|
+
};
|
|
579
643
|
const groups = new Map();
|
|
580
644
|
const addTrigger = (target, trigger, confidence, cosine, explicit) => {
|
|
581
645
|
let group = groups.get(target.id);
|
|
@@ -592,19 +656,43 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
|
|
|
592
656
|
candidateRowid: trigger.__rowid,
|
|
593
657
|
explicit,
|
|
594
658
|
});
|
|
659
|
+
notePendingRowid(trigger.__rowid); // orphan floor — group unapplied until the rewrite phase
|
|
595
660
|
}
|
|
596
661
|
};
|
|
597
662
|
for (const candidate of rows) {
|
|
598
|
-
|
|
663
|
+
// #401: runtime bounds first — exit cleanly at the last fully-considered
|
|
664
|
+
// candidate boundary (cursor = the previous candidate's rowid here).
|
|
665
|
+
if (!dryRun && deadlineHit()) {
|
|
666
|
+
deadlineStopped = true;
|
|
667
|
+
persistCursor();
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
if (!dryRun && budget.exhausted) {
|
|
671
|
+
persistCursor();
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
// #401: the per-stage call cap stops the scan at the candidate boundary
|
|
675
|
+
// (the in-loop check below is the mid-candidate backstop — supersession
|
|
676
|
+
// mirrors both).
|
|
677
|
+
if (!dryRun && maxCalls > 0 && callsUsed >= maxCalls) {
|
|
678
|
+
callCapStopped = true;
|
|
679
|
+
persistCursor();
|
|
599
680
|
break;
|
|
681
|
+
}
|
|
600
682
|
scanned++;
|
|
601
683
|
// ---- AC7: verify incoming explicit marks (corrected_by/superseded_by
|
|
602
684
|
// links targeting this candidate) before they can join a rewrite group.
|
|
685
|
+
// #401: only OPERATOR marks are verified — applyExplicitMark writes
|
|
686
|
+
// strength 1.0, while every stage-created link carries a measured cosine
|
|
687
|
+
// strength < 1 (markLink sites + supersession). Without the filter, every
|
|
688
|
+
// prior night's stage output re-entered verification: a self-sustaining
|
|
689
|
+
// backlog that re-litigated settled verdicts forever.
|
|
603
690
|
if (!dryRun) {
|
|
604
691
|
const incoming = db
|
|
605
692
|
.prepare(`SELECT source_id, relationship FROM memory_links
|
|
606
693
|
WHERE target_id = ? AND source_id != ?
|
|
607
|
-
AND relationship IN ('corrected_by', 'superseded_by')
|
|
694
|
+
AND relationship IN ('corrected_by', 'superseded_by')
|
|
695
|
+
AND strength >= 1.0`)
|
|
608
696
|
.all(candidate.id, candidate.id);
|
|
609
697
|
let markBudgetStop = false;
|
|
610
698
|
for (const mark of incoming) {
|
|
@@ -621,6 +709,7 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
|
|
|
621
709
|
}
|
|
622
710
|
const { verdict, usage } = await classifyPair(llm, target.content, candidate.content);
|
|
623
711
|
budget.recordUsage(exports.RECONSOLIDATION_STAGE_LABEL, usage);
|
|
712
|
+
callsUsed++; // #401
|
|
624
713
|
pairsEvaluated++;
|
|
625
714
|
if (!verdict) {
|
|
626
715
|
skippedInfra++; // mark retained; the neighborhood is revisited via newer candidacies
|
|
@@ -639,8 +728,10 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
|
|
|
639
728
|
`(verdict ${verdict.action}, confidence ${verdict.confidence.toFixed(2)}) — mark retained`);
|
|
640
729
|
}
|
|
641
730
|
}
|
|
642
|
-
if (markBudgetStop)
|
|
731
|
+
if (markBudgetStop) {
|
|
732
|
+
persistCursor(); // #401: this candidate's remaining marks re-verify next run
|
|
643
733
|
break;
|
|
734
|
+
}
|
|
644
735
|
}
|
|
645
736
|
// ---- AC2: detection pairs against older KNN neighbors.
|
|
646
737
|
let neighbors;
|
|
@@ -650,13 +741,16 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
|
|
|
650
741
|
catch (err) {
|
|
651
742
|
console.warn(`[hicortex] reconsolidation: discovery failed for ${candidate.id.slice(0, 8)} — ${err instanceof Error ? err.message : String(err)}`);
|
|
652
743
|
cursor = candidate.__rowid;
|
|
744
|
+
persistCursor(); // #401: every exit path persists
|
|
653
745
|
continue;
|
|
654
746
|
}
|
|
655
747
|
for (const neighbor of neighbors) {
|
|
748
|
+
pairsDiscovered++; // every neighbor passed the floor gate
|
|
656
749
|
if (alreadyResolutionLinked(db, neighbor.id, candidate.id)) {
|
|
657
750
|
skippedIdempotent++;
|
|
658
751
|
continue;
|
|
659
752
|
}
|
|
753
|
+
pairsDiscoveredUnlinked++; // still unlinked — the actionable candidate
|
|
660
754
|
// #392: pairs at/above the ceiling belong to the deterministic zone —
|
|
661
755
|
// counted here, never LLM-judged (the zone merges them or holds them
|
|
662
756
|
// for its cap; re-detection is structural, not cursor-based).
|
|
@@ -667,10 +761,16 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
|
|
|
667
761
|
}
|
|
668
762
|
if (dryRun)
|
|
669
763
|
continue; // preview only — no LLM call, no write
|
|
670
|
-
|
|
764
|
+
// #401: the per-stage call cap rides the same boundary as the budget —
|
|
765
|
+
// supersession-stage pattern (consolidate.ts stageSupersession).
|
|
766
|
+
if ((maxCalls > 0 && callsUsed >= maxCalls) || !budget.use(exports.RECONSOLIDATION_STAGE_LABEL)) {
|
|
767
|
+
callCapStopped = maxCalls > 0 && callsUsed >= maxCalls;
|
|
768
|
+
persistCursor(); // cursor still points at the last fully-considered candidate
|
|
671
769
|
break;
|
|
770
|
+
}
|
|
672
771
|
const { verdict, usage } = await classifyPair(llm, neighbor.content, candidate.content);
|
|
673
772
|
budget.recordUsage(exports.RECONSOLIDATION_STAGE_LABEL, usage);
|
|
773
|
+
callsUsed++; // #401
|
|
674
774
|
pairsEvaluated++;
|
|
675
775
|
if (!verdict) {
|
|
676
776
|
skippedInfra++;
|
|
@@ -693,6 +793,7 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
|
|
|
693
793
|
}
|
|
694
794
|
else {
|
|
695
795
|
queuedMerges.push({ oldId: neighbor.id, newId: candidate.id, candidateRowid: candidate.__rowid });
|
|
796
|
+
notePendingRowid(candidate.__rowid); // orphan floor — merge unapplied until the merge phase
|
|
696
797
|
}
|
|
697
798
|
continue;
|
|
698
799
|
}
|
|
@@ -726,6 +827,20 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
|
|
|
726
827
|
// verdict "none" → nothing to do
|
|
727
828
|
}
|
|
728
829
|
cursor = candidate.__rowid;
|
|
830
|
+
// #402 follow-up (reviewer note 1): persist after EVERY fully-considered
|
|
831
|
+
// candidate — the 50-candidate batch left a kill window that could
|
|
832
|
+
// strand several candidates of scan progress. updateState is an atomic
|
|
833
|
+
// temp-rename of a small file and the loop cadence is seconds per
|
|
834
|
+
// candidate; the cost is negligible.
|
|
835
|
+
persistCursor();
|
|
836
|
+
}
|
|
837
|
+
if (deadlineStopped) {
|
|
838
|
+
console.log(`[hicortex] Reconsolidation: wall-clock deadline reached (reconsolidationMaxMinutes) — ` +
|
|
839
|
+
`scan stopped at cursor ${cursor}; the next run resumes from there`);
|
|
840
|
+
}
|
|
841
|
+
else if (callCapStopped) {
|
|
842
|
+
console.log(`[hicortex] Reconsolidation: per-run call cap reached (reconsolidationMaxCalls) — ` +
|
|
843
|
+
`scan stopped at cursor ${cursor}; the next run resumes from there`);
|
|
729
844
|
}
|
|
730
845
|
// ---- #392 judged-merge phase: apply the queued pair merges through the
|
|
731
846
|
// dedup core (mergeMemoryIds — same canonical pick, link re-points,
|
|
@@ -838,6 +953,19 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
|
|
|
838
953
|
};
|
|
839
954
|
if (!dryRun && groups.size > 0) {
|
|
840
955
|
for (const group of groups.values()) {
|
|
956
|
+
// #401: the bounds stop the rewrite phase too — a group whose rewrite
|
|
957
|
+
// call was never made is left untouched and holds the cursor (never
|
|
958
|
+
// partially applied), exactly like the budget-exhausted path below.
|
|
959
|
+
if (deadlineHit()) {
|
|
960
|
+
deadlineStopped = true;
|
|
961
|
+
deferFrom(group.targetId);
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
if (maxCalls > 0 && callsUsed >= maxCalls) {
|
|
965
|
+
callCapStopped = true;
|
|
966
|
+
deferFrom(group.targetId);
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
841
969
|
if (!budget.use(exports.RECONSOLIDATION_STAGE_LABEL)) {
|
|
842
970
|
deferFrom(group.targetId);
|
|
843
971
|
break;
|
|
@@ -849,6 +977,7 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
|
|
|
849
977
|
const r = await llm.completeClassify(buildRewritePrompt(group.target.content, triggersArg));
|
|
850
978
|
contract = parseRewriteReply(r.text, group.triggers.map((t) => t.id), group.target.content);
|
|
851
979
|
budget.recordUsage(exports.RECONSOLIDATION_STAGE_LABEL, r.usage);
|
|
980
|
+
callsUsed++; // #401: rewrite contracts count toward the stage call cap
|
|
852
981
|
}
|
|
853
982
|
catch {
|
|
854
983
|
infraError = true;
|
|
@@ -957,6 +1086,8 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
|
|
|
957
1086
|
for (const [label, stat] of runBands)
|
|
958
1087
|
bandStats[label] = stat;
|
|
959
1088
|
if (!dryRun) {
|
|
1089
|
+
// #401: the authoritative FINAL cursor write — the mid-scan persists
|
|
1090
|
+
// above are checkpoints; this one also applies the pendingMinRowid hold.
|
|
960
1091
|
(0, state_js_1.updateState)((s) => {
|
|
961
1092
|
s.reconsolidationCursor = cursor;
|
|
962
1093
|
// Cumulative judged-band accumulation (#392) — the zone already
|
|
@@ -984,6 +1115,8 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
|
|
|
984
1115
|
return {
|
|
985
1116
|
scanned,
|
|
986
1117
|
pairs_evaluated: pairsEvaluated,
|
|
1118
|
+
pairs_discovered: pairsDiscovered,
|
|
1119
|
+
pairs_discovered_unlinked: pairsDiscoveredUnlinked,
|
|
987
1120
|
rewritten,
|
|
988
1121
|
absorbed,
|
|
989
1122
|
kept_linked: keptLinked,
|
package/dist/types.d.ts
CHANGED
|
@@ -224,6 +224,16 @@ export interface ConsolidationReport {
|
|
|
224
224
|
scanned: number;
|
|
225
225
|
/** Pairs actually sent to the verdict LLM (detection + explicit-mark verification). */
|
|
226
226
|
pairs_evaluated: number;
|
|
227
|
+
/**
|
|
228
|
+
* #394: pairs the similarity floor discovered this run (KNN neighbors
|
|
229
|
+
* at/above correctionMinSimilarity), counted before any skip or
|
|
230
|
+
* judgment — the only sizing number a dry-run can show, where
|
|
231
|
+
* pairs_evaluated is always 0.
|
|
232
|
+
*/
|
|
233
|
+
pairs_discovered: number;
|
|
234
|
+
/** #394: discovered pairs with no resolution link yet — the actionable
|
|
235
|
+
* candidates (deterministic-zone work + would-be verdict calls). */
|
|
236
|
+
pairs_discovered_unlinked: number;
|
|
227
237
|
/** Targets rewritten in place this run (one history row each). */
|
|
228
238
|
rewritten: number;
|
|
229
239
|
/** Triggers absorbed (invisible to recall: vector + FTS dropped). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamaze/hicortex",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.7",
|
|
4
4
|
"description": "Persistent agent identity for AI agents \u2014 a hand-edited identity layer, nightly-distilled experience, and lessons injected every session, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, Pi, and opencode.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|