@openagents-org/agent-launcher 0.2.185 → 0.2.186

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": "@openagents-org/agent-launcher",
3
- "version": "0.2.185",
3
+ "version": "0.2.186",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -76,7 +76,6 @@ const IS_WINDOWS = process.platform === 'win32';
76
76
 
77
77
  // Max wall-clock for a single headless run, after which the process group is
78
78
  // killed and the turn reported as interrupted.
79
- const TIMEOUT_MS = 600000; // 10 minutes
80
79
 
81
80
  // Idle watchdog: with no stdout frame for this long we nudge the channel, and
82
81
  // after MAX consecutive silences we kill a run that is probably wedged.
@@ -615,7 +614,6 @@ class CodeBuddyAdapter extends BaseAdapter {
615
614
  if (settled) return;
616
615
  settled = true;
617
616
  clearInterval(watchdog);
618
- clearTimeout(timeout);
619
617
  if (this._channelProcesses[channel] === proc) delete this._channelProcesses[channel];
620
618
  resolve(payload);
621
619
  };
@@ -721,12 +719,6 @@ class CodeBuddyAdapter extends BaseAdapter {
721
719
  }
722
720
  }, WATCHDOG_INTERVAL_MS);
723
721
 
724
- const timeout = setTimeout(() => {
725
- this._log(`Run exceeded ${TIMEOUT_MS}ms — killing`);
726
- killedByWatchdog = true;
727
- void this._stopProcess(proc);
728
- }, TIMEOUT_MS);
729
-
730
722
  try {
731
723
  proc.stdin.on('error', () => {});
732
724
  proc.stdin.write(prompt, 'utf-8');
@@ -71,7 +71,6 @@ const IS_WINDOWS = process.platform === 'win32';
71
71
 
72
72
  // Max wall-clock for a single headless run, after which the process group is
73
73
  // killed and the turn reported as interrupted.
74
- const TIMEOUT_MS = 600000; // 10 minutes
75
74
 
76
75
  // Idle watchdog: with no stdout frame for this long we nudge the channel, and
77
76
  // after MAX consecutive silences we kill a run that is probably wedged.
@@ -567,7 +566,6 @@ class CommandCodeAdapter extends BaseAdapter {
567
566
  if (settled) return;
568
567
  settled = true;
569
568
  clearInterval(watchdog);
570
- clearTimeout(timeout);
571
569
  if (this._channelProcesses[channel] === proc) delete this._channelProcesses[channel];
572
570
  resolve(payload);
573
571
  };
@@ -653,12 +651,6 @@ class CommandCodeAdapter extends BaseAdapter {
653
651
  }
654
652
  }, WATCHDOG_INTERVAL_MS);
655
653
 
656
- const timeout = setTimeout(() => {
657
- this._log(`Run exceeded ${TIMEOUT_MS}ms — killing`);
658
- killedByWatchdog = true;
659
- void this._stopProcess(proc);
660
- }, TIMEOUT_MS);
661
-
662
654
  // Feed the prompt. stdin is closed immediately: Command Code times out a
663
655
  // piped stdin after 30s of silence, so an unclosed pipe would stall.
