sweet-search 2.6.9 → 2.6.10

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 CHANGED
@@ -711,6 +711,21 @@ without you ever running a command.
711
711
  - **Resource-polite:** ticks are budgeted (≤50 files / ≤2 s CPU per tick), run CPU-only (the GPU is reserved for cold full indexing), and the interval auto-tunes from load average, churn, and backlog.
712
712
  - `sweet-search reconcile status` / `reconcile inspect <path>` explain exactly what the daemon thinks and why. Opt out any time with `SWEET_SEARCH_RECONCILE_V2=0`.
713
713
 
714
+ **Memory controls.** The resident daemons show up in `ps` / Activity Monitor as
715
+ `sweet-search-maintainer` and `sweet-search-daemon`. A maintainer's steady state is
716
+ roughly 2–3 GB (embedding + late-interaction models stay loaded so ticks are fast),
717
+ and four independent mechanisms keep that bounded:
718
+
719
+ | Mechanism | Default | Override |
720
+ |-----------|---------|----------|
721
+ | Background ORT profile (arena-off + parked threads) in the maintainer | on | `SWEET_SEARCH_ORT_BACKGROUND=0` |
722
+ | Per-process recycle ceiling — the maintainer finishes its tick, exits cleanly, and respawns fresh on the next edit when its RSS crosses the line | clamp(25 % of RAM, 4 GiB, 8 GiB) | `SWEET_SEARCH_MAINTAINER_RSS_MAX_MB` (0 disables) |
723
+ | Idle TTL — unattended daemons shut down and respawn on demand | tier-aware | `SWEET_SEARCH_MAINTAINER_IDLE_TTL_MS` / `SWEET_SEARCH_DAEMON_IDLE_TTL_MS` |
724
+ | Fleet RSS budget — across all repos' daemons, the longest-idle one is evicted when the sum crosses a RAM-scaled budget | tier-aware | `SWEET_SEARCH_RSS_BUDGET_FRACTION` |
725
+
726
+ A recycle or eviction never touches index state: every tick publishes atomically
727
+ before the process exits, and the next edit (or query) respawns a fresh daemon.
728
+
714
729
  </details>
715
730
 
716
731
  ## 🦀 The Native Engine Room
package/core/cli.js CHANGED
@@ -94,6 +94,13 @@ if (args[0] === 'init') {
94
94
  await runIndexer();
95
95
  }
