bareloop 0.32.0 → 0.33.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/CHANGELOG.md +79 -0
- package/README.md +4 -1
- package/bareloop.context.md +150 -77
- package/package.json +1 -1
- package/src/authorflow.js +5 -3
- package/src/authorjob.js +3 -1
- package/src/authorrun.js +36 -8
- package/src/authorscout.js +10 -6
- package/src/bundlerun.js +266 -0
- package/src/cli.js +24 -352
- package/src/interviewrun.js +3 -2
- package/src/invoke.js +22 -0
- package/src/judged.js +5 -3
- package/src/monthly.js +92 -22
- package/src/panel/authorsession.js +24 -4
- package/src/planrun.js +7 -3
- package/src/providerrows.js +85 -1
- package/src/run.js +7 -2
- package/src/runlist.js +79 -7
- package/src/source.js +8 -3
- package/src/u-watchdog.mjs +296 -0
- package/src/userrun.js +186 -64
- package/types/authorflow.d.ts +8 -2
- package/types/authorjob.d.ts +6 -1
- package/types/authorscout.d.ts +11 -2
- package/types/bundlerun.d.ts +26 -0
- package/types/invoke.d.ts +8 -0
- package/types/judged.d.ts +5 -1
- package/types/monthly.d.ts +43 -8
- package/types/planrun.d.ts +13 -1
- package/types/providerrows.d.ts +65 -0
- package/types/run.d.ts +14 -1
- package/types/runlist.d.ts +65 -3
- package/types/source.d.ts +8 -3
- package/types/userrun.d.ts +49 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// The OUTSIDE watchdog (F67) — the only stop that survives a frozen run.
|
|
2
|
+
//
|
|
3
|
+
// MEASURED reason it exists. U run ms3jh76q spent 81.5 minutes emitting nothing,
|
|
4
|
+
// then died `read ETIMEDOUT` — the kernel giving up on a socket the process never
|
|
5
|
+
// read. The in-process stall fuse (F66) did not fire once, and it is not broken:
|
|
6
|
+
// firing was reproduced against that run's exact shape (four rounds, then
|
|
7
|
+
// silence). It never RAN, because a timer in the same event loop as whatever
|
|
8
|
+
// froze that loop shares its fate. Every guard bareloop had lived inside the run:
|
|
9
|
+
//
|
|
10
|
+
// bare-agent's timeoutMs idle SOCKET timer — resets on bytes, saw nothing wrong
|
|
11
|
+
// the wall clock read BETWEEN rounds — no round ever arrived
|
|
12
|
+
// the stall fuse (F66) a setTimeout — cannot fire on a blocked event loop
|
|
13
|
+
//
|
|
14
|
+
// So this one is a separate process and shares nothing with the run. It reads one
|
|
15
|
+
// file's mtime and calls kill(2). That is the whole design.
|
|
16
|
+
//
|
|
17
|
+
// It is deliberately DUMB, and each omission is load-bearing:
|
|
18
|
+
// - it parses no JSON (a malformed spine must not blind the guard)
|
|
19
|
+
// - it imports nothing from src/ (no shared code means no shared failure)
|
|
20
|
+
// - it holds no model of what the run is doing (nothing to get out of sync)
|
|
21
|
+
// - it reads bytes, not meaning (cost is one stat(2) per poll)
|
|
22
|
+
//
|
|
23
|
+
// TWO triggers, ONE reconciled decision (hamr's ruling, 2026-07-30: "the kill from
|
|
24
|
+
// outside should check for activity/bytes or other markers for activity, not a
|
|
25
|
+
// silent kill"):
|
|
26
|
+
// STALE no progress for longer than the worst LEGAL silence → the run is wedged
|
|
27
|
+
// DEAD past the deadline (wall + stages × close timeout) AND the spine flat for
|
|
28
|
+
// the whole dead window → the process is producing nothing, and its own
|
|
29
|
+
// deadline has already funded every legal silence
|
|
30
|
+
// Past the deadline but still ACTIVE is NOT a kill any more. The in-process fuses
|
|
31
|
+
// and the money cap own bounding a run that is alive; this guard owns processes
|
|
32
|
+
// that are dead. A live run is never killed from outside purely by the clock —
|
|
33
|
+
// that is the point of the change, and the previous behaviour could destroy a
|
|
34
|
+
// verdict mid-close. Overshoot is reported LOUD on every poll instead. The full
|
|
35
|
+
// decision table sits above the decision code.
|
|
36
|
+
//
|
|
37
|
+
// The kill is RECORDED next to the spine, and it is never silent: before the
|
|
38
|
+
// signal goes out this process says which deadline passed, which marker it
|
|
39
|
+
// checked, its last value and age, and why it judged the process dead. A run
|
|
40
|
+
// stopped by the arbiter must never read as a mystery crash — that is the
|
|
41
|
+
// distinction between a governance stop and a casualty, and it decides whether the
|
|
42
|
+
// row is evidence or noise (F45/F48).
|
|
43
|
+
//
|
|
44
|
+
// Usage:
|
|
45
|
+
// node src/u-watchdog.mjs --spine <path> --pid <n>
|
|
46
|
+
// [--stale-ms N] [--wall-ms N] [--grace-ms N] [--dead-ms N] [--poll-ms N] [--term-grace-ms N]
|
|
47
|
+
import { statSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
|
|
48
|
+
|
|
49
|
+
const arg = (/** @type {string} */ n, /** @type {string|null} */ dflt = null) => {
|
|
50
|
+
const i = process.argv.indexOf(`--${n}`);
|
|
51
|
+
return i === -1 ? dflt : (process.argv[i + 1] ?? dflt);
|
|
52
|
+
};
|
|
53
|
+
// A flag the operator PASSED is never quietly replaced by a default. An ABSENT flag
|
|
54
|
+
// takes its default (`--wall-ms` omitted is the legal unbounded choice, and run-u
|
|
55
|
+
// omits it on a spec with no `maxWallMs`); a flag that is present and unreadable —
|
|
56
|
+
// `0`, a negative, `undefined`, `soon` — is an operator error and stops this process
|
|
57
|
+
// LOUD. Silently defaulting it disarms a trigger while the caller's own banner still
|
|
58
|
+
// claims the bound, which is the guard reading zero and saying nothing: the
|
|
59
|
+
// blind-instrument class, and W-2's case besides (a wall remainder of nothing is a
|
|
60
|
+
// decision, not an unbounded launch).
|
|
61
|
+
const num = (/** @type {string} */ n, /** @type {number|null} */ dflt) => {
|
|
62
|
+
const v = arg(n);
|
|
63
|
+
if (v === null) return dflt;
|
|
64
|
+
const x = Number(v);
|
|
65
|
+
if (!Number.isFinite(x) || x <= 0) {
|
|
66
|
+
console.error(`[u-watchdog] REFUSED: --${n} ${JSON.stringify(v)} is not a positive number of milliseconds. It is NOT defaulted — a defaulted bound disarms this guard's trigger while the run's banner still advertises one. Pass a real remainder, or omit the flag to take the default deliberately (an omitted --wall-ms is the unbounded choice, and the STALE trigger stays armed either way).`);
|
|
67
|
+
process.exit(2);
|
|
68
|
+
}
|
|
69
|
+
return x;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const spine = arg('spine');
|
|
73
|
+
const pid = Number(arg('pid'));
|
|
74
|
+
if (!spine || !Number.isInteger(pid) || pid <= 0) {
|
|
75
|
+
console.error('usage: u-watchdog.mjs --spine <path> --pid <n> [--stale-ms N] [--wall-ms N] [--grace-ms N] [--dead-ms N] [--poll-ms N] [--term-grace-ms N]');
|
|
76
|
+
process.exit(2);
|
|
77
|
+
}
|
|
78
|
+
// The watched pid must be this process's PARENT. kill(pid, 0) answers "is SOME
|
|
79
|
+
// process holding this pid" — after a SIGKILL/OOM the kernel can recycle the
|
|
80
|
+
// run's pid onto a stranger, and an armed guard would SIGTERM/SIGKILL it and
|
|
81
|
+
// leave a marker blaming the run. The parent link cannot be recycled: run-u
|
|
82
|
+
// spawns this guard as its direct child, and `process.ppid` flips to init
|
|
83
|
+
// within one poll of the parent dying — verified live, including under SIGKILL.
|
|
84
|
+
// A guard pointed at a non-parent refuses LOUD at startup (exit 2, no marker):
|
|
85
|
+
// an armed guard aimed at a stranger is worse than no guard, and a silent no-op
|
|
86
|
+
// is the blind-instrument class.
|
|
87
|
+
if (process.ppid !== pid) {
|
|
88
|
+
console.error(`[u-watchdog] REFUSED: --pid ${pid} is not this process's parent (ppid ${process.ppid}) — the liveness check is the parent link, which pid reuse cannot forge`);
|
|
89
|
+
process.exit(2);
|
|
90
|
+
}
|
|
91
|
+
// Defaults are DERIVED, not chosen: the stale window is twice the in-process fuse
|
|
92
|
+
// (F66, hamr's 5 min), so the inner guard always gets its three tries first and
|
|
93
|
+
// this one only speaks when the inner one could not. It stays a flag because a
|
|
94
|
+
// threshold is arbiter territory — a number the agent picks off its own sample is
|
|
95
|
+
// fitted, not set.
|
|
96
|
+
const staleMs = num('stale-ms', 600_000) ?? 600_000;
|
|
97
|
+
const wallMs = num('wall-ms', null);
|
|
98
|
+
// The wall's grace covers the CLOSE, which legitimately runs after the last round
|
|
99
|
+
// and is the longest legal silence a healthy run produces. It is sized the same way
|
|
100
|
+
// the run's own clock sizes its enforced deadline (src/clock.js: maxWallMs +
|
|
101
|
+
// closeStages × closeTimeoutMs) and the same way the stale window is sized —
|
|
102
|
+
// STAGES × the close timeout, because a staged close (PRD v1.28) gives EVERY stage
|
|
103
|
+
// the full timeout. The 900_000 default is ONE stage: the single-predicate floor,
|
|
104
|
+
// not a number any staged job should run on. A caller with a staged close passes
|
|
105
|
+
// its own (scripts/run-u.mjs does; a 4-stage close needs 60min, not 15min, and the
|
|
106
|
+
// stale window already got this arithmetic under F67 while the grace did not).
|
|
107
|
+
const graceMs = num('grace-ms', 900_000) ?? 900_000;
|
|
108
|
+
// How long the spine must be flat, PAST the deadline, before the process is judged
|
|
109
|
+
// dead. Derived, not chosen: it is the in-process stall fuse's own window (F66,
|
|
110
|
+
// hamr's 5 minutes). Past the deadline the grace has ALREADY paid
|
|
111
|
+
// for every legal close stage, so there is no legal silence left to protect — one
|
|
112
|
+
// fuse window is patience, not budget. Never above the stale window, which would
|
|
113
|
+
// make it inert.
|
|
114
|
+
const deadMs = Math.min(num('dead-ms', 300_000) ?? 300_000, staleMs);
|
|
115
|
+
const pollMs = num('poll-ms', 5_000) ?? 5_000;
|
|
116
|
+
// How long SIGTERM gets before SIGKILL. A flag, not a constant, for the same reason
|
|
117
|
+
// every other window here is one — and because a test cannot exercise the escalation
|
|
118
|
+
// against a ten-minute wait. The DEFAULT is unchanged (10s): long enough for a live run
|
|
119
|
+
// to flush its spine, short enough that a run swallowing the polite signal cannot
|
|
120
|
+
// outlive its own guard.
|
|
121
|
+
const termGraceMs = num('term-grace-ms', 10_000) ?? 10_000;
|
|
122
|
+
|
|
123
|
+
const startedAt = Date.now();
|
|
124
|
+
const iso = (/** @type {number} */ ms) => new Date(ms).toISOString();
|
|
125
|
+
// Liveness is the PARENT LINK, not kill(pid, 0): once the parent dies this
|
|
126
|
+
// process is reparented and ppid stops matching — reuse-proof where a pid probe
|
|
127
|
+
// is not (see the startup refusal above).
|
|
128
|
+
const alive = () => process.ppid === pid;
|
|
129
|
+
|
|
130
|
+
// ── THE ACTIVITY MARKER ─────────────────────────────────────────────────────────
|
|
131
|
+
// "Is it dead" is answered by EVIDENCE, never by the clock alone. ONE marker:
|
|
132
|
+
//
|
|
133
|
+
// spine PROGRESS. Bytes reaching the run's own append-only log. Growth here is
|
|
134
|
+
// the run doing the job. One stat(2): size OR mtime moving counts.
|
|
135
|
+
//
|
|
136
|
+
// ONE on purpose. A second, CPU-liveness marker (utime+stime+cutime+cstime from
|
|
137
|
+
// /proc/<pid>/stat) was built here and then MEASURED broken in BOTH directions, so
|
|
138
|
+
// it is deleted rather than kept as decoration:
|
|
139
|
+
// - it read DEAD on a live run. A close runs its suite in a CHILD, and a child's
|
|
140
|
+
// ticks are credited to the parent only when the parent REAPS it — so the
|
|
141
|
+
// busiest minutes of a multi-minute close show a parent whose own ticks never
|
|
142
|
+
// move.
|
|
143
|
+
// - it read ALIVE on a dead one. run-u's event-loop lag sampler wakes this very
|
|
144
|
+
// process every second, moving its ticks roughly every 3.3s; at the production
|
|
145
|
+
// constants (deadMs 300_000 against pollMs 5_000) the marker could therefore
|
|
146
|
+
// never go flat, and the deadline kill could effectively never fire. The kill
|
|
147
|
+
// tests only passed at --dead-ms 500 — three orders of magnitude below what
|
|
148
|
+
// production runs on.
|
|
149
|
+
// An instrument that cannot see the variable makes every reading unusable, and a
|
|
150
|
+
// veto that is always ON is not a safety, it is a disarmed trigger. hamr's ruling,
|
|
151
|
+
// 2026-07-30: "keep what is simpler/available" — the spine, which is the marker
|
|
152
|
+
// that actually distinguishes progress from silence.
|
|
153
|
+
|
|
154
|
+
/** spine size+mtime in one stat(2). A missing file reads as the watchdog's own
|
|
155
|
+
* start: a run that wedges BEFORE its first event is the most stalled run there
|
|
156
|
+
* is, and treating "no file" as "no news is good news" would blind the guard to
|
|
157
|
+
* exactly that case. */
|
|
158
|
+
const readSpine = () => { try { const st = statSync(spine); return { mtimeMs: st.mtimeMs, size: st.size }; } catch { return { mtimeMs: startedAt, size: -1 }; } };
|
|
159
|
+
|
|
160
|
+
let spineSeen = readSpine();
|
|
161
|
+
let spineMovedAt = spineSeen.mtimeMs;
|
|
162
|
+
|
|
163
|
+
/** advance the marker's last-movement stamp. Called once per poll, before any
|
|
164
|
+
* decision reads it. */
|
|
165
|
+
function sampleMarkers(/** @type {number} */ now) {
|
|
166
|
+
const st = readSpine();
|
|
167
|
+
if (st.size !== spineSeen.size) spineMovedAt = now;
|
|
168
|
+
if (st.mtimeMs > spineMovedAt) spineMovedAt = st.mtimeMs;
|
|
169
|
+
spineSeen = st;
|
|
170
|
+
}
|
|
171
|
+
/** what every log line and every kill record says about the marker: its value and
|
|
172
|
+
* its age. Kept under a `markers` key — the record is a read-once forensic
|
|
173
|
+
* artifact, and a reader comparing an old kill to a new one should see the shape
|
|
174
|
+
* hold and the CPU entry simply be gone. */
|
|
175
|
+
const markerReport = (/** @type {number} */ now) => ({
|
|
176
|
+
spine: { path: spine, bytes: spineSeen.size < 0 ? 'no file yet' : spineSeen.size, lastMovedAt: iso(spineMovedAt), coldMs: now - spineMovedAt },
|
|
177
|
+
});
|
|
178
|
+
const spineColdMs = (/** @type {number} */ now) => now - spineMovedAt;
|
|
179
|
+
|
|
180
|
+
function stop(/** @type {string} */ reason, /** @type {string} */ judgement, /** @type {object} */ detail) {
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
const markers = markerReport(now);
|
|
183
|
+
const record = {
|
|
184
|
+
watchdog: 'u-watchdog', reason, killed: true, pid, spine,
|
|
185
|
+
// Was the target still THERE when the decision was taken (F72 park B — the checks
|
|
186
|
+
// that ran before the signal belong in the record, not only in the reasoning). It
|
|
187
|
+
// is the parent link, not `kill(pid, 0)`: a pid probe answers "is SOME process
|
|
188
|
+
// holding this pid" and cannot survive pid reuse (see the startup refusal). A
|
|
189
|
+
// `false` here says the run died on its own between the last poll and this line —
|
|
190
|
+
// a different event from stopping a running process, and the record must be able
|
|
191
|
+
// to say which one happened.
|
|
192
|
+
pidAlive: alive(),
|
|
193
|
+
at: iso(now),
|
|
194
|
+
startedAt: iso(startedAt),
|
|
195
|
+
elapsedMs: now - startedAt,
|
|
196
|
+
// Every bound this guard was ARMED with, so the decision is reproducible from the
|
|
197
|
+
// record alone — including the two that shape what happens next (`pollMs`, which
|
|
198
|
+
// says how many flat observations the age above represents, and `termGraceMs`,
|
|
199
|
+
// which says how long the run had to die politely).
|
|
200
|
+
staleMs, wallMs, graceMs, deadMs, pollMs, termGraceMs,
|
|
201
|
+
lastEventAt: iso(spineMovedAt),
|
|
202
|
+
judgement, markers,
|
|
203
|
+
...detail,
|
|
204
|
+
};
|
|
205
|
+
// Write the verdict BEFORE the kill: if this process dies mid-stop, the record of why
|
|
206
|
+
// must already be on disk. A kill with no explanation is the failure mode this whole
|
|
207
|
+
// file exists to prevent.
|
|
208
|
+
//
|
|
209
|
+
// TEMP + RENAME, because the record is read by a script that runs after a crash
|
|
210
|
+
// (run-u.mjs / run-reuse.mjs both `JSON.parse` it): rename is atomic within a
|
|
211
|
+
// directory, so a reader can only ever see the whole file or no file — never the
|
|
212
|
+
// half-written one a direct write leaves if this process is killed mid-write.
|
|
213
|
+
//
|
|
214
|
+
// And it is WRAPPED, because a guard that can be disarmed through its own logging is
|
|
215
|
+
// not a guard (F70: the instrument carrying the failure mode it exists to catch). The
|
|
216
|
+
// kill below runs whether or not this succeeded; a failure is named on stderr so a
|
|
217
|
+
// missing marker is never read as "the guard never fired".
|
|
218
|
+
const reportPath = `${spine}.watchdog.json`;
|
|
219
|
+
const tmpPath = `${reportPath}.tmp`;
|
|
220
|
+
try {
|
|
221
|
+
writeFileSync(tmpPath, `${JSON.stringify(record, null, 2)}\n`);
|
|
222
|
+
renameSync(tmpPath, reportPath);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
console.error(`[u-watchdog] REPORT WRITE FAILED (${reportPath}): ${e?.message ?? e} — the kill still proceeds; this stderr IS the record now`);
|
|
225
|
+
try { unlinkSync(tmpPath); } catch { /* nothing to clean, or unwritable too */ }
|
|
226
|
+
}
|
|
227
|
+
// NEVER a silent kill (hamr, 2026-07-30). Three lines, in the order a reader
|
|
228
|
+
// needs them: what fired, which bound it was measured against, and the exact
|
|
229
|
+
// marker evidence that says the process is dead rather than slow.
|
|
230
|
+
console.error(`[u-watchdog] KILL ${reason}: pid ${pid} judged DEAD — ${judgement}`);
|
|
231
|
+
console.error(`[u-watchdog] deadline: ${JSON.stringify({ elapsedMs: record.elapsedMs, wallMs, graceMs, staleMs, deadMs, ...detail })}`);
|
|
232
|
+
console.error(`[u-watchdog] marker checked: ${JSON.stringify(markers)}`);
|
|
233
|
+
console.error(`[u-watchdog] SIGTERM -> pid ${pid}; SIGKILL in ${termGraceMs}ms if it is still there`);
|
|
234
|
+
try { process.kill(pid, 'SIGTERM'); } catch { /* already gone */ }
|
|
235
|
+
// SIGTERM lets the run flush its spine. A wedged event loop cannot run a signal
|
|
236
|
+
// handler, so SIGKILL is the one that actually lands — but only after giving the
|
|
237
|
+
// recoverable case its chance. A run that INSTALLS a handler and swallows the signal
|
|
238
|
+
// reaches the same place: the polite signal is an opportunity, never a veto.
|
|
239
|
+
setTimeout(() => {
|
|
240
|
+
if (alive()) {
|
|
241
|
+
console.error(`[u-watchdog] SIGKILL -> pid ${pid}: it survived the ${termGraceMs}ms grace`);
|
|
242
|
+
try { process.kill(pid, 'SIGKILL'); } catch { /* raced */ }
|
|
243
|
+
}
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}, termGraceMs);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.error(`[u-watchdog] watching pid ${pid} — marker [spine], stale ${Math.round(staleMs / 1000)}s${wallMs ? `, wall ${Math.round(wallMs / 1000)}s (+${Math.round(graceMs / 1000)}s grace, then ${Math.round(deadMs / 1000)}s of a flat spine)` : ', no wall'}`);
|
|
249
|
+
|
|
250
|
+
// ── THE DECISION ────────────────────────────────────────────────────────────────
|
|
251
|
+
// One table, evaluated every poll after the marker is sampled. "Past deadline"
|
|
252
|
+
// means elapsed >= wallMs + graceMs, where the grace is stages × close timeout.
|
|
253
|
+
//
|
|
254
|
+
// past deadline | spine (progress) | action
|
|
255
|
+
// --------------+-------------------+--------------------------------------------
|
|
256
|
+
// any | cold >= staleMs | KILL 'stale'
|
|
257
|
+
// no | cold < staleMs | quiet — the run is inside its worst legal
|
|
258
|
+
// | | silence
|
|
259
|
+
// yes | cold < deadMs | LOUD, no kill (still progressing)
|
|
260
|
+
// yes | cold >= deadMs | KILL 'wall-dead'
|
|
261
|
+
//
|
|
262
|
+
// Two things this table says on purpose:
|
|
263
|
+
// 1. Only BYTES hold a kill off. Nothing else is consulted — in particular not
|
|
264
|
+
// CPU, which was measured unable to tell a working process from a spun-out
|
|
265
|
+
// one and, worse, unable to go flat at all at the production constants (see
|
|
266
|
+
// THE ACTIVITY MARKER above). A hard-frozen event loop burns 100% CPU forever
|
|
267
|
+
// and emits nothing; it is F67's own case, and it must die on both triggers.
|
|
268
|
+
// 2. Past the deadline with the spine still moving, NOTHING is killed. The
|
|
269
|
+
// in-process fuses (F66), the wall clock and the money cap own bounding a run
|
|
270
|
+
// that is alive; this guard owns processes that are dead. It says so loudly
|
|
271
|
+
// every poll rather than going quiet — the overshoot is still a fact the
|
|
272
|
+
// operator must see, it is just not this process's to end.
|
|
273
|
+
const tick = setInterval(() => {
|
|
274
|
+
if (!alive()) { clearInterval(tick); process.exit(0); } // the run finished on its own
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
sampleMarkers(now);
|
|
277
|
+
const spineCold = spineColdMs(now);
|
|
278
|
+
const elapsed = now - startedAt;
|
|
279
|
+
|
|
280
|
+
if (spineCold >= staleMs) {
|
|
281
|
+
clearInterval(tick);
|
|
282
|
+
stop('stale', `the spine has not grown for ${Math.round(spineCold / 1000)}s, past the ${Math.round(staleMs / 1000)}s window sized above the worst LEGAL silence (a full staged close) — the run is producing nothing, whatever it is or is not burning CPU on`, { coldMs: spineCold });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (wallMs === null || elapsed < wallMs + graceMs) return;
|
|
286
|
+
|
|
287
|
+
const overMs = elapsed - (wallMs + graceMs);
|
|
288
|
+
if (spineCold < deadMs) {
|
|
289
|
+
// LOUD, every poll. hamr: the outside kill checks for activity/bytes, not the
|
|
290
|
+
// clock alone — so an overrun that is still working gets named, not executed.
|
|
291
|
+
console.error(`[u-watchdog] PAST DEADLINE by ${Math.round(overMs / 1000)}s (wall ${Math.round(wallMs / 1000)}s + ${Math.round(graceMs / 1000)}s close grace) — NOT killing: still ACTIVE on spine (grew ${Math.round(spineCold / 1000)}s ago, ${spineSeen.size < 0 ? 'no file yet' : `${spineSeen.size} bytes`}). This guard stops DEAD processes; a live run is bounded by its own fuses and its money cap.`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
clearInterval(tick);
|
|
295
|
+
stop('wall-dead', `${Math.round(overMs / 1000)}s past the deadline (wall ${Math.round(wallMs / 1000)}s + ${Math.round(graceMs / 1000)}s close grace, which has already funded every legal close stage) AND the spine flat for at least ${Math.round(deadMs / 1000)}s — no bytes are reaching the run's own log, so there is nothing left that could still be producing a verdict`, { requestedMs: wallMs, enforcedMs: wallMs + graceMs, pastDeadlineMs: overMs });
|
|
296
|
+
}, pollMs);
|