@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.cjs CHANGED
@@ -2725,11 +2725,11 @@ var import_node_ws = require("@hono/node-ws");
2725
2725
  var import_client = require("@temporalio/client");
2726
2726
  var import_scanner3 = require("@threadbase-sh/scanner");
2727
2727
  var import_events = require("events");
2728
- var import_fs17 = require("fs");
2728
+ var import_fs18 = require("fs");
2729
2729
  var import_promises7 = require("fs/promises");
2730
2730
  var import_http = require("http");
2731
- var import_os8 = require("os");
2732
- var import_path17 = require("path");
2731
+ var import_os9 = require("os");
2732
+ var import_path18 = require("path");
2733
2733
  var import_readline = require("readline");
2734
2734
 
2735
2735
  // node_modules/nanoid/index.js
@@ -5456,6 +5456,14 @@ function seal(plaintext, recipientPublicKeyBase64) {
5456
5456
  };
5457
5457
  }
5458
5458
 
5459
+ // src/services/cache/cacheMetadata.ts
5460
+ function getCacheMetadata(repo, key) {
5461
+ return repo.getCacheMetadata(key);
5462
+ }
5463
+ function setCacheMetadata(repo, key, value) {
5464
+ repo.setCacheMetadata(key, value);
5465
+ }
5466
+
5459
5467
  // src/services/cache-integrity/cacheIntegrityMonitor.ts
5460
5468
  var import_crypto8 = require("crypto");
5461
5469
  var import_fs13 = require("fs");
@@ -5996,6 +6004,140 @@ function pruneAgentConversations(cache) {
5996
6004
  return { scanned: rows.length, pruned, missing };
5997
6005
  }
5998
6006
 
