memhtml 0.2.4 → 0.2.5

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.
@@ -644,6 +644,139 @@ const applyMmr = (candidates, limit, lambda = MMR_LAMBDA) => {
644
644
  return selected;
645
645
  };
646
646
 
647
+ //#endregion
648
+ //#region packages/domain/dist/neighbors.js
649
+ /**
650
+ * A `Float32Array` over stored bytes, copying only when it must.
651
+ *
652
+ * `Float32Array` requires a 4-byte-aligned `byteOffset`, and a driver row's `Uint8Array` may be
653
+ * a view into a pooled buffer at any offset. Viewing in place is the common case and costs
654
+ * nothing. A misaligned or ragged blob is copied rather than rejected, because the vector arm's
655
+ * job is to rank and a throw here would fail a whole search over one row.
656
+ */
657
+ const float32View = (bytes) => {
658
+ if (bytes.byteLength === 0 || bytes.byteLength % 4 !== 0) return void 0;
659
+ const aligned = bytes.byteOffset % 4 === 0 ? bytes : Uint8Array.from(bytes);
660
+ return new Float32Array(aligned.buffer, aligned.byteOffset, aligned.byteLength / 4);
661
+ };
662
+ /** `sqrt(Σ x²)` accumulated in index order — the same order {@link cosine} accumulates its norms. */
663
+ const sqrtNorm = (vec) => {
664
+ let sum = 0;
665
+ for (let index = 0; index < vec.length; index += 1) {
666
+ const x = vec[index];
667
+ sum += x * x;
668
+ }
669
+ return Math.sqrt(sum);
670
+ };
671
+ /**
672
+ * Cosine similarity from precomputed norms: {@link cosine}'s operations with the square roots
673
+ * hoisted out of the pair loop, so the result is bit-identical for equal-length vectors. The
674
+ * zero-norm rule and the `[-1, 1]` clamp are the same ones, for the same reasons. Mismatched
675
+ * lengths fall back to {@link cosine}, whose min-length walk defines that case; equal lengths are
676
+ * what the `embed_model` watermark guarantees for stored vectors.
677
+ */
678
+ const pairSimilarity = (a, aNorm, b, bNorm) => {
679
+ if (a.length !== b.length) return cosine(a, b);
680
+ if (aNorm === 0 || bNorm === 0) return 0;
681
+ let dot = 0;
682
+ for (let index = 0; index < a.length; index += 1) dot += a[index] * b[index];
683
+ return Math.max(-1, Math.min(1, dot / (aNorm * bNorm)));
684
+ };
685
+ /**
686
+ * Insert into a per-source list ordered `sim` DESC then `dst` ASC, bounded at `k`. Linear
687
+ * insertion, because `k` is single-digit everywhere this runs and a heap's constant factors lose
688
+ * at that size. Memory across the whole selection is O(n·k), never the pair space.
689
+ */
690
+ const insertBounded = (list, k, dst, sim) => {
691
+ let at = list.length;
692
+ for (let index = 0; index < list.length; index += 1) {
693
+ const held = list[index];
694
+ if (sim > held.sim || sim === held.sim && dst < held.dst) {
695
+ at = index;
696
+ break;
697
+ }
698
+ }
699
+ if (at >= k) return;
700
+ list.splice(at, 0, {
701
+ dst,
702
+ sim
703
+ });
704
+ if (list.length > k) list.pop();
705
+ };
706
+ /** The final ordering every consumer sees: `sim` DESC, then `src` ASC, then `dst` ASC, then cap. */
707
+ const collectRanked = (bySource, limit) => {
708
+ const rows = [];
709
+ for (const [src, list] of bySource) for (const held of list) rows.push({
710
+ src,
711
+ dst: held.dst,
712
+ sim: held.sim
713
+ });
714
+ rows.sort((left, right) => {
715
+ if (left.sim !== right.sim) return left.sim < right.sim ? 1 : -1;
716
+ if (left.src !== right.src) return left.src < right.src ? -1 : 1;
717
+ return left.dst < right.dst ? -1 : left.dst > right.dst ? 1 : 0;
718
+ });
719
+ return rows.slice(0, limit);
720
+ };
721
+ /**
722
+ * Per-source top-`k` nearest neighbors above a similarity floor, over every unordered pair.
723
+ *
724
+ * Each pair's similarity is computed ONCE and offered to BOTH endpoints' neighborhoods, so the
725
+ * output can hold `(a, b)` and `(b, a)` — each is a fact about a different source's neighborhood,
726
+ * and a consumer folding pairs must dedup the mirror itself (dedup-merge does, with its `seen`
727
+ * set). A floor comparison a NaN similarity cannot pass keeps a vector carrying NaN bytes out of
728
+ * every neighborhood rather than poisoning an ordering.
729
+ */
730
+ const topNeighborPairs = (vectors, options) => {
731
+ const norms = vectors.map((entry) => sqrtNorm(entry.vec));
732
+ const bySource = /* @__PURE__ */ new Map();
733
+ for (const entry of vectors) bySource.set(entry.key, []);
734
+ for (let i = 0; i < vectors.length; i += 1) {
735
+ const left = vectors[i];
736
+ const leftNorm = norms[i];
737
+ const leftList = bySource.get(left.key);
738
+ for (let j = i + 1; j < vectors.length; j += 1) {
739
+ const right = vectors[j];
740
+ const sim = pairSimilarity(left.vec, leftNorm, right.vec, norms[j]);
741
+ if (!(sim >= options.floor)) continue;
742
+ insertBounded(leftList, options.perSourceK, right.key, sim);
743
+ insertBounded(bySource.get(right.key), options.perSourceK, left.key, sim);
744
+ }
745
+ }
746
+ return collectRanked(bySource, options.limit);
747
+ };
748
+ /**
749
+ * Rank an ENUMERATED pair set: similarity, floor, per-source top-`k`, final ordering, cap.
750
+ *
751
+ * This is the shape for a consumer whose candidate pairs come from a selective predicate — the
752
+ * conflict scan's shared-entity join — rather than from the whole pair space. The predicate runs
753
+ * BEFORE ranking, exactly as a `WHERE` inside the ranking CTE would, so per-source top-`k` is
754
+ * computed over passing pairs only. A pair naming a key with no vector contributes nothing, the
755
+ * same outcome the SQL join's missing-embedding row produces.
756
+ */
757
+ const rankCandidatePairs = (pairs, vectors, options) => {
758
+ const byKey = /* @__PURE__ */ new Map();
759
+ for (const entry of vectors) byKey.set(entry.key, {
760
+ vec: entry.vec,
761
+ norm: sqrtNorm(entry.vec)
762
+ });
763
+ const bySource = /* @__PURE__ */ new Map();
764
+ for (const pair of pairs) {
765
+ const left = byKey.get(pair.src);
766
+ const right = byKey.get(pair.dst);
767
+ if (left === void 0 || right === void 0) continue;
768
+ const sim = pairSimilarity(left.vec, left.norm, right.vec, right.norm);
769
+ if (!(sim >= options.floor)) continue;
770
+ let list = bySource.get(pair.src);
771
+ if (list === void 0) {
772
+ list = [];
773
+ bySource.set(pair.src, list);
774
+ }
775
+ insertBounded(list, options.perSourceK, pair.dst, sim);
776
+ }
777
+ return collectRanked(bySource, options.limit);
778
+ };
779
+
647
780
  //#endregion
