@threadbase-sh/streamer 1.65.0 → 1.67.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.js CHANGED
@@ -4926,7 +4926,7 @@ function redactPath(path) {
4926
4926
  function worstStatus(checks) {
4927
4927
  const rank = { ok: 0, unknown: 1, degraded: 2, failed: 3 };
4928
4928
  return checks.reduce(
4929
- (worst, c) => rank[c.status] > rank[worst] ? c.status : worst,
4929
+ (worst2, c) => rank[c.status] > rank[worst2] ? c.status : worst2,
4930
4930
  "ok"
4931
4931
  );
4932
4932
  }
@@ -5946,7 +5946,12 @@ var createMiscRoutes = (deps) => {
5946
5946
  // Whether to encrypt to this server. Additive, same contract as `push`:
5947
5947
  // absent means an older server, which a client must read as "unknown" and
5948
5948
  // resolve as today's plaintext path — never as a reason to fail.
5949
- e2ee: describeE2eeCapability(deps.featureFlagsConfig().values.e2ee)
5949
+ e2ee: describeE2eeCapability(deps.featureFlagsConfig().values.e2ee),
5950
+ // This build samples cheap host signals and pushes `host_pressure` when
5951
+ // the box is starved. Additive capability flag only — live readings stay
5952
+ // off this polled endpoint. Absent means an older server that never
5953
+ // samples. Informational: pressure never holds, kills, or refuses sessions.
5954
+ hostPressure: true
5950
5955
  });
5951
5956
  });
5952
5957
  app.get("/api/profiles", (c) => c.json([]));
@@ -12851,6 +12856,7 @@ function createApiDeps(deps) {
12851
12856
  wsHub: deps.wsHub,
12852
12857
  cache: () => deps.cache(),
12853
12858
  cacheMonitor: () => deps.cacheMonitor(),
12859
+ hostPressureMonitor: () => deps.hostPressureMonitor(),
12854
12860
  pushRepo: () => deps.pushRepo(),
12855
12861
  liveActivityPushEnabled: () => deps.liveActivityPushEnabled(),
12856
12862
  devicesRepo: () => deps.devicesRepo(),
@@ -12901,6 +12907,8 @@ function createApiDeps(deps) {
12901
12907
  }
12902
12908
  const alertMsg = deps.cacheMonitor()?.wsMessage();
12903
12909
  if (alertMsg) deps.wsHub.unicast(ws, alertMsg);
12910
+ const pressureMsg = deps.hostPressureMonitor()?.wsMessage();
12911
+ if (pressureMsg) deps.wsHub.unicast(ws, pressureMsg);
12904
12912
  },
12905
12913
  handleWsMessage: async (ws, raw, principal) => {
12906
12914
  const deny = (type, required) => {
@@ -12971,6 +12979,13 @@ function createApiDeps(deps) {
12971
12979
  );
12972
12980
  }
12973
12981
  }
12982
+ if (msg.type === "unsubscribe_session" && typeof msg.sessionId === "string") {
12983
+ if (!wsAllows(principal, "history:read")) {
12984
+ deny(msg.type, "history:read");
12985
+ return;
12986
+ }
12987
+ deps.removeSessionSubscriber(msg.sessionId, ws);
12988
+ }
12974
12989
  if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
