mtech-ai-local-runner 0.1.40 → 0.1.42

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/README.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## 0.1.42: keep the guardian's reports when a late terminate fails
2
+
3
+ The owned guardian now reports to the host on its own pipe (fd3) and takes the host's commands on another (fd5);
4
+ fd4 stays parent liveness, and none of them reaches the executor. Before, one control socket carried both. When
5
+ Codex exited by itself and the guardian confirmed and exited while the daemon was busy, the daemon's late terminate
6
+ write failed on that socket, Node destroyed it, and the unread `root_exit` and `cleaned` reports were lost. 0.1.41
7
+ recovered the cleanup confirmation from the exit code, but the lost root exit outcome still failed the P1 publication
8
+ freeze with `OWNED_PROCESS_EXIT_UNCONFIRMED`. A failed command write now leaves the report pipe intact, so the host reads
9
+ every report to its end. Cleanup is again confirmed only by `cleaned`; the exit-code fallback is removed.
10
+
11
+ ## 0.1.41: confirm a cleanup the guardian finished before a late terminate
12
+
13
+ When Codex exits by itself, the owned guardian confirms the cleanup and exits. A busy daemon could then write its
14
+ terminate request to the closed control socket before reading `cleaned`. That write error turned a confirmed cleanup
15
+ into an unconfirmed one and suspended the job. A control socket error alone no longer does that: cleanup is
16
+ confirmed by `cleaned` or by the guardian's exit code 0, which it uses only after sending `cleaned`. Timeouts,
17
+ liveness errors, malformed messages and `unconfirmed` reports still fail closed.
18
+
1
19
  ## 0.1.40: say why the owned guardian could not confirm cleanup
2
20
 
3
21
  When the owned guardian reports an unconfirmed cleanup, the `executor_cleanup` diagnostic now carries fixed
@@ -1,15 +1,18 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  // Trusted, packaged JavaScript, deliberately independent of TypeScript loaders.
3
- // Only this helper creates and signals the new executor group. fd3 is private
4
- // control, fd4 parent liveness; neither is passed to the executor.
3
+ // Only this helper creates and signals the new executor group. fd3 carries its
4
+ // private reports to the host, fd4 parent liveness and fd5 the host's commands;
5
+ // none is passed to the executor. Commands have their own pipe: a host write
6
+ // that fails after the guardian exited cannot discard reports not yet read.
5
7
  const GUARDIAN_SOURCE = String.raw `
6
8
  const {spawn}=require('node:child_process');
7
9
  const {Socket}=require('node:net');
8
- const control=new Socket({fd:3,readable:true,writable:true});
9
- control.setEncoding('utf8');
10
+ const reports=new Socket({fd:3,readable:false,writable:true});
10
11
  const liveness=new Socket({fd:4,readable:true,writable:false});
12
+ const commands=new Socket({fd:5,readable:true,writable:false});
13
+ commands.setEncoding('utf8');
11
14
  let child, ownedPid, ownedGone=false, cleaning, ready=Promise.resolve(), rootExited=false, closed=false, input='', configured=false, orphaned=false;
12
- const send=value=>{if(!control.destroyed)control.write(JSON.stringify(value)+'\n');};
15
+ const send=value=>{if(!reports.destroyed)reports.write(JSON.stringify(value)+'\n');};
13
16
  const delay=ms=>new Promise(resolve=>setTimeout(resolve,ms));
14
17
  const alive=()=>{if(!ownedPid||ownedGone)return false;try{process.kill(-ownedPid,0);return true;}catch(e){if(e.code==='ESRCH'){ownedGone=true;return false;}if(e.code==='EPERM')return true;throw Error('cleanup');}};
15
18
  const signal=s=>{if(!ownedPid)throw Error('ownership');if(ownedGone)return 'esrch';try{process.kill(-ownedPid,s);return 'sent';}catch(e){if(e.code==='ESRCH'){ownedGone=true;return 'esrch';}if(e.code==='EPERM'){if(!rootExited)child.kill(s);return 'eperm';}throw Error('cleanup');}};
@@ -39,11 +42,11 @@ function cleanup(immediate=false,trigger='terminate'){
39
42
  })();
40
43
  return cleaning;
41
44
  }
42
- function finish(code){process.exitCode=code;process.stdin.destroy();liveness.destroy();control.end(()=>control.destroy());process.stdout.end();process.stderr.end();}
45
+ function finish(code){process.exitCode=code;process.stdin.destroy();liveness.destroy();commands.destroy();reports.end(()=>reports.destroy());process.stdout.end();process.stderr.end();}
43
46
  function lost(){orphaned=true;if(configured)void cleanup(false,'lost');else finish(1);}
44
47
  liveness.on('data',()=>{});liveness.once('end',lost);liveness.once('error',lost);
45
- control.on('error',lost);control.on('end',lost);
46
- control.on('data',chunk=>{
48
+ reports.on('error',lost);commands.on('error',lost);commands.on('end',lost);
49
+ commands.on('data',chunk=>{
47
50
  input+=chunk.toString('utf8');if(input.length>2000000){lost();return;}
48
51
  let end;while((end=input.indexOf('\n'))>=0){
49
52
  let message;try{message=JSON.parse(input.slice(0,end));}catch{lost();return;}input=input.slice(end+1);
@@ -68,25 +71,31 @@ process.stderr.on('error',()=>{child?.stderr.resume();void cleanup(true,'output_
68
71
  `;
