dominds 1.25.18 → 1.26.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.
Files changed (38) hide show
  1. package/README.md +17 -0
  2. package/README.zh.md +17 -0
  3. package/dist/apps/runtime.d.ts +2 -4
  4. package/dist/apps-host/client.d.ts +2 -5
  5. package/dist/apps-host/ipc-types.d.ts +2 -5
  6. package/dist/apps-host/ipc-types.js +5 -1
  7. package/dist/bootstrap/rtws-cli.d.ts +0 -1
  8. package/dist/bootstrap/rtws-cli.js +9 -3
  9. package/dist/cli/cert.d.ts +2 -0
  10. package/dist/cli/cert.js +198 -0
  11. package/dist/cli/create.d.ts +1 -1
  12. package/dist/cli/create.js +1 -1
  13. package/dist/cli/read.js +1 -1
  14. package/dist/cli/tui.js +1 -1
  15. package/dist/cli/webui.d.ts +1 -1
  16. package/dist/cli/webui.js +35 -5
  17. package/dist/cli.d.ts +1 -0
  18. package/dist/cli.js +21 -16
  19. package/dist/docs/cli-usage.md +43 -7
  20. package/dist/docs/cli-usage.zh.md +43 -7
  21. package/dist/docs/dialog-persistence.md +1 -1
  22. package/dist/docs/dominds-terminology.md +2 -2
  23. package/dist/server/auth.d.ts +7 -0
  24. package/dist/server/auth.js +15 -4
  25. package/dist/server/certificates.d.ts +61 -0
  26. package/dist/server/certificates.js +418 -0
  27. package/dist/server/dominds-self-update-restart-helper.d.ts +15 -0
  28. package/dist/server/dominds-self-update-restart-helper.js +305 -0
  29. package/dist/server/dominds-self-update.js +162 -120
  30. package/dist/server/network-hosts.d.ts +5 -0
  31. package/dist/server/network-hosts.js +188 -0
  32. package/dist/server/server-core.d.ts +12 -1
  33. package/dist/server/server-core.js +26 -4
  34. package/dist/server/websocket-handler.d.ts +3 -2
  35. package/dist/server/websocket-handler.js +15 -8
  36. package/dist/server.d.ts +5 -0
  37. package/dist/server.js +105 -10
  38. package/package.json +3 -3
