@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/cli.cjs CHANGED
@@ -141048,6 +141048,9 @@ var import_path22 = require("path");
141048
141048
  init_logger();
141049
141049
 
141050
141050
  // src/services/questions/detectPermissionGate.ts
141051
+ function permissionContentKey(gate) {
141052
+ return `${gate.prompt ?? ""}::${gate.detail ?? ""}::${gate.options.map((o) => `${o.index}.${o.label}`).join(",")}::${gate.cursor ?? ""}`;
141053
+ }
141051
141054
  var OSC_777_PERMISSION_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*needs your permission/;
141052
141055
  var OSC_777_WAITING_RE = /\x1b\]777;notify;Claude Code;[^\x07\x1b]*waiting for your input/;
141053
141056
  function hasPermissionOsc(rawData) {
@@ -144070,6 +144073,11 @@ var StreamerServer = class {
144070
144073
  // to pendingQuestions; mobile answers it by sending the option index via
144071
144074
  // /input { keys }. Cleared when the gate closes.
144072
144075
  pendingPermission = /* @__PURE__ */ new Map();
144076
+ // Content key (prompt + detail + options + cursor) of the permission gate
144077
+ // currently broadcast for a session — mirrors pendingQuestionKey so a PTY
144078
+ // repaint of the same gate doesn't re-broadcast on every tick. Cleared
144079
+ // alongside pendingPermission.
144080
+ pendingPermissionKey = /* @__PURE__ */ new Map();
144073
144081
  scanner = null;
144074
144082
  // Set when better-sqlite3 is unusable (e.g. node ABI mismatch made
144075
144083
  // ConversationCache.open throw), or when config.scannerPersistent is false
@@ -144085,6 +144093,10 @@ var StreamerServer = class {
144085
144093
  // Set by onConversationChanged while a scan is in-flight; getScanner() does
144086
144094
  // a single rescan after the current one completes instead of restarting it.
144087
144095
  scannerStale = false;
144096
+ // Single-flight guard for the background disk reconcile: a burst of list
144097
+ // polls during active session writes shares one rescan instead of queueing
144098
+ // a full rescan per request.
144099
+ conversationReconcileInFlight = null;
144088
144100
  // Single-flight + TTL guard around scanner.refreshFile (see refreshFileGuarded).
144089
144101
  // A live file's mtime is always newer than the snapshot, so an unguarded
144090
144102
  // refresh fires on every request and re-parses the whole file from byte 0.
@@ -144363,6 +144375,7 @@ var StreamerServer = class {
144363
144375
  this.cancelPendingQuestion(session.id);
144364
144376
  }
144365
144377
  this.pendingPermission.delete(session.id);
144378
+ this.pendingPermissionKey.delete(session.id);
144366
144379
  this.contendedSessions.delete(session.id);
144367
144380
  this.rememberSelfPtyEnded(session.id);
144368
144381
  }
@@ -145102,9 +145115,9 @@ var StreamerServer = class {
145102
145115
  * the automatic freshness path when the directory watcher marked the scanner
145103
145116
  * stale or shouldRefreshProjectsFromHdd detected disk drift.
145104
145117
  */
145105
- async reconcileConversationsCacheFromDisk() {
145118
+ async reconcileConversationsCacheFromDisk(onProgress) {
145106
145119
  if (!this.cache) return;
145107
- const scanner = await this.rescanForRefresh();
145120
+ const scanner = await this.rescanForRefresh(onProgress);
145108
145121
  const metas = [...scanner.getMetadataCache().values()];
145109
145122
  try {
145110
145123
  this.cache.upsertFromScannerMeta(metas);
@@ -145132,6 +145145,18 @@ var StreamerServer = class {
145132
145145
  );
145133
145146
  }
145134
145147
  }
145148
+ // Reconcile the cache from disk without blocking the caller. Single-flighted
145149
+ // so a burst of list polls during active session writes shares one rescan
145150
+ // rather than queueing a full rescan each; tracked so close() awaits the
145151
+ // in-flight cache write before shutting the DB.
145152
+ startBackgroundConversationReconcile() {
145153
+ if (this.conversationReconcileInFlight) return;
145154
+ const task = this.reconcileConversationsCacheFromDisk().finally(() => {
145155
+ this.conversationReconcileInFlight = null;
145156
+ });
145157
+ this.conversationReconcileInFlight = task;
145158
+ this.trackCacheWrite(task);
145159
+ }
145135
145160
  shouldAutoReconcileConversationList() {
145136
145161
  if (!this.cache) return false;
145137
145162
  if (this.scannerStale) return true;
@@ -145149,13 +145174,19 @@ var StreamerServer = class {
145149
145174
  const providerFilter = url2.searchParams.get("provider") ?? void 0;
145150
145175
  const bustCache = url2.searchParams.get("refresh") === "1";
145151
145176
  if (this.cache && (bustCache || this.shouldAutoReconcileConversationList())) {
145152
- if (bustCache) {
145177
+ const canServeStale = !bustCache && this.cache.listConversations({ limit: 0, offset: 0 }).total > 0;
145178
+ if (canServeStale) {
145179
+ this.startBackgroundConversationReconcile();
145180
+ } else {
145181
+ const shouldEmitProgress = createScanProgressThrottle();
145153
145182
  await this.withWarmup(
145154
145183
  "conversation_refresh",
145155
- () => this.reconcileConversationsCacheFromDisk()
145184
+ () => this.reconcileConversationsCacheFromDisk((scanned, total2) => {
145185
+ if (shouldEmitProgress(scanned, total2)) {
145186
+ this.wsHub.broadcast({ type: "scan_progress", scanned, total: total2 });
145187
+ }
145188
+ })
145156
145189
  );
145157
- } else {
145158
- await this.reconcileConversationsCacheFromDisk();
145159
145190
  }
145160
145191
  }
145161
145192
  if (this.cache) {
@@ -145391,7 +145422,7 @@ var StreamerServer = class {
145391
145422
  // getFreshScanner() this does NOT discard the warm scanner. scannerReady is
145392
145423
  // only ever reassigned to a live scan promise (never nulled mid-scan), so the
145393
145424
  // getScanner() anti-infinite-loop guard is preserved.
145394
- async rescanForRefresh() {
145425
+ async rescanForRefresh(onProgress) {
145395
145426
  if (this.scannerReady) await this.scannerReady;
145396
145427
  this.scannerStale = false;
145397
145428
  if (!this.scanner) {
@@ -145402,7 +145433,8 @@ var StreamerServer = class {
145402
145433
  this.scannerReady = scanner.scan({
145403
145434
  ...this.scanProfiles ? { profiles: this.scanProfiles } : {},
145404
145435
  ...this.codexScanOpts(),
145405
- fullRescan: true
145436
+ fullRescan: true,
145437
+ ...onProgress ? { onProgress } : {}
145406
145438
  });
145407
145439
  await this.scannerReady;
145408
145440
  return scanner;
@@ -146380,10 +146412,14 @@ var StreamerServer = class {
146380
146412
  if (gate === null) {
146381
146413
  if (!this.pendingPermission.has(sessionId)) return;
146382
146414
  this.pendingPermission.delete(sessionId);
146415
+ this.pendingPermissionKey.delete(sessionId);
146383
146416
  this.wsHub.broadcast({ type: "permission_cancelled", sessionId });
146384
146417
  return;
146385
146418
  }
146419
+ const key = permissionContentKey(gate);
146420
+ if (this.pendingPermissionKey.get(sessionId) === key) return;
146386
146421
  this.pendingPermission.set(sessionId, gate);
146422
+ this.pendingPermissionKey.set(sessionId, key);
146387
146423
  const subscriberCount = this.sessionSubscribers.get(sessionId)?.size ?? 0;
146388
146424
  this.log.info(
146389
146425
  `[ws.broadcast_permission] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,