@threadbase-sh/streamer 1.36.2 → 1.36.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1195,11 +1195,13 @@ declare class StreamerServer {
1195
1195
  private selfPtyEndedAt;
1196
1196
  private pendingQuestionKey;
1197
1197
  private pendingPermission;
1198
+ private pendingPermissionKey;
1198
1199
  private scanner;
1199
1200
  private scannerPersistenceDisabled;
1200
1201
  private allScanners;
1201
1202
  private scannerReady;
1202
1203
  private scannerStale;
1204
+ private conversationReconcileInFlight;
1203
1205
  private refreshInFlight;
1204
1206
  private binding;
1205
1207
  private activeWarmups;
@@ -1293,6 +1295,7 @@ declare class StreamerServer {
1293
1295
  * stale or shouldRefreshProjectsFromHdd detected disk drift.
1294
1296
  */
1295
1297
  private reconcileConversationsCacheFromDisk;
1298
+ private startBackgroundConversationReconcile;
1296
1299
  private shouldAutoReconcileConversationList;
1297
1300
  private handleListConversations;
1298
1301
  private handleConversationsCount;
package/dist/index.d.ts CHANGED
@@ -1195,11 +1195,13 @@ declare class StreamerServer {
1195
1195
  private selfPtyEndedAt;
1196
1196
  private pendingQuestionKey;
1197
1197
  private pendingPermission;
1198
+ private pendingPermissionKey;
1198
1199
  private scanner;
1199
1200
  private scannerPersistenceDisabled;
1200
1201
  private allScanners;
1201
1202
  private scannerReady;
1202
1203
  private scannerStale;
1204
+ private conversationReconcileInFlight;
1203
1205
  private refreshInFlight;
1204
1206
  private binding;
1205
1207
  private activeWarmups;
@@ -1293,6 +1295,7 @@ declare class StreamerServer {
1293
1295
  * stale or shouldRefreshProjectsFromHdd detected disk drift.
1294
1296
  */
1295
1297
  private reconcileConversationsCacheFromDisk;
1298
+ private startBackgroundConversationReconcile;
1296
1299
  private shouldAutoReconcileConversationList;
1297
1300
  private handleListConversations;
1298
1301
  private handleConversationsCount;
package/dist/index.js CHANGED
@@ -1397,6 +1397,9 @@ import { existsSync as existsSync3 } from "fs";
1397
1397
  import { basename as basename2 } from "path";
1398
1398
 
1399
1399
  // src/services/questions/detectPermissionGate.ts
1400
+ function permissionContentKey(gate) {
1401
+ return `${gate.prompt ?? ""}::${gate.detail ?? ""}::${gate.options.map((o) => `${o.index}.${o.label}`).join(",")}::${gate.cursor ?? ""}`;
1402
+ }
1400
1403
  var OSC_777_PERMISSION_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*needs your permission/;
1401
1404
  var OSC_777_WAITING_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*waiting for your input/;
1402
1405
  function hasPermissionOsc(rawData) {
@@ -6848,6 +6851,11 @@ var StreamerServer = class {
6848
6851
  // to pendingQuestions; mobile answers it by sending the option index via
6849
6852
  // /input { keys }. Cleared when the gate closes.
6850
6853
  pendingPermission = /* @__PURE__ */ new Map();
6854
+ // Content key (prompt + detail + options + cursor) of the permission gate
6855
+ // currently broadcast for a session — mirrors pendingQuestionKey so a PTY
6856
+ // repaint of the same gate doesn't re-broadcast on every tick. Cleared
6857
+ // alongside pendingPermission.
6858
+ pendingPermissionKey = /* @__PURE__ */ new Map();
6851
6859
  scanner = null;
6852
6860
  // Set when better-sqlite3 is unusable (e.g. node ABI mismatch made
6853
6861
  // ConversationCache.open throw), or when config.scannerPersistent is false
@@ -6863,6 +6871,10 @@ var StreamerServer = class {
6863
6871
  // Set by onConversationChanged while a scan is in-flight; getScanner() does
6864
6872
  // a single rescan after the current one completes instead of restarting it.
6865
6873
  scannerStale = false;
6874
+ // Single-flight guard for the background disk reconcile: a burst of list
6875
+ // polls during active session writes shares one rescan instead of queueing
6876
+ // a full rescan per request.
6877
+ conversationReconcileInFlight = null;
6866
6878
  // Single-flight + TTL guard around scanner.refreshFile (see refreshFileGuarded).
6867
6879
  // A live file's mtime is always newer than the snapshot, so an unguarded
6868
6880
  // refresh fires on every request and re-parses the whole file from byte 0.
@@ -7141,6 +7153,7 @@ var StreamerServer = class {
7141
7153
  this.cancelPendingQuestion(session.id);
7142
7154
  }
7143
7155
  this.pendingPermission.delete(session.id);
7156
+ this.pendingPermissionKey.delete(session.id);
7144
7157
  this.contendedSessions.delete(session.id);
7145
7158
  this.rememberSelfPtyEnded(session.id);
7146
7159
  }
@@ -7880,9 +7893,9 @@ var StreamerServer = class {
7880
7893
  * the automatic freshness path when the directory watcher marked the scanner
7881
7894
  * stale or shouldRefreshProjectsFromHdd detected disk drift.
7882
7895
  */
7883
- async reconcileConversationsCacheFromDisk() {
7896
+ async reconcileConversationsCacheFromDisk(onProgress) {
7884
7897
  if (!this.cache) return;
7885
- const scanner = await this.rescanForRefresh();
7898
+ const scanner = await this.rescanForRefresh(onProgress);
7886
7899
  const metas = [...scanner.getMetadataCache().values()];
7887
7900
  try {
7888
7901
  this.cache.upsertFromScannerMeta(metas);
@@ -7910,6 +7923,18 @@ var StreamerServer = class {
7910
7923
  );
7911
7924
  }
7912
7925
  }
7926
+ // Reconcile the cache from disk without blocking the caller. Single-flighted
7927
+ // so a burst of list polls during active session writes shares one rescan
7928
+ // rather than queueing a full rescan each; tracked so close() awaits the
7929
+ // in-flight cache write before shutting the DB.
7930
+ startBackgroundConversationReconcile() {
7931
+ if (this.conversationReconcileInFlight) return;
7932
+ const task = this.reconcileConversationsCacheFromDisk().finally(() => {
7933
+ this.conversationReconcileInFlight = null;
7934
+ });
7935
+ this.conversationReconcileInFlight = task;
7936
+ this.trackCacheWrite(task);
7937
+ }
7913
7938
  shouldAutoReconcileConversationList() {
7914
7939
  if (!this.cache) return false;
7915
7940
  if (this.scannerStale) return true;
@@ -7927,13 +7952,19 @@ var StreamerServer = class {
7927
7952
  const providerFilter = url.searchParams.get("provider") ?? void 0;
7928
7953
  const bustCache = url.searchParams.get("refresh") === "1";
7929
7954
  if (this.cache && (bustCache || this.shouldAutoReconcileConversationList())) {
7930
- if (bustCache) {
7955
+ const canServeStale = !bustCache && this.cache.listConversations({ limit: 0, offset: 0 }).total > 0;
7956
+ if (canServeStale) {
7957
+ this.startBackgroundConversationReconcile();
7958
+ } else {
7959
+ const shouldEmitProgress = createScanProgressThrottle();
7931
7960
  await this.withWarmup(
7932
7961
  "conversation_refresh",
7933
- () => this.reconcileConversationsCacheFromDisk()
7962
+ () => this.reconcileConversationsCacheFromDisk((scanned, total2) => {
7963
+ if (shouldEmitProgress(scanned, total2)) {
7964
+ this.wsHub.broadcast({ type: "scan_progress", scanned, total: total2 });
7965
+ }
7966
+ })
7934
7967
  );
7935
- } else {
7936
- await this.reconcileConversationsCacheFromDisk();
7937
7968
  }
7938
7969
  }
7939
7970
  if (this.cache) {
@@ -8169,7 +8200,7 @@ var StreamerServer = class {
8169
8200
  // getFreshScanner() this does NOT discard the warm scanner. scannerReady is
8170
8201
  // only ever reassigned to a live scan promise (never nulled mid-scan), so the
8171
8202
  // getScanner() anti-infinite-loop guard is preserved.
8172
- async rescanForRefresh() {
8203
+ async rescanForRefresh(onProgress) {
8173
8204
  if (this.scannerReady) await this.scannerReady;
8174
8205
  this.scannerStale = false;
8175
8206
  if (!this.scanner) {
@@ -8180,7 +8211,8 @@ var StreamerServer = class {
8180
8211
  this.scannerReady = scanner.scan({
8181
8212
  ...this.scanProfiles ? { profiles: this.scanProfiles } : {},
8182
8213
  ...this.codexScanOpts(),
8183
- fullRescan: true
8214
+ fullRescan: true,
8215
+ ...onProgress ? { onProgress } : {}
8184
8216
  });
8185
8217
  await this.scannerReady;
8186
8218
  return scanner;
@@ -9158,10 +9190,14 @@ var StreamerServer = class {
9158
9190
  if (gate === null) {
9159
9191
  if (!this.pendingPermission.has(sessionId)) return;
9160
9192
  this.pendingPermission.delete(sessionId);
9193
+ this.pendingPermissionKey.delete(sessionId);
9161
9194
  this.wsHub.broadcast({ type: "permission_cancelled", sessionId });
9162
9195
  return;
9163
9196
  }
9197
+ const key = permissionContentKey(gate);
9198
+ if (this.pendingPermissionKey.get(sessionId) === key) return;
9164
9199
  this.pendingPermission.set(sessionId, gate);
9200
+ this.pendingPermissionKey.set(sessionId, key);
9165
9201
  const subscriberCount = this.sessionSubscribers.get(sessionId)?.size ?? 0;
9166
9202
  this.log.info(
9167
9203
  `[ws.broadcast_permission] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,