@@ -0,0 +1,305 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const child_process_1 = require("child_process");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const net_1 = __importDefault(require("net"));
9
+ const time_1 = require("@longrun-ai/kernel/utils/time");
10
+ function isRecord(value) {
11
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ }
13
+ function isStringArray(value) {
14
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
15
+ }
16
+ function readString(value, key) {
17
+ const candidate = value[key];
18
+ if (typeof candidate !== 'string' || candidate.trim() === '') {
19
+ throw new Error(`Invalid Dominds restart helper payload: ${key} must be a non-empty string`);
20
+ }
21
+ return candidate;
22
+ }
23
+ function readNumber(value, key) {
24
+ const candidate = value[key];
25
+ if (typeof candidate !== 'number' || !Number.isFinite(candidate)) {
26
+ throw new Error(`Invalid Dominds restart helper payload: ${key} must be a finite number`);
27
+ }
28
+ return candidate;
29
+ }
30
+ function readPositiveInteger(value, key) {
31
+ const candidate = readNumber(value, key);
32
+ if (!Number.isInteger(candidate) || candidate <= 0) {
33
+ throw new Error(`Invalid Dominds restart helper payload: ${key} must be a positive integer`);
34
+ }
35
+ return candidate;
36
+ }
37
+ function readPort(value, key) {
38
+ const candidate = readPositiveInteger(value, key);
39
+ if (candidate > 65535) {
40
+ throw new Error(`Invalid Dominds restart helper payload: ${key} must be <= 65535`);
41
+ }
42
+ return candidate;
43
+ }
44
+ function parsePayload(raw) {
45
+ if (typeof raw !== 'string' || raw.trim() === '') {
46
+ throw new Error('Missing Dominds restart helper payload');
47
+ }
48
+ const parsed = JSON.parse(raw);
49
+ if (!isRecord(parsed)) {
50
+ throw new Error('Invalid Dominds restart helper payload: expected an object');
51
+ }
52
+ const args = parsed['args'];
53
+ if (!isStringArray(args)) {
54
+ throw new Error('Invalid Dominds restart helper payload: args must be a string array');
55
+ }
56
+ const stdioMode = parsed['stdioMode'];
57
+ if (stdioMode !== 'inherit' && stdioMode !== 'ignore') {
58
+ throw new Error('Invalid Dominds restart helper payload: stdioMode must be inherit or ignore');
59
+ }
60
+ return {
61
+ command: readString(parsed, 'command'),
62
+ args,
63
+ cwd: readString(parsed, 'cwd'),
64
+ host: readString(parsed, 'host'),
65
+ port: readPort(parsed, 'port'),
66
+ retiringPid: readPositiveInteger(parsed, 'retiringPid'),
67
+ forceKillAfterMs: readPositiveInteger(parsed, 'forceKillAfterMs'),
68
+ probeIntervalMs: readPositiveInteger(parsed, 'probeIntervalMs'),
69
+ portReleaseTimeoutMs: readPositiveInteger(parsed, 'portReleaseTimeoutMs'),
70
+ stdioMode,
71
+ traceFile: readString(parsed, 'traceFile'),
72
+ debugDir: readString(parsed, 'debugDir'),
73
+ };
74
+ }
75
+ function trace(payload, event, details = {}) {
76
+ const record = {
77
+ ...details,
78
+ event,
79
+ capturedAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
80
+ helperPid: process.pid,
81
+ platform: process.platform,
82
+ };
83
+ try {
84
+ fs_1.default.mkdirSync(payload.debugDir, { recursive: true });
85
+ fs_1.default.appendFileSync(payload.traceFile, `${JSON.stringify(record)}\n`, 'utf8');
86
+ }
87
+ catch (error) {
88
+ const message = error instanceof Error ? error.message : String(error);
89
+ console.error(`Failed to write Dominds restart helper trace: ${message}`);
90
+ }
91
+ }
92
+ function isPortBusy(payload) {
93
+ return new Promise((resolve) => {
94
+ const socket = net_1.default.createConnection({ host: payload.host, port: payload.port });
95
+ let settled = false;
96
+ const finish = (busy) => {
97
+ if (settled)
98
+ return;
99
+ settled = true;
100
+ socket.destroy();
101
+ resolve(busy);
102
+ };
103
+ socket.once('connect', () => finish(true));
104
+ socket.once('error', () => finish(false));
105
+ socket.setTimeout(1000, () => finish(true));
106
+ });
107
+ }
108
+ function getErrorCode(error) {
109
+ return isRecord(error) && typeof error['code'] === 'string' ? error['code'] : '';
110
+ }
111
+ function assertValidRetiringPid(payload) {
112
+ if (!Number.isInteger(payload.retiringPid) || payload.retiringPid <= 0) {
113
+ throw new Error(`Invalid retiring Dominds pid for restart: ${String(payload.retiringPid)}`);
114
+ }
115
+ if (payload.retiringPid === process.pid) {
116
+ throw new Error(`Refusing to kill restart helper pid ${String(process.pid)}`);
117
+ }
118
+ }
119
+ function isRetiringProcessAlive(payload) {
120
+ assertValidRetiringPid(payload);
121
+ try {
122
+ process.kill(payload.retiringPid, 0);
123
+ return true;
124
+ }
125
+ catch (error) {
126
+ const code = getErrorCode(error);
127
+ if (code === 'ESRCH')
128
+ return false;
129
+ if (code === 'EPERM')
130
+ return true;
131
+ throw error;
132
+ }
133
+ }
134
+ async function delayMs(ms) {
135
+ await new Promise((resolve) => {
136
+ setTimeout(resolve, ms);
137
+ });
138
+ }
139
+ async function waitForRetiringProcessExit(payload, timeoutMs) {
140
+ const deadline = Date.now() + timeoutMs;
141
+ while (Date.now() < deadline) {
142
+ if (!isRetiringProcessAlive(payload))
143
+ return true;
144
+ await delayMs(payload.probeIntervalMs);
145
+ }
146
+ return false;
147
+ }
148
+ async function waitForPortReleaseUntil(payload, deadline) {
149
+ let consecutiveReady = 0;
150
+ while (Date.now() < deadline) {
151
+ if (!(await isPortBusy(payload))) {
152
+ consecutiveReady += 1;
153
+ if (consecutiveReady >= 2)
154
+ return true;
155
+ }
156
+ else {
157
+ consecutiveReady = 0;
158
+ }
159
+ await delayMs(payload.probeIntervalMs);
160
+ }
161
+ return false;
162
+ }
163
+ async function runBestEffortKiller(payload, command, args) {
164
+ await new Promise((resolve) => {
165
+ trace(payload, 'helper.force_kill.spawn', { command, args });
166
+ const killer = (0, child_process_1.spawn)(command, [...args], {
167
+ stdio: payload.stdioMode,
168
+ windowsHide: payload.stdioMode !== 'inherit',
169
+ });
170
+ killer.once('error', (error) => {
171
+ trace(payload, 'helper.force_kill.error', { message: error.message });
172
+ resolve();
173
+ });
174
+ killer.once('exit', (code, signal) => {
175
+ trace(payload, 'helper.force_kill.exit', { code, signal });
176
+ resolve();
177
+ });
178
+ });
179
+ }
180
+ async function forceKillRetiringProcess(payload) {
181
+ assertValidRetiringPid(payload);
182
+ try {
183
+ process.kill(payload.retiringPid, 'SIGKILL');
184
+ }
185
+ catch (error) {
186
+ const code = getErrorCode(error);
187
+ if (code === 'ESRCH')
188
+ return;
189
+ if (process.platform !== 'win32')
190
+ throw error;
191
+ }
192
+ if (process.platform === 'win32') {
193
+ await runBestEffortKiller(payload, 'taskkill.exe', ['/PID', String(payload.retiringPid), '/F']);
194
+ }
195
+ }
196
+ async function runRestartHelper(payload) {
197
+ const detached = payload.stdioMode !== 'inherit';
198
+ trace(payload, 'helper.start', {
199
+ command: payload.command,
200
+ args: payload.args,
201
+ cwd: payload.cwd,
202
+ host: payload.host,
203
+ port: payload.port,
204
+ retiringPid: payload.retiringPid,
205
+ detached,
206
+ stdioMode: payload.stdioMode,
207
+ forceKillAfterMs: payload.forceKillAfterMs,
208
+ portReleaseTimeoutMs: payload.portReleaseTimeoutMs,
209
+ probeIntervalMs: payload.probeIntervalMs,
210
+ });
211
+ const forceKillDeadline = Date.now() + payload.forceKillAfterMs;
212
+ trace(payload, 'helper.wait_retiring_process_exit.start', {
213
+ retiringPid: payload.retiringPid,
214
+ timeoutMs: payload.forceKillAfterMs,
215
+ });
216
+ const exitedGracefully = await waitForRetiringProcessExit(payload, payload.forceKillAfterMs);
217
+ trace(payload, 'helper.wait_retiring_process_exit.finish', {
218
+ retiringPid: payload.retiringPid,
219
+ exited: exitedGracefully,
220
+ });
221
+ if (!exitedGracefully) {
222
+ trace(payload, 'helper.force_kill.start', { retiringPid: payload.retiringPid });
223
+ await forceKillRetiringProcess(payload);
224
+ trace(payload, 'helper.force_kill.finish', { retiringPid: payload.retiringPid });
225
+ trace(payload, 'helper.wait_retiring_process_exit_after_kill.start', {
226
+ retiringPid: payload.retiringPid,
227
+ timeoutMs: payload.portReleaseTimeoutMs,
228
+ });
229
+ const exitedAfterKill = await waitForRetiringProcessExit(payload, payload.portReleaseTimeoutMs);
230
+ trace(payload, 'helper.wait_retiring_process_exit_after_kill.finish', {
231
+ retiringPid: payload.retiringPid,
232
+ exited: exitedAfterKill,
233
+ });
234
+ if (!exitedAfterKill) {
235
+ throw new Error(`Dominds retiring process is still alive after force-killing pid ${String(payload.retiringPid)}`);
236
+ }
237
+ }
238
+ const portReleaseDeadline = Math.max(forceKillDeadline, Date.now() + payload.portReleaseTimeoutMs);
239
+ trace(payload, 'helper.wait_port_release.start', {
240
+ host: payload.host,
241
+ port: payload.port,
242
+ deadlineMsFromNow: portReleaseDeadline - Date.now(),
243
+ });
244
+ const portReleased = await waitForPortReleaseUntil(payload, portReleaseDeadline);
245
+ trace(payload, 'helper.wait_port_release.finish', {
246
+ host: payload.host,
247
+ port: payload.port,
248
+ released: portReleased,
249
+ });
250
+ if (!portReleased) {
251
+ throw new Error(`Dominds restart port is still busy after retiring pid ${String(payload.retiringPid)} exited; port=${String(payload.host)}:${String(payload.port)}`);
252
+ }
253
+ trace(payload, 'helper.spawn_new_process.start', {
254
+ command: payload.command,
255
+ args: payload.args,
256
+ cwd: payload.cwd,
257
+ detached,
258
+ stdioMode: payload.stdioMode,
259
+ });
260
+ const child = (0, child_process_1.spawn)(payload.command, [...payload.args], {
261
+ cwd: payload.cwd,
262
+ env: process.env,
263
+ detached,
264
+ stdio: payload.stdioMode,
265
+ shell: false,
266
+ windowsHide: payload.stdioMode !== 'inherit',
267
+ });
268
+ if (detached)
269
+ child.unref();
270
+ await new Promise((resolve, reject) => {
271
+ child.once('error', (error) => {
272
+ trace(payload, 'helper.spawn_new_process.error', {
273
+ command: payload.command,
274
+ args: payload.args,
275
+ cwd: payload.cwd,
276
+ message: error.message,
277
+ stack: error.stack ?? null,
278
+ });
279
+ reject(error);
280
+ });
281
+ child.once('spawn', resolve);
282
+ });
283
+ trace(payload, 'helper.spawn_new_process.finish', { childPid: child.pid ?? null });
284
+ trace(payload, 'helper.exit', { code: 0 });
285
+ }
286
+ async function main() {
287
+ let payload = null;
288
+ try {
289
+ payload = parsePayload(process.argv[2]);
290
+ await runRestartHelper(payload);
291
+ process.exit(0);
292
+ }
293
+ catch (error) {
294
+ const message = error instanceof Error ? error.message : String(error);
295
+ const stack = error instanceof Error ? (error.stack ?? null) : null;
296
+ if (payload !== null) {
297
+ trace(payload, 'helper.error', { message, stack });
298
+ }
299
+ console.error(message);
300
+ process.exit(1);
301
+ }
302
+ }
303
+ if (require.main === module) {
304
+ void main();
305
+ }
@@ -10,6 +10,7 @@ exports.getDomindsSelfUpdateStatus = getDomindsSelfUpdateStatus;
10
10
  exports.installLatestDominds = installLatestDominds;
