instar 1.3.635 → 1.3.636
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/core/SleepWakeDetector.d.ts +42 -1
- package/dist/core/SleepWakeDetector.d.ts.map +1 -1
- package/dist/core/SleepWakeDetector.js +50 -0
- package/dist/core/SleepWakeDetector.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.636.md +59 -0
- package/upgrades/side-effects/sleepwake-stall-not-sleep.md +102 -0
|
@@ -104,12 +104,42 @@ export interface SleepWakeDetectorConfig {
|
|
|
104
104
|
cpuCountProvider?: () => number;
|
|
105
105
|
/** Injectable wall-clock source (testing). Default: Date.now. */
|
|
106
106
|
nowProvider?: () => number;
|
|
107
|
+
/**
|
|
108
|
+
* Fraction of a drift gap during which THIS process must have consumed CPU for the
|
|
109
|
+
* drift to be classified as an event-loop BLOCK (a wedge/stall) rather than sleep.
|
|
110
|
+
*
|
|
111
|
+
* This is the per-PROCESS signal the load heuristics can't provide: a suspended
|
|
112
|
+
* (sleeping) process burns ~0 CPU during the gap, while a blocked event loop burns
|
|
113
|
+
* CPU for most of it. One Node thread blocking for 14s doesn't move a 16-core
|
|
114
|
+
* `loadavg` above `maxLoadRatio`, so the load guards emit a FALSE "sleep" — but the
|
|
115
|
+
* process's own CPU usage exposes it definitively. Applies to LONG drifts too: a
|
|
116
|
+
* multi-minute CPU-busy drift is the event-loop wedge, never sleep. Default: 0.5.
|
|
117
|
+
* Set 0 to disable.
|
|
118
|
+
*/
|
|
119
|
+
cpuBlockBusyRatio?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Injectable cumulative-process-CPU source in MICROSECONDS (testing). Default sums
|
|
122
|
+
* `process.cpuUsage()` user + system. Used to compute CPU burned across a drift gap.
|
|
123
|
+
*/
|
|
124
|
+
cpuUsageProvider?: () => number;
|
|
107
125
|
}
|
|
108
126
|
export interface WakeEvent {
|
|
109
127
|
sleepDurationSeconds: number;
|
|
110
128
|
timestamp: string;
|
|
111
129
|
}
|
|
112
|
-
export type WakeSuppressionReason = 'cpu-starvation' | 'cooldown';
|
|
130
|
+
export type WakeSuppressionReason = 'cpu-starvation' | 'cooldown' | 'event-loop-block';
|
|
131
|
+
/**
|
|
132
|
+
* Emitted when a drift is PROVEN to be an event-loop block (this process burned CPU
|
|
133
|
+
* through the gap), not sleep. Signal-only so wedge watchers can see a real stall that
|
|
134
|
+
* the detector would otherwise have mislabeled as a wake. No consumer = harmless.
|
|
135
|
+
*/
|
|
136
|
+
export interface StallEvent {
|
|
137
|
+
/** Drift duration in seconds (how long the loop was blocked). */
|
|
138
|
+
stallSeconds: number;
|
|
139
|
+
/** Fraction of the gap this process spent on CPU (≈1 = fully CPU-bound block). */
|
|
140
|
+
cpuBusyRatio: number;
|
|
141
|
+
timestamp: string;
|
|
142
|
+
}
|
|
113
143
|
export interface SuppressedWakeEvent {
|
|
114
144
|
reason: WakeSuppressionReason;
|
|
115
145
|
driftSeconds: number;
|
|
@@ -152,11 +182,22 @@ export declare class SleepWakeDetector extends EventEmitter {
|
|
|
152
182
|
private loadAvgProvider;
|
|
153
183
|
private cpuCountProvider;
|
|
154
184
|
private now;
|
|
185
|
+
private cpuBlockBusyRatio;
|
|
186
|
+
private cpuUsageProvider;
|
|
187
|
+
/** Cumulative process CPU (µs) sampled at the previous tick — drives the per-process
|
|
188
|
+
* CPU-busy-through-the-gap discriminator that separates a real sleep from a block. */
|
|
189
|
+
private lastCpuMicros;
|
|
155
190
|
private wakeHistory;
|
|
156
191
|
private suppressionHistory;
|
|
157
192
|
constructor(config?: SleepWakeDetectorConfig);
|
|
158
193
|
start(): void;
|
|
159
194
|
stop(): void;
|
|
195
|
+
/** Read cumulative process CPU (µs) defensively. A provider error (or a non-finite
|
|
196
|
+
* reading) yields the prior value, so the gap delta becomes 0 → no block signal →
|
|
197
|
+
* the tick falls through to the load guards. That is the safe direction: a CPU-read
|
|
198
|
+
* failure can never crash the tick (it runs inside setInterval) nor force a false
|
|
199
|
+
* block classification — it just disables this one discriminator for that tick. */
|
|
200
|
+
private readCpuMicros;
|
|
160
201
|
/** Current `loadavg[0] / cpuCount` ratio. Returns 0 when load is unavailable
|
|
161
202
|
* (e.g. Windows reports [0,0,0]), which disables the starvation guard there. */
|
|
162
203
|
private currentLoadRatio;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SleepWakeDetector.d.ts","sourceRoot":"","sources":["../../src/core/SleepWakeDetector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;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;;;;;;OAMG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,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;
|
|
1
|
+
{"version":3,"file":"SleepWakeDetector.d.ts","sourceRoot":"","sources":["../../src/core/SleepWakeDetector.ts"],"names":[],"mappings":"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"}
|
|
@@ -80,6 +80,11 @@ export class SleepWakeDetector extends EventEmitter {
|
|
|
80
80
|
loadAvgProvider;
|
|
81
81
|
cpuCountProvider;
|
|
82
82
|
now;
|
|
83
|
+
cpuBlockBusyRatio;
|
|
84
|
+
cpuUsageProvider;
|
|
85
|
+
/** Cumulative process CPU (µs) sampled at the previous tick — drives the per-process
|
|
86
|
+
* CPU-busy-through-the-gap discriminator that separates a real sleep from a block. */
|
|
87
|
+
lastCpuMicros = 0;
|
|
83
88
|
wakeHistory = [];
|
|
84
89
|
suppressionHistory = [];
|
|
85
90
|
constructor(config = {}) {
|
|
@@ -95,16 +100,25 @@ export class SleepWakeDetector extends EventEmitter {
|
|
|
95
100
|
this.loadAvgProvider = config.loadAvgProvider ?? (() => os.loadavg());
|
|
96
101
|
this.cpuCountProvider = config.cpuCountProvider ?? (() => os.cpus().length);
|
|
97
102
|
this.now = config.nowProvider ?? (() => Date.now());
|
|
103
|
+
this.cpuBlockBusyRatio = config.cpuBlockBusyRatio ?? 0.5;
|
|
104
|
+
this.cpuUsageProvider =
|
|
105
|
+
config.cpuUsageProvider ?? (() => { const c = process.cpuUsage(); return c.user + c.system; });
|
|
98
106
|
this.lastTick = this.now();
|
|
107
|
+
this.lastCpuMicros = this.readCpuMicros();
|
|
99
108
|
}
|
|
100
109
|
start() {
|
|
101
110
|
if (this.interval)
|
|
102
111
|
return;
|
|
103
112
|
this.lastTick = this.now();
|
|
113
|
+
this.lastCpuMicros = this.readCpuMicros();
|
|
104
114
|
this.interval = setInterval(() => {
|
|
105
115
|
const now = this.now();
|
|
106
116
|
const elapsed = now - this.lastTick;
|
|
107
117
|
this.lastTick = now;
|
|
118
|
+
// CPU burned by THIS process across the gap — the per-process sleep-vs-block signal.
|
|
119
|
+
const cpuNowMicros = this.readCpuMicros();
|
|
120
|
+
const cpuBusyRatio = elapsed > 0 ? ((cpuNowMicros - this.lastCpuMicros) / 1000) / elapsed : 0;
|
|
121
|
+
this.lastCpuMicros = cpuNowMicros;
|
|
108
122
|
if (elapsed <= this.driftThresholdMs) {
|
|
109
123
|
this.consecutiveDrifts = 0;
|
|
110
124
|
return;
|
|
@@ -118,6 +132,27 @@ export class SleepWakeDetector extends EventEmitter {
|
|
|
118
132
|
const prevShortDriftAtMs = this.lastShortDriftAtMs;
|
|
119
133
|
if (!isLongSleep)
|
|
120
134
|
this.lastShortDriftAtMs = now;
|
|
135
|
+
// Per-process CPU check — the DEFINITIVE sleep-vs-block discriminator, ahead of the
|
|
136
|
+
// load heuristics. A suspended (sleeping) process burns ~0 CPU during the gap; a
|
|
137
|
+
// blocked event loop burns CPU through most of it. So a high busy ratio means the
|
|
138
|
+
// loop was BLOCKED and the machine did NOT sleep — regardless of duration (a
|
|
139
|
+
// multi-minute CPU-busy drift is the event-loop WEDGE, not sleep) and regardless of
|
|
140
|
+
// the system loadavg (one blocked Node thread doesn't move a 16-core average). Emit a
|
|
141
|
+
// `stall` signal for the wedge watchers; never a `wake`. (2026-06-21: fixes the
|
|
142
|
+
// misdiagnosis where 11-18s event-loop blocks on a caffeinated host — where sleep is
|
|
143
|
+
// physically impossible — were logged as "Wake detected after ~Ns sleep".)
|
|
144
|
+
if (this.cpuBlockBusyRatio > 0 && cpuBusyRatio >= this.cpuBlockBusyRatio) {
|
|
145
|
+
this.recordSuppression('event-loop-block', sleepDuration, loadRatio, now);
|
|
146
|
+
console.warn(`[SleepWakeDetector] Drift ~${sleepDuration}s but this process burned ` +
|
|
147
|
+
`${Math.round(cpuBusyRatio * 100)}% CPU through the gap — event-loop BLOCK, not ` +
|
|
148
|
+
`sleep (the machine did not sleep). Emitting stall, suppressing wake.`);
|
|
149
|
+
this.emit('stall', {
|
|
150
|
+
stallSeconds: sleepDuration,
|
|
151
|
+
cpuBusyRatio,
|
|
152
|
+
timestamp: new Date(now).toISOString(),
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
121
156
|
// Consecutive-drift burst = sustained CPU starvation, not sleep. A genuine sleep
|
|
122
157
|
// is ONE isolated drift (the next on-time tick resets the counter); the 2nd+
|
|
123
158
|
// back-to-back SHORT drift is a storm. Suppress it regardless of the (lagging,
|
|
@@ -185,6 +220,20 @@ export class SleepWakeDetector extends EventEmitter {
|
|
|
185
220
|
this.interval = null;
|
|
186
221
|
}
|
|
187
222
|
}
|
|
223
|
+
/** Read cumulative process CPU (µs) defensively. A provider error (or a non-finite
|
|
224
|
+
* reading) yields the prior value, so the gap delta becomes 0 → no block signal →
|
|
225
|
+
* the tick falls through to the load guards. That is the safe direction: a CPU-read
|
|
226
|
+
* failure can never crash the tick (it runs inside setInterval) nor force a false
|
|
227
|
+
* block classification — it just disables this one discriminator for that tick. */
|
|
228
|
+
readCpuMicros() {
|
|
229
|
+
try {
|
|
230
|
+
const v = this.cpuUsageProvider();
|
|
231
|
+
return Number.isFinite(v) ? v : this.lastCpuMicros;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return this.lastCpuMicros;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
188
237
|
/** Current `loadavg[0] / cpuCount` ratio. Returns 0 when load is unavailable
|
|
189
238
|
* (e.g. Windows reports [0,0,0]), which disables the starvation guard there. */
|
|
190
239
|
currentLoadRatio() {
|
|
@@ -253,6 +302,7 @@ export class SleepWakeDetector extends EventEmitter {
|
|
|
253
302
|
const suppressedByReason = {
|
|
254
303
|
'cpu-starvation': 0,
|
|
255
304
|
cooldown: 0,
|
|
305
|
+
'event-loop-block': 0,
|
|
256
306
|
};
|
|
257
307
|
for (const e of suppressed)
|
|
258
308
|
suppressedByReason[e.reason]++;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SleepWakeDetector.js","sourceRoot":"","sources":["../../src/core/SleepWakeDetector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"SleepWakeDetector.js","sourceRoot":"","sources":["../../src/core/SleepWakeDetector.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-21T21:13:59.179Z",
|
|
5
|
+
"instarVersion": "1.3.636",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The sleep/wake watcher (`SleepWakeDetector`) used to decide "did the machine sleep?"
|
|
9
|
+
purely from a timer-drift jump plus **system load**. On a multi-core machine, one
|
|
10
|
+
internal thread can freeze for 10–60 seconds without moving the system load average — so
|
|
11
|
+
the watcher would announce a "wake after ~Ns sleep" when the machine never slept at all
|
|
12
|
+
(it was plugged in, lid open, with `caffeinate` running — sleep was impossible). Those
|
|
13
|
+
false "sleep" reports hid the real cause: short event-loop freezes.
|
|
14
|
+
|
|
15
|
+
Now the watcher also measures how much CPU **its own process** burned during the gap. A
|
|
16
|
+
truly sleeping process burns almost no CPU; a frozen-but-busy one burns CPU the whole
|
|
17
|
+
time. So a busy gap is correctly reported as a `stall` (a freeze the wedge watchers can
|
|
18
|
+
act on) instead of a phantom "sleep." The new check runs first, applies even to
|
|
19
|
+
multi-minute gaps, defaults on with no configuration, and fails safe (if the CPU reading
|
|
20
|
+
can't be taken it quietly falls back to the old behavior).
|
|
21
|
+
|
|
22
|
+
## What to Tell Your User
|
|
23
|
+
|
|
24
|
+
If you ever saw your agent claim it "woke from sleep" on a machine that was plugged in
|
|
25
|
+
and awake — that was a misread. The watcher now tells the difference between a real sleep
|
|
26
|
+
and an internal freeze by checking its own CPU use, so those phantom "sleep" messages stop
|
|
27
|
+
and genuine freezes get flagged honestly. Nothing to configure.
|
|
28
|
+
|
|
29
|
+
## Summary of New Capabilities
|
|
30
|
+
|
|
31
|
+
No new user-facing capability — a correctness fix. `SleepWakeDetector` gains a per-process
|
|
32
|
+
CPU-burn discriminator that separates an event-loop block from real sleep: it emits a
|
|
33
|
+
signal-only `stall` event (and suppresses the false `wake`) when this process burned CPU
|
|
34
|
+
through the drift gap. Tunable via the optional `cpuBlockBusyRatio` (default 0.5; set 0 to
|
|
35
|
+
disable). `getStats().suppressedByReason` gains an `event-loop-block` count. Existing
|
|
36
|
+
agents get it via the normal update; on-disk formats and API shapes are unchanged.
|
|
37
|
+
|
|
38
|
+
## Evidence
|
|
39
|
+
|
|
40
|
+
**Reproduction (live, 2026-06-21):** Echo's server was flapping (watchdog SIGKILL+respawn
|
|
41
|
+
on a cadence) from event-loop wedges. Throughout, `logs/server.log` carried
|
|
42
|
+
`[SleepWakeDetector] Wake detected after ~Ns sleep` lines — on a host that was plugged in,
|
|
43
|
+
lid open, with `caffeinate` running, where OS sleep is physically impossible. `pmset -g`
|
|
44
|
+
confirmed no sleep occurred. The drifts were single-process event-loop blocks (one Node
|
|
45
|
+
thread pinned), and the 16-core `loadavg` stayed well under `maxLoadRatio` (1.5), so the
|
|
46
|
+
existing load guard never tripped and each block printed a phantom "sleep."
|
|
47
|
+
|
|
48
|
+
**Before:** a 14s CPU-bound event-loop block under normal system load → `wake` emitted
|
|
49
|
+
(`sleepDurationSeconds ≈ 14`), credited as real sleep in `wakeHistory`; the wedge was
|
|
50
|
+
invisible (laundered into "sleep").
|
|
51
|
+
|
|
52
|
+
**After:** the same 14s CPU-bound drift → the per-process CPU check measures ~100% busy
|
|
53
|
+
across the gap → a `stall` event is emitted and the `wake` is suppressed (recorded under
|
|
54
|
+
`suppressedByReason['event-loop-block']`), so it is never credited as sleep. A genuine
|
|
55
|
+
idle/suspend gap (~0% CPU) still emits `wake` unchanged.
|
|
56
|
+
|
|
57
|
+
**Automated:** `tests/unit/SleepWakeDetector-cpu-block.test.ts` reproduces both directions
|
|
58
|
+
with injected clock + CPU providers (CPU-busy short drift → stall; idle drift → wake; long
|
|
59
|
+
CPU-busy drift → stall; throwing provider → no crash). 15/15 unit tests green; `tsc` clean.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Side-Effects Review — SleepWakeDetector: per-process CPU check separates event-loop block from sleep
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `sleepwake-stall-not-sleep`
|
|
4
|
+
**Tier:** 1 (surgical bug-fix, one core file + tests; no new route, config contract, or persistence schema)
|
|
5
|
+
|
|
6
|
+
## Summary of the change
|
|
7
|
+
|
|
8
|
+
`SleepWakeDetector` inferred "sleep" from timer drift + **system loadavg**. A single
|
|
9
|
+
Node thread blocking its own event loop for tens of seconds does NOT move a 16-core
|
|
10
|
+
`loadavg` above `maxLoadRatio` (1.5), so an isolated event-loop block under normal
|
|
11
|
+
system load emitted a FALSE `wake` ("Wake detected after ~14s sleep") — even on a
|
|
12
|
+
caffeinated host where sleep is physically impossible. That laundered real wedges into
|
|
13
|
+
"sleep" and masked the actual fault.
|
|
14
|
+
|
|
15
|
+
The fix adds a per-PROCESS discriminator: `process.cpuUsage()` sampled across the drift
|
|
16
|
+
gap. A suspended (sleeping) process burns ~0 CPU; a blocked event loop burns CPU through
|
|
17
|
+
most of the gap. When `cpuBusyRatio >= cpuBlockBusyRatio` (default 0.5) the drift is a
|
|
18
|
+
BLOCK — emit a new signal-only `stall` event and SUPPRESS the false `wake`. The check
|
|
19
|
+
runs ahead of the existing load/burst/cooldown guards and, unlike the old `isLongSleep`
|
|
20
|
+
exemption, applies to LONG drifts too (a multi-minute CPU-busy drift is the wedge, never
|
|
21
|
+
sleep).
|
|
22
|
+
|
|
23
|
+
## Decision-point inventory
|
|
24
|
+
|
|
25
|
+
- **Threshold `cpuBlockBusyRatio` = 0.5.** A real wake burns ~0 CPU over the gap (ratio
|
|
26
|
+
~0); a CPU-bound block burns ~1 core (ratio ~1.0). 0.5 is the safe midpoint; set 0 to
|
|
27
|
+
disable. Tunable, defaults on.
|
|
28
|
+
- **Ordering:** the CPU check is FIRST (most authoritative). A real long sleep (≈0 CPU)
|
|
29
|
+
falls through it and still emits a wake via the existing `isLongSleep` exemption.
|
|
30
|
+
- **New `stall` event:** signal-only. No consumer = harmless (the value is the suppressed
|
|
31
|
+
false wake). Wedge watchers MAY consume it later.
|
|
32
|
+
|
|
33
|
+
## 1. Over-block (false positive — suppressing a REAL wake)
|
|
34
|
+
|
|
35
|
+
Could a genuine wake be misread as a block? On real OS suspend the process is frozen →
|
|
36
|
+
~0 CPU over the gap → ratio ~0 → NOT suppressed → wake emits correctly. A wake immediately
|
|
37
|
+
followed by heavy CPU still reads low, because the drift tick fires once at wake and the
|
|
38
|
+
gap's CPU (the frozen span) is ~0. Verified by the `genuine sleep → WAKE` test.
|
|
39
|
+
|
|
40
|
+
## 2. Under-block (missing a block)
|
|
41
|
+
|
|
42
|
+
A low-CPU stall (IO-wait, not CPU-bound) reads ratio ~0 and is NOT flagged by this check —
|
|
43
|
+
by design; it falls through to the existing load/burst/recurring guards exactly as before.
|
|
44
|
+
This change only ADDS detection for the CPU-bound case the load heuristics were blind to;
|
|
45
|
+
it removes no existing suppression path.
|
|
46
|
+
|
|
47
|
+
## 4. Signal vs authority compliance
|
|
48
|
+
|
|
49
|
+
The detector only decides whether to emit `wake` vs `stall` — both are signals to other
|
|
50
|
+
watchers; it gates nothing and takes no destructive action. Recovery authority stays with
|
|
51
|
+
the consumers (ServerSupervisor / wedge watchers), unchanged.
|
|
52
|
+
|
|
53
|
+
## 5. Interactions
|
|
54
|
+
|
|
55
|
+
`getCumulativeSleepMsBetween` (the wake-reaper's sleep-credit source) reads only EMITTED
|
|
56
|
+
wakes; a suppressed block is never credited as sleep — so a wedge can no longer inflate a
|
|
57
|
+
job's sleep credit and cause an early reap. `getStats().suppressedByReason` gains an
|
|
58
|
+
`event-loop-block` counter (additive; existing keys unchanged).
|
|
59
|
+
|
|
60
|
+
## 6. External surfaces
|
|
61
|
+
|
|
62
|
+
None. No new route, no config-file contract change (the two new config fields are optional
|
|
63
|
+
with safe defaults), no persistence, no messaging. `GET /sleep/stats` (routes.ts) returns
|
|
64
|
+
the same `SleepWakeStats` shape plus the additive reason key.
|
|
65
|
+
|
|
66
|
+
## 6b. Operator-surface quality
|
|
67
|
+
|
|
68
|
+
N/A — no operator/dashboard/approval surface is touched. Change is internal to a core
|
|
69
|
+
detector.
|
|
70
|
+
|
|
71
|
+
## Framework generality
|
|
72
|
+
|
|
73
|
+
N/A — `SleepWakeDetector` is a framework-agnostic core monitor (not part of the session
|
|
74
|
+
launch/inject abstraction). It runs per-process regardless of which agentic framework the
|
|
75
|
+
session uses.
|
|
76
|
+
|
|
77
|
+
## 7. Multi-machine posture
|
|
78
|
+
|
|
79
|
+
Per-process and per-machine by nature; each machine's detector watches its own loop. No
|
|
80
|
+
replicated state, no cross-machine contract. Safe on single- and multi-machine installs.
|
|
81
|
+
|
|
82
|
+
## 8. Rollback cost
|
|
83
|
+
|
|
84
|
+
Trivial and safe: set `cpuBlockBusyRatio: 0` to disable the new branch (reverts to the
|
|
85
|
+
prior load-only behavior), or revert the commit. The defensive CPU read fails toward the
|
|
86
|
+
old behavior, so even a provider error degrades to pre-change semantics rather than
|
|
87
|
+
breaking.
|
|
88
|
+
|
|
89
|
+
## Evidence pointers
|
|
90
|
+
|
|
91
|
+
- New tests: `tests/unit/SleepWakeDetector-cpu-block.test.ts` (5 tests — CPU-busy short
|
|
92
|
+
drift → stall; genuine sleep → wake; long CPU-busy drift → stall; stats record; throwing
|
|
93
|
+
provider never crashes the tick). Existing `SleepWakeDetector.test.ts` (10 tests) still
|
|
94
|
+
green — 15/15. `tsc --noEmit` clean.
|
|
95
|
+
- Live root cause (2026-06-21): a caffeinated, lid-open, plugged-in host logged "Wake
|
|
96
|
+
detected after ~Ns sleep" for what were event-loop blocks — the misdiagnosis this fixes.
|
|
97
|
+
|
|
98
|
+
## Conclusion
|
|
99
|
+
|
|
100
|
+
A correctness fix that makes drift classification honest: CPU-bound event-loop blocks are
|
|
101
|
+
flagged as `stall`, not laundered into a false `wake`. Defaults on, fails safe, trivially
|
|
102
|
+
reversible. Ship.
|