mixdog 0.9.51 → 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.
Files changed (110) hide show
  1. package/package.json +5 -3
  2. package/scripts/abort-recovery-test.mjs +17 -1
  3. package/scripts/agent-model-liveness-test.mjs +79 -2
  4. package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
  5. package/scripts/anthropic-transport-policy-test.mjs +466 -0
  6. package/scripts/atomic-lock-tryonce-test.mjs +60 -1
  7. package/scripts/build-tui.mjs +13 -1
  8. package/scripts/channel-daemon-smoke.mjs +630 -10
  9. package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
  10. package/scripts/code-graph-disk-hit-test.mjs +11 -0
  11. package/scripts/code-graph-root-federation-test.mjs +273 -0
  12. package/scripts/compact-pressure-test.mjs +55 -1
  13. package/scripts/compact-smoke.mjs +80 -0
  14. package/scripts/context-mcp-metering-test.mjs +1350 -19
  15. package/scripts/deferred-tool-loading-test.mjs +17 -0
  16. package/scripts/gemini-provider-test.mjs +1053 -0
  17. package/scripts/interrupted-turn-history-test.mjs +371 -0
  18. package/scripts/lifecycle-api-test.mjs +76 -0
  19. package/scripts/max-output-recovery-test.mjs +55 -0
  20. package/scripts/mcp-grace-deferred-test.mjs +89 -13
  21. package/scripts/memory-pg-recovery-test.mjs +59 -0
  22. package/scripts/openai-oauth-ws-1006-retry-test.mjs +324 -3
  23. package/scripts/process-lifecycle-test.mjs +389 -0
  24. package/scripts/provider-admission-scheduler-test.mjs +582 -0
  25. package/scripts/provider-contract-test.mjs +268 -0
  26. package/scripts/provider-toolcall-test.mjs +320 -1
  27. package/scripts/reactive-compact-persist-smoke.mjs +59 -0
  28. package/scripts/resource-admission-test.mjs +789 -0
  29. package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
  30. package/scripts/steering-drain-buckets-test.mjs +18 -0
  31. package/scripts/toolcall-args-test.mjs +14 -6
  32. package/scripts/tui-transcript-perf-test.mjs +5 -7
  33. package/src/cli.mjs +15 -2
  34. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +26 -0
  35. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
  36. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +118 -26
  38. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +29 -17
  39. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -38
  40. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +24 -4
  41. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
  42. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
  43. package/src/runtime/agent/orchestrator/providers/gemini.mjs +95 -46
  44. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -4
  45. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
  46. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -3
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
  49. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +89 -17
  50. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +93 -26
  51. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +130 -47
  52. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
  53. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -3
  54. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -17
  55. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
  56. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
  57. package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
  58. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +224 -104
  59. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +122 -63
  60. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
  61. package/src/runtime/agent/orchestrator/session/context-utils.mjs +17 -1
  62. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +69 -37
  63. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
  64. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
  65. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +6 -7
  66. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
  67. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
  68. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
  69. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
  70. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +20 -4
  71. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
  72. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
  73. package/src/runtime/agent/orchestrator/session/store.mjs +89 -22
  74. package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
  75. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
  76. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
  77. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
  78. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +380 -37
  79. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
  80. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
  81. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
  82. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
  83. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
  84. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -14
  85. package/src/runtime/memory/index.mjs +22 -4
  86. package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
  87. package/src/runtime/memory/lib/pg/process.mjs +91 -47
  88. package/src/runtime/memory/lib/pg/supervisor.mjs +50 -13
  89. package/src/runtime/shared/atomic-file.mjs +28 -150
  90. package/src/runtime/shared/process-lifecycle.mjs +363 -0
  91. package/src/runtime/shared/process-shutdown.mjs +27 -4
  92. package/src/runtime/shared/resource-admission.mjs +359 -0
  93. package/src/runtime/shared/staged-child-result.mjs +19 -0
  94. package/src/session-runtime/context-status.mjs +38 -27
  95. package/src/session-runtime/lifecycle-api.mjs +26 -6
  96. package/src/session-runtime/provider-request-tools.mjs +72 -0
  97. package/src/session-runtime/runtime-core.mjs +6 -3
  98. package/src/session-runtime/session-turn-api.mjs +5 -4
  99. package/src/session-runtime/tool-catalog.mjs +375 -15
  100. package/src/standalone/agent-tool.mjs +17 -38
  101. package/src/standalone/channel-daemon-client.mjs +200 -38
  102. package/src/standalone/channel-daemon-transport.mjs +136 -9
  103. package/src/standalone/channel-worker.mjs +79 -12
  104. package/src/tui/dist/index.mjs +73 -278
  105. package/src/tui/engine/session-api-ext.mjs +13 -2
  106. package/src/tui/engine/session-api.mjs +1 -1
  107. package/src/tui/engine/turn.mjs +10 -6
  108. package/src/tui/engine.mjs +13 -4
  109. package/src/tui/index.jsx +4 -1
  110. package/src/tui/lib/voice-setup.mjs +16 -0
@@ -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 { beforeCleanup?.(reason, { code, error }); } catch {}
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) hardExit(code);
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