6007
+ // src/utils/dates.ts
6008
+ var import_date_fns = require("date-fns");
6009
+ function parseIsoDateOrNull(value) {
6010
+ if (!value) return null;
6011
+ const parsed = (0, import_date_fns.parseISO)(value);
6012
+ return (0, import_date_fns.isValid)(parsed) ? parsed : null;
6013
+ }
6014
+ function compareIsoDesc(a, b) {
6015
+ const dateA = parseIsoDateOrNull(a);
6016
+ const dateB = parseIsoDateOrNull(b);
6017
+ if (!dateA && !dateB) return 0;
6018
+ if (!dateA) return 1;
6019
+ if (!dateB) return -1;
6020
+ return (0, import_date_fns.compareDesc)(dateA, dateB);
6021
+ }
6022
+
6023
+ // src/services/projects/ensureProjectsForConversations.ts
6024
+ function ensureProjectsForConversations(repo, conversations) {
6025
+ const conversationsByPath = /* @__PURE__ */ new Map();
6026
+ for (const conversation of conversations) {
6027
+ if (!conversation.projectPath) continue;
6028
+ const canonical = canonicalizeProjectPath(conversation.projectPath);
6029
+ if (!canonical) continue;
6030
+ const existing = conversationsByPath.get(canonical) ?? [];
6031
+ existing.push(conversation);
6032
+ conversationsByPath.set(canonical, existing);
6033
+ }
6034
+ const pathToProjectId = /* @__PURE__ */ new Map();
6035
+ for (const [path, projectConversations] of conversationsByPath) {
6036
+ const latest = pickLatestConversation(projectConversations);
6037
+ const project = repo.upsertProjectByPath(path, {
6038
+ lastConversationId: latest?.id ?? null,
6039
+ lastConversationCreatedAt: latest?.createdAt ?? null,
6040
+ latestMessageAt: latest?.latestMessageAt ?? null
6041
+ });
6042
+ pathToProjectId.set(path, project.id);
6043
+ }
6044
+ return pathToProjectId;
6045
+ }
6046
+ function pickLatestConversation(conversations) {
6047
+ if (conversations.length === 0) return void 0;
6048
+ return [...conversations].sort((a, b) => {
6049
+ const cmp = compareIsoDesc(a.latestMessageAt ?? null, b.latestMessageAt ?? null);
6050
+ if (cmp !== 0) return cmp;
6051
+ return compareIsoDesc(a.createdAt ?? null, b.createdAt ?? null);
6052
+ })[0];
6053
+ }
6054
+
6055
+ // src/services/conversations/refreshConversationCache.ts
6056
+ function refreshConversationCache(deps) {
6057
+ const { projectsRepo, conversationsRepo, cacheMetadataRepo } = deps;
6058
+ const conversations = conversationsRepo.listConversationsForProjectBackfill();
6059
+ const pathToProjectId = ensureProjectsForConversations(
6060
+ projectsRepo,
6061
+ conversations.map((c) => ({
6062
+ id: c.id,
6063
+ projectPath: c.projectPath,
6064
+ latestMessageAt: c.lastActivity ?? null,
6065
+ createdAt: c.lastActivity ?? null
6066
+ }))
6067
+ );
6068
+ let conversationsBackfilled = 0;
6069
+ for (const conversation of conversations) {
6070
+ if (!conversation.projectPath) continue;
6071
+ if (conversation.projectId) continue;
6072
+ const projectId = pathToProjectId.get(canonicalizeProjectPath(conversation.projectPath));
6073
+ if (!projectId) continue;
6074
+ conversationsRepo.updateConversationProjectId({
6075
+ conversationId: conversation.id,
6076
+ projectId
6077
+ });
6078
+ conversationsBackfilled += 1;
6079
+ }
6080
+ const latest = conversationsRepo.getLatestConversation();
6081
+ if (latest) {
6082
+ setCacheMetadata(cacheMetadataRepo, "last_conversation_id", latest.id);
6083
+ if (latest.lastActivity) {
6084
+ setCacheMetadata(cacheMetadataRepo, "last_conversation_created_at", latest.lastActivity);
6085
+ }
6086
+ }
6087
+ setCacheMetadata(cacheMetadataRepo, "conversations_last_indexed_at", (/* @__PURE__ */ new Date()).toISOString());
6088
+ return {
6089
+ projectsTouched: pathToProjectId.size,
6090
+ conversationsBackfilled,
6091
+ latestConversationId: latest?.id ?? null
6092
+ };
6093
+ }
6094
+
6095
+ // src/services/conversations/shouldRefreshProjectsFromHdd.ts
6096
+ var import_fs16 = require("fs");
6097
+ var import_os8 = require("os");
6098
+ var import_path16 = require("path");
6099
+ var DEFAULT_PROJECTS_DIR = (0, import_path16.join)((0, import_os8.homedir)(), ".claude", "projects");
6100
+ function maxProjectsTreeMtimeMs(projectsDir) {
6101
+ let maxMs;
6102
+ try {
6103
+ maxMs = (0, import_fs16.statSync)(projectsDir).mtimeMs;
6104
+ } catch {
6105
+ return null;
6106
+ }
6107
+ try {
6108
+ for (const ent of (0, import_fs16.readdirSync)(projectsDir, { withFileTypes: true })) {
6109
+ if (!ent.isDirectory()) continue;
6110
+ try {
6111
+ const childMs = (0, import_fs16.statSync)((0, import_path16.join)(projectsDir, ent.name)).mtimeMs;
6112
+ if (childMs > maxMs) maxMs = childMs;
6113
+ } catch {
6114
+ }
6115
+ }
6116
+ } catch {
6117
+ }
6118
+ return maxMs;
6119
+ }
6120
+ function shouldRefreshProjectsFromHdd(conversationsRepo, cacheMetadataRepo, opts = {}) {
6121
+ if (conversationsRepo.hasOrphanRows()) return true;
6122
+ const dirs = /* @__PURE__ */ new Set();
6123
+ if (opts.projectsDirs) {
6124
+ for (const d of opts.projectsDirs) dirs.add(d);
6125
+ }
6126
+ dirs.add(opts.projectsDir ?? DEFAULT_PROJECTS_DIR);
6127
+ let newestMs = null;
6128
+ for (const dir of dirs) {
6129
+ const ms = maxProjectsTreeMtimeMs(dir);
6130
+ if (ms === null) continue;
6131
+ if (newestMs === null || ms > newestMs) newestMs = ms;
6132
+ }
6133
+ if (newestMs === null) return false;
6134
+ const lastIndexedIso = getCacheMetadata(cacheMetadataRepo, "conversations_last_indexed_at");
6135
+ if (!lastIndexedIso) return true;
6136
+ const lastIndexedMs = Date.parse(lastIndexedIso);
6137
+ if (Number.isNaN(lastIndexedMs)) return true;
6138
+ return newestMs > lastIndexedMs;
6139
+ }
6140
+
5999
6141
  // src/services/projectChats/deriveProjectChatTitle.ts
