instar 1.3.73 → 1.3.75
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/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +11 -3
- package/dist/commands/server.js.map +1 -1
- package/dist/core/SleepWakeDetector.d.ts +83 -5
- package/dist/core/SleepWakeDetector.d.ts.map +1 -1
- package/dist/core/SleepWakeDetector.js +117 -11
- package/dist/core/SleepWakeDetector.js.map +1 -1
- package/dist/core/types.d.ts +17 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/PresenceProxy.d.ts.map +1 -1
- package/dist/monitoring/PresenceProxy.js +28 -11
- package/dist/monitoring/PresenceProxy.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +6 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +3 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +4 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +14 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +47 -47
- package/src/scaffold/templates.ts +6 -0
- package/upgrades/1.3.74.md +95 -0
- package/upgrades/1.3.75.md +54 -0
- package/upgrades/side-effects/presence-proxy-post-ack-suppression.md +77 -0
|
@@ -6,6 +6,32 @@
|
|
|
6
6
|
* We detect this drift and fire a callback.
|
|
7
7
|
*
|
|
8
8
|
* Ported from Dawn's infrastructure — battle-tested in production.
|
|
9
|
+
*
|
|
10
|
+
* ## CPU-starvation guard (2026-05-28)
|
|
11
|
+
*
|
|
12
|
+
* Timer drift has TWO causes, not one:
|
|
13
|
+
* 1. Real sleep — the OS suspends the process; the wall clock jumps forward.
|
|
14
|
+
* 2. CPU starvation — the machine is so oversubscribed (load >> cores) that the
|
|
15
|
+
* event loop can't service the `checkIntervalMs` timer on time. The wall
|
|
16
|
+
* clock advances normally, but the callback fires seconds late.
|
|
17
|
+
*
|
|
18
|
+
* The original detector could not tell these apart, so on a heavily-loaded box
|
|
19
|
+
* (e.g. many concurrent agent sessions) it fired hundreds of false "wake" events
|
|
20
|
+
* — each triggering expensive wake-recovery (tunnel restart, re-registration,
|
|
21
|
+
* failure-counter resets) that piled MORE load on, a self-reinforcing storm.
|
|
22
|
+
*
|
|
23
|
+
* The guard distinguishes the two:
|
|
24
|
+
* - A **long** drift (>= `longSleepFloorSeconds`) is unambiguously real sleep —
|
|
25
|
+
* a live machine never starves its own event loop for minutes (a watchdog
|
|
26
|
+
* would declare it dead first). Always emitted, regardless of load.
|
|
27
|
+
* - A **short** drift under high system load (`loadavg[0] / cpuCount >
|
|
28
|
+
* maxLoadRatio`) is treated as CPU starvation and SUPPRESSED — no `wake`.
|
|
29
|
+
* - A short drift under normal load is a brief real sleep — emitted.
|
|
30
|
+
* On top of classification, a `minWakeIntervalMs` cooldown caps the emit rate so
|
|
31
|
+
* even a misclassified burst can't trigger a recovery storm.
|
|
32
|
+
*
|
|
33
|
+
* Suppressed events are NOT added to `wakeHistory`, so `getCumulativeSleepMsBetween`
|
|
34
|
+
* (the wake-reaper's sleep-credit source) only ever counts genuine sleep.
|
|
9
35
|
*/
|
|
10
36
|
import { EventEmitter } from 'node:events';
|
|
11
37
|
export interface SleepWakeDetectorConfig {
|
|
@@ -13,20 +39,73 @@ export interface SleepWakeDetectorConfig {
|
|
|
13
39
|
checkIntervalMs?: number;
|
|
14
40
|
/** How much drift (ms) indicates a sleep event. Default: 10000 */
|
|
15
41
|
driftThresholdMs?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Above this `loadavg[0] / cpuCount` ratio, a SHORT drift is classified as CPU
|
|
44
|
+
* starvation and suppressed rather than emitted as a wake. Default: 1.5.
|
|
45
|
+
* Set to `Infinity` to disable the load guard (always trust the drift).
|
|
46
|
+
*/
|
|
47
|
+
maxLoadRatio?: number;
|
|
48
|
+
/**
|
|
49
|
+
* A drift at least this long (seconds) is always treated as real sleep,
|
|
50
|
+
* regardless of load — a live event loop never starves for this long.
|
|
51
|
+
* Default: 300 (5 minutes).
|
|
52
|
+
*/
|
|
53
|
+
longSleepFloorSeconds?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Minimum gap (ms) between EMITTED wake events. A short drift that would emit
|
|
56
|
+
* within this cooldown of the previous emitted wake is suppressed, bounding
|
|
57
|
+
* recovery storms. Long sleeps bypass the cooldown. Default: 60000 (1 min).
|
|
58
|
+
*/
|
|
59
|
+
minWakeIntervalMs?: number;
|
|
60
|
+
/** Injectable system-load source (testing). Default: os.loadavg. */
|
|
61
|
+
loadAvgProvider?: () => number[];
|
|
62
|
+
/** Injectable CPU-count source (testing). Default: os.cpus().length. */
|
|
63
|
+
cpuCountProvider?: () => number;
|
|
64
|
+
/** Injectable wall-clock source (testing). Default: Date.now. */
|
|
65
|
+
nowProvider?: () => number;
|
|
16
66
|
}
|
|
17
67
|
export interface WakeEvent {
|
|
18
68
|
sleepDurationSeconds: number;
|
|
19
69
|
timestamp: string;
|
|
20
70
|
}
|
|
71
|
+
export type WakeSuppressionReason = 'cpu-starvation' | 'cooldown';
|
|
72
|
+
export interface SuppressedWakeEvent {
|
|
73
|
+
reason: WakeSuppressionReason;
|
|
74
|
+
driftSeconds: number;
|
|
75
|
+
loadRatio: number;
|
|
76
|
+
timestamp: string;
|
|
77
|
+
}
|
|
78
|
+
export interface SleepWakeStats {
|
|
79
|
+
wakeCount: number;
|
|
80
|
+
totalSleepSeconds: number;
|
|
81
|
+
longestSleepSeconds: number;
|
|
82
|
+
/** Drifts classified as CPU starvation / rate-limited and NOT emitted. */
|
|
83
|
+
suppressedCount: number;
|
|
84
|
+
suppressedByReason: Record<WakeSuppressionReason, number>;
|
|
85
|
+
/** ISO timestamp of the most recent suppression, or null. */
|
|
86
|
+
lastSuppressedAt: string | null;
|
|
87
|
+
}
|
|
21
88
|
export declare class SleepWakeDetector extends EventEmitter {
|
|
22
89
|
private interval;
|
|
23
90
|
private lastTick;
|
|
91
|
+
private lastEmittedWakeAtMs;
|
|
24
92
|
private checkIntervalMs;
|
|
25
93
|
private driftThresholdMs;
|
|
94
|
+
private maxLoadRatio;
|
|
95
|
+
private longSleepFloorSeconds;
|
|
96
|
+
private minWakeIntervalMs;
|
|
97
|
+
private loadAvgProvider;
|
|
98
|
+
private cpuCountProvider;
|
|
99
|
+
private now;
|
|
26
100
|
private wakeHistory;
|
|
101
|
+
private suppressionHistory;
|
|
27
102
|
constructor(config?: SleepWakeDetectorConfig);
|
|
28
103
|
start(): void;
|
|
29
104
|
stop(): void;
|
|
105
|
+
/** Current `loadavg[0] / cpuCount` ratio. Returns 0 when load is unavailable
|
|
106
|
+
* (e.g. Windows reports [0,0,0]), which disables the starvation guard there. */
|
|
107
|
+
private currentLoadRatio;
|
|
108
|
+
private recordSuppression;
|
|
30
109
|
/**
|
|
31
110
|
* Cumulative wall-time-asleep during the half-open window [startMs, endMs).
|
|
32
111
|
* Used by the wake-reaper (UNIFIED-SESSION-LIFECYCLE §P0 #9 / SE-8) to subtract
|
|
@@ -38,13 +117,12 @@ export declare class SleepWakeDetector extends EventEmitter {
|
|
|
38
117
|
* [wakeTimestamp − sleepDurationSeconds, wakeTimestamp].
|
|
39
118
|
* Returns the sum of overlap with the query window in milliseconds. Returns 0
|
|
40
119
|
* when the history is empty or no event overlaps.
|
|
120
|
+
*
|
|
121
|
+
* Only EMITTED wakes are in `wakeHistory`; suppressed CPU-starvation drifts are
|
|
122
|
+
* deliberately excluded so starvation is never credited as real sleep.
|
|
41
123
|
*/
|
|
42
124
|
getCumulativeSleepMsBetween(startMs: number, endMs: number): number;
|
|
43
125
|
/** Get wake event stats for telemetry reporting. */
|
|
44
|
-
getStats(sinceMs?: number):
|
|
45
|
-
wakeCount: number;
|
|
46
|
-
totalSleepSeconds: number;
|
|
47
|
-
longestSleepSeconds: number;
|
|
48
|
-
};
|
|
126
|
+
getStats(sinceMs?: number): SleepWakeStats;
|
|
49
127
|
}
|
|
50
128
|
//# sourceMappingURL=SleepWakeDetector.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SleepWakeDetector.d.ts","sourceRoot":"","sources":["../../src/core/SleepWakeDetector.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"SleepWakeDetector.d.ts","sourceRoot":"","sources":["../../src/core/SleepWakeDetector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,MAAM,WAAW,uBAAuB;IACtC,uDAAuD;IACvD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oEAAoE;IACpE,eAAe,CAAC,EAAE,MAAM,MAAM,EAAE,CAAC;IACjC,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,MAAM,CAAC;IAChC,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,SAAS;IACxB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,qBAAqB,GAAG,gBAAgB,GAAG,UAAU,CAAC;AAElE,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,0EAA0E;IAC1E,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;IAC1D,6DAA6D;IAC7D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,qBAAa,iBAAkB,SAAQ,YAAY;IACjD,OAAO,CAAC,QAAQ,CAA+C;IAC/D,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,eAAe,CAAiB;IACxC,OAAO,CAAC,gBAAgB,CAAe;IACvC,OAAO,CAAC,GAAG,CAAe;IAC1B,OAAO,CAAC,WAAW,CAAmB;IACtC,OAAO,CAAC,kBAAkB,CAA6B;gBAE3C,MAAM,GAAE,uBAA4B;IAahD,KAAK,IAAI,IAAI;IAkDb,IAAI,IAAI,IAAI;IAOZ;qFACiF;IACjF,OAAO,CAAC,gBAAgB;IAiBxB,OAAO,CAAC,iBAAiB;IAezB;;;;;;;;;;;;;;OAcG;IACH,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAanE,oDAAoD;IACpD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,cAAc;CAkB3C"}
|
|
@@ -6,36 +6,98 @@
|
|
|
6
6
|
* We detect this drift and fire a callback.
|
|
7
7
|
*
|
|
8
8
|
* Ported from Dawn's infrastructure — battle-tested in production.
|
|
9
|
+
*
|
|
10
|
+
* ## CPU-starvation guard (2026-05-28)
|
|
11
|
+
*
|
|
12
|
+
* Timer drift has TWO causes, not one:
|
|
13
|
+
* 1. Real sleep — the OS suspends the process; the wall clock jumps forward.
|
|
14
|
+
* 2. CPU starvation — the machine is so oversubscribed (load >> cores) that the
|
|
15
|
+
* event loop can't service the `checkIntervalMs` timer on time. The wall
|
|
16
|
+
* clock advances normally, but the callback fires seconds late.
|
|
17
|
+
*
|
|
18
|
+
* The original detector could not tell these apart, so on a heavily-loaded box
|
|
19
|
+
* (e.g. many concurrent agent sessions) it fired hundreds of false "wake" events
|
|
20
|
+
* — each triggering expensive wake-recovery (tunnel restart, re-registration,
|
|
21
|
+
* failure-counter resets) that piled MORE load on, a self-reinforcing storm.
|
|
22
|
+
*
|
|
23
|
+
* The guard distinguishes the two:
|
|
24
|
+
* - A **long** drift (>= `longSleepFloorSeconds`) is unambiguously real sleep —
|
|
25
|
+
* a live machine never starves its own event loop for minutes (a watchdog
|
|
26
|
+
* would declare it dead first). Always emitted, regardless of load.
|
|
27
|
+
* - A **short** drift under high system load (`loadavg[0] / cpuCount >
|
|
28
|
+
* maxLoadRatio`) is treated as CPU starvation and SUPPRESSED — no `wake`.
|
|
29
|
+
* - A short drift under normal load is a brief real sleep — emitted.
|
|
30
|
+
* On top of classification, a `minWakeIntervalMs` cooldown caps the emit rate so
|
|
31
|
+
* even a misclassified burst can't trigger a recovery storm.
|
|
32
|
+
*
|
|
33
|
+
* Suppressed events are NOT added to `wakeHistory`, so `getCumulativeSleepMsBetween`
|
|
34
|
+
* (the wake-reaper's sleep-credit source) only ever counts genuine sleep.
|
|
9
35
|
*/
|
|
10
36
|
import { EventEmitter } from 'node:events';
|
|
37
|
+
import os from 'node:os';
|
|
11
38
|
export class SleepWakeDetector extends EventEmitter {
|
|
12
39
|
interval = null;
|
|
13
|
-
lastTick
|
|
40
|
+
lastTick;
|
|
41
|
+
lastEmittedWakeAtMs = null;
|
|
14
42
|
checkIntervalMs;
|
|
15
43
|
driftThresholdMs;
|
|
44
|
+
maxLoadRatio;
|
|
45
|
+
longSleepFloorSeconds;
|
|
46
|
+
minWakeIntervalMs;
|
|
47
|
+
loadAvgProvider;
|
|
48
|
+
cpuCountProvider;
|
|
49
|
+
now;
|
|
16
50
|
wakeHistory = [];
|
|
51
|
+
suppressionHistory = [];
|
|
17
52
|
constructor(config = {}) {
|
|
18
53
|
super();
|
|
19
54
|
this.checkIntervalMs = config.checkIntervalMs ?? 2000;
|
|
20
55
|
this.driftThresholdMs = config.driftThresholdMs ?? 10000;
|
|
56
|
+
this.maxLoadRatio = config.maxLoadRatio ?? 1.5;
|
|
57
|
+
this.longSleepFloorSeconds = config.longSleepFloorSeconds ?? 300;
|
|
58
|
+
this.minWakeIntervalMs = config.minWakeIntervalMs ?? 60000;
|
|
59
|
+
this.loadAvgProvider = config.loadAvgProvider ?? (() => os.loadavg());
|
|
60
|
+
this.cpuCountProvider = config.cpuCountProvider ?? (() => os.cpus().length);
|
|
61
|
+
this.now = config.nowProvider ?? (() => Date.now());
|
|
62
|
+
this.lastTick = this.now();
|
|
21
63
|
}
|
|
22
64
|
start() {
|
|
23
65
|
if (this.interval)
|
|
24
66
|
return;
|
|
25
|
-
this.lastTick =
|
|
67
|
+
this.lastTick = this.now();
|
|
26
68
|
this.interval = setInterval(() => {
|
|
27
|
-
const now =
|
|
69
|
+
const now = this.now();
|
|
28
70
|
const elapsed = now - this.lastTick;
|
|
29
71
|
this.lastTick = now;
|
|
30
|
-
if (elapsed
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.
|
|
72
|
+
if (elapsed <= this.driftThresholdMs)
|
|
73
|
+
return;
|
|
74
|
+
const sleepDuration = Math.round((elapsed - this.checkIntervalMs) / 1000);
|
|
75
|
+
const isLongSleep = sleepDuration >= this.longSleepFloorSeconds;
|
|
76
|
+
const loadRatio = this.currentLoadRatio();
|
|
77
|
+
// Short drift under heavy CPU load = event-loop starvation, not sleep.
|
|
78
|
+
if (!isLongSleep && loadRatio > this.maxLoadRatio) {
|
|
79
|
+
this.recordSuppression('cpu-starvation', sleepDuration, loadRatio, now);
|
|
80
|
+
console.warn(`[SleepWakeDetector] Drift ~${sleepDuration}s under load ratio ${loadRatio.toFixed(2)} ` +
|
|
81
|
+
`(> ${this.maxLoadRatio}) — treating as CPU starvation, suppressing wake`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Rate-limit emitted wakes so even a misclassified burst can't storm
|
|
85
|
+
// recovery. Long sleeps bypass the cooldown — recovery there is essential.
|
|
86
|
+
if (!isLongSleep &&
|
|
87
|
+
this.lastEmittedWakeAtMs !== null &&
|
|
88
|
+
now - this.lastEmittedWakeAtMs < this.minWakeIntervalMs) {
|
|
89
|
+
this.recordSuppression('cooldown', sleepDuration, loadRatio, now);
|
|
90
|
+
console.warn(`[SleepWakeDetector] Wake within cooldown ` +
|
|
91
|
+
`(${now - this.lastEmittedWakeAtMs}ms < ${this.minWakeIntervalMs}ms) — suppressing duplicate recovery`);
|
|
92
|
+
return;
|
|
38
93
|
}
|
|
94
|
+
console.log(`[SleepWakeDetector] Wake detected after ~${sleepDuration}s sleep`);
|
|
95
|
+
const event = { sleepDurationSeconds: sleepDuration, timestamp: new Date(now).toISOString() };
|
|
96
|
+
this.wakeHistory.push(event);
|
|
97
|
+
if (this.wakeHistory.length > 100)
|
|
98
|
+
this.wakeHistory.shift();
|
|
99
|
+
this.lastEmittedWakeAtMs = now;
|
|
100
|
+
this.emit('wake', event);
|
|
39
101
|
}, this.checkIntervalMs);
|
|
40
102
|
this.interval.unref(); // Don't prevent process exit in CLI contexts
|
|
41
103
|
}
|
|
@@ -45,6 +107,37 @@ export class SleepWakeDetector extends EventEmitter {
|
|
|
45
107
|
this.interval = null;
|
|
46
108
|
}
|
|
47
109
|
}
|
|
110
|
+
/** Current `loadavg[0] / cpuCount` ratio. Returns 0 when load is unavailable
|
|
111
|
+
* (e.g. Windows reports [0,0,0]), which disables the starvation guard there. */
|
|
112
|
+
currentLoadRatio() {
|
|
113
|
+
let cpuCount = 1;
|
|
114
|
+
try {
|
|
115
|
+
cpuCount = Math.max(1, this.cpuCountProvider());
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
cpuCount = 1;
|
|
119
|
+
}
|
|
120
|
+
let load1 = 0;
|
|
121
|
+
try {
|
|
122
|
+
load1 = this.loadAvgProvider()[0] ?? 0;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
load1 = 0;
|
|
126
|
+
}
|
|
127
|
+
if (!Number.isFinite(load1) || load1 <= 0)
|
|
128
|
+
return 0;
|
|
129
|
+
return load1 / cpuCount;
|
|
130
|
+
}
|
|
131
|
+
recordSuppression(reason, driftSeconds, loadRatio, nowMs) {
|
|
132
|
+
this.suppressionHistory.push({
|
|
133
|
+
reason,
|
|
134
|
+
driftSeconds,
|
|
135
|
+
loadRatio: Math.round(loadRatio * 100) / 100,
|
|
136
|
+
timestamp: new Date(nowMs).toISOString(),
|
|
137
|
+
});
|
|
138
|
+
if (this.suppressionHistory.length > 100)
|
|
139
|
+
this.suppressionHistory.shift();
|
|
140
|
+
}
|
|
48
141
|
/**
|
|
49
142
|
* Cumulative wall-time-asleep during the half-open window [startMs, endMs).
|
|
50
143
|
* Used by the wake-reaper (UNIFIED-SESSION-LIFECYCLE §P0 #9 / SE-8) to subtract
|
|
@@ -56,6 +149,9 @@ export class SleepWakeDetector extends EventEmitter {
|
|
|
56
149
|
* [wakeTimestamp − sleepDurationSeconds, wakeTimestamp].
|
|
57
150
|
* Returns the sum of overlap with the query window in milliseconds. Returns 0
|
|
58
151
|
* when the history is empty or no event overlaps.
|
|
152
|
+
*
|
|
153
|
+
* Only EMITTED wakes are in `wakeHistory`; suppressed CPU-starvation drifts are
|
|
154
|
+
* deliberately excluded so starvation is never credited as real sleep.
|
|
59
155
|
*/
|
|
60
156
|
getCumulativeSleepMsBetween(startMs, endMs) {
|
|
61
157
|
if (endMs <= startMs)
|
|
@@ -75,10 +171,20 @@ export class SleepWakeDetector extends EventEmitter {
|
|
|
75
171
|
getStats(sinceMs) {
|
|
76
172
|
const since = sinceMs ?? 0;
|
|
77
173
|
const relevant = this.wakeHistory.filter(e => new Date(e.timestamp).getTime() >= since);
|
|
174
|
+
const suppressed = this.suppressionHistory.filter(e => new Date(e.timestamp).getTime() >= since);
|
|
175
|
+
const suppressedByReason = {
|
|
176
|
+
'cpu-starvation': 0,
|
|
177
|
+
cooldown: 0,
|
|
178
|
+
};
|
|
179
|
+
for (const e of suppressed)
|
|
180
|
+
suppressedByReason[e.reason]++;
|
|
78
181
|
return {
|
|
79
182
|
wakeCount: relevant.length,
|
|
80
183
|
totalSleepSeconds: relevant.reduce((sum, e) => sum + e.sleepDurationSeconds, 0),
|
|
81
184
|
longestSleepSeconds: relevant.length > 0 ? Math.max(...relevant.map(e => e.sleepDurationSeconds)) : 0,
|
|
185
|
+
suppressedCount: suppressed.length,
|
|
186
|
+
suppressedByReason,
|
|
187
|
+
lastSuppressedAt: suppressed.length > 0 ? suppressed[suppressed.length - 1].timestamp : null,
|
|
82
188
|
};
|
|
83
189
|
}
|
|
84
190
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SleepWakeDetector.js","sourceRoot":"","sources":["../../src/core/SleepWakeDetector.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"SleepWakeDetector.js","sourceRoot":"","sources":["../../src/core/SleepWakeDetector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,MAAM,SAAS,CAAC;AA0DzB,MAAM,OAAO,iBAAkB,SAAQ,YAAY;IACzC,QAAQ,GAA0C,IAAI,CAAC;IACvD,QAAQ,CAAS;IACjB,mBAAmB,GAAkB,IAAI,CAAC;IAC1C,eAAe,CAAS;IACxB,gBAAgB,CAAS;IACzB,YAAY,CAAS;IACrB,qBAAqB,CAAS;IAC9B,iBAAiB,CAAS;IAC1B,eAAe,CAAiB;IAChC,gBAAgB,CAAe;IAC/B,GAAG,CAAe;IAClB,WAAW,GAAgB,EAAE,CAAC;IAC9B,kBAAkB,GAA0B,EAAE,CAAC;IAEvD,YAAY,SAAkC,EAAE;QAC9C,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,eAAe,IAAI,IAAI,CAAC;QACtD,IAAI,CAAC,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,IAAI,KAAK,CAAC;QACzD,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAY,IAAI,GAAG,CAAC;QAC/C,IAAI,CAAC,qBAAqB,GAAG,MAAM,CAAC,qBAAqB,IAAI,GAAG,CAAC;QACjE,IAAI,CAAC,iBAAiB,GAAG,MAAM,CAAC,iBAAiB,IAAI,KAAK,CAAC;QAC3D,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,eAAe,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;QACtE,IAAI,CAAC,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC;QAC5E,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,WAAW,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QACpD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO;QAC1B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAE3B,IAAI,CAAC,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE;YAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACvB,MAAM,OAAO,GAAG,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC;YACpC,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC;YAEpB,IAAI,OAAO,IAAI,IAAI,CAAC,gBAAgB;gBAAE,OAAO;YAE7C,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,IAAI,CAAC,CAAC;YAC1E,MAAM,WAAW,GAAG,aAAa,IAAI,IAAI,CAAC,qBAAqB,CAAC;YAChE,MAAM,SAAS,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAE1C,uEAAuE;YACvE,IAAI,CAAC,WAAW,IAAI,SAAS,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;gBAClD,IAAI,CAAC,iBAAiB,CAAC,gBAAgB,EAAE,aAAa,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC;gBACxE,OAAO,CAAC,IAAI,CACV,8BAA8B,aAAa,sBAAsB,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG;oBACtF,MAAM,IAAI,CAAC,YAAY,kDAAkD,CAC5E,CAAC;gBACF,OAAO;YACT,CAAC;YAED,qEAAqE;YACrE,2EAA2E;YAC3E,IACE,CAAC,WAAW;gBACZ,IAAI,CAAC,mBAAmB,KAAK,IAAI;gBACjC,GAAG,GAAG,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,iBAAiB,EACvD,CAAC;gBACD,IAAI,CAAC,iBAAiB,CAAC,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC;gBAClE,OAAO,CAAC,IAAI,CACV,2CAA2C;oBACzC,IAAI,GAAG,GAAG,IAAI,CAAC,mBAAmB,QAAQ,IAAI,CAAC,iBAAiB,sCAAsC,CACzG,CAAC;gBACF,OAAO;YACT,CAAC;YAED,OAAO,CAAC,GAAG,CAAC,4CAA4C,aAAa,SAAS,CAAC,CAAC;YAChF,MAAM,KAAK,GAAc,EAAE,oBAAoB,EAAE,aAAa,EAAE,SAAS,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;YACzG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC7B,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,GAAG;gBAAE,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;YAC5D,IAAI,CAAC,mBAAmB,GAAG,GAAG,CAAC;YAC/B,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC3B,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC;QACzB,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,6CAA6C;IACtE,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC7B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACvB,CAAC;IACH,CAAC;IAED;qFACiF;IACzE,gBAAgB;QACtB,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,IAAI,CAAC;YACH,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,QAAQ,GAAG,CAAC,CAAC;QACf,CAAC;QACD,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,CAAC;YACH,KAAK,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACzC,CAAC;QAAC,MAAM,CAAC;YACP,KAAK,GAAG,CAAC,CAAC;QACZ,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;YAAE,OAAO,CAAC,CAAC;QACpD,OAAO,KAAK,GAAG,QAAQ,CAAC;IAC1B,CAAC;IAEO,iBAAiB,CACvB,MAA6B,EAC7B,YAAoB,EACpB,SAAiB,EACjB,KAAa;QAEb,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC;YAC3B,MAAM;YACN,YAAY;YACZ,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,GAAG,CAAC,GAAG,GAAG;YAC5C,SAAS,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE;SACzC,CAAC,CAAC;QACH,IAAI,IAAI,CAAC,kBAAkB,CAAC,MAAM,GAAG,GAAG;YAAE,IAAI,CAAC,kBAAkB,CAAC,KAAK,EAAE,CAAC;IAC5E,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACH,2BAA2B,CAAC,OAAe,EAAE,KAAa;QACxD,IAAI,KAAK,IAAI,OAAO;YAAE,OAAO,CAAC,CAAC;QAC/B,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC;YAC/C,MAAM,YAAY,GAAG,MAAM,GAAG,CAAC,CAAC,oBAAoB,GAAG,IAAI,CAAC;YAC5D,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;YACrD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAC3C,IAAI,UAAU,GAAG,YAAY;gBAAE,KAAK,IAAI,UAAU,GAAG,YAAY,CAAC;QACpE,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,oDAAoD;IACpD,QAAQ,CAAC,OAAgB;QACvB,MAAM,KAAK,GAAG,OAAO,IAAI,CAAC,CAAC;QAC3B,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,IAAI,KAAK,CAAC,CAAC;QACxF,MAAM,UAAU,GAAG,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,IAAI,KAAK,CAAC,CAAC;QACjG,MAAM,kBAAkB,GAA0C;YAChE,gBAAgB,EAAE,CAAC;YACnB,QAAQ,EAAE,CAAC;SACZ,CAAC;QACF,KAAK,MAAM,CAAC,IAAI,UAAU;YAAE,kBAAkB,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3D,OAAO;YACL,SAAS,EAAE,QAAQ,CAAC,MAAM;YAC1B,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,oBAAoB,EAAE,CAAC,CAAC;YAC/E,mBAAmB,EAAE,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACrG,eAAe,EAAE,UAAU,CAAC,MAAM;YAClC,kBAAkB;YAClB,gBAAgB,EAAE,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;SAC7F,CAAC;IACJ,CAAC;CACF"}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -2542,6 +2542,23 @@ export interface MonitoringConfig {
|
|
|
2542
2542
|
dryRun?: boolean;
|
|
2543
2543
|
};
|
|
2544
2544
|
};
|
|
2545
|
+
/**
|
|
2546
|
+
* SleepWakeDetector CPU-starvation guard tuning. All optional — the class ships
|
|
2547
|
+
* sane defaults, so absence of this block (every existing agent) still gets the
|
|
2548
|
+
* fix on update with no config migration. Read at runtime as a fallback against
|
|
2549
|
+
* the shipped defaults.
|
|
2550
|
+
*/
|
|
2551
|
+
sleepWake?: {
|
|
2552
|
+
/** loadavg[0]/cpuCount above which a SHORT drift is treated as CPU starvation
|
|
2553
|
+
* (suppressed, not a wake). Default: 1.5. Set high to disable the guard. */
|
|
2554
|
+
maxLoadRatio?: number;
|
|
2555
|
+
/** A drift at least this long (seconds) is always treated as real sleep,
|
|
2556
|
+
* regardless of load. Default: 300. */
|
|
2557
|
+
longSleepFloorSeconds?: number;
|
|
2558
|
+
/** Minimum gap (ms) between EMITTED wakes; short drifts within it are
|
|
2559
|
+
* rate-limited. Long sleeps bypass it. Default: 60000. */
|
|
2560
|
+
minWakeIntervalMs?: number;
|
|
2561
|
+
};
|
|
2545
2562
|
/**
|
|
2546
2563
|
* SessionReaper — pressure-aware reaper of idle-but-alive sessions. The only
|
|
2547
2564
|
* monitor that *kills* on a heuristic, so it ships OFF + dry-run by default.
|