@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.d.cts CHANGED
@@ -371,15 +371,18 @@ declare class ConversationScanner {
371
371
  private readonly dbPath;
372
372
  private readonly sidecarEnabled;
373
373
  private engineInstance;
374
+ private inFlightScan;
374
375
  private emitter;
375
376
  private watcher;
376
377
  private queue;
377
378
  private periodicTimer;
379
+ private inFlightReconcile;
378
380
  constructor(options?: ConversationScannerOptions);
379
381
  private get persistent();
380
382
  private engine;
381
- close(): void;
383
+ close(): Promise<void>;
382
384
  scan(options?: ScanOptions): Promise<ScanResult>;
385
+ private runScan;
383
386
  private scanPersistent;
384
387
  private finalize;
385
388
  private scanInMemory;
package/dist/index.d.ts CHANGED
@@ -371,15 +371,18 @@ declare class ConversationScanner {
371
371
  private readonly dbPath;
372
372
  private readonly sidecarEnabled;
373
373
  private engineInstance;
374
+ private inFlightScan;
374
375
  private emitter;
375
376
  private watcher;
376
377
  private queue;
377
378
  private periodicTimer;
379
+ private inFlightReconcile;
378
380
  constructor(options?: ConversationScannerOptions);
379
381
  private get persistent();
380
382
  private engine;
381
- close(): void;
383
+ close(): Promise<void>;
382
384
  scan(options?: ScanOptions): Promise<ScanResult>;
385
+ private runScan;
383
386
  private scanPersistent;
384
387
  private finalize;
385
388
  private scanInMemory;
package/dist/index.js CHANGED
@@ -1781,6 +1781,19 @@ var ConversationFilesRepo = class {
1781
1781
  const rows = this.db.prepare("SELECT absolute_path FROM conversation_files WHERE status != 'deleted'").all();
1782
1782
  return rows.map((r) => r.absolute_path);
1783
1783
  }
1784
+ // Active file paths belonging to any of the given accounts. Backs the
1785
+ // deletion-reconcile so a scan that covered only some accounts can't mark
1786
+ // another account's files deleted (they live in the same shared index.db but
1787
+ // a different profile owns them). Returns [] for an empty account list.
1788
+ activePathsByAccounts(accounts) {
1789
+ if (accounts.length === 0) return [];
1790
+ const placeholders = accounts.map(() => "?").join(", ");
1791
+ const rows = this.db.prepare(
1792
+ `SELECT absolute_path FROM conversation_files
1793
+ WHERE status != 'deleted' AND account IN (${placeholders})`
1794
+ ).all(...accounts);
1795
+ return rows.map((r) => r.absolute_path);
1796
+ }
1784
1797
  // Active files whose immediate parent is exactly parentDir (no nested
1785
1798
  // subdirectories). Backs the dir-mtime gate's reuse path: a project dir with
1786
1799
  // an unchanged mtime and no nested files can skip the glob entirely.
@@ -2153,8 +2166,15 @@ var PersistentEngine = class {
2153
2166
  scanned += batch.length;
2154
2167
  options.onProgress?.(scanned, discovered.length);
2155
2168
  }
2169
+ const coveredAccounts = /* @__PURE__ */ new Set();
2170
+ if (enabled.includes(CLAUDE_CODE_PROVIDER)) {
2171
+ for (const p of activeProfiles) coveredAccounts.add(p.id);
2172
+ }
2173
+ if (enabled.includes(CODEX_CLI_PROVIDER) && (options.codexRoots?.length ?? 0) > 0) {
2174
+ coveredAccounts.add("codex");
2175
+ }
2156
2176
  const seen = new Set(discovered.map((d) => d.filePath));
2157
- for (const path of this.files.allActivePaths()) {
2177
+ for (const path of this.files.activePathsByAccounts([...coveredAccounts])) {
2158
2178
  if (!seen.has(path)) this.markDeleted(path);
2159
2179
  }
2160
2180
  log.info({ scanned, indexed: this.conversations.count() }, "persistent: indexAll complete");
@@ -2464,10 +2484,21 @@ var ConversationScanner = class {
2464
2484
  dbPath;
2465
2485
  sidecarEnabled;
2466
2486
  engineInstance = null;
2487
+ // The most recent scan()'s promise while it is still running, or null when
2488
+ // idle. close() awaits it before closing the SQLite handle so a fire-and-
2489
+ // forget scan can't have its DB shut mid-indexAll ("database connection is
2490
+ // not open"). Tracks the latest scan only — concurrent scans on one instance
2491
+ // aren't a supported pattern (a single writer DB), and the latest resolving
2492
+ // implies earlier ones already settled in practice.
2493
+ inFlightScan = null;
2467
2494
  emitter = new EventEmitter();
2468
2495
  watcher = null;
2469
2496
  queue = null;
2470
2497
  periodicTimer = null;
2498
+ // The in-flight periodicReconcile() job, if one is mid-run. unwatch() awaits
2499
+ // it before dropping the queue/engine so its post-await engine.files access
2500
+ // can't hit a closed DB (the watch-mode half of Bug #4).
2501
+ inFlightReconcile = null;
2471
2502
  constructor(options) {
2472
2503
  this.conversationLRU = new LRUCache(options?.conversationCacheSize ?? 5);
2473
2504
  if (options?.persistent === false) {
@@ -2490,14 +2521,31 @@ var ConversationScanner = class {
2490
2521
  return this.engineInstance;
2491
2522
  }
2492
2523
  // Release the SQLite connection. No-op in legacy mode. Safe to call
2493
- // repeatedly. Stops the watcher first if one is running; call unwatch()
2494
- // explicitly beforehand if you need to await watcher teardown.
2495
- close() {
2496
- if (this.watcher || this.queue || this.periodicTimer) void this.unwatch();
2524
+ // repeatedly. Awaits watcher teardown AND any in-flight scan before closing
2525
+ // the DB handle, so a fire-and-forget scan() can never have its connection
2526
+ // shut mid-indexAll() ("database connection is not open"). This is why close()
2527
+ // is async callers should await it during shutdown. (Scanner review Bug #4.)
2528
+ async close() {
2529
+ if (this.watcher || this.queue || this.periodicTimer) await this.unwatch();
2530
+ if (this.inFlightScan) {
2531
+ try {
2532
+ await this.inFlightScan;
2533
+ } catch {
2534
+ }
2535
+ }
2497
2536
  this.engineInstance?.close();
2498
2537
  this.engineInstance = null;
2499
2538
  }
2500
2539
  async scan(options = {}) {
2540
+ const promise = this.runScan(options);
2541
+ this.inFlightScan = promise;
2542
+ try {
2543
+ return await promise;
2544
+ } finally {
2545
+ if (this.inFlightScan === promise) this.inFlightScan = null;
2546
+ }
2547
+ }
2548
+ async runScan(options) {
2501
2549
  const profiles = await this.resolveProfiles(options.profiles);
2502
2550
  const activeProfiles = profiles.filter((p) => p.enabled && p.scanHistory !== false);
2503
2551
  this.lastTier = resolveTier(options.tier ?? "standard", options.tiers);
@@ -2933,7 +2981,10 @@ var ConversationScanner = class {
2933
2981
  const periodicMs = options.periodicMs ?? 6e4;
2934
2982
  if (periodicMs > 0) {
2935
2983
  this.periodicTimer = setInterval(() => {
2936
- void this.periodicReconcile(activeProfiles);
2984
+ const job = this.periodicReconcile(activeProfiles).finally(() => {
2985
+ if (this.inFlightReconcile === job) this.inFlightReconcile = null;
2986
+ });
2987
+ this.inFlightReconcile = job;
2937
2988
  }, periodicMs);
2938
2989
  this.periodicTimer.unref?.();
2939
2990
  }
@@ -2977,6 +3028,9 @@ var ConversationScanner = class {
2977
3028
  clearInterval(this.periodicTimer);
2978
3029
  this.periodicTimer = null;
2979
3030
  }
3031
+ if (this.inFlightReconcile) {
3032
+ await this.inFlightReconcile;
3033
+ }
2980
3034
  if (this.watcher) {
2981
3035
  await this.watcher.stop();
2982
3036
  this.watcher = null;