@adhdev/daemon-core 1.0.18-rc.2 → 1.0.18-rc.20
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-adapters/cli-state-engine.d.ts +77 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +33 -0
- package/dist/cli-adapters/pty-transport.d.ts +2 -1
- package/dist/commands/cli-manager.d.ts +9 -0
- package/dist/commands/process-lifecycle.d.ts +43 -0
- package/dist/commands/upgrade-helper.d.ts +1 -0
- package/dist/commands/windows-atomic-upgrade.d.ts +17 -1
- package/dist/config/mesh-json-config.d.ts +62 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6674 -4237
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6361 -3927
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +76 -0
- package/dist/mesh/mesh-active-work.d.ts +1 -1
- package/dist/mesh/mesh-disk-retention.d.ts +105 -0
- package/dist/mesh/mesh-ledger.d.ts +28 -1
- package/dist/mesh/mesh-node-identity.d.ts +23 -0
- package/dist/mesh/mesh-refine-gates.d.ts +89 -0
- package/dist/mesh/mesh-remote-event-pull.d.ts +3 -0
- package/dist/mesh/mesh-runtime-store.d.ts +11 -0
- package/dist/mesh/mesh-work-queue.d.ts +24 -1
- package/dist/providers/auto-approve-modes.d.ts +14 -0
- package/dist/providers/chat-message-normalization.d.ts +22 -0
- package/dist/providers/cli-provider-instance-types.d.ts +4 -0
- package/dist/providers/cli-provider-instance.d.ts +78 -0
- package/dist/providers/contracts.d.ts +17 -0
- package/dist/providers/provider-schema.d.ts +1 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +14 -0
- package/dist/providers/types/interactive-prompt.d.ts +18 -0
- package/dist/repo-mesh-types.d.ts +38 -6
- package/dist/shared-types.d.ts +4 -2
- package/package.json +3 -3
- package/src/boot/daemon-lifecycle.ts +10 -0
- package/src/cli-adapters/cli-state-engine.ts +220 -3
- package/src/cli-adapters/provider-cli-adapter.ts +41 -11
- package/src/cli-adapters/provider-cli-shared.ts +48 -0
- package/src/cli-adapters/pty-transport.d.ts +2 -1
- package/src/cli-adapters/pty-transport.ts +1 -1
- package/src/cli-adapters/session-host-transport.ts +8 -3
- package/src/commands/cli-manager.ts +72 -8
- package/src/commands/high-family/mesh-coordinator-launch.ts +17 -2
- package/src/commands/high-family/mesh-events.ts +13 -2
- package/src/commands/med-family/mesh-crud.ts +155 -0
- package/src/commands/med-family/mesh-queue.ts +74 -1
- package/src/commands/process-lifecycle.ts +248 -0
- package/src/commands/router-refine.ts +71 -4
- package/src/commands/router.ts +8 -0
- package/src/commands/upgrade-helper.ts +106 -82
- package/src/commands/windows-atomic-upgrade.ts +281 -35
- package/src/config/mesh-json-config.ts +111 -0
- package/src/index.ts +21 -1
- package/src/mesh/coordinator-prompt.ts +258 -11
- package/src/mesh/mesh-active-work.ts +11 -1
- package/src/mesh/mesh-completion-synthesis.ts +12 -1
- package/src/mesh/mesh-disk-retention.ts +370 -0
- package/src/mesh/mesh-event-classify.ts +19 -0
- package/src/mesh/mesh-event-forwarding.ts +64 -6
- package/src/mesh/mesh-events-utils.ts +42 -0
- package/src/mesh/mesh-ledger.ts +77 -0
- package/src/mesh/mesh-node-identity.ts +49 -0
- package/src/mesh/mesh-queue-assignment.ts +210 -11
- package/src/mesh/mesh-reconcile-loop.ts +306 -3
- package/src/mesh/mesh-refine-gates.ts +300 -0
- package/src/mesh/mesh-remote-event-pull.ts +68 -47
- package/src/mesh/mesh-runtime-store.ts +21 -0
- package/src/mesh/mesh-work-queue.ts +85 -2
- package/src/providers/auto-approve-modes.ts +103 -0
- package/src/providers/chat-message-normalization.ts +53 -0
- package/src/providers/cli-provider-instance-types.ts +53 -0
- package/src/providers/cli-provider-instance.ts +497 -15
- package/src/providers/contracts.ts +25 -1
- package/src/providers/provider-schema.ts +83 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +23 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +20 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +44 -0
- package/src/providers/types/interactive-prompt.ts +77 -0
- package/src/repo-mesh-types.ts +112 -12
- package/src/session-host/managed-host.ts +34 -0
- package/src/shared-types.ts +9 -1
- package/src/status/reporter.ts +1 -0
- package/src/status/snapshot.ts +2 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { execFileSync, type ExecFileSyncOptions } from 'child_process';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface ProcessLifecycleOptions {
|
|
5
|
+
platform?: NodeJS.Platform;
|
|
6
|
+
execFileSync?: typeof import('child_process').execFileSync;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface OwnedProcessInfo {
|
|
10
|
+
pid: number;
|
|
11
|
+
commandLine: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function defaultExecFileSync(): typeof import('child_process').execFileSync {
|
|
15
|
+
return execFileSync;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getWindowsProcessCommandLine(
|
|
19
|
+
pid: number,
|
|
20
|
+
exec: typeof import('child_process').execFileSync,
|
|
21
|
+
): string | null {
|
|
22
|
+
const pidFilter = `ProcessId=${pid}`;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const psOut = exec('powershell.exe', [
|
|
26
|
+
'-NoProfile',
|
|
27
|
+
'-NonInteractive',
|
|
28
|
+
'-ExecutionPolicy', 'Bypass',
|
|
29
|
+
'-Command',
|
|
30
|
+
`(Get-CimInstance Win32_Process -Filter "${pidFilter}").CommandLine`,
|
|
31
|
+
], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
32
|
+
const text = String(psOut).trim();
|
|
33
|
+
if (text) return text;
|
|
34
|
+
} catch {
|
|
35
|
+
// fall through to wmic fallback
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const wmicOut = exec('wmic', [
|
|
40
|
+
'process', 'where', pidFilter, 'get', 'CommandLine',
|
|
41
|
+
], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
42
|
+
const text = String(wmicOut).trim();
|
|
43
|
+
if (text) return text;
|
|
44
|
+
} catch {
|
|
45
|
+
// noop
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getProcessCommandLine(
|
|
52
|
+
pid: number,
|
|
53
|
+
options: ProcessLifecycleOptions = {},
|
|
54
|
+
): string | null {
|
|
55
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
56
|
+
const exec = options.execFileSync ?? defaultExecFileSync();
|
|
57
|
+
const platform = options.platform ?? process.platform;
|
|
58
|
+
|
|
59
|
+
if (platform === 'win32') {
|
|
60
|
+
return getWindowsProcessCommandLine(pid, exec);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const text = String(exec('ps', ['-o', 'command=', '-p', String(pid)], {
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
timeout: 3000,
|
|
67
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
68
|
+
})).trim();
|
|
69
|
+
return text || null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extract the script argument (the token after the node executable) from a
|
|
77
|
+
* command line. Handles both quoted and unquoted executables/scripts.
|
|
78
|
+
*/
|
|
79
|
+
export function parseNodeScriptPath(commandLine: string | null): string | null {
|
|
80
|
+
if (!commandLine) return null;
|
|
81
|
+
let rest = commandLine.trim();
|
|
82
|
+
|
|
83
|
+
// Skip the leading executable token.
|
|
84
|
+
if (rest.startsWith('"')) {
|
|
85
|
+
const end = rest.indexOf('"', 1);
|
|
86
|
+
if (end === -1) return null;
|
|
87
|
+
rest = rest.slice(end + 1).trim();
|
|
88
|
+
} else {
|
|
89
|
+
const idx = rest.search(/\s/);
|
|
90
|
+
if (idx === -1) return null;
|
|
91
|
+
rest = rest.slice(idx + 1).trim();
|
|
92
|
+
}
|
|
93
|
+
if (!rest) return null;
|
|
94
|
+
|
|
95
|
+
if (rest.startsWith('"')) {
|
|
96
|
+
const end = rest.indexOf('"', 1);
|
|
97
|
+
return end === -1 ? rest.slice(1) : rest.slice(1, end);
|
|
98
|
+
}
|
|
99
|
+
const idx = rest.search(/\s/);
|
|
100
|
+
return idx === -1 ? rest : rest.slice(0, idx);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeWindowsPath(value: string): string {
|
|
104
|
+
return value.toLowerCase().replace(/\//g, '\\').replace(/\\+$/, '');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isCommandLineUnderPrefix(commandLine: string | null, prefix: string): boolean {
|
|
108
|
+
if (!commandLine) return false;
|
|
109
|
+
const needle = normalizeWindowsPath(prefix);
|
|
110
|
+
// Command-line paths may quote Windows separators as either single or
|
|
111
|
+
// doubled backslashes; collapse doubles before matching.
|
|
112
|
+
const haystack = commandLine.split('\\\\').join('\\').toLowerCase();
|
|
113
|
+
return haystack.includes(needle);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function killProcess(pid: number, options: ProcessLifecycleOptions = {}): boolean {
|
|
117
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
118
|
+
const exec = options.execFileSync ?? defaultExecFileSync();
|
|
119
|
+
const platform = options.platform ?? process.platform;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
if (platform === 'win32') {
|
|
123
|
+
exec('taskkill', ['/PID', String(pid), '/T', '/F'], {
|
|
124
|
+
stdio: 'ignore',
|
|
125
|
+
windowsHide: true,
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
process.kill(pid, 'SIGTERM');
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function waitForPidExit(pid: number, timeoutMs: number): Promise<boolean> {
|
|
137
|
+
const start = Date.now();
|
|
138
|
+
while (Date.now() - start < timeoutMs) {
|
|
139
|
+
try {
|
|
140
|
+
process.kill(pid, 0);
|
|
141
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
142
|
+
} catch {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* List Node processes whose command line places them under any of the supplied
|
|
151
|
+
* prefixes. Windows-only — the versioned-prefix lifecycle this supports does not
|
|
152
|
+
* exist on POSIX, so the helper is a no-op there.
|
|
153
|
+
*/
|
|
154
|
+
export function listOwnedNodeProcesses(options: {
|
|
155
|
+
prefixes: readonly string[];
|
|
156
|
+
excludePids?: readonly number[];
|
|
157
|
+
markers?: readonly string[];
|
|
158
|
+
} & ProcessLifecycleOptions): OwnedProcessInfo[] {
|
|
159
|
+
const platform = options.platform ?? process.platform;
|
|
160
|
+
if (platform !== 'win32') return [];
|
|
161
|
+
|
|
162
|
+
const exec = options.execFileSync ?? defaultExecFileSync();
|
|
163
|
+
const prefixes = options.prefixes.map((p) => normalizeWindowsPath(p));
|
|
164
|
+
const exclude = new Set((options.excludePids ?? []).filter((n) => Number.isFinite(n) && n > 0));
|
|
165
|
+
// Command-line paths use Windows separators; normalize marker separators the
|
|
166
|
+
// same way so a marker like "dist/cli/index.js" matches "dist\cli\index.js".
|
|
167
|
+
const markers = (options.markers ?? []).map((m) => m.toLowerCase().replace(/\//g, '\\'));
|
|
168
|
+
|
|
169
|
+
let pids: number[] = [];
|
|
170
|
+
try {
|
|
171
|
+
const out = String(exec('powershell.exe', [
|
|
172
|
+
'-NoProfile',
|
|
173
|
+
'-NonInteractive',
|
|
174
|
+
'-ExecutionPolicy', 'Bypass',
|
|
175
|
+
'-Command',
|
|
176
|
+
'Get-Process node -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id | ConvertTo-Json -Compress',
|
|
177
|
+
], { encoding: 'utf8', timeout: 8000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true })).trim();
|
|
178
|
+
if (out) {
|
|
179
|
+
const parsed = JSON.parse(out);
|
|
180
|
+
pids = Array.isArray(parsed) ? parsed : [parsed];
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const results: OwnedProcessInfo[] = [];
|
|
187
|
+
for (const pid of pids) {
|
|
188
|
+
if (!Number.isFinite(pid) || pid <= 0 || exclude.has(pid)) continue;
|
|
189
|
+
const commandLine = getProcessCommandLine(pid, { platform, execFileSync: exec });
|
|
190
|
+
if (!commandLine) continue;
|
|
191
|
+
const lower = commandLine.toLowerCase();
|
|
192
|
+
const underPrefix = prefixes.some((prefix) => lower.includes(prefix));
|
|
193
|
+
if (!underPrefix) continue;
|
|
194
|
+
if (markers.length > 0 && !markers.some((marker) => lower.includes(marker))) continue;
|
|
195
|
+
results.push({ pid, commandLine });
|
|
196
|
+
}
|
|
197
|
+
return results;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function stopOwnedProcesses(options: {
|
|
201
|
+
processes: readonly OwnedProcessInfo[];
|
|
202
|
+
waitMs?: number;
|
|
203
|
+
} & ProcessLifecycleOptions): Promise<{ stopped: number; survivors: OwnedProcessInfo[] }> {
|
|
204
|
+
const waitMs = options.waitMs ?? 15_000;
|
|
205
|
+
const killed = new Set<number>();
|
|
206
|
+
|
|
207
|
+
for (const p of options.processes) {
|
|
208
|
+
if (killProcess(p.pid, options)) killed.add(p.pid);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const survivors: OwnedProcessInfo[] = [];
|
|
212
|
+
for (const p of options.processes) {
|
|
213
|
+
if (!killed.has(p.pid)) {
|
|
214
|
+
survivors.push(p);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const exited = await waitForPidExit(p.pid, waitMs);
|
|
218
|
+
if (!exited) survivors.push(p);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// A process counts as stopped only when it left the process table; a failed
|
|
222
|
+
// kill or a post-kill timeout both land it in `survivors`.
|
|
223
|
+
return { stopped: options.processes.length - survivors.length, survivors };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function stopOwnedProcessesForPrefixes(options: {
|
|
227
|
+
prefixes: readonly string[];
|
|
228
|
+
excludePids?: readonly number[];
|
|
229
|
+
markers?: readonly string[];
|
|
230
|
+
waitMs?: number;
|
|
231
|
+
log?: (message: string) => void;
|
|
232
|
+
} & ProcessLifecycleOptions): Promise<{ stopped: number; survivors: OwnedProcessInfo[] }> {
|
|
233
|
+
const processes = listOwnedNodeProcesses(options);
|
|
234
|
+
if (processes.length === 0) return { stopped: 0, survivors: [] };
|
|
235
|
+
|
|
236
|
+
options.log?.(`Stopping ${processes.length} owned process(es) under prefixes: ${options.prefixes.join(', ')}`);
|
|
237
|
+
const result = await stopOwnedProcesses({
|
|
238
|
+
processes,
|
|
239
|
+
waitMs: options.waitMs,
|
|
240
|
+
platform: options.platform,
|
|
241
|
+
execFileSync: options.execFileSync,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (result.survivors.length > 0) {
|
|
245
|
+
options.log?.(`Could not stop ${result.survivors.length} owned process(es): ${result.survivors.map((s) => s.pid).join(', ')}`);
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
@@ -36,8 +36,10 @@ import {
|
|
|
36
36
|
RefineExecFileAsync,
|
|
37
37
|
RefineStageOutcome,
|
|
38
38
|
classifyPatchEquivalenceFailure,
|
|
39
|
+
convergeDivergedSubmoduleGitlinks,
|
|
39
40
|
recordMeshRefineStage,
|
|
40
41
|
resolveRefineryAutoPublishSubmoduleMainCommits,
|
|
42
|
+
rootRebaseResolvingGitlinks,
|
|
41
43
|
runMeshRefineEffectiveDiffGate,
|
|
42
44
|
runMeshRefinePatchEquivalenceGate,
|
|
43
45
|
runMeshRefineSubmoduleReachabilityGate,
|
|
@@ -539,6 +541,9 @@ async function computeBranchBaseDivergence(
|
|
|
539
541
|
export async function refineSyncBaseStage(self: DaemonCommandRouter, ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
540
542
|
const { repoRoot, baseHead, node, branch, baseBranch, refineStages, execFileAsync } = ctx;
|
|
541
543
|
let branchHead = ctx.branchHead;
|
|
544
|
+
// Converged submodule gitlink resolutions (path → rebased commit) from STEP 1;
|
|
545
|
+
// consumed by the gitlink-aware root rebase (STEP 2) to resolve gitlink conflicts.
|
|
546
|
+
let gitlinkResolutions: Array<{ path: string; rebasedCommit: string }> = [];
|
|
542
547
|
const syncStarted = Date.now();
|
|
543
548
|
const divergence = await computeBranchBaseDivergence(execFileAsync, node.workspace, baseHead, branchHead);
|
|
544
549
|
|
|
@@ -571,25 +576,87 @@ export async function refineSyncBaseStage(self: DaemonCommandRouter, ctx: Refine
|
|
|
571
576
|
const preRebasePe = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
572
577
|
const alreadyMerged = !preRebasePe.actualPatchId && !!preRebasePe.expectedPatchId;
|
|
573
578
|
const submoduleConflict = preRebasePe.actionableHint?.kind === 'submodule_conflict';
|
|
574
|
-
if (alreadyMerged
|
|
579
|
+
if (alreadyMerged) {
|
|
575
580
|
recordMeshRefineStage(refineStages, 'sync_base', 'passed', syncStarted, {
|
|
576
581
|
ahead: divergence.ahead,
|
|
577
582
|
behind: divergence.behind,
|
|
578
583
|
rebased: false,
|
|
579
|
-
reason:
|
|
584
|
+
reason: 'already_merged_via_other_path_skip_rebase',
|
|
580
585
|
});
|
|
581
586
|
return { kind: 'continue', ctx };
|
|
582
587
|
}
|
|
588
|
+
if (submoduleConflict) {
|
|
589
|
+
// DS3: a "submodule conflict" here means base and branch advanced the
|
|
590
|
+
// SAME submodule to DIVERGED sibling commits (neither an ancestor of the
|
|
591
|
+
// other), so the gitlink stays in the diff and patch-equivalence fails.
|
|
592
|
+
// Attempt to auto-converge it (STEP 1): rebase the branch-side submodule
|
|
593
|
+
// commit onto the base-side commit INSIDE the worktree submodule, so the
|
|
594
|
+
// base-side commit becomes a strict ancestor of the rebased tip. The root
|
|
595
|
+
// rebase below (STEP 2) then resolves the gitlink conflict to that rebased
|
|
596
|
+
// commit. Together this automates the documented manual strict-ff bypass
|
|
597
|
+
// and keeps the landed oss history linear. On any real submodule content
|
|
598
|
+
// conflict it backs out cleanly → we FALL BACK to the historical
|
|
599
|
+
// defer→patch_equivalence path below.
|
|
600
|
+
const converge = convergeDivergedSubmoduleGitlinks(node.workspace, repoRoot, baseHead, branchHead);
|
|
601
|
+
if (converge.converged) {
|
|
602
|
+
gitlinkResolutions = converge.resolutions;
|
|
603
|
+
recordMeshRefineStage(refineStages, 'submodule_gitlink_converge', 'passed', syncStarted, {
|
|
604
|
+
reason: 'submodule_diverged_auto_rebased',
|
|
605
|
+
gitlinks: converge.gitlinks,
|
|
606
|
+
});
|
|
607
|
+
LOG.info('Mesh', `[Refinery] Auto-converged diverged submodule gitlink(s) onto base for node ${node.id}: `
|
|
608
|
+
+ converge.resolutions.map(r => `${r.path}→${r.rebasedCommit.slice(0, 12)}`).join(', '));
|
|
609
|
+
// Fall through (do NOT return) → the gitlink-aware root rebase below runs.
|
|
610
|
+
} else {
|
|
611
|
+
// Fail-safe: convergence declined (conflict / unreachable / not a real
|
|
612
|
+
// divergence) → preserve the historical defer→blocked_review behavior.
|
|
613
|
+
recordMeshRefineStage(refineStages, 'submodule_gitlink_converge', 'skipped', syncStarted, {
|
|
614
|
+
reason: 'submodule_conflict_defer_to_patch_equivalence',
|
|
615
|
+
convergeReason: converge.reason,
|
|
616
|
+
gitlinks: converge.gitlinks,
|
|
617
|
+
});
|
|
618
|
+
recordMeshRefineStage(refineStages, 'sync_base', 'passed', syncStarted, {
|
|
619
|
+
ahead: divergence.ahead,
|
|
620
|
+
behind: divergence.behind,
|
|
621
|
+
rebased: false,
|
|
622
|
+
reason: 'submodule_conflict_defer_to_patch_equivalence',
|
|
623
|
+
});
|
|
624
|
+
return { kind: 'continue', ctx };
|
|
625
|
+
}
|
|
626
|
+
}
|
|
583
627
|
} catch { /* fail-open: on gate error, fall through to the rebase */ }
|
|
584
628
|
|
|
585
629
|
// behind>0: strictly-behind OR diverged. Rebase the branch onto the pinned
|
|
586
630
|
// baseHead. A conflict aborts and terminates blocked_review (retryable=false —
|
|
587
631
|
// a real content conflict needs human resolution, not a base-movement retry).
|
|
632
|
+
//
|
|
633
|
+
// When STEP 1 converged a diverged submodule gitlink, use the gitlink-aware
|
|
634
|
+
// root rebase (STEP 2): git's recursive merge refuses to auto-merge the still-
|
|
635
|
+
// diverged intermediate gitlink, so we drive the rebase and resolve each
|
|
636
|
+
// submodule-gitlink conflict to the converged commit. A non-gitlink conflict
|
|
637
|
+
// aborts and falls through to the same blocked_review handling as a plain
|
|
638
|
+
// rebase conflict below (via the thrown gitlinkRebaseError).
|
|
588
639
|
const rebaseStarted = Date.now();
|
|
589
640
|
try {
|
|
590
|
-
|
|
641
|
+
if (gitlinkResolutions.length > 0) {
|
|
642
|
+
const gitlinkRebase = rootRebaseResolvingGitlinks(node.workspace, baseHead, gitlinkResolutions);
|
|
643
|
+
if (!gitlinkRebase.ok) {
|
|
644
|
+
// Surface as a rebase failure so the shared blocked_review handling
|
|
645
|
+
// (submodule-hint recovery included) runs — nothing was left mid-rebase
|
|
646
|
+
// (rootRebaseResolvingGitlinks aborts on failure).
|
|
647
|
+
const err: any = new Error(`gitlink-aware rebase aborted: ${gitlinkRebase.reason || 'unknown'}`);
|
|
648
|
+
err.gitlinkRebaseReason = gitlinkRebase.reason;
|
|
649
|
+
err.gitlinkRebaseConflicts = gitlinkRebase.conflictPaths;
|
|
650
|
+
err.alreadyAborted = true;
|
|
651
|
+
throw err;
|
|
652
|
+
}
|
|
653
|
+
} else {
|
|
654
|
+
execFileSync('git', ['rebase', baseHead], { cwd: node.workspace, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
655
|
+
}
|
|
591
656
|
} catch (rebaseErr: any) {
|
|
592
|
-
|
|
657
|
+
if (!rebaseErr?.alreadyAborted) {
|
|
658
|
+
try { execFileSync('git', ['rebase', '--abort'], { cwd: node.workspace, stdio: 'ignore' }); } catch { /* ignore */ }
|
|
659
|
+
}
|
|
593
660
|
// A rebase conflict on a submodule/gitlink divergence is a SPECIAL case the
|
|
594
661
|
// patch-equivalence gate describes with a rich actionable hint (which
|
|
595
662
|
// submodule, base vs branch commit, how to resolve). Run that gate against
|
package/src/commands/router.ts
CHANGED
|
@@ -227,6 +227,14 @@ const MESH_FORWARDABLE_SESSION_COMMANDS = new Set([
|
|
|
227
227
|
// MUTATES the worker PTY, so forwarding to the real owner (not a wrong local session) is
|
|
228
228
|
// doubly important. The daemon re-enforces the destructive-key confirm gate after the forward.
|
|
229
229
|
'send_keys',
|
|
230
|
+
// interactive_prompt_response (mesh_answer_question, mission f1d25e11): the coordinator
|
|
231
|
+
// answers a REMOTE worker's AskUserQuestion (waiting_choice). The answer must reach the
|
|
232
|
+
// OWNING worker session's live instance — its activeInteractivePrompt (the authoritative
|
|
233
|
+
// prompt the labels/indexes resolve against) and its adapter.setInteractivePromptResponse
|
|
234
|
+
// live only there. Without forwarding, the coordinator's local high-family handler returns
|
|
235
|
+
// 'No running instance for session …' and the question is never answered — the exact
|
|
236
|
+
// remote-worker forwarding gap of mission 6938892f, now closed for questions too.
|
|
237
|
+
'interactive_prompt_response',
|
|
230
238
|
]);
|
|
231
239
|
|
|
232
240
|
function normalizeCommandSource(source: string): CommandLogEntry['source'] {
|
|
@@ -4,11 +4,18 @@ import * as fs from 'fs';
|
|
|
4
4
|
import * as os from 'os';
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import {
|
|
7
|
+
ADHDEV_OWNED_MARKERS,
|
|
7
8
|
createDefaultWindowsAtomicHooks,
|
|
8
9
|
findPortableNode22,
|
|
9
10
|
performWindowsAtomicUpgrade,
|
|
10
11
|
resolveWindowsInstallerLayout,
|
|
11
12
|
} from './windows-atomic-upgrade.js';
|
|
13
|
+
import {
|
|
14
|
+
getProcessCommandLine,
|
|
15
|
+
killProcess,
|
|
16
|
+
stopOwnedProcessesForPrefixes,
|
|
17
|
+
waitForPidExit,
|
|
18
|
+
} from './process-lifecycle.js';
|
|
12
19
|
|
|
13
20
|
const UPGRADE_HELPER_ENV = 'ADHDEV_DAEMON_UPGRADE_HELPER';
|
|
14
21
|
|
|
@@ -137,19 +144,68 @@ function resolveInstallPrefixFromPackageRoot(packageRoot: string, packageName: s
|
|
|
137
144
|
return maybeLibDir;
|
|
138
145
|
}
|
|
139
146
|
|
|
147
|
+
// True when `prefix` is the bin dir of a portable Node 22 the installer manages
|
|
148
|
+
// under ~/.adhdev/tools/node22/<node-vX>/. A `npm i -g adhdev` run while that
|
|
149
|
+
// portable node is the active `node` installs adhdev into node's own default
|
|
150
|
+
// global prefix (= that dir) — a "legacy node22-prefix" install that lives at
|
|
151
|
+
// the FRONT of PATH (Enable-NodePath prepends node22) and shadows the canonical
|
|
152
|
+
// dispatcher shims in ~/.adhdev/npm-global. Because self-upgrade reuses the
|
|
153
|
+
// running prefix, that install then re-installs into the same node22 dir forever
|
|
154
|
+
// and never converts to the dispatcher. Detecting it lets us force convergence.
|
|
155
|
+
function isPortableNode22Prefix(prefix: string | null, homeDir: string): boolean {
|
|
156
|
+
if (!prefix) return false;
|
|
157
|
+
const portableRoot = path.join(homeDir, '.adhdev', 'tools', 'node22');
|
|
158
|
+
const normalizedPrefix = path.resolve(prefix).replace(/[\\/]+$/, '').toLowerCase();
|
|
159
|
+
const normalizedRoot = path.resolve(portableRoot).replace(/[\\/]+$/, '').toLowerCase();
|
|
160
|
+
return normalizedPrefix === normalizedRoot || normalizedPrefix.startsWith(`${normalizedRoot}${path.sep.toLowerCase()}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Redirect a legacy node22-prefix install to the canonical dispatcher install
|
|
164
|
+
// root so resolveWindowsInstallerLayout accepts it and the atomic-upgrade
|
|
165
|
+
// publishes the ~/.adhdev/npm-global pointer + shims. Prefer the version the
|
|
166
|
+
// dispatcher pointer already names (so we sit on the real active dispatcher
|
|
167
|
+
// prefix); otherwise synthesize a stable migration sentinel under npm-installs.
|
|
168
|
+
// resolveWindowsInstallerLayout only requires a `npm-installs/version-*` path —
|
|
169
|
+
// performWindowsAtomicUpgrade stages a fresh version- prefix of its own and uses
|
|
170
|
+
// this only as the "old prefix" to stop/clean, so a non-existent path is a no-op.
|
|
171
|
+
function canonicalDispatcherInstallPrefix(homeDir: string): string {
|
|
172
|
+
const installRoot = path.join(homeDir, '.adhdev', 'npm-installs');
|
|
173
|
+
const pointerPath = path.join(homeDir, '.adhdev', 'npm-global', '.adhdev-current');
|
|
174
|
+
try {
|
|
175
|
+
const activeVersion = fs.readFileSync(pointerPath, 'utf8').trim();
|
|
176
|
+
if (activeVersion.startsWith('version-')) return path.join(installRoot, activeVersion);
|
|
177
|
+
} catch {
|
|
178
|
+
// No dispatcher pointer yet (first migration off the legacy layout).
|
|
179
|
+
}
|
|
180
|
+
return path.join(installRoot, 'version-legacy-migrate');
|
|
181
|
+
}
|
|
182
|
+
|
|
140
183
|
export function resolveCurrentGlobalInstallSurface(options: {
|
|
141
184
|
packageName: string;
|
|
142
185
|
currentCliPath?: string;
|
|
143
186
|
nodeExecutable?: string;
|
|
144
187
|
platform?: NodeJS.Platform;
|
|
188
|
+
homeDir?: string;
|
|
145
189
|
}): CurrentGlobalInstallSurface {
|
|
146
190
|
const packageRoot = findCurrentPackageRoot(options.currentCliPath || process.argv[1], options.packageName);
|
|
147
191
|
const npmInvocation = resolveSiblingNpmInvocation(options.nodeExecutable || process.execPath, options.platform);
|
|
192
|
+
const platform = options.platform || process.platform;
|
|
193
|
+
const homeDir = options.homeDir || os.homedir();
|
|
194
|
+
let installPrefix = packageRoot ? resolveInstallPrefixFromPackageRoot(packageRoot, options.packageName) : null;
|
|
195
|
+
// FIX C: on Windows, never let a self-upgrade perpetuate the legacy
|
|
196
|
+
// node22-prefix install. If the running adhdev lives under ~/.adhdev/tools/
|
|
197
|
+
// node22, force the install onto the canonical dispatcher prefix so the update
|
|
198
|
+
// converges to the ~/.adhdev/npm-global pointer + shims. Scoped to win32 AND a
|
|
199
|
+
// tools/node22 prefix so npm-linked dev / standalone / real dispatcher installs
|
|
200
|
+
// are untouched.
|
|
201
|
+
if (platform === 'win32' && isPortableNode22Prefix(installPrefix, homeDir)) {
|
|
202
|
+
installPrefix = canonicalDispatcherInstallPrefix(homeDir);
|
|
203
|
+
}
|
|
148
204
|
return {
|
|
149
205
|
npmExecutable: npmInvocation.executable,
|
|
150
206
|
npmArgsPrefix: npmInvocation.argsPrefix,
|
|
151
207
|
packageRoot,
|
|
152
|
-
installPrefix
|
|
208
|
+
installPrefix,
|
|
153
209
|
execOptions: npmInvocation.execOptions,
|
|
154
210
|
};
|
|
155
211
|
}
|
|
@@ -235,77 +291,11 @@ export function execNpmCommandSync(
|
|
|
235
291
|
);
|
|
236
292
|
}
|
|
237
293
|
|
|
238
|
-
function killPid(pid: number): boolean {
|
|
239
|
-
try {
|
|
240
|
-
if (process.platform === 'win32') {
|
|
241
|
-
execFileSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
|
|
242
|
-
} else {
|
|
243
|
-
process.kill(pid, 'SIGTERM');
|
|
244
|
-
}
|
|
245
|
-
return true;
|
|
246
|
-
} catch {
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function getWindowsProcessCommandLine(pid: number): string | null {
|
|
252
|
-
const pidFilter = `ProcessId=${pid}`;
|
|
253
|
-
try {
|
|
254
|
-
const psOut = execFileSync('powershell.exe', [
|
|
255
|
-
'-NoProfile',
|
|
256
|
-
'-NonInteractive',
|
|
257
|
-
'-ExecutionPolicy', 'Bypass',
|
|
258
|
-
'-Command',
|
|
259
|
-
`(Get-CimInstance Win32_Process -Filter "${pidFilter}").CommandLine`,
|
|
260
|
-
], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }).trim();
|
|
261
|
-
if (psOut) return psOut;
|
|
262
|
-
} catch {
|
|
263
|
-
// fall through to wmic fallback
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
const wmicOut = execFileSync('wmic', [
|
|
268
|
-
'process', 'where', pidFilter, 'get', 'CommandLine',
|
|
269
|
-
], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }).trim();
|
|
270
|
-
if (wmicOut) return wmicOut;
|
|
271
|
-
} catch {
|
|
272
|
-
// noop
|
|
273
|
-
}
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function getProcessCommandLine(pid: number): string | null {
|
|
278
|
-
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
279
|
-
if (process.platform === 'win32') return getWindowsProcessCommandLine(pid);
|
|
280
|
-
try {
|
|
281
|
-
const text = execFileSync('ps', ['-o', 'command=', '-p', String(pid)], {
|
|
282
|
-
encoding: 'utf8',
|
|
283
|
-
timeout: 3000,
|
|
284
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
285
|
-
}).trim();
|
|
286
|
-
return text || null;
|
|
287
|
-
} catch {
|
|
288
|
-
return null;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
294
|
function isManagedSessionHostPid(pid: number): boolean {
|
|
293
295
|
const commandLine = getProcessCommandLine(pid);
|
|
294
296
|
return !!commandLine && /session-host-daemon/i.test(commandLine);
|
|
295
297
|
}
|
|
296
298
|
|
|
297
|
-
async function waitForPidExit(pid: number, timeoutMs: number): Promise<void> {
|
|
298
|
-
const start = Date.now();
|
|
299
|
-
while (Date.now() - start < timeoutMs) {
|
|
300
|
-
try {
|
|
301
|
-
process.kill(pid, 0);
|
|
302
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
303
|
-
} catch {
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
299
|
export async function stopSessionHostProcesses(appName: string): Promise<void> {
|
|
310
300
|
const pidFile = path.join(os.homedir(), '.adhdev', `${appName}-session-host.pid`);
|
|
311
301
|
let killedPid: number | null = null;
|
|
@@ -313,7 +303,7 @@ export async function stopSessionHostProcesses(appName: string): Promise<void> {
|
|
|
313
303
|
if (fs.existsSync(pidFile)) {
|
|
314
304
|
const pid = Number.parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
315
305
|
if (Number.isFinite(pid) && pid !== process.pid && isManagedSessionHostPid(pid)) {
|
|
316
|
-
if (
|
|
306
|
+
if (killProcess(pid)) killedPid = pid;
|
|
317
307
|
}
|
|
318
308
|
}
|
|
319
309
|
} catch {
|
|
@@ -430,7 +420,7 @@ export async function stopForeignNativeAddonHolders(
|
|
|
430
420
|
appendUpgradeLog(
|
|
431
421
|
`Foreign native-addon holder found: pid ${holder.pid}${holder.commandLine ? ` — ${holder.commandLine}` : ''}`,
|
|
432
422
|
);
|
|
433
|
-
const killed =
|
|
423
|
+
const killed = killProcess(holder.pid);
|
|
434
424
|
if (killed) {
|
|
435
425
|
await waitForPidExit(holder.pid, 15000);
|
|
436
426
|
appendUpgradeLog(`Terminated foreign native-addon holder pid ${holder.pid}`);
|
|
@@ -600,21 +590,55 @@ async function runDaemonUpgradeHelper(payload: DaemonUpgradeHelperPayload): Prom
|
|
|
600
590
|
throw new Error(`portable Node.js 22 npm CLI is missing: ${npmCliPath}`);
|
|
601
591
|
}
|
|
602
592
|
appendUpgradeLog(`Installer-managed pointer layout detected; active prefix will remain untouched: ${windowsInstallerLayout.activePrefix}`);
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
593
|
+
|
|
594
|
+
// Terminate any ADHDev-owned process still executing from the current active
|
|
595
|
+
// prefix or the legacy stable shim tree before activation. The parent daemon
|
|
596
|
+
// and this helper itself are excluded: the parent is already exiting, and the
|
|
597
|
+
// helper must survive to complete the upgrade.
|
|
598
|
+
const upgradePids = [process.pid, payload.parentPid].filter((n): n is number => Number.isFinite(n) && n > 0);
|
|
599
|
+
const preStop = await stopOwnedProcessesForPrefixes({
|
|
600
|
+
prefixes: [windowsInstallerLayout.activePrefix, windowsInstallerLayout.stablePrefix],
|
|
601
|
+
excludePids: upgradePids,
|
|
602
|
+
markers: Array.from(ADHDEV_OWNED_MARKERS),
|
|
603
|
+
waitMs: 15_000,
|
|
604
|
+
log: appendUpgradeLog,
|
|
605
|
+
});
|
|
606
|
+
if (preStop.survivors.length > 0) {
|
|
607
|
+
throw new Error(
|
|
608
|
+
`Cannot upgrade: owned processes still running under current prefix: ${preStop.survivors.map((s) => s.pid).join(', ')}`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
await performWindowsAtomicUpgrade({
|
|
614
|
+
layout: windowsInstallerLayout,
|
|
609
615
|
packageName: payload.packageName,
|
|
610
616
|
targetVersion: payload.targetVersion,
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
617
|
+
portableNode,
|
|
618
|
+
excludePids: upgradePids,
|
|
619
|
+
hooks: createDefaultWindowsAtomicHooks({
|
|
620
|
+
packageName: payload.packageName,
|
|
621
|
+
targetVersion: payload.targetVersion,
|
|
622
|
+
npmCliPath,
|
|
623
|
+
restartArgv,
|
|
624
|
+
cwd: payload.cwd || process.cwd(),
|
|
625
|
+
env: Object.fromEntries(Object.entries(process.env).filter(([key]) => key !== UPGRADE_HELPER_ENV)),
|
|
626
|
+
log: appendUpgradeLog,
|
|
627
|
+
}),
|
|
628
|
+
});
|
|
629
|
+
} catch (error: any) {
|
|
630
|
+
// performWindowsAtomicUpgrade already rolled the pointer/shims back to the
|
|
631
|
+
// prior version before rethrowing. Without a durable notice that rollback
|
|
632
|
+
// was silent — the daemon simply kept running the old version (the rc.6
|
|
633
|
+
// stuck-upgrade defect). Leave an actionable last-error file naming the
|
|
634
|
+
// target that failed its health/version gate.
|
|
635
|
+
emitUpgradeFailureNotice([
|
|
636
|
+
`adhdev ${payload.packageName}@${payload.targetVersion} upgrade failed and was rolled back: ${error?.message || String(error)}`,
|
|
637
|
+
`Previous version preserved (active prefix: ${windowsInstallerLayout.activePrefix}).`,
|
|
638
|
+
'See daemon-upgrade.log for the full install/health trace. The next daemon start will retry.',
|
|
639
|
+
]);
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
618
642
|
try { fs.unlinkSync(getUpgradeFailureNoticePath()); } catch { /* no previous failure notice */ }
|
|
619
643
|
appendUpgradeLog('Installer-managed Windows atomic upgrade completed');
|
|
620
644
|
return;
|