@remcp/remcp 0.2.221 → 0.2.223

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/remcp",
3
- "version": "0.2.221",
3
+ "version": "0.2.223",
4
4
  "description": "ReMCP device client: pair a computer with ReMCP and run the outbound-only agent that hosts the local MCP runtime.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,7 +18,7 @@
18
18
  "README.md"
19
19
  ],
20
20
  "scripts": {
21
- "check": "node --check bin/remcp.mjs && node --check src/agent-deploy-reconnect.mjs && node --check src/agent-network-probe-client.mjs && node --check src/agent-relay-connection.mjs && node --check src/agent-responder.mjs && node --check src/agent-runtime-capabilities.mjs && node --check src/agent-update-monitor.mjs && node --check src/agent-update.mjs && node --check src/agent-utilities.mjs && node --check src/agent.mjs && node --check src/cli.mjs && node --check src/cli/config.mjs && node --check src/cli/connect.mjs && node --check src/cli/doctor.mjs && node --check src/cli/env.mjs && node --check src/cli/installation-audit.mjs && node --check src/cli/mac-native-temperature.mjs && node --check src/cli/npm-global.mjs && node --check src/cli/pairing-qr.mjs && node --check src/cli/service-inventory.mjs && node --check src/cli/service-macos.mjs && node --check src/cli/service-paths.mjs && node --check src/cli/service.mjs && node --check src/cli/shell.mjs && node --check src/cli/update.mjs && node --check src/cli/windows-supervisor.mjs && node --check src/cli/windows-task-restart.mjs && node --check src/cli/windows-update-lock.mjs && node --check src/cli/windows-user-supervisor.mjs && node --check src/fetch-update-manifest.mjs && node --check src/fs-access.mjs && node --check src/npm.mjs && node --check src/runtime.mjs && node --check src/version.mjs",
21
+ "check": "node --check bin/remcp.mjs && node --check src/agent-deploy-reconnect.mjs && node --check src/agent-network-probe-client.mjs && node --check src/agent-relay-connection.mjs && node --check src/agent-responder.mjs && node --check src/agent-runtime-capabilities.mjs && node --check src/agent-update-monitor.mjs && node --check src/agent-update.mjs && node --check src/agent-utilities.mjs && node --check src/agent.mjs && node --check src/cli.mjs && node --check src/cli/config.mjs && node --check src/cli/connect.mjs && node --check src/cli/doctor.mjs && node --check src/cli/env.mjs && node --check src/cli/installation-audit.mjs && node --check src/cli/mac-native-temperature.mjs && node --check src/cli/npm-global.mjs && node --check src/cli/pairing-qr.mjs && node --check src/cli/service-inventory.mjs && node --check src/cli/service-macos.mjs && node --check src/cli/service-paths.mjs && node --check src/cli/service.mjs && node --check src/cli/shell.mjs && node --check src/cli/update.mjs && node --check src/cli/windows-owned-job-source.mjs && node --check src/cli/windows-owned-job.mjs && node --check src/cli/windows-supervisor.mjs && node --check src/cli/windows-task-restart.mjs && node --check src/cli/windows-update-lock.mjs && node --check src/cli/windows-user-supervisor.mjs && node --check src/fetch-update-manifest.mjs && node --check src/fs-access.mjs && node --check src/npm.mjs && node --check src/runtime.mjs && node --check src/version.mjs",
22
22
  "test": "node --test test/*.test.mjs"
23
23
  },
