@ai-dossier/sched 0.66.0 → 0.67.1

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/README.md CHANGED
@@ -423,6 +423,48 @@ when a tick does nothing (`nothing to do`) — a log that stops growing while
423
423
  the failure mode #679 fixed: the engine exited cleanly after its first tick because its
424
424
  inter-tick sleep handles were unref'd.
425
425
 
426
+ ### Engine exit logging, heartbeat and crash alerts (#945)
427
+
428
+ The engine must never die silently. Every exit the process can observe writes a final
429
+ `engine-exit` journal line with its `reason` (every cause is kept: a crash during a stop
430
+ adds a second line with its stack):
431
+
432
+ | reason | cause | lease |
433
+ |---|---|---|
434
+ | `signal:SIGTERM` / `SIGINT` | graceful stop: finish the tick, agents keep running. A `engine-stopping.json` marker is written first; a second signal exits at once (`signal-repeat`); a hard deadline (120 s) forces `stop-timeout` if the stop never completes | released |
435
+ | `signal-ignored:SIGHUP` | SIGHUP never stops the engine (nohup / a closing terminal must not kill an unattended engine); journaled, engine keeps running | kept |
436
+ | `normal` | the loop returned after a stop request (or `--once` finished its tick) | released |
437
+ | `stop-timeout` | a requested stop was still running at the deadline; forced exit | released |
438
+ | `uncaught-exception` / `unhandled-rejection` | crash, stack in `detail`, exit code 70 | **kept** as the crash marker |
439
+ | `process-exit` | a bare `process.exit()` nothing else logged | released |
440
+ | *(no line)* | SIGKILL / OOM killer — unobservable | left stale |
441
+
442
+ The exit logger is installed immediately after the lease is acquired, so no later startup
443
+ step can exit unlogged. A tick that is blocked in a synchronous call cannot run signal
444
+ handlers, so every synchronous exec on the tick path is timeout-bounded (`defaultExec`: 120 s;
445
+ the engine's own execs have explicit budgets) and the stop is therefore bounded too.
446
+
447
+ The lease's `updated_at` is a heartbeat rewritten (compare-and-swap on the lease id) every
448
+ tick; `sched status` shows it. A live pid whose heartbeat is older than the hung threshold
449
+ (10 x the reconcile interval, never under 5 min; `$DOSSIER_SCHED_HUNG_INTERVALS` overrides
450
+ the multiplier) with unfinished work is an `engine-hung` warning/alert. Visible alerts are
451
+ journaled, printed to stderr, and posted as a comment when a tracking issue is given
452
+ (`--alert-issue <n>` or `$DOSSIER_SCHED_ALERT_ISSUE`; validated at `sched start` startup):
453
+
454
+ - `engine-restarted-after-crash`: a `sched start` reclaimed a lease whose holder died without
455
+ releasing it (crash, SIGKILL, OOM). If the previous engine had begun a stop (marker for the
456
+ same pid) it is reported as `stop-interrupted` (a wedged stop), not a crash.
457
+ - `stale-engine-lease-alert`: the lease holder is dead (`reason=stale-lease`) or alive but
458
+ hung (`reason=engine-hung`) while work is unfinished. Once per episode (an `O_EXCL` marker
459
+ file, so racing watchers cannot double-alert); raised by a watcher — `sched status --alert`
460
+ (cron-able). An unwritable store never makes the watcher throw.
461
+
462
+ **#920's silent exit:** the log ended in `nothing to do`, which is just an idle tick's line.
463
+ Only SIGINT had a handler, so a SIGTERM/SIGHUP (another session's restart, a closing
464
+ terminal) killed the engine with no line and no lease release. That is now logged; if the
465
+ 2026-09-29 cause was SIGKILL/OOM, the missing `engine-exit` plus the stale lease is now the
466
+ documented signature (`journalctl -k` for the OOM window).
467
+
426
468
  ### Zombie-run fencing (#504)
427
469
 
428
470
  The ladder redispatches the SAME run, so a takeover inherits the run id and its milestone
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Visible engine-death alerts (#945 ask 2, #940 ask 2). `sched status` showing
3
+ * a `stale-engine-lease` warning is not an alert — nobody runs it. These
4
+ * helpers journal a durable event AND hand a message to a pluggable notifier
5
+ * (the CLI wires stderr + an optional GitHub comment on a tracking issue), so a
6
+ * dead or hung engine is visible without anyone looking.
7
+ *
8
+ * Triggers, all deduplicated:
9
+ * - `checkStaleLeaseAlert`: lease holder dead + unfinished work → ONE alert
10
+ * per stale episode; lease holder alive but its heartbeat older than the
11
+ * hung threshold → ONE `engine-hung` alert per episode. An episode is
12
+ * remembered by an `O_EXCL` marker file, so two concurrent watchers cannot
13
+ * both alert. Called by watchers (`sched status --alert`, cron-able).
14
+ * - `reportCrashRestart`: a new `sched start` reclaimed a dead holder's lease
15
+ * → `engine-restarted-after-crash` (or `engine-restarted-after-stop-timeout`
16
+ * when the previous engine had begun a graceful stop first).
17
+ *
18
+ * A watcher runs unattended: an unwritable store or journal downgrades to a
19
+ * stderr warning (and possibly a repeat alert) — it never throws.
20
+ */
21
+ import type { Journal } from './journal';
22
+ import type { ReclaimedEngineLease, SchedStore } from './persist';
23
+ export interface EngineAlert {
24
+ kind: 'stale-engine-lease' | 'engine-hung' | 'engine-restarted-after-crash' | 'engine-restarted-after-stop-timeout';
25
+ message: string;
26
+ }
27
+ export type AlertNotifier = (alert: EngineAlert) => void;
28
+ export type StaleLeaseAlertOutcome = 'no-lease' | 'alive' | 'no-work' | 'already-alerted' | 'alerted';
29
+ export declare function checkStaleLeaseAlert(store: SchedStore, journal: Journal, notify: AlertNotifier, now?: Date, opts?: {
30
+ hungAfterMs?: number;
31
+ }): StaleLeaseAlertOutcome;
32
+ export declare function reportCrashRestart(journal: Journal, reclaimed: ReclaimedEngineLease, notify: AlertNotifier, now?: Date, stopping?: {
33
+ signal: string;
34
+ at: string;
35
+ } | null): void;
36
+ //# sourceMappingURL=engine-alert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-alert.d.ts","sourceRoot":"","sources":["../src/engine-alert.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAGlE,MAAM,WAAW,WAAW;IAC1B,IAAI,EACA,oBAAoB,GACpB,aAAa,GACb,8BAA8B,GAC9B,qCAAqC,CAAC;IAC1C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;AAKzD,MAAM,MAAM,sBAAsB,GAC9B,UAAU,GACV,OAAO,GACP,SAAS,GACT,iBAAiB,GACjB,SAAS,CAAC;AA+Bd,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,aAAa,EACrB,GAAG,GAAE,IAAiB,EACtB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,sBAAsB,CAkDxB;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,oBAAoB,EAC/B,MAAM,EAAE,aAAa,EACrB,GAAG,GAAE,IAAiB,EACtB,QAAQ,CAAC,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAC/C,IAAI,CAuBN"}
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ /**
3
+ * Visible engine-death alerts (#945 ask 2, #940 ask 2). `sched status` showing
4
+ * a `stale-engine-lease` warning is not an alert — nobody runs it. These
5
+ * helpers journal a durable event AND hand a message to a pluggable notifier
6
+ * (the CLI wires stderr + an optional GitHub comment on a tracking issue), so a
7
+ * dead or hung engine is visible without anyone looking.
8
+ *
9
+ * Triggers, all deduplicated:
10
+ * - `checkStaleLeaseAlert`: lease holder dead + unfinished work → ONE alert
11
+ * per stale episode; lease holder alive but its heartbeat older than the
12
+ * hung threshold → ONE `engine-hung` alert per episode. An episode is
13
+ * remembered by an `O_EXCL` marker file, so two concurrent watchers cannot
14
+ * both alert. Called by watchers (`sched status --alert`, cron-able).
15
+ * - `reportCrashRestart`: a new `sched start` reclaimed a dead holder's lease
16
+ * → `engine-restarted-after-crash` (or `engine-restarted-after-stop-timeout`
17
+ * when the previous engine had begun a graceful stop first).
18
+ *
19
+ * A watcher runs unattended: an unwritable store or journal downgrades to a
20
+ * stderr warning (and possibly a repeat alert) — it never throws.
21
+ */
22
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ var desc = Object.getOwnPropertyDescriptor(m, k);
25
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
26
+ desc = { enumerable: true, get: function() { return m[k]; } };
27
+ }
28
+ Object.defineProperty(o, k2, desc);
29
+ }) : (function(o, m, k, k2) {
30
+ if (k2 === undefined) k2 = k;
31
+ o[k2] = m[k];
32
+ }));
33
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
35
+ }) : function(o, v) {
36
+ o["default"] = v;
37
+ });
38
+ var __importStar = (this && this.__importStar) || (function () {
39
+ var ownKeys = function(o) {
40
+ ownKeys = Object.getOwnPropertyNames || function (o) {
41
+ var ar = [];
42
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
43
+ return ar;
44
+ };
45
+ return ownKeys(o);
46
+ };
47
+ return function (mod) {
48
+ if (mod && mod.__esModule) return mod;
49
+ var result = {};
50
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
51
+ __setModuleDefault(result, mod);
52
+ return result;
53
+ };
54
+ })();
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.checkStaleLeaseAlert = checkStaleLeaseAlert;
57
+ exports.reportCrashRestart = reportCrashRestart;
58
+ const fs = __importStar(require("node:fs"));
59
+ const path = __importStar(require("node:path"));
60
+ const status_1 = require("./status");
61
+ const ALERT_MARKER_PREFIX = '.alerted-';
62
+ const LEGACY_MARKER_FILE = '.stale-lease-alerted';
63
+ /** Episode ids come from lease ids (`pid-ms-random`); keep the marker name filesystem-safe. */
64
+ function markerName(episode) {
65
+ return `${ALERT_MARKER_PREFIX}${episode.replace(/[^A-Za-z0-9._-]/g, '_')}`;
66
+ }
67
+ function clearMarkers(dir) {
68
+ try {
69
+ for (const f of fs.readdirSync(dir)) {
70
+ if (f.startsWith(ALERT_MARKER_PREFIX) || f === LEGACY_MARKER_FILE) {
71
+ fs.rmSync(path.join(dir, f), { force: true });
72
+ }
73
+ }
74
+ }
75
+ catch {
76
+ // no store dir yet / unreadable: nothing to clear
77
+ }
78
+ }
79
+ /** True when this caller won the episode (marker created); false when it already existed. Throws only on an unwritable store. */
80
+ function claimEpisode(dir, episode) {
81
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
82
+ try {
83
+ fs.closeSync(fs.openSync(path.join(dir, markerName(episode)), 'wx', 0o600));
84
+ return true;
85
+ }
86
+ catch (err) {
87
+ if (err.code === 'EEXIST')
88
+ return false;
89
+ throw err;
90
+ }
91
+ }
92
+ function checkStaleLeaseAlert(store, journal, notify, now = new Date(), opts = {}) {
93
+ const lease = store.engineLeaseStatus();
94
+ if (lease === null) {
95
+ clearMarkers(store.dir);
96
+ return 'no-lease';
97
+ }
98
+ const hungAfterMs = opts.hungAfterMs ?? (0, status_1.resolveHungAfterMs)(0);
99
+ const hungFor = (0, status_1.engineHungForMs)(lease, now, hungAfterMs);
100
+ if (lease.alive && hungFor === null) {
101
+ clearMarkers(store.dir);
102
+ return 'alive';
103
+ }
104
+ const { unfinished, liveSlots } = (0, status_1.countUnfinishedWork)(store.load());
105
+ if (unfinished === 0 && liveSlots === 0)
106
+ return 'no-work';
107
+ const hung = lease.alive;
108
+ const baseEpisode = lease.id ?? `pid-${lease.pid}`;
109
+ const episode = hung ? `${baseEpisode}-hung` : baseEpisode;
110
+ try {
111
+ if (!claimEpisode(store.dir, episode))
112
+ return 'already-alerted';
113
+ }
114
+ catch (err) {
115
+ // Cannot remember the episode: still alert (a repeat beats silence).
116
+ process.stderr.write(`⚠ sched alert: could not record alert marker: ${err.message}\n`);
117
+ }
118
+ const queue = `${unfinished} queue entr${unfinished === 1 ? 'y is' : 'ies are'} unfinished and ${liveSlots} slot(s) live`;
119
+ const message = hung
120
+ ? `sched engine looks HUNG: pid ${lease.pid} is alive but its last heartbeat was ${lease.updated_at} (${Math.round((hungFor ?? 0) / 60_000)} min ago) while ${queue} — a tick is probably wedged. Stop it (SIGTERM; SIGKILL if it ignores that) and run \`ai-dossier sched start\`.`
121
+ : `sched engine is DOWN: lease holder pid ${lease.pid} is not running${lease.updated_at ? ` (last heartbeat ${lease.updated_at})` : ''} while ${queue} — nothing is ticking. Restart with \`ai-dossier sched start\`.`;
122
+ try {
123
+ journal.append({
124
+ event: 'stale-engine-lease-alert',
125
+ pid: lease.pid,
126
+ reason: hung ? 'engine-hung' : 'stale-lease',
127
+ detail: message,
128
+ }, now);
129
+ }
130
+ catch (err) {
131
+ process.stderr.write(`⚠ sched alert: could not journal alert: ${err.message}\n`);
132
+ }
133
+ try {
134
+ notify({ kind: hung ? 'engine-hung' : 'stale-engine-lease', message });
135
+ }
136
+ catch (err) {
137
+ process.stderr.write(`⚠ sched alert notifier failed: ${err.message}\n`);
138
+ }
139
+ return 'alerted';
140
+ }
141
+ function reportCrashRestart(journal, reclaimed, notify, now = new Date(), stopping) {
142
+ const since = reclaimed.updated_at ? `; its last heartbeat was ${reclaimed.updated_at}` : '';
143
+ const stopGoneWrong = stopping !== undefined && stopping !== null;
144
+ const message = stopGoneWrong
145
+ ? `sched engine restarted after a stop that never finished: previous engine pid ${reclaimed.pid} began a graceful stop on ${stopping.signal} at ${stopping.at} and was then killed${since}. This is a wedged stop, not a crash.`
146
+ : `sched engine restarted after an unexpected exit: previous engine pid ${reclaimed.pid} died without releasing its lease${since}. Check the journal for its final \`engine-exit\` line (none means SIGKILL/OOM).`;
147
+ journal.append({
148
+ event: 'engine-restarted-after-crash',
149
+ pid: reclaimed.pid,
150
+ reason: stopGoneWrong ? 'stop-interrupted' : 'lease-reclaimed',
151
+ detail: message,
152
+ }, now);
153
+ try {
154
+ notify({
155
+ kind: stopGoneWrong ? 'engine-restarted-after-stop-timeout' : 'engine-restarted-after-crash',
156
+ message,
157
+ });
158
+ }
159
+ catch (err) {
160
+ process.stderr.write(`⚠ sched alert notifier failed: ${err.message}\n`);
161
+ }
162
+ }
163
+ //# sourceMappingURL=engine-alert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-alert.js","sourceRoot":"","sources":["../src/engine-alert.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Engine termination logging (#945, #940 ask 3): the engine must never die
3
+ * silently. Every exit the process can observe writes one final `engine-exit`
4
+ * journal line with the reason (and stack for a crash):
5
+ *
6
+ * signal:<SIG> SIGTERM / SIGINT — graceful stop is requested (the loop
7
+ * finishes its tick, agents keep running). A `stopping`
8
+ * marker is written first, a second signal exits at once,
9
+ * and a hard deadline (`stopTimeoutMs`) bounds a wedged
10
+ * stop (`stop-timeout`)
11
+ * signal-ignored:SIGHUP SIGHUP never stops the engine (nohup / a closing terminal
12
+ * must not kill an unattended engine); journaled, ignored
13
+ * stop-timeout a stop was requested but the process was still alive at
14
+ * the deadline; forced exit, lease released (NOT a crash)
15
+ * uncaught-exception journaled with stack, exit 70, lease deliberately NOT
16
+ * unhandled-rejection released so the next start / watcher sees a crash (also
17
+ * journaled when it follows a signal — every cause is kept)
18
+ * normal the loop returned after a stop request
19
+ * process-exit a bare `process.exit()` nobody else logged
20
+ *
21
+ * #920: the engine's log ended in "nothing to do" (an idle tick's line) with no
22
+ * error. Only SIGINT had a handler, so a SIGTERM/SIGHUP (another session's
23
+ * restart, a closing terminal) killed it with no log line and no lease release.
24
+ * SIGKILL/OOM remain unobservable by design — the stale heartbeat lease is what
25
+ * catches those (`engine-restarted-after-crash`, `stale-engine-lease-alert`).
26
+ */
27
+ import type { Journal } from './journal';
28
+ /** Written on the FIRST stop signal: a later SIGKILL/timeout is a stop gone wrong, not a crash. */
29
+ export interface StoppingMarker {
30
+ pid: number;
31
+ signal: string;
32
+ at: string;
33
+ }
34
+ export declare function writeStoppingMarker(dir: string, marker: StoppingMarker): void;
35
+ export declare function readStoppingMarker(dir: string): StoppingMarker | null;
36
+ export declare function clearStoppingMarker(dir: string): void;
37
+ /** Deadline for a requested stop to finish before the engine forces its own exit. */
38
+ export declare const DEFAULT_STOP_TIMEOUT_MS = 120000;
39
+ /** The slice of `process` the installer needs — injectable so tests never signal the test runner. */
40
+ export interface ExitProcess {
41
+ pid: number;
42
+ on(event: string, listener: (...args: never[]) => void): unknown;
43
+ removeListener(event: string, listener: (...args: never[]) => void): unknown;
44
+ exit(code?: number): never;
45
+ }
46
+ export interface EngineExitLogger {
47
+ /** Journal a graceful, loop-returned exit (`normal`). Idempotent with the rest. */
48
+ logNormalExit(detail?: string): void;
49
+ /** False after a crash: the lease must stay behind as the crash marker. */
50
+ shouldReleaseLease(): boolean;
51
+ /** The recorded reason, or null before any exit path fired. */
52
+ reason(): string | null;
53
+ dispose(): void;
54
+ }
55
+ export declare function installEngineExitLogging(opts: {
56
+ journal: Journal;
57
+ /** Ask the loop to stop after the current tick (graceful). */
58
+ requestStop: () => void;
59
+ /** Called once on the first stop signal, before `requestStop` (writes the `stopping` marker). */
60
+ markStopping?: (signal: string) => void;
61
+ /** Hard deadline for a requested stop (default {@link DEFAULT_STOP_TIMEOUT_MS}); 0 disables. */
62
+ stopTimeoutMs?: number;
63
+ proc?: ExitProcess;
64
+ now?: () => Date;
65
+ /** Timer factory — injectable so tests never wait out a real deadline. */
66
+ setTimer?: (fn: () => void, ms: number) => {
67
+ unref(): unknown;
68
+ };
69
+ }): EngineExitLogger;
70
+ //# sourceMappingURL=engine-exit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-exit.d.ts","sourceRoot":"","sources":["../src/engine-exit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIzC,mGAAmG;AACnG,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,IAAI,CAS7E;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAWrE;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAErD;AAED,qFAAqF;AACrF,eAAO,MAAM,uBAAuB,SAAU,CAAC;AAE/C,qGAAqG;AACrG,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC;IACjE,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC;IAC7E,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,mFAAmF;IACnF,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,2EAA2E;IAC3E,kBAAkB,IAAI,OAAO,CAAC;IAC9B,+DAA+D;IAC/D,MAAM,IAAI,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,IAAI,IAAI,CAAC;CACjB;AAkBD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE,OAAO,CAAC;IACjB,8DAA8D;IAC9D,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,iGAAiG;IACjG,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,gGAAgG;IAChG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK;QAAE,KAAK,IAAI,OAAO,CAAA;KAAE,CAAC;CACjE,GAAG,gBAAgB,CAiGnB"}
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ /**
3
+ * Engine termination logging (#945, #940 ask 3): the engine must never die
4
+ * silently. Every exit the process can observe writes one final `engine-exit`
5
+ * journal line with the reason (and stack for a crash):
6
+ *
7
+ * signal:<SIG> SIGTERM / SIGINT — graceful stop is requested (the loop
8
+ * finishes its tick, agents keep running). A `stopping`
9
+ * marker is written first, a second signal exits at once,
10
+ * and a hard deadline (`stopTimeoutMs`) bounds a wedged
11
+ * stop (`stop-timeout`)
12
+ * signal-ignored:SIGHUP SIGHUP never stops the engine (nohup / a closing terminal
13
+ * must not kill an unattended engine); journaled, ignored
14
+ * stop-timeout a stop was requested but the process was still alive at
15
+ * the deadline; forced exit, lease released (NOT a crash)
16
+ * uncaught-exception journaled with stack, exit 70, lease deliberately NOT
17
+ * unhandled-rejection released so the next start / watcher sees a crash (also
18
+ * journaled when it follows a signal — every cause is kept)
19
+ * normal the loop returned after a stop request
20
+ * process-exit a bare `process.exit()` nobody else logged
21
+ *
22
+ * #920: the engine's log ended in "nothing to do" (an idle tick's line) with no
23
+ * error. Only SIGINT had a handler, so a SIGTERM/SIGHUP (another session's
24
+ * restart, a closing terminal) killed it with no log line and no lease release.
25
+ * SIGKILL/OOM remain unobservable by design — the stale heartbeat lease is what
26
+ * catches those (`engine-restarted-after-crash`, `stale-engine-lease-alert`).
27
+ */
28
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
29
+ if (k2 === undefined) k2 = k;
30
+ var desc = Object.getOwnPropertyDescriptor(m, k);
31
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
32
+ desc = { enumerable: true, get: function() { return m[k]; } };
33
+ }
34
+ Object.defineProperty(o, k2, desc);
35
+ }) : (function(o, m, k, k2) {
36
+ if (k2 === undefined) k2 = k;
37
+ o[k2] = m[k];
38
+ }));
39
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
40
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
41
+ }) : function(o, v) {
42
+ o["default"] = v;
43
+ });
44
+ var __importStar = (this && this.__importStar) || (function () {
45
+ var ownKeys = function(o) {
46
+ ownKeys = Object.getOwnPropertyNames || function (o) {
47
+ var ar = [];
48
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
49
+ return ar;
50
+ };
51
+ return ownKeys(o);
52
+ };
53
+ return function (mod) {
54
+ if (mod && mod.__esModule) return mod;
55
+ var result = {};
56
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
57
+ __setModuleDefault(result, mod);
58
+ return result;
59
+ };
60
+ })();
61
+ Object.defineProperty(exports, "__esModule", { value: true });
62
+ exports.DEFAULT_STOP_TIMEOUT_MS = void 0;
63
+ exports.writeStoppingMarker = writeStoppingMarker;
64
+ exports.readStoppingMarker = readStoppingMarker;
65
+ exports.clearStoppingMarker = clearStoppingMarker;
66
+ exports.installEngineExitLogging = installEngineExitLogging;
67
+ const fs = __importStar(require("node:fs"));
68
+ const path = __importStar(require("node:path"));
69
+ const STOPPING_MARKER_FILE = 'engine-stopping.json';
70
+ function writeStoppingMarker(dir, marker) {
71
+ try {
72
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
73
+ fs.writeFileSync(path.join(dir, STOPPING_MARKER_FILE), `${JSON.stringify(marker)}\n`, {
74
+ mode: 0o600,
75
+ });
76
+ }
77
+ catch {
78
+ // Advisory: a missing marker only downgrades "stop gone wrong" to "crash".
79
+ }
80
+ }
81
+ function readStoppingMarker(dir) {
82
+ try {
83
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, STOPPING_MARKER_FILE), 'utf8'));
84
+ if (raw === null || typeof raw !== 'object')
85
+ return null;
86
+ const m = raw;
87
+ return typeof m.pid === 'number' && typeof m.signal === 'string' && typeof m.at === 'string'
88
+ ? { pid: m.pid, signal: m.signal, at: m.at }
89
+ : null;
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ }
95
+ function clearStoppingMarker(dir) {
96
+ fs.rmSync(path.join(dir, STOPPING_MARKER_FILE), { force: true });
97
+ }
98
+ /** Deadline for a requested stop to finish before the engine forces its own exit. */
99
+ exports.DEFAULT_STOP_TIMEOUT_MS = 120_000;
100
+ const CRASH_EXIT_CODE = 70;
101
+ const STOP_SIGNALS = ['SIGTERM', 'SIGINT'];
102
+ const SIGNAL_NUMBERS = {
103
+ SIGINT: 2,
104
+ SIGTERM: 15,
105
+ };
106
+ function describeError(err) {
107
+ if (err instanceof Error)
108
+ return err.stack ?? `${err.name}: ${err.message}`;
109
+ try {
110
+ return typeof err === 'string' ? err : JSON.stringify(err);
111
+ }
112
+ catch {
113
+ return String(err);
114
+ }
115
+ }
116
+ function installEngineExitLogging(opts) {
117
+ const proc = opts.proc ?? process;
118
+ const now = opts.now ?? (() => new Date());
119
+ let recorded = null;
120
+ let crashed = false;
121
+ let signalled = false;
122
+ const append = (reason, detail) => {
123
+ try {
124
+ opts.journal.append({ event: 'engine-exit', pid: proc.pid, reason, ...(detail ? { detail } : {}) }, now());
125
+ }
126
+ catch {
127
+ // The exit path must still exit: an unwritable journal cannot block it.
128
+ }
129
+ };
130
+ const record = (reason, detail) => {
131
+ if (recorded !== null)
132
+ return;
133
+ recorded = reason;
134
+ append(reason, detail);
135
+ };
136
+ /** A crash is always journaled (with its stack), even after an earlier signal line. */
137
+ const recordCrash = (reason, detail) => {
138
+ crashed = true;
139
+ if (recorded === null)
140
+ recorded = reason;
141
+ append(reason, detail);
142
+ };
143
+ const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
144
+ const stopTimeoutMs = opts.stopTimeoutMs ?? exports.DEFAULT_STOP_TIMEOUT_MS;
145
+ const onSignal = (signal) => () => {
146
+ if (signalled) {
147
+ record('signal-repeat', `second ${signal}: exiting immediately`);
148
+ proc.exit(128 + SIGNAL_NUMBERS[signal]);
149
+ return;
150
+ }
151
+ signalled = true;
152
+ try {
153
+ opts.markStopping?.(signal);
154
+ }
155
+ catch {
156
+ // advisory
157
+ }
158
+ record(`signal:${signal}`, 'graceful stop requested; spawned agents keep running');
159
+ if (stopTimeoutMs > 0) {
160
+ setTimer(() => {
161
+ append('stop-timeout', `stop requested by ${signal} ${stopTimeoutMs} ms ago but the engine is still running (wedged tick?); forcing exit`);
162
+ proc.exit(128 + SIGNAL_NUMBERS[signal]);
163
+ }, stopTimeoutMs).unref();
164
+ }
165
+ opts.requestStop();
166
+ };
167
+ const handlers = STOP_SIGNALS.map((sig) => [sig, onSignal(sig)]);
168
+ // A SIGHUP handler must exist: Node's default (terminate) would kill a nohup'd
169
+ // engine, and installing any handler overrides an inherited SIG_IGN anyway.
170
+ const onHup = () => {
171
+ append('signal-ignored:SIGHUP', 'SIGHUP received; ignored — the engine keeps running');
172
+ };
173
+ const onUncaught = (err) => {
174
+ recordCrash('uncaught-exception', describeError(err));
175
+ process.stderr.write(`⚠ sched engine crashed: ${describeError(err)}\n`);
176
+ proc.exit(CRASH_EXIT_CODE);
177
+ };
178
+ const onRejection = (err) => {
179
+ recordCrash('unhandled-rejection', describeError(err));
180
+ process.stderr.write(`⚠ sched engine crashed (unhandled rejection): ${describeError(err)}\n`);
181
+ proc.exit(CRASH_EXIT_CODE);
182
+ };
183
+ const onExit = (code) => {
184
+ record('process-exit', `exit code ${code}`);
185
+ };
186
+ for (const [sig, h] of handlers)
187
+ proc.on(sig, h);
188
+ proc.on('SIGHUP', onHup);
189
+ proc.on('uncaughtException', onUncaught);
190
+ proc.on('unhandledRejection', onRejection);
191
+ proc.on('exit', onExit);
192
+ return {
193
+ logNormalExit: (detail) => record('normal', detail),
194
+ shouldReleaseLease: () => !crashed,
195
+ reason: () => recorded,
196
+ dispose: () => {
197
+ for (const [sig, h] of handlers)
198
+ proc.removeListener(sig, h);
199
+ proc.removeListener('SIGHUP', onHup);
200
+ proc.removeListener('uncaughtException', onUncaught);
201
+ proc.removeListener('unhandledRejection', onRejection);
202
+ proc.removeListener('exit', onExit);
203
+ },
204
+ };
205
+ }
206
+ //# sourceMappingURL=engine-exit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-exit.js","sourceRoot":"","sources":["../src/engine-exit.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
package/dist/engine.js CHANGED
@@ -2756,6 +2756,9 @@ async function runLoop(deps, configSource, shouldStop, onTick) {
2756
2756
  const resolveConfig = typeof configSource === 'function' ? configSource : () => configSource;
2757
2757
  const interval = (0, dispatch_1.resolveDispatch)(resolveConfig()).reconcileIntervalMs;
2758
2758
  while (!shouldStop()) {
2759
+ // #945: heartbeat every tick, before the work — a wedged tick then shows as
2760
+ // a stale `updated_at` on a live pid, and an idle one as a fresh one.
2761
+ deps.store.touchEngineLease?.(deps.now());
2759
2762
  try {
2760
2763
  const result = tick(deps, resolveConfig());
2761
2764
  onTick?.(result);