@threadbase-sh/streamer 1.64.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/api/handlers/sessions.handlers.d.ts +6 -0
- package/dist/api/handlers/sessions.handlers.d.ts.map +1 -1
- package/dist/api/routes/misc.routes.d.ts.map +1 -1
- package/dist/api/types/api-deps.d.ts +2 -0
- package/dist/api/types/api-deps.d.ts.map +1 -1
- package/dist/cli.cjs +346 -16
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +346 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +334 -4
- package/dist/index.js.map +1 -1
- package/dist/server-wiring.d.ts +7 -0
- package/dist/server-wiring.d.ts.map +1 -1
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/services/host-pressure/hostPressure.d.ts +83 -0
- package/dist/services/host-pressure/hostPressure.d.ts.map +1 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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
|
-
(
|
|
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([]));
|
|
@@ -10620,6 +10625,19 @@ var SessionHandlers = class {
|
|
|
10620
10625
|
sessions: this.sessionStore.list(this.deps.ptyAttachedIds())
|
|
10621
10626
|
});
|
|
10622
10627
|
}
|
|
10628
|
+
/**
|
|
10629
|
+
* Same empty-unused check stop uses (promptCount === 0 and no cache row,
|
|
10630
|
+
* including boundConversationId / resumedFromConversationId). Safe after
|
|
10631
|
+
* putOnHold: the runner may already have dropped the live session.
|
|
10632
|
+
*/
|
|
10633
|
+
forgetIfEmptyUnused(sessionId) {
|
|
10634
|
+
const live = this.ptyManager.getSession(sessionId);
|
|
10635
|
+
const stored = this.sessionStore.getManaged(sessionId);
|
|
10636
|
+
const session = live ?? stored;
|
|
10637
|
+
if (!session) return;
|
|
10638
|
+
if (!this.shouldForgetEmptySession(session)) return;
|
|
10639
|
+
this.forgetEmptyStoppedSession(sessionId);
|
|
10640
|
+
}
|
|
10623
10641
|
async handleAdopt(sessionId, res) {
|
|
10624
10642
|
const discovered = await discoverClaudeProcesses();
|
|
10625
10643
|
this.sessionStore.setDiscovered(discovered);
|
|
@@ -12814,6 +12832,7 @@ function createLiveSessionOptions(deps) {
|
|
|
12814
12832
|
void deps.liveActivityNotifier()?.onStatusChange(session, previousStatus);
|
|
12815
12833
|
void deps.waitingInputNotifier()?.onStatusChange(session, previousStatus);
|
|
12816
12834
|
deps.sessionStatusBus.emit(`status:${session.id}`, session.status, session);
|
|
12835
|
+
deps.maybeFireHoldWhenIdle?.(session);
|
|
12817
12836
|
}
|
|
12818
12837
|
};
|
|
12819
12838
|
}
|
|
@@ -12837,6 +12856,7 @@ function createApiDeps(deps) {
|
|
|
12837
12856
|
wsHub: deps.wsHub,
|
|
12838
12857
|
cache: () => deps.cache(),
|
|
12839
12858
|
cacheMonitor: () => deps.cacheMonitor(),
|
|
12859
|
+
hostPressureMonitor: () => deps.hostPressureMonitor(),
|
|
12840
12860
|
pushRepo: () => deps.pushRepo(),
|
|
12841
12861
|
liveActivityPushEnabled: () => deps.liveActivityPushEnabled(),
|
|
12842
12862
|
devicesRepo: () => deps.devicesRepo(),
|
|
@@ -12887,6 +12907,8 @@ function createApiDeps(deps) {
|
|
|
12887
12907
|
}
|
|
12888
12908
|
const alertMsg = deps.cacheMonitor()?.wsMessage();
|
|
12889
12909
|
if (alertMsg) deps.wsHub.unicast(ws, alertMsg);
|
|
12910
|
+
const pressureMsg = deps.hostPressureMonitor()?.wsMessage();
|
|
12911
|
+
if (pressureMsg) deps.wsHub.unicast(ws, pressureMsg);
|
|
12890
12912
|
},
|
|
12891
12913
|
handleWsMessage: async (ws, raw, principal) => {
|
|
12892
12914
|
const deny = (type, required) => {
|
|
@@ -12962,7 +12984,20 @@ function createApiDeps(deps) {
|
|
|
12962
12984
|
deny(msg.type, "session:control");
|
|
12963
12985
|
return;
|
|
12964
12986
|
}
|
|
12965
|
-
|
|
12987
|
+
const when = msg.when;
|
|
12988
|
+
if (when === void 0 || when === "grace") {
|
|
12989
|
+
deps.startGraceTimer(msg.sessionId, deps.ptyGracePeriodMs);
|
|
12990
|
+
return;
|
|
12991
|
+
}
|
|
12992
|
+
if (when === "waiting_input") {
|
|
12993
|
+
deps.armHoldWhenIdle(msg.sessionId);
|
|
12994
|
+
return;
|
|
12995
|
+
}
|
|
12996
|
+
deps.log().warn(`[pty.hold_when_unknown] hold_session when=${String(when)}`, {
|
|
12997
|
+
event: "pty.hold_when_unknown",
|
|
12998
|
+
sessionId: msg.sessionId,
|
|
12999
|
+
when
|
|
13000
|
+
});
|
|
12966
13001
|
}
|
|
12967
13002
|
} catch {
|
|
12968
13003
|
}
|
|
@@ -13499,6 +13534,221 @@ function pruneAgentConversations(cache) {
|
|
|
13499
13534
|
return { scanned: rows.length, pruned, missing };
|
|
13500
13535
|
}
|
|
13501
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
|
+
|
|
13502
13752
|
// src/services/push/expoPushSender.ts
|
|
13503
13753
|
var log5 = getLogger("expo-push");
|
|
13504
13754
|
var EXPO_PUSH_ENDPOINT = "https://exp.host/--/api/v2/push/send";
|
|
@@ -15518,6 +15768,10 @@ var StreamerServer = class {
|
|
|
15518
15768
|
// Consecutive grace-timer defers for a still-`running` session (see
|
|
15519
15769
|
// GRACE_MAX_DEFERS). Reset when a subscriber reconnects or the PTY settles.
|
|
15520
15770
|
ptyGraceDeferCounts = /* @__PURE__ */ new Map();
|
|
15771
|
+
// Session ids that should be putOnHold at the next waiting_input/idle.
|
|
15772
|
+
// Same lifetime as ptyGraceTimers: in-memory, dropped on close/restart.
|
|
15773
|
+
// Last writer wins against the grace timer — never both armed.
|
|
15774
|
+
holdWhenIdle = /* @__PURE__ */ new Set();
|
|
15521
15775
|
// Map of sessionId → set of subscribed WS clients
|
|
15522
15776
|
sessionSubscribers = /* @__PURE__ */ new Map();
|
|
15523
15777
|
// sessionId → wall-clock ms of the last PTY chunk. Written from onOutput for
|
|
@@ -15543,6 +15797,7 @@ var StreamerServer = class {
|
|
|
15543
15797
|
wsToClientId = /* @__PURE__ */ new Map();
|
|
15544
15798
|
cache = null;
|
|
15545
15799
|
cacheMonitor = null;
|
|
15800
|
+
hostPressureMonitor = null;
|
|
15546
15801
|
projectsRepo = null;
|
|
15547
15802
|
conversationsRepo = null;
|
|
15548
15803
|
sessionsRepo = null;
|
|
@@ -15745,7 +16000,8 @@ var StreamerServer = class {
|
|
|
15745
16000
|
waitingInputNotifier: () => this.waitingInputNotifier,
|
|
15746
16001
|
ptyAttachedIds: () => this.ptyAttachedIds(),
|
|
15747
16002
|
cancelPendingQuestion: (sessionId) => this.cancelPendingQuestion(sessionId),
|
|
15748
|
-
rememberSelfPtyEnded: (conversationId) => this.rememberSelfPtyEnded(conversationId)
|
|
16003
|
+
rememberSelfPtyEnded: (conversationId) => this.rememberSelfPtyEnded(conversationId),
|
|
16004
|
+
maybeFireHoldWhenIdle: (session) => this.maybeFireHoldWhenIdle(session)
|
|
15749
16005
|
})
|
|
15750
16006
|
);
|
|
15751
16007
|
this.sessionWatchers = new SessionWatchers({
|
|
@@ -15892,6 +16148,7 @@ var StreamerServer = class {
|
|
|
15892
16148
|
// reset-and-rescan, and tests swap methods on the server instance.
|
|
15893
16149
|
cache: () => this.cache,
|
|
15894
16150
|
cacheMonitor: () => this.cacheMonitor,
|
|
16151
|
+
hostPressureMonitor: () => this.hostPressureMonitor,
|
|
15895
16152
|
pushRepo: () => this.pushRepo,
|
|
15896
16153
|
liveActivityPushEnabled: () => this.liveActivityNotifier !== null,
|
|
15897
16154
|
devicesRepo: () => this.devicesRepo,
|
|
@@ -15908,6 +16165,7 @@ var StreamerServer = class {
|
|
|
15908
16165
|
currentWarmupState: () => this.currentWarmupState(),
|
|
15909
16166
|
addSessionSubscriber: (sessionId, ws) => this.addSessionSubscriber(sessionId, ws),
|
|
15910
16167
|
startGraceTimer: (sessionId, delayMs) => this.startGraceTimer(sessionId, delayMs),
|
|
16168
|
+
armHoldWhenIdle: (sessionId) => this.armHoldWhenIdle(sessionId),
|
|
15911
16169
|
handleSessionsCount: (res) => this.handleSessionsCount(res),
|
|
15912
16170
|
applyLiveSessionSetting: (id, req, res, setting) => this.applyLiveSessionSetting(id, req, res, setting),
|
|
15913
16171
|
handlePairStart: (res) => this.handlePairStart(res),
|
|
@@ -16027,6 +16285,13 @@ var StreamerServer = class {
|
|
|
16027
16285
|
this.ptyGraceTimers.delete(sessionId);
|
|
16028
16286
|
}
|
|
16029
16287
|
this.ptyGraceDeferCounts.delete(sessionId);
|
|
16288
|
+
if (this.holdWhenIdle.delete(sessionId)) {
|
|
16289
|
+
this.log.info(
|
|
16290
|
+
`[hold-when-idle] cancelled ${sessionId} (subscribe)`,
|
|
16291
|
+
{ sessionId, event: "pty.hold_when_idle_cancel", reason: "subscribe" },
|
|
16292
|
+
"pino"
|
|
16293
|
+
);
|
|
16294
|
+
}
|
|
16030
16295
|
}
|
|
16031
16296
|
/**
|
|
16032
16297
|
* Bring up Live Activity push, if credentials are present (Feature 12).
|
|
@@ -16134,6 +16399,7 @@ var StreamerServer = class {
|
|
|
16134
16399
|
return reaped;
|
|
16135
16400
|
}
|
|
16136
16401
|
startGraceTimer(sessionId, delayMs) {
|
|
16402
|
+
this.holdWhenIdle.delete(sessionId);
|
|
16137
16403
|
const existing = this.ptyGraceTimers.get(sessionId);
|
|
16138
16404
|
if (existing) clearTimeout(existing);
|
|
16139
16405
|
const timer = setTimeout(() => {
|
|
@@ -16175,6 +16441,63 @@ var StreamerServer = class {
|
|
|
16175
16441
|
}, delayMs);
|
|
16176
16442
|
this.ptyGraceTimers.set(sessionId, timer);
|
|
16177
16443
|
}
|
|
16444
|
+
clearGrace(sessionId) {
|
|
16445
|
+
const existing = this.ptyGraceTimers.get(sessionId);
|
|
16446
|
+
if (existing) {
|
|
16447
|
+
clearTimeout(existing);
|
|
16448
|
+
this.ptyGraceTimers.delete(sessionId);
|
|
16449
|
+
}
|
|
16450
|
+
this.ptyGraceDeferCounts.delete(sessionId);
|
|
16451
|
+
}
|
|
16452
|
+
/**
|
|
16453
|
+
* Arm the in-app "Kill on idle" latch: hold now if already settled, otherwise
|
|
16454
|
+
* on the next running → waiting_input (or idle). No grace delay, no defer cap.
|
|
16455
|
+
* A subscribed leaving socket must not block an immediate hold.
|
|
16456
|
+
*/
|
|
16457
|
+
armHoldWhenIdle(sessionId) {
|
|
16458
|
+
if (!this.ptyManager.hasSession(sessionId)) return;
|
|
16459
|
+
this.clearGrace(sessionId);
|
|
16460
|
+
const status = this.ptyManager.getSession(sessionId)?.status ?? this.sessionStore.getManaged(sessionId)?.status;
|
|
16461
|
+
if (status === "waiting_input" || status === "idle") {
|
|
16462
|
+
this.holdWhenIdle.delete(sessionId);
|
|
16463
|
+
this.ptyManager.putOnHold(sessionId);
|
|
16464
|
+
this.forgetIfEmptyUnused(sessionId);
|
|
16465
|
+
return;
|
|
16466
|
+
}
|
|
16467
|
+
this.holdWhenIdle.add(sessionId);
|
|
16468
|
+
this.log.info(
|
|
16469
|
+
`[hold-when-idle] armed ${sessionId}`,
|
|
16470
|
+
{ sessionId, event: "pty.hold_when_idle_armed" },
|
|
16471
|
+
"pino"
|
|
16472
|
+
);
|
|
16473
|
+
}
|
|
16474
|
+
/**
|
|
16475
|
+
* Fire the Kill-on-idle latch from the shared onStatusChange funnel.
|
|
16476
|
+
* Delete first so the ensuing idle transition cannot re-enter.
|
|
16477
|
+
*/
|
|
16478
|
+
maybeFireHoldWhenIdle(session) {
|
|
16479
|
+
if (session.status !== "waiting_input" && session.status !== "idle") return;
|
|
16480
|
+
if (!this.holdWhenIdle.has(session.id)) return;
|
|
16481
|
+
this.holdWhenIdle.delete(session.id);
|
|
16482
|
+
if (this.hasSessionSubscriber(session.id)) {
|
|
16483
|
+
this.log.info(
|
|
16484
|
+
`[hold-when-idle] cancelled ${session.id} (subscriber)`,
|
|
16485
|
+
{ sessionId: session.id, event: "pty.hold_when_idle_cancel", reason: "subscriber" },
|
|
16486
|
+
"pino"
|
|
16487
|
+
);
|
|
16488
|
+
return;
|
|
16489
|
+
}
|
|
16490
|
+
this.log.info(
|
|
16491
|
+
`[hold-when-idle] holding ${session.id}`,
|
|
16492
|
+
{ sessionId: session.id, event: "pty.hold_when_idle_fire" },
|
|
16493
|
+
"pino"
|
|
16494
|
+
);
|
|
16495
|
+
this.ptyManager.putOnHold(session.id);
|
|
16496
|
+
this.forgetIfEmptyUnused(session.id);
|
|
16497
|
+
}
|
|
16498
|
+
forgetIfEmptyUnused(sessionId) {
|
|
16499
|
+
this.sessionHandlers.forgetIfEmptyUnused(sessionId);
|
|
16500
|
+
}
|
|
16178
16501
|
get port() {
|
|
16179
16502
|
const addr = this.httpServer.address();
|
|
16180
16503
|
return typeof addr === "object" && addr ? addr.port : 0;
|
|
@@ -16300,6 +16623,10 @@ var StreamerServer = class {
|
|
|
16300
16623
|
this.idleReaperTimer = setInterval(() => this.reapIdleSessions(), IDLE_REAP_SWEEP_MS);
|
|
16301
16624
|
this.idleReaperTimer.unref?.();
|
|
16302
16625
|
}
|
|
16626
|
+
this.hostPressureMonitor = createHostPressureMonitor(
|
|
16627
|
+
this.wsHub,
|
|
16628
|
+
() => this.ptyAttachedIds().size
|
|
16629
|
+
);
|
|
16303
16630
|
const warmUp = new Promise((resolveWarm) => {
|
|
16304
16631
|
{
|
|
16305
16632
|
this.log.info(`Streamer server listening on port ${port}`, {
|
|
@@ -16607,10 +16934,13 @@ var StreamerServer = class {
|
|
|
16607
16934
|
async close() {
|
|
16608
16935
|
for (const timer of this.ptyGraceTimers.values()) clearTimeout(timer);
|
|
16609
16936
|
this.ptyGraceTimers.clear();
|
|
16937
|
+
this.holdWhenIdle.clear();
|
|
16610
16938
|
if (this.idleReaperTimer) {
|
|
16611
16939
|
clearInterval(this.idleReaperTimer);
|
|
16612
16940
|
this.idleReaperTimer = null;
|
|
16613
16941
|
}
|
|
16942
|
+
this.hostPressureMonitor?.dispose();
|
|
16943
|
+
this.hostPressureMonitor = null;
|
|
16614
16944
|
this.lastAgentChunkAt.clear();
|
|
16615
16945
|
this.terminalSeq.clear();
|
|
16616
16946
|
if (this.ptyManager.isRemote()) this.ptyManager.dispose();
|