@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/index.d.cts CHANGED
@@ -276,6 +276,12 @@ type WSMessage = {
276
276
  };
277
277
  /** The four cache-integrity resolution actions (POST /api/cache/alert/resolve). */
278
278
  type CacheAlertResolveAction = "prune_all" | "prune_selected" | "ignore" | "reset_rescan";
279
+ type ServerWarmupState = "startup" | "cache_reset" | "conversation_refresh";
280
+ interface ServerWarmingUpResponse {
281
+ error: "Server is warming up";
282
+ code: "SERVER_WARMING_UP";
283
+ warmupState: ServerWarmupState;
284
+ }
279
285
  interface SessionResponse {
280
286
  id: string;
281
287
  conversationId: string;
@@ -957,11 +963,12 @@ declare class CacheIntegrityMonitor {
957
963
  private readonly log;
958
964
  private readonly cacheDir;
959
965
  private readonly rescan?;
966
+ private readonly runDuringReset?;
960
967
  private _pending;
961
968
  private ignoredIds;
962
969
  private deferredUnlinks;
963
970
  private unlinkTimes;
964
- constructor(cache: ConversationCache, wsHub: WSHub, log: Logger, cacheDir: string, rescan?: (() => Promise<ScannerMeta[]>) | undefined);
971
+ constructor(cache: ConversationCache, wsHub: WSHub, log: Logger, cacheDir: string, rescan?: (() => Promise<ScannerMeta[]>) | undefined, runDuringReset?: (<T>(operation: () => Promise<T>) => Promise<T>) | undefined);
965
972
  get pending(): PendingAlert | null;
966
973
  private persist;
967
974
  private classifySeverity;
@@ -1185,7 +1192,8 @@ declare class StreamerServer {
1185
1192
  private scannerStale;
1186
1193
  private refreshInFlight;
1187
1194
  private binding;
1188
- private cacheReady;
1195
+ private activeWarmups;
1196
+ private nextWarmupId;
1189
1197
  private inFlightCacheWrites;
1190
1198
  private apiKey;
1191
1199
  private apiKeySource;
@@ -1246,6 +1254,11 @@ declare class StreamerServer {
1246
1254
  private addSessionSubscriber;
1247
1255
  private startGraceTimer;
1248
1256
  get port(): number;
1257
+ private currentWarmupState;
1258
+ private beginWarmup;
1259
+ private finishWarmup;
1260
+ private withWarmup;
1261
+ private rejectIfWarmingUp;
1249
1262
  listen(port: number, opts?: {
1250
1263
  awaitReady?: boolean;
1251
1264
  }): Promise<void>;
@@ -1448,4 +1461,4 @@ declare class ConversationWatcher {
1448
1461
  private readNewLines;
1449
1462
  }
1450
1463
 
1451
- export { type AgentClient, type AgentClientOpts, type AgentConfig, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type CacheAlertResolveAction, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProcessLiveness, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type SessionActivity, type SessionCursor, type SessionListPage, type SessionListQuery, type SessionOwnership, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, StreamerServer, type UserMessage, WSHub, type WSMessage, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
1464
+ export { type AgentClient, type AgentClientOpts, type AgentConfig, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type CacheAlertResolveAction, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProcessLiveness, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type ServerWarmingUpResponse, type ServerWarmupState, type SessionActivity, type SessionCursor, type SessionListPage, type SessionListQuery, type SessionOwnership, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, StreamerServer, type UserMessage, WSHub, type WSMessage, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
package/dist/index.d.ts CHANGED
@@ -276,6 +276,12 @@ type WSMessage = {
276
276
  };
277
277
  /** The four cache-integrity resolution actions (POST /api/cache/alert/resolve). */
278
278
  type CacheAlertResolveAction = "prune_all" | "prune_selected" | "ignore" | "reset_rescan";
279
+ type ServerWarmupState = "startup" | "cache_reset" | "conversation_refresh";
280
+ interface ServerWarmingUpResponse {
281
+ error: "Server is warming up";
282
+ code: "SERVER_WARMING_UP";
283
+ warmupState: ServerWarmupState;
284
+ }
279
285
  interface SessionResponse {
280
286
  id: string;
281
287
  conversationId: string;
@@ -957,11 +963,12 @@ declare class CacheIntegrityMonitor {
957
963
  private readonly log;
958
964
  private readonly cacheDir;
959
965
  private readonly rescan?;
966
+ private readonly runDuringReset?;
960
967
  private _pending;
961
968
  private ignoredIds;
962
969
  private deferredUnlinks;
963
970
  private unlinkTimes;
964
- constructor(cache: ConversationCache, wsHub: WSHub, log: Logger, cacheDir: string, rescan?: (() => Promise<ScannerMeta[]>) | undefined);
971
+ constructor(cache: ConversationCache, wsHub: WSHub, log: Logger, cacheDir: string, rescan?: (() => Promise<ScannerMeta[]>) | undefined, runDuringReset?: (<T>(operation: () => Promise<T>) => Promise<T>) | undefined);
965
972
  get pending(): PendingAlert | null;
966
973
  private persist;
967
974
  private classifySeverity;
@@ -1185,7 +1192,8 @@ declare class StreamerServer {
1185
1192
  private scannerStale;
1186
1193
  private refreshInFlight;
1187
1194
  private binding;
1188
- private cacheReady;
1195
+ private activeWarmups;
1196
+ private nextWarmupId;
1189
1197
  private inFlightCacheWrites;
1190
1198
  private apiKey;
1191
1199
  private apiKeySource;
@@ -1246,6 +1254,11 @@ declare class StreamerServer {
1246
1254
  private addSessionSubscriber;
1247
1255
  private startGraceTimer;
1248
1256
  get port(): number;
1257
+ private currentWarmupState;
1258
+ private beginWarmup;
1259
+ private finishWarmup;
1260
+ private withWarmup;
1261
+ private rejectIfWarmingUp;
1249
1262
  listen(port: number, opts?: {
1250
1263
  awaitReady?: boolean;
1251
1264
  }): Promise<void>;
@@ -1448,4 +1461,4 @@ declare class ConversationWatcher {
1448
1461
  private readNewLines;
1449
1462
  }
1450
1463
 
1451
- export { type AgentClient, type AgentClientOpts, type AgentConfig, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type CacheAlertResolveAction, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProcessLiveness, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type SessionActivity, type SessionCursor, type SessionListPage, type SessionListQuery, type SessionOwnership, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, StreamerServer, type UserMessage, WSHub, type WSMessage, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
1464
+ export { type AgentClient, type AgentClientOpts, type AgentConfig, type AppendArgs, type AskOption, type AskQuestion, CLAUDE_CODE_PROVIDER, CODEX_CLI_PROVIDER, type CacheAlertResolveAction, type ConversationListResponse, ConversationWatcher, type ConversationWriter, type DbConfig, type DiscoveredProcess, LiveSessionManager, type ManagedSession, PTYManager, type PTYManagerOptions, type PermissionOption, type ProcessLiveness, type ProgressDedupeLRU, type ProviderName, type ServerConfig, type ServerWarmingUpResponse, type ServerWarmupState, type SessionActivity, type SessionCursor, type SessionListPage, type SessionListQuery, type SessionOwnership, type SessionResponse, type SessionRunner, type SessionSortKey, type SessionStatus, SessionStore, type SortOrder, type StartFreshSessionOptions, type StartSessionOptions, StreamerServer, type UserMessage, WSHub, type WSMessage, createAgentClient, createConversationWriter, createPool, createProgressDedupeLRU, createProgressRoutes, discoverClaudeProcesses, generateApiKey, getDbConfig, isDbEnabled, isProviderName, isProviderResumable, loadOrCreateApiKey, maskConnectionString, readAgentConfig, validateApiKey };
package/dist/index.js CHANGED
@@ -5477,12 +5477,13 @@ function fingerprintOf(ids) {
5477
5477
  return `sha256:${createHash2("sha256").update(sorted.join("\n")).digest("hex")}`;
5478
5478
  }
5479
5479
  var CacheIntegrityMonitor = class {
5480
- constructor(cache, wsHub, log3, cacheDir, rescan) {
5480
+ constructor(cache, wsHub, log3, cacheDir, rescan, runDuringReset) {
5481
5481
  this.cache = cache;
5482
5482
  this.wsHub = wsHub;
5483
5483
  this.log = log3;
5484
5484
  this.cacheDir = cacheDir;
5485
5485
  this.rescan = rescan;
5486
+ this.runDuringReset = runDuringReset;
5486
5487
  const state = loadAlertState();
5487
5488
  this._pending = state.pending ?? null;
5488
5489
  this.ignoredIds = new Set(state.ignoredIds ?? []);
@@ -5492,6 +5493,7 @@ var CacheIntegrityMonitor = class {
5492
5493
  log;
5493
5494
  cacheDir;
5494
5495
  rescan;
5496
+ runDuringReset;
5495
5497
  _pending;
5496
5498
  ignoredIds;
5497
5499
  deferredUnlinks = [];
@@ -5695,18 +5697,20 @@ var CacheIntegrityMonitor = class {
5695
5697
  }
5696
5698
  case "reset_rescan": {
5697
5699
  const backupPath = await this.ensureBackup(pending);
5698
- this.cache.clearAll();
5699
- if (this.rescan) {
5700
- try {
5700
+ const reset = async () => {
5701
+ this.cache.clearAll();
5702
+ if (this.rescan) {
5701
5703
  const metas = await this.rescan();
5702
5704
  this.cache.upsertFromScannerMeta(metas);
5703
- } catch (err) {
5704
- this.log.error("cache-integrity reset rescan failed", {
5705
- event: "cache_integrity.reset_rescan_failed",
5706
- error: err instanceof Error ? err.message : String(err)
5707
- });
5708
5705
  }
5709
- }
5706
+ };
5707
+ const resetPromise = this.runDuringReset ? this.runDuringReset(reset) : reset();
5708
+ void resetPromise.catch((err) => {
5709
+ this.log.error("cache-integrity reset rescan failed", {
5710
+ event: "cache_integrity.reset_rescan_failed",
5711
+ error: err instanceof Error ? err.message : String(err)
5712
+ });
5713
+ });
5710
5714
  this.clearPending();
5711
5715
  this.broadcastResolved(fingerprint, action);
5712
5716
  return { ok: true, action, backupPath };
@@ -6697,7 +6701,8 @@ var StreamerServer = class {
6697
6701
  // listener-level 'error' handler demotes EADDRINUSE to debug during this
6698
6702
  // window so the self-healing kickstart-relaunch race doesn't spam warn.
6699
6703
  binding = false;
6700
- cacheReady = false;
6704
+ activeWarmups = /* @__PURE__ */ new Map([[0, "startup"]]);
6705
+ nextWarmupId = 1;
6701
6706
  // Every fire-and-forget task that runs a scan and then writes to this.cache
6702
6707
  // in an async continuation (startup warm-up, background count refresh, …).
6703
6708
  // close() awaits all of them before closing this.cache, so a scan's post-scan
@@ -7047,7 +7052,7 @@ var StreamerServer = class {
7047
7052
  this.wsHub.addClient(ws);
7048
7053
  const sessions = this.sessionStore.list(this.ptyAttachedIds());
7049
7054
  ws.send(JSON.stringify({ type: "session_list", sessions }));
7050
- if (this.cacheReady) {
7055
+ if (!this.currentWarmupState()) {
7051
7056
  ws.send(JSON.stringify({ type: "cache_ready" }));
7052
7057
  }
7053
7058
  const alertMsg = this.cacheMonitor?.wsMessage();
@@ -7256,6 +7261,39 @@ var StreamerServer = class {
7256
7261
  const addr = this.httpServer.address();
7257
7262
  return typeof addr === "object" && addr ? addr.port : 0;
7258
7263
  }
7264
+ currentWarmupState() {
7265
+ let current = null;
7266
+ for (const state of this.activeWarmups.values()) current = state;
7267
+ return current;
7268
+ }
7269
+ beginWarmup(state) {
7270
+ const id = this.nextWarmupId++;
7271
+ this.activeWarmups.set(id, state);
7272
+ return id;
7273
+ }
7274
+ finishWarmup(id) {
7275
+ if (!this.activeWarmups.delete(id) || this.activeWarmups.size > 0) return;
7276
+ this.wsHub.broadcast({ type: "cache_ready" });
7277
+ }
7278
+ async withWarmup(state, operation) {
7279
+ const id = this.beginWarmup(state);
7280
+ try {
7281
+ return await operation();
7282
+ } finally {
7283
+ this.finishWarmup(id);
7284
+ }
7285
+ }
7286
+ rejectIfWarmingUp(res) {
7287
+ const warmupState = this.currentWarmupState();
7288
+ if (!warmupState) return false;
7289
+ const body = {
7290
+ error: "Server is warming up",
7291
+ code: "SERVER_WARMING_UP",
7292
+ warmupState
7293
+ };
7294
+ json(res, 503, body);
7295
+ return true;
7296
+ }
7259
7297
  async listen(port, opts) {
7260
7298
  const dbConfig = this.disableDb ? null : getDbConfig();
7261
7299
  if (dbConfig) {
@@ -7313,6 +7351,11 @@ var StreamerServer = class {
7313
7351
  async () => {
7314
7352
  const scanner = await this.rescanForRefresh();
7315
7353
  return [...scanner.getMetadataCache().values()];
7354
+ },
7355
+ (operation) => {
7356
+ const reset = this.withWarmup("cache_reset", operation);
7357
+ this.trackCacheWrite(reset);
7358
+ return reset;
7316
7359
  }
7317
7360
  );
7318
7361
  for (const dir of this.projectsDirs()) {
@@ -7408,8 +7451,7 @@ var StreamerServer = class {
7408
7451
  event: "cache.warmup_failed"
7409
7452
  });
7410
7453
  }).finally(() => {
7411
- this.cacheReady = true;
7412
- this.wsHub.broadcast({ type: "cache_ready" });
7454
+ this.finishWarmup(0);
7413
7455
  resolveWarm();
7414
7456
  });
7415
7457
  }
@@ -7641,6 +7683,7 @@ var StreamerServer = class {
7641
7683
  return this.checkRateLimit(this.sessionInputAttempts, sessionId, 500, 6e4);
7642
7684
  }
7643
7685
  async handleListConversations(url, res) {
7686
+ if (this.rejectIfWarmingUp(res)) return;
7644
7687
  const limit = intParam(url, "limit", 50);
7645
7688
  const offset = intParam(url, "offset", 0);
7646
7689
  const sort = url.searchParams.get("sort") ?? "recent";
@@ -7648,7 +7691,7 @@ var StreamerServer = class {
7648
7691
  const providerFilter = url.searchParams.get("provider") ?? void 0;
7649
7692
  const bustCache = url.searchParams.get("refresh") === "1";
7650
7693
  if (bustCache && this.cache) {
7651
- const scanner2 = await this.rescanForRefresh();
7694
+ const scanner2 = await this.withWarmup("conversation_refresh", () => this.rescanForRefresh());
7652
7695
  const metas2 = [...scanner2.getMetadataCache().values()];
7653
7696
  try {
7654
7697
  this.cache.upsertFromScannerMeta(metas2);
@@ -7729,6 +7772,7 @@ var StreamerServer = class {
7729
7772
  json(res, 200, { conversations: adapted, hasMore: offset + limit < total, offset, total });
7730
7773
  }
7731
7774
  async handleConversationsCount(url, res) {
7775
+ if (this.rejectIfWarmingUp(res)) return;
7732
7776
  const project = url.searchParams.get("project") ?? void 0;
7733
7777
  const providerFilter = url.searchParams.get("provider") ?? void 0;
7734
7778
  const bustCache = url.searchParams.get("refresh") === "1";
@@ -7756,7 +7800,7 @@ var StreamerServer = class {
7756
7800
  // path — refresh=1 returns the cached total synchronously and this catches up.
7757
7801
  refreshCountInBackground() {
7758
7802
  this.trackCacheWrite(
7759
- (async () => {
7803
+ this.withWarmup("conversation_refresh", async () => {
7760
7804
  try {
7761
7805
  const scanner = await this.getFreshScanner();
7762
7806
  if (this.cache) {
@@ -7768,13 +7812,15 @@ var StreamerServer = class {
7768
7812
  { event: "count.refresh_failed" }
7769
7813
  );
7770
7814
  }
7771
- })()
7815
+ })
7772
7816
  );
7773
7817
  }
7774
7818
  handleSessionsCount(res) {
7819
+ if (this.rejectIfWarmingUp(res)) return;
7775
7820
  json(res, 200, { total: this.sessionStore.list(this.ptyAttachedIds()).length });
7776
7821
  }
7777
7822
  handleGetRecentSessions(url, res) {
7823
+ if (this.rejectIfWarmingUp(res)) return;
7778
7824
  const limit = intParam(url, "limit", 20);
7779
7825
  if (!this.cache) {
7780
7826
  json(res, 200, { sessions: [], total: 0 });
@@ -8222,6 +8268,7 @@ var StreamerServer = class {
8222
8268
  return isScannedSnapshotStale(conv.timestamp, mtimeMs);
8223
8269
  }
8224
8270
  async handleGetConversation(id, url, res, ifNoneMatch) {
8271
+ if (this.rejectIfWarmingUp(res)) return;
8225
8272
  const conversation = await this.findConversationByUuid(id);
8226
8273
  if (!conversation && this.cache) {
8227
8274
  const isFirstLoad = !url.searchParams.has("before_index");
@@ -8549,6 +8596,7 @@ var StreamerServer = class {
8549
8596
  });
8550
8597
  }
8551
8598
  async handleListSessions(url, res) {
8599
+ if (this.rejectIfWarmingUp(res)) return;
8552
8600
  const now = Date.now();
8553
8601
  if (!this.discoveryCache || now - this.discoveryCache.fetchedAt >= DISCOVERY_TTL_MS) {
8554
8602
  try {
@@ -8581,6 +8629,7 @@ var StreamerServer = class {
8581
8629
  }
8582
8630
  }
8583
8631
  handleGetSession(sessionId, res) {
8632
+ if (this.rejectIfWarmingUp(res)) return;
8584
8633
  const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
8585
8634
  if (session) {
8586
8635
  if (!existsSync10(session.projectPath)) {
@@ -9032,12 +9081,21 @@ var StreamerServer = class {
9032
9081
  json(res, 404, { error: "Discovered session not found" });
9033
9082
  return;
9034
9083
  }
9035
- const { projectPath, projectName, branch } = discSession;
9084
+ const { branch } = discSession;
9085
+ let { projectPath, projectName } = discSession;
9036
9086
  const convId = discSession.id;
9037
9087
  if (discSession.pid == null) {
9038
9088
  json(res, 400, { error: "Session has no known PID" });
9039
9089
  return;
9040
9090
  }
9091
+ if (!projectPath) {
9092
+ const jsonlPath = this.findJsonlPath(convId);
9093
+ const jsonlCwd = jsonlPath ? await this.readCwdFromJsonl(jsonlPath) : null;
9094
+ if (jsonlCwd) {
9095
+ projectPath = jsonlCwd;
9096
+ projectName = projectName || basename5(jsonlCwd);
9097
+ }
9098
+ }
9041
9099
  if (!projectPath) {
9042
9100
  this.log.warn("adopt: refusing, working directory unknown", {
9043
9101
  event: "adopt.no_project_path",
@@ -9050,6 +9108,22 @@ var StreamerServer = class {
9050
9108
  });
9051
9109
  return;
9052
9110
  }
9111
+ const availability = classifyResumability(projectPath);
9112
+ if (!availability.resumable) {
9113
+ this.log.warn("adopt: refusing, project directory no longer exists", {
9114
+ event: "adopt.project_path_missing",
9115
+ sessionId,
9116
+ pid: discSession.pid,
9117
+ projectPath,
9118
+ reason: availability.unavailable_reason
9119
+ });
9120
+ json(res, 400, {
9121
+ error: "Cannot take over this session: its project directory no longer exists",
9122
+ code: "ADOPT_PROJECT_PATH_MISSING",
9123
+ reason: availability.unavailable_reason
9124
+ });
9125
+ return;
9126
+ }
9053
9127
  this.ptyManager.killPid(discSession.pid);
9054
9128
  const exited = await waitForProcessExit(discSession.pid, ADOPT_KILL_TIMEOUT_MS);
9055
9129
  if (!exited) {