@threadbase-sh/streamer 1.34.0 → 1.35.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/cli.cjs CHANGED
@@ -142454,12 +142454,13 @@ function fingerprintOf(ids) {
142454
142454
  return `sha256:${(0, import_crypto10.createHash)("sha256").update(sorted.join("\n")).digest("hex")}`;
142455
142455
  }
142456
142456
  var CacheIntegrityMonitor = class {
142457
- constructor(cache, wsHub, log8, cacheDir, rescan) {
142457
+ constructor(cache, wsHub, log8, cacheDir, rescan, runDuringReset) {
142458
142458
  this.cache = cache;
142459
142459
  this.wsHub = wsHub;
142460
142460
  this.log = log8;
142461
142461
  this.cacheDir = cacheDir;
142462
142462
  this.rescan = rescan;
142463
+ this.runDuringReset = runDuringReset;
142463
142464
  const state = loadAlertState();
142464
142465
  this._pending = state.pending ?? null;
142465
142466
  this.ignoredIds = new Set(state.ignoredIds ?? []);
@@ -142469,6 +142470,7 @@ var CacheIntegrityMonitor = class {
142469
142470
  log;
142470
142471
  cacheDir;
142471
142472
  rescan;
142473
+ runDuringReset;
142472
142474
  _pending;
142473
142475
  ignoredIds;
142474
142476
  deferredUnlinks = [];
@@ -142672,18 +142674,20 @@ var CacheIntegrityMonitor = class {
142672
142674
  }
142673
142675
  case "reset_rescan": {
142674
142676
  const backupPath = await this.ensureBackup(pending);
142675
- this.cache.clearAll();
142676
- if (this.rescan) {
142677
- try {
142677
+ const reset = async () => {
142678
+ this.cache.clearAll();
142679
+ if (this.rescan) {
142678
142680
  const metas = await this.rescan();
142679
142681
  this.cache.upsertFromScannerMeta(metas);
142680
- } catch (err) {
142681
- this.log.error("cache-integrity reset rescan failed", {
142682
- event: "cache_integrity.reset_rescan_failed",
142683
- error: err instanceof Error ? err.message : String(err)
142684
- });
142685
142682
  }
142686
- }
142683
+ };
142684
+ const resetPromise = this.runDuringReset ? this.runDuringReset(reset) : reset();
142685
+ void resetPromise.catch((err) => {
142686
+ this.log.error("cache-integrity reset rescan failed", {
142687
+ event: "cache_integrity.reset_rescan_failed",
142688
+ error: err instanceof Error ? err.message : String(err)
142689
+ });
142690
+ });
142687
142691
  this.clearPending();
142688
142692
  this.broadcastResolved(fingerprint2, action);
142689
142693
  return { ok: true, action, backupPath };
@@ -143911,7 +143915,8 @@ var StreamerServer = class {
143911
143915
  // listener-level 'error' handler demotes EADDRINUSE to debug during this
143912
143916
  // window so the self-healing kickstart-relaunch race doesn't spam warn.
143913
143917
  binding = false;
143914
- cacheReady = false;
143918
+ activeWarmups = /* @__PURE__ */ new Map([[0, "startup"]]);
143919
+ nextWarmupId = 1;
143915
143920
  // Every fire-and-forget task that runs a scan and then writes to this.cache
143916
143921
  // in an async continuation (startup warm-up, background count refresh, …).
143917
143922
  // close() awaits all of them before closing this.cache, so a scan's post-scan
@@ -144261,7 +144266,7 @@ var StreamerServer = class {
144261
144266
  this.wsHub.addClient(ws2);
144262
144267
  const sessions = this.sessionStore.list(this.ptyAttachedIds());
144263
144268
  ws2.send(JSON.stringify({ type: "session_list", sessions }));
144264
- if (this.cacheReady) {
144269
+ if (!this.currentWarmupState()) {
144265
144270
  ws2.send(JSON.stringify({ type: "cache_ready" }));
144266
144271
  }
144267
144272
  const alertMsg = this.cacheMonitor?.wsMessage();
@@ -144470,6 +144475,39 @@ var StreamerServer = class {
144470
144475
  const addr = this.httpServer.address();
144471
144476
  return typeof addr === "object" && addr ? addr.port : 0;
144472
144477
  }
144478
+ currentWarmupState() {
144479
+ let current = null;
144480
+ for (const state of this.activeWarmups.values()) current = state;
144481
+ return current;
144482
+ }
144483
+ beginWarmup(state) {
144484
+ const id = this.nextWarmupId++;
144485
+ this.activeWarmups.set(id, state);
144486
+ return id;
144487
+ }
144488
+ finishWarmup(id) {
144489
+ if (!this.activeWarmups.delete(id) || this.activeWarmups.size > 0) return;
144490
+ this.wsHub.broadcast({ type: "cache_ready" });
144491
+ }
144492
+ async withWarmup(state, operation) {
144493
+ const id = this.beginWarmup(state);
144494
+ try {
144495
+ return await operation();
144496
+ } finally {
144497
+ this.finishWarmup(id);
144498
+ }
144499
+ }
144500
+ rejectIfWarmingUp(res) {
144501
+ const warmupState = this.currentWarmupState();
144502
+ if (!warmupState) return false;
144503
+ const body = {
144504
+ error: "Server is warming up",
144505
+ code: "SERVER_WARMING_UP",
144506
+ warmupState
144507
+ };
144508
+ json2(res, 503, body);
144509
+ return true;
144510
+ }
144473
144511
  async listen(port, opts) {
144474
144512
  const dbConfig = this.disableDb ? null : getDbConfig();
144475
144513
  if (dbConfig) {
@@ -144527,6 +144565,11 @@ var StreamerServer = class {
144527
144565
  async () => {
144528
144566
  const scanner = await this.rescanForRefresh();
144529
144567
  return [...scanner.getMetadataCache().values()];
144568
+ },
144569
+ (operation) => {
144570
+ const reset = this.withWarmup("cache_reset", operation);
144571
+ this.trackCacheWrite(reset);
144572
+ return reset;
144530
144573
  }
144531
144574
  );
144532
144575
  for (const dir of this.projectsDirs()) {
@@ -144622,8 +144665,7 @@ var StreamerServer = class {
144622
144665
  event: "cache.warmup_failed"
144623
144666
  });
144624
144667
  }).finally(() => {
144625
- this.cacheReady = true;
144626
- this.wsHub.broadcast({ type: "cache_ready" });
144668
+ this.finishWarmup(0);
144627
144669
  resolveWarm();
144628
144670
  });
144629
144671
  }
@@ -144855,6 +144897,7 @@ var StreamerServer = class {
144855
144897
  return this.checkRateLimit(this.sessionInputAttempts, sessionId, 500, 6e4);
144856
144898
  }
144857
144899
  async handleListConversations(url2, res) {
144900
+ if (this.rejectIfWarmingUp(res)) return;
144858
144901
  const limit = intParam(url2, "limit", 50);
144859
144902
  const offset = intParam(url2, "offset", 0);
144860
144903
  const sort = url2.searchParams.get("sort") ?? "recent";
@@ -144862,7 +144905,7 @@ var StreamerServer = class {
144862
144905
  const providerFilter = url2.searchParams.get("provider") ?? void 0;
144863
144906
  const bustCache = url2.searchParams.get("refresh") === "1";
144864
144907
  if (bustCache && this.cache) {
144865
- const scanner2 = await this.rescanForRefresh();
144908
+ const scanner2 = await this.withWarmup("conversation_refresh", () => this.rescanForRefresh());
144866
144909
  const metas2 = [...scanner2.getMetadataCache().values()];
144867
144910
  try {
144868
144911
  this.cache.upsertFromScannerMeta(metas2);
@@ -144943,6 +144986,7 @@ var StreamerServer = class {
144943
144986
  json2(res, 200, { conversations: adapted, hasMore: offset + limit < total, offset, total });
144944
144987
  }
144945
144988
  async handleConversationsCount(url2, res) {
144989
+ if (this.rejectIfWarmingUp(res)) return;
144946
144990
  const project = url2.searchParams.get("project") ?? void 0;
144947
144991
  const providerFilter = url2.searchParams.get("provider") ?? void 0;
144948
144992
  const bustCache = url2.searchParams.get("refresh") === "1";
@@ -144970,7 +145014,7 @@ var StreamerServer = class {
144970
145014
  // path — refresh=1 returns the cached total synchronously and this catches up.
144971
145015
  refreshCountInBackground() {
144972
145016
  this.trackCacheWrite(
144973
- (async () => {
145017
+ this.withWarmup("conversation_refresh", async () => {
144974
145018
  try {
144975
145019
  const scanner = await this.getFreshScanner();
144976
145020
  if (this.cache) {
@@ -144982,13 +145026,15 @@ var StreamerServer = class {
144982
145026
  { event: "count.refresh_failed" }
144983
145027
  );
144984
145028
  }
144985
- })()
145029
+ })
144986
145030
  );
144987
145031
  }
144988
145032
  handleSessionsCount(res) {
145033
+ if (this.rejectIfWarmingUp(res)) return;
144989
145034
  json2(res, 200, { total: this.sessionStore.list(this.ptyAttachedIds()).length });
144990
145035
  }
144991
145036
  handleGetRecentSessions(url2, res) {
145037
+ if (this.rejectIfWarmingUp(res)) return;
144992
145038
  const limit = intParam(url2, "limit", 20);
144993
145039
  if (!this.cache) {
144994
145040
  json2(res, 200, { sessions: [], total: 0 });
@@ -145436,6 +145482,7 @@ var StreamerServer = class {
145436
145482
  return isScannedSnapshotStale(conv.timestamp, mtimeMs);
145437
145483
  }
145438
145484
  async handleGetConversation(id, url2, res, ifNoneMatch) {
145485
+ if (this.rejectIfWarmingUp(res)) return;
145439
145486
  const conversation = await this.findConversationByUuid(id);
145440
145487
  if (!conversation && this.cache) {
145441
145488
  const isFirstLoad = !url2.searchParams.has("before_index");
@@ -145763,6 +145810,7 @@ var StreamerServer = class {
145763
145810
  });
145764
145811
  }
145765
145812
  async handleListSessions(url2, res) {
145813
+ if (this.rejectIfWarmingUp(res)) return;
145766
145814
  const now = Date.now();
145767
145815
  if (!this.discoveryCache || now - this.discoveryCache.fetchedAt >= DISCOVERY_TTL_MS) {
145768
145816
  try {
@@ -145795,6 +145843,7 @@ var StreamerServer = class {
145795
145843
  }
145796
145844
  }
145797
145845
  handleGetSession(sessionId, res) {
145846
+ if (this.rejectIfWarmingUp(res)) return;
145798
145847
  const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
145799
145848
  if (session) {
145800
145849
  if (!(0, import_fs28.existsSync)(session.projectPath)) {
@@ -146246,12 +146295,21 @@ var StreamerServer = class {
146246
146295
  json2(res, 404, { error: "Discovered session not found" });
146247
146296
  return;
146248
146297
  }
146249
- const { projectPath, projectName, branch } = discSession;
146298
+ const { branch } = discSession;
146299
+ let { projectPath, projectName } = discSession;
146250
146300
  const convId = discSession.id;
146251
146301
  if (discSession.pid == null) {
146252
146302
  json2(res, 400, { error: "Session has no known PID" });
146253
146303
  return;
146254
146304
  }
146305
+ if (!projectPath) {
146306
+ const jsonlPath = this.findJsonlPath(convId);
146307
+ const jsonlCwd = jsonlPath ? await this.readCwdFromJsonl(jsonlPath) : null;
146308
+ if (jsonlCwd) {
146309
+ projectPath = jsonlCwd;
146310
+ projectName = projectName || (0, import_path28.basename)(jsonlCwd);
146311
+ }
146312
+ }
146255
146313
  if (!projectPath) {
146256
146314
  this.log.warn("adopt: refusing, working directory unknown", {
146257
146315
  event: "adopt.no_project_path",
@@ -146264,6 +146322,22 @@ var StreamerServer = class {
146264
146322
  });
146265
146323
  return;
146266
146324
  }
146325
+ const availability = classifyResumability(projectPath);
146326
+ if (!availability.resumable) {
146327
+ this.log.warn("adopt: refusing, project directory no longer exists", {
146328
+ event: "adopt.project_path_missing",
146329
+ sessionId,
146330
+ pid: discSession.pid,
146331
+ projectPath,
146332
+ reason: availability.unavailable_reason
146333
+ });
146334
+ json2(res, 400, {
146335
+ error: "Cannot take over this session: its project directory no longer exists",
146336
+ code: "ADOPT_PROJECT_PATH_MISSING",
146337
+ reason: availability.unavailable_reason
146338
+ });
146339
+ return;
146340
+ }
146267
146341
  this.ptyManager.killPid(discSession.pid);
146268
146342
  const exited = await waitForProcessExit(discSession.pid, ADOPT_KILL_TIMEOUT_MS);
146269
146343
  if (!exited) {