@threadbase-sh/streamer 1.34.0 → 1.36.0

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
@@ -5513,12 +5513,13 @@ function fingerprintOf(ids) {
5513
5513
  return `sha256:${(0, import_crypto8.createHash)("sha256").update(sorted.join("\n")).digest("hex")}`;
5514
5514
  }
5515
5515
  var CacheIntegrityMonitor = class {
5516
- constructor(cache, wsHub, log3, cacheDir, rescan) {
5516
+ constructor(cache, wsHub, log3, cacheDir, rescan, runDuringReset) {
5517
5517
  this.cache = cache;
5518
5518
  this.wsHub = wsHub;
5519
5519
  this.log = log3;
5520
5520
  this.cacheDir = cacheDir;
5521
5521
  this.rescan = rescan;
5522
+ this.runDuringReset = runDuringReset;
5522
5523
  const state = loadAlertState();
5523
5524
  this._pending = state.pending ?? null;
5524
5525
  this.ignoredIds = new Set(state.ignoredIds ?? []);
@@ -5528,6 +5529,7 @@ var CacheIntegrityMonitor = class {
5528
5529
  log;
5529
5530
  cacheDir;
5530
5531
  rescan;
5532
+ runDuringReset;
5531
5533
  _pending;
5532
5534
  ignoredIds;
5533
5535
  deferredUnlinks = [];
@@ -5731,18 +5733,20 @@ var CacheIntegrityMonitor = class {
5731
5733
  }
5732
5734
  case "reset_rescan": {
5733
5735
  const backupPath = await this.ensureBackup(pending);
5734
- this.cache.clearAll();
5735
- if (this.rescan) {
5736
- try {
5736
+ const reset = async () => {
5737
+ this.cache.clearAll();
5738
+ if (this.rescan) {
5737
5739
  const metas = await this.rescan();
5738
5740
  this.cache.upsertFromScannerMeta(metas);
5739
- } catch (err) {
5740
- this.log.error("cache-integrity reset rescan failed", {
5741
- event: "cache_integrity.reset_rescan_failed",
5742
- error: err instanceof Error ? err.message : String(err)
5743
- });
5744
5741
  }
5745
- }
5742
+ };
5743
+ const resetPromise = this.runDuringReset ? this.runDuringReset(reset) : reset();
5744
+ void resetPromise.catch((err) => {
5745
+ this.log.error("cache-integrity reset rescan failed", {
5746
+ event: "cache_integrity.reset_rescan_failed",
5747
+ error: err instanceof Error ? err.message : String(err)
5748
+ });
5749
+ });
5746
5750
  this.clearPending();
5747
5751
  this.broadcastResolved(fingerprint, action);
5748
5752
  return { ok: true, action, backupPath };
@@ -6733,7 +6737,8 @@ var StreamerServer = class {
6733
6737
  // listener-level 'error' handler demotes EADDRINUSE to debug during this
6734
6738
  // window so the self-healing kickstart-relaunch race doesn't spam warn.
6735
6739
  binding = false;
6736
- cacheReady = false;
6740
+ activeWarmups = /* @__PURE__ */ new Map([[0, "startup"]]);
6741
+ nextWarmupId = 1;
6737
6742
  // Every fire-and-forget task that runs a scan and then writes to this.cache
6738
6743
  // in an async continuation (startup warm-up, background count refresh, …).
6739
6744
  // close() awaits all of them before closing this.cache, so a scan's post-scan
@@ -7083,7 +7088,7 @@ var StreamerServer = class {
7083
7088
  this.wsHub.addClient(ws);
7084
7089
  const sessions = this.sessionStore.list(this.ptyAttachedIds());
7085
7090
  ws.send(JSON.stringify({ type: "session_list", sessions }));
7086
- if (this.cacheReady) {
7091
+ if (!this.currentWarmupState()) {
7087
7092
  ws.send(JSON.stringify({ type: "cache_ready" }));
7088
7093
  }
7089
7094
  const alertMsg = this.cacheMonitor?.wsMessage();
@@ -7292,6 +7297,39 @@ var StreamerServer = class {
7292
7297
  const addr = this.httpServer.address();
7293
7298
  return typeof addr === "object" && addr ? addr.port : 0;
7294
7299
  }
7300
+ currentWarmupState() {
7301
+ let current = null;
7302
+ for (const state of this.activeWarmups.values()) current = state;
7303
+ return current;
7304
+ }
7305
+ beginWarmup(state) {
7306
+ const id = this.nextWarmupId++;
7307
+ this.activeWarmups.set(id, state);
7308
+ return id;
7309
+ }
7310
+ finishWarmup(id) {
7311
+ if (!this.activeWarmups.delete(id) || this.activeWarmups.size > 0) return;
7312
+ this.wsHub.broadcast({ type: "cache_ready" });
7313
+ }
7314
+ async withWarmup(state, operation) {
7315
+ const id = this.beginWarmup(state);
7316
+ try {
7317
+ return await operation();
7318
+ } finally {
7319
+ this.finishWarmup(id);
7320
+ }
7321
+ }
7322
+ rejectIfWarmingUp(res) {
7323
+ const warmupState = this.currentWarmupState();
7324
+ if (!warmupState) return false;
7325
+ const body = {
7326
+ error: "Server is warming up",
7327
+ code: "SERVER_WARMING_UP",
7328
+ warmupState
7329
+ };
7330
+ json(res, 503, body);
7331
+ return true;
7332
+ }
7295
7333
  async listen(port, opts) {
7296
7334
  const dbConfig = this.disableDb ? null : getDbConfig();
7297
7335
  if (dbConfig) {
@@ -7349,6 +7387,11 @@ var StreamerServer = class {
7349
7387
  async () => {
7350
7388
  const scanner = await this.rescanForRefresh();
7351
7389
  return [...scanner.getMetadataCache().values()];
7390
+ },
7391
+ (operation) => {
7392
+ const reset = this.withWarmup("cache_reset", operation);
7393
+ this.trackCacheWrite(reset);
7394
+ return reset;
7352
7395
  }
7353
7396
  );
7354
7397
  for (const dir of this.projectsDirs()) {
@@ -7444,8 +7487,7 @@ var StreamerServer = class {
7444
7487
  event: "cache.warmup_failed"
7445
7488
  });
7446
7489
  }).finally(() => {
7447
- this.cacheReady = true;
7448
- this.wsHub.broadcast({ type: "cache_ready" });
7490
+ this.finishWarmup(0);
7449
7491
  resolveWarm();
7450
7492
  });
7451
7493
  }
@@ -7677,6 +7719,7 @@ var StreamerServer = class {
7677
7719
  return this.checkRateLimit(this.sessionInputAttempts, sessionId, 500, 6e4);
7678
7720
  }
7679
7721
  async handleListConversations(url, res) {
7722
+ if (this.rejectIfWarmingUp(res)) return;
7680
7723
  const limit = intParam(url, "limit", 50);
7681
7724
  const offset = intParam(url, "offset", 0);
7682
7725
  const sort = url.searchParams.get("sort") ?? "recent";
@@ -7684,7 +7727,7 @@ var StreamerServer = class {
7684
7727
  const providerFilter = url.searchParams.get("provider") ?? void 0;
7685
7728
  const bustCache = url.searchParams.get("refresh") === "1";
7686
7729
  if (bustCache && this.cache) {
7687
- const scanner2 = await this.rescanForRefresh();
7730
+ const scanner2 = await this.withWarmup("conversation_refresh", () => this.rescanForRefresh());
7688
7731
  const metas2 = [...scanner2.getMetadataCache().values()];
7689
7732
  try {
7690
7733
  this.cache.upsertFromScannerMeta(metas2);
@@ -7765,6 +7808,7 @@ var StreamerServer = class {
7765
7808
  json(res, 200, { conversations: adapted, hasMore: offset + limit < total, offset, total });
7766
7809
  }
7767
7810
  async handleConversationsCount(url, res) {
7811
+ if (this.rejectIfWarmingUp(res)) return;
7768
7812
  const project = url.searchParams.get("project") ?? void 0;
7769
7813
  const providerFilter = url.searchParams.get("provider") ?? void 0;
7770
7814
  const bustCache = url.searchParams.get("refresh") === "1";
@@ -7792,7 +7836,7 @@ var StreamerServer = class {
7792
7836
  // path — refresh=1 returns the cached total synchronously and this catches up.
7793
7837
  refreshCountInBackground() {
7794
7838
  this.trackCacheWrite(
7795
- (async () => {
7839
+ this.withWarmup("conversation_refresh", async () => {
7796
7840
  try {
7797
7841
  const scanner = await this.getFreshScanner();
7798
7842
  if (this.cache) {
@@ -7804,13 +7848,15 @@ var StreamerServer = class {
7804
7848
  { event: "count.refresh_failed" }
7805
7849
  );
7806
7850
  }
7807
- })()
7851
+ })
7808
7852
  );
7809
7853
  }
7810
7854
  handleSessionsCount(res) {
7855
+ if (this.rejectIfWarmingUp(res)) return;
7811
7856
  json(res, 200, { total: this.sessionStore.list(this.ptyAttachedIds()).length });
7812
7857
  }
7813
7858
  handleGetRecentSessions(url, res) {
7859
+ if (this.rejectIfWarmingUp(res)) return;
7814
7860
  const limit = intParam(url, "limit", 20);
7815
7861
  if (!this.cache) {
7816
7862
  json(res, 200, { sessions: [], total: 0 });
@@ -8258,6 +8304,7 @@ var StreamerServer = class {
8258
8304
  return isScannedSnapshotStale(conv.timestamp, mtimeMs);
8259
8305
  }
8260
8306
  async handleGetConversation(id, url, res, ifNoneMatch) {
8307
+ if (this.rejectIfWarmingUp(res)) return;
8261
8308
  const conversation = await this.findConversationByUuid(id);
8262
8309
  if (!conversation && this.cache) {
8263
8310
  const isFirstLoad = !url.searchParams.has("before_index");
@@ -8280,6 +8327,7 @@ var StreamerServer = class {
8280
8327
  id,
8281
8328
  profile_id: cachedMeta?.account ?? void 0,
8282
8329
  project_name: cachedMeta?.projectName ?? void 0,
8330
+ session_name: cachedMeta?.title ?? void 0,
8283
8331
  project_path: cachedMeta?.projectPath ?? void 0,
8284
8332
  file_path: cachedMeta?.filePath ?? void 0,
8285
8333
  last_updated_at: cachedMeta?.lastActivity ?? void 0,
@@ -8453,6 +8501,7 @@ var StreamerServer = class {
8453
8501
  id,
8454
8502
  profile_id: conv.account,
8455
8503
  project_name: conv.projectName,
8504
+ session_name: conv.sessionName || void 0,
8456
8505
  project_path: conv.projectPath,
8457
8506
  file_path: conv.filePath,
8458
8507
  last_updated_at: metaLastUpdatedAt,
@@ -8585,6 +8634,7 @@ var StreamerServer = class {
8585
8634
  });
8586
8635
  }
8587
8636
  async handleListSessions(url, res) {
8637
+ if (this.rejectIfWarmingUp(res)) return;
8588
8638
  const now = Date.now();
8589
8639
  if (!this.discoveryCache || now - this.discoveryCache.fetchedAt >= DISCOVERY_TTL_MS) {
8590
8640
  try {
@@ -8617,6 +8667,7 @@ var StreamerServer = class {
8617
8667
  }
8618
8668
  }
8619
8669
  handleGetSession(sessionId, res) {
8670
+ if (this.rejectIfWarmingUp(res)) return;
8620
8671
  const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
8621
8672
  if (session) {
8622
8673
  if (!(0, import_fs17.existsSync)(session.projectPath)) {
@@ -9068,12 +9119,21 @@ var StreamerServer = class {
9068
9119
  json(res, 404, { error: "Discovered session not found" });
9069
9120
  return;
9070
9121
  }
9071
- const { projectPath, projectName, branch } = discSession;
9122
+ const { branch } = discSession;
9123
+ let { projectPath, projectName } = discSession;
9072
9124
  const convId = discSession.id;
9073
9125
  if (discSession.pid == null) {
9074
9126
  json(res, 400, { error: "Session has no known PID" });
9075
9127
  return;
9076
9128
  }
9129
+ if (!projectPath) {
9130
+ const jsonlPath = this.findJsonlPath(convId);
9131
+ const jsonlCwd = jsonlPath ? await this.readCwdFromJsonl(jsonlPath) : null;
9132
+ if (jsonlCwd) {
9133
+ projectPath = jsonlCwd;
9134
+ projectName = projectName || (0, import_path17.basename)(jsonlCwd);
9135
+ }
9136
+ }
9077
9137
  if (!projectPath) {
9078
9138
  this.log.warn("adopt: refusing, working directory unknown", {
9079
9139
  event: "adopt.no_project_path",
@@ -9086,6 +9146,22 @@ var StreamerServer = class {
9086
9146
  });
9087
9147
  return;
9088
9148
  }
9149
+ const availability = classifyResumability(projectPath);
9150
+ if (!availability.resumable) {
9151
+ this.log.warn("adopt: refusing, project directory no longer exists", {
9152
+ event: "adopt.project_path_missing",
9153
+ sessionId,
9154
+ pid: discSession.pid,
9155
+ projectPath,
9156
+ reason: availability.unavailable_reason
9157
+ });
9158
+ json(res, 400, {
9159
+ error: "Cannot take over this session: its project directory no longer exists",
9160
+ code: "ADOPT_PROJECT_PATH_MISSING",
9161
+ reason: availability.unavailable_reason
9162
+ });
9163
+ return;
9164
+ }
9089
9165
  this.ptyManager.killPid(discSession.pid);
9090
9166
  const exited = await waitForProcessExit(discSession.pid, ADOPT_KILL_TIMEOUT_MS);
9091
9167
  if (!exited) {