@threadbase-sh/streamer 1.23.0 → 1.23.1

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
@@ -454,7 +454,24 @@ declare class ConversationCache {
454
454
  getSessionName(sessionId: string): string | null;
455
455
  listSessionNames(): Record<string, string>;
456
456
  invalidate(id?: string): void;
457
- invalidateByFilePath(filePath: string): string | null;
457
+ /**
458
+ * Drop the cached row for a file. Two callers with opposite intent:
459
+ * - a directory-watch "change" event (the file was appended to) — pass
460
+ * `skipIfTailed: true`. A cached tail means the row is being actively
461
+ * maintained from the file's real content by the live-tail
462
+ * (updateFromLines) or warm-up path, fresher than any scanner-derived
463
+ * view. Both watchers fire on the same append with no ordering guarantee;
464
+ * without this guard the invalidate can land after the tail write and wipe
465
+ * the just-cached row, flickering the conversation out of
466
+ * /api/conversations on nearly every message (CRITICAL #2). The debounced
467
+ * rescan still re-derives metadata, so skipping the eager drop loses
468
+ * nothing.
469
+ * - a genuine unlink (the file is gone) — leave `skipIfTailed` false so the
470
+ * row is always removed, otherwise a deleted session ghosts in the cache.
471
+ */
472
+ invalidateByFilePath(filePath: string, opts?: {
473
+ skipIfTailed?: boolean;
474
+ }): string | null;
458
475
  /**
459
476
  * Drop rows whose `file_path` no longer exists on disk AND which have no
460
477
  * cached tail to fall back to. Rows with a tail are left alone so
package/dist/index.d.ts CHANGED
@@ -454,7 +454,24 @@ declare class ConversationCache {
454
454
  getSessionName(sessionId: string): string | null;
455
455
  listSessionNames(): Record<string, string>;
456
456
  invalidate(id?: string): void;
457
- invalidateByFilePath(filePath: string): string | null;
457
+ /**
458
+ * Drop the cached row for a file. Two callers with opposite intent:
459
+ * - a directory-watch "change" event (the file was appended to) — pass
460
+ * `skipIfTailed: true`. A cached tail means the row is being actively
461
+ * maintained from the file's real content by the live-tail
462
+ * (updateFromLines) or warm-up path, fresher than any scanner-derived
463
+ * view. Both watchers fire on the same append with no ordering guarantee;
464
+ * without this guard the invalidate can land after the tail write and wipe
465
+ * the just-cached row, flickering the conversation out of
466
+ * /api/conversations on nearly every message (CRITICAL #2). The debounced
467
+ * rescan still re-derives metadata, so skipping the eager drop loses
468
+ * nothing.
469
+ * - a genuine unlink (the file is gone) — leave `skipIfTailed` false so the
470
+ * row is always removed, otherwise a deleted session ghosts in the cache.
471
+ */
472
+ invalidateByFilePath(filePath: string, opts?: {
473
+ skipIfTailed?: boolean;
474
+ }): string | null;
458
475
  /**
459
476
  * Drop rows whose `file_path` no longer exists on disk AND which have no
460
477
  * cached tail to fall back to. Rows with a tail are left alone so
package/dist/index.js CHANGED
@@ -2532,7 +2532,10 @@ var ConversationCache = class _ConversationCache {
2532
2532
  model = excluded.model,
2533
2533
  account = excluded.account,
2534
2534
  branch = excluded.branch,
2535
- message_count = excluded.message_count,
2535
+ -- message_count is incremented by live tailing but recounted from
2536
+ -- scratch by a scanner rescan; a stale rescan must not carry a live
2537
+ -- session's count backwards, so take the max instead of overwriting.
2538
+ message_count = MAX(conversation_meta.message_count, excluded.message_count),
2536
2539
  last_activity = excluded.last_activity,
2537
2540
  first_message = excluded.first_message,
2538
2541
  last_message = excluded.last_message,
@@ -2809,6 +2812,7 @@ var ConversationCache = class _ConversationCache {
2809
2812
  fileSize = s.size;
2810
2813
  } catch {
2811
2814
  }
2815
+ const seq = ++this.tailSeq;
2812
2816
  this.stmts.upsertFull.run({
2813
2817
  id,
2814
2818
  file_path: m.filePath,
@@ -2823,7 +2827,7 @@ var ConversationCache = class _ConversationCache {
2823
2827
  first_message: m.firstMessage ? JSON.stringify(m.firstMessage) : null,
2824
2828
  last_message: m.lastMessage ? JSON.stringify(m.lastMessage) : null,
2825
2829
  preview: m.preview ?? null,
2826
- updated_at: 0,
2830
+ updated_at: seq,
2827
2831
  mtime_ms: mtimeMs,
2828
2832
  file_size: fileSize,
2829
2833
  provider: m.provider ?? CLAUDE_CODE_PROVIDER
@@ -3040,9 +3044,25 @@ var ConversationCache = class _ConversationCache {
3040
3044
  this.fileIndex.clear();
3041
3045
  }
3042
3046
  }
3043
- invalidateByFilePath(filePath) {
3047
+ /**
3048
+ * Drop the cached row for a file. Two callers with opposite intent:
3049
+ * - a directory-watch "change" event (the file was appended to) — pass
3050
+ * `skipIfTailed: true`. A cached tail means the row is being actively
3051
+ * maintained from the file's real content by the live-tail
3052
+ * (updateFromLines) or warm-up path, fresher than any scanner-derived
3053
+ * view. Both watchers fire on the same append with no ordering guarantee;
3054
+ * without this guard the invalidate can land after the tail write and wipe
3055
+ * the just-cached row, flickering the conversation out of
3056
+ * /api/conversations on nearly every message (CRITICAL #2). The debounced
3057
+ * rescan still re-derives metadata, so skipping the eager drop loses
3058
+ * nothing.
3059
+ * - a genuine unlink (the file is gone) — leave `skipIfTailed` false so the
3060
+ * row is always removed, otherwise a deleted session ghosts in the cache.
3061
+ */
3062
+ invalidateByFilePath(filePath, opts) {
3044
3063
  const row = this.stmts.getIdByFilePath.get(filePath);
3045
3064
  if (!row) return null;
3065
+ if (opts?.skipIfTailed && this.stmts.hasTail.get(row.id)) return null;
3046
3066
  this.invalidate(row.id);
3047
3067
  return row.id;
3048
3068
  }
@@ -4268,7 +4288,7 @@ var StreamerServer = class {
4268
4288
  }
4269
4289
  },
4270
4290
  onConversationChanged: (filePath) => {
4271
- this.cache?.invalidateByFilePath(filePath);
4291
+ this.cache?.invalidateByFilePath(filePath, { skipIfTailed: true });
4272
4292
  this.markScannerStaleDebounced();
4273
4293
  this.log.debug?.(`Scanner invalidated by directory event: ${filePath}`, {
4274
4294
  filePath,