69
72
  export function spawnOwnedGuardian(command, args, options) {
70
73
  const child = spawn(process.execPath, ['--input-type=commonjs', '-e', GUARDIAN_SOURCE], {
71
- cwd: options.cwd, env: {}, shell: false, detached: true, stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
74
+ cwd: options.cwd, env: {}, shell: false, detached: true, stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
72
75
  });
73
- const control = child.stdio[3], liveness = child.stdio[4];
74
- control.setEncoding('utf8');
76
+ // Node types only the first five stdio entries; fd5 is the sixth 'pipe' above.
77
+ const stdio = child.stdio;
78
+ const reports = stdio[3], liveness = stdio[4], commands = stdio[5];
79
+ reports.setEncoding('utf8');
75
80
  liveness.resume();
76
81
  let resolveStarted, rejectStarted;
77
82
  const started = new Promise((resolve, reject) => { resolveStarted = resolve; rejectStarted = reject; });
78
83
  // Hosts can attach observers after spawn returns without unhandled rejection.
79
84
  void started.catch(() => { });
80
85
  let confirmed = false, spawnFailed = false, protocolFailed = false, buffer = '';
81
- let terminateWritten = false, hostTimeout = false, detail;
86
+ let terminateWritten = false, hostTimeout = false, commandFailed = false, detail;
82
87
  let outcome;
83
88
  let closePromise;
84
89
  const closed = new Promise(resolve => child.once('close', () => { liveness.destroy(); resolve(); }));
85
- control.on('error', () => { if (!confirmed && !spawnFailed)
90
+ // A guardian that has exited no longer reads commands, so a terminate written late by a busy host
91
+ // fails. That failure stays on the command pipe: the host still reads every report to its end.
92
+ commands.on('error', () => { if (!confirmed && !spawnFailed)
93
+ commandFailed = true; });
94
+ reports.on('error', () => { if (!confirmed && !spawnFailed)
86
95
  protocolFailed = true; });
87
96
  liveness.on('error', () => { if (!confirmed && !spawnFailed)
88
97
  protocolFailed = true; });
89
- control.on('data', (chunk) => {
98
+ reports.on('data', (chunk) => {
90
99
  buffer += chunk;
91
100
  if (buffer.length > 8192) {
92
101
  protocolFailed = true;
@@ -128,10 +137,10 @@ export function spawnOwnedGuardian(command, args, options) {
128
137
  });
129
138
  child.once('error', () => { spawnFailed = true; rejectStarted(new Error('Provider process could not start')); });
130
139
  child.once('close', () => { rejectStarted(new Error('Provider process could not start')); });
131
- control.write(JSON.stringify({ type: 'launch', command, args, ...options }) + '\n');
140
+ commands.write(JSON.stringify({ type: 'launch', command, args, ...options }) + '\n');
132
141
  return { child, started, closed, outcome: () => outcome, spawnFailed: () => spawnFailed,
133
- signal(immediate) { if (child.exitCode === null && child.signalCode === null && !confirmed && !spawnFailed && !control.destroyed) {
134
- control.write(JSON.stringify({ type: 'terminate', immediate }) + '\n');
142
+ signal(immediate) { if (child.exitCode === null && child.signalCode === null && !confirmed && !spawnFailed && !commands.destroyed) {
143
+ commands.write(JSON.stringify({ type: 'terminate', immediate }) + '\n');
135
144
  terminateWritten = true;
136
145
  } },
137
146
  terminate(immediate = false) {
@@ -151,13 +160,15 @@ export function spawnOwnedGuardian(command, args, options) {
151
160
  cleanupObservations() {
152
161
  if (confirmed && !protocolFailed)
153
162
  return [];
154
- if (!protocolFailed && !hostTimeout)
163
+ if (!protocolFailed && !hostTimeout && !commandFailed)
155
164
  return [];
156
165
  const observed = [terminateWritten ? 'cleanup_guardian_terminate_written' : 'cleanup_guardian_terminate_not_written'];
157
166
  if (hostTimeout)
158
167
  observed.push('cleanup_guardian_host_timeout');
168
+ if (commandFailed)
169
+ observed.push('cleanup_guardian_control_failed');
159
170
  if (!detail) {
160
- if (!hostTimeout)
171
+ if (!hostTimeout && protocolFailed)
161
172
  observed.push('cleanup_guardian_protocol_failed');
162
173
  return observed;
163
174
  }
@@ -20,7 +20,7 @@ export const STARTUP_OBSERVATIONS = Object.freeze([
20
20
  'cleanup_exec_tools_revoke_failed', 'cleanup_job_tools_revoke_failed', 'cleanup_stdin_end_failed', 'cleanup_process_terminate_failed',
21
21
  'cleanup_tool_server_close_failed', 'cleanup_timeout',
22
22
  // The owned guardian's own account of an unconfirmed cleanup (fixed words, no PIDs or paths).
23
- 'cleanup_guardian_terminate_written', 'cleanup_guardian_terminate_not_written', 'cleanup_guardian_host_timeout', 'cleanup_guardian_protocol_failed',
23
+ 'cleanup_guardian_terminate_written', 'cleanup_guardian_terminate_not_written', 'cleanup_guardian_host_timeout', 'cleanup_guardian_control_failed', 'cleanup_guardian_protocol_failed',
24
24
  'cleanup_guardian_trigger_terminate', 'cleanup_guardian_trigger_root_exit', 'cleanup_guardian_trigger_lost', 'cleanup_guardian_trigger_output_error', 'cleanup_guardian_trigger_spawn_error',
25
25
  'cleanup_guardian_term_sent', 'cleanup_guardian_term_eperm', 'cleanup_guardian_term_esrch', 'cleanup_guardian_term_not_needed',
26
26
  'cleanup_guardian_kill_sent', 'cleanup_guardian_kill_eperm', 'cleanup_guardian_kill_esrch', 'cleanup_guardian_kill_not_needed',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtech-ai-local-runner",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "bin": {