mixdog 0.9.50 → 0.9.52
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/package.json +5 -3
- package/scripts/abort-recovery-test.mjs +17 -1
- package/scripts/agent-model-liveness-test.mjs +79 -2
- package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
- package/scripts/anthropic-transport-policy-test.mjs +466 -0
- package/scripts/atomic-lock-tryonce-test.mjs +60 -1
- package/scripts/build-tui.mjs +13 -1
- package/scripts/channel-daemon-smoke.mjs +630 -10
- package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
- package/scripts/code-graph-disk-hit-test.mjs +11 -0
- package/scripts/code-graph-root-federation-test.mjs +273 -0
- package/scripts/compact-pressure-test.mjs +55 -1
- package/scripts/compact-smoke.mjs +80 -0
- package/scripts/context-mcp-metering-test.mjs +1350 -19
- package/scripts/deferred-tool-loading-test.mjs +17 -0
- package/scripts/gemini-provider-test.mjs +1053 -0
- package/scripts/hook-bus-test.mjs +23 -0
- package/scripts/internal-tools-normalization-test.mjs +10 -0
- package/scripts/interrupted-turn-history-test.mjs +371 -0
- package/scripts/lifecycle-api-test.mjs +76 -0
- package/scripts/max-output-recovery-test.mjs +55 -0
- package/scripts/mcp-grace-deferred-test.mjs +89 -13
- package/scripts/memory-pg-recovery-test.mjs +59 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +391 -4
- package/scripts/process-lifecycle-test.mjs +389 -0
- package/scripts/provider-admission-scheduler-test.mjs +582 -0
- package/scripts/provider-contract-test.mjs +268 -0
- package/scripts/provider-toolcall-test.mjs +520 -3
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- package/scripts/session-bench-cache-break-test.mjs +102 -0
- package/scripts/session-bench.mjs +101 -42
- package/scripts/shell-failure-diagnostics-test.mjs +73 -4
- package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
- package/scripts/smoke-loop-failure-summary-test.mjs +38 -0
- package/scripts/smoke-loop-failure-summary.mjs +16 -0
- package/scripts/smoke-loop.mjs +2 -7
- package/scripts/steering-drain-buckets-test.mjs +18 -0
- package/scripts/tool-failures.mjs +15 -1
- package/scripts/toolcall-args-test.mjs +14 -6
- package/scripts/tui-transcript-perf-test.mjs +43 -7
- package/scripts/web-fetch-routing-test.mjs +158 -0
- package/src/cli.mjs +15 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +26 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -0
- package/src/runtime/agent/orchestrator/internal-tools.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +118 -26
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +29 -17
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -38
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +24 -4
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +95 -46
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +89 -17
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +93 -26
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +144 -51
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +74 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +111 -6
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +224 -104
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +127 -55
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +69 -37
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +6 -7
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +24 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +89 -22
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +380 -37
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -14
- package/src/runtime/memory/index.mjs +22 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
- package/src/runtime/memory/lib/pg/process.mjs +91 -47
- package/src/runtime/memory/lib/pg/supervisor.mjs +50 -13
- package/src/runtime/search/index.mjs +41 -0
- package/src/runtime/search/lib/http-fetch.mjs +154 -0
- package/src/runtime/search/tool-defs.mjs +23 -0
- package/src/runtime/shared/atomic-file.mjs +28 -150
- package/src/runtime/shared/process-lifecycle.mjs +363 -0
- package/src/runtime/shared/process-shutdown.mjs +27 -4
- package/src/runtime/shared/resource-admission.mjs +359 -0
- package/src/runtime/shared/staged-child-result.mjs +19 -0
- package/src/session-runtime/context-status.mjs +38 -27
- package/src/session-runtime/lifecycle-api.mjs +26 -6
- package/src/session-runtime/provider-request-tools.mjs +72 -0
- package/src/session-runtime/runtime-core.mjs +43 -18
- package/src/session-runtime/session-turn-api.mjs +5 -4
- package/src/session-runtime/tool-catalog.mjs +375 -15
- package/src/standalone/agent-tool.mjs +17 -38
- package/src/standalone/channel-daemon-client.mjs +200 -38
- package/src/standalone/channel-daemon-transport.mjs +136 -9
- package/src/standalone/channel-worker.mjs +79 -12
- package/src/standalone/hook-bus/handlers.mjs +4 -0
- package/src/standalone/hook-bus/rules.mjs +7 -1
- package/src/standalone/hook-bus.mjs +10 -2
- package/src/tui/App.jsx +17 -11
- package/src/tui/app/live-spinner-visibility.mjs +20 -0
- package/src/tui/dist/index.mjs +101 -281
- package/src/tui/engine/session-api-ext.mjs +13 -2
- package/src/tui/engine/session-api.mjs +1 -1
- package/src/tui/engine/turn.mjs +10 -6
- package/src/tui/engine.mjs +13 -4
- package/src/tui/index.jsx +4 -1
- package/src/tui/lib/voice-setup.mjs +16 -0
|
@@ -100,9 +100,8 @@ export function withFileLockSync(lockPath, fn, opts = {}) {
|
|
|
100
100
|
contErr.cause = err;
|
|
101
101
|
throw contErr;
|
|
102
102
|
}
|
|
103
|
-
//
|
|
104
|
-
// owner
|
|
105
|
-
// a pidless/empty corpse lock; a live foreign holder is never stolen.
|
|
103
|
+
// Reclaim only when owner death is positively established. Age, an
|
|
104
|
+
// unreadable owner, or a same-pid foreign token remain contended.
|
|
106
105
|
try {
|
|
107
106
|
if (_tryReclaimStaleLock(lockPath, staleMs)) continue;
|
|
108
107
|
} catch {}
|
|
@@ -127,11 +126,9 @@ export function withFileLockSync(lockPath, fn, opts = {}) {
|
|
|
127
126
|
return fn();
|
|
128
127
|
} finally {
|
|
129
128
|
try { closeSync(fd); } catch {}
|
|
130
|
-
// Only unlink if we still own the lock. If
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
// owner's lock and break mutual exclusion for whoever is
|
|
134
|
-
// queued behind it.
|
|
129
|
+
// Only unlink if we still own the lock. If the path was externally
|
|
130
|
+
// replaced, unconditionally unlinking would destroy the new owner's
|
|
131
|
+
// lock and break mutual exclusion for whoever is queued behind it.
|
|
135
132
|
try {
|
|
136
133
|
if (_lockOwnedBySelf(lockPath)) unlinkSync(lockPath);
|
|
137
134
|
} catch {}
|
|
@@ -147,33 +144,6 @@ export function withFileLockSync(lockPath, fn, opts = {}) {
|
|
|
147
144
|
// token is the owner pid (see the writeFileSync above). Best-effort
|
|
148
145
|
// only: a parse failure or stat error is treated as "not ours" /
|
|
149
146
|
// "owner unknown" so we err on the side of NOT stealing.
|
|
150
|
-
function _readLockOwnerPid(lockPath) {
|
|
151
|
-
try {
|
|
152
|
-
const raw = readFileSync(lockPath, 'utf8');
|
|
153
|
-
const tok = String(raw).trim().split(/\s+/)[0];
|
|
154
|
-
const pid = Number.parseInt(tok, 10);
|
|
155
|
-
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
156
|
-
} catch {
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function _pidIsDead(pid) {
|
|
162
|
-
// Unreadable / unparseable pid: be conservative — treat as alive so
|
|
163
|
-
// we wait instead of stealing.
|
|
164
|
-
if (pid === null) return false;
|
|
165
|
-
if (pid === process.pid) return false;
|
|
166
|
-
try {
|
|
167
|
-
process.kill(pid, 0);
|
|
168
|
-
return false; // signal delivered → process exists
|
|
169
|
-
} catch (err) {
|
|
170
|
-
// ESRCH = no such process → owner is gone, safe to steal.
|
|
171
|
-
// EPERM = process exists but we can't signal it → still alive,
|
|
172
|
-
// do NOT steal.
|
|
173
|
-
return err?.code === 'ESRCH';
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
147
|
// Full owner identity: pid plus per-instance token. token is null for an
|
|
178
148
|
// old-format (`<pid> <ts>`) lock, in which case callers fall back to
|
|
179
149
|
// pid-authoritative handling so a still-running old-format holder is
|
|
@@ -196,13 +166,8 @@ function _readLockOwner(lockPath) {
|
|
|
196
166
|
// - pid null (empty/unparseable): not live (unknown owner).
|
|
197
167
|
// - pid === our pid: treated as LIVE. A foreign/absent token on our own
|
|
198
168
|
// pid is ambiguous — it may be a recycled-pid corpse, but it may also
|
|
199
|
-
// be a live sibling worker_thread (which shares process.pid
|
|
200
|
-
//
|
|
201
|
-
// from the token alone, so we never call a same-pid owner instantly
|
|
202
|
-
// dead; the reclaim path gates a same-pid foreign-token steal behind
|
|
203
|
-
// stale mtime (see _ownerIsSelfForeign / _tryReclaimStaleLock) so a
|
|
204
|
-
// live sibling is never yanked, while a real corpse still frees once
|
|
205
|
-
// mtime goes stale.
|
|
169
|
+
// be a live sibling worker_thread (which shares process.pid). Ambiguity
|
|
170
|
+
// is never reclaimed.
|
|
206
171
|
// - foreign pid: probed with kill(pid, 0); ESRCH ⇒ dead, else live.
|
|
207
172
|
function _ownerIsLive(owner) {
|
|
208
173
|
if (owner.pid === null) return false;
|
|
@@ -215,15 +180,6 @@ function _ownerIsLive(owner) {
|
|
|
215
180
|
}
|
|
216
181
|
}
|
|
217
182
|
|
|
218
|
-
// Our own pid but a foreign (present, non-matching) token: either a
|
|
219
|
-
// recycled-pid corpse or a live sibling worker_thread. Reclaimable only
|
|
220
|
-
// once mtime is past staleMs (handled by the pidless-style stale gate).
|
|
221
|
-
function _ownerIsSelfForeign(owner) {
|
|
222
|
-
return owner.pid === process.pid
|
|
223
|
-
&& owner.token !== null
|
|
224
|
-
&& owner.token !== _OWNER_TOKEN;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
183
|
// Release-time guard: unlink only if the lock still carries OUR identity
|
|
228
184
|
// (our pid AND our token). Protects against deleting a lock another
|
|
229
185
|
// instance stole+recreated, and against a same-pid sibling.
|
|
@@ -238,23 +194,15 @@ function _lockOwnedBySelf(lockPath) {
|
|
|
238
194
|
// they wait between attempts. Returns true iff lockPath was reclaimed.
|
|
239
195
|
//
|
|
240
196
|
// Reclaim decision table (owner = identity recorded in the lock file):
|
|
241
|
-
// owner pid DEAD (ESRCH
|
|
242
|
-
// owner pid NULL
|
|
197
|
+
// owner pid DEAD (ESRCH) → reclaim
|
|
198
|
+
// owner pid NULL/uncertain or same-pid foreign token → back off
|
|
243
199
|
// owner token MISMATCH on guarded re-read (new holder took over) → back off
|
|
244
|
-
// owner LIVE
|
|
200
|
+
// owner LIVE → back off regardless of age
|
|
245
201
|
function _tryReclaimStaleLock(lockPath, staleMs) {
|
|
246
|
-
|
|
247
|
-
try { st = statSync(lockPath); } catch { return false; }
|
|
202
|
+
try { statSync(lockPath); } catch { return false; }
|
|
248
203
|
const owner = _readLockOwner(lockPath);
|
|
249
|
-
const stale = Date.now() - st.mtimeMs > staleMs;
|
|
250
204
|
const dead = owner.pid !== null && !_ownerIsLive(owner);
|
|
251
|
-
|
|
252
|
-
const selfForeign = _ownerIsSelfForeign(owner);
|
|
253
|
-
// A dead foreign owner reclaims immediately. A pidless corpse, or a
|
|
254
|
-
// same-pid foreign-token owner (which may still be a live sibling
|
|
255
|
-
// worker_thread), reclaims only once mtime is past staleMs; a live
|
|
256
|
-
// foreign holder is never stolen even after mtime goes stale.
|
|
257
|
-
if (!dead && !((pidless || selfForeign) && stale)) return false;
|
|
205
|
+
if (!dead) return false;
|
|
258
206
|
const reclaim = _tryAcquireReclaimGuard(lockPath, staleMs);
|
|
259
207
|
if (reclaim === null) return false;
|
|
260
208
|
try {
|
|
@@ -266,11 +214,8 @@ function _tryReclaimStaleLock(lockPath, staleMs) {
|
|
|
266
214
|
// the guard window has a different token and is not mistaken for the
|
|
267
215
|
// corpse — so we never yank a freshly-acquired live lock.
|
|
268
216
|
if (curOwner.pid !== owner.pid || curOwner.token !== owner.token) return false;
|
|
269
|
-
const curStale = Date.now() - cur.mtimeMs > staleMs;
|
|
270
217
|
const curDead = curOwner.pid !== null && !_ownerIsLive(curOwner);
|
|
271
|
-
|
|
272
|
-
const curSelfForeign = _ownerIsSelfForeign(curOwner);
|
|
273
|
-
if (curDead || ((curPidless || curSelfForeign) && curStale)) {
|
|
218
|
+
if (curDead) {
|
|
274
219
|
try { unlinkSync(lockPath); return true; } catch { return false; }
|
|
275
220
|
}
|
|
276
221
|
return false;
|
|
@@ -279,89 +224,23 @@ function _tryReclaimStaleLock(lockPath, staleMs) {
|
|
|
279
224
|
}
|
|
280
225
|
}
|
|
281
226
|
|
|
282
|
-
function _reclaimGuardIsUnreadableAndStale(guardPath, staleMs) {
|
|
283
|
-
if (_readLockOwnerPid(guardPath) !== null) return false;
|
|
284
|
-
try {
|
|
285
|
-
const st = statSync(guardPath);
|
|
286
|
-
return Date.now() - st.mtimeMs > staleMs;
|
|
287
|
-
} catch {
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function _reclaimGuardMatchesToken(guardPath, token) {
|
|
293
|
-
try {
|
|
294
|
-
return readFileSync(guardPath, 'utf8') === token;
|
|
295
|
-
} catch {
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function _readGuardContent(guardPath) {
|
|
301
|
-
try { return readFileSync(guardPath, 'utf8'); } catch { return null; }
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function _parseStampPid(raw) {
|
|
305
|
-
if (raw === null) return null;
|
|
306
|
-
const tok = String(raw).trim().split(/\s+/)[0];
|
|
307
|
-
const pid = Number.parseInt(tok, 10);
|
|
308
|
-
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Unlink the guard only if its FULL content (pid ts token) is still the
|
|
312
|
-
// exact content the stale/dead decision was made against. A regenerated
|
|
313
|
-
// guard — even one whose pid was recycled to the same value (same-pid
|
|
314
|
-
// ABA) — carries a fresh ts+token and therefore never matches, so a
|
|
315
|
-
// fresh guard is never removed by a stale decision.
|
|
316
|
-
function _unlinkReclaimGuardIfContentMatches(guardPath, content) {
|
|
317
|
-
if (_readGuardContent(guardPath) !== content) return false;
|
|
318
|
-
try { unlinkSync(guardPath); return true; } catch { return false; }
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function _unlinkUnreadableStaleReclaimGuard(guardPath, staleMs) {
|
|
322
|
-
if (!_reclaimGuardIsUnreadableAndStale(guardPath, staleMs)) return false;
|
|
323
|
-
try { unlinkSync(guardPath); return true; } catch { return false; }
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function _tryClearStaleReclaimGuard(guardPath, staleMs) {
|
|
327
|
-
const guardContent = _readGuardContent(guardPath);
|
|
328
|
-
const guardPid = _parseStampPid(guardContent);
|
|
329
|
-
if (guardPid !== null) {
|
|
330
|
-
// Re-read the guard's full content immediately before unlinking so a
|
|
331
|
-
// regenerated guard (different owner OR same-pid ABA) is not removed
|
|
332
|
-
// by a stale decision.
|
|
333
|
-
// This is still not a serialized primitive: correctness comes from
|
|
334
|
-
// the lockPath owner-token reverify before unlinking lockPath, not
|
|
335
|
-
// from making guard cleanup itself authoritative.
|
|
336
|
-
// A guard whose pid was recycled onto a LIVE process would otherwise
|
|
337
|
-
// never clear, starving reclaim permanently; so also clear a guard
|
|
338
|
-
// past staleMs regardless of apparent pid-liveness. Safe because the
|
|
339
|
-
// guard is best-effort — the lockPath owner-token reverify still gates
|
|
340
|
-
// the actual lock unlink.
|
|
341
|
-
let guardStale = false;
|
|
342
|
-
try { guardStale = Date.now() - statSync(guardPath).mtimeMs > staleMs; } catch {}
|
|
343
|
-
if (_pidIsDead(guardPid) || guardStale) {
|
|
344
|
-
_unlinkReclaimGuardIfContentMatches(guardPath, guardContent);
|
|
345
|
-
}
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
// A crash between openSync('wx') and the pid write can leave an empty
|
|
349
|
-
// guard. There is no pid to prove dead, so clear only once the guard
|
|
350
|
-
// file itself is stale; any contender that proceeds must still prove
|
|
351
|
-
// the lockPath token dead before unlinking the lock.
|
|
352
|
-
_unlinkUnreadableStaleReclaimGuard(guardPath, staleMs);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
227
|
function _tryAcquireReclaimGuard(lockPath, staleMs) {
|
|
356
228
|
const guardPath = `${lockPath}.reclaim`;
|
|
229
|
+
const token = `${process.pid} ${Date.now()} ${randomBytes(8).toString('hex')}\n`;
|
|
230
|
+
const stagedPath = `${guardPath}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
357
231
|
try {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
232
|
+
// Publish a fully formed guard with no empty-file window. linkSync is
|
|
233
|
+
// atomic and fails rather than replacing an existing pathname.
|
|
234
|
+
writeFileSync(stagedPath, token, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
235
|
+
linkSync(stagedPath, guardPath);
|
|
236
|
+
unlinkSync(stagedPath);
|
|
237
|
+
return { guardPath };
|
|
362
238
|
} catch (err) {
|
|
239
|
+
try { unlinkSync(stagedPath); } catch {}
|
|
363
240
|
if (LOCK_WAIT_CODES.has(err?.code)) {
|
|
364
|
-
|
|
241
|
+
// There is no portable ownership-conditional unlink. Never revoke a
|
|
242
|
+
// published guard: age and contents are diagnostic only, and deleting
|
|
243
|
+
// after a pathname re-read admits an ABA that can steal a live guard.
|
|
365
244
|
return null;
|
|
366
245
|
}
|
|
367
246
|
throw err;
|
|
@@ -369,10 +248,9 @@ function _tryAcquireReclaimGuard(lockPath, staleMs) {
|
|
|
369
248
|
}
|
|
370
249
|
|
|
371
250
|
function _releaseReclaimGuard(reclaim) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
251
|
+
// Protocol participants cannot replace a published guard: contenders only
|
|
252
|
+
// wait, so the exact owner may remove its non-revocable pathname directly.
|
|
253
|
+
try { unlinkSync(reclaim.guardPath); } catch {}
|
|
376
254
|
}
|
|
377
255
|
|
|
378
256
|
// ── Async lock variant (non-blocking wait) ──────────────────────────
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
closeSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
fsyncSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
openSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
statSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
writeSync,
|
|
16
|
+
} from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { execFileSync } from 'node:child_process';
|
|
19
|
+
import { withFileLockSync } from './atomic-file.mjs';
|
|
20
|
+
import { resolvePluginData } from './plugin-paths.mjs';
|
|
21
|
+
|
|
22
|
+
export const LIFECYCLE_LEDGER_MAX_BYTES = 64 * 1024;
|
|
23
|
+
export const LIFECYCLE_LEDGER_KEEP_FILES = 2;
|
|
24
|
+
const LEDGER_NAME = 'process-lifecycle.jsonl';
|
|
25
|
+
const MARKER_DIR_NAME = 'process-lifecycle.active';
|
|
26
|
+
const LEGACY_MARKER_NAME = 'process-lifecycle.active.json';
|
|
27
|
+
const REPORT_NAME = 'mixdog-node-report.json';
|
|
28
|
+
const LEDGER_LOCK_NAME = 'process-lifecycle.lock';
|
|
29
|
+
const LEDGER_LOCK_TIMEOUT_MS = 2000;
|
|
30
|
+
const LEDGER_LOCK_STALE_MS = 10000;
|
|
31
|
+
const STATE_KEY = Symbol.for('mixdog.processLifecycle.v1');
|
|
32
|
+
const VALID_REASONS = new Set([
|
|
33
|
+
'process-start',
|
|
34
|
+
'prior-process-vanished',
|
|
35
|
+
'clean-shutdown',
|
|
36
|
+
'catchable-fatal-error',
|
|
37
|
+
'forced-cleanup',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
function sharedState() {
|
|
41
|
+
if (!globalThis[STATE_KEY]) globalThis[STATE_KEY] = { active: null };
|
|
42
|
+
return globalThis[STATE_KEY];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function paths(directory) {
|
|
46
|
+
const dir = directory || join(resolvePluginData(), 'diagnostics');
|
|
47
|
+
return {
|
|
48
|
+
dir,
|
|
49
|
+
ledger: join(dir, LEDGER_NAME),
|
|
50
|
+
previousLedger: join(dir, `${LEDGER_NAME}.1`),
|
|
51
|
+
markerDir: join(dir, MARKER_DIR_NAME),
|
|
52
|
+
legacyMarker: join(dir, LEGACY_MARKER_NAME),
|
|
53
|
+
lock: join(dir, LEDGER_LOCK_NAME),
|
|
54
|
+
report: join(dir, REPORT_NAME),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function memoryCounters() {
|
|
59
|
+
try {
|
|
60
|
+
const value = process.memoryUsage();
|
|
61
|
+
return {
|
|
62
|
+
rss: value.rss,
|
|
63
|
+
heapTotal: value.heapTotal,
|
|
64
|
+
heapUsed: value.heapUsed,
|
|
65
|
+
external: value.external,
|
|
66
|
+
arrayBuffers: value.arrayBuffers,
|
|
67
|
+
};
|
|
68
|
+
} catch {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function rotateLedger(active) {
|
|
74
|
+
const size = Number(statSync(active.ledger).size) || 0;
|
|
75
|
+
try { unlinkSync(active.previousLedger); } catch {}
|
|
76
|
+
if (size <= LIFECYCLE_LEDGER_MAX_BYTES) {
|
|
77
|
+
renameSync(active.ledger, active.previousLedger);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// An oversized file cannot have been produced by this writer. Do not carry
|
|
82
|
+
// an untrusted/partial record into the retained generation.
|
|
83
|
+
writeFileSync(active.previousLedger, '', { mode: 0o600 });
|
|
84
|
+
unlinkSync(active.ledger);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function boundPreviousLedger(active) {
|
|
88
|
+
if (existsSync(active.previousLedger)
|
|
89
|
+
&& statSync(active.previousLedger).size > LIFECYCLE_LEDGER_MAX_BYTES) {
|
|
90
|
+
writeFileSync(active.previousLedger, '', { mode: 0o600 });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function appendEntry(active, entry) {
|
|
95
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
96
|
+
if (Buffer.byteLength(line) > LIFECYCLE_LEDGER_MAX_BYTES) return false;
|
|
97
|
+
try {
|
|
98
|
+
return withFileLockSync(active.lock, () => {
|
|
99
|
+
boundPreviousLedger(active);
|
|
100
|
+
if (existsSync(active.ledger)
|
|
101
|
+
&& statSync(active.ledger).size + Buffer.byteLength(line) > LIFECYCLE_LEDGER_MAX_BYTES) {
|
|
102
|
+
rotateLedger(active);
|
|
103
|
+
}
|
|
104
|
+
const fd = openSync(active.ledger, 'a', 0o600);
|
|
105
|
+
try {
|
|
106
|
+
writeSync(fd, line, null, 'utf8');
|
|
107
|
+
fsyncSync(fd);
|
|
108
|
+
} finally {
|
|
109
|
+
closeSync(fd);
|
|
110
|
+
}
|
|
111
|
+
return statSync(active.ledger).size <= LIFECYCLE_LEDGER_MAX_BYTES;
|
|
112
|
+
}, {
|
|
113
|
+
timeoutMs: LEDGER_LOCK_TIMEOUT_MS,
|
|
114
|
+
staleMs: LEDGER_LOCK_STALE_MS,
|
|
115
|
+
});
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function currentProcessIdentity() {
|
|
122
|
+
return processIdentityForPid(process.pid);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function processIdentityForPid(pid) {
|
|
126
|
+
if (process.platform === 'linux') {
|
|
127
|
+
try {
|
|
128
|
+
const raw = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
129
|
+
const fields = raw.slice(raw.lastIndexOf(') ') + 2).trim().split(/\s+/);
|
|
130
|
+
return fields[19] && /^\d+$/.test(fields[19])
|
|
131
|
+
? { kind: 'linux-start-ticks', value: fields[19] }
|
|
132
|
+
: null;
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
if (process.platform === 'win32') {
|
|
139
|
+
const out = execFileSync('powershell.exe', [
|
|
140
|
+
'-NoProfile', '-NonInteractive', '-Command',
|
|
141
|
+
`([DateTimeOffset](Get-Process -Id ${pid} -ErrorAction Stop).StartTime).ToUnixTimeSeconds()`,
|
|
142
|
+
], { encoding: 'utf8', timeout: 2000, windowsHide: true });
|
|
143
|
+
const text = String(out).trim();
|
|
144
|
+
const value = /^\d+$/.test(text) ? Number(text) : NaN;
|
|
145
|
+
return Number.isSafeInteger(value) ? { kind: 'start-seconds', value } : null;
|
|
146
|
+
}
|
|
147
|
+
const out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
|
|
148
|
+
encoding: 'utf8',
|
|
149
|
+
timeout: 2000,
|
|
150
|
+
windowsHide: true,
|
|
151
|
+
});
|
|
152
|
+
const value = Math.floor(Date.parse(String(out).trim()) / 1000);
|
|
153
|
+
return Number.isInteger(value) ? { kind: 'start-seconds', value } : null;
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function sameProcessIdentity(expected, observed) {
|
|
160
|
+
if (!expected || !observed || expected.kind !== observed.kind) return null;
|
|
161
|
+
if (expected.kind === 'linux-start-ticks') {
|
|
162
|
+
if (!/^\d+$/.test(String(expected.value)) || !/^\d+$/.test(String(observed.value))) return null;
|
|
163
|
+
if (BigInt(expected.value) < 1n || BigInt(observed.value) < 1n) return null;
|
|
164
|
+
return String(expected.value) === String(observed.value);
|
|
165
|
+
}
|
|
166
|
+
if (expected.kind === 'start-seconds') {
|
|
167
|
+
if (!Number.isSafeInteger(expected.value) || expected.value < 1
|
|
168
|
+
|| !Number.isSafeInteger(observed.value) || observed.value < 1) return null;
|
|
169
|
+
return expected.value === observed.value;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function recordCurrent(reason, exitCode = null) {
|
|
175
|
+
const active = sharedState().active;
|
|
176
|
+
if (!active || !VALID_REASONS.has(reason)) return false;
|
|
177
|
+
return appendEntry(active, {
|
|
178
|
+
version: 1,
|
|
179
|
+
timestamp: new Date().toISOString(),
|
|
180
|
+
pid: process.pid,
|
|
181
|
+
ppid: process.ppid,
|
|
182
|
+
reason,
|
|
183
|
+
exitCode: Number.isInteger(exitCode) ? exitCode : null,
|
|
184
|
+
memory: memoryCounters(),
|
|
185
|
+
cwd: active.cwd,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function recordPriorVanished(active, previous) {
|
|
190
|
+
return appendEntry(active, {
|
|
191
|
+
version: 1,
|
|
192
|
+
timestamp: new Date().toISOString(),
|
|
193
|
+
pid: previous.pid,
|
|
194
|
+
ppid: Number.isInteger(previous.ppid) ? previous.ppid : null,
|
|
195
|
+
reason: 'prior-process-vanished',
|
|
196
|
+
exitCode: null,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function pidLiveness(pid) {
|
|
201
|
+
if (!Number.isInteger(pid) || pid < 1 || pid > 2147483647) return 'unknown';
|
|
202
|
+
try {
|
|
203
|
+
process.kill(pid, 0);
|
|
204
|
+
return 'occupied';
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (error?.code === 'ESRCH') return 'dead';
|
|
207
|
+
if (error?.code === 'EPERM') return 'occupied';
|
|
208
|
+
return 'unknown';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function markerOwned(active) {
|
|
213
|
+
try {
|
|
214
|
+
const marker = JSON.parse(readFileSync(active.markerPath, 'utf8'));
|
|
215
|
+
return marker?.pid === process.pid && marker?.token === active.token;
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function writeMarker(active) {
|
|
222
|
+
try {
|
|
223
|
+
mkdirSync(active.markerDir, { recursive: true, mode: 0o700 });
|
|
224
|
+
writeFileSync(active.markerPath, `${JSON.stringify({
|
|
225
|
+
version: 1,
|
|
226
|
+
timestamp: new Date().toISOString(),
|
|
227
|
+
pid: process.pid,
|
|
228
|
+
ppid: process.ppid,
|
|
229
|
+
token: active.token,
|
|
230
|
+
processIdentity: active.processIdentity,
|
|
231
|
+
})}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
232
|
+
const fd = openSync(active.markerPath, 'r');
|
|
233
|
+
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
234
|
+
return true;
|
|
235
|
+
} catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function reapVanishedMarkers(active) {
|
|
241
|
+
const candidates = [];
|
|
242
|
+
try {
|
|
243
|
+
for (const entry of readdirSync(active.markerDir, { withFileTypes: true })) {
|
|
244
|
+
if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
245
|
+
candidates.push(join(active.markerDir, entry.name));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch {}
|
|
249
|
+
if (existsSync(active.legacyMarker)) candidates.push(active.legacyMarker);
|
|
250
|
+
for (const markerPath of candidates) {
|
|
251
|
+
try {
|
|
252
|
+
const previous = JSON.parse(readFileSync(markerPath, 'utf8'));
|
|
253
|
+
const liveness = pidLiveness(previous?.pid);
|
|
254
|
+
let vanished = liveness === 'dead';
|
|
255
|
+
if (liveness === 'occupied') {
|
|
256
|
+
const identityMatch = sameProcessIdentity(
|
|
257
|
+
previous.processIdentity,
|
|
258
|
+
processIdentityForPid(previous.pid),
|
|
259
|
+
);
|
|
260
|
+
vanished = identityMatch === false;
|
|
261
|
+
}
|
|
262
|
+
if (!vanished) continue;
|
|
263
|
+
if (recordPriorVanished(active, previous)) unlinkSync(markerPath);
|
|
264
|
+
} catch {}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function configureNodeReports(reportPath, safeCommandLine) {
|
|
269
|
+
const report = process.report;
|
|
270
|
+
if (!report || !safeCommandLine || !('excludeEnv' in report)) return false;
|
|
271
|
+
try {
|
|
272
|
+
report.directory = '';
|
|
273
|
+
report.filename = reportPath;
|
|
274
|
+
report.compact = true;
|
|
275
|
+
report.excludeEnv = true;
|
|
276
|
+
if ('excludeNetwork' in report) report.excludeNetwork = true;
|
|
277
|
+
report.reportOnFatalError = true;
|
|
278
|
+
report.reportOnUncaughtException = true;
|
|
279
|
+
return true;
|
|
280
|
+
} catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function beginProcessLifecycle({
|
|
286
|
+
directory,
|
|
287
|
+
configureReports = true,
|
|
288
|
+
safeCommandLine = process.argv.length <= 2,
|
|
289
|
+
} = {}) {
|
|
290
|
+
const state = sharedState();
|
|
291
|
+
if (state.active) return state.active.api;
|
|
292
|
+
const resolved = paths(directory);
|
|
293
|
+
try {
|
|
294
|
+
mkdirSync(resolved.dir, { recursive: true, mode: 0o700 });
|
|
295
|
+
mkdirSync(resolved.markerDir, { recursive: true, mode: 0o700 });
|
|
296
|
+
} catch {}
|
|
297
|
+
const token = `${Date.now().toString(36)}-${process.hrtime.bigint().toString(36)}`;
|
|
298
|
+
const active = {
|
|
299
|
+
...resolved,
|
|
300
|
+
token,
|
|
301
|
+
processIdentity: currentProcessIdentity(),
|
|
302
|
+
markerPath: join(resolved.markerDir, `${process.pid}-${token}.json`),
|
|
303
|
+
cwd: (() => { try { return process.cwd(); } catch { return null; } })(),
|
|
304
|
+
};
|
|
305
|
+
state.active = active;
|
|
306
|
+
|
|
307
|
+
reapVanishedMarkers(active);
|
|
308
|
+
writeMarker(active);
|
|
309
|
+
recordCurrent('process-start');
|
|
310
|
+
if (configureReports && safeCommandLine) {
|
|
311
|
+
try { unlinkSync(`${active.report}.1`); } catch {}
|
|
312
|
+
try { renameSync(active.report, `${active.report}.1`); } catch {}
|
|
313
|
+
}
|
|
314
|
+
const reportsEnabled = configureReports && configureNodeReports(active.report, safeCommandLine);
|
|
315
|
+
active.api = {
|
|
316
|
+
directory: active.dir,
|
|
317
|
+
ledgerPath: active.ledger,
|
|
318
|
+
markerDir: active.markerDir,
|
|
319
|
+
markerPath: active.markerPath,
|
|
320
|
+
reportPath: reportsEnabled ? active.report : null,
|
|
321
|
+
};
|
|
322
|
+
return active.api;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function recordCatchableFatal(exitCode = 1) {
|
|
326
|
+
const active = sharedState().active;
|
|
327
|
+
if (active) {
|
|
328
|
+
const next = strongerReason(active.finalReason, 'catchable-fatal-error');
|
|
329
|
+
if (next !== active.finalReason || !Number.isInteger(active.finalExitCode)) {
|
|
330
|
+
active.finalExitCode = exitCode;
|
|
331
|
+
}
|
|
332
|
+
active.finalReason = next;
|
|
333
|
+
}
|
|
334
|
+
return recordCurrent('catchable-fatal-error', exitCode);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function strongerReason(current, candidate) {
|
|
338
|
+
const rank = { 'clean-shutdown': 0, 'catchable-fatal-error': 1, 'forced-cleanup': 2 };
|
|
339
|
+
return (rank[candidate] ?? 0) > (rank[current] ?? -1) ? candidate : current;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function finishProcessLifecycle(reason = 'clean-shutdown', exitCode = 0) {
|
|
343
|
+
const state = sharedState();
|
|
344
|
+
const active = state.active;
|
|
345
|
+
if (!active) return false;
|
|
346
|
+
const finalReason = VALID_REASONS.has(reason) ? reason : 'clean-shutdown';
|
|
347
|
+
const nextReason = strongerReason(active.finalReason, finalReason);
|
|
348
|
+
if (nextReason !== active.finalReason || !Number.isInteger(active.finalExitCode)) {
|
|
349
|
+
active.finalExitCode = exitCode;
|
|
350
|
+
}
|
|
351
|
+
active.finalReason = nextReason;
|
|
352
|
+
const written = recordCurrent(active.finalReason, active.finalExitCode);
|
|
353
|
+
if (!written) return false;
|
|
354
|
+
if (markerOwned(active)) {
|
|
355
|
+
try { unlinkSync(active.markerPath); } catch { return false; }
|
|
356
|
+
}
|
|
357
|
+
state.active = null;
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function lifecyclePathsForTest(directory) {
|
|
362
|
+
return paths(directory);
|
|
363
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
finishProcessLifecycle,
|
|
5
|
+
recordCatchableFatal,
|
|
6
|
+
} from './process-lifecycle.mjs';
|
|
7
|
+
|
|
3
8
|
const SIGNAL_EXIT_CODES = {
|
|
4
9
|
SIGINT: 130,
|
|
5
10
|
SIGTERM: 143,
|
|
@@ -27,7 +32,6 @@ export function waitWithTimeout(promise, timeoutMs, label = 'cleanup') {
|
|
|
27
32
|
settled = true;
|
|
28
33
|
reject(new Error(`${label} timed out after ${ms}ms`));
|
|
29
34
|
}, ms);
|
|
30
|
-
if (typeof timer.unref === 'function') timer.unref();
|
|
31
35
|
Promise.resolve(promise)
|
|
32
36
|
.then((value) => {
|
|
33
37
|
if (settled) return;
|
|
@@ -67,6 +71,7 @@ export function installProcessSignalCleanup({
|
|
|
67
71
|
};
|
|
68
72
|
|
|
69
73
|
const hardExit = (code) => {
|
|
74
|
+
finishProcessLifecycle('forced-cleanup', code);
|
|
70
75
|
try { process.exit(code); } catch {}
|
|
71
76
|
};
|
|
72
77
|
|
|
@@ -81,6 +86,9 @@ export function installProcessSignalCleanup({
|
|
|
81
86
|
}
|
|
82
87
|
running = true;
|
|
83
88
|
removeHandlers();
|
|
89
|
+
let cleanupFailed = false;
|
|
90
|
+
|
|
91
|
+
if (error) recordCatchableFatal(code);
|
|
84
92
|
|
|
85
93
|
if (shouldExit) {
|
|
86
94
|
hardExitTimer = setTimeout(() => hardExit(code), Math.max(1000, Number(timeoutMs) + 1000));
|
|
@@ -91,21 +99,36 @@ export function installProcessSignalCleanup({
|
|
|
91
99
|
log(`[${name}] ${reason}: ${errorText(error)}`);
|
|
92
100
|
}
|
|
93
101
|
|
|
94
|
-
try {
|
|
102
|
+
try { globalThis.__mixdogShutdownProviderAdmission?.(reason); } catch {}
|
|
103
|
+
try { globalThis.__mixdogDrainProviderConnections?.(reason); } catch {}
|
|
104
|
+
try { beforeCleanup?.(reason, { code, error }); } catch (cleanupError) {
|
|
105
|
+
cleanupFailed = true;
|
|
106
|
+
if (typeof log === 'function') log(`[${name}] cleanup failed: ${errorText(cleanupError)}`);
|
|
107
|
+
}
|
|
95
108
|
try {
|
|
96
109
|
if (typeof cleanup === 'function') {
|
|
97
110
|
await waitWithTimeout(cleanup(reason, { code, error }), timeoutMs, `${name} shutdown`);
|
|
98
111
|
}
|
|
99
112
|
} catch (cleanupError) {
|
|
113
|
+
cleanupFailed = true;
|
|
114
|
+
if (typeof log === 'function') log(`[${name}] cleanup failed: ${errorText(cleanupError)}`);
|
|
115
|
+
}
|
|
116
|
+
try { afterCleanup?.(reason, { code, error }); } catch (cleanupError) {
|
|
117
|
+
cleanupFailed = true;
|
|
100
118
|
if (typeof log === 'function') log(`[${name}] cleanup failed: ${errorText(cleanupError)}`);
|
|
101
119
|
}
|
|
102
|
-
try { afterCleanup?.(reason, { code, error }); } catch {}
|
|
103
120
|
if (hardExitTimer) {
|
|
104
121
|
clearTimeout(hardExitTimer);
|
|
105
122
|
hardExitTimer = null;
|
|
106
123
|
}
|
|
107
124
|
running = false;
|
|
108
|
-
if (shouldExit)
|
|
125
|
+
if (shouldExit) {
|
|
126
|
+
finishProcessLifecycle(
|
|
127
|
+
cleanupFailed ? 'forced-cleanup' : error ? 'catchable-fatal-error' : 'clean-shutdown',
|
|
128
|
+
code,
|
|
129
|
+
);
|
|
130
|
+
try { process.exit(code); } catch {}
|
|
131
|
+
}
|
|
109
132
|
return true;
|
|
110
133
|
};
|
|
111
134
|
|