@threadbase-sh/streamer 1.65.0 → 1.66.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) => {
@@ -13526,6 +13534,221 @@ function pruneAgentConversations(cache) {
13526
13534
  return { scanned: rows.length, pruned, missing };
13527
13535
  }
13528
13536
 
13537
+ // src/services/host-pressure/hostPressure.ts
13538
+ import { cpus, freemem, loadavg, totalmem } from "os";
13539
+ import { monitorEventLoopDelay } from "perf_hooks";
13540
+ var HOST_PRESSURE_SAMPLE_MS = 5e3;
13541
+ var HOST_PRESSURE_BARS = {
13542
+ memFreeRatio: {
13543
+ enterElevated: 0.15,
13544
+ leaveElevated: 0.17,
13545
+ enterCritical: 0.08,
13546
+ leaveCritical: 0.15
13547
+ },
13548
+ eventLoopP99Ms: {
13549
+ enterElevated: 100,
13550
+ leaveElevated: 80,
13551
+ enterCritical: 250,
13552
+ leaveCritical: 100
13553
+ },
13554
+ loadPerCpu: {
13555
+ enterElevated: 1.25,
13556
+ leaveElevated: 1.05,
13557
+ enterCritical: 2,
13558
+ leaveCritical: 1.25
13559
+ },
13560
+ // win32 has no loadavg. Busy ratio from os.cpus()[].times deltas is 0–1, so
13561
+ // it cannot reuse loadPerCpu's 2.0 critical bar. Reason on the wire stays `load`.
13562
+ cpuBusy: {
13563
+ enterElevated: 0.85,
13564
+ leaveElevated: 0.7,
13565
+ enterCritical: 0.97,
13566
+ leaveCritical: 0.85
13567
+ },
13568
+ liveAgentsPair: 4
13569
+ };
13570
+ var REASON_ORDER = ["memory", "event_loop", "load", "agents"];
13571
+ var RANK = { ok: 0, elevated: 1, critical: 2 };
13572
+ function worst(levels) {
13573
+ return levels.reduce(
13574
+ (acc, level) => RANK[level] > RANK[acc] ? level : acc,
13575
+ "ok"
13576
+ );
13577
+ }
13578
+ function schmittLowIsWorse(value, previous, enterElevated, leaveElevated, enterCritical, leaveCritical) {
13579
+ if (previous === "critical") {
13580
+ if (value < leaveCritical) return "critical";
13581
+ if (value < leaveElevated) return "elevated";
13582
+ return "ok";
13583
+ }
13584
+ if (previous === "elevated") {
13585
+ if (value < enterCritical) return "critical";
13586
+ if (value < leaveElevated) return "elevated";
13587
+ return "ok";
13588
+ }
13589
+ if (value < enterCritical) return "critical";
13590
+ if (value < enterElevated) return "elevated";
13591
+ return "ok";
13592
+ }
13593
+ function schmittHighIsWorse(value, previous, enterElevated, leaveElevated, enterCritical, leaveCritical) {
13594
+ if (previous === "critical") {
13595
+ if (value > leaveCritical) return "critical";
13596
+ if (value > leaveElevated) return "elevated";
13597
+ return "ok";
13598
+ }
13599
+ if (previous === "elevated") {
13600
+ if (value > enterCritical) return "critical";
13601
+ if (value > leaveElevated) return "elevated";
13602
+ return "ok";
13603
+ }
13604
+ if (value > enterCritical) return "critical";
13605
+ if (value > enterElevated) return "elevated";
13606
+ return "ok";
13607
+ }
13608
+ function timesTotal(times) {
13609
+ return times.user + times.nice + times.sys + times.idle + times.irq;
13610
+ }
13611
+ function hostPressureOs(platform3) {
13612
+ if (platform3 === "darwin" || platform3 === "linux" || platform3 === "win32") return platform3;
13613
+ return void 0;
13614
+ }
13615
+ function cpuBusyRatio(previous, next) {
13616
+ if (!previous || previous.length === 0 || next.length === 0 || previous.length !== next.length) {
13617
+ return 0;
13618
+ }
13619
+ let idle = 0;
13620
+ let total = 0;
13621
+ for (let i = 0; i < next.length; i++) {
13622
+ const dt = timesTotal(next[i]) - timesTotal(previous[i]);
13623
+ if (dt <= 0) continue;
13624
+ total += dt;
13625
+ idle += Math.max(0, next[i].idle - previous[i].idle);
13626
+ }
13627
+ if (total <= 0) return 0;
13628
+ return 1 - idle / total;
13629
+ }
13630
+ function classifyHostPressure(sample, previous, platform3) {
13631
+ const memRaw = schmittLowIsWorse(
13632
+ sample.memFreeRatio,
13633
+ previous,
13634
+ HOST_PRESSURE_BARS.memFreeRatio.enterElevated,
13635
+ HOST_PRESSURE_BARS.memFreeRatio.leaveElevated,
13636
+ HOST_PRESSURE_BARS.memFreeRatio.enterCritical,
13637
+ HOST_PRESSURE_BARS.memFreeRatio.leaveCritical
13638
+ );
13639
+ const mem = platform3 === "darwin" && memRaw === "critical" ? "elevated" : memRaw;
13640
+ const eventLoop = schmittHighIsWorse(
13641
+ sample.eventLoopP99Ms,
13642
+ previous,
13643
+ HOST_PRESSURE_BARS.eventLoopP99Ms.enterElevated,
13644
+ HOST_PRESSURE_BARS.eventLoopP99Ms.leaveElevated,
13645
+ HOST_PRESSURE_BARS.eventLoopP99Ms.enterCritical,
13646
+ HOST_PRESSURE_BARS.eventLoopP99Ms.leaveCritical
13647
+ );
13648
+ const ncpu = sample.ncpu > 0 ? sample.ncpu : 1;
13649
+ const windows = platform3 === "win32";
13650
+ const loadValue = windows ? sample.cpuBusyRatio ?? 0 : sample.load1 / ncpu;
13651
+ const loadBars = windows ? HOST_PRESSURE_BARS.cpuBusy : HOST_PRESSURE_BARS.loadPerCpu;
13652
+ const load = schmittHighIsWorse(
13653
+ loadValue,
13654
+ previous,
13655
+ loadBars.enterElevated,
13656
+ loadBars.leaveElevated,
13657
+ loadBars.enterCritical,
13658
+ loadBars.leaveCritical
13659
+ );
13660
+ const resourceLevel = worst([mem, eventLoop, load]);
13661
+ const agentsPair = sample.liveAgents >= HOST_PRESSURE_BARS.liveAgentsPair && resourceLevel !== "ok";
13662
+ const level = resourceLevel;
13663
+ const firing = [];
13664
+ if (mem !== "ok") firing.push("memory");
13665
+ if (eventLoop !== "ok") firing.push("event_loop");
13666
+ if (load !== "ok") firing.push("load");
13667
+ if (agentsPair) firing.push("agents");
13668
+ const reasons = REASON_ORDER.filter((reason) => firing.includes(reason));
13669
+ return { level, reasons };
13670
+ }
13671
+ var HostPressureMonitor = class {
13672
+ constructor(opts) {
13673
+ this.opts = opts;
13674
+ }
13675
+ opts;
13676
+ timer = null;
13677
+ level = "ok";
13678
+ lastWarning = null;
13679
+ start() {
13680
+ this.opts.histogram?.enable();
13681
+ if (this.timer) return;
13682
+ const intervalMs = this.opts.intervalMs ?? HOST_PRESSURE_SAMPLE_MS;
13683
+ this.timer = setInterval(() => this.tick(), intervalMs);
13684
+ this.timer.unref?.();
13685
+ }
13686
+ tick() {
13687
+ const sample = this.opts.readSample();
13688
+ const platform3 = this.opts.platform ?? process.platform;
13689
+ const classified = classifyHostPressure(sample, this.level, platform3);
13690
+ if (classified.level === this.level) return;
13691
+ this.level = classified.level;
13692
+ const updatedAt = (this.opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
13693
+ if (classified.level === "ok") {
13694
+ this.lastWarning = null;
13695
+ this.opts.wsHub.broadcast({ type: "host_pressure_cleared", updatedAt });
13696
+ return;
13697
+ }
13698
+ const os2 = hostPressureOs(platform3);
13699
+ const message = {
13700
+ type: "host_pressure",
13701
+ level: classified.level,
13702
+ reasons: classified.reasons,
13703
+ liveAgents: sample.liveAgents,
13704
+ updatedAt,
13705
+ ...os2 ? { os: os2 } : {}
13706
+ };
13707
+ this.lastWarning = message;
13708
+ this.opts.wsHub.broadcast(message);
13709
+ }
13710
+ wsMessage() {
13711
+ return this.lastWarning;
13712
+ }
13713
+ dispose() {
13714
+ if (this.timer) {
13715
+ clearInterval(this.timer);
13716
+ this.timer = null;
13717
+ }
13718
+ this.opts.histogram?.disable();
13719
+ }
13720
+ };
13721
+ function createHostPressureMonitor(wsHub, liveAgents) {
13722
+ const histogram = monitorEventLoopDelay({ resolution: 20 });
13723
+ const windows = process.platform === "win32";
13724
+ let prevCpuTimes = null;
13725
+ const monitor = new HostPressureMonitor({
13726
+ wsHub,
13727
+ histogram,
13728
+ readSample: () => {
13729
+ const eventLoopP99Ms = histogram.percentile(99) / 1e6;
13730
+ histogram.reset();
13731
+ const total = totalmem();
13732
+ const cpuList = cpus();
13733
+ const sample = {
13734
+ liveAgents: liveAgents(),
13735
+ memFreeRatio: total > 0 ? freemem() / total : 1,
13736
+ eventLoopP99Ms,
13737
+ load1: loadavg()[0],
13738
+ ncpu: cpuList.length
13739
+ };
13740
+ if (windows) {
13741
+ const cpuTimes = cpuList.map((cpu) => cpu.times);
13742
+ sample.cpuBusyRatio = cpuBusyRatio(prevCpuTimes, cpuTimes);
13743
+ prevCpuTimes = cpuTimes;
13744
+ }
13745
+ return sample;
13746
+ }
13747
+ });
13748
+ monitor.start();
13749
+ return monitor;
13750
+ }
13751
+
13529
13752
  // src/services/push/expoPushSender.ts
13530
13753
  var log5 = getLogger("expo-push");
13531
13754
  var EXPO_PUSH_ENDPOINT = "https://exp.host/--/api/v2/push/send";
@@ -15574,6 +15797,7 @@ var StreamerServer = class {
15574
15797
  wsToClientId = /* @__PURE__ */ new Map();
15575
15798
  cache = null;
15576
15799
  cacheMonitor = null;
15800
+ hostPressureMonitor = null;
15577
15801
  projectsRepo = null;
15578
15802
  conversationsRepo = null;
15579
15803
  sessionsRepo = null;
@@ -15924,6 +16148,7 @@ var StreamerServer = class {
15924
16148
  // reset-and-rescan, and tests swap methods on the server instance.
15925
16149
  cache: () => this.cache,
15926
16150
  cacheMonitor: () => this.cacheMonitor,
16151
+ hostPressureMonitor: () => this.hostPressureMonitor,
15927
16152
  pushRepo: () => this.pushRepo,
15928
16153
  liveActivityPushEnabled: () => this.liveActivityNotifier !== null,
15929
16154
  devicesRepo: () => this.devicesRepo,
@@ -16398,6 +16623,10 @@ var StreamerServer = class {
16398
16623
  this.idleReaperTimer = setInterval(() => this.reapIdleSessions(), IDLE_REAP_SWEEP_MS);
16399
16624
  this.idleReaperTimer.unref?.();
16400
16625
  }
16626
+ this.hostPressureMonitor = createHostPressureMonitor(
16627
+ this.wsHub,
16628
+ () => this.ptyAttachedIds().size
16629
+ );
16401
16630
  const warmUp = new Promise((resolveWarm) => {
16402
16631
  {
16403
16632
  this.log.info(`Streamer server listening on port ${port}`, {
@@ -16710,6 +16939,8 @@ var StreamerServer = class {
16710
16939
  clearInterval(this.idleReaperTimer);
16711
16940
  this.idleReaperTimer = null;
16712
16941
  }
16942
+ this.hostPressureMonitor?.dispose();
16943
+ this.hostPressureMonitor = null;
16713
16944
  this.lastAgentChunkAt.clear();
16714
16945
  this.terminalSeq.clear();
16715
16946
  if (this.ptyManager.isRemote()) this.ptyManager.dispose();