@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.
@@ -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 14-field stage report (typed once, in ConsolidationReport). */
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 {
@@ -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
- if (!dryRun && budget.exhausted)
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
- if (!budget.use(exports.RECONSOLIDATION_STAGE_LABEL))
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.5",
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": {