648
781
  //#region packages/domain/dist/reinforce.js
649
782
  /** The signal a reinforcement carries. `negative` is what drives the outcome EWMA down. */
@@ -5487,19 +5620,6 @@ const packSentences = (units, maxChars) => {
5487
5620
  */
5488
5621
  const BUSY_TIMEOUT_MS = 5e3;
5489
5622
  /**
5490
- * A `Float32Array` over stored bytes, copying only when it must.
5491
- *
5492
- * `Float32Array` requires a 4-byte-aligned `byteOffset`, and a driver row's `Uint8Array` may be
5493
- * a view into a pooled buffer at any offset. Viewing in place is the common case and costs
5494
- * nothing. A misaligned or ragged blob is copied rather than rejected, because the vector arm's
5495
- * job is to rank and a throw here would fail a whole search over one row.
5496
- */
5497
- const float32View = (bytes) => {
5498
- if (bytes.byteLength === 0 || bytes.byteLength % 4 !== 0) return void 0;
5499
- const aligned = bytes.byteOffset % 4 === 0 ? bytes : Uint8Array.from(bytes);
5500
- return new Float32Array(aligned.buffer, aligned.byteOffset, aligned.byteLength / 4);
5501
- };
5502
- /**
5503
5623
  * Register `vector_distance_cos(a, b)`, the cosine distance over two float32 blobs.
5504
5624
  *
5505
5625
  * SQLite ships no vector functions, so the vector retrieval arm's distance is this. It calls
@@ -5513,6 +5633,12 @@ const float32View = (bytes) => {
5513
5633
  * vectors at top-40 (probed 2026-08-12 on node 24.19.0), against a Bedrock query-embedding round
5514
5634
  * trip of a few hundred milliseconds that every vector search pays first. An approximate index buys
5515
5635
  * nothing until the corpus is an order of magnitude larger.
5636
+ *
5637
+ * That measurement is the 1×n shape — one bound query vector against the table, n calls, n blob
5638
+ * copies — and it is the ONLY shape this function serves. Each invocation materializes a fresh
5639
+ * `Uint8Array` per blob argument, so an n×n consumer pays the corpus re-copied n times (probed
5640
+ * 2026-08-18, issue #40: 8.45M calls and an OOM at n = 2,907). The sleep pair scans decode once
5641
+ * and rank in `@memhtml/domain`'s neighbors module instead.
5516
5642
  */
5517
5643
  const registerVectorDistance = (db) => {
5518
5644
  db.function("vector_distance_cos", { deterministic: true }, (a, b) => {
@@ -9080,8 +9206,10 @@ const isSleepExcluded = (memoryType) => SLEEP_EXCLUDED_TYPES.includes(memoryType
9080
9206
  const activeCorpus = (db) => db.all(`SELECT path, memory_type, title, gist, body_text, content_hash, confidence, importance,
9081
9207
  word_count, created_at, updated_at, valid_until, reprieves
9082
9208
  FROM files WHERE archived = 0 ORDER BY created_at ASC, path ASC`);
9209
+ /** A `memory_type NOT IN (…)` clause against alias `f`, or nothing when nothing is excluded. */
9210
+ const typeFilterFor = (alias, excluded) => excluded.length === 0 ? "" : ` AND ${alias}.memory_type NOT IN (${excluded.map(() => "?").join(", ")})`;
9083
9211
  /**
9084
- * Per-source top-`k` nearest neighbors above a similarity floor, over first-chunk vectors.
9212
+ * Every active file's first-chunk vector, decoded ONCE into the shape the pair kernel ranks.
9085
9213
  *
9086
9214
  * `ordinal = 0` collapses a file to its first chunk, not its best chunk. The format is one
9087
9215
  * fact per file, so almost every file is a single chunk. Taking the first keeps the pair set
@@ -9089,36 +9217,35 @@ const activeCorpus = (db) => db.all(`SELECT path, memory_type, title, gist, body
9089
9217
  * would make `(a, b)` a candidate while `(b, a)` is not, so which of two files was read first
9090
9218
  * would decide whether they merge.
9091
9219
  *
9092
- * `ROW_NUMBER() OVER (PARTITION BY src ...)` is the per-source cap. `vector_distance_cos` takes two
9093
- * STORED blobs here instead of a blob and a bound parameter. It can, because it is a registered
9094
- * SQL function over two `Uint8Array` arguments (`packages/index/src/database.ts`) and not a driver
9095
- * builtin with a fixed calling shape.
9220
+ * A row whose blob does not decode (empty or ragged) is dropped, the same exclusion the SQL
9221
+ * UDF's NULL produces for it in the retrieval arm.
9096
9222
  */
9097
- const neighborPairs = (db, options) => {
9098
- const excluded = options.excludeTypes ?? [];
9099
- const typeFilter = excluded.length === 0 ? "" : ` AND f.memory_type NOT IN (${excluded.map(() => "?").join(", ")})`;
9100
- return db.all(`WITH vecs AS (
9101
- SELECT f.path AS path, e.vec AS vec
9223
+ const firstChunkVectors = (db, excluded) => db.all(`SELECT f.path AS path, e.vec AS vec
9102
9224
  FROM files f
9103
9225
  JOIN chunks c ON c.path = f.path AND c.ordinal = 0
9104
9226
  JOIN embeddings e ON e.chunk_id = c.chunk_id
9105
- WHERE f.archived = 0${typeFilter}
9106
- ),
9107
- pairs AS (
9108
- SELECT l.path AS src, r.path AS dst, 1 - vector_distance_cos(l.vec, r.vec) AS sim
9109
- FROM vecs l JOIN vecs r ON r.path <> l.path
9110
- ),
9111
- ranked AS (
9112
- SELECT src, dst, sim, ROW_NUMBER() OVER (PARTITION BY src ORDER BY sim DESC, dst ASC) AS k
9113
- FROM pairs WHERE sim >= ?
9114
- )
9115
- SELECT src, dst, sim FROM ranked WHERE k <= ? ORDER BY sim DESC, src ASC, dst ASC LIMIT ?`, [
9116
- ...excluded,
9117
- options.floor,
9118
- options.perSourceK,
9119
- options.limit
9120
- ]);
9121
- };
9227
+ WHERE f.archived = 0${typeFilterFor("f", excluded)}`, [...excluded]).pipe(Effect.map((rows) => rows.flatMap((row) => {
9228
+ const vec = float32View(row.vec);
9229
+ return vec === void 0 ? [] : [{
9230
+ key: row.path,
9231
+ vec
9232
+ }];
9233
+ })));
9234
+ /**
9235
+ * Per-source top-`k` nearest neighbors above a similarity floor, over first-chunk vectors.
9236
+ *
9237
+ * The corpus filter is SQL, because that is where the index's reading semantics live. The pair
9238
+ * space is n² and ranks in TypeScript (`topNeighborPairs`), because a pair routed through the
9239
+ * `vector_distance_cos` UDF pays a fresh decode of BOTH 4 KB blobs per call — at a ~3k corpus
9240
+ * that is 8.45M calls and an OOM before the first phase records (issue #40), against ~12 MB
9241
+ * decoded once. The kernel reproduces this ordering exactly: floor, then per-source `sim` DESC /
9242
+ * `dst` ASC, then global `sim` DESC / `src` ASC / `dst` ASC, then the cap.
9243
+ */
9244
+ const neighborPairs = (db, options) => firstChunkVectors(db, options.excludeTypes ?? []).pipe(Effect.map((vectors) => topNeighborPairs(vectors, {
9245
+ floor: options.floor,
9246
+ perSourceK: options.perSourceK,
9247
+ limit: options.limit
9248
+ })));
9122
9249
  /**
9123
9250
  * Candidate pairs for conflict detection: embedding-near, sharing an entity, and carrying no
9124
9251
  * AUTHORED edge between them in either direction.
@@ -9133,42 +9260,32 @@ const neighborPairs = (db, options) => {
9133
9260
  * pairs above the 0.80 conflict floor. An anti-join over ALL edges therefore excludes every candidate
9134
9261
  * this phase exists to find, and the phase reports `candidates: 0` forever with no error anywhere.
9135
9262
  * A mined edge is a machine suspicion, not a settled relationship; only an authored one closes a pair.
9263
+ *
9264
+ * The statement ENUMERATES pairs from the shared-entity join instead of filtering an n×n vector
9265
+ * self-join, so its cost follows the entity sharing that actually exists. Similarity then ranks in
9266
+ * TypeScript over vectors decoded once (`rankCandidatePairs`), with the enumerated set standing
9267
+ * where the ranking CTE's `WHERE` stood: the predicates run BEFORE per-source top-`k`. `re.path <
9268
+ * le.path` orients each pair once, dst below src.
9136
9269
  */
9137
9270
  const conflictCandidates = (db, options) => {
9138
9271
  const excluded = options.excludeTypes ?? [];
9139
- const typeFilter = excluded.length === 0 ? "" : ` AND f.memory_type NOT IN (${excluded.map(() => "?").join(", ")})`;
9140
- return db.all(`WITH vecs AS (
9141
- SELECT f.path AS path, e.vec AS vec
9142
- FROM files f
9143
- JOIN chunks c ON c.path = f.path AND c.ordinal = 0
9144
- JOIN embeddings e ON e.chunk_id = c.chunk_id
9145
- WHERE f.archived = 0${typeFilter}
9146
- ),
9147
- pairs AS (
9148
- SELECT l.path AS src, r.path AS dst, 1 - vector_distance_cos(l.vec, r.vec) AS sim
9149
- FROM vecs l JOIN vecs r ON r.path < l.path
9150
- WHERE EXISTS (
9151
- SELECT 1 FROM file_entities le
9152
- JOIN file_entities re ON re.entity_type = le.entity_type AND re.entity_name = le.entity_name
9153
- WHERE le.path = l.path AND re.path = r.path
9154
- )
9155
- AND NOT EXISTS (
9156
- SELECT 1 FROM edges e
9157
- WHERE e.derived = 0
9158
- AND ((e.src_path = l.path AND e.dst_path = r.path)
9159
- OR (e.src_path = r.path AND e.dst_path = l.path))
9160
- )
9161
- ),
9162
- ranked AS (
9163
- SELECT src, dst, sim, ROW_NUMBER() OVER (PARTITION BY src ORDER BY sim DESC, dst ASC) AS k
9164
- FROM pairs WHERE sim >= ?
9165
- )
9166
- SELECT src, dst, sim FROM ranked WHERE k <= ? ORDER BY sim DESC, src ASC, dst ASC LIMIT ?`, [
9167
- ...excluded,
9168
- options.floor,
9169
- options.perSourceK,
9170
- options.limit
9171
- ]);
9272
+ const pairs = db.all(`SELECT DISTINCT le.path AS src, re.path AS dst
9273
+ FROM file_entities le
9274
+ JOIN file_entities re ON re.entity_type = le.entity_type
9275
+ AND re.entity_name = le.entity_name AND re.path < le.path
9276
+ JOIN files fl ON fl.path = le.path AND fl.archived = 0${typeFilterFor("fl", excluded)}
9277
+ JOIN files fr ON fr.path = re.path AND fr.archived = 0${typeFilterFor("fr", excluded)}
9278
+ WHERE NOT EXISTS (
9279
+ SELECT 1 FROM edges e
9280
+ WHERE e.derived = 0
9281
+ AND ((e.src_path = le.path AND e.dst_path = re.path)
9282
+ OR (e.src_path = re.path AND e.dst_path = le.path))
9283
+ )`, [...excluded, ...excluded]);
9284
+ return Effect.all([pairs, firstChunkVectors(db, excluded)]).pipe(Effect.map(([candidatePairs, vectors]) => rankCandidatePairs(candidatePairs, vectors, {
9285
+ floor: options.floor,
9286
+ perSourceK: options.perSourceK,
9287
+ limit: options.limit
9288
+ })));
9172
9289
  };
9173
9290
  /**
9174
9291
  * Every entity on an active NON-TASK file, with its file count. The union-find's input.
@@ -10716,7 +10833,11 @@ const preflight = (env) => Effect.gen(function* () {
10716
10833
  const MINING_COSINE_FLOOR = .85;
10717
10834
  /** Nearest neighbors considered per source file. */
10718
10835
  const MINING_PER_SOURCE_K = 5;
10719
- /** Pairs mined per cycle. The cost guard on a corpus whose pair space is quadratic. */
10836
+ /**
10837
+ * Pairs mined per cycle: a cap on what {@link replaceMinedEdges} writes, not on the scan — the
10838
+ * kernel's arithmetic is O(n²·d) whatever this says, and it bounds the edge table so one dense
10839
+ * neighborhood cannot flood the graph the lateral arm and PageRank read.
10840
+ */
10720
10841
  const MINING_SAMPLE_LIMIT = 2e3;
10721
10842
  const relationshipMining = (env) => Effect.gen(function* () {
10722
10843
  /**
@@ -12847,4 +12968,4 @@ const latest = (left, right) => left === null ? right : right === null ? left :
12847
12968
 
12848
12969
  //#endregion
12849
12970
  export { STATE_SIDECAR_PATH as $, makeIndexRecorder as A, makeGitPort as B, ModelClientLive as C, EMBED_DIM as D, EmbeddingsLive as E, reinforce as F, STATE_MIGRATIONS_DIR as G, DatabaseService as H, Indexer as I, expandRoot as J, STATE_SCHEMA as K, makeIndexer as L, readWatermark as M, Retrieval as N, EMBED_WATERMARK as O, makeRetrieval as P, STATE_DB_PATH as Q, readIndexState as R, ModelClient as S, Embeddings as T, makeDatabase as U, sanitizeFtsQuery as V, MIGRATIONS_DIR as W, INDEX_DB_PATH as X, makeStore as Y, SLEEP_REPORTS_DIR as Z, unlink as _, parseSidecar as a, commitSubject as at, discriminationGate as b, generateArtifacts as c, isValidDatetime as ct, danglingEdges as d, REINFORCE_SIGNALS as dt, attemptIo as et, publishRows as f, frameKeyOf as ft, meta as g, link as h, makeSleep as i, makeGit as it, persistScanned as j, IndexRecorder as k, accessRows as l, closesFence as lt, hrefFor as m, scanTraceRoot as n, readFileOrNull as nt, renderSidecar as o, checkMemory as ot, applyHeadEdits as p, Store as q, Sleep as r, Git as rt, archivedFormOf as s, setMeta as st, mergeTailExtract as t, initRepo as tt, allPaths as u, fenceOpeningOf as ut, SLEEP_PHASES as v, wrapAsData as w, runDiscrimination as x, isSleepPhase as y, IndexGit as z };
12850
- //# sourceMappingURL=dist-dfrcDFad.mjs.map
12971
+ //# sourceMappingURL=dist-Uj47oBRC.mjs.map