@threadbase-sh/streamer 1.64.0 → 1.65.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
@@ -10660,6 +10660,19 @@ var SessionHandlers = class {
10660
10660
  sessions: this.sessionStore.list(this.deps.ptyAttachedIds())
10661
10661
  });
10662
10662
  }
10663
+ /**
10664
+ * Same empty-unused check stop uses (promptCount === 0 and no cache row,
10665
+ * including boundConversationId / resumedFromConversationId). Safe after
10666
+ * putOnHold: the runner may already have dropped the live session.
10667
+ */
10668
+ forgetIfEmptyUnused(sessionId) {
10669
+ const live = this.ptyManager.getSession(sessionId);
10670
+ const stored = this.sessionStore.getManaged(sessionId);
10671
+ const session = live ?? stored;
10672
+ if (!session) return;
10673
+ if (!this.shouldForgetEmptySession(session)) return;
10674
+ this.forgetEmptyStoppedSession(sessionId);
10675
+ }
10663
10676
  async handleAdopt(sessionId, res) {
10664
10677
  const discovered = await discoverClaudeProcesses();
10665
10678
  this.sessionStore.setDiscovered(discovered);
@@ -12843,6 +12856,7 @@ function createLiveSessionOptions(deps) {
12843
12856
  void deps.liveActivityNotifier()?.onStatusChange(session, previousStatus);
12844
12857
  void deps.waitingInputNotifier()?.onStatusChange(session, previousStatus);
12845
12858
  deps.sessionStatusBus.emit(`status:${session.id}`, session.status, session);
12859
+ deps.maybeFireHoldWhenIdle?.(session);
12846
12860
  }
12847
12861
  };
12848
12862
  }
@@ -12991,7 +13005,20 @@ function createApiDeps(deps) {
12991
13005
  deny(msg.type, "session:control");
12992
13006
  return;
12993
13007
  }
12994
- deps.startGraceTimer(msg.sessionId, deps.ptyGracePeriodMs);
13008
+ const when = msg.when;
13009
+ if (when === void 0 || when === "grace") {
13010
+ deps.startGraceTimer(msg.sessionId, deps.ptyGracePeriodMs);
13011
+ return;
13012
+ }
13013
+ if (when === "waiting_input") {
13014
+ deps.armHoldWhenIdle(msg.sessionId);
13015
+ return;
13016
+ }
13017
+ deps.log().warn(`[pty.hold_when_unknown] hold_session when=${String(when)}`, {
13018
+ event: "pty.hold_when_unknown",
13019
+ sessionId: msg.sessionId,
13020
+ when
13021
+ });
12995
13022
  }
12996
13023
  } catch {
12997
13024
  }
@@ -15547,6 +15574,10 @@ var StreamerServer = class {
15547
15574
  // Consecutive grace-timer defers for a still-`running` session (see
15548
15575
  // GRACE_MAX_DEFERS). Reset when a subscriber reconnects or the PTY settles.
15549
15576
  ptyGraceDeferCounts = /* @__PURE__ */ new Map();
15577
+ // Session ids that should be putOnHold at the next waiting_input/idle.
15578
+ // Same lifetime as ptyGraceTimers: in-memory, dropped on close/restart.
15579
+ // Last writer wins against the grace timer — never both armed.
15580
+ holdWhenIdle = /* @__PURE__ */ new Set();
15550
15581
  // Map of sessionId → set of subscribed WS clients
15551
15582
  sessionSubscribers = /* @__PURE__ */ new Map();
15552
15583
  // sessionId → wall-clock ms of the last PTY chunk. Written from onOutput for
@@ -15774,7 +15805,8 @@ var StreamerServer = class {
15774
15805
  waitingInputNotifier: () => this.waitingInputNotifier,
15775
15806
  ptyAttachedIds: () => this.ptyAttachedIds(),
15776
15807
  cancelPendingQuestion: (sessionId) => this.cancelPendingQuestion(sessionId),
15777
- rememberSelfPtyEnded: (conversationId) => this.rememberSelfPtyEnded(conversationId)
15808
+ rememberSelfPtyEnded: (conversationId) => this.rememberSelfPtyEnded(conversationId),
15809
+ maybeFireHoldWhenIdle: (session) => this.maybeFireHoldWhenIdle(session)
15778
15810
  })
15779
15811
  );
