@threadbase-sh/streamer 1.36.1 → 1.36.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
@@ -1200,6 +1200,7 @@ declare class StreamerServer {
1200
1200
  private allScanners;
1201
1201
  private scannerReady;
1202
1202
  private scannerStale;
1203
+ private conversationReconcileInFlight;
1203
1204
  private refreshInFlight;
1204
1205
  private binding;
1205
1206
  private activeWarmups;
@@ -1285,6 +1286,16 @@ declare class StreamerServer {
1285
1286
  private checkExchangeRateLimit;
1286
1287
  private checkSessionStartRateLimit;
1287
1288
  private checkSessionInputRateLimit;
1289
+ /** Project roots watched for conversation JSONLs (profiles or ~/.claude/projects). */
1290
+ private projectsDirsForFreshnessCheck;
1291
+ /**
1292
+ * Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by
1293
+ * the automatic freshness path when the directory watcher marked the scanner
1294
+ * stale or shouldRefreshProjectsFromHdd detected disk drift.
1295
+ */
1296
+ private reconcileConversationsCacheFromDisk;
1297
+ private startBackgroundConversationReconcile;
1298
+ private shouldAutoReconcileConversationList;
1288
1299
  private handleListConversations;
1289
1300
  private handleConversationsCount;
1290
1301
  private refreshCountInBackground;
package/dist/index.d.ts CHANGED
@@ -1200,6 +1200,7 @@ declare class StreamerServer {
1200
1200
  private allScanners;
1201
1201
  private scannerReady;
1202
1202
  private scannerStale;
1203
+ private conversationReconcileInFlight;
1203
1204
  private refreshInFlight;
1204
1205
  private binding;
1205
1206
  private activeWarmups;
@@ -1285,6 +1286,16 @@ declare class StreamerServer {
1285
1286
  private checkExchangeRateLimit;
1286
1287
  private checkSessionStartRateLimit;
1287
1288
  private checkSessionInputRateLimit;
1289
+ /** Project roots watched for conversation JSONLs (profiles or ~/.claude/projects). */
1290
+ private projectsDirsForFreshnessCheck;
1291
+ /**
1292
+ * Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by
1293
+ * the automatic freshness path when the directory watcher marked the scanner
1294
+ * stale or shouldRefreshProjectsFromHdd detected disk drift.
1295
+ */
1296
+ private reconcileConversationsCacheFromDisk;
1297
+ private startBackgroundConversationReconcile;
1298
+ private shouldAutoReconcileConversationList;
1288
1299
  private handleListConversations;
1289
1300
  private handleConversationsCount;
1290
1301
  private refreshCountInBackground;
package/dist/index.js CHANGED
@@ -2684,14 +2684,14 @@ import {
2684
2684
  createReadStream,
2685
2685
  existsSync as existsSync10,
2686
2686
  watch as fsWatch,
2687
- readdirSync as readdirSync5,
2687
+ readdirSync as readdirSync6,
2688
2688
  readFileSync as readFileSync8,
2689
- statSync as statSync8
2689
+ statSync as statSync9
2690
2690
  } from "fs";
2691
2691
  import { realpath as realpath2 } from "fs/promises";
2692
2692
  import { createServer } from "http";
2693
- import { homedir as homedir8 } from "os";
2694
- import { basename as basename5, dirname as dirname9, join as join17 } from "path";
2693
+ import { homedir as homedir9 } from "os";
2694
+ import { basename as basename5, dirname as dirname9, join as join18 } from "path";
2695
2695
  import { createInterface } from "readline";
2696
2696
 
2697
2697
  // node_modules/nanoid/index.js
@@ -5420,6 +5420,14 @@ function seal(plaintext, recipientPublicKeyBase64) {
5420
5420
  };
5421
5421
  }
5422
5422
 
5423
+ // src/services/cache/cacheMetadata.ts
5424
+ function getCacheMetadata(repo, key) {
5425
+ return repo.getCacheMetadata(key);
5426
+ }
5427
+ function setCacheMetadata(repo, key, value) {
5428
+ repo.setCacheMetadata(key, value);
5429
+ }
5430
+
5423
5431
  // src/services/cache-integrity/cacheIntegrityMonitor.ts
5424
5432
  import { createHash as createHash2 } from "crypto";
5425
5433
  import { existsSync as existsSync8 } from "fs";
@@ -5960,6 +5968,140 @@ function pruneAgentConversations(cache) {
5960
5968
  return { scanned: rows.length, pruned, missing };
5961
5969
  }
5962
5970
 
5971
+ // src/utils/dates.ts
5972
+ import { compareDesc, isValid, parseISO } from "date-fns";
5973
+ function parseIsoDateOrNull(value) {
5974
+ if (!value) return null;
5975
+ const parsed = parseISO(value);
5976
+ return isValid(parsed) ? parsed : null;
5977
+ }
5978
+ function compareIsoDesc(a, b) {
5979
+ const dateA = parseIsoDateOrNull(a);
5980
+ const dateB = parseIsoDateOrNull(b);
5981
+ if (!dateA && !dateB) return 0;
5982
+ if (!dateA) return 1;
5983
+ if (!dateB) return -1;
5984
+ return compareDesc(dateA, dateB);
5985
+ }
5986
+
5987
+ // src/services/projects/ensureProjectsForConversations.ts
5988
+ function ensureProjectsForConversations(repo, conversations) {
5989
+ const conversationsByPath = /* @__PURE__ */ new Map();
5990
+ for (const conversation of conversations) {
5991
+ if (!conversation.projectPath) continue;
5992
+ const canonical = canonicalizeProjectPath(conversation.projectPath);
5993
+ if (!canonical) continue;
5994
+ const existing = conversationsByPath.get(canonical) ?? [];
5995
+ existing.push(conversation);
5996
+ conversationsByPath.set(canonical, existing);
5997
+ }
5998
+ const pathToProjectId = /* @__PURE__ */ new Map();
5999
+ for (const [path, projectConversations] of conversationsByPath) {
6000
+ const latest = pickLatestConversation(projectConversations);
6001
+ const project = repo.upsertProjectByPath(path, {
6002
+ lastConversationId: latest?.id ?? null,
6003
+ lastConversationCreatedAt: latest?.createdAt ?? null,
6004
+ latestMessageAt: latest?.latestMessageAt ?? null
6005
+ });
6006
+ pathToProjectId.set(path, project.id);
6007
+ }
6008
+ return pathToProjectId;
6009
+ }
6010
+ function pickLatestConversation(conversations) {
6011
+ if (conversations.length === 0) return void 0;
6012
+ return [...conversations].sort((a, b) => {
6013
+ const cmp = compareIsoDesc(a.latestMessageAt ?? null, b.latestMessageAt ?? null);
6014
+ if (cmp !== 0) return cmp;
6015
+ return compareIsoDesc(a.createdAt ?? null, b.createdAt ?? null);
6016
+ })[0];
6017
+ }
6018
+
6019
+ // src/services/conversations/refreshConversationCache.ts
6020
+ function refreshConversationCache(deps) {
6021
+ const { projectsRepo, conversationsRepo, cacheMetadataRepo } = deps;
6022
+ const conversations = conversationsRepo.listConversationsForProjectBackfill();
6023
+ const pathToProjectId = ensureProjectsForConversations(
6024
+ projectsRepo,
6025
+ conversations.map((c) => ({
6026
+ id: c.id,
6027
+ projectPath: c.projectPath,
6028
+ latestMessageAt: c.lastActivity ?? null,
6029
+ createdAt: c.lastActivity ?? null
6030
+ }))
6031
+ );
6032
+ let conversationsBackfilled = 0;
6033
+ for (const conversation of conversations) {
6034
+ if (!conversation.projectPath) continue;
6035
+ if (conversation.projectId) continue;
6036
+ const projectId = pathToProjectId.get(canonicalizeProjectPath(conversation.projectPath));
6037
+ if (!projectId) continue;
6038
+ conversationsRepo.updateConversationProjectId({
6039
+ conversationId: conversation.id,
6040
+ projectId
6041
+ });
6042
+ conversationsBackfilled += 1;
6043
+ }
6044
+ const latest = conversationsRepo.getLatestConversation();
6045
+ if (latest) {
6046
+ setCacheMetadata(cacheMetadataRepo, "last_conversation_id", latest.id);
6047
+ if (latest.lastActivity) {
6048
+ setCacheMetadata(cacheMetadataRepo, "last_conversation_created_at", latest.lastActivity);
6049
+ }
6050
+ }
6051
+ setCacheMetadata(cacheMetadataRepo, "conversations_last_indexed_at", (/* @__PURE__ */ new Date()).toISOString());
6052
+ return {
6053
+ projectsTouched: pathToProjectId.size,
6054
+ conversationsBackfilled,
6055
+ latestConversationId: latest?.id ?? null
6056
+ };
6057
+ }
6058
+
6059
+ // src/services/conversations/shouldRefreshProjectsFromHdd.ts
6060
+ import { readdirSync as readdirSync5, statSync as statSync7 } from "fs";
6061
+ import { homedir as homedir8 } from "os";
6062
+ import { join as join16 } from "path";
6063
+ var DEFAULT_PROJECTS_DIR = join16(homedir8(), ".claude", "projects");
6064
+ function maxProjectsTreeMtimeMs(projectsDir) {
6065
+ let maxMs;
6066
+ try {
6067
+ maxMs = statSync7(projectsDir).mtimeMs;
6068
+ } catch {
6069
+ return null;
6070
+ }
6071
+ try {
6072
+ for (const ent of readdirSync5(projectsDir, { withFileTypes: true })) {
6073
+ if (!ent.isDirectory()) continue;
6074
+ try {
6075
+ const childMs = statSync7(join16(projectsDir, ent.name)).mtimeMs;
6076
+ if (childMs > maxMs) maxMs = childMs;
6077
+ } catch {
6078
+ }
6079
+ }
6080
+ } catch {
6081
+ }
6082
+ return maxMs;
6083
+ }
6084
+ function shouldRefreshProjectsFromHdd(conversationsRepo, cacheMetadataRepo, opts = {}) {
6085
+ if (conversationsRepo.hasOrphanRows()) return true;
6086
+ const dirs = /* @__PURE__ */ new Set();
6087
+ if (opts.projectsDirs) {
6088
+ for (const d of opts.projectsDirs) dirs.add(d);
6089
+ }
6090
+ dirs.add(opts.projectsDir ?? DEFAULT_PROJECTS_DIR);
6091
+ let newestMs = null;
6092
+ for (const dir of dirs) {
6093
+ const ms = maxProjectsTreeMtimeMs(dir);
6094
+ if (ms === null) continue;
6095
+ if (newestMs === null || ms > newestMs) newestMs = ms;
6096
+ }
6097
+ if (newestMs === null) return false;
6098
+ const lastIndexedIso = getCacheMetadata(cacheMetadataRepo, "conversations_last_indexed_at");
6099
+ if (!lastIndexedIso) return true;
6100
+ const lastIndexedMs = Date.parse(lastIndexedIso);
6101
+ if (Number.isNaN(lastIndexedMs)) return true;
6102
+ return newestMs > lastIndexedMs;
6103
+ }
6104
+
5963
6105
  // src/services/projectChats/deriveProjectChatTitle.ts
5964
6106
  function deriveProjectChatTitle(input) {
5965
6107
  const trimmed = input.title?.trim();
@@ -6121,7 +6263,7 @@ function resolveAnswer(pending, body) {
6121
6263
  }
6122
6264
 
6123
6265
  // src/services/sessions/conversationBusy.ts
6124
- import { statSync as statSync7 } from "fs";
6266
+ import { statSync as statSync8 } from "fs";
6125
6267
  var RESUME_BUSY_WINDOW_MS = 12e4;
6126
6268
  function resolveResumeBusyWindowMs(env = process.env) {
6127
6269
  const raw = env.THREADBASE_RESUME_BUSY_WINDOW_MS;
@@ -6138,7 +6280,7 @@ function conversationBusy(input) {
6138
6280
  let lastActivityMs = null;
6139
6281
  if (input.jsonlPath) {
6140
6282
  try {
6141
- const mtimeMs = statSync7(input.jsonlPath).mtimeMs;
6283
+ const mtimeMs = statSync8(input.jsonlPath).mtimeMs;
6142
6284
  const age = now - mtimeMs;
6143
6285
  lastActivityMs = Math.max(0, age);
6144
6286
  const isSelfEcho = input.selfPtyEndedAt != null && mtimeMs <= input.selfPtyEndedAt + SELF_ACTIVITY_SKEW_MS;
@@ -6366,7 +6508,7 @@ function discoveredToResponse(d, conversationId) {
6366
6508
  import { randomBytes as randomBytes3 } from "crypto";
6367
6509
  import { mkdir as mkdir3, writeFile } from "fs/promises";
6368
6510
  import heicConvert from "heic-convert";
6369
- import { join as join16 } from "path";
6511
+ import { join as join17 } from "path";
6370
6512
  var UPLOAD_DIR_NAME = ".threadbase-uploads";
6371
6513
  var MAX_BYTES = 25 * 1024 * 1024;
6372
6514
  var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
@@ -6399,9 +6541,9 @@ async function saveUploadFile(input) {
6399
6541
  }
6400
6542
  const id = `up_${randomBytes3(8).toString("hex")}`;
6401
6543
  const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
6402
- const dir = join16(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
6544
+ const dir = join17(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
6403
6545
  await mkdir3(dir, { recursive: true });
6404
- const filePath = join16(dir, `${Date.now()}-${id}-${safeName}`);
6546
+ const filePath = join17(dir, `${Date.now()}-${id}-${safeName}`);
6405
6547
  await writeFile(filePath, buffer);
6406
6548
  return {
6407
6549
  id,
@@ -6511,14 +6653,6 @@ function computeConversationEtag({
6511
6653
  return `"${digest}"`;
6512
6654
  }
6513
6655
 
6514
- // src/utils/dates.ts
6515
- import { compareDesc, isValid, parseISO } from "date-fns";
6516
- function parseIsoDateOrNull(value) {
6517
- if (!value) return null;
6518
- const parsed = parseISO(value);
6519
- return isValid(parsed) ? parsed : null;
6520
- }
6521
-
6522
6656
  // src/utils/isScannedSnapshotStale.ts
6523
6657
  var STALENESS_TOLERANCE_MS = 1e3;
6524
6658
  function isScannedSnapshotStale(snapshotTimestamp, fileMtimeMs) {
@@ -6729,6 +6863,10 @@ var StreamerServer = class {
6729
6863
  // Set by onConversationChanged while a scan is in-flight; getScanner() does
6730
6864
  // a single rescan after the current one completes instead of restarting it.
6731
6865
  scannerStale = false;
6866
+ // Single-flight guard for the background disk reconcile: a burst of list
6867
+ // polls during active session writes shares one rescan instead of queueing
6868
+ // a full rescan per request.
6869
+ conversationReconcileInFlight = null;
6732
6870
  // Single-flight + TTL guard around scanner.refreshFile (see refreshFileGuarded).
6733
6871
  // A live file's mtime is always newer than the snapshot, so an unguarded
6734
6872
  // refresh fires on every request and re-parses the whole file from byte 0.
@@ -6819,13 +6957,13 @@ var StreamerServer = class {
6819
6957
  this.disableDb = config.disableDb ?? false;
6820
6958
  this.scannerPersistenceDisabled = config.scannerPersistent === false;
6821
6959
  this.scanProfiles = config.scanProfiles;
6822
- this.codexRoots = config.codexRoots ?? [join17(homedir8(), ".codex", "sessions")];
6960
+ this.codexRoots = config.codexRoots ?? [join18(homedir9(), ".codex", "sessions")];
6823
6961
  this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
6824
6962
  this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
6825
6963
  this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
6826
6964
  this.defaultModel = config.defaultModel ?? "sonnet";
6827
6965
  this.defaultEffort = config.defaultEffort ?? "low";
6828
- this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join17(homedir8(), ".threadbase", "cache");
6966
+ this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join18(homedir9(), ".threadbase", "cache");
6829
6967
  this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
6830
6968
  this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
6831
6969
  this.markScannerStaleDebounced = debounce(() => {
@@ -6866,7 +7004,7 @@ var StreamerServer = class {
6866
7004
  const seqs = cache.extendMessageIndex(
6867
7005
  filePath,
6868
7006
  spans,
6869
- statSync8(filePath),
7007
+ statSync9(filePath),
6870
7008
  readFrom,
6871
7009
  endOffset
6872
7010
  );
@@ -7032,7 +7170,7 @@ var StreamerServer = class {
7032
7170
  temporalClient,
7033
7171
  taskQueue: agentConfig.temporal.taskQueue
7034
7172
  });
7035
- const conversationsBaseDir = agentConfig.conversationsDir || join17(dirname9(this.cacheDir), "conversations");
7173
+ const conversationsBaseDir = agentConfig.conversationsDir || join18(dirname9(this.cacheDir), "conversations");
7036
7174
  conversationWriter = createConversationWriter({
7037
7175
  baseDir: conversationsBaseDir
7038
7176
  });
@@ -7355,7 +7493,7 @@ var StreamerServer = class {
7355
7493
  });
7356
7494
  try {
7357
7495
  this.cache = ConversationCache.open(
7358
- join17(this.cacheDir, "cache.db"),
7496
+ join18(this.cacheDir, "cache.db"),
7359
7497
  this.tailSize,
7360
7498
  void 0,
7361
7499
  {
@@ -7481,6 +7619,20 @@ var StreamerServer = class {
7481
7619
  count: pruned.length,
7482
7620
  event: "cache.prune_ghosts"
7483
7621
  });
7622
+ if (this.projectsRepo && this.conversationsRepo && this.cacheMetadataRepo) {
7623
+ refreshConversationCache({
7624
+ cache: this.cache,
7625
+ projectsRepo: this.projectsRepo,
7626
+ conversationsRepo: this.conversationsRepo,
7627
+ cacheMetadataRepo: this.cacheMetadataRepo
7628
+ });
7629
+ } else if (this.cacheMetadataRepo) {
7630
+ setCacheMetadata(
7631
+ this.cacheMetadataRepo,
7632
+ "conversations_last_indexed_at",
7633
+ (/* @__PURE__ */ new Date()).toISOString()
7634
+ );
7635
+ }
7484
7636
  }
7485
7637
  }).catch((err) => {
7486
7638
  const message = err instanceof Error ? err.message : String(err);
@@ -7720,6 +7872,68 @@ var StreamerServer = class {
7720
7872
  checkSessionInputRateLimit(sessionId) {
7721
7873
  return this.checkRateLimit(this.sessionInputAttempts, sessionId, 500, 6e4);
7722
7874
  }
7875
+ /** Project roots watched for conversation JSONLs (profiles or ~/.claude/projects). */
7876
+ projectsDirsForFreshnessCheck() {
7877
+ if (this.scanProfiles && this.scanProfiles.length > 0) {
7878
+ return this.scanProfiles.filter((p) => p.enabled).map((p) => join18(p.configDir, "projects"));
7879
+ }
7880
+ return [join18(homedir9(), ".claude", "projects")];
7881
+ }
7882
+ /**
7883
+ * Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by
7884
+ * the automatic freshness path when the directory watcher marked the scanner
7885
+ * stale or shouldRefreshProjectsFromHdd detected disk drift.
7886
+ */
7887
+ async reconcileConversationsCacheFromDisk(onProgress) {
7888
+ if (!this.cache) return;
7889
+ const scanner = await this.rescanForRefresh(onProgress);
7890
+ const metas = [...scanner.getMetadataCache().values()];
7891
+ try {
7892
+ this.cache.upsertFromScannerMeta(metas);
7893
+ if (!this.cacheMonitor?.pending) {
7894
+ this.cache.reconcileDeletions(canonicalLivePathSet(metas));
7895
+ }
7896
+ if (this.projectsRepo && this.conversationsRepo && this.cacheMetadataRepo) {
7897
+ refreshConversationCache({
7898
+ cache: this.cache,
7899
+ projectsRepo: this.projectsRepo,
7900
+ conversationsRepo: this.conversationsRepo,
7901
+ cacheMetadataRepo: this.cacheMetadataRepo
7902
+ });
7903
+ } else if (this.cacheMetadataRepo) {
7904
+ setCacheMetadata(
7905
+ this.cacheMetadataRepo,
7906
+ "conversations_last_indexed_at",
7907
+ (/* @__PURE__ */ new Date()).toISOString()
7908
+ );
7909
+ }
7910
+ } catch (err) {
7911
+ this.log.warn(
7912
+ `refresh reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
7913
+ { event: "conversations.reconcile_failed" }
7914
+ );
7915
+ }
7916
+ }
7917
+ // Reconcile the cache from disk without blocking the caller. Single-flighted
7918
+ // so a burst of list polls during active session writes shares one rescan
7919
+ // rather than queueing a full rescan each; tracked so close() awaits the
7920
+ // in-flight cache write before shutting the DB.
7921
+ startBackgroundConversationReconcile() {
7922
+ if (this.conversationReconcileInFlight) return;
7923
+ const task = this.reconcileConversationsCacheFromDisk().finally(() => {
7924
+ this.conversationReconcileInFlight = null;
7925
+ });
7926
+ this.conversationReconcileInFlight = task;
7927
+ this.trackCacheWrite(task);
7928
+ }
7929
+ shouldAutoReconcileConversationList() {
7930
+ if (!this.cache) return false;
7931
+ if (this.scannerStale) return true;
7932
+ if (!this.conversationsRepo || !this.cacheMetadataRepo) return false;
7933
+ return shouldRefreshProjectsFromHdd(this.conversationsRepo, this.cacheMetadataRepo, {
7934
+ projectsDirs: this.projectsDirsForFreshnessCheck()
7935
+ });
7936
+ }
7723
7937
  async handleListConversations(url, res) {
7724
7938
  if (this.rejectIfWarmingUp(res)) return;
7725
7939
  const limit = intParam(url, "limit", 50);
@@ -7728,18 +7942,19 @@ var StreamerServer = class {
7728
7942
  const project = url.searchParams.get("project") ?? void 0;
7729
7943
  const providerFilter = url.searchParams.get("provider") ?? void 0;
7730
7944
  const bustCache = url.searchParams.get("refresh") === "1";
7731
- if (bustCache && this.cache) {
7732
- const scanner2 = await this.withWarmup("conversation_refresh", () => this.rescanForRefresh());
7733
- const metas2 = [...scanner2.getMetadataCache().values()];
7734
- try {
7735
- this.cache.upsertFromScannerMeta(metas2);
7736
- if (!this.cacheMonitor?.pending) {
7737
- this.cache.reconcileDeletions(canonicalLivePathSet(metas2));
7738
- }
7739
- } catch (err) {
7740
- this.log.warn(
7741
- `refresh reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
7742
- { event: "conversations.reconcile_failed" }
7945
+ if (this.cache && (bustCache || this.shouldAutoReconcileConversationList())) {
7946
+ const canServeStale = !bustCache && this.cache.listConversations({ limit: 0, offset: 0 }).total > 0;
7947
+ if (canServeStale) {
7948
+ this.startBackgroundConversationReconcile();
7949
+ } else {
7950
+ const shouldEmitProgress = createScanProgressThrottle();
7951
+ await this.withWarmup(
7952
+ "conversation_refresh",
7953
+ () => this.reconcileConversationsCacheFromDisk((scanned, total2) => {
7954
+ if (shouldEmitProgress(scanned, total2)) {
7955
+ this.wsHub.broadcast({ type: "scan_progress", scanned, total: total2 });
7956
+ }
7957
+ })
7743
7958
  );
7744
7959
  }
7745
7960
  }
@@ -7976,7 +8191,7 @@ var StreamerServer = class {
7976
8191
  // getFreshScanner() this does NOT discard the warm scanner. scannerReady is
7977
8192
  // only ever reassigned to a live scan promise (never nulled mid-scan), so the
7978
8193
  // getScanner() anti-infinite-loop guard is preserved.
7979
- async rescanForRefresh() {
8194
+ async rescanForRefresh(onProgress) {
7980
8195
  if (this.scannerReady) await this.scannerReady;
7981
8196
  this.scannerStale = false;
7982
8197
  if (!this.scanner) {
@@ -7987,7 +8202,8 @@ var StreamerServer = class {
7987
8202
  this.scannerReady = scanner.scan({
7988
8203
  ...this.scanProfiles ? { profiles: this.scanProfiles } : {},
7989
8204
  ...this.codexScanOpts(),
7990
- fullRescan: true
8205
+ fullRescan: true,
8206
+ ...onProgress ? { onProgress } : {}
7991
8207
  });
7992
8208
  await this.scannerReady;
7993
8209
  return scanner;
@@ -8002,21 +8218,21 @@ var StreamerServer = class {
8002
8218
  */
8003
8219
  projectsDirs() {
8004
8220
  if (this.scanProfiles && this.scanProfiles.length > 0) {
8005
- return this.scanProfiles.filter((p) => p.enabled).map((p) => join17(p.configDir, "projects"));
8221
+ return this.scanProfiles.filter((p) => p.enabled).map((p) => join18(p.configDir, "projects"));
8006
8222
  }
8007
- return [join17(homedir8(), ".claude", "projects")];
8223
+ return [join18(homedir9(), ".claude", "projects")];
8008
8224
  }
8009
8225
  findJsonlPath(uuid) {
8010
8226
  const filename = `${uuid}.jsonl`;
8011
8227
  for (const projectsDir of this.projectsDirs()) {
8012
8228
  if (!existsSync10(projectsDir)) continue;
8013
- for (const dir of readdirSync5(projectsDir)) {
8014
- const fp = join17(projectsDir, dir, filename);
8229
+ for (const dir of readdirSync6(projectsDir)) {
8230
+ const fp = join18(projectsDir, dir, filename);
8015
8231
  if (existsSync10(fp)) return fp;
8016
- const projectDir = join17(projectsDir, dir);
8232
+ const projectDir = join18(projectsDir, dir);
8017
8233
  try {
8018
- for (const sub of readdirSync5(projectDir)) {
8019
- const subagentPath = join17(projectDir, sub, "subagents", filename);
8234
+ for (const sub of readdirSync6(projectDir)) {
8235
+ const subagentPath = join18(projectDir, sub, "subagents", filename);
8020
8236
  if (existsSync10(subagentPath)) return subagentPath;
8021
8237
  }
8022
8238
  } catch {
@@ -8117,7 +8333,7 @@ var StreamerServer = class {
8117
8333
  if (this.isManagedTailPath(key)) return;
8118
8334
  let mtimeMs;
8119
8335
  try {
8120
- mtimeMs = statSync8(filePath).mtimeMs;
8336
+ mtimeMs = statSync9(filePath).mtimeMs;
8121
8337
  } catch {
8122
8338
  return;
8123
8339
  }
@@ -8299,7 +8515,7 @@ var StreamerServer = class {
8299
8515
  if (!conv.filePath) return false;
8300
8516
  let mtimeMs = null;
8301
8517
  try {
8302
- mtimeMs = statSync8(conv.filePath).mtimeMs;
8518
+ mtimeMs = statSync9(conv.filePath).mtimeMs;
8303
8519
  } catch {
8304
8520
  return false;
8305
8521
  }
@@ -9219,7 +9435,7 @@ var StreamerServer = class {
9219
9435
  sessionStore: this.sessionStore,
9220
9436
  // biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
9221
9437
  agentClient: this.agentClient,
9222
- conversationsDir: this.cacheDir ? join17(dirname9(this.cacheDir), "conversations") : "",
9438
+ conversationsDir: this.cacheDir ? join18(dirname9(this.cacheDir), "conversations") : "",
9223
9439
  agentConfig: this.agentConfig
9224
9440
  });
9225
9441
  json(res, result.status, result.body);
@@ -9378,9 +9594,9 @@ var StreamerServer = class {
9378
9594
  // was passed to Claude via --session-id so the filename matches from the start.
9379
9595
  watchForJsonl(sessionId, projectPath) {
9380
9596
  const encoded = projectPath.replace(/[/\\:.]/g, "-");
9381
- const projectsDir = join17(homedir8(), ".claude", "projects", encoded);
9597
+ const projectsDir = join18(homedir9(), ".claude", "projects", encoded);
9382
9598
  const expectedFile = `${sessionId}.jsonl`;
9383
- const filePath = join17(projectsDir, expectedFile);
9599
+ const filePath = join18(projectsDir, expectedFile);
9384
9600
  const deadline = Date.now() + 12e4;
9385
9601
  let watcher = null;
9386
9602
  const cleanup = () => {
@@ -9402,10 +9618,10 @@ var StreamerServer = class {
9402
9618
  if (!resolvedFilePath && existsSync10(projectsDir)) {
9403
9619
  try {
9404
9620
  const now = Date.now();
9405
- const match = readdirSync5(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync8(join17(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
9406
- ({ f }) => basename5(f, ".jsonl") === sessionId || this.readFirstLineSessionId(join17(projectsDir, f)) === sessionId
9621
+ const match = readdirSync6(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync9(join18(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
9622
+ ({ f }) => basename5(f, ".jsonl") === sessionId || this.readFirstLineSessionId(join18(projectsDir, f)) === sessionId
9407
9623
  ).sort((a, b) => b.mtime - a.mtime)[0];
9408
- if (match) resolvedFilePath = join17(projectsDir, match.f);
9624
+ if (match) resolvedFilePath = join18(projectsDir, match.f);
9409
9625
  } catch {
9410
9626
  }
9411
9627
  }
@@ -9453,7 +9669,7 @@ var StreamerServer = class {
9453
9669
  watchForCodexRollout(sessionId, projectPath) {
9454
9670
  const deadline = Date.now() + 12e4;
9455
9671
  const now = /* @__PURE__ */ new Date();
9456
- const dateDir = join17(
9672
+ const dateDir = join18(
9457
9673
  String(now.getFullYear()),
9458
9674
  String(now.getMonth() + 1).padStart(2, "0"),
9459
9675
  String(now.getDate()).padStart(2, "0")
@@ -9494,18 +9710,18 @@ var StreamerServer = class {
9494
9710
  this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
9495
9711
  );
9496
9712
  for (const root of this.codexRoots) {
9497
- const sessionsDir = join17(root, dateDir);
9713
+ const sessionsDir = join18(root, dateDir);
9498
9714
  if (!existsSync10(sessionsDir)) continue;
9499
9715
  let candidateFiles;
9500
9716
  try {
9501
- candidateFiles = readdirSync5(sessionsDir).filter((f) => f.endsWith(".jsonl"));
9717
+ candidateFiles = readdirSync6(sessionsDir).filter((f) => f.endsWith(".jsonl"));
9502
9718
  } catch {
9503
9719
  continue;
9504
9720
  }
9505
9721
  const nowMs = Date.now();
9506
- const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync8(join17(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
9722
+ const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync9(join18(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
9507
9723
  for (const { f } of recentCandidates) {
9508
- const candidatePath = join17(sessionsDir, f);
9724
+ const candidatePath = join18(sessionsDir, f);
9509
9725
  const match = matchesProjectPath(candidatePath);
9510
9726
  if (!match) continue;
9511
9727
  if (boundElsewhere.has(match.id)) continue;