@threadbase-sh/streamer 1.36.0 → 1.36.2

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.js CHANGED
@@ -1397,9 +1397,13 @@ import { existsSync as existsSync3 } from "fs";
1397
1397
  import { basename as basename2 } from "path";
1398
1398
 
1399
1399
  // src/services/questions/detectPermissionGate.ts
1400
- var OSC_777_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*/;
1400
+ var OSC_777_PERMISSION_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*needs your permission/;
1401
+ var OSC_777_WAITING_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*waiting for your input/;
1401
1402
  function hasPermissionOsc(rawData) {
1402
- return OSC_777_RE.test(rawData);
1403
+ return OSC_777_PERMISSION_RE.test(rawData);
1404
+ }
1405
+ function hasWaitingForInputOsc(rawData) {
1406
+ return OSC_777_WAITING_RE.test(rawData);
1403
1407
  }
1404
1408
  var OPTION_RE = /^\s*(❯)?\s*(\d+)\.\s+(.+?)\s*$/;
1405
1409
  var FOOTER_RE = /Enter to select|Esc to cancel|↑|↓|to navigate|to cancel/i;
@@ -1550,22 +1554,29 @@ function detectShellPrompt(lines) {
1550
1554
  };
1551
1555
  }
1552
1556
  const lastNumberedIdx = (() => {
1553
- for (let i = last.idx; i >= 0; i--) {
1554
- if (NUMBERED_RE.test(lines[i])) return i;
1557
+ let i = last.idx;
1558
+ if (!NUMBERED_RE.test(lines[i])) {
1559
+ if (i > 0 && (PRESS_ENTER_RE.test(lines[i].trim()) || CONTINUE_RE.test(lines[i].trim()))) {
1560
+ i--;
1561
+ } else {
1562
+ return -1;
1563
+ }
1555
1564
  }
1556
- return -1;
1565
+ while (i >= 0 && !NUMBERED_RE.test(lines[i]) && lines[i].trim().length === 0) i--;
1566
+ return i >= 0 && NUMBERED_RE.test(lines[i]) ? i : -1;
1557
1567
  })();
1558
1568
  if (lastNumberedIdx >= 0) {
1559
1569
  const options = [];
1560
- for (let i = 0; i <= lastNumberedIdx; i++) {
1570
+ let firstRow = lastNumberedIdx;
1571
+ for (let i = lastNumberedIdx; i >= 0; i--) {
1561
1572
  const m = NUMBERED_RE.exec(lines[i]);
1562
- if (!m) continue;
1573
+ if (!m) break;
1563
1574
  const num = Number.parseInt(m[1], 10);
1564
- if (!Number.isFinite(num)) continue;
1565
- options.push({ index: num, label: m[2].trim(), answerKeys: `${num}${ENTER}` });
1575
+ if (!Number.isFinite(num)) break;
1576
+ options.unshift({ index: num, label: m[2].trim(), answerKeys: `${num}${ENTER}` });
1577
+ firstRow = i;
1566
1578
  }
1567
1579
  if (options.length >= 2) {
1568
- const firstRow = lines.findIndex((l) => NUMBERED_RE.test(l));
1569
1580
  let prompt = "";
1570
1581
  for (let i = firstRow - 1; i >= 0; i--) {
1571
1582
  const t = lines[i].trim();
@@ -2134,12 +2145,13 @@ var PTYManager = class {
2134
2145
  const session = this.sessions.get(sessionId);
2135
2146
  if (!session) return;
2136
2147
  const oscPermission = hasPermissionOsc(rawData);
2148
+ const oscWaitingForInput = hasWaitingForInputOsc(rawData);
2137
2149
  const hasAskFooter = /Enter to select/i.test(stripped);
2138
2150
  const hasPromptMarker = CLAUDE_PROMPT_MARKERS.some((m) => stripped.includes(m));
2139
2151
  const hasShellPromptHint = /[[(]\s*y\s*\/\s*n\s*[\])]|press\s+(enter|return|any key)|\bcontinue\b\s*\?|^\s*(?:❯|>)?\s*\d+[.)]\s+\S/im.test(
2140
2152
  stripped
2141
2153
  );
2142
- if (!oscPermission && !hasAskFooter && !hasShellPromptHint && !this.permissionOpen.has(sessionId) && !this.shellPromptOpen.has(sessionId) && !this.lastScreenQuestionKey.has(sessionId)) {
2154
+ if (!oscPermission && !oscWaitingForInput && !hasAskFooter && !hasShellPromptHint && !this.permissionOpen.has(sessionId) && !this.shellPromptOpen.has(sessionId) && !this.lastScreenQuestionKey.has(sessionId)) {
2143
2155
  return;
2144
2156
  }
2145
2157
  const lines = await this.getOutputLines(sessionId, 60);
@@ -2162,11 +2174,11 @@ var PTYManager = class {
2162
2174
  this.onPermissionChange?.(sessionId, gate ?? { options: [] });
2163
2175
  } else if (this.permissionOpen.has(sessionId) && !askFooterOnScreen) {
2164
2176
  const gate = scrapePermissionGate(lines);
2165
- if (gate) {
2166
- this.onPermissionChange?.(sessionId, gate);
2167
- } else if (hasPromptMarker) {
2177
+ if (oscWaitingForInput || !gate && hasPromptMarker) {
2168
2178
  this.permissionOpen.delete(sessionId);
2169
2179
  this.onPermissionChange?.(sessionId, null);
2180
+ } else if (gate) {
2181
+ this.onPermissionChange?.(sessionId, gate);
2170
2182
  }
2171
2183
  }
2172
2184
  if (askFooterOnScreen) {
@@ -2672,14 +2684,14 @@ import {
2672
2684
  createReadStream,
2673
2685
  existsSync as existsSync10,
2674
2686
  watch as fsWatch,
2675
- readdirSync as readdirSync5,
2687
+ readdirSync as readdirSync6,
2676
2688
  readFileSync as readFileSync8,
2677
- statSync as statSync8
2689
+ statSync as statSync9
2678
2690
  } from "fs";
2679
2691
  import { realpath as realpath2 } from "fs/promises";
2680
2692
  import { createServer } from "http";
2681
- import { homedir as homedir8 } from "os";
2682
- 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";
2683
2695
  import { createInterface } from "readline";
2684
2696
 
2685
2697
  // node_modules/nanoid/index.js
@@ -3592,8 +3604,8 @@ var createSessionRoutes = (deps) => {
3592
3604
  await deps.handleStopSession(c.req.param("id"), c.env.outgoing);
3593
3605
  return alreadyHandled6();
3594
3606
  });
3595
- app.get("/:id", (c) => {
3596
- deps.handleGetSession(c.req.param("id"), c.env.outgoing);
3607
+ app.get("/:id", async (c) => {
3608
+ await deps.handleGetSession(c.req.param("id"), c.env.outgoing);
3597
3609
  return alreadyHandled6();
3598
3610
  });
3599
3611
  return app;
@@ -5408,6 +5420,14 @@ function seal(plaintext, recipientPublicKeyBase64) {
5408
5420
  };
5409
5421
  }
5410
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
+
5411
5431
  // src/services/cache-integrity/cacheIntegrityMonitor.ts
5412
5432
  import { createHash as createHash2 } from "crypto";
5413
5433
  import { existsSync as existsSync8 } from "fs";
@@ -5948,6 +5968,140 @@ function pruneAgentConversations(cache) {
5948
5968
  return { scanned: rows.length, pruned, missing };
5949
5969
  }
5950
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
+
5951
6105
  // src/services/projectChats/deriveProjectChatTitle.ts
5952
6106
  function deriveProjectChatTitle(input) {
5953
6107
  const trimmed = input.title?.trim();
@@ -5959,6 +6113,32 @@ function deriveProjectChatTitle(input) {
5959
6113
  return `Untitled \xB7 ${input.id.slice(0, 8)}`;
5960
6114
  }
5961
6115
 
6116
+ // src/services/questions/parseStatusLine.ts
6117
+ var MODEL_RE = /(Opus|Sonnet|Haiku|Fable)\s+[\d.]+(?:\s*\([^)]*\))?/;
6118
+ var EFFORT_RE = /●\s*([A-Za-z]+)\s*·\s*\/effort/;
6119
+ var PERMISSION_MODE_RE = /⏵⏵\s*([^(·\n]+?)\s*(?:\(|·|$)/;
6120
+ function parseStatusLine(lines) {
6121
+ const info = {};
6122
+ for (let i = lines.length - 1; i >= 0; i--) {
6123
+ const line = lines[i];
6124
+ if (!line) continue;
6125
+ if (info.effort === void 0) {
6126
+ const m = EFFORT_RE.exec(line);
6127
+ if (m) info.effort = m[1];
6128
+ }
6129
+ if (info.permissionMode === void 0) {
6130
+ const m = PERMISSION_MODE_RE.exec(line);
6131
+ if (m) info.permissionMode = m[1].trim();
6132
+ }
6133
+ if (info.model === void 0) {
6134
+ const m = MODEL_RE.exec(line);
6135
+ if (m) info.model = m[0].replace(/\s+/g, " ").trim();
6136
+ }
6137
+ if (info.model && info.effort && info.permissionMode) break;
6138
+ }
6139
+ return info;
6140
+ }
6141
+
5962
6142
  // src/services/questions/detectAskUserQuestion.ts
5963
6143
  function normalizeContent2(raw) {
5964
6144
  if (Array.isArray(raw)) return raw;
@@ -6083,7 +6263,7 @@ function resolveAnswer(pending, body) {
6083
6263
  }
6084
6264
 
6085
6265
  // src/services/sessions/conversationBusy.ts
6086
- import { statSync as statSync7 } from "fs";
6266
+ import { statSync as statSync8 } from "fs";
6087
6267
  var RESUME_BUSY_WINDOW_MS = 12e4;
6088
6268
  function resolveResumeBusyWindowMs(env = process.env) {
6089
6269
  const raw = env.THREADBASE_RESUME_BUSY_WINDOW_MS;
@@ -6100,7 +6280,7 @@ function conversationBusy(input) {
6100
6280
  let lastActivityMs = null;
6101
6281
  if (input.jsonlPath) {
6102
6282
  try {
6103
- const mtimeMs = statSync7(input.jsonlPath).mtimeMs;
6283
+ const mtimeMs = statSync8(input.jsonlPath).mtimeMs;
6104
6284
  const age = now - mtimeMs;
6105
6285
  lastActivityMs = Math.max(0, age);
6106
6286
  const isSelfEcho = input.selfPtyEndedAt != null && mtimeMs <= input.selfPtyEndedAt + SELF_ACTIVITY_SKEW_MS;
@@ -6328,7 +6508,7 @@ function discoveredToResponse(d, conversationId) {
6328
6508
  import { randomBytes as randomBytes3 } from "crypto";
6329
6509
  import { mkdir as mkdir3, writeFile } from "fs/promises";
6330
6510
  import heicConvert from "heic-convert";
6331
- import { join as join16 } from "path";
6511
+ import { join as join17 } from "path";
6332
6512
  var UPLOAD_DIR_NAME = ".threadbase-uploads";
6333
6513
  var MAX_BYTES = 25 * 1024 * 1024;
6334
6514
  var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
@@ -6361,9 +6541,9 @@ async function saveUploadFile(input) {
6361
6541
  }
6362
6542
  const id = `up_${randomBytes3(8).toString("hex")}`;
6363
6543
  const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
6364
- const dir = join16(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
6544
+ const dir = join17(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
6365
6545
  await mkdir3(dir, { recursive: true });
6366
- const filePath = join16(dir, `${Date.now()}-${id}-${safeName}`);
6546
+ const filePath = join17(dir, `${Date.now()}-${id}-${safeName}`);
6367
6547
  await writeFile(filePath, buffer);
6368
6548
  return {
6369
6549
  id,
@@ -6473,14 +6653,6 @@ function computeConversationEtag({
6473
6653
  return `"${digest}"`;
6474
6654
  }
6475
6655
 
6476
- // src/utils/dates.ts
6477
- import { compareDesc, isValid, parseISO } from "date-fns";
6478
- function parseIsoDateOrNull(value) {
6479
- if (!value) return null;
6480
- const parsed = parseISO(value);
6481
- return isValid(parsed) ? parsed : null;
6482
- }
6483
-
6484
6656
  // src/utils/isScannedSnapshotStale.ts
6485
6657
  var STALENESS_TOLERANCE_MS = 1e3;
6486
6658
  function isScannedSnapshotStale(snapshotTimestamp, fileMtimeMs) {
@@ -6781,13 +6953,13 @@ var StreamerServer = class {
6781
6953
  this.disableDb = config.disableDb ?? false;
6782
6954
  this.scannerPersistenceDisabled = config.scannerPersistent === false;
6783
6955
  this.scanProfiles = config.scanProfiles;
6784
- this.codexRoots = config.codexRoots ?? [join17(homedir8(), ".codex", "sessions")];
6956
+ this.codexRoots = config.codexRoots ?? [join18(homedir9(), ".codex", "sessions")];
6785
6957
  this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
6786
6958
  this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
6787
6959
  this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
6788
6960
  this.defaultModel = config.defaultModel ?? "sonnet";
6789
6961
  this.defaultEffort = config.defaultEffort ?? "low";
6790
- this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join17(homedir8(), ".threadbase", "cache");
6962
+ this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join18(homedir9(), ".threadbase", "cache");
6791
6963
  this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
6792
6964
  this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
6793
6965
  this.markScannerStaleDebounced = debounce(() => {
@@ -6828,7 +7000,7 @@ var StreamerServer = class {
6828
7000
  const seqs = cache.extendMessageIndex(
6829
7001
  filePath,
6830
7002
  spans,
6831
- statSync8(filePath),
7003
+ statSync9(filePath),
6832
7004
  readFrom,
6833
7005
  endOffset
6834
7006
  );
@@ -6994,7 +7166,7 @@ var StreamerServer = class {
6994
7166
  temporalClient,
6995
7167
  taskQueue: agentConfig.temporal.taskQueue
6996
7168
  });
6997
- const conversationsBaseDir = agentConfig.conversationsDir || join17(dirname9(this.cacheDir), "conversations");
7169
+ const conversationsBaseDir = agentConfig.conversationsDir || join18(dirname9(this.cacheDir), "conversations");
6998
7170
  conversationWriter = createConversationWriter({
6999
7171
  baseDir: conversationsBaseDir
7000
7172
  });
@@ -7115,7 +7287,7 @@ var StreamerServer = class {
7115
7287
  }
7116
7288
  }
7117
7289
  if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
7118
- this.startGraceTimer(msg.sessionId, 0);
7290
+ this.startGraceTimer(msg.sessionId, this.ptyGracePeriodMs);
7119
7291
  }
7120
7292
  } catch {
7121
7293
  }
@@ -7317,7 +7489,7 @@ var StreamerServer = class {
7317
7489
  });
7318
7490
  try {
7319
7491
  this.cache = ConversationCache.open(
7320
- join17(this.cacheDir, "cache.db"),
7492
+ join18(this.cacheDir, "cache.db"),
7321
7493
  this.tailSize,
7322
7494
  void 0,
7323
7495
  {
@@ -7443,6 +7615,20 @@ var StreamerServer = class {
7443
7615
  count: pruned.length,
7444
7616
  event: "cache.prune_ghosts"
7445
7617
  });
7618
+ if (this.projectsRepo && this.conversationsRepo && this.cacheMetadataRepo) {
7619
+ refreshConversationCache({
7620
+ cache: this.cache,
7621
+ projectsRepo: this.projectsRepo,
7622
+ conversationsRepo: this.conversationsRepo,
7623
+ cacheMetadataRepo: this.cacheMetadataRepo
7624
+ });
7625
+ } else if (this.cacheMetadataRepo) {
7626
+ setCacheMetadata(
7627
+ this.cacheMetadataRepo,
7628
+ "conversations_last_indexed_at",
7629
+ (/* @__PURE__ */ new Date()).toISOString()
7630
+ );
7631
+ }
7446
7632
  }
7447
7633
  }).catch((err) => {
7448
7634
  const message = err instanceof Error ? err.message : String(err);
@@ -7682,6 +7868,56 @@ var StreamerServer = class {
7682
7868
  checkSessionInputRateLimit(sessionId) {
7683
7869
  return this.checkRateLimit(this.sessionInputAttempts, sessionId, 500, 6e4);
7684
7870
  }
7871
+ /** Project roots watched for conversation JSONLs (profiles or ~/.claude/projects). */
7872
+ projectsDirsForFreshnessCheck() {
7873
+ if (this.scanProfiles && this.scanProfiles.length > 0) {
7874
+ return this.scanProfiles.filter((p) => p.enabled).map((p) => join18(p.configDir, "projects"));
7875
+ }
7876
+ return [join18(homedir9(), ".claude", "projects")];
7877
+ }
7878
+ /**
7879
+ * Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by
7880
+ * the automatic freshness path when the directory watcher marked the scanner
7881
+ * stale or shouldRefreshProjectsFromHdd detected disk drift.
7882
+ */
7883
+ async reconcileConversationsCacheFromDisk() {
7884
+ if (!this.cache) return;
7885
+ const scanner = await this.rescanForRefresh();
7886
+ const metas = [...scanner.getMetadataCache().values()];
7887
+ try {
7888
+ this.cache.upsertFromScannerMeta(metas);
7889
+ if (!this.cacheMonitor?.pending) {
7890
+ this.cache.reconcileDeletions(canonicalLivePathSet(metas));
7891
+ }
7892
+ if (this.projectsRepo && this.conversationsRepo && this.cacheMetadataRepo) {
7893
+ refreshConversationCache({
7894
+ cache: this.cache,
7895
+ projectsRepo: this.projectsRepo,
7896
+ conversationsRepo: this.conversationsRepo,
7897
+ cacheMetadataRepo: this.cacheMetadataRepo
7898
+ });
7899
+ } else if (this.cacheMetadataRepo) {
7900
+ setCacheMetadata(
7901
+ this.cacheMetadataRepo,
7902
+ "conversations_last_indexed_at",
7903
+ (/* @__PURE__ */ new Date()).toISOString()
7904
+ );
7905
+ }
7906
+ } catch (err) {
7907
+ this.log.warn(
7908
+ `refresh reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
7909
+ { event: "conversations.reconcile_failed" }
7910
+ );
7911
+ }
7912
+ }
7913
+ shouldAutoReconcileConversationList() {
7914
+ if (!this.cache) return false;
7915
+ if (this.scannerStale) return true;
7916
+ if (!this.conversationsRepo || !this.cacheMetadataRepo) return false;
7917
+ return shouldRefreshProjectsFromHdd(this.conversationsRepo, this.cacheMetadataRepo, {
7918
+ projectsDirs: this.projectsDirsForFreshnessCheck()
7919
+ });
7920
+ }
7685
7921
  async handleListConversations(url, res) {
7686
7922
  if (this.rejectIfWarmingUp(res)) return;
7687
7923
  const limit = intParam(url, "limit", 50);
@@ -7690,19 +7926,14 @@ var StreamerServer = class {
7690
7926
  const project = url.searchParams.get("project") ?? void 0;
7691
7927
  const providerFilter = url.searchParams.get("provider") ?? void 0;
7692
7928
  const bustCache = url.searchParams.get("refresh") === "1";
7693
- if (bustCache && this.cache) {
7694
- const scanner2 = await this.withWarmup("conversation_refresh", () => this.rescanForRefresh());
7695
- const metas2 = [...scanner2.getMetadataCache().values()];
7696
- try {
7697
- this.cache.upsertFromScannerMeta(metas2);
7698
- if (!this.cacheMonitor?.pending) {
7699
- this.cache.reconcileDeletions(canonicalLivePathSet(metas2));
7700
- }
7701
- } catch (err) {
7702
- this.log.warn(
7703
- `refresh reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
7704
- { event: "conversations.reconcile_failed" }
7929
+ if (this.cache && (bustCache || this.shouldAutoReconcileConversationList())) {
7930
+ if (bustCache) {
7931
+ await this.withWarmup(
7932
+ "conversation_refresh",
7933
+ () => this.reconcileConversationsCacheFromDisk()
7705
7934
  );
7935
+ } else {
7936
+ await this.reconcileConversationsCacheFromDisk();
7706
7937
  }
7707
7938
  }
7708
7939
  if (this.cache) {
@@ -7964,21 +8195,21 @@ var StreamerServer = class {
7964
8195
  */
7965
8196
  projectsDirs() {
7966
8197
  if (this.scanProfiles && this.scanProfiles.length > 0) {
7967
- return this.scanProfiles.filter((p) => p.enabled).map((p) => join17(p.configDir, "projects"));
8198
+ return this.scanProfiles.filter((p) => p.enabled).map((p) => join18(p.configDir, "projects"));
7968
8199
  }
7969
- return [join17(homedir8(), ".claude", "projects")];
8200
+ return [join18(homedir9(), ".claude", "projects")];
7970
8201
  }
7971
8202
  findJsonlPath(uuid) {
7972
8203
  const filename = `${uuid}.jsonl`;
7973
8204
  for (const projectsDir of this.projectsDirs()) {
7974
8205
  if (!existsSync10(projectsDir)) continue;
7975
- for (const dir of readdirSync5(projectsDir)) {
7976
- const fp = join17(projectsDir, dir, filename);
8206
+ for (const dir of readdirSync6(projectsDir)) {
8207
+ const fp = join18(projectsDir, dir, filename);
7977
8208
  if (existsSync10(fp)) return fp;
7978
- const projectDir = join17(projectsDir, dir);
8209
+ const projectDir = join18(projectsDir, dir);
7979
8210
  try {
7980
- for (const sub of readdirSync5(projectDir)) {
7981
- const subagentPath = join17(projectDir, sub, "subagents", filename);
8211
+ for (const sub of readdirSync6(projectDir)) {
8212
+ const subagentPath = join18(projectDir, sub, "subagents", filename);
7982
8213
  if (existsSync10(subagentPath)) return subagentPath;
7983
8214
  }
7984
8215
  } catch {
@@ -8079,7 +8310,7 @@ var StreamerServer = class {
8079
8310
  if (this.isManagedTailPath(key)) return;
8080
8311
  let mtimeMs;
8081
8312
  try {
8082
- mtimeMs = statSync8(filePath).mtimeMs;
8313
+ mtimeMs = statSync9(filePath).mtimeMs;
8083
8314
  } catch {
8084
8315
  return;
8085
8316
  }
@@ -8261,7 +8492,7 @@ var StreamerServer = class {
8261
8492
  if (!conv.filePath) return false;
8262
8493
  let mtimeMs = null;
8263
8494
  try {
8264
- mtimeMs = statSync8(conv.filePath).mtimeMs;
8495
+ mtimeMs = statSync9(conv.filePath).mtimeMs;
8265
8496
  } catch {
8266
8497
  return false;
8267
8498
  }
@@ -8630,13 +8861,23 @@ var StreamerServer = class {
8630
8861
  throw err;
8631
8862
  }
8632
8863
  }
8633
- handleGetSession(sessionId, res) {
8864
+ async handleGetSession(sessionId, res) {
8634
8865
  if (this.rejectIfWarmingUp(res)) return;
8635
8866
  const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
8636
8867
  if (session) {
8637
8868
  if (!existsSync10(session.projectPath)) {
8638
8869
  session.failureReason = `Project directory not found: ${session.projectPath}`;
8639
8870
  }
8871
+ if (this.ptyManager.hasSession(sessionId)) {
8872
+ try {
8873
+ const lines = await this.ptyManager.getOutputLines(sessionId, 10);
8874
+ const status = parseStatusLine(lines);
8875
+ if (session.model == null && status.model) session.model = status.model;
8876
+ if (status.effort) session.effort = status.effort;
8877
+ if (status.permissionMode) session.permissionMode = status.permissionMode;
8878
+ } catch {
8879
+ }
8880
+ }
8640
8881
  json(res, 200, session);
8641
8882
  return;
8642
8883
  }
@@ -9171,7 +9412,7 @@ var StreamerServer = class {
9171
9412
  sessionStore: this.sessionStore,
9172
9413
  // biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
9173
9414
  agentClient: this.agentClient,
9174
- conversationsDir: this.cacheDir ? join17(dirname9(this.cacheDir), "conversations") : "",
9415
+ conversationsDir: this.cacheDir ? join18(dirname9(this.cacheDir), "conversations") : "",
9175
9416
  agentConfig: this.agentConfig
9176
9417
  });
9177
9418
  json(res, result.status, result.body);
@@ -9330,9 +9571,9 @@ var StreamerServer = class {
9330
9571
  // was passed to Claude via --session-id so the filename matches from the start.
9331
9572
  watchForJsonl(sessionId, projectPath) {
9332
9573
  const encoded = projectPath.replace(/[/\\:.]/g, "-");
9333
- const projectsDir = join17(homedir8(), ".claude", "projects", encoded);
9574
+ const projectsDir = join18(homedir9(), ".claude", "projects", encoded);
9334
9575
  const expectedFile = `${sessionId}.jsonl`;
9335
- const filePath = join17(projectsDir, expectedFile);
9576
+ const filePath = join18(projectsDir, expectedFile);
9336
9577
  const deadline = Date.now() + 12e4;
9337
9578
  let watcher = null;
9338
9579
  const cleanup = () => {
@@ -9354,10 +9595,10 @@ var StreamerServer = class {
9354
9595
  if (!resolvedFilePath && existsSync10(projectsDir)) {
9355
9596
  try {
9356
9597
  const now = Date.now();
9357
- const match = readdirSync5(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync8(join17(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
9358
- ({ f }) => basename5(f, ".jsonl") === sessionId || this.readFirstLineSessionId(join17(projectsDir, f)) === sessionId
9598
+ const match = readdirSync6(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync9(join18(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
9599
+ ({ f }) => basename5(f, ".jsonl") === sessionId || this.readFirstLineSessionId(join18(projectsDir, f)) === sessionId
9359
9600
  ).sort((a, b) => b.mtime - a.mtime)[0];
9360
- if (match) resolvedFilePath = join17(projectsDir, match.f);
9601
+ if (match) resolvedFilePath = join18(projectsDir, match.f);
9361
9602
  } catch {
9362
9603
  }
9363
9604
  }
@@ -9405,7 +9646,7 @@ var StreamerServer = class {
9405
9646
  watchForCodexRollout(sessionId, projectPath) {
9406
9647
  const deadline = Date.now() + 12e4;
9407
9648
  const now = /* @__PURE__ */ new Date();
9408
- const dateDir = join17(
9649
+ const dateDir = join18(
9409
9650
  String(now.getFullYear()),
9410
9651
  String(now.getMonth() + 1).padStart(2, "0"),
9411
9652
  String(now.getDate()).padStart(2, "0")
@@ -9446,18 +9687,18 @@ var StreamerServer = class {
9446
9687
  this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
9447
9688
  );
9448
9689
  for (const root of this.codexRoots) {
9449
- const sessionsDir = join17(root, dateDir);
9690
+ const sessionsDir = join18(root, dateDir);
9450
9691
  if (!existsSync10(sessionsDir)) continue;
9451
9692
  let candidateFiles;
9452
9693
  try {
9453
- candidateFiles = readdirSync5(sessionsDir).filter((f) => f.endsWith(".jsonl"));
9694
+ candidateFiles = readdirSync6(sessionsDir).filter((f) => f.endsWith(".jsonl"));
9454
9695
  } catch {
9455
9696
  continue;
9456
9697
  }
9457
9698
  const nowMs = Date.now();
9458
- const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync8(join17(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
9699
+ const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync9(join18(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
9459
9700
  for (const { f } of recentCandidates) {
9460
- const candidatePath = join17(sessionsDir, f);
9701
+ const candidatePath = join18(sessionsDir, f);
9461
9702
  const match = matchesProjectPath(candidatePath);
9462
9703
  if (!match) continue;
9463
9704
  if (boundElsewhere.has(match.id)) continue;