@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/cli.cjs CHANGED
@@ -146439,6 +146439,34 @@ function paginate(results, offset, limit) {
146439
146439
  };
146440
146440
  }
146441
146441
 
146442
+ // src/services/sessions/autoResumeOnBoot.ts
146443
+ var AUTO_RESUME_WINDOW_MS = 15 * 60 * 1e3;
146444
+ var AUTO_RESUME_MAX = 5;
146445
+ var AUTO_RESUME_CONCURRENCY = 2;
146446
+ var AUTO_RESUME_STAGGER_MS = 500;
146447
+ function autoResumeSkipReason(row, opts) {
146448
+ if (row.status_source !== "shutdown") return "not_shutdown";
146449
+ if (row.status !== "running" && row.status !== "waiting_input") return "not_interrupted";
146450
+ if (opts.now - row.status_updated_at > AUTO_RESUME_WINDOW_MS) return "too_old";
146451
+ if (!opts.projectExists(row.project_path)) return "project_missing";
146452
+ if (resumeIdForRow(row) == null) return "resume_identity_missing";
146453
+ return null;
146454
+ }
146455
+ function planAutoResume(rows, opts) {
146456
+ const eligible = [];
146457
+ const skipped = [];
146458
+ for (const row of rows) {
146459
+ const reason = autoResumeSkipReason(row, opts);
146460
+ if (reason) skipped.push({ row, reason });
146461
+ else eligible.push(row);
146462
+ }
146463
+ return {
146464
+ attempts: eligible.slice(0, AUTO_RESUME_MAX),
146465
+ skipped,
146466
+ overflow: eligible.slice(AUTO_RESUME_MAX)
146467
+ };
146468
+ }
146469
+
146442
146470
  // src/services/sessions/conversationBusy.ts
146443
146471
  var import_fs29 = require("fs");
146444
146472
  var RESUME_BUSY_WINDOW_MS = 12e4;
