@gamaze/hicortex 0.20.6 → 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.
@@ -133,13 +133,6 @@ exports.DEFAULT_RECONSOLIDATION_MAX_MINUTES = 120;
133
133
  * Invalid → default.
134
134
  */
135
135
  exports.DEFAULT_RECONSOLIDATION_MAX_CALLS = 600;
136
- /**
137
- * Candidates between mid-scan cursor persists (#401). The cursor also
138
- * persists at EVERY scan-loop exit path (deadline, call/budget cap,
139
- * discovery failure), so a killed run loses at most K-1 candidates of scan
140
- * progress instead of the whole night.
141
- */
142
- const RECONSOLIDATION_CURSOR_PERSIST_EVERY = 50;
143
136
  /** Neighbor pool size before older/similarity filtering narrows to top 5 (supersession mirror). */
144
137
  const CORRECTION_NEIGHBOR_POOL = 15;
145
138
  /** Older-neighbor pairs kept per candidate after filtering (supersession mirror). */
@@ -615,21 +608,36 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
615
608
  };
616
609
  // ---- #401: mid-scan cursor persistence. Called at EVERY scan-loop exit
617
610
  // path (deadline, call/budget cap, mark-verify budget stop, discovery
618
- // failure) plus a every-K batch tick, so a killed run loses at most K-1
619
- // candidates of scan progress instead of the whole night. The end-of-stage
620
- // updateState below stays the authoritative final write (it also applies
621
- // the pendingMinRowid hold — pendingMinRowid is only ever set AFTER the
622
- // scan loop, so the raw cursor is the effective cursor at every call site
623
- // here). updateState is load→mutate→temp-rename atomic.
624
- let scanBatch = 0;
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.
625
617
  let callsUsed = 0;
626
618
  let deadlineStopped = false;
627
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
+ };
628
635
  const persistCursor = () => {
629
636
  if (dryRun)
630
637
  return;
638
+ const checkpoint = scanPendingMinRowid !== null ? Math.min(cursor, scanPendingMinRowid - 1) : cursor;
631
639
  (0, state_js_1.updateState)((s) => {
632
- s.reconsolidationCursor = cursor;
640
+ s.reconsolidationCursor = checkpoint;
633
641
  }, stateDir);
634
642
  };
635
643
  const groups = new Map();
@@ -648,6 +656,7 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
648
656
  candidateRowid: trigger.__rowid,
649
657
  explicit,
650
658
  });
659
+ notePendingRowid(trigger.__rowid); // orphan floor — group unapplied until the rewrite phase
651
660
  }
652
661
  };
653
662
  for (const candidate of rows) {
@@ -784,6 +793,7 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
784
793
  }
785
794
  else {
786
795
  queuedMerges.push({ oldId: neighbor.id, newId: candidate.id, candidateRowid: candidate.__rowid });
796
+ notePendingRowid(candidate.__rowid); // orphan floor — merge unapplied until the merge phase
787
797
  }
788
798
  continue;
789
799
  }
@@ -817,12 +827,12 @@ async function stageReconsolidation(db, llm, budget, embedFn, dryRun, stateDir,
817
827
  // verdict "none" → nothing to do
818
828
  }
819
829
  cursor = candidate.__rowid;
820
- // #401: batch-tick persistence every K candidates — a kill between exit
821
- // paths loses at most K-1 candidates of scan progress.
822
- if (!dryRun && ++scanBatch >= RECONSOLIDATION_CURSOR_PERSIST_EVERY) {
823
- persistCursor();
824
- scanBatch = 0;
825
- }
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();
826
836
  }
827
837
  if (deadlineStopped) {
828
838
  console.log(`[hicortex] Reconsolidation: wall-clock deadline reached (reconsolidationMaxMinutes) — ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.20.6",
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": {