@threadbase-sh/streamer 1.42.0 → 1.43.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
@@ -8818,6 +8818,34 @@ function paginate(results, offset, limit) {
8818
8818
  };
8819
8819
  }
8820
8820
 
8821
+ // src/services/sessions/autoResumeOnBoot.ts
8822
+ var AUTO_RESUME_WINDOW_MS = 15 * 60 * 1e3;
8823
+ var AUTO_RESUME_MAX = 5;
8824
+ var AUTO_RESUME_CONCURRENCY = 2;
8825
+ var AUTO_RESUME_STAGGER_MS = 500;
8826
+ function autoResumeSkipReason(row, opts) {
8827
+ if (row.status_source !== "shutdown") return "not_shutdown";
8828
+ if (row.status !== "running" && row.status !== "waiting_input") return "not_interrupted";
8829
+ if (opts.now - row.status_updated_at > AUTO_RESUME_WINDOW_MS) return "too_old";
8830
+ if (!opts.projectExists(row.project_path)) return "project_missing";
8831
+ if (resumeIdForRow(row) == null) return "resume_identity_missing";
8832
+ return null;
8833
+ }
8834
+ function planAutoResume(rows, opts) {
8835
+ const eligible = [];
8836
+ const skipped = [];
8837
+ for (const row of rows) {
8838
+ const reason = autoResumeSkipReason(row, opts);
8839
+ if (reason) skipped.push({ row, reason });
8840
+ else eligible.push(row);
8841
+ }
8842
+ return {
8843
+ attempts: eligible.slice(0, AUTO_RESUME_MAX),
8844
+ skipped,
8845
+ overflow: eligible.slice(AUTO_RESUME_MAX)
8846
+ };
8847
+ }
8848
+
8821
8849
  // src/services/sessions/conversationBusy.ts
8822
8850
  var import_fs18 = require("fs");
8823
8851
  var RESUME_BUSY_WINDOW_MS = 12e4;
