@adhdev/daemon-core 1.0.28-rc.10 → 1.0.28-rc.12
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/dist/cli-adapter-types.d.ts +7 -0
- package/dist/commands/upgrade-helper.d.ts +14 -0
- package/dist/index.js +623 -84
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +623 -84
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +69 -16
- package/dist/providers/spec/cli-adapter.d.ts +7 -0
- package/dist/providers/spec/fsm-driver.d.ts +35 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +28 -2
- package/dist/providers/spec/fsm-types.d.ts +19 -1
- package/dist/providers/spec/signal-envelope.d.ts +59 -0
- package/dist/providers/transcript-signal-source.d.ts +59 -0
- package/package.json +3 -3
- package/src/cli-adapter-types.ts +7 -0
- package/src/commands/low-family/daemon-lifecycle.ts +41 -0
- package/src/commands/med-family/mesh-restart.ts +233 -30
- package/src/commands/upgrade-helper.ts +34 -3
- package/src/providers/cli-provider-instance.ts +204 -87
- package/src/providers/spec/cli-adapter.ts +10 -0
- package/src/providers/spec/fsm-driver.ts +62 -1
- package/src/providers/spec/fsm-evaluator.ts +110 -5
- package/src/providers/spec/fsm-loader.ts +7 -0
- package/src/providers/spec/fsm-types.ts +20 -0
- package/src/providers/spec/signal-envelope.ts +136 -0
- package/src/providers/transcript-signal-source.ts +179 -0
|
@@ -1,23 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* RF-ROUTER MED family — coordinator-triggered daemon restart.
|
|
3
3
|
*
|
|
4
|
-
* restart_daemon_node exposes the
|
|
5
|
-
*
|
|
6
|
-
* a
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
4
|
+
* restart_daemon_node exposes the daemon lifecycle paths (low-family
|
|
5
|
+
* daemon_upgrade / daemon_restart) as a mesh command, so a coordinator can roll
|
|
6
|
+
* a worker daemon onto a freshly deployed version — or just reset its state —
|
|
7
|
+
* without a manual restart round-trip. It mirrors fast_forward_mesh_node's
|
|
8
|
+
* remote-forward shape — resolve the target node, and if it belongs to a remote
|
|
9
|
+
* daemon forward the command there so the owning daemon (not the coordinator)
|
|
10
|
+
* restarts itself — and adds an idle-gate: a node with a generating /
|
|
11
|
+
* waiting_approval / starting session is refused so an in-flight turn is never
|
|
12
|
+
* killed mid-restart.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* Modes and gate overrides (all opt-in; a bare call behaves exactly like v1):
|
|
15
|
+
*
|
|
16
|
+
* - mode: 'upgrade' (default) reuses daemon_upgrade verbatim — update to the
|
|
17
|
+
* latest published version on the resolved channel, then detached-restart.
|
|
18
|
+
* Already-latest is a no-op (no restart), matching the dashboard button.
|
|
19
|
+
* mode: 'restart' uses daemon_restart — a pure re-spawn with no npm
|
|
20
|
+
* reinstall, so it restarts even when already latest and the downtime is
|
|
21
|
+
* just the detached re-spawn. Use it to reset wedged daemon state.
|
|
22
|
+
*
|
|
23
|
+
* - selfOnly: waives blocking sessions that are THIS mesh's own coordinator
|
|
24
|
+
* session (settings.meshCoordinatorFor === meshId). This breaks the
|
|
25
|
+
* structural deadlock where the coordinator can never restart its own
|
|
26
|
+
* daemon because its calling turn is always 'generating'. Other nodes'
|
|
27
|
+
* blocking sessions still refuse. Ad-hoc coordinator sessions launched
|
|
28
|
+
* without mesh_coordinator_launch carry no marker — use force for those.
|
|
29
|
+
*
|
|
30
|
+
* - force: bypasses the idle-gate entirely. Destructive: in-flight turns are
|
|
31
|
+
* killed and the in-memory pendingOutboundQueue is lost (it has no
|
|
32
|
+
* persistence/restore path). Audit-logged.
|
|
33
|
+
*
|
|
34
|
+
* - whenIdle: instead of refusing, schedule the restart and run it
|
|
35
|
+
* automatically once all blocking sessions go idle (the safest path — the
|
|
36
|
+
* pendingOutboundQueue is empty at that point). The schedule expires after
|
|
37
|
+
* timeoutMs (default 30 min). cancelWhenIdle cancels it; every response
|
|
38
|
+
* carries the current schedule under `deferredRestart` for visibility.
|
|
39
|
+
*
|
|
40
|
+
* - killSessionHost: also stop the session-host process, destroying EVERY
|
|
41
|
+
* hosted CLI session (no idle-gate — SessionHostServer.stop kills all PTYs).
|
|
42
|
+
* Mirrors what Windows already does on upgrade (conpty.node lock). Default
|
|
43
|
+
* off; POSIX daemons otherwise keep hosted sessions alive across a restart.
|
|
18
44
|
*/
|
|
19
45
|
import { daemonIdsEquivalent, meshNodeIdMatches } from '@adhdev/mesh-shared';
|
|
20
46
|
import { daemonLifecycleHandlers } from '../low-family/daemon-lifecycle.js';
|
|
47
|
+
import { LOG } from '../../logging/logger.js';
|
|
21
48
|
import type { CommandRouterResult } from '../router.js';
|
|
22
49
|
import type { MedFamilyContext, MedFamilyHandler } from './types.js';
|
|
23
50
|
|
|
@@ -26,18 +53,167 @@ import type { MedFamilyContext, MedFamilyHandler } from './types.js';
|
|
|
26
53
|
// daemon-cloud mandatory-update idle-gate (hasBlockingSessionsForMandatoryUpdate).
|
|
27
54
|
const RESTART_BLOCKING_STATES = new Set(['generating', 'waiting_approval', 'starting']);
|
|
28
55
|
|
|
29
|
-
|
|
56
|
+
const DEFERRED_RESTART_DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
57
|
+
const DEFERRED_RESTART_MAX_TIMEOUT_MS = 6 * 60 * 60 * 1000;
|
|
58
|
+
const DEFERRED_RESTART_POLL_MS = 10_000;
|
|
59
|
+
|
|
60
|
+
type BlockingSession = {
|
|
61
|
+
instanceId: string | null;
|
|
62
|
+
status: string;
|
|
63
|
+
/** True when this session is the calling mesh's own coordinator session. */
|
|
64
|
+
selfCoordinator: boolean;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function collectBlockingSessions(ctx: MedFamilyContext, meshId: string): BlockingSession[] {
|
|
68
|
+
const blocking: BlockingSession[] = [];
|
|
30
69
|
const states = ctx.deps.instanceManager.collectAllStates();
|
|
70
|
+
const consider = (state: any) => {
|
|
71
|
+
const status = String(state?.status || '');
|
|
72
|
+
if (!RESTART_BLOCKING_STATES.has(status)) return;
|
|
73
|
+
blocking.push({
|
|
74
|
+
instanceId: typeof state?.instanceId === 'string' ? state.instanceId : null,
|
|
75
|
+
status,
|
|
76
|
+
// The coordinator marker is stamped by mesh_coordinator_launch and
|
|
77
|
+
// re-stamped on restore (cli-manager.restoreHostedSessions). Ad-hoc
|
|
78
|
+
// coordinator sessions have no marker and are NOT waived by selfOnly.
|
|
79
|
+
selfCoordinator: !!meshId && state?.settings?.meshCoordinatorFor === meshId,
|
|
80
|
+
});
|
|
81
|
+
};
|
|
31
82
|
for (const state of states) {
|
|
32
|
-
|
|
83
|
+
consider(state);
|
|
33
84
|
const childStates = 'extensions' in state && Array.isArray((state as any).extensions)
|
|
34
85
|
? (state as any).extensions
|
|
35
86
|
: [];
|
|
36
|
-
for (const child of childStates)
|
|
37
|
-
|
|
38
|
-
|
|
87
|
+
for (const child of childStates) consider(child);
|
|
88
|
+
}
|
|
89
|
+
return blocking;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type RestartMode = 'upgrade' | 'restart';
|
|
93
|
+
|
|
94
|
+
type PendingDeferredRestart = {
|
|
95
|
+
meshId: string;
|
|
96
|
+
nodeId: string;
|
|
97
|
+
mode: RestartMode;
|
|
98
|
+
killSessionHost: boolean;
|
|
99
|
+
scheduledAt: number;
|
|
100
|
+
expiresAt: number;
|
|
101
|
+
timer: NodeJS.Timeout;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// One scheduled restart per daemon process. The record lives on the OWNING
|
|
105
|
+
// daemon (the command is forwarded there before scheduling), which is also the
|
|
106
|
+
// process whose session states the poll inspects.
|
|
107
|
+
let pendingDeferredRestart: PendingDeferredRestart | null = null;
|
|
108
|
+
|
|
109
|
+
function deferredRestartInfo(): Record<string, unknown> | null {
|
|
110
|
+
if (!pendingDeferredRestart) return null;
|
|
111
|
+
return {
|
|
112
|
+
meshId: pendingDeferredRestart.meshId,
|
|
113
|
+
nodeId: pendingDeferredRestart.nodeId,
|
|
114
|
+
mode: pendingDeferredRestart.mode,
|
|
115
|
+
killSessionHost: pendingDeferredRestart.killSessionHost,
|
|
116
|
+
scheduledAt: new Date(pendingDeferredRestart.scheduledAt).toISOString(),
|
|
117
|
+
expiresAt: new Date(pendingDeferredRestart.expiresAt).toISOString(),
|
|
118
|
+
runCondition: 'executes automatically once no session is generating / waiting_approval / starting',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function clearPendingDeferredRestart(): void {
|
|
123
|
+
if (pendingDeferredRestart) clearInterval(pendingDeferredRestart.timer);
|
|
124
|
+
pendingDeferredRestart = null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeRestartMode(value: unknown): RestartMode {
|
|
128
|
+
return value === 'restart' ? 'restart' : 'upgrade';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function restartWarnings(args: { killSessionHost: boolean; forced: boolean }): string[] {
|
|
132
|
+
const warnings: string[] = [];
|
|
133
|
+
if (args.forced) {
|
|
134
|
+
warnings.push('forced restart over active sessions — in-flight turns were interrupted and the in-memory pendingOutboundQueue is permanently lost (no persistence/restore path).');
|
|
135
|
+
}
|
|
136
|
+
if (args.killSessionHost) {
|
|
137
|
+
warnings.push('killSessionHost: the session-host process was stopped — ALL hosted CLI sessions on this machine are destroyed (hard refresh).');
|
|
138
|
+
}
|
|
139
|
+
if (process.platform === 'win32') {
|
|
140
|
+
warnings.push('Windows: the daemon restart/upgrade path stops the session-host regardless of options — all hosted sessions terminate.');
|
|
141
|
+
} else {
|
|
142
|
+
warnings.push('POSIX: hosted PTY sessions survive a plain daemon restart and rebind on next boot (unless killSessionHost was set).');
|
|
143
|
+
}
|
|
144
|
+
return warnings;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function executeRestart(ctx: MedFamilyContext, args: any, opts: { forced: boolean }): Promise<CommandRouterResult> {
|
|
148
|
+
const mode = normalizeRestartMode(args?.mode);
|
|
149
|
+
const killSessionHost = args?.killSessionHost === true;
|
|
150
|
+
const result = mode === 'restart'
|
|
151
|
+
? await daemonLifecycleHandlers.daemon_restart({ deps: ctx.deps }, { killSessionHost })
|
|
152
|
+
: await daemonLifecycleHandlers.daemon_upgrade({ deps: ctx.deps }, args);
|
|
153
|
+
const restarting = (result as any)?.restarting === true;
|
|
154
|
+
return {
|
|
155
|
+
...result,
|
|
156
|
+
mode,
|
|
157
|
+
restarted: restarting,
|
|
158
|
+
...(restarting ? {
|
|
159
|
+
warnings: restartWarnings({ killSessionHost, forced: opts.forced }),
|
|
160
|
+
// MCP IPC has no automatic reconnect/retry — tell the caller when to
|
|
161
|
+
// talk to this daemon again.
|
|
162
|
+
clientHint: 'daemon restarting — retry your next call in ~10s',
|
|
163
|
+
} : {}),
|
|
164
|
+
} as CommandRouterResult;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function scheduleDeferredRestart(ctx: MedFamilyContext, args: any, meshId: string, nodeId: string): CommandRouterResult {
|
|
168
|
+
clearPendingDeferredRestart();
|
|
169
|
+
const requestedTimeout = typeof args?.timeoutMs === 'number' && Number.isFinite(args.timeoutMs)
|
|
170
|
+
? args.timeoutMs
|
|
171
|
+
: DEFERRED_RESTART_DEFAULT_TIMEOUT_MS;
|
|
172
|
+
const timeoutMs = Math.min(Math.max(requestedTimeout, DEFERRED_RESTART_POLL_MS), DEFERRED_RESTART_MAX_TIMEOUT_MS);
|
|
173
|
+
const record: PendingDeferredRestart = {
|
|
174
|
+
meshId,
|
|
175
|
+
nodeId,
|
|
176
|
+
mode: normalizeRestartMode(args?.mode),
|
|
177
|
+
killSessionHost: args?.killSessionHost === true,
|
|
178
|
+
scheduledAt: Date.now(),
|
|
179
|
+
expiresAt: Date.now() + timeoutMs,
|
|
180
|
+
timer: setInterval(() => { void deferredRestartTick(ctx); }, DEFERRED_RESTART_POLL_MS),
|
|
181
|
+
};
|
|
182
|
+
record.timer.unref?.();
|
|
183
|
+
pendingDeferredRestart = record;
|
|
184
|
+
LOG.info('MeshRestart', `Deferred restart scheduled for node ${nodeId} (mode=${record.mode}, expires in ${Math.round(timeoutMs / 60000)}min)`);
|
|
185
|
+
return {
|
|
186
|
+
success: true,
|
|
187
|
+
restarted: false,
|
|
188
|
+
scheduled: true,
|
|
189
|
+
code: 'restart_scheduled_when_idle',
|
|
190
|
+
reason: 'Restart scheduled — it will execute automatically as soon as no session on this daemon is generating / waiting_approval / starting. Running at an idle point also avoids pendingOutboundQueue loss, making this the safest restart path.',
|
|
191
|
+
deferredRestart: deferredRestartInfo(),
|
|
192
|
+
} as CommandRouterResult;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function deferredRestartTick(ctx: MedFamilyContext): Promise<void> {
|
|
196
|
+
const record = pendingDeferredRestart;
|
|
197
|
+
if (!record) return;
|
|
198
|
+
if (Date.now() >= record.expiresAt) {
|
|
199
|
+
LOG.warn('MeshRestart', `Deferred restart for node ${record.nodeId} expired without reaching an idle point; dropping the schedule`);
|
|
200
|
+
clearPendingDeferredRestart();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const blocking = collectBlockingSessions(ctx, record.meshId);
|
|
204
|
+
if (blocking.length > 0) return;
|
|
205
|
+
LOG.info('MeshRestart', `Deferred restart for node ${record.nodeId} executing — daemon is idle`);
|
|
206
|
+
clearPendingDeferredRestart();
|
|
207
|
+
try {
|
|
208
|
+
await executeRestart(ctx, {
|
|
209
|
+
meshId: record.meshId,
|
|
210
|
+
nodeId: record.nodeId,
|
|
211
|
+
mode: record.mode,
|
|
212
|
+
killSessionHost: record.killSessionHost,
|
|
213
|
+
}, { forced: false });
|
|
214
|
+
} catch (e: any) {
|
|
215
|
+
LOG.error('MeshRestart', `Deferred restart execution failed: ${e?.message || String(e)}`);
|
|
39
216
|
}
|
|
40
|
-
return false;
|
|
41
217
|
}
|
|
42
218
|
|
|
43
219
|
export const meshRestartHandlers: Record<string, MedFamilyHandler> = {
|
|
@@ -69,24 +245,51 @@ export const meshRestartHandlers: Record<string, MedFamilyHandler> = {
|
|
|
69
245
|
return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
|
|
70
246
|
}
|
|
71
247
|
|
|
248
|
+
// Deferred-schedule management on the owning daemon.
|
|
249
|
+
if (args?.cancelWhenIdle === true) {
|
|
250
|
+
const had = pendingDeferredRestart !== null;
|
|
251
|
+
clearPendingDeferredRestart();
|
|
252
|
+
return { success: true, restarted: false, cancelled: had, deferredRestart: null } as CommandRouterResult;
|
|
253
|
+
}
|
|
254
|
+
if (args?.whenIdleStatus === true) {
|
|
255
|
+
return { success: true, restarted: false, deferredRestart: deferredRestartInfo() } as CommandRouterResult;
|
|
256
|
+
}
|
|
257
|
+
|
|
72
258
|
// Idle-gate: refuse if any session on THIS daemon is mid-turn / awaiting
|
|
73
|
-
// approval / starting
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
259
|
+
// approval / starting — unless an explicit opt-in waives it.
|
|
260
|
+
const blocking = collectBlockingSessions(ctx, meshId);
|
|
261
|
+
if (blocking.length > 0) {
|
|
262
|
+
if (args?.force === true) {
|
|
263
|
+
// Bypass the gate entirely. Audit-logged: this kills in-flight
|
|
264
|
+
// turns and drops the unpersisted pendingOutboundQueue.
|
|
265
|
+
LOG.warn('MeshRestart', `force restart over ${blocking.length} blocking session(s): ${blocking.map((b) => `${b.instanceId || '?'}(${b.status})`).join(', ')} — pendingOutboundQueue will be lost`);
|
|
266
|
+
return executeRestart(ctx, args, { forced: true });
|
|
267
|
+
}
|
|
268
|
+
const foreignBlocking = blocking.filter((b) => !b.selfCoordinator);
|
|
269
|
+
if (args?.selfOnly === true && foreignBlocking.length === 0) {
|
|
270
|
+
// Only the calling mesh's own coordinator session is blocking —
|
|
271
|
+
// the structural self-deadlock case. Waive exactly those.
|
|
272
|
+
LOG.info('MeshRestart', `selfOnly restart: waiving ${blocking.length} self-coordinator session(s) for mesh ${meshId}`);
|
|
273
|
+
return executeRestart(ctx, args, { forced: false });
|
|
274
|
+
}
|
|
275
|
+
if (args?.whenIdle === true) {
|
|
276
|
+
return scheduleDeferredRestart(ctx, args, meshId, nodeId);
|
|
277
|
+
}
|
|
78
278
|
return {
|
|
79
279
|
success: false,
|
|
80
280
|
restarted: false,
|
|
81
281
|
code: 'blocking_sessions',
|
|
82
282
|
reason: 'Daemon has an active session (generating / waiting_approval / starting); restart refused to avoid interrupting in-flight work. Retry when the node is idle.',
|
|
83
|
-
|
|
283
|
+
blockingSessions: blocking,
|
|
284
|
+
options: {
|
|
285
|
+
selfOnly: 'waive only this mesh\'s own coordinator session (settings.meshCoordinatorFor === meshId)',
|
|
286
|
+
force: 'bypass the idle-gate entirely — kills in-flight turns and loses the unpersisted pendingOutboundQueue',
|
|
287
|
+
whenIdle: 'schedule the restart to run automatically once the daemon goes idle (safest)',
|
|
288
|
+
},
|
|
289
|
+
deferredRestart: deferredRestartInfo(),
|
|
290
|
+
} as CommandRouterResult;
|
|
84
291
|
}
|
|
85
292
|
|
|
86
|
-
|
|
87
|
-
// latest published version on the resolved channel, then detached-restart.
|
|
88
|
-
// Already-latest is a no-op (no restart), matching the dashboard button.
|
|
89
|
-
const result = await daemonLifecycleHandlers.daemon_upgrade({ deps: ctx.deps }, args);
|
|
90
|
-
return { ...result, restarted: (result as any)?.restarting === true };
|
|
293
|
+
return executeRestart(ctx, args, { forced: false });
|
|
91
294
|
},
|
|
92
295
|
};
|
|
@@ -39,6 +39,20 @@ export interface DaemonUpgradeHelperPayload {
|
|
|
39
39
|
restartArgv: string[];
|
|
40
40
|
cwd?: string;
|
|
41
41
|
sessionHostAppName?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Restart-only mode: wait for the parent daemon to exit, then re-spawn it
|
|
44
|
+
* without running any npm install. Used by daemon_restart (mesh
|
|
45
|
+
* restart_daemon_node mode="restart") to reset daemon state (memory leaks,
|
|
46
|
+
* zombie sessions, wedged internals) with minimal downtime.
|
|
47
|
+
*/
|
|
48
|
+
skipInstall?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Opt-in hard refresh: also stop the session-host process, which destroys
|
|
51
|
+
* EVERY hosted CLI session (no idle-gate — see SessionHostServer.stop).
|
|
52
|
+
* Mirrors what Windows already does unconditionally on upgrade (conpty.node
|
|
53
|
+
* lock). Default off: POSIX leaves the host running so sessions rebind.
|
|
54
|
+
*/
|
|
55
|
+
killSessionHost?: boolean;
|
|
42
56
|
}
|
|
43
57
|
|
|
44
58
|
export interface CurrentGlobalInstallSurface {
|
|
@@ -605,12 +619,25 @@ async function runDaemonUpgradeHelper(payload: DaemonUpgradeHelperPayload): Prom
|
|
|
605
619
|
// host running on POSIX; on the next boot `ensureSessionHostReady()` reuses
|
|
606
620
|
// the still-listening socket and `cliManager.restoreHostedSessions()` rebinds
|
|
607
621
|
// the live runtimes. Windows keeps the kill unchanged.
|
|
608
|
-
|
|
622
|
+
// killSessionHost is an explicit opt-in hard refresh (mesh restart_daemon_node
|
|
623
|
+
// kill_session_host) that forces the same teardown on any platform.
|
|
624
|
+
if (process.platform === 'win32' || payload.killSessionHost === true) {
|
|
609
625
|
await stopSessionHostProcesses(sessionHostAppName);
|
|
610
626
|
} else {
|
|
611
627
|
appendUpgradeLog('POSIX — session-host left running (survives upgrade; sessions rebind on next boot)');
|
|
612
628
|
}
|
|
613
629
|
removeDaemonPidFile();
|
|
630
|
+
|
|
631
|
+
// Restart-only mode (daemon_restart): the parent has exited and the pid file
|
|
632
|
+
// is gone — re-spawn the daemon as-is. No npm install, no prefix rotation, so
|
|
633
|
+
// there is no Windows lock hazard and downtime is just the re-spawn.
|
|
634
|
+
if (payload.skipInstall) {
|
|
635
|
+
appendUpgradeLog('Restart-only mode — package install skipped, re-spawning daemon');
|
|
636
|
+
spawnDetachedDaemonRestart(restartArgv, payload.cwd);
|
|
637
|
+
try { fs.unlinkSync(getUpgradeFailureNoticePath()); } catch { /* no previous failure notice */ }
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
614
641
|
// Scope the Windows atomic-upgrade layout to THIS daemon's instance so
|
|
615
642
|
// `adhdev-preview update` rotates only the preview prefix/pointer/tools tree
|
|
616
643
|
// and never touches the stable install (and vice-versa). Stable / no-override
|
|
@@ -758,6 +785,11 @@ async function runDaemonUpgradeHelper(payload: DaemonUpgradeHelperPayload): Prom
|
|
|
758
785
|
appendUpgradeLog('Post-install staging cleanup complete');
|
|
759
786
|
}
|
|
760
787
|
|
|
788
|
+
spawnDetachedDaemonRestart(restartArgv, payload.cwd);
|
|
789
|
+
try { fs.unlinkSync(getUpgradeFailureNoticePath()); } catch { /* no previous failure notice */ }
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function spawnDetachedDaemonRestart(restartArgv: string[], cwd?: string): void {
|
|
761
793
|
if (restartArgv.length > 0) {
|
|
762
794
|
const env = { ...process.env };
|
|
763
795
|
delete env[UPGRADE_HELPER_ENV];
|
|
@@ -766,14 +798,13 @@ async function runDaemonUpgradeHelper(payload: DaemonUpgradeHelperPayload): Prom
|
|
|
766
798
|
detached: true,
|
|
767
799
|
stdio: 'ignore',
|
|
768
800
|
windowsHide: true,
|
|
769
|
-
cwd:
|
|
801
|
+
cwd: cwd || process.cwd(),
|
|
770
802
|
env,
|
|
771
803
|
});
|
|
772
804
|
child.unref();
|
|
773
805
|
} else {
|
|
774
806
|
appendUpgradeLog('No restart argv provided; upgrade completed without restart');
|
|
775
807
|
}
|
|
776
|
-
try { fs.unlinkSync(getUpgradeFailureNoticePath()); } catch { /* no previous failure notice */ }
|
|
777
808
|
}
|
|
778
809
|
|
|
779
810
|
export async function maybeRunDaemonUpgradeHelperFromEnv(): Promise<boolean> {
|