@threadbase-sh/streamer 1.27.2 → 1.27.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
@@ -5134,6 +5134,7 @@ var WSHub = class {
5134
5134
  var BROWSE_SYSTEM_PROMPT = (browseRoot) => `You are working within the project boundary: ${browseRoot}. Do not read, write, or execute commands that access files or directories outside this boundary.`;
5135
5135
  var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
5136
5136
  var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
5137
+ var REFRESH_TTL_MS = 2e3;
5137
5138
  var START_READY_TIMEOUT_MS = 15e3;
5138
5139
  function parseIncludeAgentsEnv(raw) {
5139
5140
  if (raw === void 0) return false;
@@ -5173,6 +5174,12 @@ var StreamerServer = class {
5173
5174
  // Set by onConversationChanged while a scan is in-flight; getScanner() does
5174
5175
  // a single rescan after the current one completes instead of restarting it.
5175
5176
  scannerStale = false;
5177
+ // Single-flight + TTL guard around scanner.refreshFile (see refreshFileGuarded).
5178
+ // A live file's mtime is always newer than the snapshot, so an unguarded
5179
+ // refresh fires on every request and re-parses the whole file from byte 0.
5180
+ // Keyed by filePath; entries drop on settle + TTL expiry, so the map stays
5181
+ // bounded by the active-file set.
5182
+ refreshInFlight = /* @__PURE__ */ new Map();
5176
5183
  // True only while bindWithRetry is actively retrying. The persistent
5177
5184
  // listener-level 'error' handler demotes EADDRINUSE to debug during this
5178
5185
  // window so the self-healing kickstart-relaunch race doesn't spam warn.
@@ -5371,7 +5378,7 @@ var StreamerServer = class {
5371
5378
  if (session.status === "waiting_input" || session.status === "idle") {
5372
5379
  const filePath = this.sessionFileMap.get(session.id);
5373
5380
  if (filePath) {
5374
- this.getScanner().then((scanner) => scanner.refreshFile(filePath)).then((meta) => {
5381
+ this.getScanner().then((scanner) => this.refreshFileGuarded(scanner, filePath)).then((meta) => {
5375
5382
  this.log.info("scanner.refreshFile: ok", {
5376
5383
  event: "scanner.refresh",
5377
5384
  sessionId: session.id,
@@ -5820,6 +5827,34 @@ var StreamerServer = class {
5820
5827
  this.inFlightCacheWrites.delete(guarded);
5821
5828
  });
5822
5829
  }
5830
+ // Single-flight + TTL wrapper for scanner.refreshFile. Both call sites (the
5831
+ // detail-stale branch and the per-turn refresh) route through here so that
5832
+ // N stacked retries on a live, actively-appended file cost one parse, not N:
5833
+ // - a refresh already in flight for the path → await the same promise;
5834
+ // - a refresh that settled within REFRESH_TTL_MS → skip, return null (the
5835
+ // caller keeps serving the current snapshot);
5836
+ // - otherwise start one, cache the promise, and stamp completedAt on settle.
5837
+ // The map is bounded by the active-file set (entries only live while a
5838
+ // refresh is in flight or within its TTL and are overwritten on the next
5839
+ // refresh of the same path).
5840
+ refreshFileGuarded(scanner, filePath) {
5841
+ const existing = this.refreshInFlight.get(filePath);
5842
+ if (existing) {
5843
+ const settled = existing.completedAt > 0;
5844
+ if (!settled) {
5845
+ return existing.promise;
5846
+ }
5847
+ if (Date.now() - existing.completedAt < REFRESH_TTL_MS) {
5848
+ return Promise.resolve(null);
5849
+ }
5850
+ }
5851
+ const entry = { promise: Promise.resolve(null), completedAt: 0 };
5852
+ entry.promise = scanner.refreshFile(filePath).finally(() => {
5853
+ entry.completedAt = Date.now();
5854
+ });
5855
+ this.refreshInFlight.set(filePath, entry);
5856
+ return entry.promise;
5857
+ }
5823
5858
  async close() {
5824
5859
  for (const timer of this.ptyGraceTimers.values()) clearTimeout(timer);
5825
5860
  this.ptyGraceTimers.clear();
@@ -6284,10 +6319,22 @@ var StreamerServer = class {
6284
6319
  const scanner = await this.getScanner(true);
6285
6320
  const fromIndex = await scanner.getConversation(uuid);
6286
6321
  if (fromIndex) {
6322
+ if (this.ptyManager.hasSession(uuid)) {
6323
+ return fromIndex;
6324
+ }
6287
6325
  if (fromIndex.filePath && this.isConversationSnapshotStale(fromIndex)) {
6288
- const refreshedMeta = await scanner.refreshFile(fromIndex.filePath);
6289
- if (!refreshedMeta) return null;
6290
- return await scanner.getConversation(uuid) ?? fromIndex;
6326
+ const filePath2 = fromIndex.filePath;
6327
+ this.trackCacheWrite(
6328
+ this.refreshFileGuarded(scanner, filePath2).catch((err) => {
6329
+ this.log.warn("scanner.refreshFile: failed", {
6330
+ event: "scanner.refresh_failed",
6331
+ conversationId: uuid,
6332
+ filePath: filePath2,
6333
+ trigger: "detail-swr",
6334
+ err
6335
+ });
6336
+ })
6337
+ );
6291
6338
  }
6292
6339
  return fromIndex;
6293
6340
  }