@pwrdrvr/agent-acp 0.13.1 → 0.15.0

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/index.d.ts CHANGED
@@ -27,6 +27,10 @@ interface AcpAgentConnection {
27
27
  request(method: string, params: Record<string, unknown>): Promise<unknown>;
28
28
  notify(method: string, params: Record<string, unknown>): Promise<void>;
29
29
  }
30
+ /** Prefix on the rejection when a request outlived its budget. Lets a caller
31
+ * tell "the agent did not answer in time" apart from "the agent answered with
32
+ * an error" — the two want different recovery (retry / restart vs. surface). */
33
+ declare const ACP_REQUEST_TIMEOUT_MESSAGE_PREFIX = "acp request timed out";
30
34
  /** Built connection + a teardown handle (kills the child / closes streams). */
31
35
  type AcpAgentConnectionHandle = {
32
36
  connection: AcpAgentConnection;
@@ -55,7 +59,8 @@ declare class AcpConnection implements AcpJsonRpcTransport {
55
59
  private ensureConnected;
56
60
  private connect;
57
61
  private defaultCreateConnection;
58
- request(method: string, params?: Record<string, unknown>, _timeoutMs?: number): Promise<unknown>;
62
+ request(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
63
+ private dispatch;
59
64
  notify(method: string, params?: Record<string, unknown>): Promise<void>;
60
65
  close(): Promise<void>;
61
66
  onNotification(listener: (method: string, params: Record<string, unknown>) => void): () => void;
@@ -66,7 +71,14 @@ type LocalAcpProbeResult = {
66
71
  stdout?: string | Buffer;
67
72
  stderr?: string | Buffer;
68
73
  };
69
- type LocalAcpAgentProbe = (command: string, args: string[]) => Promise<LocalAcpProbeResult>;
74
+ type LocalAcpProbeOptions = {
75
+ /** Budget for this probe spawn. An injected probe should honor it so a slow
76
+ * CLI is reported as "did not answer in time", not as "not installed". */
77
+ timeoutMs?: number | undefined;
78
+ /** Fires when discovery is abandoned mid-probe. */
79
+ signal?: AbortSignal | undefined;
80
+ };
81
+ type LocalAcpAgentProbe = (command: string, args: string[], options?: LocalAcpProbeOptions) => Promise<LocalAcpProbeResult>;
70
82
  /** How a strategy decides it is installed + ACP-capable on this machine. */
71
83
  type AcpDiscoveryProbe = {
72
84
  /** Args that print a version (probed first; failure = not installed). */
@@ -700,6 +712,18 @@ type DiscoveredAcpAgentInstance = {
700
712
  /** Where this candidate came from (user override, `PATH`, or a fallback path). */
701
713
  source: AcpAgentInstanceSource;
702
714
  };
715
+ /** A detected executable that did not satisfy a strategy's discovery probe.
716
+ * It is diagnostic-only: hosts must never use it to launch an ACP session. */
717
+ type RejectedAcpAgentInstance = {
718
+ command: string;
719
+ /** Parsed from successful `--version` output when available. */
720
+ version?: string;
721
+ source: AcpAgentInstanceSource;
722
+ /** `probe-timed-out` means the CLI did not answer within the probe budget —
723
+ * UNLIKE the other reasons it is not a verdict on the CLI, and a host may
724
+ * re-probe on a larger `probeTimeoutMs` instead of reporting it unusable. */
725
+ reason: "version-probe-failed" | "acp-probe-failed" | "acp-help-mismatch" | "probe-timed-out";
726
+ };
703
727
  /** Every installed instance of one agent found on this machine. */
704
728
  type DiscoveredAcpAgentGroup = {
705
729
  strategyId: string;
@@ -711,6 +735,9 @@ type DiscoveredAcpAgentGroup = {
711
735
  env: Record<string, string>;
712
736
  /** All instances that passed the probe, in candidate order (override → PATH → fallback). */
713
737
  instances: DiscoveredAcpAgentInstance[];
738
+ /** Detected candidates that failed the version or ACP-capability probe. Only
739
+ * populated when `includeRejectedCandidates` is enabled. */
740
+ rejectedInstances?: RejectedAcpAgentInstance[];
714
741
  discoveredAt: number;
715
742
  };
716
743
  type DiscoveredAcpAgent = {
@@ -740,11 +767,31 @@ type LocalAcpDiscoveryOptions = {
740
767
  env?: NodeJS.ProcessEnv;
741
768
  /** Injectable `PATH` executable lister (real-fs scan by default). */
742
769
  listExecutables?: AcpPathExecutableLister;
770
+ /** Include candidates known to exist but rejected by a discovery probe.
771
+ * Defaults false to preserve the legacy installed-only result. */
772
+ includeRejectedCandidates?: boolean;
773
+ /** Budget for ONE probe spawn (the default probe only — an injected `probe`
774
+ * owns its own timing). Defaults to `DEFAULT_ACP_PROBE_TIMEOUT_MS`. */
775
+ probeTimeoutMs?: number;
776
+ /** Abandon discovery in flight (re-triggered discovery, app quitting).
777
+ * Already-probed groups are still returned. */
778
+ signal?: AbortSignal;
743
779
  };
780
+ /**
781
+ * Budget for one ACP discovery probe spawn, in ms.
782
+ *
783
+ * Unchanged from the value this module has always used. Kept lower than
784
+ * codex-discovery's version-probe budget on purpose: `discoverStrategyInstances`
785
+ * walks its candidates SERIALLY, so the worst case here is (candidates x
786
+ * budget) rather than a single budget. Hosts that scan machines with many
787
+ * installed copies can raise it knowingly via `probeTimeoutMs`.
788
+ */
789
+ declare const DEFAULT_ACP_PROBE_TIMEOUT_MS = 5000;
744
790
  /**
745
791
  * Discover every installed instance of each agent (all `PATH` matches +
746
792
  * fallbacks + override that pass the probe), grouped per strategy. Groups with
747
- * no passing instance are omitted.
793
+ * no passing instance are omitted unless `includeRejectedCandidates` is true
794
+ * and a detected candidate failed a probe.
748
795
  */
749
796
  declare function discoverLocalAcpAgentInstances(options?: LocalAcpDiscoveryOptions): Promise<DiscoveredAcpAgentGroup[]>;
750
797
  /**
@@ -874,4 +921,4 @@ declare class AcpRegistryService {
874
921
  declare function evaluateAcpDistributionPolicy(distribution: AcpRegistryDistribution, allowlist: AcpAllowlistDecision): AcpDistributionPolicy;
875
922
  declare function normalizeRegistry(raw: unknown): AcpRegistryAgent[];
876
923
 
877
- export { ACP_REGISTRY_URL, AcpAgentAllowlist, type AcpAgentAllowlistRule, AcpAgentClient, type AcpAgentClientFactory, type AcpAgentClientOptions, AcpAgentClientPool, type AcpAgentClientPoolOptions, type AcpAgentConnection, type AcpAgentConnectionHandle, type AcpAgentInstanceSource, type AcpAgentQuirks, type AcpAgentStrategy, type AcpAllowlistDecision, type AcpApplyContext, type AcpBinaryPlatformDistribution, AcpConnection, type AcpConnectionOptions, type AcpDiscoveryProbe, type AcpDistributionKind, type AcpDistributionPolicy, type AcpHttpMcpServerConfig, type AcpJsonRpcTransport, type AcpLoadThreadOptions, type AcpMcpKeyValue, type AcpMcpKeyValueCollection, type AcpMcpServerConfig, type AcpMcpServerWireConfig, type AcpNormalizeResult, type AcpNormalizerOptions, AcpOneShotClient, type AcpOneShotClientOptions, type AcpOneShotRequest, type AcpOneShotResponse, type AcpPackageDistribution, type AcpPathExecutableLister, type AcpPromptContentBlock, type AcpRegistryAgent, type AcpRegistryAgentWithPolicy, type AcpRegistryAuthDescriptor, type AcpRegistryAuthMethod, type AcpRegistryDistribution, type AcpRegistryFetch, AcpRegistryService, type AcpRegistryServiceOptions, type AcpRegistrySnapshot, type AcpRuntimeAgentCapabilities, type AcpRuntimeAgentInfo, type AcpRuntimeCapabilities, type AcpRuntimeCapabilitiesHandler, type AcpRuntimeCapabilitiesSource, type AcpRuntimeConfigOption, type AcpRuntimeConfigOptionValue, type AcpRuntimeMcpCapabilities, type AcpRuntimeMode, type AcpRuntimeModel, type AcpRuntimeModels, type AcpRuntimeModes, type AcpRuntimeOptionSource, AcpSessionNormalizer, type AcpSessionRuntimeState, type AcpSpawnSpec, type AcpSseMcpServerConfig, type AcpStartThreadOptions, type AcpStartTurnOptions, type AcpStdioMcpServerConfig, type AcpTitleHandler, type AcpVerificationStatus, BANNED_ACP_REGISTRY_IDS, BUILT_IN_ACP_STRATEGIES, DEFAULT_ACP_AGENT_ALLOWLIST, type DiscoveredAcpAgent, type DiscoveredAcpAgentGroup, type DiscoveredAcpAgentInstance, type LocalAcpAgentProbe, type LocalAcpDiscoveryOptions, type LocalAcpProbeResult, acpRuntimeSupportsHttpMcp, acpRuntimeSupportsSessionLoad, acpRuntimeSupportsSseMcp, acpSessionRuntimeStateFromCapabilities, acpSessionRuntimeStateFromUpdate, asRecord, buildAcpBackendId, buildStrategyTable, defaultAcpAgentAllowlist, defaultQuirks, discoverLocalAcpAgentInstances, discoverLocalAcpAgents, evaluateAcpDistributionPolicy, geminiStrategy, grokStrategy, isBannedAcpRegistryId, kimiStrategy, mergeAcpRuntimeState, modeLabelFor, modelConfigOption, modelIdFromCapabilities, modelsFromCapabilities, normalizeAcpMcpServerConfig, normalizeAcpMcpServerConfigs, normalizeAcpRuntimeCapabilities, normalizeRegistry, qwenStrategy, readAcpContentText, readBoolean, readContentText, readFirstLocationPath, readFirstString, readKind, readNonEmptyString, readNumber, readPromptText, readString, readToolCallId, readToolContentText, readToolOutput, readUpdateText, reasoningValueForThoughtLevel, strategyByBackendId, strategyById, toolCallFromUpdate, wellKnownAgentBinDirs };
924
+ export { ACP_REGISTRY_URL, ACP_REQUEST_TIMEOUT_MESSAGE_PREFIX, AcpAgentAllowlist, type AcpAgentAllowlistRule, AcpAgentClient, type AcpAgentClientFactory, type AcpAgentClientOptions, AcpAgentClientPool, type AcpAgentClientPoolOptions, type AcpAgentConnection, type AcpAgentConnectionHandle, type AcpAgentInstanceSource, type AcpAgentQuirks, type AcpAgentStrategy, type AcpAllowlistDecision, type AcpApplyContext, type AcpBinaryPlatformDistribution, AcpConnection, type AcpConnectionOptions, type AcpDiscoveryProbe, type AcpDistributionKind, type AcpDistributionPolicy, type AcpHttpMcpServerConfig, type AcpJsonRpcTransport, type AcpLoadThreadOptions, type AcpMcpKeyValue, type AcpMcpKeyValueCollection, type AcpMcpServerConfig, type AcpMcpServerWireConfig, type AcpNormalizeResult, type AcpNormalizerOptions, AcpOneShotClient, type AcpOneShotClientOptions, type AcpOneShotRequest, type AcpOneShotResponse, type AcpPackageDistribution, type AcpPathExecutableLister, type AcpPromptContentBlock, type AcpRegistryAgent, type AcpRegistryAgentWithPolicy, type AcpRegistryAuthDescriptor, type AcpRegistryAuthMethod, type AcpRegistryDistribution, type AcpRegistryFetch, AcpRegistryService, type AcpRegistryServiceOptions, type AcpRegistrySnapshot, type AcpRuntimeAgentCapabilities, type AcpRuntimeAgentInfo, type AcpRuntimeCapabilities, type AcpRuntimeCapabilitiesHandler, type AcpRuntimeCapabilitiesSource, type AcpRuntimeConfigOption, type AcpRuntimeConfigOptionValue, type AcpRuntimeMcpCapabilities, type AcpRuntimeMode, type AcpRuntimeModel, type AcpRuntimeModels, type AcpRuntimeModes, type AcpRuntimeOptionSource, AcpSessionNormalizer, type AcpSessionRuntimeState, type AcpSpawnSpec, type AcpSseMcpServerConfig, type AcpStartThreadOptions, type AcpStartTurnOptions, type AcpStdioMcpServerConfig, type AcpTitleHandler, type AcpVerificationStatus, BANNED_ACP_REGISTRY_IDS, BUILT_IN_ACP_STRATEGIES, DEFAULT_ACP_AGENT_ALLOWLIST, DEFAULT_ACP_PROBE_TIMEOUT_MS, type DiscoveredAcpAgent, type DiscoveredAcpAgentGroup, type DiscoveredAcpAgentInstance, type LocalAcpAgentProbe, type LocalAcpDiscoveryOptions, type LocalAcpProbeOptions, type LocalAcpProbeResult, type RejectedAcpAgentInstance, acpRuntimeSupportsHttpMcp, acpRuntimeSupportsSessionLoad, acpRuntimeSupportsSseMcp, acpSessionRuntimeStateFromCapabilities, acpSessionRuntimeStateFromUpdate, asRecord, buildAcpBackendId, buildStrategyTable, defaultAcpAgentAllowlist, defaultQuirks, discoverLocalAcpAgentInstances, discoverLocalAcpAgents, evaluateAcpDistributionPolicy, geminiStrategy, grokStrategy, isBannedAcpRegistryId, kimiStrategy, mergeAcpRuntimeState, modeLabelFor, modelConfigOption, modelIdFromCapabilities, modelsFromCapabilities, normalizeAcpMcpServerConfig, normalizeAcpMcpServerConfigs, normalizeAcpRuntimeCapabilities, normalizeRegistry, qwenStrategy, readAcpContentText, readBoolean, readContentText, readFirstLocationPath, readFirstString, readKind, readNonEmptyString, readNumber, readPromptText, readString, readToolCallId, readToolContentText, readToolOutput, readUpdateText, reasoningValueForThoughtLevel, strategyByBackendId, strategyById, toolCallFromUpdate, wellKnownAgentBinDirs };
package/dist/index.js CHANGED
@@ -10,6 +10,27 @@ import {
10
10
  createCommandInvocation,
11
11
  prependCommandDirToPath
12
12
  } from "@pwrdrvr/agent-transport";
13
+ var ACP_REQUEST_TIMEOUT_MESSAGE_PREFIX = "acp request timed out";
14
+ function withAcpRequestTimeout(method, timeoutMs, run) {
15
+ if (timeoutMs === void 0 || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
16
+ return run();
17
+ }
18
+ const budget = Math.max(1, Math.floor(timeoutMs));
19
+ const call = run();
20
+ void call.catch(() => void 0);
21
+ let timer;
22
+ const expiry = new Promise((_, reject) => {
23
+ timer = setTimeout(
24
+ () => reject(
25
+ new Error(`${ACP_REQUEST_TIMEOUT_MESSAGE_PREFIX} after ${budget}ms: ${method}`)
26
+ ),
27
+ budget
28
+ );
29
+ });
30
+ return Promise.race([call, expiry]).finally(() => {
31
+ if (timer !== void 0) clearTimeout(timer);
32
+ });
33
+ }
13
34
  var AcpConnection = class {
14
35
  constructor(options) {
15
36
  this.options = options;
@@ -101,10 +122,17 @@ var AcpConnection = class {
101
122
  };
102
123
  };
103
124
  }
104
- async request(method, params = {}, _timeoutMs) {
125
+ async request(method, params = {}, timeoutMs) {
105
126
  await this.ensureConnected();
106
127
  const conn = this.connection;
107
128
  if (!conn) throw new Error("acp connection unavailable");
129
+ return await withAcpRequestTimeout(
130
+ method,
131
+ timeoutMs,
132
+ () => this.dispatch(conn, method, params)
133
+ );
134
+ }
135
+ async dispatch(conn, method, params) {
108
136
  switch (method) {
109
137
  case "initialize":
110
138
  return await conn.initialize(params);
@@ -1436,6 +1464,8 @@ var AcpAgentClient = class {
1436
1464
  }
1437
1465
  async initialize() {
1438
1466
  if (this.initialized) return;
1467
+ this.unsubscribeNotification?.();
1468
+ this.unsubscribeRequest?.();
1439
1469
  this.unsubscribeNotification = this.transport.onNotification((method, params) => {
1440
1470
  this.handleNotification(method, params);
1441
1471
  });
@@ -2314,12 +2344,17 @@ import {
2314
2344
  prependCommandDirToPath as prependCommandDirToPath2
2315
2345
  } from "@pwrdrvr/agent-transport";
2316
2346
  var execFile = promisify(execFileCallback);
2347
+ var DEFAULT_ACP_PROBE_TIMEOUT_MS = 5e3;
2317
2348
  async function discoverLocalAcpAgentInstances(options = {}) {
2318
2349
  const strategies = options.strategies ?? BUILT_IN_ACP_STRATEGIES;
2319
2350
  const env = options.env ?? process.env;
2320
2351
  const probe = options.probe ?? makeDefaultProbe(env);
2321
2352
  const listExecutables = options.listExecutables ?? defaultListExecutables;
2322
2353
  const now = options.now ?? Date.now;
2354
+ const probeOptions = {
2355
+ timeoutMs: options.probeTimeoutMs ?? DEFAULT_ACP_PROBE_TIMEOUT_MS,
2356
+ signal: options.signal
2357
+ };
2323
2358
  const groups = await Promise.all(
2324
2359
  strategies.map(
2325
2360
  (strategy) => discoverStrategyInstances(
@@ -2328,7 +2363,9 @@ async function discoverLocalAcpAgentInstances(options = {}) {
2328
2363
  now,
2329
2364
  env,
2330
2365
  listExecutables,
2331
- options.overrides?.[strategy.id]
2366
+ options.overrides?.[strategy.id],
2367
+ options.includeRejectedCandidates === true,
2368
+ probeOptions
2332
2369
  )
2333
2370
  )
2334
2371
  );
@@ -2352,21 +2389,45 @@ async function discoverLocalAcpAgents(options = {}) {
2352
2389
  return [agent];
2353
2390
  });
2354
2391
  }
2355
- async function discoverStrategyInstances(strategy, probe, now, env, listExecutables, override) {
2392
+ async function discoverStrategyInstances(strategy, probe, now, env, listExecutables, override, includeRejectedCandidates = false, probeOptions = {}) {
2356
2393
  const candidates = candidateCommands(strategy, env, listExecutables, override);
2357
2394
  const instances = [];
2395
+ const rejectedInstances = [];
2358
2396
  for (const candidate of candidates) {
2359
- const [versionResult, helpResult] = await Promise.all([
2360
- runProbe(probe, candidate.command, strategy.discoveryProbe.versionArgs),
2361
- runProbe(probe, candidate.command, strategy.discoveryProbe.helpArgs)
2397
+ if (probeOptions.signal?.aborted) break;
2398
+ const [versionAttempt, helpAttempt] = await Promise.all([
2399
+ runProbe(probe, candidate.command, strategy.discoveryProbe.versionArgs, probeOptions),
2400
+ runProbe(probe, candidate.command, strategy.discoveryProbe.helpArgs, probeOptions)
2362
2401
  ]);
2402
+ if (probeOptions.signal?.aborted) break;
2403
+ const versionResult = versionAttempt.ok ? versionAttempt.result : void 0;
2404
+ const helpResult = helpAttempt.ok ? helpAttempt.result : void 0;
2405
+ const version = versionResult ? parseCliVersion(resultText(versionResult)) : void 0;
2363
2406
  if (!versionResult || !helpResult) {
2407
+ const timedOut = !versionAttempt.ok && versionAttempt.timedOut || !helpAttempt.ok && helpAttempt.timedOut;
2408
+ if (candidate.detected && (timedOut || includeRejectedCandidates)) {
2409
+ const rejected = {
2410
+ command: candidate.command,
2411
+ source: candidate.source,
2412
+ reason: timedOut ? "probe-timed-out" : versionResult ? "acp-probe-failed" : "version-probe-failed"
2413
+ };
2414
+ if (version !== void 0) rejected.version = version;
2415
+ rejectedInstances.push(rejected);
2416
+ }
2364
2417
  continue;
2365
2418
  }
2366
2419
  if (strategy.discoveryProbe.helpMatches !== void 0 && !strategy.discoveryProbe.helpMatches.test(resultText(helpResult))) {
2420
+ if (includeRejectedCandidates && candidate.detected) {
2421
+ const rejected = {
2422
+ command: candidate.command,
2423
+ source: candidate.source,
2424
+ reason: "acp-help-mismatch"
2425
+ };
2426
+ if (version !== void 0) rejected.version = version;
2427
+ rejectedInstances.push(rejected);
2428
+ }
2367
2429
  continue;
2368
2430
  }
2369
- const version = parseCliVersion(resultText(versionResult));
2370
2431
  const instance = {
2371
2432
  command: candidate.command,
2372
2433
  source: candidate.source
@@ -2374,7 +2435,7 @@ async function discoverStrategyInstances(strategy, probe, now, env, listExecutab
2374
2435
  if (version !== void 0) instance.version = version;
2375
2436
  instances.push(instance);
2376
2437
  }
2377
- if (instances.length === 0) {
2438
+ if (instances.length === 0 && rejectedInstances.length === 0) {
2378
2439
  return void 0;
2379
2440
  }
2380
2441
  return {
@@ -2384,34 +2445,43 @@ async function discoverStrategyInstances(strategy, probe, now, env, listExecutab
2384
2445
  args: ensureArgs(strategy.spawn.args, strategy.spawn.ensureArgs),
2385
2446
  env: strategy.spawn.env ?? {},
2386
2447
  instances,
2448
+ ...rejectedInstances.length > 0 ? { rejectedInstances } : {},
2387
2449
  discoveredAt: now()
2388
2450
  };
2389
2451
  }
2390
2452
  function candidateCommands(strategy, env, listExecutables, override) {
2391
2453
  const out = [];
2392
2454
  const seen = /* @__PURE__ */ new Set();
2393
- const push = (command, source) => {
2455
+ const push = (command, source, detected) => {
2394
2456
  const trimmed = command.trim();
2395
2457
  if (trimmed.length === 0) return;
2396
2458
  const key = canonicalize(trimmed);
2397
2459
  if (seen.has(key)) return;
2398
2460
  seen.add(key);
2399
- out.push({ command: trimmed, source });
2461
+ out.push({ command: trimmed, source, detected });
2400
2462
  };
2401
2463
  if (override && override.trim().length > 0) {
2402
- push(override, "override");
2464
+ push(override, "override", isExecutableFile(override));
2403
2465
  }
2404
2466
  const pathMatches = listExecutables(strategy.discoveryProbe.command, env);
2405
2467
  if (pathMatches.length > 0) {
2406
- for (const match of pathMatches) push(match, "path");
2468
+ for (const match of pathMatches) push(match, "path", true);
2407
2469
  } else {
2408
- push(strategy.discoveryProbe.command, "path");
2470
+ push(strategy.discoveryProbe.command, "path", false);
2409
2471
  }
2410
2472
  for (const fallback of strategy.discoveryProbe.fallbackCommands ?? []) {
2411
- push(fallback, "fallback");
2473
+ push(fallback, "fallback", isExecutableFile(fallback));
2412
2474
  }
2413
2475
  return out;
2414
2476
  }
2477
+ function isExecutableFile(command) {
2478
+ try {
2479
+ const stat = statSync(command);
2480
+ return stat.isFile() && (process.platform === "win32" || (stat.mode & 73) !== 0);
2481
+ } catch {
2482
+ return false;
2483
+ }
2484
+ }
2415
2485
  function canonicalize(command) {
2416
2486
  if (!path4.isAbsolute(command)) {
2417
2487
  return process.platform === "win32" ? command.toLowerCase() : command;
@@ -2502,25 +2572,78 @@ function ensureArgs(args, ensure) {
2502
2572
  }
2503
2573
  return result;
2504
2574
  }
2575
+ var AcpProbeTimeoutError = class extends Error {
2576
+ constructor(command, timeoutMs) {
2577
+ super(`acp probe did not answer within ${timeoutMs}ms: ${command}`);
2578
+ this.name = "AcpProbeTimeoutError";
2579
+ }
2580
+ };
2581
+ function isProbeTimeout(error) {
2582
+ if (error instanceof AcpProbeTimeoutError) return true;
2583
+ const name = error?.name;
2584
+ return error?.killed === true || name === "AbortError" || name === "TimeoutError";
2585
+ }
2586
+ function normalizeProbeTimeoutMs(timeoutMs) {
2587
+ if (timeoutMs === void 0 || !Number.isFinite(timeoutMs)) {
2588
+ return DEFAULT_ACP_PROBE_TIMEOUT_MS;
2589
+ }
2590
+ return Math.max(1, Math.floor(timeoutMs));
2591
+ }
2505
2592
  function makeDefaultProbe(env) {
2506
- return async (command, args) => {
2593
+ return async (command, args, options = {}) => {
2594
+ const timeoutMs = normalizeProbeTimeoutMs(options.timeoutMs);
2507
2595
  const launchEnv = prependCommandDirToPath2(command, env);
2508
2596
  const invocation = createCommandInvocation2({ command, args, env: launchEnv });
2509
- return await execFile(invocation.command, invocation.args, {
2510
- timeout: 5e3,
2511
- maxBuffer: 1024 * 1024,
2512
- // Prepend the candidate's own dir so a node-script CLI (e.g. an nvm-
2513
- // installed `qwen`) finds its sibling `node` during the probe.
2514
- env: launchEnv,
2515
- windowsVerbatimArguments: invocation.windowsVerbatimArguments
2597
+ const controller = new AbortController();
2598
+ let timedOut = false;
2599
+ const onAbort = () => controller.abort();
2600
+ options.signal?.addEventListener("abort", onAbort, { once: true });
2601
+ const timer = setTimeout(() => {
2602
+ timedOut = true;
2603
+ controller.abort();
2604
+ }, timeoutMs);
2605
+ const expired = new Promise((resolve) => {
2606
+ controller.signal.addEventListener("abort", () => resolve("expired"), {
2607
+ once: true
2608
+ });
2516
2609
  });
2610
+ try {
2611
+ const execution = execFile(invocation.command, invocation.args, {
2612
+ timeout: timeoutMs,
2613
+ signal: controller.signal,
2614
+ maxBuffer: 1024 * 1024,
2615
+ // A Windows `.cmd` shim runs through `cmd.exe`; without this every
2616
+ // discovery pass flashes console windows over the host app.
2617
+ windowsHide: true,
2618
+ // Prepend the candidate's own dir so a node-script CLI (e.g. an nvm-
2619
+ // installed `qwen`) finds its sibling `node` during the probe.
2620
+ env: launchEnv,
2621
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments
2622
+ }).then(
2623
+ (result) => ({ ok: true, result }),
2624
+ (error) => ({ ok: false, error })
2625
+ );
2626
+ const settled = await Promise.race([execution, expired]);
2627
+ if (settled === "expired") {
2628
+ if (timedOut) throw new AcpProbeTimeoutError(command, timeoutMs);
2629
+ throw new Error(`acp probe aborted: ${command}`);
2630
+ }
2631
+ if (!settled.ok) {
2632
+ if (timedOut) throw new AcpProbeTimeoutError(command, timeoutMs);
2633
+ throw settled.error;
2634
+ }
2635
+ return settled.result;
2636
+ } finally {
2637
+ clearTimeout(timer);
2638
+ options.signal?.removeEventListener("abort", onAbort);
2639
+ }
2517
2640
  };
2518
2641
  }
2519
- async function runProbe(probe, command, args) {
2642
+ async function runProbe(probe, command, args, options) {
2520
2643
  try {
2521
- return await probe(command, args);
2522
- } catch {
2523
- return void 0;
2644
+ return { ok: true, result: await probe(command, args, options) };
2645
+ } catch (error) {
2646
+ return { ok: false, timedOut: isProbeTimeout(error) };
2524
2647
  }
2525
2648
  }
2526
2649
  function resultText(result) {
@@ -2865,6 +2988,7 @@ function stringRecord(value) {
2865
2988
  }
2866
2989
  export {
2867
2990
  ACP_REGISTRY_URL,
2991
+ ACP_REQUEST_TIMEOUT_MESSAGE_PREFIX,
2868
2992
  AcpAgentAllowlist,
2869
2993
  AcpAgentClient,
2870
2994
  AcpAgentClientPool,
@@ -2875,6 +2999,7 @@ export {
2875
2999
  BANNED_ACP_REGISTRY_IDS,
2876
3000
  BUILT_IN_ACP_STRATEGIES,
2877
3001
  DEFAULT_ACP_AGENT_ALLOWLIST,
3002
+ DEFAULT_ACP_PROBE_TIMEOUT_MS,
2878
3003
  acpRuntimeSupportsHttpMcp,
2879
3004
  acpRuntimeSupportsSessionLoad,
2880
3005
  acpRuntimeSupportsSseMcp,