@@ -9659,6 +9687,7 @@ var StreamerServer = class {
9659
9687
  disableDb = false;
9660
9688
  // Skip the startup warm-up scan (test hook; see ServerConfig.skipStartupWarmup).
9661
9689
  skipStartupWarmup;
9690
+ autoResumeOnBoot;
9662
9691
  browseRoot = null;
9663
9692
  publicUrl = null;
9664
9693
  browserCors;
@@ -9775,6 +9804,7 @@ var StreamerServer = class {
9775
9804
  this.verbose = config.verbose ?? false;
9776
9805
  this.disableDb = config.disableDb ?? false;
9777
9806
  this.skipStartupWarmup = config.skipStartupWarmup ?? false;
9807
+ this.autoResumeOnBoot = config.autoResumeOnBoot ?? false;
9778
9808
  this.scannerPersistenceDisabled = config.scannerPersistent === false;
9779
9809
  this.scanProfiles = config.scanProfiles;
9780
9810
  this.codexRoots = config.codexRoots ?? [(0, import_path18.join)((0, import_os10.homedir)(), ".codex", "sessions")];
@@ -10255,16 +10285,19 @@ var StreamerServer = class {
10255
10285
  broadcastOrUnicastSessionList(req) {
10256
10286
  const clientId = req.headers["x-client-id"];
10257
10287
  const ws = typeof clientId === "string" ? this.clientIdToWs.get(clientId) : void 0;
10258
- const payload = {
10259
- type: "session_list",
10260
- sessions: this.withReconciledLifecycle(this.sessionStore.list(this.ptyAttachedIds()))
10261
- };
10288
+ const payload = this.sessionListPayload();
10262
10289
  if (ws) {
10263
10290
  this.wsHub.unicast(ws, payload);
10264
10291
  } else {
10265
10292
  this.wsHub.broadcast(payload);
10266
10293
  }
10267
10294
  }
10295
+ sessionListPayload() {
10296
+ return {
10297
+ type: "session_list",
10298
+ sessions: this.withReconciledLifecycle(this.sessionStore.list(this.ptyAttachedIds()))
10299
+ };
10300
+ }
10268
10301
  /**
10269
10302
  * Overlay boot-reconciliation verdicts onto session responses.
10270
10303
  *
@@ -10438,7 +10471,7 @@ var StreamerServer = class {
10438
10471
  * by id rather than duplicating it.
10439
10472
  */
10440
10473
  rehydratePreviousSessions(verdicts) {
10441
- if (!this.featureFlags.sessionRehydration || !this.managedSessionsRepo) return;
10474
+ if (!this.managedSessionsRepo) return [];
10442
10475
  try {
10443
10476
  const now = Date.now();
10444
10477
  const rows = this.managedSessionsRepo.listRecoverable({
@@ -10447,7 +10480,7 @@ var StreamerServer = class {
10447
10480
  });
10448
10481
  const truncated = rows.length > REHYDRATE_MAX;
10449
10482
  const candidates = truncated ? rows.slice(0, REHYDRATE_MAX) : rows;
10450
- if (candidates.length === 0) return;
10483
+ if (!this.featureFlags.sessionRehydration || candidates.length === 0) return candidates;
10451
10484
  const verdictById = new Map(verdicts.map((v) => [v.sessionId, v]));
10452
10485
  let rehydrated = 0;
10453
10486
  const skippedBy = {};
@@ -10482,12 +10515,107 @@ var StreamerServer = class {
10482
10515
  skippedBy,
10483
10516
  truncated
10484
10517
  });
10518
+ return candidates;
10485
10519
  } catch (err) {
10486
10520
  this.log.warn("[rehydrate] failed to rehydrate previous sessions", {
10487
10521
  event: "sessions.rehydrate_failed",
10488
10522
  err
10489
10523
  });
10524
+ return [];
10525
+ }
10526
+ }
10527
+ /** Resume only the recent sessions the user explicitly allowed us to start at boot. */
10528
+ async autoResumePreviousSessions(rows) {
10529
+ if (!this.autoResumeOnBoot) return;
10530
+ const plan = planAutoResume(rows, { now: Date.now(), projectExists: import_fs19.existsSync });
10531
+ const skippedBy = {};
10532
+ for (const { row, reason } of plan.skipped) {
10533
+ skippedBy[reason] = (skippedBy[reason] ?? 0) + 1;
10534
+ this.log.debug(`[auto-resume] skipped ${row.session_id}: ${reason}`, {
10535
+ event: "sessions.auto_resume_skipped",
10536
+ sessionId: row.session_id,
10537
+ reason
10538
+ });
10490
10539
  }
10540
+ if (plan.skipped.length > 0) {
10541
+ this.log.info(
10542
+ `[auto-resume] left ${plan.skipped.length} ineligible session(s) for manual resume`,
10543
+ {
10544
+ event: "sessions.auto_resume_skipped",
10545
+ skipped: plan.skipped.length,
10546
+ skippedBy
10547
+ }
10548
+ );
10549
+ }
10550
+ for (const row of plan.overflow) {
10551
+ this.log.info(`[auto-resume] left ${row.session_id} for manual resume: ceiling reached`, {
10552
+ event: "sessions.auto_resume_skipped",
10553
+ sessionId: row.session_id,
10554
+ reason: "ceiling_reached"
10555
+ });
10556
+ }
10557
+ let resumed = 0;
10558
+ let failed = 0;
10559
+ const inFlight = /* @__PURE__ */ new Set();
10560
+ let started = 0;
10561
+ const resume = async (row) => {
10562
+ try {
10563
+ const outcome = await this.resumeSession({
10564
+ sessionId: row.session_id,
10565
+ projectName: row.project_name,
10566
+ branch: row.branch
10567
+ });
10568
+ if (!outcome.ok) {
10569
+ failed++;
10570
+ this.log.info(`[auto-resume] skipped ${row.session_id}: ${outcome.reason}`, {
10571
+ event: "sessions.auto_resume_skipped",
10572
+ sessionId: row.session_id,
10573
+ reason: outcome.reason,
10574
+ ...outcome.reason === "conversation_busy" && {
10575
+ detectedBy: outcome.detectedBy,
10576
+ lastActivityMs: outcome.lastActivityMs,
10577
+ likelyOwner: outcome.likelyOwner
10578
+ }
10579
+ });
10580
+ return;
10581
+ }
10582
+ resumed++;
10583
+ this.log.info(`[auto-resume] resumed ${row.session_id}`, {
10584
+ event: "sessions.auto_resume_succeeded",
10585
+ sessionId: row.session_id,
10586
+ alreadyRunning: outcome.alreadyRunning
10587
+ });
10588
+ } catch (err) {
10589
+ failed++;
10590
+ this.log.warn(`[auto-resume] failed to resume ${row.session_id}`, {
10591
+ event: "sessions.auto_resume_failed",
10592
+ sessionId: row.session_id,
10593
+ err
10594
+ });
10595
+ }
10596
+ };
10597
+ for (const row of plan.attempts) {
10598
+ while (inFlight.size >= AUTO_RESUME_CONCURRENCY) {
10599
+ await Promise.race(inFlight);
10600
+ }
10601
+ if (started > 0) {
10602
+ await new Promise((resolve2) => setTimeout(resolve2, AUTO_RESUME_STAGGER_MS));
10603
+ }
10604
+ const task = resume(row);
10605
+ inFlight.add(task);
10606
+ void task.then(() => inFlight.delete(task));
10607
+ started++;
10608
+ }
10609
+ await Promise.all(inFlight);
10610
+ if (resumed > 0) this.wsHub.broadcast(this.sessionListPayload());
10611
+ this.log.info(`[auto-resume] completed boot recovery: ${resumed} resumed`, {
10612
+ event: "sessions.auto_resume_completed",
10613
+ attempted: plan.attempts.length,
10614
+ resumed,
10615
+ failed,
10616
+ ineligible: plan.skipped.length,
10617
+ overflow: plan.overflow.length
10618
+ });
10491
10619
  }
10492
10620
  /**
10493
10621
  * Pick a token guaranteed to appear in the spawned process's argv, for the
@@ -10809,8 +10937,9 @@ var StreamerServer = class {
10809
10937
  );
10810
10938
  this.scannerPersistenceDisabled = true;
10811
10939
  }
10812
- void this.reconcilePreviousSessions().then((v) => {
10813
- this.rehydratePreviousSessions(v);
10940
+ void this.reconcilePreviousSessions().then(async (v) => {
10941
+ const recoverableRows = this.rehydratePreviousSessions(v);
10942
+ await this.autoResumePreviousSessions(recoverableRows);
10814
10943
  this.pruneTerminalSessions();
10815
10944
  });
10816
10945
  if (this.skipStartupWarmup) {