@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.cjs CHANGED
@@ -1449,9 +1449,13 @@ var import_fs6 = require("fs");
1449
1449
  var import_path6 = require("path");
1450
1450
 
1451
1451
  // src/services/questions/detectPermissionGate.ts
1452
- var OSC_777_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*/;
1452
+ var OSC_777_PERMISSION_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*needs your permission/;
1453
+ var OSC_777_WAITING_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*waiting for your input/;
1453
1454
  function hasPermissionOsc(rawData) {
1454
- return OSC_777_RE.test(rawData);
1455
+ return OSC_777_PERMISSION_RE.test(rawData);
1456
+ }
1457
+ function hasWaitingForInputOsc(rawData) {
1458
+ return OSC_777_WAITING_RE.test(rawData);
1455
1459
  }
1456
1460
  var OPTION_RE = /^\s*(❯)?\s*(\d+)\.\s+(.+?)\s*$/;
1457
1461
  var FOOTER_RE = /Enter to select|Esc to cancel|↑|↓|to navigate|to cancel/i;
@@ -1602,22 +1606,29 @@ function detectShellPrompt(lines) {
1602
1606
  };
1603
1607
  }
1604
1608
  const lastNumberedIdx = (() => {
1605
- for (let i = last.idx; i >= 0; i--) {
1606
- if (NUMBERED_RE.test(lines[i])) return i;
1609
+ let i = last.idx;
1610
+ if (!NUMBERED_RE.test(lines[i])) {
1611
+ if (i > 0 && (PRESS_ENTER_RE.test(lines[i].trim()) || CONTINUE_RE.test(lines[i].trim()))) {
1612
+ i--;
1613
+ } else {
1614
+ return -1;
1615
+ }
1607
1616
  }
1608
- return -1;
1617
+ while (i >= 0 && !NUMBERED_RE.test(lines[i]) && lines[i].trim().length === 0) i--;
1618
+ return i >= 0 && NUMBERED_RE.test(lines[i]) ? i : -1;
1609
1619
  })();
1610
1620
  if (lastNumberedIdx >= 0) {
1611
1621
  const options = [];
1612
- for (let i = 0; i <= lastNumberedIdx; i++) {
1622
+ let firstRow = lastNumberedIdx;
1623
+ for (let i = lastNumberedIdx; i >= 0; i--) {
1613
1624
  const m = NUMBERED_RE.exec(lines[i]);
1614
- if (!m) continue;
1625
+ if (!m) break;
1615
1626
  const num = Number.parseInt(m[1], 10);
1616
- if (!Number.isFinite(num)) continue;
1617
- options.push({ index: num, label: m[2].trim(), answerKeys: `${num}${ENTER}` });
1627
+ if (!Number.isFinite(num)) break;
1628
+ options.unshift({ index: num, label: m[2].trim(), answerKeys: `${num}${ENTER}` });
1629
+ firstRow = i;
1618
1630
  }
1619
1631
  if (options.length >= 2) {
1620
- const firstRow = lines.findIndex((l) => NUMBERED_RE.test(l));
1621
1632
  let prompt = "";
1622
1633
  for (let i = firstRow - 1; i >= 0; i--) {
1623
1634
  const t = lines[i].trim();
@@ -2186,12 +2197,13 @@ var PTYManager = class {
2186
2197
  const session = this.sessions.get(sessionId);
2187
2198
  if (!session) return;
2188
2199
  const oscPermission = hasPermissionOsc(rawData);
2200
+ const oscWaitingForInput = hasWaitingForInputOsc(rawData);
2189
2201
  const hasAskFooter = /Enter to select/i.test(stripped);
2190
2202
  const hasPromptMarker = CLAUDE_PROMPT_MARKERS.some((m) => stripped.includes(m));
2191
2203
  const hasShellPromptHint = /[[(]\s*y\s*\/\s*n\s*[\])]|press\s+(enter|return|any key)|\bcontinue\b\s*\?|^\s*(?:❯|>)?\s*\d+[.)]\s+\S/im.test(
2192
2204
  stripped
2193
2205
  );
2194
- if (!oscPermission && !hasAskFooter && !hasShellPromptHint && !this.permissionOpen.has(sessionId) && !this.shellPromptOpen.has(sessionId) && !this.lastScreenQuestionKey.has(sessionId)) {
2206
+ if (!oscPermission && !oscWaitingForInput && !hasAskFooter && !hasShellPromptHint && !this.permissionOpen.has(sessionId) && !this.shellPromptOpen.has(sessionId) && !this.lastScreenQuestionKey.has(sessionId)) {
2195
2207
  return;
2196
2208
  }
2197
2209
  const lines = await this.getOutputLines(sessionId, 60);
@@ -2214,11 +2226,11 @@ var PTYManager = class {
2214
2226
  this.onPermissionChange?.(sessionId, gate ?? { options: [] });
2215
2227
  } else if (this.permissionOpen.has(sessionId) && !askFooterOnScreen) {
2216
2228
  const gate = scrapePermissionGate(lines);
2217
- if (gate) {
2218
- this.onPermissionChange?.(sessionId, gate);
2219
- } else if (hasPromptMarker) {
2229
+ if (oscWaitingForInput || !gate && hasPromptMarker) {
2220
2230
  this.permissionOpen.delete(sessionId);
2221
2231
  this.onPermissionChange?.(sessionId, null);
2232
+ } else if (gate) {
2233
+ this.onPermissionChange?.(sessionId, gate);
2222
2234
  }
2223
2235
  }
2224
2236
  if (askFooterOnScreen) {
@@ -2713,11 +2725,11 @@ var import_node_ws = require("@hono/node-ws");
2713
2725
  var import_client = require("@temporalio/client");
2714
2726
  var import_scanner3 = require("@threadbase-sh/scanner");
2715
2727
  var import_events = require("events");
2716
- var import_fs17 = require("fs");
2728
+ var import_fs18 = require("fs");
2717
2729
  var import_promises7 = require("fs/promises");
2718
2730
  var import_http = require("http");
2719
- var import_os8 = require("os");
2720
- var import_path17 = require("path");
2731
+ var import_os9 = require("os");
2732
+ var import_path18 = require("path");
2721
2733
  var import_readline = require("readline");
2722
2734
 
2723
2735
  // node_modules/nanoid/index.js
@@ -3630,8 +3642,8 @@ var createSessionRoutes = (deps) => {
3630
3642
  await deps.handleStopSession(c.req.param("id"), c.env.outgoing);
3631
3643
  return alreadyHandled6();
3632
3644
  });
3633
- app.get("/:id", (c) => {
3634
- deps.handleGetSession(c.req.param("id"), c.env.outgoing);
3645
+ app.get("/:id", async (c) => {
3646
+ await deps.handleGetSession(c.req.param("id"), c.env.outgoing);
3635
3647
  return alreadyHandled6();
3636
3648
  });
3637
3649
  return app;
@@ -5444,6 +5456,14 @@ function seal(plaintext, recipientPublicKeyBase64) {
5444
5456
  };
5445
5457
  }
5446
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
+
5447
5467
  // src/services/cache-integrity/cacheIntegrityMonitor.ts
5448
5468
  var import_crypto8 = require("crypto");
5449
5469
  var import_fs13 = require("fs");
@@ -5984,6 +6004,140 @@ function pruneAgentConversations(cache) {
5984
6004
  return { scanned: rows.length, pruned, missing };
5985
6005
  }
5986
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
+
5987
6141
  // src/services/projectChats/deriveProjectChatTitle.ts
5988
6142
  function deriveProjectChatTitle(input) {
5989
6143
  const trimmed = input.title?.trim();
@@ -5995,6 +6149,32 @@ function deriveProjectChatTitle(input) {
5995
6149
  return `Untitled \xB7 ${input.id.slice(0, 8)}`;
5996
6150
  }
5997
6151
 
6152
+ // src/services/questions/parseStatusLine.ts
6153
+ var MODEL_RE = /(Opus|Sonnet|Haiku|Fable)\s+[\d.]+(?:\s*\([^)]*\))?/;
6154
+ var EFFORT_RE = /●\s*([A-Za-z]+)\s*·\s*\/effort/;
6155
+ var PERMISSION_MODE_RE = /⏵⏵\s*([^(·\n]+?)\s*(?:\(|·|$)/;
6156
+ function parseStatusLine(lines) {
6157
+ const info = {};
6158
+ for (let i = lines.length - 1; i >= 0; i--) {
6159
+ const line = lines[i];
6160
+ if (!line) continue;
6161
+ if (info.effort === void 0) {
6162
+ const m = EFFORT_RE.exec(line);
6163
+ if (m) info.effort = m[1];
6164
+ }
6165
+ if (info.permissionMode === void 0) {
6166
+ const m = PERMISSION_MODE_RE.exec(line);
6167
+ if (m) info.permissionMode = m[1].trim();
6168
+ }
6169
+ if (info.model === void 0) {
6170
+ const m = MODEL_RE.exec(line);
6171
+ if (m) info.model = m[0].replace(/\s+/g, " ").trim();
6172
+ }
6173
+ if (info.model && info.effort && info.permissionMode) break;
6174
+ }
6175
+ return info;
6176
+ }
6177
+
5998
6178
  // src/services/questions/detectAskUserQuestion.ts
5999
6179
  function normalizeContent2(raw) {
6000
6180
  if (Array.isArray(raw)) return raw;
@@ -6119,7 +6299,7 @@ function resolveAnswer(pending, body) {
6119
6299
  }
6120
6300
 
6121
6301
  // src/services/sessions/conversationBusy.ts
6122
- var import_fs16 = require("fs");
6302
+ var import_fs17 = require("fs");
6123
6303
  var RESUME_BUSY_WINDOW_MS = 12e4;
6124
6304
  function resolveResumeBusyWindowMs(env = process.env) {
6125
6305
  const raw = env.THREADBASE_RESUME_BUSY_WINDOW_MS;
@@ -6136,7 +6316,7 @@ function conversationBusy(input) {
6136
6316
  let lastActivityMs = null;
6137
6317
  if (input.jsonlPath) {
6138
6318
  try {
6139
- const mtimeMs = (0, import_fs16.statSync)(input.jsonlPath).mtimeMs;
6319
+ const mtimeMs = (0, import_fs17.statSync)(input.jsonlPath).mtimeMs;
6140
6320
  const age = now - mtimeMs;
6141
6321
  lastActivityMs = Math.max(0, age);
6142
6322
  const isSelfEcho = input.selfPtyEndedAt != null && mtimeMs <= input.selfPtyEndedAt + SELF_ACTIVITY_SKEW_MS;
@@ -6364,7 +6544,7 @@ function discoveredToResponse(d, conversationId) {
6364
6544
  var import_crypto9 = require("crypto");
6365
6545
  var import_promises6 = require("fs/promises");
6366
6546
  var import_heic_convert = __toESM(require("heic-convert"), 1);
6367
- var import_path16 = require("path");
6547
+ var import_path17 = require("path");
6368
6548
  var UPLOAD_DIR_NAME = ".threadbase-uploads";
6369
6549
  var MAX_BYTES = 25 * 1024 * 1024;
6370
6550
  var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
@@ -6397,9 +6577,9 @@ async function saveUploadFile(input) {
6397
6577
  }
6398
6578
  const id = `up_${(0, import_crypto9.randomBytes)(8).toString("hex")}`;
6399
6579
  const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
6400
- 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);
6401
6581
  await (0, import_promises6.mkdir)(dir, { recursive: true });
6402
- const filePath = (0, import_path16.join)(dir, `${Date.now()}-${id}-${safeName}`);
6582
+ const filePath = (0, import_path17.join)(dir, `${Date.now()}-${id}-${safeName}`);
6403
6583
  await (0, import_promises6.writeFile)(filePath, buffer);
6404
6584
  return {
6405
6585
  id,
@@ -6509,14 +6689,6 @@ function computeConversationEtag({
6509
6689
  return `"${digest}"`;
6510
6690
  }
6511
6691
 
6512
- // src/utils/dates.ts
6513
- var import_date_fns = require("date-fns");
6514
- function parseIsoDateOrNull(value) {
6515
- if (!value) return null;
6516
- const parsed = (0, import_date_fns.parseISO)(value);
6517
- return (0, import_date_fns.isValid)(parsed) ? parsed : null;
6518
- }
6519
-
6520
6692
  // src/utils/isScannedSnapshotStale.ts
6521
6693
  var STALENESS_TOLERANCE_MS = 1e3;
6522
6694
  function isScannedSnapshotStale(snapshotTimestamp, fileMtimeMs) {
@@ -6817,13 +6989,13 @@ var StreamerServer = class {
6817
6989
  this.disableDb = config.disableDb ?? false;
6818
6990
  this.scannerPersistenceDisabled = config.scannerPersistent === false;
6819
6991
  this.scanProfiles = config.scanProfiles;
6820
- this.codexRoots = config.codexRoots ?? [(0, import_path17.join)((0, import_os8.homedir)(), ".codex", "sessions")];
6992
+ this.codexRoots = config.codexRoots ?? [(0, import_path18.join)((0, import_os9.homedir)(), ".codex", "sessions")];
6821
6993
  this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
6822
6994
  this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
6823
6995
  this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
6824
6996
  this.defaultModel = config.defaultModel ?? "sonnet";
6825
6997
  this.defaultEffort = config.defaultEffort ?? "low";
6826
- this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path17.join)((0, import_os8.homedir)(), ".threadbase", "cache");
6998
+ this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path18.join)((0, import_os9.homedir)(), ".threadbase", "cache");
6827
6999
  this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
6828
7000
  this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
6829
7001
  this.markScannerStaleDebounced = debounce(() => {
@@ -6864,7 +7036,7 @@ var StreamerServer = class {
6864
7036
  const seqs = cache.extendMessageIndex(
6865
7037
  filePath,
6866
7038
  spans,
6867
- (0, import_fs17.statSync)(filePath),
7039
+ (0, import_fs18.statSync)(filePath),
6868
7040
  readFrom,
6869
7041
  endOffset
6870
7042
  );
@@ -7030,7 +7202,7 @@ var StreamerServer = class {
7030
7202
  temporalClient,
7031
7203
  taskQueue: agentConfig.temporal.taskQueue
7032
7204
  });
7033
- const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path17.join)((0, import_path17.dirname)(this.cacheDir), "conversations");
7205
+ const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path18.join)((0, import_path18.dirname)(this.cacheDir), "conversations");
7034
7206
  conversationWriter = createConversationWriter({
7035
7207
  baseDir: conversationsBaseDir
7036
7208
  });
@@ -7151,7 +7323,7 @@ var StreamerServer = class {
7151
7323
  }
7152
7324
  }
7153
7325
  if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
7154
- this.startGraceTimer(msg.sessionId, 0);
7326
+ this.startGraceTimer(msg.sessionId, this.ptyGracePeriodMs);
7155
7327
  }
7156
7328
  } catch {
7157
7329
  }
@@ -7353,7 +7525,7 @@ var StreamerServer = class {
7353
7525
  });
7354
7526
  try {
7355
7527
  this.cache = ConversationCache.open(
7356
- (0, import_path17.join)(this.cacheDir, "cache.db"),
7528
+ (0, import_path18.join)(this.cacheDir, "cache.db"),
7357
7529
  this.tailSize,
7358
7530
  void 0,
7359
7531
  {
@@ -7398,7 +7570,7 @@ var StreamerServer = class {
7398
7570
  this.fileWatcher.watchDirectory(dir);
7399
7571
  }
7400
7572
  for (const dir of this.codexRoots) {
7401
- if (!(0, import_fs17.existsSync)(dir)) continue;
7573
+ if (!(0, import_fs18.existsSync)(dir)) continue;
7402
7574
  this.fileWatcher.watchDirectory(dir);
7403
7575
  }
7404
7576
  } catch (err) {
@@ -7479,6 +7651,20 @@ var StreamerServer = class {
7479
7651
  count: pruned.length,
7480
7652
  event: "cache.prune_ghosts"
7481
7653
  });
7654
+ if (this.projectsRepo && this.conversationsRepo && this.cacheMetadataRepo) {
7655
+ refreshConversationCache({
7656
+ cache: this.cache,
7657
+ projectsRepo: this.projectsRepo,
7658
+ conversationsRepo: this.conversationsRepo,
7659
+ cacheMetadataRepo: this.cacheMetadataRepo
7660
+ });
7661
+ } else if (this.cacheMetadataRepo) {
7662
+ setCacheMetadata(
7663
+ this.cacheMetadataRepo,
7664
+ "conversations_last_indexed_at",
7665
+ (/* @__PURE__ */ new Date()).toISOString()
7666
+ );
7667
+ }
7482
7668
  }
7483
7669
  }).catch((err) => {
7484
7670
  const message = err instanceof Error ? err.message : String(err);
@@ -7718,6 +7904,56 @@ var StreamerServer = class {
7718
7904
  checkSessionInputRateLimit(sessionId) {
7719
7905
  return this.checkRateLimit(this.sessionInputAttempts, sessionId, 500, 6e4);
7720
7906
  }
7907
+ /** Project roots watched for conversation JSONLs (profiles or ~/.claude/projects). */
7908
+ projectsDirsForFreshnessCheck() {
7909
+ if (this.scanProfiles && this.scanProfiles.length > 0) {
7910
+ return this.scanProfiles.filter((p) => p.enabled).map((p) => (0, import_path18.join)(p.configDir, "projects"));
7911
+ }
7912
+ return [(0, import_path18.join)((0, import_os9.homedir)(), ".claude", "projects")];
7913
+ }
7914
+ /**
7915
+ * Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by
7916
+ * the automatic freshness path when the directory watcher marked the scanner
7917
+ * stale or shouldRefreshProjectsFromHdd detected disk drift.
7918
+ */
7919
+ async reconcileConversationsCacheFromDisk() {
7920
+ if (!this.cache) return;
7921
+ const scanner = await this.rescanForRefresh();
7922
+ const metas = [...scanner.getMetadataCache().values()];
7923
+ try {
7924
+ this.cache.upsertFromScannerMeta(metas);
7925
+ if (!this.cacheMonitor?.pending) {
7926
+ this.cache.reconcileDeletions(canonicalLivePathSet(metas));
7927
+ }
7928
+ if (this.projectsRepo && this.conversationsRepo && this.cacheMetadataRepo) {
7929
+ refreshConversationCache({
7930
+ cache: this.cache,
7931
+ projectsRepo: this.projectsRepo,
7932
+ conversationsRepo: this.conversationsRepo,
7933
+ cacheMetadataRepo: this.cacheMetadataRepo
7934
+ });
7935
+ } else if (this.cacheMetadataRepo) {
7936
+ setCacheMetadata(
7937
+ this.cacheMetadataRepo,
7938
+ "conversations_last_indexed_at",
7939
+ (/* @__PURE__ */ new Date()).toISOString()
7940
+ );
7941
+ }
7942
+ } catch (err) {
7943
+ this.log.warn(
7944
+ `refresh reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
7945
+ { event: "conversations.reconcile_failed" }
7946
+ );
7947
+ }
7948
+ }
7949
+ shouldAutoReconcileConversationList() {
7950
+ if (!this.cache) return false;
7951
+ if (this.scannerStale) return true;
7952
+ if (!this.conversationsRepo || !this.cacheMetadataRepo) return false;
7953
+ return shouldRefreshProjectsFromHdd(this.conversationsRepo, this.cacheMetadataRepo, {
7954
+ projectsDirs: this.projectsDirsForFreshnessCheck()
7955
+ });
7956
+ }
7721
7957
  async handleListConversations(url, res) {
7722
7958
  if (this.rejectIfWarmingUp(res)) return;
7723
7959
  const limit = intParam(url, "limit", 50);
@@ -7726,19 +7962,14 @@ var StreamerServer = class {
7726
7962
  const project = url.searchParams.get("project") ?? void 0;
7727
7963
  const providerFilter = url.searchParams.get("provider") ?? void 0;
7728
7964
  const bustCache = url.searchParams.get("refresh") === "1";
7729
- if (bustCache && this.cache) {
7730
- const scanner2 = await this.withWarmup("conversation_refresh", () => this.rescanForRefresh());
7731
- const metas2 = [...scanner2.getMetadataCache().values()];
7732
- try {
7733
- this.cache.upsertFromScannerMeta(metas2);
7734
- if (!this.cacheMonitor?.pending) {
7735
- this.cache.reconcileDeletions(canonicalLivePathSet(metas2));
7736
- }
7737
- } catch (err) {
7738
- this.log.warn(
7739
- `refresh reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
7740
- { event: "conversations.reconcile_failed" }
7965
+ if (this.cache && (bustCache || this.shouldAutoReconcileConversationList())) {
7966
+ if (bustCache) {
7967
+ await this.withWarmup(
7968
+ "conversation_refresh",
7969
+ () => this.reconcileConversationsCacheFromDisk()
7741
7970
  );
7971
+ } else {
7972
+ await this.reconcileConversationsCacheFromDisk();
7742
7973
  }
7743
7974
  }
7744
7975
  if (this.cache) {
@@ -8000,22 +8231,22 @@ var StreamerServer = class {
8000
8231
  */
8001
8232
  projectsDirs() {
8002
8233
  if (this.scanProfiles && this.scanProfiles.length > 0) {
8003
- return this.scanProfiles.filter((p) => p.enabled).map((p) => (0, import_path17.join)(p.configDir, "projects"));
8234
+ return this.scanProfiles.filter((p) => p.enabled).map((p) => (0, import_path18.join)(p.configDir, "projects"));
8004
8235
  }
8005
- return [(0, import_path17.join)((0, import_os8.homedir)(), ".claude", "projects")];
8236
+ return [(0, import_path18.join)((0, import_os9.homedir)(), ".claude", "projects")];
8006
8237
  }
8007
8238
  findJsonlPath(uuid) {
8008
8239
  const filename = `${uuid}.jsonl`;
8009
8240
  for (const projectsDir of this.projectsDirs()) {
8010
- if (!(0, import_fs17.existsSync)(projectsDir)) continue;
8011
- for (const dir of (0, import_fs17.readdirSync)(projectsDir)) {
8012
- const fp = (0, import_path17.join)(projectsDir, dir, filename);
8013
- if ((0, import_fs17.existsSync)(fp)) return fp;
8014
- const projectDir = (0, import_path17.join)(projectsDir, dir);
8241
+ if (!(0, import_fs18.existsSync)(projectsDir)) continue;
8242
+ for (const dir of (0, import_fs18.readdirSync)(projectsDir)) {
8243
+ const fp = (0, import_path18.join)(projectsDir, dir, filename);
8244
+ if ((0, import_fs18.existsSync)(fp)) return fp;
8245
+ const projectDir = (0, import_path18.join)(projectsDir, dir);
8015
8246
  try {
8016
- for (const sub of (0, import_fs17.readdirSync)(projectDir)) {
8017
- const subagentPath = (0, import_path17.join)(projectDir, sub, "subagents", filename);
8018
- if ((0, import_fs17.existsSync)(subagentPath)) return subagentPath;
8247
+ for (const sub of (0, import_fs18.readdirSync)(projectDir)) {
8248
+ const subagentPath = (0, import_path18.join)(projectDir, sub, "subagents", filename);
8249
+ if ((0, import_fs18.existsSync)(subagentPath)) return subagentPath;
8019
8250
  }
8020
8251
  } catch {
8021
8252
  }
@@ -8025,7 +8256,7 @@ var StreamerServer = class {
8025
8256
  }
8026
8257
  async readCwdFromJsonl(filePath) {
8027
8258
  return new Promise((resolve2) => {
8028
- const rl = (0, import_readline.createInterface)({ input: (0, import_fs17.createReadStream)(filePath), crlfDelay: Infinity });
8259
+ const rl = (0, import_readline.createInterface)({ input: (0, import_fs18.createReadStream)(filePath), crlfDelay: Infinity });
8029
8260
  let found = false;
8030
8261
  rl.on("line", (line) => {
8031
8262
  if (found) return;
@@ -8115,7 +8346,7 @@ var StreamerServer = class {
8115
8346
  if (this.isManagedTailPath(key)) return;
8116
8347
  let mtimeMs;
8117
8348
  try {
8118
- mtimeMs = (0, import_fs17.statSync)(filePath).mtimeMs;
8349
+ mtimeMs = (0, import_fs18.statSync)(filePath).mtimeMs;
8119
8350
  } catch {
8120
8351
  return;
8121
8352
  }
@@ -8297,7 +8528,7 @@ var StreamerServer = class {
8297
8528
  if (!conv.filePath) return false;
8298
8529
  let mtimeMs = null;
8299
8530
  try {
8300
- mtimeMs = (0, import_fs17.statSync)(conv.filePath).mtimeMs;
8531
+ mtimeMs = (0, import_fs18.statSync)(conv.filePath).mtimeMs;
8301
8532
  } catch {
8302
8533
  return false;
8303
8534
  }
@@ -8666,13 +8897,23 @@ var StreamerServer = class {
8666
8897
  throw err;
8667
8898
  }
8668
8899
  }
8669
- handleGetSession(sessionId, res) {
8900
+ async handleGetSession(sessionId, res) {
8670
8901
  if (this.rejectIfWarmingUp(res)) return;
8671
8902
  const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
8672
8903
  if (session) {
8673
- if (!(0, import_fs17.existsSync)(session.projectPath)) {
8904
+ if (!(0, import_fs18.existsSync)(session.projectPath)) {
8674
8905
  session.failureReason = `Project directory not found: ${session.projectPath}`;
8675
8906
  }
8907
+ if (this.ptyManager.hasSession(sessionId)) {
8908
+ try {
8909
+ const lines = await this.ptyManager.getOutputLines(sessionId, 10);
8910
+ const status = parseStatusLine(lines);
8911
+ if (session.model == null && status.model) session.model = status.model;
8912
+ if (status.effort) session.effort = status.effort;
8913
+ if (status.permissionMode) session.permissionMode = status.permissionMode;
8914
+ } catch {
8915
+ }
8916
+ }
8676
8917
  json(res, 200, session);
8677
8918
  return;
8678
8919
  }
@@ -9131,7 +9372,7 @@ var StreamerServer = class {
9131
9372
  const jsonlCwd = jsonlPath ? await this.readCwdFromJsonl(jsonlPath) : null;
9132
9373
  if (jsonlCwd) {
9133
9374
  projectPath = jsonlCwd;
9134
- projectName = projectName || (0, import_path17.basename)(jsonlCwd);
9375
+ projectName = projectName || (0, import_path18.basename)(jsonlCwd);
9135
9376
  }
9136
9377
  }
9137
9378
  if (!projectPath) {
@@ -9207,7 +9448,7 @@ var StreamerServer = class {
9207
9448
  sessionStore: this.sessionStore,
9208
9449
  // biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
9209
9450
  agentClient: this.agentClient,
9210
- conversationsDir: this.cacheDir ? (0, import_path17.join)((0, import_path17.dirname)(this.cacheDir), "conversations") : "",
9451
+ conversationsDir: this.cacheDir ? (0, import_path18.join)((0, import_path18.dirname)(this.cacheDir), "conversations") : "",
9211
9452
  agentConfig: this.agentConfig
9212
9453
  });
9213
9454
  json(res, result.status, result.body);
@@ -9351,7 +9592,7 @@ var StreamerServer = class {
9351
9592
  // file isn't slurped in full.
9352
9593
  readFirstLineSessionId(filePath) {
9353
9594
  try {
9354
- const content = (0, import_fs17.readFileSync)(filePath, "utf8");
9595
+ const content = (0, import_fs18.readFileSync)(filePath, "utf8");
9355
9596
  const nl = content.indexOf("\n");
9356
9597
  const firstLine = nl === -1 ? content : content.slice(0, nl);
9357
9598
  if (!firstLine.trim()) return null;
@@ -9366,9 +9607,9 @@ var StreamerServer = class {
9366
9607
  // was passed to Claude via --session-id so the filename matches from the start.
9367
9608
  watchForJsonl(sessionId, projectPath) {
9368
9609
  const encoded = projectPath.replace(/[/\\:.]/g, "-");
9369
- const projectsDir = (0, import_path17.join)((0, import_os8.homedir)(), ".claude", "projects", encoded);
9610
+ const projectsDir = (0, import_path18.join)((0, import_os9.homedir)(), ".claude", "projects", encoded);
9370
9611
  const expectedFile = `${sessionId}.jsonl`;
9371
- const filePath = (0, import_path17.join)(projectsDir, expectedFile);
9612
+ const filePath = (0, import_path18.join)(projectsDir, expectedFile);
9372
9613
  const deadline = Date.now() + 12e4;
9373
9614
  let watcher = null;
9374
9615
  const cleanup = () => {
@@ -9386,14 +9627,14 @@ var StreamerServer = class {
9386
9627
  cleanup();
9387
9628
  return;
9388
9629
  }
9389
- let resolvedFilePath = (0, import_fs17.existsSync)(filePath) ? filePath : null;
9390
- if (!resolvedFilePath && (0, import_fs17.existsSync)(projectsDir)) {
9630
+ let resolvedFilePath = (0, import_fs18.existsSync)(filePath) ? filePath : null;
9631
+ if (!resolvedFilePath && (0, import_fs18.existsSync)(projectsDir)) {
9391
9632
  try {
9392
9633
  const now = Date.now();
9393
- 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(
9394
- ({ f }) => (0, import_path17.basename)(f, ".jsonl") === sessionId || this.readFirstLineSessionId((0, import_path17.join)(projectsDir, f)) === sessionId
9634
+ 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(
9635
+ ({ f }) => (0, import_path18.basename)(f, ".jsonl") === sessionId || this.readFirstLineSessionId((0, import_path18.join)(projectsDir, f)) === sessionId
9395
9636
  ).sort((a, b) => b.mtime - a.mtime)[0];
9396
- if (match) resolvedFilePath = (0, import_path17.join)(projectsDir, match.f);
9637
+ if (match) resolvedFilePath = (0, import_path18.join)(projectsDir, match.f);
9397
9638
  } catch {
9398
9639
  }
9399
9640
  }
@@ -9401,7 +9642,7 @@ var StreamerServer = class {
9401
9642
  cleanup();
9402
9643
  this.sessionFileMap.set(sessionId, resolvedFilePath);
9403
9644
  try {
9404
- const existing = (0, import_fs17.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
9645
+ const existing = (0, import_fs18.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
9405
9646
  if (existing.length > 0) {
9406
9647
  this.broadcastConversationLines(sessionId, existing);
9407
9648
  }
@@ -9425,7 +9666,7 @@ var StreamerServer = class {
9425
9666
  if (this.sessionFileMap.has(sessionId)) return;
9426
9667
  try {
9427
9668
  require("fs").mkdirSync(projectsDir, { recursive: true });
9428
- watcher = (0, import_fs17.watch)(projectsDir, tryWire);
9669
+ watcher = (0, import_fs18.watch)(projectsDir, tryWire);
9429
9670
  watcher.on("error", cleanup);
9430
9671
  } catch {
9431
9672
  }
@@ -9441,7 +9682,7 @@ var StreamerServer = class {
9441
9682
  watchForCodexRollout(sessionId, projectPath) {
9442
9683
  const deadline = Date.now() + 12e4;
9443
9684
  const now = /* @__PURE__ */ new Date();
9444
- const dateDir = (0, import_path17.join)(
9685
+ const dateDir = (0, import_path18.join)(
9445
9686
  String(now.getFullYear()),
9446
9687
  String(now.getMonth() + 1).padStart(2, "0"),
9447
9688
  String(now.getDate()).padStart(2, "0")
@@ -9454,7 +9695,7 @@ var StreamerServer = class {
9454
9695
  };
9455
9696
  const matchesProjectPath = (candidatePath) => {
9456
9697
  try {
9457
- const firstLine = (0, import_fs17.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
9698
+ const firstLine = (0, import_fs18.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
9458
9699
  if (!firstLine) return null;
9459
9700
  const parsed = JSON.parse(firstLine);
9460
9701
  if (parsed?.type !== "session_meta") return null;
@@ -9482,18 +9723,18 @@ var StreamerServer = class {
9482
9723
  this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
9483
9724
  );
9484
9725
  for (const root of this.codexRoots) {
9485
- const sessionsDir = (0, import_path17.join)(root, dateDir);
9486
- if (!(0, import_fs17.existsSync)(sessionsDir)) continue;
9726
+ const sessionsDir = (0, import_path18.join)(root, dateDir);
9727
+ if (!(0, import_fs18.existsSync)(sessionsDir)) continue;
9487
9728
  let candidateFiles;
9488
9729
  try {
9489
- candidateFiles = (0, import_fs17.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
9730
+ candidateFiles = (0, import_fs18.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
9490
9731
  } catch {
9491
9732
  continue;
9492
9733
  }
9493
9734
  const nowMs = Date.now();
9494
- 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);
9735
+ 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);
9495
9736
  for (const { f } of recentCandidates) {
9496
- const candidatePath = (0, import_path17.join)(sessionsDir, f);
9737
+ const candidatePath = (0, import_path18.join)(sessionsDir, f);
9497
9738
  const match = matchesProjectPath(candidatePath);
9498
9739
  if (!match) continue;
9499
9740
  if (boundElsewhere.has(match.id)) continue;
@@ -9503,7 +9744,7 @@ var StreamerServer = class {
9503
9744
  this.sessionFileMap.set(sessionId, candidatePath);
9504
9745
  this.fileWatcher.watch(candidatePath);
9505
9746
  try {
9506
- const existing = (0, import_fs17.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
9747
+ const existing = (0, import_fs18.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
9507
9748
  if (existing.length > 0) {
9508
9749
  this.broadcastConversationLines(sessionId, existing);
9509
9750
  }
@@ -9637,7 +9878,7 @@ async function waitForProcessExit(pid, timeoutMs, pollMs = ADOPT_KILL_POLL_MS) {
9637
9878
  }
9638
9879
  function classifyResumability(cwd) {
9639
9880
  if (!cwd) return { resumable: true };
9640
- if ((0, import_fs17.existsSync)(cwd)) return { resumable: true };
9881
+ if ((0, import_fs18.existsSync)(cwd)) return { resumable: true };
9641
9882
  const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
9642
9883
  return {
9643
9884
  resumable: false,