12975
12990
  if (!wsAllows(principal, "session:control")) {
12976
12991
  deny(msg.type, "session:control");
@@ -13526,6 +13541,221 @@ function pruneAgentConversations(cache) {
13526
13541
  return { scanned: rows.length, pruned, missing };
13527
13542
  }
13528
13543
 
13544
+ // src/services/host-pressure/hostPressure.ts
13545
+ import { cpus, freemem, loadavg, totalmem } from "os";
13546
+ import { monitorEventLoopDelay } from "perf_hooks";
13547
+ var HOST_PRESSURE_SAMPLE_MS = 5e3;
13548
+ var HOST_PRESSURE_BARS = {
13549
+ memFreeRatio: {
13550
+ enterElevated: 0.15,
13551
+ leaveElevated: 0.17,
13552
+ enterCritical: 0.08,
13553
+ leaveCritical: 0.15
13554
+ },
13555
+ eventLoopP99Ms: {
13556
+ enterElevated: 100,
13557
+ leaveElevated: 80,
13558
+ enterCritical: 250,
13559
+ leaveCritical: 100
13560
+ },
13561
+ loadPerCpu: {
13562
+ enterElevated: 1.25,
13563
+ leaveElevated: 1.05,
13564
+ enterCritical: 2,
13565
+ leaveCritical: 1.25
13566
+ },
13567
+ // win32 has no loadavg. Busy ratio from os.cpus()[].times deltas is 0–1, so
13568
+ // it cannot reuse loadPerCpu's 2.0 critical bar. Reason on the wire stays `load`.
13569
+ cpuBusy: {
13570
+ enterElevated: 0.85,
13571
+ leaveElevated: 0.7,
13572
+ enterCritical: 0.97,
13573
+ leaveCritical: 0.85
13574
+ },
13575
+ liveAgentsPair: 4
13576
+ };
13577
+ var REASON_ORDER = ["memory", "event_loop", "load", "agents"];
13578
+ var RANK = { ok: 0, elevated: 1, critical: 2 };
13579
+ function worst(levels) {
13580
+ return levels.reduce(
13581
+ (acc, level) => RANK[level] > RANK[acc] ? level : acc,
13582
+ "ok"
13583
+ );
13584
+ }
13585
+ function schmittLowIsWorse(value, previous, enterElevated, leaveElevated, enterCritical, leaveCritical) {
13586
+ if (previous === "critical") {
13587
+ if (value < leaveCritical) return "critical";
13588
+ if (value < leaveElevated) return "elevated";
13589
+ return "ok";
13590
+ }
13591
+ if (previous === "elevated") {
13592
+ if (value < enterCritical) return "critical";
13593
+ if (value < leaveElevated) return "elevated";
13594
+ return "ok";
13595
+ }
13596
+ if (value < enterCritical) return "critical";
13597
+ if (value < enterElevated) return "elevated";
13598
+ return "ok";
13599
+ }
13600
+ function schmittHighIsWorse(value, previous, enterElevated, leaveElevated, enterCritical, leaveCritical) {
13601
+ if (previous === "critical") {
13602
+ if (value > leaveCritical) return "critical";
13603
+ if (value > leaveElevated) return "elevated";
13604
+ return "ok";
13605
+ }
13606
+ if (previous === "elevated") {
13607
+ if (value > enterCritical) return "critical";
13608
+ if (value > leaveElevated) return "elevated";
13609
+ return "ok";
13610
+ }
13611
+ if (value > enterCritical) return "critical";
13612
+ if (value > enterElevated) return "elevated";
13613
+ return "ok";
13614
+ }
13615
+ function timesTotal(times) {
13616
+ return times.user + times.nice + times.sys + times.idle + times.irq;
13617
+ }
13618
+ function hostPressureOs(platform3) {
13619
+ if (platform3 === "darwin" || platform3 === "linux" || platform3 === "win32") return platform3;
13620
+ return void 0;
13621
+ }
13622
+ function cpuBusyRatio(previous, next) {
13623
+ if (!previous || previous.length === 0 || next.length === 0 || previous.length !== next.length) {
13624
+ return 0;
13625
+ }
13626
+ let idle = 0;
13627
+ let total = 0;
13628
+ for (let i = 0; i < next.length; i++) {
13629
+ const dt = timesTotal(next[i]) - timesTotal(previous[i]);
13630
+ if (dt <= 0) continue;
13631
+ total += dt;
13632
+ idle += Math.max(0, next[i].idle - previous[i].idle);
13633
+ }
13634
+ if (total <= 0) return 0;
13635
+ return 1 - idle / total;
13636
+ }
13637
+ function classifyHostPressure(sample, previous, platform3) {
13638
+ const memRaw = schmittLowIsWorse(
13639
+ sample.memFreeRatio,
13640
+ previous,
13641
+ HOST_PRESSURE_BARS.memFreeRatio.enterElevated,
13642
+ HOST_PRESSURE_BARS.memFreeRatio.leaveElevated,
13643
+ HOST_PRESSURE_BARS.memFreeRatio.enterCritical,
13644
+ HOST_PRESSURE_BARS.memFreeRatio.leaveCritical
13645
+ );
13646
+ const mem = platform3 === "darwin" && memRaw === "critical" ? "elevated" : memRaw;
13647
+ const eventLoop = schmittHighIsWorse(
13648
+ sample.eventLoopP99Ms,
13649
+ previous,
13650
+ HOST_PRESSURE_BARS.eventLoopP99Ms.enterElevated,
13651
+ HOST_PRESSURE_BARS.eventLoopP99Ms.leaveElevated,
13652
+ HOST_PRESSURE_BARS.eventLoopP99Ms.enterCritical,
13653
+ HOST_PRESSURE_BARS.eventLoopP99Ms.leaveCritical
13654
+ );
13655
+ const ncpu = sample.ncpu > 0 ? sample.ncpu : 1;
13656
+ const windows = platform3 === "win32";
13657
+ const loadValue = windows ? sample.cpuBusyRatio ?? 0 : sample.load1 / ncpu;
13658
+ const loadBars = windows ? HOST_PRESSURE_BARS.cpuBusy : HOST_PRESSURE_BARS.loadPerCpu;
13659
+ const load = schmittHighIsWorse(
13660
+ loadValue,
13661
+ previous,
13662
+ loadBars.enterElevated,
13663
+ loadBars.leaveElevated,
13664
+ loadBars.enterCritical,
13665
+ loadBars.leaveCritical
13666
+ );
13667
+ const resourceLevel = worst([mem, eventLoop, load]);
13668
+ const agentsPair = sample.liveAgents >= HOST_PRESSURE_BARS.liveAgentsPair && resourceLevel !== "ok";
13669
+ const level = resourceLevel;
13670
+ const firing = [];
13671
+ if (mem !== "ok") firing.push("memory");
13672
+ if (eventLoop !== "ok") firing.push("event_loop");
13673
+ if (load !== "ok") firing.push("load");
13674
+ if (agentsPair) firing.push("agents");
13675
+ const reasons = REASON_ORDER.filter((reason) => firing.includes(reason));
13676
+ return { level, reasons };
13677
+ }
13678
+ var HostPressureMonitor = class {
13679
+ constructor(opts) {
13680
+ this.opts = opts;
13681
+ }
13682
+ opts;
13683
+ timer = null;
13684
+ level = "ok";
13685
+ lastWarning = null;
13686
+ start() {
13687
+ this.opts.histogram?.enable();
13688
+ if (this.timer) return;
13689
+ const intervalMs = this.opts.intervalMs ?? HOST_PRESSURE_SAMPLE_MS;
13690
+ this.timer = setInterval(() => this.tick(), intervalMs);
13691
+ this.timer.unref?.();
13692
+ }
13693
+ tick() {
13694
+ const sample = this.opts.readSample();
13695
+ const platform3 = this.opts.platform ?? process.platform;
13696
+ const classified = classifyHostPressure(sample, this.level, platform3);
13697
+ if (classified.level === this.level) return;
13698
+ this.level = classified.level;
13699
+ const updatedAt = (this.opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
13700
+ if (classified.level === "ok") {
13701
+ this.lastWarning = null;
13702
+ this.opts.wsHub.broadcast({ type: "host_pressure_cleared", updatedAt });
13703
+ return;
13704
+ }
13705
+ const os2 = hostPressureOs(platform3);
13706
+ const message = {
13707
+ type: "host_pressure",
13708
+ level: classified.level,
13709
+ reasons: classified.reasons,
13710
+ liveAgents: sample.liveAgents,
13711
+ updatedAt,
13712
+ ...os2 ? { os: os2 } : {}
13713
+ };
13714
+ this.lastWarning = message;
13715
+ this.opts.wsHub.broadcast(message);
13716
+ }
13717
+ wsMessage() {
13718
+ return this.lastWarning;
13719
+ }
13720
+ dispose() {
13721
+ if (this.timer) {
13722
+ clearInterval(this.timer);
13723
+ this.timer = null;
13724
+ }
13725
+ this.opts.histogram?.disable();
13726
+ }
13727
+ };
13728
+ function createHostPressureMonitor(wsHub, liveAgents) {
13729
+ const histogram = monitorEventLoopDelay({ resolution: 20 });
13730
+ const windows = process.platform === "win32";
13731
+ let prevCpuTimes = null;
13732
+ const monitor = new HostPressureMonitor({
13733
+ wsHub,
13734
+ histogram,
13735
+ readSample: () => {
13736
+ const eventLoopP99Ms = histogram.percentile(99) / 1e6;
13737
+ histogram.reset();
13738
+ const total = totalmem();
13739
+ const cpuList = cpus();
13740
+ const sample = {
13741
+ liveAgents: liveAgents(),
13742
+ memFreeRatio: total > 0 ? freemem() / total : 1,
13743
+ eventLoopP99Ms,
13744
+ load1: loadavg()[0],
13745
+ ncpu: cpuList.length
13746
+ };
13747
+ if (windows) {
13748
+ const cpuTimes = cpuList.map((cpu) => cpu.times);
13749
+ sample.cpuBusyRatio = cpuBusyRatio(prevCpuTimes, cpuTimes);
13750
+ prevCpuTimes = cpuTimes;
13751
+ }
13752
+ return sample;
13753
+ }
13754
+ });
13755
+ monitor.start();
13756
+ return monitor;
13757
+ }
13758
+
13529
13759
  // src/services/push/expoPushSender.ts
13530
13760
  var log5 = getLogger("expo-push");
13531
13761
  var EXPO_PUSH_ENDPOINT = "https://exp.host/--/api/v2/push/send";
@@ -15574,6 +15804,7 @@ var StreamerServer = class {
15574
15804
  wsToClientId = /* @__PURE__ */ new Map();
15575
15805
  cache = null;
15576
15806
  cacheMonitor = null;
15807
+ hostPressureMonitor = null;
15577
15808
  projectsRepo = null;
15578
15809
  conversationsRepo = null;
15579
15810
  sessionsRepo = null;
@@ -15924,6 +16155,7 @@ var StreamerServer = class {
15924
16155
  // reset-and-rescan, and tests swap methods on the server instance.
15925
16156
  cache: () => this.cache,
15926
16157
  cacheMonitor: () => this.cacheMonitor,
16158
+ hostPressureMonitor: () => this.hostPressureMonitor,
15927
16159
  pushRepo: () => this.pushRepo,
15928
16160
  liveActivityPushEnabled: () => this.liveActivityNotifier !== null,
15929
16161
  devicesRepo: () => this.devicesRepo,
@@ -15939,6 +16171,7 @@ var StreamerServer = class {
15939
16171
  withReconciledLifecycle: (sessions) => this.withReconciledLifecycle(sessions),
15940
16172
  currentWarmupState: () => this.currentWarmupState(),
15941
16173
  addSessionSubscriber: (sessionId, ws) => this.addSessionSubscriber(sessionId, ws),
16174
+ removeSessionSubscriber: (sessionId, ws) => this.removeSessionSubscriber(sessionId, ws),
15942
16175
  startGraceTimer: (sessionId, delayMs) => this.startGraceTimer(sessionId, delayMs),
15943
16176
  armHoldWhenIdle: (sessionId) => this.armHoldWhenIdle(sessionId),
15944
16177
  handleSessionsCount: (res) => this.handleSessionsCount(res),
@@ -16068,6 +16301,12 @@ var StreamerServer = class {
16068
16301
  );
16069
16302
  }
16070
16303
  }
16304
+ removeSessionSubscriber(sessionId, ws) {
16305
+ const subs = this.sessionSubscribers.get(sessionId);
16306
+ if (!subs) return;
16307
+ subs.delete(ws);
16308
+ if (subs.size === 0) this.sessionSubscribers.delete(sessionId);
16309
+ }
16071
16310
  /**
16072
16311
  * Bring up Live Activity push, if credentials are present (Feature 12).
16073
16312
  *
@@ -16398,6 +16637,10 @@ var StreamerServer = class {
16398
16637
  this.idleReaperTimer = setInterval(() => this.reapIdleSessions(), IDLE_REAP_SWEEP_MS);
16399
16638
  this.idleReaperTimer.unref?.();
16400
16639
  }
16640
+ this.hostPressureMonitor = createHostPressureMonitor(
16641
+ this.wsHub,
16642
+ () => this.ptyAttachedIds().size
16643
+ );
16401
16644
  const warmUp = new Promise((resolveWarm) => {
16402
16645
  {
16403
16646
  this.log.info(`Streamer server listening on port ${port}`, {
@@ -16710,6 +16953,8 @@ var StreamerServer = class {
16710
16953
  clearInterval(this.idleReaperTimer);
16711
16954
  this.idleReaperTimer = null;
16712
16955
  }
16956
+ this.hostPressureMonitor?.dispose();
16957
+ this.hostPressureMonitor = null;
16713
16958
  this.lastAgentChunkAt.clear();
16714
16959
  this.terminalSeq.clear();
16715
16960
  if (this.ptyManager.isRemote()) this.ptyManager.dispose();