@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.cjs CHANGED
@@ -4136,7 +4136,7 @@ var import_events = require("events");
4136
4136
  var import_fs28 = require("fs");
4137
4137
  var import_promises7 = require("fs/promises");
4138
4138
  var import_http = require("http");
4139
- var import_os14 = require("os");
4139
+ var import_os15 = require("os");
4140
4140
  var import_path24 = require("path");
4141
4141
 
4142
4142
  // src/api/app.ts
@@ -4979,7 +4979,7 @@ function redactPath(path) {
4979
4979
  function worstStatus(checks) {
4980
4980
  const rank = { ok: 0, unknown: 1, degraded: 2, failed: 3 };
4981
4981
  return checks.reduce(
4982
- (worst, c) => rank[c.status] > rank[worst] ? c.status : worst,
4982
+ (worst2, c) => rank[c.status] > rank[worst2] ? c.status : worst2,
4983
4983
  "ok"
4984
4984
  );
4985
4985
  }
@@ -5994,7 +5994,12 @@ var createMiscRoutes = (deps) => {
5994
5994
  // Whether to encrypt to this server. Additive, same contract as `push`:
5995
5995
  // absent means an older server, which a client must read as "unknown" and
5996
5996
  // resolve as today's plaintext path — never as a reason to fail.
5997
- e2ee: describeE2eeCapability(deps.featureFlagsConfig().values.e2ee)
5997
+ e2ee: describeE2eeCapability(deps.featureFlagsConfig().values.e2ee),
5998
+ // This build samples cheap host signals and pushes `host_pressure` when
5999
+ // the box is starved. Additive capability flag only — live readings stay
6000
+ // off this polled endpoint. Absent means an older server that never
6001
+ // samples. Informational: pressure never holds, kills, or refuses sessions.
6002
+ hostPressure: true
5998
6003
  });
5999
6004
  });
6000
6005
  app.get("/api/profiles", (c) => c.json([]));
@@ -12880,6 +12885,7 @@ function createApiDeps(deps) {
12880
12885
  wsHub: deps.wsHub,
12881
12886
  cache: () => deps.cache(),
12882
12887
  cacheMonitor: () => deps.cacheMonitor(),
12888
+ hostPressureMonitor: () => deps.hostPressureMonitor(),
12883
12889
  pushRepo: () => deps.pushRepo(),
12884
12890
  liveActivityPushEnabled: () => deps.liveActivityPushEnabled(),
12885
12891
  devicesRepo: () => deps.devicesRepo(),
@@ -12930,6 +12936,8 @@ function createApiDeps(deps) {
12930
12936
  }
12931
12937
  const alertMsg = deps.cacheMonitor()?.wsMessage();
12932
12938
  if (alertMsg) deps.wsHub.unicast(ws, alertMsg);
12939
+ const pressureMsg = deps.hostPressureMonitor()?.wsMessage();
12940
+ if (pressureMsg) deps.wsHub.unicast(ws, pressureMsg);
12933
12941
  },
