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.
- 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/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 +324 -3
- 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 +320 -1
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
- package/scripts/steering-drain-buckets-test.mjs +18 -0
- package/scripts/toolcall-args-test.mjs +14 -6
- package/scripts/tui-transcript-perf-test.mjs +5 -7
- 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/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 +130 -47
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -3
- 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 +122 -63
- 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/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/turn-interruption.mjs +220 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +20 -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/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 +6 -3
- 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/tui/dist/index.mjs +73 -278
- 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
|
@@ -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
|
|