11
11
  exports.restartDomindsIntoLatest = restartDomindsIntoLatest;
12
12
  const child_process_1 = require("child_process");
13
+ const crypto_1 = require("crypto");
13
14
  const promises_1 = __importDefault(require("fs/promises"));
14
15
  const path_1 = __importDefault(require("path"));
15
16
  const time_1 = require("@longrun-ai/kernel/utils/time");
@@ -43,6 +44,7 @@ let restartPromise = null;
43
44
  let restartState = IDLE_RESTART_STATE;
44
45
  let installFailureObservation = null;
45
46
  let broadcastStatusUpdate = null;
47
+ let activeRestartTraceFile = null;
46
48
  function normalizeVersionString(value) {
47
49
  return value.trim().replace(/^v/i, '');
48
50
  }
@@ -101,6 +103,55 @@ function normalizeComparablePath(value) {
101
103
  const normalized = path_1.default.normalize(value);
102
104
  return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
103
105
  }
106
+ function sanitizeDebugFileSegment(value) {
107
+ const sanitized = value.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^_+|_+$/g, '');
108
+ return sanitized.length > 0 ? sanitized.slice(0, 80) : 'unknown';
109
+ }
110
+ function buildRestartTraceContext() {
111
+ const capturedAt = (0, time_1.formatUnifiedTimestamp)(new Date());
112
+ const debugDir = path_1.default.resolve(process.cwd(), '.dialogs', 'debug');
113
+ const traceFile = path_1.default.join(debugDir, [
114
+ 'dominds-self-update-restart',
115
+ sanitizeDebugFileSegment(capturedAt),
116
+ String(process.pid),
117
+ `${(0, crypto_1.randomUUID)()}.jsonl`,
118
+ ].join('-'));
119
+ return { debugDir, traceFile };
120
+ }
121
+ function getRestartHelperEntrypoint() {
122
+ return path_1.default.resolve(__dirname, 'dominds-self-update-restart-helper.js');
123
+ }
124
+ async function appendRestartTrace(trace, event, details = {}) {
125
+ const payload = {
126
+ ...details,
127
+ event,
128
+ capturedAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
129
+ pid: process.pid,
130
+ platform: process.platform,
131
+ rtwsRootAbs: process.cwd(),
132
+ };
133
+ await promises_1.default.mkdir(trace.debugDir, { recursive: true });
134
+ await promises_1.default.appendFile(trace.traceFile, `${JSON.stringify(payload)}\n`, 'utf-8');
135
+ }
136
+ function appendRestartTraceSoon(trace, event, details = {}) {
137
+ void appendRestartTrace(trace, event, details).catch((error) => {
138
+ log.warn('Failed to write Dominds restart trace', error, {
139
+ traceFile: trace.traceFile,
140
+ event,
141
+ });
142
+ });
143
+ }
144
+ async function appendRestartTraceBestEffort(trace, event, details = {}) {
145
+ try {
146
+ await appendRestartTrace(trace, event, details);
147
+ }
148
+ catch (error) {
149
+ log.warn('Failed to write Dominds restart trace', error, {
150
+ traceFile: trace.traceFile,
151
+ event,
152
+ });
153
+ }
154
+ }
104
155
  async function getComparableRealPath(absPath) {
105
156
  try {
106
157
  return normalizeComparablePath(await promises_1.default.realpath(absPath));
@@ -1331,9 +1382,11 @@ async function installLatestDominds() {
1331
1382
  publishStatusUpdateSoon();
1332
1383
  return await getDomindsSelfUpdateStatus();
1333
1384
  }
1334
- function spawnDetachedRestartHelper(params) {
1385
+ async function spawnDetachedRestartHelper(params) {
1335
1386
  const stdioMode = getRestartHelperStdio();
1336
- const helperPayload = JSON.stringify({
1387
+ const helperEntrypoint = getRestartHelperEntrypoint();
1388
+ await promises_1.default.access(helperEntrypoint);
1389
+ const helperPayload = {
1337
1390
  command: params.command,
1338
1391
  args: [...params.args],
1339
1392
  cwd: params.cwd,
@@ -1344,145 +1397,88 @@ function spawnDetachedRestartHelper(params) {
1344
1397
  probeIntervalMs: RESTART_PORT_PROBE_INTERVAL_MS,
1345
1398
  portReleaseTimeoutMs: RESTART_PORT_RELEASE_TIMEOUT_MS,
1346
1399
  stdioMode,
1347
- });
1348
- const helperScript = [
1349
- "const net = require('net');",
1350
- "const { spawn } = require('child_process');",
1351
- 'const payload = JSON.parse(process.argv[1]);',
1352
- 'const detached = payload.stdioMode !== "inherit";',
1353
- 'function isPortBusy() {',
1354
- ' return new Promise((resolve) => {',
1355
- ' const socket = net.createConnection({ host: payload.host, port: payload.port });',
1356
- ' let settled = false;',
1357
- ' const finish = (busy) => {',
1358
- ' if (settled) return;',
1359
- ' settled = true;',
1360
- ' socket.destroy();',
1361
- ' resolve(busy);',
1362
- ' };',
1363
- ' socket.once("connect", () => finish(true));',
1364
- ' socket.once("error", () => finish(false));',
1365
- ' socket.setTimeout(1000, () => finish(true));',
1366
- ' });',
1367
- '}',
1368
- 'function getErrorCode(error) {',
1369
- ' return error && typeof error === "object" && typeof error.code === "string" ? error.code : "";',
1370
- '}',
1371
- 'function assertValidRetiringPid() {',
1372
- ' if (!Number.isInteger(payload.retiringPid) || payload.retiringPid <= 0) {',
1373
- ' throw new Error(`Invalid retiring Dominds pid for restart: ${String(payload.retiringPid)}`);',
1374
- ' }',
1375
- ' if (payload.retiringPid === process.pid) {',
1376
- ' throw new Error(`Refusing to kill restart helper pid ${String(process.pid)}`);',
1377
- ' }',
1378
- '}',
1379
- 'function isRetiringProcessAlive() {',
1380
- ' assertValidRetiringPid();',
1381
- ' try {',
1382
- ' process.kill(payload.retiringPid, 0);',
1383
- ' return true;',
1384
- ' } catch (error) {',
1385
- ' const code = getErrorCode(error);',
1386
- ' if (code === "ESRCH") return false;',
1387
- ' if (code === "EPERM") return true;',
1388
- ' throw error;',
1389
- ' }',
1390
- '}',
1391
- 'async function waitForRetiringProcessExit(timeoutMs) {',
1392
- ' const deadline = Date.now() + timeoutMs;',
1393
- ' while (Date.now() < deadline) {',
1394
- ' if (!isRetiringProcessAlive()) return true;',
1395
- ' await new Promise((resolve) => setTimeout(resolve, payload.probeIntervalMs));',
1396
- ' }',
1397
- ' return false;',
1398
- '}',
1399
- 'async function waitForPortReleaseUntil(deadline) {',
1400
- ' let consecutiveReady = 0;',
1401
- ' while (Date.now() < deadline) {',
1402
- ' if (!(await isPortBusy())) {',
1403
- ' consecutiveReady += 1;',
1404
- ' if (consecutiveReady >= 2) return true;',
1405
- ' } else {',
1406
- ' consecutiveReady = 0;',
1407
- ' }',
1408
- ' await new Promise((resolve) => setTimeout(resolve, payload.probeIntervalMs));',
1409
- ' }',
1410
- ' return false;',
1411
- '}',
1412
- 'function runBestEffortKiller(command, args) {',
1413
- ' return new Promise((resolve) => {',
1414
- ' const killer = spawn(command, args, { stdio: payload.stdioMode, windowsHide: payload.stdioMode !== "inherit" });',
1415
- " killer.once('error', () => resolve());",
1416
- " killer.once('exit', () => resolve());",
1417
- ' });',
1418
- '}',
1419
- 'async function forceKillRetiringProcess() {',
1420
- ' assertValidRetiringPid();',
1421
- ' try {',
1422
- " process.kill(payload.retiringPid, 'SIGKILL');",
1423
- ' } catch (error) {',
1424
- ' const code = getErrorCode(error);',
1425
- ' if (code === "ESRCH") return;',
1426
- ' if (process.platform !== "win32") throw error;',
1427
- ' }',
1428
- ' if (process.platform === "win32") {',
1429
- " await runBestEffortKiller('taskkill.exe', ['/PID', String(payload.retiringPid), '/F']);",
1430
- ' }',
1431
- '}',
1432
- '(async () => {',
1433
- ' try {',
1434
- ' const forceKillDeadline = Date.now() + payload.forceKillAfterMs;',
1435
- ' const exitedGracefully = await waitForRetiringProcessExit(payload.forceKillAfterMs);',
1436
- ' if (!exitedGracefully) {',
1437
- ' await forceKillRetiringProcess();',
1438
- ' const exitedAfterKill = await waitForRetiringProcessExit(payload.portReleaseTimeoutMs);',
1439
- ' if (!exitedAfterKill) {',
1440
- ' throw new Error(`Dominds retiring process is still alive after force-killing pid ${String(payload.retiringPid)}`);',
1441
- ' }',
1442
- ' }',
1443
- ' const portReleaseDeadline = Math.max(forceKillDeadline, Date.now() + payload.portReleaseTimeoutMs);',
1444
- ' const portReleased = await waitForPortReleaseUntil(portReleaseDeadline);',
1445
- ' if (!portReleased) {',
1446
- ' throw new Error(`Dominds restart port is still busy after retiring pid ${String(payload.retiringPid)} exited; port=${String(payload.host)}:${String(payload.port)}`);',
1447
- ' }',
1448
- ' const child = spawn(payload.command, payload.args, { cwd: payload.cwd, env: process.env, detached, stdio: payload.stdioMode, shell: false, windowsHide: payload.stdioMode !== "inherit" });',
1449
- ' if (detached) child.unref();',
1450
- ' await new Promise((resolve, reject) => {',
1451
- " child.once('error', reject);",
1452
- " child.once('spawn', resolve);",
1453
- ' });',
1454
- ' process.exit(0);',
1455
- ' } catch (error) {',
1456
- ' console.error(error instanceof Error ? error.message : String(error));',
1457
- ' process.exit(1);',
1458
- ' }',
1459
- '})();',
1460
- ].join('\n');
1461
- const helper = (0, child_process_1.spawn)(process.execPath, ['-e', helperScript, helperPayload], {
1400
+ traceFile: params.trace.traceFile,
1401
+ debugDir: params.trace.debugDir,
1402
+ };
1403
+ const helper = (0, child_process_1.spawn)(process.execPath, [helperEntrypoint, JSON.stringify(helperPayload)], {
1462
1404
  cwd: params.cwd,
1463
1405
  env: process.env,
1464
1406
  detached: stdioMode !== 'inherit',
1465
1407
  stdio: stdioMode,
1466
1408
  windowsHide: stdioMode !== 'inherit',
1467
1409
  });
1410
+ const helperPid = typeof helper.pid === 'number' ? helper.pid : null;
1411
+ const helperSpawned = new Promise((resolve, reject) => {
1412
+ helper.once('spawn', () => {
1413
+ appendRestartTraceSoon(params.trace, 'parent.helper_spawn_event', {
1414
+ helperPid,
1415
+ });
1416
+ resolve(helperPid);
1417
+ });
1418
+ helper.once('error', (error) => {
1419
+ appendRestartTraceSoon(params.trace, 'parent.helper_error_event', {
1420
+ message: error.message,
1421
+ stack: error.stack ?? null,
1422
+ });
1423
+ reject(error);
1424
+ });
1425
+ });
1426
+ helper.once('exit', (code, signal) => {
1427
+ appendRestartTraceSoon(params.trace, 'parent.helper_exit_event', {
1428
+ code,
1429
+ signal,
1430
+ });
1431
+ });
1468
1432
  if (stdioMode !== 'inherit') {
1469
1433
  helper.unref();
1470
1434
  }
1435
+ return await helperSpawned;
1471
1436
  }
1472
1437
  async function stopAndExitForRestart() {
1473
1438
  const cfg = assertRuntimeConfig();
1439
+ const trace = activeRestartTraceFile === null
1440
+ ? null
1441
+ : { debugDir: path_1.default.dirname(activeRestartTraceFile), traceFile: activeRestartTraceFile };
1474
1442
  let stopSettled = false;
1475
- void cfg
1443
+ if (trace !== null) {
1444
+ await appendRestartTraceBestEffort(trace, 'parent.stop_and_exit.start', {
1445
+ host: cfg.host,
1446
+ port: cfg.port,
1447
+ exitGraceMs: RESTART_EXIT_GRACE_MS,
1448
+ });
1449
+ }
1450
+ const stopPromise = cfg
1476
1451
  .stopServer()
1477
1452
  .then(() => {
1478
1453
  stopSettled = true;
1454
+ if (trace !== null) {
1455
+ return appendRestartTraceBestEffort(trace, 'parent.stop_server.finish', {
1456
+ host: cfg.host,
1457
+ port: cfg.port,
1458
+ });
1459
+ }
1460
+ return undefined;
1479
1461
  })
1480
1462
  .catch((error) => {
1481
1463
  stopSettled = true;
1464
+ if (trace !== null) {
1465
+ return appendRestartTraceBestEffort(trace, 'parent.stop_server.error', {
1466
+ host: cfg.host,
1467
+ port: cfg.port,
1468
+ message: error instanceof Error ? error.message : String(error),
1469
+ stack: error instanceof Error ? (error.stack ?? null) : null,
1470
+ }).then(() => {
1471
+ log.error('Failed to stop Dominds HTTP server during restart grace window', error, {
1472
+ host: cfg.host,
1473
+ port: cfg.port,
1474
+ });
1475
+ });
1476
+ }
1482
1477
  log.error('Failed to stop Dominds HTTP server during restart grace window', error, {
1483
1478
  host: cfg.host,
1484
1479
  port: cfg.port,
1485
1480
  });
1481
+ return undefined;
1486
1482
  });
1487
1483
  cfg.closeWebSocketClients();
1488
1484
  await delayMs(RESTART_EXIT_GRACE_MS);
@@ -1493,6 +1489,17 @@ async function stopAndExitForRestart() {
1493
1489
  graceMs: RESTART_EXIT_GRACE_MS,
1494
1490
  });
1495
1491
  }
1492
+ if (trace !== null) {
1493
+ if (stopSettled) {
1494
+ await stopPromise;
1495
+ }
1496
+ await appendRestartTraceBestEffort(trace, 'parent.process_exit', {
1497
+ host: cfg.host,
1498
+ port: cfg.port,
1499
+ stopSettled,
1500
+ code: 0,
1501
+ });
1502
+ }
1496
1503
  process.exit(0);
1497
1504
  }
1498
1505
  async function restartDomindsIntoLatest() {
@@ -1529,24 +1536,59 @@ async function restartDomindsIntoLatest() {
1529
1536
  }
1530
1537
  restartState = { kind: 'restarting', targetVersion: status.targetVersion };
1531
1538
  publishStatusUpdateSoon();
1539
+ const restartCwd = process.cwd();
1540
+ const trace = buildRestartTraceContext();
1541
+ activeRestartTraceFile = trace.traceFile;
1532
1542
  try {
1533
- spawnDetachedRestartHelper({
1543
+ await appendRestartTraceBestEffort(trace, 'parent.restart_requested', {
1544
+ runKind,
1545
+ currentVersion: status.currentVersion,
1546
+ targetVersion: status.targetVersion,
1547
+ launchCommand: launchSpec.command,
1548
+ launchArgs: launchSpec.args,
1549
+ restartCwd,
1550
+ host: cfg.host,
1551
+ port: cfg.port,
1552
+ traceFile: trace.traceFile,
1553
+ });
1554
+ const helperPid = await spawnDetachedRestartHelper({
1534
1555
  command: launchSpec.command,
1535
1556
  args: launchSpec.args,
1536
- cwd: PROCESS_START_CWD,
1557
+ cwd: restartCwd,
1558
+ host: cfg.host,
1559
+ port: cfg.port,
1560
+ trace,
1561
+ });
1562
+ await appendRestartTraceBestEffort(trace, 'parent.helper_spawned', {
1563
+ helperPid,
1564
+ retiringPid: process.pid,
1537
1565
  host: cfg.host,
1538
1566
  port: cfg.port,
1539
1567
  });
1568
+ log.info('Dominds restart helper spawned', undefined, {
1569
+ helperPid,
1570
+ traceFile: trace.traceFile,
1571
+ });
1540
1572
  setImmediate(() => {
1541
1573
  void stopAndExitForRestart().catch((error) => {
1574
+ void appendRestartTraceBestEffort(trace, 'parent.stop_and_exit.error', {
1575
+ message: error instanceof Error ? error.message : String(error),
1576
+ stack: error instanceof Error ? (error.stack ?? null) : null,
1577
+ });
1542
1578
  log.error('Failed to stop Dominds server during restart', error);
1543
1579
  restartState = previousRestartRequiredState ?? IDLE_RESTART_STATE;
1580
+ activeRestartTraceFile = null;
1544
1581
  publishStatusUpdateSoon();
1545
1582
  });
1546
1583
  });
1547
1584
  }
1548
1585
  catch (error) {
1586
+ await appendRestartTraceBestEffort(trace, 'parent.restart_error', {
1587
+ message: error instanceof Error ? error.message : String(error),
1588
+ stack: error instanceof Error ? (error.stack ?? null) : null,
1589
+ });
1549
1590
  restartState = previousRestartRequiredState ?? IDLE_RESTART_STATE;
1591
+ activeRestartTraceFile = null;
1550
1592
  publishStatusUpdateSoon();
1551
1593
  throw error;
1552
1594
  }
@@ -0,0 +1,5 @@
1
+ export declare function normalizeNetworkHost(host: string): string;
2
+ export declare function isLanHttpsHost(host: string): boolean;
3
+ export declare function resolveHttpUrlHostForBindHost(bindHost: string): string;
4
+ export declare function resolveLanHttpsHostsForBindHost(bindHost: string): Promise<readonly string[]>;
5
+ export declare function resolveDefaultLanHttpsHosts(): Promise<readonly string[]>;