@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.d.cts CHANGED
@@ -1815,6 +1815,7 @@ declare class StreamerServer {
1815
1815
  private dbInstanceId;
1816
1816
  private disableDb;
1817
1817
  private skipStartupWarmup;
1818
+ private autoResumeOnBoot;
1818
1819
  private browseRoot;
1819
1820
  private publicUrl;
1820
1821
  private browserCors;
@@ -1881,6 +1882,7 @@ declare class StreamerServer {
1881
1882
  * a full broadcast if no match exists (old clients, or no WS registered yet).
1882
1883
  */
1883
1884
  private broadcastOrUnicastSessionList;
1885
+ private sessionListPayload;
1884
1886
  /**
1885
1887
  * Overlay boot-reconciliation verdicts onto session responses.
1886
1888
  *
@@ -1944,6 +1946,8 @@ declare class StreamerServer {
1944
1946
  * by id rather than duplicating it.
1945
1947
  */
1946
1948
  private rehydratePreviousSessions;
1949
+ /** Resume only the recent sessions the user explicitly allowed us to start at boot. */
1950
+ private autoResumePreviousSessions;
1947
1951
  /**
1948
1952
  * Pick a token guaranteed to appear in the spawned process's argv, for the
1949
1953
  * reconciler's pid-reuse guard.
package/dist/index.d.ts CHANGED
@@ -1815,6 +1815,7 @@ declare class StreamerServer {
1815
1815
  private dbInstanceId;
1816
1816
  private disableDb;
1817
1817
  private skipStartupWarmup;
1818
+ private autoResumeOnBoot;
1818
1819
  private browseRoot;
1819
1820
  private publicUrl;
1820
1821
  private browserCors;
@@ -1881,6 +1882,7 @@ declare class StreamerServer {
1881
1882
  * a full broadcast if no match exists (old clients, or no WS registered yet).
1882
1883
  */
1883
1884
  private broadcastOrUnicastSessionList;
1885
+ private sessionListPayload;
1884
1886
  /**
1885
1887
  * Overlay boot-reconciliation verdicts onto session responses.
1886
1888
  *
@@ -1944,6 +1946,8 @@ declare class StreamerServer {
1944
1946
  * by id rather than duplicating it.
1945
1947
  */
1946
1948
  private rehydratePreviousSessions;
1949
+ /** Resume only the recent sessions the user explicitly allowed us to start at boot. */
1950
+ private autoResumePreviousSessions;
1947
1951
  /**
1948
1952
  * Pick a token guaranteed to appear in the spawned process's argv, for the
1949
1953
  * reconciler's pid-reuse guard.
package/dist/index.js CHANGED
@@ -8781,6 +8781,34 @@ function paginate(results, offset, limit) {
8781
8781
  };
8782
8782
  }
8783
8783
 
8784
+ // src/services/sessions/autoResumeOnBoot.ts
8785
+ var AUTO_RESUME_WINDOW_MS = 15 * 60 * 1e3;
8786
+ var AUTO_RESUME_MAX = 5;
8787
+ var AUTO_RESUME_CONCURRENCY = 2;
8788
+ var AUTO_RESUME_STAGGER_MS = 500;
8789
+ function autoResumeSkipReason(row, opts) {
8790
+ if (row.status_source !== "shutdown") return "not_shutdown";
8791
+ if (row.status !== "running" && row.status !== "waiting_input") return "not_interrupted";
8792
+ if (opts.now - row.status_updated_at > AUTO_RESUME_WINDOW_MS) return "too_old";
8793
+ if (!opts.projectExists(row.project_path)) return "project_missing";
8794
+ if (resumeIdForRow(row) == null) return "resume_identity_missing";
8795
+ return null;
8796
+ }
8797
+ function planAutoResume(rows, opts) {
8798
+ const eligible = [];
8799
+ const skipped = [];
8800
+ for (const row of rows) {
8801
+ const reason = autoResumeSkipReason(row, opts);
8802
+ if (reason) skipped.push({ row, reason });
8803
+ else eligible.push(row);
8804
+ }
8805
+ return {
8806
+ attempts: eligible.slice(0, AUTO_RESUME_MAX),
8807
+ skipped,
8808
+ overflow: eligible.slice(AUTO_RESUME_MAX)
8809
+ };
8810
+ }
8811
+
8784
8812
  // src/services/sessions/conversationBusy.ts
8785
8813
  import { statSync as statSync8 } from "fs";
8786
8814
  var RESUME_BUSY_WINDOW_MS = 12e4;
@@ -9622,6 +9650,7 @@ var StreamerServer = class {
9622
9650
  disableDb = false;
9623
9651
  // Skip the startup warm-up scan (test hook; see ServerConfig.skipStartupWarmup).
9624
9652
  skipStartupWarmup;
9653
+ autoResumeOnBoot;
9625
9654
  browseRoot = null;
9626
9655
  publicUrl = null;
9627
9656
  browserCors;
@@ -9738,6 +9767,7 @@ var StreamerServer = class {
9738
9767
  this.verbose = config.verbose ?? false;
9739
9768
  this.disableDb = config.disableDb ?? false;
9740
9769
  this.skipStartupWarmup = config.skipStartupWarmup ?? false;
9770
+ this.autoResumeOnBoot = config.autoResumeOnBoot ?? false;
9741
9771
  this.scannerPersistenceDisabled = config.scannerPersistent === false;
9742
9772
  this.scanProfiles = config.scanProfiles;
9743
9773
  this.codexRoots = config.codexRoots ?? [join18(homedir9(), ".codex", "sessions")];
@@ -10218,16 +10248,19 @@ var StreamerServer = class {
10218
10248
  broadcastOrUnicastSessionList(req) {
10219
10249
  const clientId = req.headers["x-client-id"];
10220
10250
  const ws = typeof clientId === "string" ? this.clientIdToWs.get(clientId) : void 0;
10221
- const payload = {
10222
- type: "session_list",
10223
- sessions: this.withReconciledLifecycle(this.sessionStore.list(this.ptyAttachedIds()))
10224
- };
10251
+ const payload = this.sessionListPayload();
10225
10252
  if (ws) {
10226
10253
  this.wsHub.unicast(ws, payload);
10227
10254
  } else {
10228
10255
  this.wsHub.broadcast(payload);
10229
10256
  }
10230
10257
  }
10258
+ sessionListPayload() {
10259
+ return {
10260
+ type: "session_list",
10261
+ sessions: this.withReconciledLifecycle(this.sessionStore.list(this.ptyAttachedIds()))
10262
+ };
10263
+ }
10231
10264
  /**
10232
10265
  * Overlay boot-reconciliation verdicts onto session responses.
10233
10266
  *
@@ -10401,7 +10434,7 @@ var StreamerServer = class {
10401
10434
  * by id rather than duplicating it.
10402
10435
  */
10403
10436
  rehydratePreviousSessions(verdicts) {
10404
- if (!this.featureFlags.sessionRehydration || !this.managedSessionsRepo) return;
10437
+ if (!this.managedSessionsRepo) return [];
10405
10438
  try {
10406
10439
  const now = Date.now();
10407
10440
  const rows = this.managedSessionsRepo.listRecoverable({
@@ -10410,7 +10443,7 @@ var StreamerServer = class {
10410
10443
  });
10411
10444
  const truncated = rows.length > REHYDRATE_MAX;
10412
10445
  const candidates = truncated ? rows.slice(0, REHYDRATE_MAX) : rows;
10413
- if (candidates.length === 0) return;
10446
+ if (!this.featureFlags.sessionRehydration || candidates.length === 0) return candidates;
10414
10447
  const verdictById = new Map(verdicts.map((v) => [v.sessionId, v]));
10415
10448
  let rehydrated = 0;
10416
10449
  const skippedBy = {};
@@ -10445,12 +10478,107 @@ var StreamerServer = class {
10445
10478
  skippedBy,
10446
10479
  truncated
10447
10480
  });
10481
+ return candidates;
10448
10482
  } catch (err) {
10449
10483
  this.log.warn("[rehydrate] failed to rehydrate previous sessions", {
10450
10484
  event: "sessions.rehydrate_failed",
10451
10485
  err
10452
10486
  });
10487
+ return [];
10488
+ }
10489
+ }
10490
+ /** Resume only the recent sessions the user explicitly allowed us to start at boot. */
10491
+ async autoResumePreviousSessions(rows) {
10492
+ if (!this.autoResumeOnBoot) return;
10493
+ const plan = planAutoResume(rows, { now: Date.now(), projectExists: existsSync11 });
10494
+ const skippedBy = {};
10495
+ for (const { row, reason } of plan.skipped) {
10496
+ skippedBy[reason] = (skippedBy[reason] ?? 0) + 1;
10497
+ this.log.debug(`[auto-resume] skipped ${row.session_id}: ${reason}`, {
10498
+ event: "sessions.auto_resume_skipped",
10499
+ sessionId: row.session_id,
10500
+ reason
10501
+ });
10453
10502
  }
10503
+ if (plan.skipped.length > 0) {
10504
+ this.log.info(
10505
+ `[auto-resume] left ${plan.skipped.length} ineligible session(s) for manual resume`,
10506
+ {
10507
+ event: "sessions.auto_resume_skipped",
10508
+ skipped: plan.skipped.length,
10509
+ skippedBy
10510
+ }
10511
+ );
10512
+ }
10513
+ for (const row of plan.overflow) {
10514
+ this.log.info(`[auto-resume] left ${row.session_id} for manual resume: ceiling reached`, {
10515
+ event: "sessions.auto_resume_skipped",
10516
+ sessionId: row.session_id,
10517
+ reason: "ceiling_reached"
10518
+ });
10519
+ }
10520
+ let resumed = 0;
10521
+ let failed = 0;
10522
+ const inFlight = /* @__PURE__ */ new Set();
10523
+ let started = 0;
10524
+ const resume = async (row) => {
10525
+ try {
10526
+ const outcome = await this.resumeSession({
10527
+ sessionId: row.session_id,
10528
+ projectName: row.project_name,
10529
+ branch: row.branch
10530
+ });
10531
+ if (!outcome.ok) {
10532
+ failed++;
10533
+ this.log.info(`[auto-resume] skipped ${row.session_id}: ${outcome.reason}`, {
10534
+ event: "sessions.auto_resume_skipped",
10535
+ sessionId: row.session_id,
10536
+ reason: outcome.reason,
10537
+ ...outcome.reason === "conversation_busy" && {
10538
+ detectedBy: outcome.detectedBy,
10539
+ lastActivityMs: outcome.lastActivityMs,
10540
+ likelyOwner: outcome.likelyOwner
10541
+ }
10542
+ });
10543
+ return;
10544
+ }
10545
+ resumed++;
10546
+ this.log.info(`[auto-resume] resumed ${row.session_id}`, {
10547
+ event: "sessions.auto_resume_succeeded",
10548
+ sessionId: row.session_id,
10549
+ alreadyRunning: outcome.alreadyRunning
10550
+ });
10551
+ } catch (err) {
10552
+ failed++;
10553
+ this.log.warn(`[auto-resume] failed to resume ${row.session_id}`, {
10554
+ event: "sessions.auto_resume_failed",
10555
+ sessionId: row.session_id,
10556
+ err
10557
+ });
10558
+ }
10559
+ };
10560
+ for (const row of plan.attempts) {
10561
+ while (inFlight.size >= AUTO_RESUME_CONCURRENCY) {
10562
+ await Promise.race(inFlight);
10563
+ }
10564
+ if (started > 0) {
10565
+ await new Promise((resolve2) => setTimeout(resolve2, AUTO_RESUME_STAGGER_MS));
10566
+ }
10567
+ const task = resume(row);
10568
+ inFlight.add(task);
10569
+ void task.then(() => inFlight.delete(task));
10570
+ started++;
10571
+ }
10572
+ await Promise.all(inFlight);
10573
+ if (resumed > 0) this.wsHub.broadcast(this.sessionListPayload());
10574
+ this.log.info(`[auto-resume] completed boot recovery: ${resumed} resumed`, {
10575
+ event: "sessions.auto_resume_completed",
10576
+ attempted: plan.attempts.length,
10577
+ resumed,
10578
+ failed,
10579
+ ineligible: plan.skipped.length,
10580
+ overflow: plan.overflow.length
10581
+ });
10454
10582
  }
10455
10583
  /**
10456
10584
  * Pick a token guaranteed to appear in the spawned process's argv, for the
@@ -10772,8 +10900,9 @@ var StreamerServer = class {
10772
10900
  );
10773
10901
  this.scannerPersistenceDisabled = true;
10774
10902
  }
10775
- void this.reconcilePreviousSessions().then((v) => {
10776
- this.rehydratePreviousSessions(v);
10903
+ void this.reconcilePreviousSessions().then(async (v) => {
10904
+ const recoverableRows = this.rehydratePreviousSessions(v);
10905
+ await this.autoResumePreviousSessions(recoverableRows);
10777
10906
  this.pruneTerminalSessions();
10778
10907
  });
10779
10908
  if (this.skipStartupWarmup) {