664
656
  try {
@@ -76,7 +76,7 @@ const SECRET_ENV_VARS = [
76
76
  const ACT_ALLOW_TOOLS = ['shell', 'write'];
77
77
 
78
78
  // How long to wait for a turn before giving up (no output / hung CLI).
79
- const TURN_TIMEOUT_MS = 10 * 60 * 1000;
79
+ const TURN_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
80
80
 
81
81
  class CopilotAdapter extends BaseAdapter {
82
82
  /**
@@ -565,10 +565,15 @@ class CopilotAdapter extends BaseAdapter {
565
565
  let pending = Promise.resolve();
566
566
  let settled = false;
567
567
 
568
- const timeout = setTimeout(() => {
569
- this._stopProcess(proc).catch(() => {});
570
- finish({ timedOut: true, finalText: accumulatedText() });
571
- }, TURN_TIMEOUT_MS);
568
+ let timeout = null;
569
+ const resetTimeout = () => {
570
+ if (timeout) clearTimeout(timeout);
571
+ timeout = setTimeout(() => {
572
+ this._stopProcess(proc).catch(() => {});
573
+ finish({ timedOut: true, finalText: accumulatedText() });
574
+ }, TURN_IDLE_TIMEOUT_MS);
575
+ };
576
+ resetTimeout();
572
577
 
573
578
  const finish = (summary) => {
574
579
  if (settled) return;
@@ -657,6 +662,9 @@ class CopilotAdapter extends BaseAdapter {
657
662
  }
658
663
  };
659
664
 
665
+ if (proc.stdout) proc.stdout.on('data', resetTimeout);
666
+ if (proc.stderr) proc.stderr.on('data', resetTimeout);
667
+
660
668
  proc.stdout.on('data', (chunk) => {
661
669
  for (const ev of parser.push(chunk)) {
662
670
  pending = pending.then(() => handle(ev)).catch(() => {});
@@ -27,8 +27,10 @@ const { whichBinary, whereBinary, getEnhancedEnv } = require('../paths');
27
27
 
28
28
  const IS_WINDOWS = process.platform === 'win32';
29
29
 
30
- // Max wall-clock for a single `opencode run`. When it fires, Node kills the
31
- // child with SIGTERM, the 'close' handler sees signal='SIGTERM' / code=null.
30
+ // Max time an `opencode run` may stay completely silent. This is deliberately
31
+ // an IDLE timeout, not a wall-clock cap: long coding tasks routinely exceed
32
+ // five minutes while continuing to emit text/tool events. A fixed spawn
33
+ // timeout killed those healthy runs midway through their answer.
32
34
  const TIMEOUT_MS = 300000; // 5 minutes
33
35
 
34
36
  // Pinned OpenCode CLI version. Do NOT use @latest anywhere (registry.json /
@@ -158,6 +160,34 @@ class OpenCodeAdapter extends BaseAdapter {
158
160
  }
159
161
 
160
162
  /** Environment the CLI is spawned with. */
163
+
164
+ /** Test seam for the inactivity watchdog. */
165
+ _runTimeoutMs() {
166
+ return TIMEOUT_MS;
167
+ }
168
+
169
+ /**
170
+ * Arm a watchdog that expires only after a continuous period without CLI
171
+ * output. Every stdout/stderr chunk proves the process is still making
172
+ * progress and pushes the deadline out again.
173
+ */
174
+ _createIdleWatchdog(onTimeout) {
175
+ const timeoutMs = this._runTimeoutMs();
176
+ let timer = null;
177
+ let stopped = false;
178
+ const touch = () => {
179
+ if (stopped) return;
180
+ if (timer) clearTimeout(timer);
181
+ timer = setTimeout(onTimeout, timeoutMs);
182
+ };
183
+ const cancel = () => {
184
+ stopped = true;
185
+ if (timer) clearTimeout(timer);
186
+ timer = null;
187
+ };
188
+ touch();
189
+ return { touch, cancel, timeoutMs };
190
+ }
161
191
  _spawnEnv() {
162
192
  return { ...(this.agentEnv || process.env) };
163
193
  }
@@ -701,8 +731,8 @@ class OpenCodeAdapter extends BaseAdapter {
701
731
  * CRITICAL: opencode-ai does NOT read the OPENCODE_MODEL env var. When no
702
732
  * model is given on the command line AND none is set in opencode.json, the
703
733
  * non-interactive `run` command hangs forever waiting for interactive
704
- * provider/model selection — it emits zero output until the spawn timeout
705
- * kills it with SIGTERM (surfaced as the misleading "exited with code null").
734
+ * provider/model selection — it emits zero output until the inactivity
735
+ * watchdog kills it with SIGTERM.
706
736
  * Passing `--model` explicitly is what makes headless runs actually work.
707
737
  */
708
738
  _resolveModel() {
@@ -843,7 +873,6 @@ class OpenCodeAdapter extends BaseAdapter {
843
873
  stdio: ['pipe', 'pipe', 'pipe'],
844
874
  env: spawnEnv,
845
875
  cwd: runCwd,
846
- timeout: TIMEOUT_MS,
847
876
  detached: !IS_WINDOWS,
848
877
  // Never let a console window appear. Besides being ugly, an attached
849
878
  // console makes opencode start its interactive TUI and hang instead of
@@ -859,9 +888,16 @@ class OpenCodeAdapter extends BaseAdapter {
859
888
  let pendingEvents = Promise.resolve();
860
889
  const responseState = { finalText: '', allText: '', seenText: false };
861
890
  let settled = false;
891
+ let timedOut = false;
892
+ const watchdog = this._createIdleWatchdog(() => {
893
+ timedOut = true;
894
+ this._log(`opencode idle for ${this._runTimeoutMs() / 1000}s; terminating`);
895
+ this._stopProcess(proc).catch(() => {});
896
+ });
862
897
  const finish = (fn, arg) => {
863
898
  if (settled) return;
864
899
  settled = true;
900
+ watchdog.cancel();
865
901
  if (this._channelProcesses[msgChannel] === proc) {
866
902
  delete this._channelProcesses[msgChannel];
867
903
  }
@@ -870,6 +906,7 @@ class OpenCodeAdapter extends BaseAdapter {
870
906
 
871
907
  if (proc.stdout) {
872
908
  proc.stdout.on('data', (d) => {
909
+ watchdog.touch();
873
910
  const chunk = d.toString('utf-8');
874
911
  stdout += chunk;
875
912
  streamBuffer += chunk;
@@ -882,7 +919,10 @@ class OpenCodeAdapter extends BaseAdapter {
882
919
  }
883
920
  });
884
921
  }
885
- if (proc.stderr) proc.stderr.on('data', (d) => { stderr += d; });
922
+ if (proc.stderr) proc.stderr.on('data', (d) => {
923
+ watchdog.touch();
924
+ stderr += d;
925
+ });
886
926
 
887
927
  // Send the prompt via stdin, then close it so opencode knows the message
888
928
  // is complete and never blocks waiting for more input. Guard the write:
@@ -897,9 +937,8 @@ class OpenCodeAdapter extends BaseAdapter {
897
937
 
898
938
  proc.on('error', (err) => finish(reject, err));
899
939
  // 'close' (not 'exit') fires after stdout/stderr have fully drained, so we
900
- // never parse a truncated response. The handler receives (code, signal):
901
- // when the spawn timeout kills the child, code is null and signal is the
902
- // signal name — report that as a timeout rather than "exited with code null".
940
+ // never parse a truncated response. The explicit `timedOut` flag keeps a
941
+ // watchdog termination distinct from an external signal or crash.
903
942
  proc.on('close', async (code, signal) => {
904
943
  try { await pendingEvents; } catch {}
905
944
 
@@ -917,14 +956,18 @@ class OpenCodeAdapter extends BaseAdapter {
917
956
  // because stderr happened to be empty.
918
957
  const stdoutErr = OpenCodeAdapter._extractErrorFromStdout(stdout);
919
958
 
920
- if (signal) {
921
- this._log(`opencode killed by signal ${signal} after ${TIMEOUT_MS / 1000}s (output: ${!!stdout})`);
959
+ if (timedOut) {
960
+ this._log(`opencode timed out after ${watchdog.timeoutMs / 1000}s idle (output: ${!!stdout})`);
922
961
  const cls = this.constructor._classifyFailure({ code, signal, stdout, stderr, stdoutErr });
923
- // A signal almost always means our own timeout fired; only override to
924
- // a more specific provider error if stdout/stderr clearly show one.
925
962
  const category = (cls.category === 'unknown_error' || cls.category === 'process_crashed')
926
963
  ? 'timeout' : cls.category;
927
- return finish(reject, this._failure(category, cls.diagnostic || `signal=${signal}`, cls.detail));
964
+ return finish(reject, this._failure(category, cls.diagnostic || `idle=${watchdog.timeoutMs}ms`, cls.detail));
965
+ }
966
+
967
+ if (signal) {
968
+ this._log(`opencode killed by signal ${signal} (output: ${!!stdout})`);
969
+ const cls = this.constructor._classifyFailure({ code, signal, stdout, stderr, stdoutErr });
970
+ return finish(reject, this._failure(cls.category, cls.diagnostic || `signal=${signal}`, cls.detail));
928
971
  }
929
972
 
930
973
  if (code !== 0) {
@@ -98,7 +98,6 @@ const SERVER_READY_TIMEOUT_MS = 90000;
98
98
  const HEALTH_POLL_INTERVAL_MS = 400;
99
99
 
100
100
  // Max wall-clock for one turn, after which we interrupt and report it.
101
- const TURN_TIMEOUT_MS = 900000; // 15 minutes
102
101
 
103
102
  // Idle watchdog over the event stream, in the same shape the CLI adapters use.
104
103
  const WATCHDOG_INTERVAL_MS = 15000;
@@ -674,7 +673,6 @@ class OpenWorkerAdapter extends BaseAdapter {
674
673
  if (settled) return;
675
674
  settled = true;
676
675
  clearInterval(watchdog);
677
- clearTimeout(timeout);
678
676
  if (this._channelSockets[channel] === socket) delete this._channelSockets[channel];
679
677
  try { socket.close(); } catch {}
680
678
  const userStopped = this._stoppingChannels.has(channel);
@@ -821,12 +819,6 @@ class OpenWorkerAdapter extends BaseAdapter {
821
819
  }
822
820
  }, WATCHDOG_INTERVAL_MS);
823
821
 
824
- const timeout = setTimeout(() => {
825
- this._log(`Turn exceeded ${TURN_TIMEOUT_MS}ms — interrupting`);
826
- send({ type: 'interrupt' });
827
- if (!error) error = 'The turn ran past its time limit and was stopped.';
828
- finish({});
829
- }, TURN_TIMEOUT_MS);
830
822
  });
831
823
  }
832
824