@@ -147312,6 +147340,7 @@ var StreamerServer = class {
147312
147340
  disableDb = false;
147313
147341
  // Skip the startup warm-up scan (test hook; see ServerConfig.skipStartupWarmup).
147314
147342
  skipStartupWarmup;
147343
+ autoResumeOnBoot;
147315
147344
  browseRoot = null;
147316
147345
  publicUrl = null;
147317
147346
  browserCors;
@@ -147428,6 +147457,7 @@ var StreamerServer = class {
147428
147457
  this.verbose = config2.verbose ?? false;
147429
147458
  this.disableDb = config2.disableDb ?? false;
147430
147459
  this.skipStartupWarmup = config2.skipStartupWarmup ?? false;
147460
+ this.autoResumeOnBoot = config2.autoResumeOnBoot ?? false;
147431
147461
  this.scannerPersistenceDisabled = config2.scannerPersistent === false;
147432
147462
  this.scanProfiles = config2.scanProfiles;
147433
147463
  this.codexRoots = config2.codexRoots ?? [(0, import_path29.join)((0, import_os13.homedir)(), ".codex", "sessions")];
@@ -147908,16 +147938,19 @@ var StreamerServer = class {
147908
147938
  broadcastOrUnicastSessionList(req) {
147909
147939
  const clientId = req.headers["x-client-id"];
147910
147940
  const ws2 = typeof clientId === "string" ? this.clientIdToWs.get(clientId) : void 0;
147911
- const payload = {
147912
- type: "session_list",
147913
- sessions: this.withReconciledLifecycle(this.sessionStore.list(this.ptyAttachedIds()))
147914
- };
147941
+ const payload = this.sessionListPayload();
147915
147942
  if (ws2) {
147916
147943
  this.wsHub.unicast(ws2, payload);
147917
147944
  } else {
147918
147945
  this.wsHub.broadcast(payload);
147919
147946
  }
147920
147947
  }
147948
+ sessionListPayload() {
147949
+ return {
147950
+ type: "session_list",
147951
+ sessions: this.withReconciledLifecycle(this.sessionStore.list(this.ptyAttachedIds()))
147952
+ };
147953
+ }
147921
147954
  /**
147922
147955
  * Overlay boot-reconciliation verdicts onto session responses.
147923
147956
  *
@@ -148091,7 +148124,7 @@ var StreamerServer = class {
148091
148124
  * by id rather than duplicating it.
148092
148125
  */
148093
148126
  rehydratePreviousSessions(verdicts) {
148094
- if (!this.featureFlags.sessionRehydration || !this.managedSessionsRepo) return;
148127
+ if (!this.managedSessionsRepo) return [];
148095
148128
  try {
148096
148129
  const now = Date.now();
148097
148130
  const rows = this.managedSessionsRepo.listRecoverable({
@@ -148100,7 +148133,7 @@ var StreamerServer = class {
148100
148133
  });
148101
148134
  const truncated = rows.length > REHYDRATE_MAX;
148102
148135
  const candidates = truncated ? rows.slice(0, REHYDRATE_MAX) : rows;
148103
- if (candidates.length === 0) return;
148136
+ if (!this.featureFlags.sessionRehydration || candidates.length === 0) return candidates;
148104
148137
  const verdictById = new Map(verdicts.map((v2) => [v2.sessionId, v2]));
148105
148138
  let rehydrated = 0;
148106
148139
  const skippedBy = {};
@@ -148135,12 +148168,107 @@ var StreamerServer = class {
148135
148168
  skippedBy,
148136
148169
  truncated
148137
148170
  });
148171
+ return candidates;
148138
148172
  } catch (err) {
148139
148173
  this.log.warn("[rehydrate] failed to rehydrate previous sessions", {
148140
148174
  event: "sessions.rehydrate_failed",
148141
148175
  err
148142
148176
  });
148177
+ return [];
148178
+ }
148179
+ }
148180
+ /** Resume only the recent sessions the user explicitly allowed us to start at boot. */
148181
+ async autoResumePreviousSessions(rows) {
148182
+ if (!this.autoResumeOnBoot) return;
148183
+ const plan = planAutoResume(rows, { now: Date.now(), projectExists: import_fs30.existsSync });
148184
+ const skippedBy = {};
148185
+ for (const { row, reason } of plan.skipped) {
148186
+ skippedBy[reason] = (skippedBy[reason] ?? 0) + 1;
148187
+ this.log.debug(`[auto-resume] skipped ${row.session_id}: ${reason}`, {
148188
+ event: "sessions.auto_resume_skipped",
148189
+ sessionId: row.session_id,
148190
+ reason
148191
+ });
148143
148192
  }
148193
+ if (plan.skipped.length > 0) {
148194
+ this.log.info(
148195
+ `[auto-resume] left ${plan.skipped.length} ineligible session(s) for manual resume`,
148196
+ {
148197
+ event: "sessions.auto_resume_skipped",
148198
+ skipped: plan.skipped.length,
148199
+ skippedBy
148200
+ }
148201
+ );
148202
+ }
148203
+ for (const row of plan.overflow) {
148204
+ this.log.info(`[auto-resume] left ${row.session_id} for manual resume: ceiling reached`, {
148205
+ event: "sessions.auto_resume_skipped",
148206
+ sessionId: row.session_id,
148207
+ reason: "ceiling_reached"
148208
+ });
148209
+ }
148210
+ let resumed = 0;
148211
+ let failed = 0;
148212
+ const inFlight = /* @__PURE__ */ new Set();
148213
+ let started = 0;
148214
+ const resume = async (row) => {
148215
+ try {
148216
+ const outcome = await this.resumeSession({
148217
+ sessionId: row.session_id,
148218
+ projectName: row.project_name,
148219
+ branch: row.branch
148220
+ });
148221
+ if (!outcome.ok) {
148222
+ failed++;
148223
+ this.log.info(`[auto-resume] skipped ${row.session_id}: ${outcome.reason}`, {
148224
+ event: "sessions.auto_resume_skipped",
148225
+ sessionId: row.session_id,
148226
+ reason: outcome.reason,
148227
+ ...outcome.reason === "conversation_busy" && {
148228
+ detectedBy: outcome.detectedBy,
148229
+ lastActivityMs: outcome.lastActivityMs,
148230
+ likelyOwner: outcome.likelyOwner
148231
+ }
148232
+ });
148233
+ return;
148234
+ }
148235
+ resumed++;
148236
+ this.log.info(`[auto-resume] resumed ${row.session_id}`, {
148237
+ event: "sessions.auto_resume_succeeded",
148238
+ sessionId: row.session_id,
148239
+ alreadyRunning: outcome.alreadyRunning
148240
+ });
148241
+ } catch (err) {
148242
+ failed++;
148243
+ this.log.warn(`[auto-resume] failed to resume ${row.session_id}`, {
148244
+ event: "sessions.auto_resume_failed",
148245
+ sessionId: row.session_id,
148246
+ err
148247
+ });
148248
+ }
148249
+ };
148250
+ for (const row of plan.attempts) {
148251
+ while (inFlight.size >= AUTO_RESUME_CONCURRENCY) {
148252
+ await Promise.race(inFlight);
148253
+ }
148254
+ if (started > 0) {
148255
+ await new Promise((resolve4) => setTimeout(resolve4, AUTO_RESUME_STAGGER_MS));
148256
+ }
148257
+ const task = resume(row);
148258
+ inFlight.add(task);
148259
+ void task.then(() => inFlight.delete(task));
148260
+ started++;
148261
+ }
148262
+ await Promise.all(inFlight);
148263
+ if (resumed > 0) this.wsHub.broadcast(this.sessionListPayload());
148264
+ this.log.info(`[auto-resume] completed boot recovery: ${resumed} resumed`, {
148265
+ event: "sessions.auto_resume_completed",
148266
+ attempted: plan.attempts.length,
148267
+ resumed,
148268
+ failed,
148269
+ ineligible: plan.skipped.length,
148270
+ overflow: plan.overflow.length
148271
+ });
148144
148272
  }
148145
148273
  /**
148146
148274
  * Pick a token guaranteed to appear in the spawned process's argv, for the
@@ -148462,8 +148590,9 @@ var StreamerServer = class {
148462
148590
  );
148463
148591
  this.scannerPersistenceDisabled = true;
148464
148592
  }
148465
- void this.reconcilePreviousSessions().then((v2) => {
148466
- this.rehydratePreviousSessions(v2);
148593
+ void this.reconcilePreviousSessions().then(async (v2) => {
148594
+ const recoverableRows = this.rehydratePreviousSessions(v2);
148595
+ await this.autoResumePreviousSessions(recoverableRows);
148467
148596
  this.pruneTerminalSessions();
148468
148597
  });
148469
148598
  if (this.skipStartupWarmup) {