@threadbase-sh/scanner 0.9.1 → 0.9.3

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/index.cjs CHANGED
@@ -1845,6 +1845,19 @@ var ConversationFilesRepo = class {
1845
1845
  const rows = this.db.prepare("SELECT absolute_path FROM conversation_files WHERE status != 'deleted'").all();
1846
1846
  return rows.map((r) => r.absolute_path);
1847
1847
  }
1848
+ // Active file paths belonging to any of the given accounts. Backs the
1849
+ // deletion-reconcile so a scan that covered only some accounts can't mark
1850
+ // another account's files deleted (they live in the same shared index.db but
1851
+ // a different profile owns them). Returns [] for an empty account list.
1852
+ activePathsByAccounts(accounts) {
1853
+ if (accounts.length === 0) return [];
1854
+ const placeholders = accounts.map(() => "?").join(", ");
1855
+ const rows = this.db.prepare(
1856
+ `SELECT absolute_path FROM conversation_files
1857
+ WHERE status != 'deleted' AND account IN (${placeholders})`
1858
+ ).all(...accounts);
1859
+ return rows.map((r) => r.absolute_path);
1860
+ }
1848
1861
  // Active files whose immediate parent is exactly parentDir (no nested
1849
1862
  // subdirectories). Backs the dir-mtime gate's reuse path: a project dir with
1850
1863
  // an unchanged mtime and no nested files can skip the glob entirely.
@@ -2217,8 +2230,15 @@ var PersistentEngine = class {
2217
2230
  scanned += batch.length;
2218
2231
  options.onProgress?.(scanned, discovered.length);
2219
2232
  }
2233
+ const coveredAccounts = /* @__PURE__ */ new Set();
2234
+ if (enabled.includes(CLAUDE_CODE_PROVIDER)) {
2235
+ for (const p of activeProfiles) coveredAccounts.add(p.id);
2236
+ }
2237
+ if (enabled.includes(CODEX_CLI_PROVIDER) && (options.codexRoots?.length ?? 0) > 0) {
2238
+ coveredAccounts.add("codex");
2239
+ }
2220
2240
  const seen = new Set(discovered.map((d) => d.filePath));
2221
- for (const path of this.files.allActivePaths()) {
2241
+ for (const path of this.files.activePathsByAccounts([...coveredAccounts])) {
2222
2242
  if (!seen.has(path)) this.markDeleted(path);
2223
2243
  }
2224
2244
  log.info({ scanned, indexed: this.conversations.count() }, "persistent: indexAll complete");
@@ -2528,10 +2548,21 @@ var ConversationScanner = class {
2528
2548
  dbPath;
2529
2549
  sidecarEnabled;
2530
2550
  engineInstance = null;
2551
+ // The most recent scan()'s promise while it is still running, or null when
2552
+ // idle. close() awaits it before closing the SQLite handle so a fire-and-
2553
+ // forget scan can't have its DB shut mid-indexAll ("database connection is
2554
+ // not open"). Tracks the latest scan only — concurrent scans on one instance
2555
+ // aren't a supported pattern (a single writer DB), and the latest resolving
2556
+ // implies earlier ones already settled in practice.
2557
+ inFlightScan = null;
2531
2558
  emitter = new import_events.EventEmitter();
2532
2559
  watcher = null;
2533
2560
  queue = null;
2534
2561
  periodicTimer = null;
2562
+ // The in-flight periodicReconcile() job, if one is mid-run. unwatch() awaits
2563
+ // it before dropping the queue/engine so its post-await engine.files access
2564
+ // can't hit a closed DB (the watch-mode half of Bug #4).
2565
+ inFlightReconcile = null;
2535
2566
  constructor(options) {
2536
2567
  this.conversationLRU = new LRUCache(options?.conversationCacheSize ?? 5);
2537
2568
  if (options?.persistent === false) {
@@ -2554,14 +2585,31 @@ var ConversationScanner = class {
2554
2585
  return this.engineInstance;
2555
2586
  }
2556
2587
  // Release the SQLite connection. No-op in legacy mode. Safe to call
2557
- // repeatedly. Stops the watcher first if one is running; call unwatch()
2558
- // explicitly beforehand if you need to await watcher teardown.
2559
- close() {
2560
- if (this.watcher || this.queue || this.periodicTimer) void this.unwatch();
2588
+ // repeatedly. Awaits watcher teardown AND any in-flight scan before closing
2589
+ // the DB handle, so a fire-and-forget scan() can never have its connection
2590
+ // shut mid-indexAll() ("database connection is not open"). This is why close()
2591
+ // is async callers should await it during shutdown. (Scanner review Bug #4.)
2592
+ async close() {
2593
+ if (this.watcher || this.queue || this.periodicTimer) await this.unwatch();
2594
+ if (this.inFlightScan) {
2595
+ try {
2596
+ await this.inFlightScan;
2597
+ } catch {
2598
+ }
2599
+ }
2561
2600
  this.engineInstance?.close();
2562
2601
  this.engineInstance = null;
2563
2602
  }
2564
2603
  async scan(options = {}) {
2604
+ const promise = this.runScan(options);
2605
+ this.inFlightScan = promise;
2606
+ try {
2607
+ return await promise;
2608
+ } finally {
2609
+ if (this.inFlightScan === promise) this.inFlightScan = null;
2610
+ }
2611
+ }
2612
+ async runScan(options) {
2565
2613
  const profiles = await this.resolveProfiles(options.profiles);
2566
2614
  const activeProfiles = profiles.filter((p) => p.enabled && p.scanHistory !== false);
2567
2615
  this.lastTier = resolveTier(options.tier ?? "standard", options.tiers);
@@ -2997,7 +3045,10 @@ var ConversationScanner = class {
2997
3045
  const periodicMs = options.periodicMs ?? 6e4;
2998
3046
  if (periodicMs > 0) {
2999
3047
  this.periodicTimer = setInterval(() => {
3000
- void this.periodicReconcile(activeProfiles);
3048
+ const job = this.periodicReconcile(activeProfiles).finally(() => {
3049
+ if (this.inFlightReconcile === job) this.inFlightReconcile = null;
3050
+ });
3051
+ this.inFlightReconcile = job;
3001
3052
  }, periodicMs);
3002
3053
  this.periodicTimer.unref?.();
3003
3054
  }
@@ -3041,6 +3092,9 @@ var ConversationScanner = class {
3041
3092
  clearInterval(this.periodicTimer);
3042
3093
  this.periodicTimer = null;
3043
3094
  }
3095
+ if (this.inFlightReconcile) {
3096
+ await this.inFlightReconcile;
3097
+ }
3044
3098
  if (this.watcher) {
3045
3099
  await this.watcher.stop();
3046
3100
  this.watcher = null;