6000
6142
  function deriveProjectChatTitle(input) {
6001
6143
  const trimmed = input.title?.trim();
@@ -6157,7 +6299,7 @@ function resolveAnswer(pending, body) {
6157
6299
  }
6158
6300
 
6159
6301
  // src/services/sessions/conversationBusy.ts
6160
- var import_fs16 = require("fs");
6302
+ var import_fs17 = require("fs");
6161
6303
  var RESUME_BUSY_WINDOW_MS = 12e4;
6162
6304
  function resolveResumeBusyWindowMs(env = process.env) {
6163
6305
  const raw = env.THREADBASE_RESUME_BUSY_WINDOW_MS;
@@ -6174,7 +6316,7 @@ function conversationBusy(input) {
6174
6316
  let lastActivityMs = null;
6175
6317
  if (input.jsonlPath) {
6176
6318
  try {
6177
- const mtimeMs = (0, import_fs16.statSync)(input.jsonlPath).mtimeMs;
6319
+ const mtimeMs = (0, import_fs17.statSync)(input.jsonlPath).mtimeMs;
6178
6320
  const age = now - mtimeMs;
6179
6321
  lastActivityMs = Math.max(0, age);
6180
6322
  const isSelfEcho = input.selfPtyEndedAt != null && mtimeMs <= input.selfPtyEndedAt + SELF_ACTIVITY_SKEW_MS;
@@ -6402,7 +6544,7 @@ function discoveredToResponse(d, conversationId) {
6402
6544
  var import_crypto9 = require("crypto");
6403
6545
  var import_promises6 = require("fs/promises");
6404
6546
  var import_heic_convert = __toESM(require("heic-convert"), 1);
6405
- var import_path16 = require("path");
6547
+ var import_path17 = require("path");
6406
6548
  var UPLOAD_DIR_NAME = ".threadbase-uploads";
6407
6549
  var MAX_BYTES = 25 * 1024 * 1024;
6408
6550
  var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
@@ -6435,9 +6577,9 @@ async function saveUploadFile(input) {
6435
6577
  }
6436
6578
  const id = `up_${(0, import_crypto9.randomBytes)(8).toString("hex")}`;
6437
6579
  const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
6438
- const dir = (0, import_path16.join)(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
6580
+ const dir = (0, import_path17.join)(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
6439
6581
  await (0, import_promises6.mkdir)(dir, { recursive: true });
6440
- const filePath = (0, import_path16.join)(dir, `${Date.now()}-${id}-${safeName}`);
6582
+ const filePath = (0, import_path17.join)(dir, `${Date.now()}-${id}-${safeName}`);
6441
6583
  await (0, import_promises6.writeFile)(filePath, buffer);
6442
6584
  return {
6443
6585
  id,
@@ -6547,14 +6689,6 @@ function computeConversationEtag({
6547
6689
  return `"${digest}"`;
6548
6690
  }
6549
6691
 
6550
- // src/utils/dates.ts
6551
- var import_date_fns = require("date-fns");
6552
- function parseIsoDateOrNull(value) {
6553
- if (!value) return null;
6554
- const parsed = (0, import_date_fns.parseISO)(value);
6555
- return (0, import_date_fns.isValid)(parsed) ? parsed : null;
6556
- }
6557
-
6558
6692
  // src/utils/isScannedSnapshotStale.ts
6559
6693
  var STALENESS_TOLERANCE_MS = 1e3;
6560
6694
  function isScannedSnapshotStale(snapshotTimestamp, fileMtimeMs) {
@@ -6765,6 +6899,10 @@ var StreamerServer = class {
6765
6899
  // Set by onConversationChanged while a scan is in-flight; getScanner() does
6766
6900
  // a single rescan after the current one completes instead of restarting it.
6767
6901
  scannerStale = false;
6902
+ // Single-flight guard for the background disk reconcile: a burst of list
6903
+ // polls during active session writes shares one rescan instead of queueing
6904
+ // a full rescan per request.
6905
+ conversationReconcileInFlight = null;
6768
6906
  // Single-flight + TTL guard around scanner.refreshFile (see refreshFileGuarded).
6769
6907
  // A live file's mtime is always newer than the snapshot, so an unguarded
6770
6908
  // refresh fires on every request and re-parses the whole file from byte 0.
@@ -6855,13 +6993,13 @@ var StreamerServer = class {
6855
6993
  this.disableDb = config.disableDb ?? false;
6856
6994
  this.scannerPersistenceDisabled = config.scannerPersistent === false;
6857
6995
  this.scanProfiles = config.scanProfiles;
6858
- this.codexRoots = config.codexRoots ?? [(0, import_path17.join)((0, import_os8.homedir)(), ".codex", "sessions")];
6996
+ this.codexRoots = config.codexRoots ?? [(0, import_path18.join)((0, import_os9.homedir)(), ".codex", "sessions")];
6859
6997
  this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
6860
6998
  this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
6861
6999
  this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
6862
7000
  this.defaultModel = config.defaultModel ?? "sonnet";
6863
7001
  this.defaultEffort = config.defaultEffort ?? "low";
6864
- this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path17.join)((0, import_os8.homedir)(), ".threadbase", "cache");
7002
+ this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path18.join)((0, import_os9.homedir)(), ".threadbase", "cache");
6865
7003
  this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
6866
7004
  this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
6867
7005
  this.markScannerStaleDebounced = debounce(() => {
@@ -6902,7 +7040,7 @@ var StreamerServer = class {
6902
7040
  const seqs = cache.extendMessageIndex(
6903
7041
  filePath,
6904
7042
  spans,
6905
- (0, import_fs17.statSync)(filePath),
7043
+ (0, import_fs18.statSync)(filePath),
6906
7044
  readFrom,
6907
7045
  endOffset
6908
7046
  );
@@ -7068,7 +7206,7 @@ var StreamerServer = class {
7068
7206
  temporalClient,
7069
7207
  taskQueue: agentConfig.temporal.taskQueue
7070
7208
  });
7071
- const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path17.join)((0, import_path17.dirname)(this.cacheDir), "conversations");
7209
+ const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path18.join)((0, import_path18.dirname)(this.cacheDir), "conversations");
7072
7210
  conversationWriter = createConversationWriter({
7073
7211
  baseDir: conversationsBaseDir
7074
7212
  });
@@ -7391,7 +7529,7 @@ var StreamerServer = class {
7391
7529
  });
7392
7530
  try {
7393
7531
  this.cache = ConversationCache.open(
7394
- (0, import_path17.join)(this.cacheDir, "cache.db"),
7532
+ (0, import_path18.join)(this.cacheDir, "cache.db"),
7395
7533
  this.tailSize,
7396
7534
  void 0,
7397
7535
  {
@@ -7436,7 +7574,7 @@ var StreamerServer = class {
7436
7574
  this.fileWatcher.watchDirectory(dir);
7437
7575
  }
7438
7576
  for (const dir of this.codexRoots) {
7439
- if (!(0, import_fs17.existsSync)(dir)) continue;
7577
+ if (!(0, import_fs18.existsSync)(dir)) continue;
7440
7578
  this.fileWatcher.watchDirectory(dir);
7441
7579
  }
7442
7580
  } catch (err) {
@@ -7517,6 +7655,20 @@ var StreamerServer = class {
7517
7655
  count: pruned.length,
7518
7656
  event: "cache.prune_ghosts"
7519
7657
  });
7658
+ if (this.projectsRepo && this.conversationsRepo && this.cacheMetadataRepo) {
7659
+ refreshConversationCache({
7660
+ cache: this.cache,
7661
+ projectsRepo: this.projectsRepo,
7662
+ conversationsRepo: this.conversationsRepo,
7663
+ cacheMetadataRepo: this.cacheMetadataRepo
7664
+ });
7665
+ } else if (this.cacheMetadataRepo) {
7666
+ setCacheMetadata(
7667
+ this.cacheMetadataRepo,
7668
+ "conversations_last_indexed_at",
7669
+ (/* @__PURE__ */ new Date()).toISOString()
7670
+ );
7671
+ }
7520
7672
  }
7521
7673
  }).catch((err) => {
7522
7674
  const message = err instanceof Error ? err.message : String(err);
@@ -7756,6 +7908,68 @@ var StreamerServer = class {
7756
7908
  checkSessionInputRateLimit(sessionId) {
7757
7909
  return this.checkRateLimit(this.sessionInputAttempts, sessionId, 500, 6e4);
7758
7910
  }
7911
+ /** Project roots watched for conversation JSONLs (profiles or ~/.claude/projects). */
7912
+ projectsDirsForFreshnessCheck() {
7913
+ if (this.scanProfiles && this.scanProfiles.length > 0) {
7914
+ return this.scanProfiles.filter((p) => p.enabled).map((p) => (0, import_path18.join)(p.configDir, "projects"));
7915
+ }
7916
+ return [(0, import_path18.join)((0, import_os9.homedir)(), ".claude", "projects")];
7917
+ }
7918
+ /**
7919
+ * Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by
7920
+ * the automatic freshness path when the directory watcher marked the scanner
7921
+ * stale or shouldRefreshProjectsFromHdd detected disk drift.
7922
+ */
7923
+ async reconcileConversationsCacheFromDisk(onProgress) {
7924
+ if (!this.cache) return;
7925
+ const scanner = await this.rescanForRefresh(onProgress);
7926
+ const metas = [...scanner.getMetadataCache().values()];
7927
+ try {
7928
+ this.cache.upsertFromScannerMeta(metas);
7929
+ if (!this.cacheMonitor?.pending) {
7930
+ this.cache.reconcileDeletions(canonicalLivePathSet(metas));
7931
+ }
7932
+ if (this.projectsRepo && this.conversationsRepo && this.cacheMetadataRepo) {
7933
+ refreshConversationCache({
7934
+ cache: this.cache,
7935
+ projectsRepo: this.projectsRepo,
7936
+ conversationsRepo: this.conversationsRepo,
7937
+ cacheMetadataRepo: this.cacheMetadataRepo
7938
+ });
7939
+ } else if (this.cacheMetadataRepo) {
7940
+ setCacheMetadata(
7941
+ this.cacheMetadataRepo,
7942
+ "conversations_last_indexed_at",
7943
+ (/* @__PURE__ */ new Date()).toISOString()
7944
+ );
7945
+ }
7946
+ } catch (err) {
7947
+ this.log.warn(
7948
+ `refresh reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
7949
+ { event: "conversations.reconcile_failed" }
7950
+ );
7951
+ }
7952
+ }
7953
+ // Reconcile the cache from disk without blocking the caller. Single-flighted
7954
+ // so a burst of list polls during active session writes shares one rescan
7955
+ // rather than queueing a full rescan each; tracked so close() awaits the
7956
+ // in-flight cache write before shutting the DB.
7957
+ startBackgroundConversationReconcile() {
7958
+ if (this.conversationReconcileInFlight) return;
7959
+ const task = this.reconcileConversationsCacheFromDisk().finally(() => {
7960
+ this.conversationReconcileInFlight = null;
7961
+ });
7962
+ this.conversationReconcileInFlight = task;
7963
+ this.trackCacheWrite(task);
7964
+ }
7965
+ shouldAutoReconcileConversationList() {
7966
+ if (!this.cache) return false;
7967
+ if (this.scannerStale) return true;
7968
+ if (!this.conversationsRepo || !this.cacheMetadataRepo) return false;
7969
+ return shouldRefreshProjectsFromHdd(this.conversationsRepo, this.cacheMetadataRepo, {
7970
+ projectsDirs: this.projectsDirsForFreshnessCheck()
7971
+ });
7972
+ }
7759
7973
  async handleListConversations(url, res) {
7760
7974
  if (this.rejectIfWarmingUp(res)) return;
7761
7975
  const limit = intParam(url, "limit", 50);
@@ -7764,18 +7978,19 @@ var StreamerServer = class {
7764
7978
  const project = url.searchParams.get("project") ?? void 0;
7765
7979
  const providerFilter = url.searchParams.get("provider") ?? void 0;
7766
7980
  const bustCache = url.searchParams.get("refresh") === "1";
7767
- if (bustCache && this.cache) {
7768
- const scanner2 = await this.withWarmup("conversation_refresh", () => this.rescanForRefresh());
7769
- const metas2 = [...scanner2.getMetadataCache().values()];
7770
- try {
7771
- this.cache.upsertFromScannerMeta(metas2);
7772
- if (!this.cacheMonitor?.pending) {
7773
- this.cache.reconcileDeletions(canonicalLivePathSet(metas2));
7774
- }
7775
- } catch (err) {
7776
- this.log.warn(
7777
- `refresh reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
7778
- { event: "conversations.reconcile_failed" }
7981
+ if (this.cache && (bustCache || this.shouldAutoReconcileConversationList())) {
7982
+ const canServeStale = !bustCache && this.cache.listConversations({ limit: 0, offset: 0 }).total > 0;
7983
+ if (canServeStale) {
7984
+ this.startBackgroundConversationReconcile();
7985
+ } else {
7986
+ const shouldEmitProgress = createScanProgressThrottle();
7987
+ await this.withWarmup(
7988
+ "conversation_refresh",
7989
+ () => this.reconcileConversationsCacheFromDisk((scanned, total2) => {
7990
+ if (shouldEmitProgress(scanned, total2)) {
7991
+ this.wsHub.broadcast({ type: "scan_progress", scanned, total: total2 });
7992
+ }
7993
+ })
7779
7994
  );
7780
7995
  }
7781
7996
  }
@@ -8012,7 +8227,7 @@ var StreamerServer = class {
8012
8227
  // getFreshScanner() this does NOT discard the warm scanner. scannerReady is
8013
8228
  // only ever reassigned to a live scan promise (never nulled mid-scan), so the
8014
8229
  // getScanner() anti-infinite-loop guard is preserved.
8015
- async rescanForRefresh() {
8230
+ async rescanForRefresh(onProgress) {
8016
8231
  if (this.scannerReady) await this.scannerReady;
8017
8232
  this.scannerStale = false;
8018
8233
  if (!this.scanner) {
@@ -8023,7 +8238,8 @@ var StreamerServer = class {
8023
8238
  this.scannerReady = scanner.scan({
8024
8239
  ...this.scanProfiles ? { profiles: this.scanProfiles } : {},
8025
8240
  ...this.codexScanOpts(),
8026
- fullRescan: true
8241
+ fullRescan: true,
8242
+ ...onProgress ? { onProgress } : {}
8027
8243
  });
8028
8244
  await this.scannerReady;
8029
8245
  return scanner;
@@ -8038,22 +8254,22 @@ var StreamerServer = class {
8038
8254
  */
8039
8255
  projectsDirs() {
8040
8256
  if (this.scanProfiles && this.scanProfiles.length > 0) {
8041
- return this.scanProfiles.filter((p) => p.enabled).map((p) => (0, import_path17.join)(p.configDir, "projects"));
8257
+ return this.scanProfiles.filter((p) => p.enabled).map((p) => (0, import_path18.join)(p.configDir, "projects"));
8042
8258
  }
8043
- return [(0, import_path17.join)((0, import_os8.homedir)(), ".claude", "projects")];
8259
+ return [(0, import_path18.join)((0, import_os9.homedir)(), ".claude", "projects")];
8044
8260
  }
8045
8261
  findJsonlPath(uuid) {
8046
8262
  const filename = `${uuid}.jsonl`;
8047
8263
  for (const projectsDir of this.projectsDirs()) {
8048
- if (!(0, import_fs17.existsSync)(projectsDir)) continue;
8049
- for (const dir of (0, import_fs17.readdirSync)(projectsDir)) {
8050
- const fp = (0, import_path17.join)(projectsDir, dir, filename);
8051
- if ((0, import_fs17.existsSync)(fp)) return fp;
8052
- const projectDir = (0, import_path17.join)(projectsDir, dir);
8264
+ if (!(0, import_fs18.existsSync)(projectsDir)) continue;
8265
+ for (const dir of (0, import_fs18.readdirSync)(projectsDir)) {
8266
+ const fp = (0, import_path18.join)(projectsDir, dir, filename);
8267
+ if ((0, import_fs18.existsSync)(fp)) return fp;
8268
+ const projectDir = (0, import_path18.join)(projectsDir, dir);
8053
8269
  try {
8054
- for (const sub of (0, import_fs17.readdirSync)(projectDir)) {
8055
- const subagentPath = (0, import_path17.join)(projectDir, sub, "subagents", filename);
8056
- if ((0, import_fs17.existsSync)(subagentPath)) return subagentPath;
8270
+ for (const sub of (0, import_fs18.readdirSync)(projectDir)) {
8271
+ const subagentPath = (0, import_path18.join)(projectDir, sub, "subagents", filename);
8272
+ if ((0, import_fs18.existsSync)(subagentPath)) return subagentPath;
8057
8273
  }
8058
8274
  } catch {
8059
8275
  }
@@ -8063,7 +8279,7 @@ var StreamerServer = class {
8063
8279
  }
8064
8280
  async readCwdFromJsonl(filePath) {
8065
8281
  return new Promise((resolve2) => {
8066
- const rl = (0, import_readline.createInterface)({ input: (0, import_fs17.createReadStream)(filePath), crlfDelay: Infinity });
8282
+ const rl = (0, import_readline.createInterface)({ input: (0, import_fs18.createReadStream)(filePath), crlfDelay: Infinity });
8067
8283
  let found = false;
8068
8284
  rl.on("line", (line) => {
8069
8285
  if (found) return;
@@ -8153,7 +8369,7 @@ var StreamerServer = class {
8153
8369
  if (this.isManagedTailPath(key)) return;
8154
8370
  let mtimeMs;
8155
8371
  try {
8156
- mtimeMs = (0, import_fs17.statSync)(filePath).mtimeMs;
8372
+ mtimeMs = (0, import_fs18.statSync)(filePath).mtimeMs;
8157
8373
  } catch {
8158
8374
  return;
8159
8375
  }
@@ -8335,7 +8551,7 @@ var StreamerServer = class {
8335
8551
  if (!conv.filePath) return false;
8336
8552
  let mtimeMs = null;
8337
8553
  try {
8338
- mtimeMs = (0, import_fs17.statSync)(conv.filePath).mtimeMs;
8554
+ mtimeMs = (0, import_fs18.statSync)(conv.filePath).mtimeMs;
8339
8555
  } catch {
8340
8556
  return false;
8341
8557
  }
@@ -8708,7 +8924,7 @@ var StreamerServer = class {
8708
8924
  if (this.rejectIfWarmingUp(res)) return;
8709
8925
  const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
8710
8926
  if (session) {
8711
- if (!(0, import_fs17.existsSync)(session.projectPath)) {
8927
+ if (!(0, import_fs18.existsSync)(session.projectPath)) {
8712
8928
  session.failureReason = `Project directory not found: ${session.projectPath}`;
8713
8929
  }
8714
8930
  if (this.ptyManager.hasSession(sessionId)) {
@@ -9179,7 +9395,7 @@ var StreamerServer = class {
9179
9395
  const jsonlCwd = jsonlPath ? await this.readCwdFromJsonl(jsonlPath) : null;
9180
9396
  if (jsonlCwd) {
9181
9397
  projectPath = jsonlCwd;
9182
- projectName = projectName || (0, import_path17.basename)(jsonlCwd);
9398
+ projectName = projectName || (0, import_path18.basename)(jsonlCwd);
9183
9399
  }
9184
9400
  }
9185
9401
  if (!projectPath) {
@@ -9255,7 +9471,7 @@ var StreamerServer = class {
9255
9471
  sessionStore: this.sessionStore,
9256
9472
  // biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
9257
9473
  agentClient: this.agentClient,
9258
- conversationsDir: this.cacheDir ? (0, import_path17.join)((0, import_path17.dirname)(this.cacheDir), "conversations") : "",
9474
+ conversationsDir: this.cacheDir ? (0, import_path18.join)((0, import_path18.dirname)(this.cacheDir), "conversations") : "",
9259
9475
  agentConfig: this.agentConfig
9260
9476
  });
9261
9477
  json(res, result.status, result.body);
@@ -9399,7 +9615,7 @@ var StreamerServer = class {
9399
9615
  // file isn't slurped in full.
9400
9616
  readFirstLineSessionId(filePath) {
9401
9617
  try {
9402
- const content = (0, import_fs17.readFileSync)(filePath, "utf8");
9618
+ const content = (0, import_fs18.readFileSync)(filePath, "utf8");
9403
9619
  const nl = content.indexOf("\n");
9404
9620
  const firstLine = nl === -1 ? content : content.slice(0, nl);
9405
9621
  if (!firstLine.trim()) return null;
@@ -9414,9 +9630,9 @@ var StreamerServer = class {
9414
9630
  // was passed to Claude via --session-id so the filename matches from the start.
9415
9631
  watchForJsonl(sessionId, projectPath) {
9416
9632
  const encoded = projectPath.replace(/[/\\:.]/g, "-");
9417
- const projectsDir = (0, import_path17.join)((0, import_os8.homedir)(), ".claude", "projects", encoded);
9633
+ const projectsDir = (0, import_path18.join)((0, import_os9.homedir)(), ".claude", "projects", encoded);
9418
9634
  const expectedFile = `${sessionId}.jsonl`;
9419
- const filePath = (0, import_path17.join)(projectsDir, expectedFile);
9635
+ const filePath = (0, import_path18.join)(projectsDir, expectedFile);
9420
9636
  const deadline = Date.now() + 12e4;
9421
9637
  let watcher = null;
9422
9638
  const cleanup = () => {
@@ -9434,14 +9650,14 @@ var StreamerServer = class {
9434
9650
  cleanup();
9435
9651
  return;
9436
9652
  }
9437
- let resolvedFilePath = (0, import_fs17.existsSync)(filePath) ? filePath : null;
9438
- if (!resolvedFilePath && (0, import_fs17.existsSync)(projectsDir)) {
9653
+ let resolvedFilePath = (0, import_fs18.existsSync)(filePath) ? filePath : null;
9654
+ if (!resolvedFilePath && (0, import_fs18.existsSync)(projectsDir)) {
9439
9655
  try {
9440
9656
  const now = Date.now();
9441
- const match = (0, import_fs17.readdirSync)(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: (0, import_fs17.statSync)((0, import_path17.join)(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
9442
- ({ f }) => (0, import_path17.basename)(f, ".jsonl") === sessionId || this.readFirstLineSessionId((0, import_path17.join)(projectsDir, f)) === sessionId
9657
+ const match = (0, import_fs18.readdirSync)(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: (0, import_fs18.statSync)((0, import_path18.join)(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
9658
+ ({ f }) => (0, import_path18.basename)(f, ".jsonl") === sessionId || this.readFirstLineSessionId((0, import_path18.join)(projectsDir, f)) === sessionId
9443
9659
  ).sort((a, b) => b.mtime - a.mtime)[0];
9444
- if (match) resolvedFilePath = (0, import_path17.join)(projectsDir, match.f);
9660
+ if (match) resolvedFilePath = (0, import_path18.join)(projectsDir, match.f);
9445
9661
  } catch {
9446
9662
  }
9447
9663
  }
@@ -9449,7 +9665,7 @@ var StreamerServer = class {
9449
9665
  cleanup();
9450
9666
  this.sessionFileMap.set(sessionId, resolvedFilePath);
9451
9667
  try {
9452
- const existing = (0, import_fs17.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
9668
+ const existing = (0, import_fs18.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
9453
9669
  if (existing.length > 0) {
9454
9670
  this.broadcastConversationLines(sessionId, existing);
9455
9671
  }
@@ -9473,7 +9689,7 @@ var StreamerServer = class {
9473
9689
  if (this.sessionFileMap.has(sessionId)) return;
9474
9690
  try {
9475
9691
  require("fs").mkdirSync(projectsDir, { recursive: true });
9476
- watcher = (0, import_fs17.watch)(projectsDir, tryWire);
9692
+ watcher = (0, import_fs18.watch)(projectsDir, tryWire);
9477
9693
  watcher.on("error", cleanup);
9478
9694
  } catch {
9479
9695
  }
@@ -9489,7 +9705,7 @@ var StreamerServer = class {
9489
9705
  watchForCodexRollout(sessionId, projectPath) {
9490
9706
  const deadline = Date.now() + 12e4;
9491
9707
  const now = /* @__PURE__ */ new Date();
9492
- const dateDir = (0, import_path17.join)(
9708
+ const dateDir = (0, import_path18.join)(
9493
9709
  String(now.getFullYear()),
9494
9710
  String(now.getMonth() + 1).padStart(2, "0"),
9495
9711
  String(now.getDate()).padStart(2, "0")
@@ -9502,7 +9718,7 @@ var StreamerServer = class {
9502
9718
  };
9503
9719
  const matchesProjectPath = (candidatePath) => {
9504
9720
  try {
9505
- const firstLine = (0, import_fs17.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
9721
+ const firstLine = (0, import_fs18.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
9506
9722
  if (!firstLine) return null;
9507
9723
  const parsed = JSON.parse(firstLine);
9508
9724
  if (parsed?.type !== "session_meta") return null;
@@ -9530,18 +9746,18 @@ var StreamerServer = class {
9530
9746
  this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
9531
9747
  );
9532
9748
  for (const root of this.codexRoots) {
9533
- const sessionsDir = (0, import_path17.join)(root, dateDir);
9534
- if (!(0, import_fs17.existsSync)(sessionsDir)) continue;
9749
+ const sessionsDir = (0, import_path18.join)(root, dateDir);
9750
+ if (!(0, import_fs18.existsSync)(sessionsDir)) continue;
9535
9751
  let candidateFiles;
9536
9752
  try {
9537
- candidateFiles = (0, import_fs17.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
9753
+ candidateFiles = (0, import_fs18.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
9538
9754
  } catch {
9539
9755
  continue;
9540
9756
  }
9541
9757
  const nowMs = Date.now();
9542
- const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0, import_fs17.statSync)((0, import_path17.join)(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
9758
+ const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0, import_fs18.statSync)((0, import_path18.join)(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
9543
9759
  for (const { f } of recentCandidates) {
9544
- const candidatePath = (0, import_path17.join)(sessionsDir, f);
9760
+ const candidatePath = (0, import_path18.join)(sessionsDir, f);
9545
9761
  const match = matchesProjectPath(candidatePath);
9546
9762
  if (!match) continue;
9547
9763
  if (boundElsewhere.has(match.id)) continue;
@@ -9551,7 +9767,7 @@ var StreamerServer = class {
9551
9767
  this.sessionFileMap.set(sessionId, candidatePath);
9552
9768
  this.fileWatcher.watch(candidatePath);
9553
9769
  try {
9554
- const existing = (0, import_fs17.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
9770
+ const existing = (0, import_fs18.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
9555
9771
  if (existing.length > 0) {
9556
9772
  this.broadcastConversationLines(sessionId, existing);
9557
9773
  }
@@ -9685,7 +9901,7 @@ async function waitForProcessExit(pid, timeoutMs, pollMs = ADOPT_KILL_POLL_MS) {
9685
9901
  }
9686
9902
  function classifyResumability(cwd) {
9687
9903
  if (!cwd) return { resumable: true };
9688
- if ((0, import_fs17.existsSync)(cwd)) return { resumable: true };
9904
+ if ((0, import_fs18.existsSync)(cwd)) return { resumable: true };
9689
9905
  const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
9690
9906
  return {
9691
9907
  resumable: false,