24
24
  "dependencies": {
@@ -0,0 +1,151 @@
1
+ // Windows 10+: assign the child to an unnamed job during CreateProcess itself.
2
+ // https://learn.microsoft.com/windows/win32/api/processthreadsapi/nf-processthreadsapi-updateprocthreadattribute
3
+ // Only the helper holds the job handle. Neither child nor descendants inherit it.
4
+ export const windowsOwnedJobScript = String.raw`
5
+ $ErrorActionPreference = 'Stop'
6
+ $ProgressPreference = 'SilentlyContinue'
7
+ $job = $null
8
+ try {
9
+ Add-Type -TypeDefinition @'
10
+ using System;
11
+ using System.ComponentModel;
12
+ using System.Diagnostics;
13
+ using System.Runtime.InteropServices;
14
+ using System.Text;
15
+ using System.Threading;
16
+
17
+ public sealed class ReMcpOwnedJob : IDisposable {
18
+ [StructLayout(LayoutKind.Sequential)] struct Limits {
19
+ public long ProcessTime, JobTime; public uint Flags;
20
+ public UIntPtr MinWorkingSet, MaxWorkingSet; public uint ActiveLimit;
21
+ public UIntPtr Affinity; public uint Priority, Scheduling;
22
+ }
23
+ [StructLayout(LayoutKind.Sequential)] struct Io {
24
+ public ulong ReadOps, WriteOps, OtherOps, ReadBytes, WriteBytes, OtherBytes;
25
+ }
26
+ [StructLayout(LayoutKind.Sequential)] struct ExtendedLimits {
27
+ public Limits Basic; public Io Counters;
28
+ public UIntPtr ProcessMemory, JobMemory, PeakProcessMemory, PeakJobMemory;
29
+ }
30
+ [StructLayout(LayoutKind.Sequential)] struct Accounting {
31
+ public long UserTime, KernelTime, PeriodUserTime, PeriodKernelTime;
32
+ public uint PageFaults, TotalProcesses, ActiveProcesses, TerminatedProcesses;
33
+ }
34
+ [StructLayout(LayoutKind.Sequential)] struct Startup {
35
+ public uint Size; public IntPtr Reserved, Desktop, Title;
36
+ public uint X, Y, XSize, YSize, XChars, YChars, Fill, Flags;
37
+ public ushort Show, ReservedSize; public IntPtr ReservedData, In, Out, Error;
38
+ }
39
+ [StructLayout(LayoutKind.Sequential)] struct StartupEx {
40
+ public Startup Basic; public IntPtr Attributes;
41
+ }
42
+ [StructLayout(LayoutKind.Sequential)] struct ProcessInfo {
43
+ public IntPtr Process, Thread; public uint Pid, Tid;
44
+ }
45
+ [StructLayout(LayoutKind.Sequential)] struct Security {
46
+ public int Size; public IntPtr Descriptor;
47
+ [MarshalAs(UnmanagedType.Bool)] public bool Inherit;
48
+ }
49
+ [DllImport("kernel32.dll", SetLastError=true)] static extern IntPtr CreateJobObjectW(IntPtr security, IntPtr name);
50
+ [DllImport("kernel32.dll", SetLastError=true)] static extern bool SetInformationJobObject(IntPtr job, int kind, ref ExtendedLimits limits, uint size);
51
+ [DllImport("kernel32.dll", SetLastError=true)] static extern bool QueryInformationJobObject(IntPtr job, int kind, out Accounting info, uint size, IntPtr returned);
52
+ [DllImport("kernel32.dll", SetLastError=true)] static extern bool TerminateJobObject(IntPtr job, uint code);
53
+ [DllImport("kernel32.dll", SetLastError=true)] static extern bool InitializeProcThreadAttributeList(IntPtr list, int count, int flags, ref IntPtr size);
54
+ [DllImport("kernel32.dll", SetLastError=true)] static extern bool UpdateProcThreadAttribute(IntPtr list, uint flags, IntPtr key, IntPtr value, IntPtr size, IntPtr previous, IntPtr returned);
55
+ [DllImport("kernel32.dll")] static extern void DeleteProcThreadAttributeList(IntPtr list);
56
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern bool CreateProcessW(string application, StringBuilder command, IntPtr processSecurity, IntPtr threadSecurity, bool inherit, uint flags, IntPtr environment, string cwd, ref StartupEx startup, out ProcessInfo info);
57
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern IntPtr CreateFileW(string name, uint access, uint share, ref Security security, uint creation, uint flags, IntPtr template);
58
+ [DllImport("kernel32.dll", SetLastError=true)] static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
59
+ [DllImport("kernel32.dll", SetLastError=true)] static extern bool GetExitCodeProcess(IntPtr handle, out uint code);
60
+ [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
61
+ IntPtr job, process;
62
+ int stop;
63
+ static void Check(bool ok) { if (!ok) throw new Win32Exception(Marshal.GetLastWin32Error()); }
64
+ static void Close(IntPtr handle) { if (handle != IntPtr.Zero && handle != new IntPtr(-1)) CloseHandle(handle); }
65
+ public ReMcpOwnedJob() {
66
+ job = CreateJobObjectW(IntPtr.Zero, IntPtr.Zero);
67
+ if (job == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error());
68
+ try {
69
+ var limits = new ExtendedLimits();
70
+ limits.Basic.Flags = 0x2000; // KILL_ON_JOB_CLOSE; no breakaway allowed.
71
+ Check(SetInformationJobObject(job, 9, ref limits, (uint)Marshal.SizeOf(limits)));
72
+ } catch { Dispose(); throw; }
73
+ }
74
+ public uint Start(string application, string command, string cwd, string log) {
75
+ var security = new Security { Size=Marshal.SizeOf(typeof(Security)), Inherit=true };
76
+ IntPtr output=IntPtr.Zero, input=IntPtr.Zero, list=IntPtr.Zero, handles=IntPtr.Zero, jobs=IntPtr.Zero;
77
+ bool initialized = false;
78
+ try {
79
+ output = CreateFileW(log, 4, 7, ref security, 4, 0x80, IntPtr.Zero); // append, OPEN_ALWAYS
80
+ Check(output != new IntPtr(-1));
81
+ input = CreateFileW("NUL", 0x80000000, 7, ref security, 3, 0x80, IntPtr.Zero);
82
+ Check(input != new IntPtr(-1));
83
+ IntPtr size = IntPtr.Zero;
84
+ InitializeProcThreadAttributeList(IntPtr.Zero, 2, 0, ref size);
85
+ Check(size != IntPtr.Zero);
86
+ list = Marshal.AllocHGlobal(size);
87
+ Check(InitializeProcThreadAttributeList(list, 2, 0, ref size)); initialized = true;
88
+ handles = Marshal.AllocHGlobal(IntPtr.Size * 2);
89
+ Marshal.WriteIntPtr(handles, input); Marshal.WriteIntPtr(handles, IntPtr.Size, output);
90
+ Check(UpdateProcThreadAttribute(list, 0, new IntPtr(0x20002), handles, new IntPtr(IntPtr.Size * 2), IntPtr.Zero, IntPtr.Zero));
91
+ jobs = Marshal.AllocHGlobal(IntPtr.Size); Marshal.WriteIntPtr(jobs, job);
92
+ Check(UpdateProcThreadAttribute(list, 0, new IntPtr(0x2000D), jobs, new IntPtr(IntPtr.Size), IntPtr.Zero, IntPtr.Zero));
93
+ var startup = new StartupEx();
94
+ startup.Basic.Size = (uint)Marshal.SizeOf(startup);
95
+ startup.Basic.Flags = 0x100; // STARTF_USESTDHANDLES
96
+ startup.Basic.In = input; startup.Basic.Out = output; startup.Basic.Error = output;
97
+ startup.Attributes = list;
98
+ ProcessInfo info;
99
+ // JOB_LIST attaches before execution, avoiding a suspended-but-unassigned orphan window.
100
+ Check(CreateProcessW(application, new StringBuilder(command), IntPtr.Zero, IntPtr.Zero,
101
+ true, 0x08080000, IntPtr.Zero, cwd, ref startup, out info));
102
+ process = info.Process; Close(info.Thread);
103
+ return info.Pid;
104
+ } finally {
105
+ if (initialized) DeleteProcThreadAttributeList(list);
106
+ if (list != IntPtr.Zero) Marshal.FreeHGlobal(list);
107
+ if (handles != IntPtr.Zero) Marshal.FreeHGlobal(handles);
108
+ if (jobs != IntPtr.Zero) Marshal.FreeHGlobal(jobs);
109
+ Close(input); Close(output);
110
+ }
111
+ }
112
+ public uint Wait() {
113
+ var reader = new Thread(() => {
114
+ // EOF is ownership loss: only Node owns the write end of this private pipe.
115
+ try { Console.ReadLine(); } catch { } finally { Interlocked.Exchange(ref stop, 1); }
116
+ });
117
+ reader.IsBackground = true; reader.Start();
118
+ while (Volatile.Read(ref stop) == 0) {
119
+ uint state = WaitForSingleObject(process, 50);
120
+ if (state == 0) break;
121
+ if (state != 258) throw new Win32Exception(Marshal.GetLastWin32Error());
122
+ }
123
+ Check(TerminateJobObject(job, 1));
124
+ var timer = Stopwatch.StartNew();
125
+ while (true) {
126
+ Accounting info;
127
+ Check(QueryInformationJobObject(job, 1, out info, (uint)Marshal.SizeOf(typeof(Accounting)), IntPtr.Zero));
128
+ if (info.ActiveProcesses == 0) break;
129
+ if (timer.ElapsedMilliseconds >= 7000) throw new TimeoutException("Owned job did not become empty");
130
+ Thread.Sleep(20);
131
+ }
132
+ uint code; Check(GetExitCodeProcess(process, out code)); return code;
133
+ }
134
+ public void Dispose() { Close(job); job=IntPtr.Zero; Close(process); process=IntPtr.Zero; }
135
+ }
136
+ '@
137
+ $job = New-Object ReMcpOwnedJob
138
+ [Console]::WriteLine('{"ready":true}')
139
+ $line = [Console]::ReadLine()
140
+ if (!$line) { throw 'Job owner disconnected before start' }
141
+ $request = ConvertFrom-Json -InputObject ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($line)))
142
+ $childId = $job.Start($request.application, $request.command, $request.cwd, $request.log)
143
+ [Console]::WriteLine(('{"started":true,"pid":' + $childId + '}'))
144
+ $code = $job.Wait()
145
+ [Console]::WriteLine(('{"empty":true,"exitCode":' + $code + '}'))
146
+ } catch {
147
+ $cause = $_.Exception.GetBaseException()
148
+ [Console]::Error.WriteLine(('Windows owned job failed (' + $cause.GetType().Name + '; code=' + $cause.NativeErrorCode + ').'))
149
+ exit 1
150
+ } finally { if ($job) { $job.Dispose() } }
151
+ `;
@@ -0,0 +1,68 @@
1
+ import path from 'node:path';
2
+ import { spawn } from 'node:child_process';
3
+ import { windowsOwnedJobScript } from './windows-owned-job-source.mjs';
4
+
5
+ function quote(value) {
6
+ if (typeof value !== 'string' || value.includes('\0')) throw new Error('Invalid owned-job argument');
7
+ return '"' + value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1') + '"';
8
+ }
9
+
10
+ // The helper owns an unnamed kernel job; no PID enumeration or taskkill is used.
11
+ // Acknowledgement is emitted only after the job's ActiveProcesses count reaches zero.
12
+ export function spawnWindowsOwnedJob(application, args, { cwd, log, env = process.env } = {}) {
13
+ if (process.platform !== 'win32') throw new Error('Windows owned jobs require Windows');
14
+ if (![application, cwd, log].every(p => typeof p === 'string' && !p.includes('\0') && path.isAbsolute(p))) {
15
+ throw new Error('Owned-job paths must be absolute');
16
+ }
17
+ const command = [application, ...args].map(quote).join(' ');
18
+ if (command.length > 32766) throw new Error('Owned-job command line is too long');
19
+ const powershell = path.join(process.env.SystemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
20
+ const helper = spawn(powershell, ['-NoProfile', '-NonInteractive', '-EncodedCommand',
21
+ Buffer.from(windowsOwnedJobScript, 'utf16le').toString('base64')], {
22
+ cwd:path.dirname(powershell), env, windowsHide:true, stdio:['pipe','pipe','pipe'],
23
+ });
24
+ let output = '', stderr = '', started = false, empty = false, settled = false;
25
+ let resolveReady, rejectReady, resolveDone, rejectDone;
26
+ const ready = new Promise((resolve, reject) => { resolveReady=resolve; rejectReady=reject; });
27
+ const done = new Promise((resolve, reject) => { resolveDone=resolve; rejectDone=reject; });
28
+ // Callers may observe readiness and completion separately; suppress unhandled rejection noise.
29
+ ready.catch(() => {}); done.catch(() => {});
30
+ const fail = error => { rejectReady(error); rejectDone(error); };
31
+ const timer = setTimeout(() => { fail(new Error('Windows job startup timed out')); helper.kill(); }, 15000);
32
+ helper.stdin.on('error', () => {});
33
+ helper.stderr.on('data', chunk => { stderr = (stderr + chunk).slice(-2048); });
34
+ helper.stdout.on('data', chunk => {
35
+ output += chunk.toString('utf8');
36
+ if (output.length > 4096) { fail(new Error('Invalid Windows job response')); helper.kill(); return; }
37
+ while (output.includes('\n')) {
38
+ const index = output.indexOf('\n'), line = output.slice(0,index).trim(); output=output.slice(index+1);
39
+ try {
40
+ const message = JSON.parse(line);
41
+ if (message.ready === true && !started) {
42
+ // ASCII envelope survives Windows PowerShell 5.1's OEM console codepage.
43
+ helper.stdin.write(Buffer.from(JSON.stringify({application,command,cwd,log}),'utf8').toString('base64') + '\n');
44
+ } else if (message.started === true && Number.isSafeInteger(message.pid) && message.pid > 0 && !started) {
45
+ started=true; helper.ownedJob.running=true; helper.ownedJob.pid=message.pid;
46
+ clearTimeout(timer); resolveReady(message);
47
+ } else if (message.empty === true && started) { empty=true; helper.ownedJob.running=false; }
48
+ else throw new Error('Unexpected Windows job response');
49
+ } catch { fail(new Error('Invalid Windows job response')); helper.kill(); }
50
+ }
51
+ });
52
+ helper.on('error', error => { clearTimeout(timer); fail(error); });
53
+ helper.on('close', code => {
54
+ clearTimeout(timer); helper.ownedJob.running=false; settled=true;
55
+ helper.ownedJob.confirmedEmpty = code === 0 && started && empty;
56
+ if (helper.ownedJob.confirmedEmpty) resolveDone();
57
+ else fail(new Error('Windows owned job failed: ' + stderr.trim().slice(0,300)));
58
+ if (!started) rejectReady(new Error('Windows job exited before readiness'));
59
+ });
60
+ helper.ownedJob = { ready, done, running:false, confirmedEmpty:false, pid:null, stop: async () => {
61
+ if (!settled) helper.stdin.end('STOP\n');
62
+ let timeout;
63
+ try { await Promise.race([done, new Promise((_, reject) => {
64
+ timeout=setTimeout(() => reject(new Error('Windows owned job stop timed out')), 9000);
65
+ })]); } finally { clearTimeout(timeout); }
66
+ }};
67
+ return helper;
68
+ }
@@ -67,11 +67,25 @@ export function withWindowsUpdateLock(prefix, operation) {
67
67
  }
68
68
 
69
69
  const onExit = () => removeOwnedLock(directory, token);
70
- process.once('exit', onExit);
71
- try {
72
- return operation();
73
- } finally {
70
+ let released = false;
71
+ const release = () => {
72
+ if (released) return;
73
+ released = true;
74
74
  process.removeListener('exit', onExit);
75
75
  removeOwnedLock(directory, token);
76
+ };
77
+ process.once('exit', onExit);
78
+ try {
79
+ const result = operation();
80
+ // A future async installer must retain the prefix lock until it settles.
81
+ // Releasing here would let another updater move the same package/shims.
82
+ if (result && typeof result.then === 'function') {
83
+ return Promise.resolve(result).finally(release);
84
+ }
85
+ release();
86
+ return result;
87
+ } catch (error) {
88
+ release();
89
+ throw error;
76
90
  }
77
91
  }
@@ -8,6 +8,7 @@ import process from 'node:process';
8
8
  import crypto from 'node:crypto';
9
9
  import { spawn, spawnSync } from 'node:child_process';
10
10
  import { fileURLToPath } from 'node:url';
11
+ import { spawnWindowsOwnedJob } from './windows-owned-job.mjs';
11
12
 
12
13
  const currentFile = fileURLToPath(import.meta.url);
13
14
  const root = process.env.REMCP_CONFIG_DIR || path.join(os.homedir(), '.config', 'remcp');
@@ -94,33 +95,48 @@ async function serve(cliEntry) {
94
95
  let requestedRestart = null;
95
96
  let startedAt = 0;
96
97
  let lastExit = null;
98
+ let containmentFailure = null;
97
99
  const usedNonces = new Set();
98
100
 
99
101
  function start() {
100
- if (stopped || child) return;
102
+ if (stopped || child || containmentFailure) return;
101
103
  const logFile = path.join(root, 'windows-startup-agent.log');
102
104
  try { if (fs.statSync(logFile).size > 4 * 1024 * 1024) fs.truncateSync(logFile, 0); } catch {}
103
105
  const log = fs.openSync(logFile, 'a', 0o600);
104
106
  const prefix = path.resolve(path.dirname(cliEntry), '../../../../');
105
107
  try {
106
- child = spawn(process.execPath, [cliEntry, 'start', '--service'], {
107
- cwd: root, windowsHide: true, stdio: ['ignore', log, log, 'ipc'],
108
- env: { ...process.env, REMCP_USER_STARTUP_SUPERVISOR:'1', NPM_CONFIG_PREFIX: prefix,
109
- PATH: path.dirname(process.execPath) + path.delimiter + (process.env.PATH || '') },
110
- });
108
+ const env = { ...process.env, REMCP_USER_STARTUP_SUPERVISOR:'1', NPM_CONFIG_PREFIX: prefix,
109
+ PATH: path.dirname(process.execPath) + path.delimiter + (process.env.PATH || '') };
110
+ child = process.platform === 'win32'
111
+ ? spawnWindowsOwnedJob(process.execPath, [cliEntry, 'start', '--service'], {cwd:root,log:logFile,env})
112
+ : spawn(process.execPath, [cliEntry, 'start', '--service'], {
113
+ cwd:root, windowsHide:true, stdio:['ignore',log,log,'ipc'], env,
114
+ });
111
115
  } finally { fs.closeSync(log); }
112
116
  const ownChild = child;
113
117
  startedAt = Date.now();
118
+ ownChild.ownedJob?.ready.then(() => { startedAt=Date.now(); }).catch(error => { lastExit=error.message; });
119
+ ownChild.ownedJob?.done.catch(error => { lastExit=error.message; });
114
120
  ownChild.on('error', error => {
115
121
  lastExit = error.message;
122
+ if (ownChild.ownedJob) {
123
+ containmentFailure = new Error('Windows job ownership failed; restart refused');
124
+ stopped=true;
125
+ return; // Retain the owner until close; only confirmedEmpty can release it.
126
+ }
116
127
  if (child === ownChild) {
117
128
  child = null;
118
129
  if (!stopped) restartTimer = setTimeout(start, 3000);
119
130
  }
120
131
  });
121
- ownChild.on('exit', (code, signal) => {
132
+ ownChild.on('close', (code, signal) => {
122
133
  if (child !== ownChild) return;
123
134
  child = null;
135
+ // Completion failure does not prove the previous tree is empty. Fail closed.
136
+ if (ownChild.ownedJob && !ownChild.ownedJob.confirmedEmpty) {
137
+ containmentFailure = new Error('Windows job did not confirm an empty process tree; restart refused');
138
+ stopped=true; lastExit=containmentFailure.message; return;
139
+ }
124
140
  lastExit = code === null ? String(signal || 'unknown') : String(code);
125
141
  if (!stopped) restartTimer = setTimeout(start, 3000);
126
142
  });
@@ -128,17 +144,12 @@ async function serve(cliEntry) {
128
144
 
129
145
  async function endChild() {
130
146
  clearTimeout(restartTimer);
147
+ if (containmentFailure) throw containmentFailure;
131
148
  const ownChild = child;
132
149
  if (!ownChild) return;
150
+ if (ownChild.ownedJob) { await ownChild.ownedJob.stop(); return; }
133
151
  if (ownChild.exitCode === null && !ownChild.killed) {
134
- if (process.platform === 'win32') {
135
- const result = spawnSync('taskkill.exe', ['/PID', String(ownChild.pid), '/T', '/F'], {
136
- windowsHide: true, encoding: 'utf8', timeout: 7000, maxBuffer: 2048,
137
- });
138
- if (result.status !== 0 && ownChild.exitCode === null) {
139
- throw new Error('Could not terminate owned agent process tree');
140
- }
141
- } else ownChild.kill('SIGTERM');
152
+ ownChild.kill('SIGTERM');
142
153
  }
143
154
  if (ownChild.exitCode === null) await Promise.race([
144
155
  new Promise(resolve => ownChild.once('exit', resolve)),
@@ -184,6 +195,7 @@ async function serve(cliEntry) {
184
195
  process.exit(0);
185
196
  });
186
197
  } else if (request.action === 'restart') {
198
+ if (containmentFailure) throw containmentFailure;
187
199
  // Agent-initiated npm updates call this from a child of the current agent.
188
200
  // Killing that tree before the update CLI returns can abort the updater itself.
189
201
  // Coalesce requests and schedule the owned-child handoff after acknowledgement.
@@ -194,14 +206,14 @@ async function serve(cliEntry) {
194
206
  stopped = true;
195
207
  try { await endChild(); }
196
208
  catch (error) { console.error('ReMCP supervisor restart failed:', error.message); }
197
- finally { stopped = false; if (!child) start(); }
209
+ finally { if (!containmentFailure) { stopped = false; if (!child) start(); } }
198
210
  })();
199
211
  }, 2500);
200
212
  }
201
213
  reply({ ok: true, restartScheduled: true });
202
214
  } else reply({
203
- ok: true, childPid: child?.pid || null,
204
- running: Boolean(child?.pid && child.exitCode === null),
215
+ ok: true, childPid: child?.ownedJob?.pid || child?.pid || null,
216
+ running: Boolean(child?.pid && child.exitCode === null && (!child.ownedJob || child.ownedJob.running)),
205
217
  restartPending: Boolean(requestedRestart),
206
218
  uptimeMs: child ? Date.now() - startedAt : null, lastExit,
207
219
  });