15780
15812
  this.sessionWatchers = new SessionWatchers({
@@ -15937,6 +15969,7 @@ var StreamerServer = class {
15937
15969
  currentWarmupState: () => this.currentWarmupState(),
15938
15970
  addSessionSubscriber: (sessionId, ws) => this.addSessionSubscriber(sessionId, ws),
15939
15971
  startGraceTimer: (sessionId, delayMs) => this.startGraceTimer(sessionId, delayMs),
15972
+ armHoldWhenIdle: (sessionId) => this.armHoldWhenIdle(sessionId),
15940
15973
  handleSessionsCount: (res) => this.handleSessionsCount(res),
15941
15974
  applyLiveSessionSetting: (id, req, res, setting) => this.applyLiveSessionSetting(id, req, res, setting),
15942
15975
  handlePairStart: (res) => this.handlePairStart(res),
@@ -16056,6 +16089,13 @@ var StreamerServer = class {
16056
16089
  this.ptyGraceTimers.delete(sessionId);
16057
16090
  }
16058
16091
  this.ptyGraceDeferCounts.delete(sessionId);
16092
+ if (this.holdWhenIdle.delete(sessionId)) {
16093
+ this.log.info(
16094
+ `[hold-when-idle] cancelled ${sessionId} (subscribe)`,
16095
+ { sessionId, event: "pty.hold_when_idle_cancel", reason: "subscribe" },
16096
+ "pino"
16097
+ );
16098
+ }
16059
16099
  }
16060
16100
  /**
16061
16101
  * Bring up Live Activity push, if credentials are present (Feature 12).
@@ -16163,6 +16203,7 @@ var StreamerServer = class {
16163
16203
  return reaped;
16164
16204
  }
16165
16205
  startGraceTimer(sessionId, delayMs) {
16206
+ this.holdWhenIdle.delete(sessionId);
16166
16207
  const existing = this.ptyGraceTimers.get(sessionId);
16167
16208
  if (existing) clearTimeout(existing);
16168
16209
  const timer = setTimeout(() => {
@@ -16204,6 +16245,63 @@ var StreamerServer = class {
16204
16245
  }, delayMs);
16205
16246
  this.ptyGraceTimers.set(sessionId, timer);
16206
16247
  }
16248
+ clearGrace(sessionId) {
16249
+ const existing = this.ptyGraceTimers.get(sessionId);
16250
+ if (existing) {
16251
+ clearTimeout(existing);
16252
+ this.ptyGraceTimers.delete(sessionId);
16253
+ }
16254
+ this.ptyGraceDeferCounts.delete(sessionId);
16255
+ }
16256
+ /**
16257
+ * Arm the in-app "Kill on idle" latch: hold now if already settled, otherwise
16258
+ * on the next running → waiting_input (or idle). No grace delay, no defer cap.
16259
+ * A subscribed leaving socket must not block an immediate hold.
16260
+ */
16261
+ armHoldWhenIdle(sessionId) {
16262
+ if (!this.ptyManager.hasSession(sessionId)) return;
16263
+ this.clearGrace(sessionId);
16264
+ const status = this.ptyManager.getSession(sessionId)?.status ?? this.sessionStore.getManaged(sessionId)?.status;
16265
+ if (status === "waiting_input" || status === "idle") {
16266
+ this.holdWhenIdle.delete(sessionId);
16267
+ this.ptyManager.putOnHold(sessionId);
16268
+ this.forgetIfEmptyUnused(sessionId);
16269
+ return;
16270
+ }
16271
+ this.holdWhenIdle.add(sessionId);
16272
+ this.log.info(
16273
+ `[hold-when-idle] armed ${sessionId}`,
16274
+ { sessionId, event: "pty.hold_when_idle_armed" },
16275
+ "pino"
16276
+ );
16277
+ }
16278
+ /**
16279
+ * Fire the Kill-on-idle latch from the shared onStatusChange funnel.
16280
+ * Delete first so the ensuing idle transition cannot re-enter.
16281
+ */
16282
+ maybeFireHoldWhenIdle(session) {
16283
+ if (session.status !== "waiting_input" && session.status !== "idle") return;
16284
+ if (!this.holdWhenIdle.has(session.id)) return;
16285
+ this.holdWhenIdle.delete(session.id);
16286
+ if (this.hasSessionSubscriber(session.id)) {
16287
+ this.log.info(
16288
+ `[hold-when-idle] cancelled ${session.id} (subscriber)`,
16289
+ { sessionId: session.id, event: "pty.hold_when_idle_cancel", reason: "subscriber" },
16290
+ "pino"
16291
+ );
16292
+ return;
16293
+ }
16294
+ this.log.info(
16295
+ `[hold-when-idle] holding ${session.id}`,
16296
+ { sessionId: session.id, event: "pty.hold_when_idle_fire" },
16297
+ "pino"
16298
+ );
16299
+ this.ptyManager.putOnHold(session.id);
16300
+ this.forgetIfEmptyUnused(session.id);
16301
+ }
16302
+ forgetIfEmptyUnused(sessionId) {
16303
+ this.sessionHandlers.forgetIfEmptyUnused(sessionId);
16304
+ }
16207
16305
  get port() {
16208
16306
  const addr = this.httpServer.address();
16209
16307
  return typeof addr === "object" && addr ? addr.port : 0;
@@ -16636,6 +16734,7 @@ var StreamerServer = class {
16636
16734
  async close() {
16637
16735
  for (const timer of this.ptyGraceTimers.values()) clearTimeout(timer);
16638
16736
  this.ptyGraceTimers.clear();
16737
+ this.holdWhenIdle.clear();
16639
16738
  if (this.idleReaperTimer) {
16640
16739
  clearInterval(this.idleReaperTimer);
16641
16740
  this.idleReaperTimer = null;