12934
12942
  handleWsMessage: async (ws, raw, principal) => {
12935
12943
  const deny = (type, required) => {
@@ -13555,6 +13563,221 @@ function pruneAgentConversations(cache) {
13555
13563
  return { scanned: rows.length, pruned, missing };
13556
13564
  }
13557
13565
 
13566
+ // src/services/host-pressure/hostPressure.ts
13567
+ var import_os13 = require("os");
13568
+ var import_perf_hooks = require("perf_hooks");
13569
+ var HOST_PRESSURE_SAMPLE_MS = 5e3;
13570
+ var HOST_PRESSURE_BARS = {
13571
+ memFreeRatio: {
13572
+ enterElevated: 0.15,
13573
+ leaveElevated: 0.17,
13574
+ enterCritical: 0.08,
13575
+ leaveCritical: 0.15
13576
+ },
13577
+ eventLoopP99Ms: {
13578
+ enterElevated: 100,
13579
+ leaveElevated: 80,
13580
+ enterCritical: 250,
13581
+ leaveCritical: 100
13582
+ },
13583
+ loadPerCpu: {
13584
+ enterElevated: 1.25,
13585
+ leaveElevated: 1.05,
13586
+ enterCritical: 2,
13587
+ leaveCritical: 1.25
13588
+ },
13589
+ // win32 has no loadavg. Busy ratio from os.cpus()[].times deltas is 0–1, so
13590
+ // it cannot reuse loadPerCpu's 2.0 critical bar. Reason on the wire stays `load`.
13591
+ cpuBusy: {
13592
+ enterElevated: 0.85,
13593
+ leaveElevated: 0.7,
13594
+ enterCritical: 0.97,
13595
+ leaveCritical: 0.85
13596
+ },
13597
+ liveAgentsPair: 4
13598
+ };
13599
+ var REASON_ORDER = ["memory", "event_loop", "load", "agents"];
13600
+ var RANK = { ok: 0, elevated: 1, critical: 2 };
13601
+ function worst(levels) {
13602
+ return levels.reduce(
13603
+ (acc, level) => RANK[level] > RANK[acc] ? level : acc,
13604
+ "ok"
13605
+ );
13606
+ }
13607
+ function schmittLowIsWorse(value, previous, enterElevated, leaveElevated, enterCritical, leaveCritical) {
13608
+ if (previous === "critical") {
13609
+ if (value < leaveCritical) return "critical";
13610
+ if (value < leaveElevated) return "elevated";
13611
+ return "ok";
13612
+ }
13613
+ if (previous === "elevated") {
13614
+ if (value < enterCritical) return "critical";
13615
+ if (value < leaveElevated) return "elevated";
13616
+ return "ok";
13617
+ }
13618
+ if (value < enterCritical) return "critical";
13619
+ if (value < enterElevated) return "elevated";
13620
+ return "ok";
13621
+ }
13622
+ function schmittHighIsWorse(value, previous, enterElevated, leaveElevated, enterCritical, leaveCritical) {
13623
+ if (previous === "critical") {
13624
+ if (value > leaveCritical) return "critical";
13625
+ if (value > leaveElevated) return "elevated";
13626
+ return "ok";
13627
+ }
13628
+ if (previous === "elevated") {
13629
+ if (value > enterCritical) return "critical";
13630
+ if (value > leaveElevated) return "elevated";
13631
+ return "ok";
13632
+ }
13633
+ if (value > enterCritical) return "critical";
13634
+ if (value > enterElevated) return "elevated";
13635
+ return "ok";
13636
+ }
13637
+ function timesTotal(times) {
13638
+ return times.user + times.nice + times.sys + times.idle + times.irq;
13639
+ }
13640
+ function hostPressureOs(platform3) {
13641
+ if (platform3 === "darwin" || platform3 === "linux" || platform3 === "win32") return platform3;
13642
+ return void 0;
13643
+ }
13644
+ function cpuBusyRatio(previous, next) {
13645
+ if (!previous || previous.length === 0 || next.length === 0 || previous.length !== next.length) {
13646
+ return 0;
13647
+ }
13648
+ let idle = 0;
13649
+ let total = 0;
13650
+ for (let i = 0; i < next.length; i++) {
13651
+ const dt = timesTotal(next[i]) - timesTotal(previous[i]);
13652
+ if (dt <= 0) continue;
13653
+ total += dt;
13654
+ idle += Math.max(0, next[i].idle - previous[i].idle);
13655
+ }
13656
+ if (total <= 0) return 0;
13657
+ return 1 - idle / total;
13658
+ }
13659
+ function classifyHostPressure(sample, previous, platform3) {
13660
+ const memRaw = schmittLowIsWorse(
13661
+ sample.memFreeRatio,
13662
+ previous,
13663
+ HOST_PRESSURE_BARS.memFreeRatio.enterElevated,
13664
+ HOST_PRESSURE_BARS.memFreeRatio.leaveElevated,
13665
+ HOST_PRESSURE_BARS.memFreeRatio.enterCritical,
13666
+ HOST_PRESSURE_BARS.memFreeRatio.leaveCritical
13667
+ );
13668
+ const mem = platform3 === "darwin" && memRaw === "critical" ? "elevated" : memRaw;
13669
+ const eventLoop = schmittHighIsWorse(
13670
+ sample.eventLoopP99Ms,
13671
+ previous,
13672
+ HOST_PRESSURE_BARS.eventLoopP99Ms.enterElevated,
13673
+ HOST_PRESSURE_BARS.eventLoopP99Ms.leaveElevated,
13674
+ HOST_PRESSURE_BARS.eventLoopP99Ms.enterCritical,
13675
+ HOST_PRESSURE_BARS.eventLoopP99Ms.leaveCritical
13676
+ );
13677
+ const ncpu = sample.ncpu > 0 ? sample.ncpu : 1;
13678
+ const windows = platform3 === "win32";
13679
+ const loadValue = windows ? sample.cpuBusyRatio ?? 0 : sample.load1 / ncpu;
13680
+ const loadBars = windows ? HOST_PRESSURE_BARS.cpuBusy : HOST_PRESSURE_BARS.loadPerCpu;
13681
+ const load = schmittHighIsWorse(
13682
+ loadValue,
13683
+ previous,
13684
+ loadBars.enterElevated,
13685
+ loadBars.leaveElevated,
13686
+ loadBars.enterCritical,
13687
+ loadBars.leaveCritical
13688
+ );
13689
+ const resourceLevel = worst([mem, eventLoop, load]);
13690
+ const agentsPair = sample.liveAgents >= HOST_PRESSURE_BARS.liveAgentsPair && resourceLevel !== "ok";
13691
+ const level = resourceLevel;
13692
+ const firing = [];
13693
+ if (mem !== "ok") firing.push("memory");
13694
+ if (eventLoop !== "ok") firing.push("event_loop");
13695
+ if (load !== "ok") firing.push("load");
13696
+ if (agentsPair) firing.push("agents");
13697
+ const reasons = REASON_ORDER.filter((reason) => firing.includes(reason));
13698
+ return { level, reasons };
13699
+ }
13700
+ var HostPressureMonitor = class {
13701
+ constructor(opts) {
13702
+ this.opts = opts;
13703
+ }
13704
+ opts;
13705
+ timer = null;
13706
+ level = "ok";
13707
+ lastWarning = null;
13708
+ start() {
13709
+ this.opts.histogram?.enable();
13710
+ if (this.timer) return;
13711
+ const intervalMs = this.opts.intervalMs ?? HOST_PRESSURE_SAMPLE_MS;
13712
+ this.timer = setInterval(() => this.tick(), intervalMs);
13713
+ this.timer.unref?.();
13714
+ }
13715
+ tick() {
13716
+ const sample = this.opts.readSample();
13717
+ const platform3 = this.opts.platform ?? process.platform;
13718
+ const classified = classifyHostPressure(sample, this.level, platform3);
13719
+ if (classified.level === this.level) return;
13720
+ this.level = classified.level;
13721
+ const updatedAt = (this.opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
13722
+ if (classified.level === "ok") {
13723
+ this.lastWarning = null;
13724
+ this.opts.wsHub.broadcast({ type: "host_pressure_cleared", updatedAt });
13725
+ return;
13726
+ }
13727
+ const os2 = hostPressureOs(platform3);
13728
+ const message = {
13729
+ type: "host_pressure",
13730
+ level: classified.level,
13731
+ reasons: classified.reasons,
13732
+ liveAgents: sample.liveAgents,
13733
+ updatedAt,
13734
+ ...os2 ? { os: os2 } : {}
13735
+ };
13736
+ this.lastWarning = message;
13737
+ this.opts.wsHub.broadcast(message);
13738
+ }
13739
+ wsMessage() {
13740
+ return this.lastWarning;
13741
+ }
13742
+ dispose() {
13743
+ if (this.timer) {
13744
+ clearInterval(this.timer);
13745
+ this.timer = null;
13746
+ }
13747
+ this.opts.histogram?.disable();
13748
+ }
13749
+ };
13750
+ function createHostPressureMonitor(wsHub, liveAgents) {
13751
+ const histogram = (0, import_perf_hooks.monitorEventLoopDelay)({ resolution: 20 });
13752
+ const windows = process.platform === "win32";
13753
+ let prevCpuTimes = null;
13754
+ const monitor = new HostPressureMonitor({
13755
+ wsHub,
13756
+ histogram,
13757
+ readSample: () => {
13758
+ const eventLoopP99Ms = histogram.percentile(99) / 1e6;
13759
+ histogram.reset();
13760
+ const total = (0, import_os13.totalmem)();
13761
+ const cpuList = (0, import_os13.cpus)();
13762
+ const sample = {
13763
+ liveAgents: liveAgents(),
13764
+ memFreeRatio: total > 0 ? (0, import_os13.freemem)() / total : 1,
13765
+ eventLoopP99Ms,
13766
+ load1: (0, import_os13.loadavg)()[0],
13767
+ ncpu: cpuList.length
13768
+ };
13769
+ if (windows) {
13770
+ const cpuTimes = cpuList.map((cpu) => cpu.times);
13771
+ sample.cpuBusyRatio = cpuBusyRatio(prevCpuTimes, cpuTimes);
13772
+ prevCpuTimes = cpuTimes;
13773
+ }
13774
+ return sample;
13775
+ }
13776
+ });
13777
+ monitor.start();
13778
+ return monitor;
13779
+ }
13780
+
13558
13781
  // src/services/push/expoPushSender.ts
13559
13782
  var log5 = getLogger("expo-push");
13560
13783
  var EXPO_PUSH_ENDPOINT = "https://exp.host/--/api/v2/push/send";
@@ -15054,7 +15277,7 @@ function discoveredToResponse(d, conversationId) {
15054
15277
 
15055
15278
  // src/session-watchers.ts
15056
15279
  var import_fs27 = require("fs");
15057
- var import_os13 = require("os");
15280
+ var import_os14 = require("os");
15058
15281
  var import_path23 = require("path");
15059
15282
  var SessionWatchers = class {
15060
15283
  constructor(deps) {
@@ -15136,7 +15359,7 @@ var SessionWatchers = class {
15136
15359
  // was passed to Claude via --session-id so the filename matches from the start.
15137
15360
  watchForJsonl(sessionId, projectPath) {
15138
15361
  const encoded = projectPath.replace(/[/\\:.]/g, "-");
15139
- const projectsDir = (0, import_path23.join)((0, import_os13.homedir)(), ".claude", "projects", encoded);
15362
+ const projectsDir = (0, import_path23.join)((0, import_os14.homedir)(), ".claude", "projects", encoded);
15140
15363
  const expectedFile = `${sessionId}.jsonl`;
15141
15364
  const filePath = (0, import_path23.join)(projectsDir, expectedFile);
15142
15365
  const deadline = Date.now() + 12e4;
@@ -15603,6 +15826,7 @@ var StreamerServer = class {
15603
15826
  wsToClientId = /* @__PURE__ */ new Map();
15604
15827
  cache = null;
15605
15828
  cacheMonitor = null;
15829
+ hostPressureMonitor = null;
15606
15830
  projectsRepo = null;
15607
15831
  conversationsRepo = null;
15608
15832
  sessionsRepo = null;
@@ -15667,7 +15891,7 @@ var StreamerServer = class {
15667
15891
  this.skipStartupWarmup = config.skipStartupWarmup ?? false;
15668
15892
  this.autoResumeOnBoot = config.autoResumeOnBoot ?? false;
15669
15893
  this.scanProfiles = config.scanProfiles;
15670
- this.codexRoots = config.codexRoots ?? [(0, import_path24.join)((0, import_os14.homedir)(), ".codex", "sessions")];
15894
+ this.codexRoots = config.codexRoots ?? [(0, import_path24.join)((0, import_os15.homedir)(), ".codex", "sessions")];
15671
15895
  this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
15672
15896
  this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
15673
15897
  const flagResolution = resolveFeatureFlags({
@@ -15684,7 +15908,7 @@ var StreamerServer = class {
15684
15908
  this.claudeFlagsPersistable = config.claudeFlags === void 0;
15685
15909
  this.claudeFlags = config.claudeFlags ?? loadClaudeFlags();
15686
15910
  this.claudeExtraArgs = config.claudeExtraArgs ?? loadClaudeExtraArgs();
15687
- this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path24.join)((0, import_os14.homedir)(), ".threadbase", "cache");
15911
+ this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path24.join)((0, import_os15.homedir)(), ".threadbase", "cache");
15688
15912
  this.runtimeDbPath = resolveRuntimeDbPath(config.runtimeDbPath);
15689
15913
  this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
15690
15914
  this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
@@ -15953,6 +16177,7 @@ var StreamerServer = class {
15953
16177
  // reset-and-rescan, and tests swap methods on the server instance.
15954
16178
  cache: () => this.cache,
15955
16179
  cacheMonitor: () => this.cacheMonitor,
16180
+ hostPressureMonitor: () => this.hostPressureMonitor,
15956
16181
  pushRepo: () => this.pushRepo,
15957
16182
  liveActivityPushEnabled: () => this.liveActivityNotifier !== null,
15958
16183
  devicesRepo: () => this.devicesRepo,
@@ -16123,14 +16348,14 @@ var StreamerServer = class {
16123
16348
  }
16124
16349
  this.apnsClient = new ApnsClient(creds);
16125
16350
  const sender = new LiveActivitySender(this.apnsClient, pushRepo);
16126
- const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os14.hostname)();
16127
- this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0, import_os14.hostname)());
16351
+ const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os15.hostname)();
16352
+ this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0, import_os15.hostname)());
16128
16353
  this.liveActivityRenewal = new LiveActivityRenewalScheduler({
16129
16354
  repo: pushRepo,
16130
16355
  sender,
16131
16356
  sessionStore: this.sessionStore,
16132
16357
  serverId,
16133
- serverLabel: (0, import_os14.hostname)()
16358
+ serverLabel: (0, import_os15.hostname)()
16134
16359
  });
16135
16360
  this.liveActivityRenewal.start();
16136
16361
  this.log.info("Live Activity push enabled", {
@@ -16150,7 +16375,7 @@ var StreamerServer = class {
16150
16375
  */
16151
16376
  initWaitingInputPush(pushRepo) {
16152
16377
  const sender = new ExpoPushSender(pushRepo, process.env.THREADBASE_EXPO_ACCESS_TOKEN);
16153
- const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os14.hostname)();
16378
+ const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os15.hostname)();
16154
16379
  this.waitingInputNotifier = new WaitingInputNotifier(
16155
16380
  sender,
16156
16381
  serverId,
@@ -16380,7 +16605,7 @@ var StreamerServer = class {
16380
16605
  let sessions = null;
16381
16606
  for (let attempt = 0; attempt < 2; attempt += 1) {
16382
16607
  const transport = await connectOrSpawnHost({
16383
- instanceId: process.env.THREADBASE_INSTANCE_ID ?? (0, import_os14.hostname)()
16608
+ instanceId: process.env.THREADBASE_INSTANCE_ID ?? (0, import_os15.hostname)()
16384
16609
  });
16385
16610
  try {
16386
16611
  sessions = await this.ptyManager.useRemoteRunner(transport);
@@ -16427,6 +16652,10 @@ var StreamerServer = class {
16427
16652
  this.idleReaperTimer = setInterval(() => this.reapIdleSessions(), IDLE_REAP_SWEEP_MS);
16428
16653
  this.idleReaperTimer.unref?.();
16429
16654
  }
16655
+ this.hostPressureMonitor = createHostPressureMonitor(
16656
+ this.wsHub,
16657
+ () => this.ptyAttachedIds().size
16658
+ );
16430
16659
  const warmUp = new Promise((resolveWarm) => {
16431
16660
  {
16432
16661
  this.log.info(`Streamer server listening on port ${port}`, {
@@ -16739,6 +16968,8 @@ var StreamerServer = class {
16739
16968
  clearInterval(this.idleReaperTimer);
16740
16969
  this.idleReaperTimer = null;
16741
16970
  }
16971
+ this.hostPressureMonitor?.dispose();
16972
+ this.hostPressureMonitor = null;
16742
16973
  this.lastAgentChunkAt.clear();
16743
16974
  this.terminalSeq.clear();
16744
16975
  if (this.ptyManager.isRemote()) this.ptyManager.dispose();
@@ -16932,7 +17163,7 @@ var StreamerServer = class {
16932
17163
  deviceToken: device.deviceToken,
16933
17164
  capabilities: device.capabilities,
16934
17165
  publicUrl: this.publicUrl,
16935
- machineName: (0, import_os14.hostname)(),
17166
+ machineName: (0, import_os15.hostname)(),
16936
17167
  serverVersion: getVersion()
16937
17168
  })
16938
17169
  );
@@ -16966,7 +17197,7 @@ var StreamerServer = class {
16966
17197
  // Released builds that predate that fix DO still adopt it, so changing
16967
17198
  // this value moves where old devices talk. It is not a free field.
16968
17199
  publicUrl: this.publicUrl,
16969
- machineName: (0, import_os14.hostname)(),
17200
+ machineName: (0, import_os15.hostname)(),
16970
17201
  ...device && {
16971
17202
  deviceId: device.deviceId,
16972
17203
  deviceToken: device.deviceToken,