96
96
  } else if (args[0] === '--serve' || args[0] === '--stop') {
97
+ // Name the resident daemon so ps/Activity Monitor shows what it is instead
98
+ // of an anonymous `node`. Entrypoint-scoped: library consumers of the
99
+ // search modules are never retitled. Liveness checks are lock/pid-based,
100
+ // never command-line matching, so this is cosmetic-only.
101
+ if (args[0] === '--serve') {
102
+ try { process.title = 'sweet-search-daemon'; } catch { /* best-effort */ }
103
+ }
97
104
  // Warm search server lifecycle is implemented in JS.
98
105
  const { runCli } = await import('./search/index.js');
99
106
  await runCli(args);
@@ -117,6 +117,7 @@ export async function applyLateInteractionDelta({
117
117
  liEncoder,
118
118
  pickLiInput,
119
119
  onProgress = null,
120
+ readerCache = null,
120
121
  }) {
121
122
  const progress = typeof onProgress === 'function'
122
123
  ? (phase) => { onProgress(phase); }
@@ -128,11 +129,35 @@ export async function applyLateInteractionDelta({
128
129
  const encode = liEncoder || ((texts) => encodeDocumentsCpu(texts));
129
130
  const existing = fs.existsSync(indexPath);
130
131
  const segmented = existing ? segmentedState(indexPath) : null;
131
- const index = new LateInteractionIndex({ indexPath, loadExisting: true });
132
- await index.init();
133
- progress('li:init');
132
+
133
+ // E.1-LI reader cache: reuse the loaded read view across the tick's files.
134
+ // Only the SEGMENTED path is cacheable — everything the reader serves there
135
+ // (config fields, positions/counts of pre-tick docs) is immutable within a
136
+ // tick: per-file ops only reference the file's own pre-tick docs, appends
137
+ // go through appendGrowingSegment (which re-reads the manifest from disk),
138
+ // and tombstone sidecar state is opened fresh per call. The legacy path
139
+ // MUTATES the loaded index (rewriteLegacyIndex), so it always loads fresh
140
+ // and drops any cached reader.
141
+ const cacheable = !!(readerCache && segmented);
142
+ let index;
143
+ if (cacheable && readerCache.index && readerCache.key === indexPath) {
144
+ index = readerCache.index;
145
+ progress('li:init-cached');
146
+ } else {
147
+ index = new LateInteractionIndex({ indexPath, loadExisting: true });
148
+ await index.init();
149
+ progress('li:init');
150
+ if (cacheable) {
151
+ readerCache.key = indexPath;
152
+ readerCache.index = index;
153
+ }
154
+ }
134
155
 
135
156
  if (existing && !segmented) {
157
+ if (readerCache) {
158
+ readerCache.key = null;
159
+ readerCache.index = null;
160
+ }
136
161
  return rewriteLegacyIndex(index, ops, encode, pickLiInput, progress);
137
162
  }
138
163
 
@@ -419,7 +419,7 @@ class ProductionReconcileAdapter {
419
419
  applyGraphDelta: (file, hashes, epoch, ctx) => this.applyGraphDelta(file, hashes, epoch, ctx),
420
420
  applyVectorDelta: (file, chunks, hashes, epoch, ctx) => this.applyVectorDelta(file, chunks, hashes, epoch, ctx),
421
421
  applyBinaryHNSWDelta: (file, ops, epoch, ctx) => this.applyBinaryHNSWDelta(file, ops, epoch, ctx),
422
- applyLIDelta: (file, ops, epoch) => this.applyLIDelta(file, ops, epoch),
422
+ applyLIDelta: (file, ops, epoch, ctx) => this.applyLIDelta(file, ops, ctx),
423
423
  applySparseGramDelta: (file, ops, epoch) => this.applySparseGramDelta(file, ops, epoch),
424
424
  readMaintenanceState: () => this.readMaintenanceState(),
425
425
  scheduleMaintenance: (job) => enqueueMaintenanceJob(this.stateDir, job),
@@ -1157,7 +1157,7 @@ class ProductionReconcileAdapter {
1157
1157
  return { ops: { binary_hnsw_append: append, binary_hnsw_tombstone: tombstone }, manifest: { path: 'codebase-binary-hnsw.idx' } };
1158
1158
  }
1159
1159
 
1160
- async applyLIDelta(file, ops) {
1160
+ async applyLIDelta(file, ops, ctx = null) {
1161
1161
  if (!Array.isArray(ops) || ops.length === 0) return { ops: { li_segment_append: 0, li_tombstone: 0 } };
1162
1162
  // LI generated-content parity: full indexing's buildLateInteractionIndex runs
1163
1163
  // applyIndexingChunkPolicy so @generated / config-excluded files never reach
@@ -1170,12 +1170,21 @@ class ProductionReconcileAdapter {
1170
1170
  ? ops.filter((op) => !(op.addId && op.chunk))
1171
1171
  : ops;
1172
1172
  const { applyLateInteractionDelta } = await import('./production-li-delta.mjs');
1173
+ // E.1-LI: tick-scoped READER cache. The per-file delta previously paid a
1174
+ // full loadExisting init (segment manifest + doc positions over a multi-
1175
+ // hundred-MB index) once per file; within one tick the reader view it
1176
+ // needs is stable (per-file ops only reference the file's own pre-tick
1177
+ // docs, appendGrowingSegment re-reads the manifest from disk, and segment
1178
+ // compaction only runs in maintenance after the tick). Null ctx (per-file
1179
+ // path) ⇒ fresh load per file, exactly as before.
1180
+ const readerCache = ctx ? (ctx._liReaderCache ??= { key: null, index: null }) : null;
1173
1181
  const { appended, tombstone } = await applyLateInteractionDelta({
1174
1182
  indexPath: path.join(this.stateDir, 'codebase-late-interaction.db'),
1175
1183
  ops: filteredOps,
1176
1184
  liEncoder: this.liEncoder,
1177
1185
  pickLiInput,
1178
1186
  onProgress: () => this.progress('production:li-delta'),
1187
+ readerCache,
1179
1188
  });
1180
1189
  return { ops: { li_segment_append: appended, li_tombstone: tombstone }, manifest: { path: 'codebase-late-interaction.db', segments: 'codebase-late-interaction.db.segments/manifest.json' } };
1181
1190
  }
@@ -461,7 +461,7 @@ export class Reconciler {
461
461
  if (bin?.ops?.binary_hnsw_append != null) ops.binary_hnsw_append = bin.ops.binary_hnsw_append;
462
462
  if (bin?.ops?.binary_hnsw_tombstone != null) ops.binary_hnsw_tombstone = bin.ops.binary_hnsw_tombstone;
463
463
  this.progress('reconciler:li:start');
464
- const li = await this.adapters.applyLIDelta?.(file, vec?.tokenOps ?? [], epoch);
464
+ const li = await this.adapters.applyLIDelta?.(file, vec?.tokenOps ?? [], epoch, tickCtx);
465
465
  this.progress('reconciler:li:done');
466
466
  collectManifestTier(manifestTiers, 'lateInteraction', li);
467
467
  if (li?.ops?.li_segment_append != null) ops.li_segment_append = li.ops.li_segment_append;
@@ -52,7 +52,7 @@ export const ARTIFACT_THRESHOLDS = {
52
52
  stateFile: '.sweet-search/artifact-rebuild-state.json',
53
53
  };
54
54
 
55
- import { BinaryHNSWIndex } from '../vector-store/binary-hnsw-index.js';
55
+ import { BinaryHNSWIndex, int8SidecarCount } from '../vector-store/binary-hnsw-index.js';
56
56
  import { truncateForHNSW, fisherYatesShuffle, normalizedFloatToInt8, floatToBinary } from '../infrastructure/quantization.js';
57
57
  import { FloatVectorStore, getFloatStorePath } from '../vector-store/float-vector-store.js';
58
58
 
@@ -828,8 +828,10 @@ export async function getArtifactStats() {
828
828
  if (existsSync(int8SidecarPath)) {
829
829
  try {
830
830
  const fileStats = await fs.stat(int8SidecarPath);
831
- const int8Data = JSON.parse(await fs.readFile(int8SidecarPath, 'utf-8'));
832
- const count = Object.keys(int8Data).length;
831
+ // Header-only count (NDJSON v2 sidecar; v1 back-compat) — never
832
+ // JSON.parse the whole file, which exceeds V8's string ceiling on
833
+ // large indexes.
834
+ const count = await int8SidecarCount(int8SidecarPath);
833
835
 
834
836
  stats.int8Sidecar = {
835
837
  exists: true,
@@ -1267,12 +1267,14 @@ async function runReconcileV2Main({ runOnce, merkleOnce }) {
1267
1267
  // Best-effort — never block startup on the embedding module.
1268
1268
  if (process.env.SWEET_SEARCH_ORT_BACKGROUND !== '0') {
1269
1269
  try {
1270
- const [{ configureLocalModelRuntime }, { backgroundIntraOpThreads }] = await Promise.all([
1271
- import('../embedding/embedding-local-model.js'),
1272
- import('../infrastructure/onnx-session-utils.js'),
1273
- ]);
1274
- configureLocalModelRuntime({ intraOpThreads: backgroundIntraOpThreads(), background: true });
1275
- log('INFO', 'ORT background profile armed for maintainer daemon (force_spinning_stop + arena-off + bg threads)');
1270
+ // Arms BOTH resident CPU sessions (dense + late-interaction) — the LI
1271
+ // session previously stayed on the foreground profile (arena ON +
1272
+ // allow_spinning) inside the daemon; measured consequence: 354 × 128MB
1273
+ // arena extensions (~34GB RSS) after one edit-heavy day plus ~40% idle
1274
+ // CPU from spinning workers.
1275
+ const { armBackgroundOrtProfiles } = await import('./model-pool.js');
1276
+ armBackgroundOrtProfiles();
1277
+ log('INFO', 'ORT background profile armed for maintainer daemon (dense + LI: force_spinning_stop + arena-off + bg threads)');
1276
1278
  } catch (err) {
1277
1279
  log('WARN', `ORT background profile arming failed (continuing on foreground profile): ${err?.message ?? err}`);
1278
1280
  }
@@ -1314,15 +1316,30 @@ async function runReconcileV2Main({ runOnce, merkleOnce }) {
1314
1316
  // system-RAM tier auto-enables a cap (small-RAM hosts). Best-effort, guarded
1315
1317
  // so a missing module is a no-op.
1316
1318
  let rssRegistration = null;
1319
+ // D.5 per-process recycle ceiling: fleet-budget eviction only sheds the
1320
+ // longest-IDLE daemon, so an active maintainer needs its own line. Resolved
1321
+ // once; checked at tick boundaries below. 0 ⇒ disabled.
1322
+ let rssCeiling = { ceilingBytes: 0, minUptimeMs: 0, shouldRecycleForRss: null };
1317
1323
  try {
1318
1324
  const mod = await import('./rss-budget.mjs');
1319
1325
  if (typeof mod.isEnabled === 'function' && mod.isEnabled()
1320
1326
  && typeof mod.registerDaemon === 'function') {
1321
1327
  rssRegistration = await mod.registerDaemon({ pid: process.pid, stateDir: ctx.stateDir, kind: 'maintainer' });
1322
1328
  }
1329
+ if (typeof mod.maintainerRssCeilingBytes === 'function') {
1330
+ rssCeiling = {
1331
+ ceilingBytes: mod.maintainerRssCeilingBytes(),
1332
+ minUptimeMs: mod.maintainerRssMinUptimeMs(),
1333
+ shouldRecycleForRss: mod.shouldRecycleForRss,
1334
+ };
1335
+ if (rssCeiling.ceilingBytes > 0) {
1336
+ log('INFO', `Maintainer RSS recycle ceiling armed: ${(rssCeiling.ceilingBytes / 1048576).toFixed(0)}MB (min uptime ${rssCeiling.minUptimeMs}ms; override SWEET_SEARCH_MAINTAINER_RSS_MAX_MB)`);
1337
+ }
1338
+ }
1323
1339
  } catch (err) {
1324
1340
  log('WARN', `RSS-budget registry unavailable (no soft cap on this daemon): ${err?.message ?? err}`);
1325
1341
  }
1342
+ let completedTicks = 0;
1326
1343
 
1327
1344
  // D.1 idle-TTL: an unattended maintainer self-shuts-down after the configured
1328
1345
  // wall-clock idle budget so N resident model-loaded daemons collapse to 1–2
@@ -1432,6 +1449,23 @@ async function runReconcileV2Main({ runOnce, merkleOnce }) {
1432
1449
  maintenanceBacklog: backlog,
1433
1450
  skipped: tickCounters?.skipped === true,
1434
1451
  });
1452
+ // D.5: per-process RSS recycle check — tick boundary only (the tick
1453
+ // above has published; a recycle here can never tear an artifact).
1454
+ completedTicks += 1;
1455
+ if (rssCeiling.ceilingBytes > 0 && typeof rssCeiling.shouldRecycleForRss === 'function') {
1456
+ const rssNow = process.memoryUsage.rss();
1457
+ const verdict = rssCeiling.shouldRecycleForRss({
1458
+ rssBytes: rssNow,
1459
+ ceilingBytes: rssCeiling.ceilingBytes,
1460
+ uptimeMs: process.uptime() * 1000,
1461
+ minUptimeMs: rssCeiling.minUptimeMs,
1462
+ ticksCompleted: completedTicks,
1463
+ });
1464
+ if (verdict.recycle) {
1465
+ log('WARN', `Maintainer RSS ${(rssNow / 1048576).toFixed(0)}MB exceeds recycle ceiling ${(rssCeiling.ceilingBytes / 1048576).toFixed(0)}MB; requesting clean shutdown for on-demand respawn. If this repeats right after startup, raise SWEET_SEARCH_MAINTAINER_RSS_MAX_MB.`);
1466
+ shutdownRequested = true;
1467
+ }
1468
+ }
1435
1469
  } catch (err) {
1436
1470
  if (err instanceof MaintainerLifecycleAbort) {
1437
1471
  log('WARN', `Reconcile v2 lifecycle abort: ${err.message}. Cleaning up cancellation-orphaned temps and exiting cleanly.`);
@@ -1457,6 +1491,10 @@ async function runReconcileV2Main({ runOnce, merkleOnce }) {
1457
1491
  log('ERROR', `Reconcile v2 tick failed: ${err?.message ?? err}`);
1458
1492
  }
1459
1493
  }
1494
+ // Skip the sleep when a shutdown was requested during this iteration
1495
+ // (idle-TTL, signal, or D.5 RSS recycle) — exit promptly instead of
1496
+ // waiting out one more interval.
1497
+ if (shutdownRequested) break;
1460
1498
  await sleepWithProgress(intervalMs, lock.lockFile, {
1461
1499
  // G6 early-wake: break the sleep the instant the watcher reports new
1462
1500
  // events so a fresh edit is reconciled without waiting out the interval.
@@ -2482,6 +2520,11 @@ async function main() {
2482
2520
  const dryRun = process.argv.includes('--dry-run');
2483
2521
  const merkleOnce = process.argv.includes('--merkle-once');
2484
2522
 
2523
+ // Name the resident daemon so ps/Activity Monitor shows what it is instead
2524
+ // of an anonymous `node`. Cosmetic-only: all liveness/takeover checks are
2525
+ // lock/pid-based, never command-line matching.
2526
+ try { process.title = 'sweet-search-maintainer'; } catch { /* best-effort */ }
2527
+
2485
2528
  // A.1 (Tier-1, UNGATED): demote the maintainer daemon to low OS priority so
2486
2529
  // the foreground (editor / git / shell) never feels the background indexer's
2487
2530
  // CPU. Identical index output — only *when* CPU is granted changes. Covers
@@ -29,12 +29,15 @@ import {
29
29
  getLocalPipeline,
30
30
  callLocalModelCpu,
31
31
  unloadLocalModel,
32
+ configureLocalModelRuntime,
32
33
  } from '../embedding/embedding-local-model.js';
33
34
  import {
34
35
  getLateInteractionPipeline,
35
36
  encodeDocumentsCpu,
36
37
  unloadLateInteractionModel,
38
+ configureLateInteractionRuntime,
37
39
  } from '../ranking/late-interaction-model.js';
40
+ import { backgroundIntraOpThreads } from '../infrastructure/onnx-session-utils.js';
38
41
 
39
42
  /**
40
43
  * Small-changeset threshold. Incremental runs with fewer files than this use
@@ -69,6 +72,29 @@ export function selectAcceleratorDeviceKind(pref) {
69
72
  return null;
70
73
  }
71
74
 
75
+ /**
76
+ * G3: arm the BACKGROUND/maintainer ORT profile for BOTH resident CPU
77
+ * sessions — dense (embedding-local-model) and late-interaction. The profile
78
+ * (force_spinning_stop + arena-off + 2–4 intra-op threads) prevents the two
79
+ * resident-daemon pathologies: idle spinning workers (~a core of idle CPU)
80
+ * and monotonic RSS from ORT arena extension (#25325; measured 354 × 128MB
81
+ * ≈ 34GB after one edit-heavy day when the LI session was left on the
82
+ * foreground profile). MUST run before the first encode — both session
83
+ * singletons are built once; configuring after is a silent no-op.
84
+ *
85
+ * Lives here (model-pool owns indexing-side model lifecycle) so daemon
86
+ * entrypoints don't need their own cross-context imports of the two model
87
+ * modules.
88
+ *
89
+ * @returns {{armed: boolean}}
90
+ */
91
+ export function armBackgroundOrtProfiles() {
92
+ const intraOpThreads = backgroundIntraOpThreads();
93
+ configureLocalModelRuntime({ intraOpThreads, background: true });
94
+ configureLateInteractionRuntime({ intraOpThreads, background: true });
95
+ return { armed: true };
96
+ }
97
+
72
98
  /**
73
99
  * Whether this process can use an inference accelerator (Metal / CoreML
74
100
  * cascade / CUDA) for indexing. Requires both accelerator-capable hardware
@@ -112,6 +112,71 @@ export function isOverBudget(totalRssBytes, budgetBytesValue) {
112
112
  );
113
113
  }
114
114
 
115
+ // =============================================================================
116
+ // D.5: per-process maintainer RSS recycle ceiling
117
+ // =============================================================================
118
+ //
119
+ // The fleet budget above evicts the longest-IDLE daemon, so a single fat
120
+ // ACTIVE maintainer can legitimately ride up to the whole fleet budget
121
+ // (~76 GB on a 128 GB host) — measured in the wild when a session-config
122
+ // regression left the LI ORT session on the foreground arena profile
123
+ // (354 × 128MB arena extensions ≈ 34 GB RSS). This ceiling is the
124
+ // per-process backstop: when the maintainer's OWN rss crosses the line at a
125
+ // tick boundary, it requests the same graceful shutdown the idle-TTL uses
126
+ // (finish tick, publish, release lock, exit). The O_EXCL launch trigger
127
+ // respawns a fresh few-hundred-MB process on the next dirty event, so the
128
+ // index is unchanged — exit-to-reclaim is already the sanctioned way to
129
+ // return ORT memory (#25325).
130
+ //
131
+ // Guards against restart-thrash:
132
+ // - at least one COMPLETED tick (never abort startup work),
133
+ // - minimum uptime (default 10 min ⇒ worst case 6 recycles/hour even when
134
+ // the ceiling is misconfigured below the resident floor — visible via
135
+ // the per-recycle WARN, which names the env knob to raise).
136
+ //
137
+ // Measured anchors (2026-07-03, arena-off profile): steady floor ~2.7-2.9 GB
138
+ // on a mid-size repo, encode-active peaks ~4.7 GB — hence the 4 GiB default
139
+ // floor and 8 GiB cap.
140
+
141
+ const MAINTAINER_RSS_CEILING_FRACTION = 0.25;
142
+ const MAINTAINER_RSS_CEILING_MIN_BYTES = 4 * 1024 * 1024 * 1024; // 4 GiB
143
+ const MAINTAINER_RSS_CEILING_MAX_BYTES = 8 * 1024 * 1024 * 1024; // 8 GiB
144
+ const MAINTAINER_RSS_MIN_UPTIME_MS_DEFAULT = 10 * 60 * 1000; // 10 min
145
+
146
+ /**
147
+ * Resolve the per-process maintainer RSS ceiling in bytes.
148
+ * `SWEET_SEARCH_MAINTAINER_RSS_MAX_MB` set: explicit MB value; 0/garbage
149
+ * disables (returns 0). Unset: clamp(25% of RAM, 4 GiB, 8 GiB).
150
+ */
151
+ export function maintainerRssCeilingBytes(env = process.env, totalMem = os.totalmem()) {
152
+ const raw = env.SWEET_SEARCH_MAINTAINER_RSS_MAX_MB;
153
+ if (raw != null && raw !== '') {
154
+ const mb = Number(raw);
155
+ if (!Number.isFinite(mb) || mb <= 0) return 0;
156
+ return Math.floor(mb * 1024 * 1024);
157
+ }
158
+ const scaled = Math.floor(totalMem * MAINTAINER_RSS_CEILING_FRACTION);
159
+ return Math.min(MAINTAINER_RSS_CEILING_MAX_BYTES, Math.max(MAINTAINER_RSS_CEILING_MIN_BYTES, scaled));
160
+ }
161
+
162
+ /** Minimum uptime before an RSS recycle may fire. */
163
+ export function maintainerRssMinUptimeMs(env = process.env) {
164
+ const raw = Number(env.SWEET_SEARCH_MAINTAINER_RSS_MIN_UPTIME_MS);
165
+ return Number.isFinite(raw) && raw >= 0 ? raw : MAINTAINER_RSS_MIN_UPTIME_MS_DEFAULT;
166
+ }
167
+
168
+ /**
169
+ * Pure recycle decision — evaluated at tick boundaries only.
170
+ * @returns {{recycle: boolean, reason: string}}
171
+ */
172
+ export function shouldRecycleForRss({ rssBytes, ceilingBytes, uptimeMs, minUptimeMs, ticksCompleted }) {
173
+ if (!Number.isFinite(ceilingBytes) || ceilingBytes <= 0) return { recycle: false, reason: 'disabled' };
174
+ if (!Number.isFinite(rssBytes) || rssBytes <= ceilingBytes) return { recycle: false, reason: 'under-ceiling' };
175
+ if (!(ticksCompleted >= 1)) return { recycle: false, reason: 'no-completed-tick' };
176
+ if (!(uptimeMs >= minUptimeMs)) return { recycle: false, reason: 'min-uptime' };
177
+ return { recycle: true, reason: 'over-ceiling' };
178
+ }
179
+
115
180
  /**
116
181
  * Read the resident-set size (bytes) of an arbitrary pid, best-effort and
117
182
  * cross-platform. Returns a non-negative integer, or 0 when unknown (dead pid,
@@ -26,6 +26,18 @@ let lateInteractionPipeline = null;
26
26
  let loadPromise = null;
27
27
  let lateInteractionRuntimeConfig = {
28
28
  intraOpThreads: null,
29
+ // G3-LI: background/maintainer ORT profile for the LI session. When truthy,
30
+ // the session is created with the CPU mem arena OFF and force_spinning_stop
31
+ // instead of allow_spinning — the same profile buildLocalSessionOptions
32
+ // applies to the dense session. ORT never returns arena memory once grown
33
+ // (#25325), and per-file reconcile encodes produce highly variable batch
34
+ // shapes, so an arena-ON LI session in the resident maintainer accrues
35
+ // monotonic RSS in 128MB arena-extension steps (measured: 354×128MB ≈ 34GB
36
+ // after one heavy edit day). Must be set BEFORE the first encode — the
37
+ // session singleton is built once; configuring after is a silent no-op.
38
+ // Default null/off everywhere else (search/query path keeps the foreground
39
+ // arena+spinning profile for latency).
40
+ background: null,
29
41
  };
30
42
 
31
43
  // Lightweight timing accumulators for profiling (Phase 6a).
@@ -90,6 +102,7 @@ export function configureLateInteractionRuntime(overrides = {}) {
90
102
  export function resetLateInteractionRuntime() {
91
103
  lateInteractionRuntimeConfig = {
92
104
  intraOpThreads: null,
105
+ background: null,
93
106
  };
94
107
  }
95
108
 
@@ -191,18 +204,26 @@ async function loadModel() {
191
204
  // 'extended' + memArena + memPattern: marginal overhead for no measurable gain.
192
205
  // Conclusion: keep LI session lean. Only proven-beneficial options added.
193
206
  const { getOptimizedGraphPath } = await import('../infrastructure/onnx-session-utils.js');
207
+ const liBackground = !!lateInteractionRuntimeConfig.background;
194
208
  const session = await ort.InferenceSession.create(onnxPath, {
195
209
  executionProviders: ['cpu'],
196
210
  logSeverityLevel: 3, // ERROR — silence ORT's expected "optimized model is machine-specific" warning
197
211
  intraOpNumThreads: lateInteractionRuntimeConfig.intraOpThreads ?? bestIntraOpThreads(),
198
212
  interOpNumThreads: 1,
199
213
  optimizedModelFilePath: getOptimizedGraphPath(modelConfig.hfId, 'lateon'),
214
+ // G3-LI: background (maintainer) profile disables the CPU mem arena — ORT
215
+ // never returns arena memory once grown (#25325), and variable-shaped
216
+ // per-file reconcile batches make the arena extend monotonically (128MB
217
+ // steps) in a resident daemon. Foreground keeps the arena for throughput.
218
+ enableCpuMemArena: !liBackground,
200
219
  // Thread spinning keeps ORT worker threads hot between batches — trades idle
201
- // CPU for lower per-batch latency during sustained indexing runs.
220
+ // CPU for lower per-batch latency during sustained indexing runs. The
221
+ // background profile parks workers after the last Run() instead (the
222
+ // maintainer sits idle 20-60s between ticks; ~14% re-spin latency cost).
202
223
  extra: {
203
- session: {
204
- intra_op: { allow_spinning: '1' },
205
- },
224
+ session: liBackground
225
+ ? { force_spinning_stop: '1' }
226
+ : { intra_op: { allow_spinning: '1' } },
206
227
  },
207
228
  });
208
229
  const coremlActive = false;
@@ -11,8 +11,8 @@
11
11
  * CANONICAL INT8 STORAGE (Workstream H resolution):
12
12
  * Int8 vectors for stage-2 rescoring are stored in this index's .int8.json sidecar.
13
13
  * This file is saved/loaded alongside the binary HNSW index artifacts.
14
- * - save(): Writes .int8.json with { id: Int8Array[], ... } format
15
- * - load(): Populates this.int8Vectors Map from .int8.json
14
+ * - save(): Writes .int8.json (NDJSON v2: header line + one {id, v} per line)
15
+ * - load(): Populates this.int8Vectors Map from .int8.json (v1 back-compat)
16
16
  * - getInt8Vector(id): O(1) lookup used by sweet-search.js during stage-2
17
17
  *
18
18
  * This is the ONLY source of int8 vectors. The SQLite approach (codebase-int8.db)
@@ -28,9 +28,11 @@
28
28
 
29
29
  import fs from 'fs/promises';
30
30
  import {
31
- existsSync, statSync,
31
+ existsSync, statSync, readFileSync,
32
32
  openSync, readSync, closeSync, fstatSync,
33
+ createWriteStream, createReadStream,
33
34
  } from 'fs';
35
+ import readline from 'readline';
34
36
  import path from 'path';
35
37
  import { BINARY_HNSW_CONFIG, DB_PATHS } from '../infrastructure/config/index.js';
36
38
  import {
@@ -53,11 +55,12 @@ const PIPELINE_VERSION = 2;
53
55
  //
54
56
  // The default on-disk format is the JSON sidecar tuple
55
57
  // (.meta.json / .vectors.json / .graph.json / .int8.json / .calibration.json),
56
- // written by save() and read by load(). That format is preserved BYTE-FOR-BYTE
57
- // when SWEET_SEARCH_HNSW_MMAP !== '1' this whole subsystem is inert by
58
- // default. Existing on-disk indexes (the 200 bench repos) keep loading via the
59
- // JSON path regardless of the flag: format is detected by inspecting the .idx
60
- // file's magic header (`_indexFormatOnDisk`), never by the flag.
58
+ // written by save() and read by load() the big sidecars as NDJSON v2 with
59
+ // v1 read back-compat (see the sidecar-format block below). This whole mmap
60
+ // subsystem is inert when SWEET_SEARCH_HNSW_MMAP !== '1'. Existing on-disk
61
+ // indexes (the 200 bench repos) keep loading via the JSON path regardless of
62
+ // the flag: format is detected by inspecting the .idx file's magic header
63
+ // (`_indexFormatOnDisk`), never by the flag.
61
64
  //
62
65
  // When SWEET_SEARCH_HNSW_MMAP === '1', NEWLY written indexes are packed into a
63
66
  // single flat-binary `.idx` file (magic-prefixed) whose vectors / graph
@@ -202,6 +205,276 @@ function makeMmapVectorProxy(store) {
202
205
  });
203
206
  }
204
207
 
208
+ // =============================================================================
209
+ // NDJSON SIDECAR FORMAT (v2)
210
+ // =============================================================================
211
+ //
212
+ // The big JSON sidecars (.vectors.json / .graph.json / .int8.json) used to be
213
+ // written with one monolithic JSON.stringify. Past ~500k vectors that single
214
+ // string crosses V8's ~512 MB ceiling and save() dies with "Invalid string
215
+ // length" (libsql: 637,550 vectors) — the same failure mode the LI alias
216
+ // sidecar hit, fixed the same way in 2.6.9. Cure: NDJSON v2 — one
217
+ // {version:2,...} header line, then one record per line, flushed through a
218
+ // write stream in bounded batches so no serialized string ever approaches the
219
+ // ceiling. .meta.json / .calibration.json stay monolithic JSON (O(1)-sized).
220
+ //
221
+ // Back-compat: a v1 sidecar is a single line holding the whole payload, and
222
+ // any v1 file on disk was produced by a JSON.stringify that SUCCEEDED — so it
223
+ // fits back into one string, and the line-streaming readers handle it as
224
+ // "first line IS the payload". Format is detected from the first line, never
225
+ // from config, so old and new indexes coexist with zero migration.
226
+ //
227
+ // Truncation guard: NDJSON cut at a line boundary (crash mid-write, disk
228
+ // full) is valid prefix NDJSON and would otherwise load a silent subset, so
229
+ // every v2 reader enforces the header's record count and throws on mismatch —
230
+ // callers route that to the rebuild path like any other corrupt artifact.
231
+ // (The tmp+rename publish in save() makes this unreachable in normal
232
+ // operation; the guard covers torn tmp files that somehow got renamed.)
233
+
234
+ const NDJSON_LINE_BATCH = 20000;
235
+ // Graph chunk lines are ~MB-sized, so batch flushes are bounded by chars too.
236
+ const NDJSON_CHAR_BATCH = 8 * 1024 * 1024;
237
+ const GRAPH_CHUNK_NODES = 10000;
238
+
239
+ async function writeNdjsonSidecar(filePath, header, records) {
240
+ const ws = createWriteStream(filePath, { encoding: 'utf8' });
241
+ const finished = new Promise((resolve, reject) => {
242
+ ws.on('error', reject);
243
+ ws.on('finish', resolve);
244
+ });
245
+ const flush = (str) => new Promise((resolve, reject) => {
246
+ ws.write(str, (err) => (err ? reject(err) : resolve()));
247
+ });
248
+ try {
249
+ let lines = [JSON.stringify(header)];
250
+ let chars = lines[0].length;
251
+ for (const record of records) {
252
+ const line = JSON.stringify(record);
253
+ lines.push(line);
254
+ chars += line.length;
255
+ if (lines.length >= NDJSON_LINE_BATCH || chars >= NDJSON_CHAR_BATCH) {
256
+ await flush(lines.join('\n') + '\n');
257
+ lines = [];
258
+ chars = 0;
259
+ }
260
+ }
261
+ if (lines.length > 0) await flush(lines.join('\n') + '\n');
262
+ ws.end();
263
+ await finished;
264
+ } catch (err) {
265
+ finished.catch(() => { /* surfaced via throw below */ });
266
+ ws.destroy();
267
+ throw err;
268
+ }
269
+ }
270
+
271
+ // Yields one parsed JSON document per non-empty line. The input stream is
272
+ // destroyed in `finally`: early exits abandon the readline iterator, and
273
+ // rl.close() does NOT destroy its input — without the explicit destroy every
274
+ // early return leaks an fd (fatal only in long-lived processes like the
275
+ // daemon, but a leak everywhere).
276
+ async function* iterateJsonLines(filePath) {
277
+ const input = createReadStream(filePath, { encoding: 'utf8' });
278
+ const rl = readline.createInterface({ input, crlfDelay: Infinity });
279
+ try {
280
+ for await (const line of rl) {
281
+ if (!line) continue;
282
+ yield JSON.parse(line);
283
+ }
284
+ } finally {
285
+ rl.close();
286
+ input.destroy();
287
+ }
288
+ }
289
+
290
+ function isV2Header(doc) {
291
+ return !!doc && !Array.isArray(doc) && doc.version === 2 && Number.isFinite(doc.count);
292
+ }
293
+
294
+ export async function readVectorsSidecar(vectorsPath) {
295
+ let header = null;
296
+ let first = true;
297
+ const entries = [];
298
+ for await (const doc of iterateJsonLines(vectorsPath)) {
299
+ if (first) {
300
+ first = false;
301
+ if (Array.isArray(doc)) return doc; // v1 — the whole payload is this line
302
+ if (!isV2Header(doc)) {
303
+ throw new Error(`BinaryHNSW: unrecognized vectors sidecar format: ${vectorsPath}`);
304
+ }
305
+ header = doc;
306
+ continue;
307
+ }
308
+ entries.push(doc);
309
+ }
310
+ if (!header) {
311
+ throw new Error(`BinaryHNSW: empty vectors sidecar: ${vectorsPath}`);
312
+ }
313
+ if (entries.length !== header.count) {
314
+ throw new Error(
315
+ `BinaryHNSW: truncated vectors sidecar (${entries.length}/${header.count} records): ${vectorsPath}`
316
+ );
317
+ }
318
+ return entries;
319
+ }
320
+
321
+ // Graph v2 records tile each level's node array in fixed-size chunks. Holes in
322
+ // the (sparse) upper-level arrays serialize to null inside a chunk, matching
323
+ // what JSON.parse produced for the v1 whole-array format.
324
+ function* graphChunkRecords(graph) {
325
+ for (let level = 0; level < graph.length; level++) {
326
+ const nodes = graph[level];
327
+ for (let start = 0; start < nodes.length; start += GRAPH_CHUNK_NODES) {
328
+ yield { level, start, nodes: nodes.slice(start, start + GRAPH_CHUNK_NODES) };
329
+ }
330
+ }
331
+ }
332
+
333
+ function graphChunkCount(graph) {
334
+ let count = 0;
335
+ for (const nodes of graph) count += Math.ceil(nodes.length / GRAPH_CHUNK_NODES);
336
+ return count;
337
+ }
338
+
339
+ export async function readGraphSidecar(graphPath) {
340
+ let header = null;
341
+ let first = true;
342
+ let graph = null;
343
+ let parsed = 0;
344
+ for await (const doc of iterateJsonLines(graphPath)) {
345
+ if (first) {
346
+ first = false;
347
+ if (Array.isArray(doc)) return doc; // v1 — the whole payload is this line
348
+ if (!doc || doc.version !== 2 || !Array.isArray(doc.lengths) || !Number.isFinite(doc.chunks)) {
349
+ throw new Error(`BinaryHNSW: unrecognized graph sidecar format: ${graphPath}`);
350
+ }
351
+ header = doc;
352
+ graph = doc.lengths.map((len) => new Array(len).fill(null));
353
+ continue;
354
+ }
355
+ const target = graph[doc.level];
356
+ for (let i = 0; i < doc.nodes.length; i++) {
357
+ target[doc.start + i] = doc.nodes[i];
358
+ }
359
+ parsed++;
360
+ }
361
+ if (!header) {
362
+ throw new Error(`BinaryHNSW: empty graph sidecar: ${graphPath}`);
363
+ }
364
+ if (parsed !== header.chunks) {
365
+ throw new Error(
366
+ `BinaryHNSW: truncated graph sidecar (${parsed}/${header.chunks} chunks): ${graphPath}`
367
+ );
368
+ }
369
+ return graph;
370
+ }
371
+
372
+ export async function readInt8Sidecar(int8Path, out) {
373
+ let header = null;
374
+ let first = true;
375
+ let parsed = 0;
376
+ for await (const doc of iterateJsonLines(int8Path)) {
377
+ if (first) {
378
+ first = false;
379
+ if (!isV2Header(doc)) {
380
+ // v1 — the whole { id: number[] } map is this line. A v1 map can never
381
+ // look like a v2 header: its values are arrays, so doc.version is
382
+ // either undefined or a number[] and fails isV2Header.
383
+ for (const [id, vec] of Object.entries(doc || {})) {
384
+ out.set(id, new Int8Array(vec));
385
+ }
386
+ return;
387
+ }
388
+ header = doc;
389
+ continue;
390
+ }
391
+ out.set(doc.id, new Int8Array(doc.v));
392
+ parsed++;
393
+ }
394
+ if (header && parsed !== header.count) {
395
+ throw new Error(
396
+ `BinaryHNSW: truncated int8 sidecar (${parsed}/${header.count} records): ${int8Path}`
397
+ );
398
+ }
399
+ }
400
+
401
+ // Entry count without materializing the payload: v2 reads only the header
402
+ // line; v1 (single line = whole map) is parsed from that same first line.
403
+ // Used by diagnostics (getArtifactStats) so stats never re-load the index.
404
+ export async function int8SidecarCount(int8Path) {
405
+ for await (const doc of iterateJsonLines(int8Path)) {
406
+ return isV2Header(doc) ? doc.count : Object.keys(doc || {}).length;
407
+ }
408
+ return 0;
409
+ }
410
+
411
+ // ---------------------------------------------------------------------------
412
+ // Sync sidecar readers — TOOLING ONLY (soak probes, determinism dumps).
413
+ // These readFileSync the whole file, so they inherit the very string ceiling
414
+ // the streaming readers exist to avoid; fine at tool-corpus scale, never for
415
+ // production-scale indexes. Same v1/v2 detection and truncation guards.
416
+ // ---------------------------------------------------------------------------
417
+
418
+ function parseSidecarLinesSync(filePath) {
419
+ return readFileSync(filePath, 'utf-8')
420
+ .split('\n')
421
+ .filter((l) => l.length > 0)
422
+ .map((l) => JSON.parse(l));
423
+ }
424
+
425
+ export function readVectorsSidecarSync(vectorsPath) {
426
+ const docs = parseSidecarLinesSync(vectorsPath);
427
+ if (Array.isArray(docs[0])) return docs[0]; // v1
428
+ if (!isV2Header(docs[0])) {
429
+ throw new Error(`BinaryHNSW: unrecognized vectors sidecar format: ${vectorsPath}`);
430
+ }
431
+ const entries = docs.slice(1);
432
+ if (entries.length !== docs[0].count) {
433
+ throw new Error(
434
+ `BinaryHNSW: truncated vectors sidecar (${entries.length}/${docs[0].count} records): ${vectorsPath}`
435
+ );
436
+ }
437
+ return entries;
438
+ }
439
+
440
+ export function readGraphSidecarSync(graphPath) {
441
+ const docs = parseSidecarLinesSync(graphPath);
442
+ if (Array.isArray(docs[0])) return docs[0]; // v1
443
+ const header = docs[0];
444
+ if (!header || header.version !== 2 || !Array.isArray(header.lengths) || !Number.isFinite(header.chunks)) {
445
+ throw new Error(`BinaryHNSW: unrecognized graph sidecar format: ${graphPath}`);
446
+ }
447
+ if (docs.length - 1 !== header.chunks) {
448
+ throw new Error(
449
+ `BinaryHNSW: truncated graph sidecar (${docs.length - 1}/${header.chunks} chunks): ${graphPath}`
450
+ );
451
+ }
452
+ const graph = header.lengths.map((len) => new Array(len).fill(null));
453
+ for (const rec of docs.slice(1)) {
454
+ const target = graph[rec.level];
455
+ for (let i = 0; i < rec.nodes.length; i++) {
456
+ target[rec.start + i] = rec.nodes[i];
457
+ }
458
+ }
459
+ return graph;
460
+ }
461
+
462
+ // Returns the v1-shaped plain object { id: number[] } for both formats.
463
+ export function readInt8SidecarSync(int8Path) {
464
+ const docs = parseSidecarLinesSync(int8Path);
465
+ if (docs.length === 0) return {};
466
+ if (!isV2Header(docs[0])) return docs[0] || {}; // v1 — whole map on one line
467
+ const records = docs.slice(1);
468
+ if (records.length !== docs[0].count) {
469
+ throw new Error(
470
+ `BinaryHNSW: truncated int8 sidecar (${records.length}/${docs[0].count} records): ${int8Path}`
471
+ );
472
+ }
473
+ const out = {};
474
+ for (const rec of records) out[rec.id] = rec.v;
475
+ return out;
476
+ }
477
+
205
478
  // =============================================================================
206
479
  // BINARY HNSW INDEX CLASS
207
480
  // =============================================================================
@@ -1070,12 +1343,6 @@ export class BinaryHNSWIndex {
1070
1343
  savedAt: new Date().toISOString(),
1071
1344
  };
1072
1345
 
1073
- const vectorsData = this.vectors.map(v => ({
1074
- id: v.id,
1075
- binary: Array.from(v.binary),
1076
- metadata: v.metadata,
1077
- }));
1078
-
1079
1346
  const metaPath = indexPath.replace('.idx', '.meta.json');
1080
1347
  const vectorsPath = indexPath.replace('.idx', '.vectors.json');
1081
1348
  const graphPath = indexPath.replace('.idx', '.graph.json');
@@ -1083,21 +1350,53 @@ export class BinaryHNSWIndex {
1083
1350
  const calibPath = indexPath.replace('.idx', '.calibration.json');
1084
1351
  const pidSuffix = `.tmp.${process.pid}`;
1085
1352
 
1086
- // Stage all sidecars to sibling temp paths.
1353
+ // Stage all sidecars to sibling temp paths. The big three are NDJSON v2
1354
+ // (see the sidecar-format block above the class) — one record per line
1355
+ // through a write stream — so no serialized string can cross V8's ceiling
1356
+ // on large indexes. Records are yielded lazily so no whole-index
1357
+ // intermediate array/object is materialized either.
1087
1358
  await fs.writeFile(metaPath + pidSuffix, JSON.stringify(meta, null, 2));
1088
- await fs.writeFile(vectorsPath + pidSuffix, JSON.stringify(vectorsData));
1089
- await fs.writeFile(graphPath + pidSuffix, JSON.stringify(this.graph));
1359
+
1360
+ const vectors = this.vectors;
1361
+ await writeNdjsonSidecar(
1362
+ vectorsPath + pidSuffix,
1363
+ { version: 2, count: vectors.length },
1364
+ (function* () {
1365
+ for (const v of vectors) {
1366
+ yield { id: v.id, binary: Array.from(v.binary), metadata: v.metadata };
1367
+ }
1368
+ })()
1369
+ );
1370
+
1371
+ await writeNdjsonSidecar(
1372
+ graphPath + pidSuffix,
1373
+ {
1374
+ version: 2,
1375
+ lengths: this.graph.map((nodes) => nodes.length),
1376
+ chunks: graphChunkCount(this.graph),
1377
+ },
1378
+ graphChunkRecords(this.graph)
1379
+ );
1090
1380
 
1091
1381
  let stagedInt8 = false;
1092
1382
  if (this.int8Vectors.size > 0) {
1093
1383
  const liveIds = new Set(this.vectors.map(v => v.id));
1094
- const int8Data = {};
1095
- for (const [id, vec] of this.int8Vectors) {
1096
- if (!liveIds.has(id)) continue;
1097
- int8Data[id] = Array.from(vec);
1384
+ let liveCount = 0;
1385
+ for (const id of this.int8Vectors.keys()) {
1386
+ if (liveIds.has(id)) liveCount++;
1098
1387
  }
1099
- if (Object.keys(int8Data).length > 0) {
1100
- await fs.writeFile(int8Path + pidSuffix, JSON.stringify(int8Data));
1388
+ if (liveCount > 0) {
1389
+ const int8Vectors = this.int8Vectors;
1390
+ await writeNdjsonSidecar(
1391
+ int8Path + pidSuffix,
1392
+ { version: 2, count: liveCount },
1393
+ (function* () {
1394
+ for (const [id, vec] of int8Vectors) {
1395
+ if (!liveIds.has(id)) continue;
1396
+ yield { id, v: Array.from(vec) };
1397
+ }
1398
+ })()
1399
+ );
1101
1400
  stagedInt8 = true;
1102
1401
  }
1103
1402
  }
@@ -1183,7 +1482,7 @@ export class BinaryHNSWIndex {
1183
1482
  let attempt = 0;
1184
1483
  while (true) {
1185
1484
  meta = attempt === 0 ? initialMeta : JSON.parse(await fs.readFile(metaPath, 'utf-8'));
1186
- vectorsData = JSON.parse(await fs.readFile(vectorsPath, 'utf-8'));
1485
+ vectorsData = await readVectorsSidecar(vectorsPath);
1187
1486
  const consistent = meta.vectorCount === vectorsData.length
1188
1487
  && (meta.entryPoint === -1 || meta.entryPoint < vectorsData.length);
1189
1488
  if (consistent) break;
@@ -1224,15 +1523,12 @@ export class BinaryHNSWIndex {
1224
1523
  }
1225
1524
 
1226
1525
  // Load graph
1227
- this.graph = JSON.parse(await fs.readFile(graphPath, 'utf-8'));
1526
+ this.graph = await readGraphSidecar(graphPath);
1228
1527
 
1229
1528
  // Load int8 vectors if available
1230
1529
  this.int8Vectors.clear();
1231
1530
  if (existsSync(int8Path)) {
1232
- const int8Data = JSON.parse(await fs.readFile(int8Path, 'utf-8'));
1233
- for (const [id, vec] of Object.entries(int8Data)) {
1234
- this.int8Vectors.set(id, new Int8Array(vec));
1235
- }
1531
+ await readInt8Sidecar(int8Path, this.int8Vectors);
1236
1532
  }
1237
1533
 
1238
1534
  // Load asymmetric calibration data
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sweet-search",
3
- "version": "2.6.9",
3
+ "version": "2.6.10",
4
4
  "description": "Sweet Search - SOTA Hybrid Code Search Engine with WASM CatBoost Query Router, Semantic/Lexical/Structural Search, and Multilingual Support",
5
5
  "type": "module",
6
6
  "main": "core/search/sweet-search.js",
@@ -167,13 +167,13 @@
167
167
  "vitest": "^4.0.16"
168
168
  },
169
169
  "optionalDependencies": {
170
- "@sweet-search/native-darwin-arm64": "2.6.9",
171
- "@sweet-search/native-darwin-x64": "2.6.9",
172
- "@sweet-search/native-linux-arm64-gnu": "2.6.9",
173
- "@sweet-search/native-linux-arm64-gnu-cuda": "2.6.9",
174
- "@sweet-search/native-linux-x64-gnu": "2.6.9",
175
- "@sweet-search/native-linux-x64-gnu-cuda": "2.6.9",
176
- "@sweet-search/bg-priority": "2.6.9"
170
+ "@sweet-search/native-darwin-arm64": "2.6.10",
171
+ "@sweet-search/native-darwin-x64": "2.6.10",
172
+ "@sweet-search/native-linux-arm64-gnu": "2.6.10",
173
+ "@sweet-search/native-linux-arm64-gnu-cuda": "2.6.10",
174
+ "@sweet-search/native-linux-x64-gnu": "2.6.10",
175
+ "@sweet-search/native-linux-x64-gnu-cuda": "2.6.10",
176
+ "@sweet-search/bg-priority": "2.6.10"
177
177
  },
178
178
  "engines": {
179
179
  "node": ">=18.0.0"