heyio 0.24.1 → 0.25.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/config.js +1 -0
- package/dist/daemon.js +7 -0
- package/dist/tui/index.js +11 -2
- package/dist/watchdog.js +51 -0
- package/dist/watchdog.test.js +83 -0
- package/package.json +1 -1
package/dist/config.js
CHANGED
package/dist/daemon.js
CHANGED
|
@@ -13,6 +13,7 @@ import { backfillReviewVerdicts } from "./copilot/review-backfill.js";
|
|
|
13
13
|
import { startScheduler, stopScheduler } from "./copilot/scheduler.js";
|
|
14
14
|
import { startIoScheduler, stopIoScheduler } from "./copilot/io-scheduler.js";
|
|
15
15
|
import { config } from "./config.js";
|
|
16
|
+
import { startWatchdog } from "./watchdog.js";
|
|
16
17
|
import { ensureWikiStructure } from "./wiki/fs.js";
|
|
17
18
|
import { autoUpdate } from "./update.js";
|
|
18
19
|
import { readdirSync, statSync, rmSync } from "fs";
|
|
@@ -129,6 +130,12 @@ export async function startDaemon() {
|
|
|
129
130
|
// Daily cleanup — prune schedule runs and notifications older than 30 days
|
|
130
131
|
const PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
131
132
|
const PRUNE_RETENTION_DAYS = 30;
|
|
133
|
+
// Start event loop watchdog
|
|
134
|
+
let stopWatchdog;
|
|
135
|
+
if (config.watchdogEnabled) {
|
|
136
|
+
stopWatchdog = startWatchdog();
|
|
137
|
+
console.error("[io] Event loop watchdog started");
|
|
138
|
+
}
|
|
132
139
|
const pruneTimer = setInterval(() => {
|
|
133
140
|
try {
|
|
134
141
|
const runsDeleted = pruneOldScheduleRuns(PRUNE_RETENTION_DAYS);
|
package/dist/tui/index.js
CHANGED
|
@@ -252,9 +252,14 @@ export async function startTui() {
|
|
|
252
252
|
firstChunk = false;
|
|
253
253
|
}
|
|
254
254
|
if (done) {
|
|
255
|
+
// Finalize the current message as its own line, then reset state
|
|
256
|
+
// so the next message in a multi-turn response starts a fresh bubble.
|
|
255
257
|
clearLine();
|
|
256
|
-
|
|
257
|
-
|
|
258
|
+
if (accumulated) {
|
|
259
|
+
process.stdout.write(accumulated + "\n");
|
|
260
|
+
}
|
|
261
|
+
accumulated = "";
|
|
262
|
+
firstChunk = true;
|
|
258
263
|
}
|
|
259
264
|
else {
|
|
260
265
|
accumulated += text;
|
|
@@ -262,6 +267,10 @@ export async function startTui() {
|
|
|
262
267
|
process.stdout.write(accumulated);
|
|
263
268
|
}
|
|
264
269
|
});
|
|
270
|
+
// Restore the prompt once the handler promise fully resolves (i.e. after
|
|
271
|
+
// all turns are done), rather than inside the done callback so that
|
|
272
|
+
// multi-turn responses don't render a premature prompt between messages.
|
|
273
|
+
rl.prompt();
|
|
265
274
|
}
|
|
266
275
|
catch (err) {
|
|
267
276
|
clearLine();
|
package/dist/watchdog.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon liveness watchdog.
|
|
3
|
+
*
|
|
4
|
+
* Uses a periodic setInterval to detect event loop stalls. If the interval
|
|
5
|
+
* fires significantly late, the event loop was blocked for that duration.
|
|
6
|
+
*
|
|
7
|
+
* - Warning logged if stall exceeds WARN_THRESHOLD_MS (30s)
|
|
8
|
+
* - Process exits if stall exceeds FATAL_THRESHOLD_MS (60s), relying on
|
|
9
|
+
* the process supervisor to restart
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_CHECK_INTERVAL_MS = 30_000;
|
|
12
|
+
const DEFAULT_WARN_THRESHOLD_MS = 30_000;
|
|
13
|
+
const DEFAULT_FATAL_THRESHOLD_MS = 60_000;
|
|
14
|
+
let timer = null;
|
|
15
|
+
let lastTick = 0;
|
|
16
|
+
/**
|
|
17
|
+
* Start the event loop watchdog. Returns a stop function.
|
|
18
|
+
*/
|
|
19
|
+
export function startWatchdog(opts = {}) {
|
|
20
|
+
const checkInterval = opts.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS;
|
|
21
|
+
const warnThreshold = opts.warnThresholdMs ?? DEFAULT_WARN_THRESHOLD_MS;
|
|
22
|
+
const fatalThreshold = opts.fatalThresholdMs ?? DEFAULT_FATAL_THRESHOLD_MS;
|
|
23
|
+
lastTick = Date.now();
|
|
24
|
+
timer = setInterval(() => {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const elapsed = now - lastTick;
|
|
27
|
+
const stallMs = elapsed - checkInterval;
|
|
28
|
+
if (stallMs >= fatalThreshold) {
|
|
29
|
+
console.error(`[watchdog] FATAL: event loop stalled for ${Math.round(stallMs / 1000)}s (threshold: ${Math.round(fatalThreshold / 1000)}s) at ${new Date(now).toISOString()}`);
|
|
30
|
+
if (opts.onFatal) {
|
|
31
|
+
opts.onFatal(stallMs);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (stallMs >= warnThreshold) {
|
|
38
|
+
console.error(`[watchdog] WARNING: event loop stalled for ${Math.round(stallMs / 1000)}s at ${new Date(now).toISOString()}`);
|
|
39
|
+
opts.onStall?.(stallMs);
|
|
40
|
+
}
|
|
41
|
+
lastTick = now;
|
|
42
|
+
}, checkInterval);
|
|
43
|
+
timer.unref();
|
|
44
|
+
return () => {
|
|
45
|
+
if (timer) {
|
|
46
|
+
clearInterval(timer);
|
|
47
|
+
timer = null;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=watchdog.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { startWatchdog } from "./watchdog.js";
|
|
4
|
+
describe("watchdog", () => {
|
|
5
|
+
let stop;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
if (stop) {
|
|
8
|
+
stop();
|
|
9
|
+
stop = undefined;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
it("calls onStall when stall exceeds warn threshold", async () => {
|
|
13
|
+
const stalls = [];
|
|
14
|
+
stop = startWatchdog({
|
|
15
|
+
checkIntervalMs: 30,
|
|
16
|
+
warnThresholdMs: 40,
|
|
17
|
+
fatalThresholdMs: 5000,
|
|
18
|
+
onStall: (ms) => stalls.push(ms),
|
|
19
|
+
});
|
|
20
|
+
// Block event loop longer than checkInterval + warnThreshold
|
|
21
|
+
const start = Date.now();
|
|
22
|
+
while (Date.now() - start < 120) {
|
|
23
|
+
// busy-wait
|
|
24
|
+
}
|
|
25
|
+
// Let the interval fire
|
|
26
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
27
|
+
assert.ok(stalls.length > 0, "onStall should have been called");
|
|
28
|
+
assert.ok(stalls[0] >= 40, `stall duration ${stalls[0]} should be >= 40ms`);
|
|
29
|
+
stop();
|
|
30
|
+
stop = undefined;
|
|
31
|
+
});
|
|
32
|
+
it("calls onFatal when stall exceeds fatal threshold", async () => {
|
|
33
|
+
const fatals = [];
|
|
34
|
+
stop = startWatchdog({
|
|
35
|
+
checkIntervalMs: 30,
|
|
36
|
+
warnThresholdMs: 20,
|
|
37
|
+
fatalThresholdMs: 50,
|
|
38
|
+
onStall: () => { },
|
|
39
|
+
onFatal: (ms) => fatals.push(ms),
|
|
40
|
+
});
|
|
41
|
+
// Block event loop longer than checkInterval + fatalThreshold
|
|
42
|
+
const start = Date.now();
|
|
43
|
+
while (Date.now() - start < 150) {
|
|
44
|
+
// busy-wait
|
|
45
|
+
}
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
47
|
+
assert.ok(fatals.length > 0, "onFatal should have been called");
|
|
48
|
+
assert.ok(fatals[0] >= 50, `fatal duration ${fatals[0]} should be >= 50ms`);
|
|
49
|
+
stop();
|
|
50
|
+
stop = undefined;
|
|
51
|
+
});
|
|
52
|
+
it("does not fire when event loop is healthy", async () => {
|
|
53
|
+
const stalls = [];
|
|
54
|
+
const fatals = [];
|
|
55
|
+
stop = startWatchdog({
|
|
56
|
+
checkIntervalMs: 30,
|
|
57
|
+
warnThresholdMs: 500,
|
|
58
|
+
fatalThresholdMs: 1000,
|
|
59
|
+
onStall: (ms) => stalls.push(ms),
|
|
60
|
+
onFatal: (ms) => fatals.push(ms),
|
|
61
|
+
});
|
|
62
|
+
// Wait without blocking — interval fires on time
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
64
|
+
assert.equal(stalls.length, 0, "onStall should not fire for healthy loop");
|
|
65
|
+
assert.equal(fatals.length, 0, "onFatal should not fire for healthy loop");
|
|
66
|
+
stop();
|
|
67
|
+
stop = undefined;
|
|
68
|
+
});
|
|
69
|
+
it("stop function cleans up the interval", () => {
|
|
70
|
+
stop = startWatchdog({
|
|
71
|
+
checkIntervalMs: 30,
|
|
72
|
+
warnThresholdMs: 10,
|
|
73
|
+
fatalThresholdMs: 20,
|
|
74
|
+
onStall: () => { },
|
|
75
|
+
onFatal: () => { },
|
|
76
|
+
});
|
|
77
|
+
// Should not throw on double-stop
|
|
78
|
+
stop();
|
|
79
|
+
stop();
|
|
80
|
+
stop = undefined;
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
//# sourceMappingURL=watchdog.test.js.map
|