@buda-ai/connector 0.1.31 → 0.1.33

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.
@@ -47,7 +47,7 @@ Capture what matters. Decisions, context, things to remember. Skip the secrets u
47
47
 
48
48
  - **Memory is limited** — if you want to remember something, you MUST write it to a file using your tools (like `bash` or `write`).
49
49
  - "Mental notes" do not survive session restarts or context limits. Only files do.
50
- - When the user gives you a **new rule, a new preference, changes your identity/role, or tells you to "remember this"** → You **MUST IMMEDIATELY** use your tools to update `SOUL.md`, `AGENTS.md`, `USER.md` or `memory/YYYY-MM-DD.md`.
50
+ - When the user gives you a **new rule, a new preference, changes your identity/role, or tells you to "remember this"** → You **MUST IMMEDIATELY** use your tools to update `SOUL.md`, `AGENTS.md`, `MEMORY.md` or `memory/YYYY-MM-DD.md`.
51
51
  - DO NOT just reply "Okay, I will remember this." You must actually execute a file write operation to make it permanent.
52
52
  - When you learn a lesson → update `AGENTS.md`, `TOOLS.md`, or the relevant skill.
53
53
  - When you make a mistake → document it so future-you doesn't repeat it.
package/dist/cli.js CHANGED
@@ -25,8 +25,8 @@ async function acquireExclusiveFileLock(targetPath, options = {}) {
25
25
  } catch (error) {
26
26
  if (!isAlreadyExists(error)) throw error;
27
27
  if (await shouldRecoverLock(lockPath, staleMs)) {
28
- await unlink(lockPath).catch(() => void 0);
29
- continue;
28
+ const recovered = await unlink(lockPath).then(() => true).catch(() => false);
29
+ if (recovered) continue;
30
30
  }
31
31
  if (Date.now() >= deadline) {
32
32
  throw new Error(`Timed out waiting for exclusive access to ${targetPath}`);
@@ -35,17 +35,23 @@ async function acquireExclusiveFileLock(targetPath, options = {}) {
35
35
  continue;
36
36
  }
37
37
  const ownerToken = randomUUID();
38
- await handle.writeFile(
39
- `${JSON.stringify({ pid: process.pid, ownerToken, createdAt: Date.now() })}
38
+ try {
39
+ await handle.writeFile(
40
+ `${JSON.stringify({ pid: process.pid, ownerToken, createdAt: Date.now() })}
40
41
  `
41
- );
42
- await handle.sync();
42
+ );
43
+ await handle.sync();
44
+ } catch (error) {
45
+ await handle.close().catch(() => void 0);
46
+ await unlink(lockPath).catch(() => void 0);
47
+ throw error;
48
+ }
49
+ await handle.close();
43
50
  let released = false;
44
51
  return {
45
52
  release: async () => {
46
53
  if (released) return;
47
54
  released = true;
48
- await handle.close();
49
55
  const currentOwner = await readFile(lockPath, "utf8").then((raw) => JSON.parse(raw).ownerToken).catch(() => void 0);
50
56
  if (currentOwner === ownerToken) await unlink(lockPath).catch(() => void 0);
51
57
  }
@@ -517,7 +523,7 @@ import path2 from "path";
517
523
  // package.json
518
524
  var package_default = {
519
525
  name: "@buda-ai/connector",
520
- version: "0.1.31",
526
+ version: "0.1.33",
521
527
  private: false,
522
528
  type: "module",
523
529
  publishConfig: {
@@ -535,6 +541,7 @@ var package_default = {
535
541
  dev: "tsx src/cli.ts dev",
536
542
  daemon: "tsx src/cli.ts daemon",
537
543
  new: "tsx src/cli.ts new",
544
+ prepack: "tsup --silent",
538
545
  start: "node dist/cli.js daemon",
539
546
  build: "tsup",
540
547
  test: "vitest run",
@@ -543,7 +550,7 @@ var package_default = {
543
550
  "lint:err": "biome format . --write && biome check . --write --diagnostic-level=error"
544
551
  },
545
552
  dependencies: {
546
- "@bunny-agent/daemon": "0.9.51",
553
+ "@bunny-agent/daemon": "0.9.58",
547
554
  "@clack/prompts": "^1.0.0",
548
555
  "playwright-core": "^1.52.0",
549
556
  ws: "^8.21.0"
@@ -2208,6 +2215,13 @@ var ConnectorDeletedError = class extends Error {
2208
2215
  this.name = "ConnectorDeletedError";
2209
2216
  }
2210
2217
  };
2218
+ var ConnectorUnauthorizedError = class extends Error {
2219
+ constructor(message) {
2220
+ super(message);
2221
+ this.name = "ConnectorUnauthorizedError";
2222
+ }
2223
+ };
2224
+ var UNREGISTER_TIMEOUT_MS = 3e3;
2211
2225
  async function resolveConnectorScope(deviceId, launchConfig) {
2212
2226
  const serverUrl = normalizeServerOrigin(getServerUrl(launchConfig));
2213
2227
  const headers = { "Content-Type": "application/json" };
@@ -2294,14 +2308,15 @@ function buildAuthHeaders(config, launchConfig) {
2294
2308
  if (config.deviceToken) headers["X-Buda-Connector-Device-Token"] = config.deviceToken;
2295
2309
  return headers;
2296
2310
  }
2297
- async function postJson(serverUrl, pathname, body, config, launchConfig) {
2311
+ async function postJson(serverUrl, pathname, body, config, launchConfig, options = {}) {
2298
2312
  const response = await fetch(`${serverUrl}${pathname}`, {
2299
2313
  method: "POST",
2300
2314
  headers: {
2301
2315
  "Content-Type": "application/json",
2302
2316
  ...buildAuthHeaders(config, launchConfig)
2303
2317
  },
2304
- body: JSON.stringify(body)
2318
+ body: JSON.stringify(body),
2319
+ ...options.timeoutMs ? { signal: AbortSignal.timeout(options.timeoutMs) } : {}
2305
2320
  });
2306
2321
  if (!response.ok) {
2307
2322
  const text2 = await response.text().catch(() => "");
@@ -2321,6 +2336,9 @@ async function postJson(serverUrl, pathname, body, config, launchConfig) {
2321
2336
  if (errorCode === "connector_deleted") {
2322
2337
  throw new ConnectorDeletedError(errorMessage3 || "Connector no longer exists");
2323
2338
  }
2339
+ if (response.status === 401) {
2340
+ throw new ConnectorUnauthorizedError(errorMessage3 || "Connector authentication failed");
2341
+ }
2324
2342
  throw new Error(`${pathname} failed: ${response.status} ${errorMessage3}`);
2325
2343
  }
2326
2344
  return await response.json();
@@ -2396,7 +2414,8 @@ async function unregisterConnector(config, launchConfig) {
2396
2414
  connectorId: config.connectorId
2397
2415
  },
2398
2416
  config,
2399
- launchConfig
2417
+ launchConfig,
2418
+ { timeoutMs: UNREGISTER_TIMEOUT_MS }
2400
2419
  ).catch((error) => {
2401
2420
  void writeLog("warn", `unregister failed: ${error instanceof Error ? error.message : error}`);
2402
2421
  });
@@ -2405,32 +2424,117 @@ async function unregisterConnector(config, launchConfig) {
2405
2424
  // src/runtime-install.ts
2406
2425
  import { execFile } from "child_process";
2407
2426
  import { randomUUID as randomUUID4 } from "crypto";
2408
- import { access, cp, mkdir as mkdir5, readdir as readdir4, readFile as readFile5, rename as rename4, rm, writeFile as writeFile3 } from "fs/promises";
2427
+ import {
2428
+ access,
2429
+ cp,
2430
+ mkdir as mkdir5,
2431
+ readdir as readdir4,
2432
+ readFile as readFile5,
2433
+ realpath,
2434
+ rename as rename4,
2435
+ rm,
2436
+ writeFile as writeFile3
2437
+ } from "fs/promises";
2409
2438
  import { homedir as homedir3 } from "os";
2410
2439
  import path6 from "path";
2411
2440
  import { promisify } from "util";
2412
2441
  var execFileAsync = promisify(execFile);
2413
2442
  var RUNTIME_INSTALL_TIMEOUT_MS = 5 * 6e4;
2443
+ var WINDOWS_RENAME_RETRY_DELAYS_MS = [50, 100, 200, 400, 500, 500, 500, 500, 500];
2414
2444
  var MACOS_NODE_LIBRARY_PATTERN = /^libnode(?:\.\d+)*\.dylib$/;
2445
+ var getLoadedSharedObjects = () => {
2446
+ const report = process.report?.getReport();
2447
+ return Array.isArray(report?.sharedObjects) ? report.sharedObjects.filter((value) => typeof value === "string") : [];
2448
+ };
2449
+ var resolveBundledNodePlacement = (sourceRoot, sourceNode, pathApi = path6) => {
2450
+ const relativeNode = pathApi.relative(sourceRoot, sourceNode);
2451
+ return {
2452
+ copyNode: relativeNode.startsWith("..") || pathApi.isAbsolute(relativeNode),
2453
+ relativeNode
2454
+ };
2455
+ };
2415
2456
  var copyNodeRuntime = async (sourceNode, destinationNode, options = {}) => {
2416
- const sourceDirectory = path6.dirname(sourceNode);
2457
+ const resolvedSourceNode = await realpath(sourceNode);
2458
+ const sourceDirectory = path6.dirname(resolvedSourceNode);
2417
2459
  const destinationDirectory = path6.dirname(destinationNode);
2418
2460
  await mkdir5(destinationDirectory, { recursive: true });
2419
- await cp(sourceNode, destinationNode, { dereference: true, force: true });
2461
+ await cp(resolvedSourceNode, destinationNode, { dereference: true, force: true });
2420
2462
  if ((options.platform ?? process.platform) !== "darwin") return;
2421
- const companions = await readdir4(sourceDirectory, { withFileTypes: true });
2463
+ const launcherDirectory = path6.resolve(path6.dirname(sourceNode));
2464
+ const companionDirectories = [
2465
+ .../* @__PURE__ */ new Set([
2466
+ launcherDirectory,
2467
+ path6.resolve(launcherDirectory, "..", "lib"),
2468
+ sourceDirectory,
2469
+ path6.resolve(sourceDirectory, "..", "lib")
2470
+ ])
2471
+ ];
2472
+ const companions = (await Promise.all(
2473
+ companionDirectories.map(async (directory) => ({
2474
+ directory,
2475
+ entries: await readdir4(directory, { withFileTypes: true }).catch((error) => {
2476
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return [];
2477
+ throw error;
2478
+ })
2479
+ }))
2480
+ )).flatMap(({ directory, entries }) => entries.map((entry) => ({ directory, entry })));
2481
+ const companionFiles = /* @__PURE__ */ new Map();
2482
+ for (const library of options.loadedLibraries ?? getLoadedSharedObjects()) {
2483
+ const name = path6.basename(library);
2484
+ if (MACOS_NODE_LIBRARY_PATTERN.test(name)) companionFiles.set(name, library);
2485
+ }
2486
+ for (const { directory, entry } of companions) {
2487
+ if ((entry.isFile() || entry.isSymbolicLink()) && MACOS_NODE_LIBRARY_PATTERN.test(entry.name) && !companionFiles.has(entry.name)) {
2488
+ companionFiles.set(entry.name, path6.join(directory, entry.name));
2489
+ }
2490
+ }
2422
2491
  await Promise.all(
2423
- companions.filter(
2424
- (entry) => (entry.isFile() || entry.isSymbolicLink()) && MACOS_NODE_LIBRARY_PATTERN.test(entry.name)
2425
- ).map(
2426
- (entry) => cp(path6.join(sourceDirectory, entry.name), path6.join(destinationDirectory, entry.name), {
2492
+ [...companionFiles].map(
2493
+ ([name, source]) => cp(source, path6.join(destinationDirectory, name), {
2427
2494
  dereference: true,
2428
2495
  force: true
2429
2496
  })
2430
2497
  )
2431
2498
  );
2432
2499
  };
2500
+ var isWindowsRenameContention = (error) => error instanceof Error && "code" in error && ["EACCES", "EBUSY", "EPERM"].includes(String(error.code));
2501
+ var renameStagedRuntime = async (source, destination, options = {}) => {
2502
+ const platform4 = options.platform ?? process.platform;
2503
+ const renamePath = options.renamePath ?? rename4;
2504
+ const wait4 = options.wait ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
2505
+ const delays = platform4 === "win32" ? WINDOWS_RENAME_RETRY_DELAYS_MS : [];
2506
+ for (let attempt = 0; ; attempt += 1) {
2507
+ try {
2508
+ await renamePath(source, destination);
2509
+ return;
2510
+ } catch (error) {
2511
+ const delay = delays[attempt];
2512
+ if (delay === void 0 || !isWindowsRenameContention(error)) throw error;
2513
+ await wait4(delay);
2514
+ }
2515
+ }
2516
+ };
2433
2517
  var exists = async (filePath) => access(filePath).then(() => true).catch(() => false);
2518
+ var resolveNpmInvocation = async (options = {}) => {
2519
+ const platform4 = options.platform ?? process.platform;
2520
+ if (platform4 !== "win32") return { argsPrefix: [], executable: "npm" };
2521
+ const nodeExecutable = options.nodeExecutable ?? process.execPath;
2522
+ const pathApi = options.pathApi ?? path6;
2523
+ const pathExists = options.pathExists ?? exists;
2524
+ const candidates = [
2525
+ options.npmExecPath ?? process.env.npm_execpath,
2526
+ pathApi.join(pathApi.dirname(nodeExecutable), "node_modules", "npm", "bin", "npm-cli.js")
2527
+ ];
2528
+ for (const candidate of candidates) {
2529
+ if (!candidate || pathApi.basename(candidate).toLowerCase() !== "npm-cli.js") continue;
2530
+ if (await pathExists(candidate)) {
2531
+ return { argsPrefix: [candidate], executable: nodeExecutable };
2532
+ }
2533
+ }
2534
+ throw new Error(
2535
+ `Unable to locate npm-cli.js for the stable Connector runtime installation (node=${JSON.stringify(nodeExecutable)}, npm_execpath=${JSON.stringify(options.npmExecPath ?? process.env.npm_execpath ?? "")})`
2536
+ );
2537
+ };
2434
2538
  var runtimeRoot = () => process.env.BUDA_CONNECTOR_RUNTIME_ROOT || path6.join(homedir3(), ".buda", "runtime");
2435
2539
  var runtimePlatformKey = `${process.platform}-${process.arch}-node${process.versions.modules}`;
2436
2540
  var versionDirectory = () => path6.join(runtimeRoot(), runtimePlatformKey, CONNECTOR_RUNTIME_VERSION);
@@ -2464,9 +2568,11 @@ var validateRuntime = async (runtime) => {
2464
2568
  };
2465
2569
  var installFromNpm = async (directory) => {
2466
2570
  await mkdir5(directory, { recursive: true, mode: 448 });
2571
+ const npm = await resolveNpmInvocation();
2467
2572
  await execFileAsync(
2468
- process.platform === "win32" ? "npm.cmd" : "npm",
2573
+ npm.executable,
2469
2574
  [
2575
+ ...npm.argsPrefix,
2470
2576
  "install",
2471
2577
  "--ignore-scripts=false",
2472
2578
  "--no-audit",
@@ -2500,10 +2606,10 @@ var installFromBundledRuntime = async (directory, sourceRoot) => {
2500
2606
  if (relativeEntrypoint.startsWith("..") || path6.isAbsolute(relativeEntrypoint)) {
2501
2607
  throw new Error("Bundled Connector entrypoint must be inside its runtime root");
2502
2608
  }
2503
- const relativeNode = path6.relative(sourceRoot, sourceNode);
2609
+ const { copyNode, relativeNode } = resolveBundledNodePlacement(sourceRoot, sourceNode);
2504
2610
  const entrypoint = path6.join(directory, relativeEntrypoint);
2505
- const nodeExecutable = relativeNode.startsWith("..") ? path6.join(directory, "node", path6.basename(sourceNode)) : path6.join(directory, relativeNode);
2506
- if (relativeNode.startsWith("..")) {
2611
+ const nodeExecutable = copyNode ? path6.join(directory, "node", path6.basename(sourceNode)) : path6.join(directory, relativeNode);
2612
+ if (copyNode) {
2507
2613
  await copyNodeRuntime(sourceNode, nodeExecutable);
2508
2614
  }
2509
2615
  if (!await exists(entrypoint))
@@ -2550,7 +2656,7 @@ async function ensureStableConnectorRuntime() {
2550
2656
  };
2551
2657
  await writeManifest(temporary, rebased);
2552
2658
  await rm(finalDirectory, { recursive: true, force: true });
2553
- await rename4(temporary, finalDirectory);
2659
+ await renameStagedRuntime(temporary, finalDirectory);
2554
2660
  return rebased;
2555
2661
  } catch (error) {
2556
2662
  await rm(temporary, { recursive: true, force: true });
@@ -2564,6 +2670,8 @@ async function ensureStableConnectorRuntime() {
2564
2670
  // src/daemon-client.ts
2565
2671
  var START_TIMEOUT_MS = 45e3;
2566
2672
  var BOOTSTRAP_TIMEOUT_MS = 5 * 6e4;
2673
+ var GRACEFUL_STOP_TIMEOUT_MS = 3e3;
2674
+ var LOCK_ONLY_RECOVERY_MAX_AGE_MS = 5 * 6e4;
2567
2675
  var POLL_INTERVAL_MS = 250;
2568
2676
  var wait2 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
2569
2677
  var getConfigRoot = () => process.env.BUDA_CONNECTOR_CONFIG_ROOT;
@@ -2596,14 +2704,78 @@ var compareVersions = (left, right) => {
2596
2704
  return 0;
2597
2705
  };
2598
2706
  var shouldReplaceDaemonVersion = (runningVersion, candidateVersion) => compareVersions(candidateVersion, runningVersion) > 0;
2707
+ var shouldDisplayConnectorLogEntry = (createdAt, startedAt) => {
2708
+ const timestamp = Date.parse(createdAt);
2709
+ return Number.isNaN(timestamp) || timestamp >= startedAt;
2710
+ };
2711
+ var getErrorCode = (error) => typeof error === "object" && error !== null && "code" in error ? String(error.code) : "UNKNOWN";
2712
+ var waitForChildSpawn = (child) => new Promise((resolve, reject) => {
2713
+ const onError = (error) => {
2714
+ child.off("spawn", onSpawn);
2715
+ reject(error);
2716
+ };
2717
+ const onSpawn = () => {
2718
+ child.off("error", onError);
2719
+ resolve();
2720
+ };
2721
+ child.once("error", onError);
2722
+ child.once("spawn", onSpawn);
2723
+ });
2724
+ async function spawnDetachedDaemonProcess(command, args, options, dependencies = {}) {
2725
+ const platform4 = dependencies.platform ?? process.platform;
2726
+ const spawnProcess = dependencies.spawnProcess ?? ((file, fileArgs, spawnOptions) => spawn(file, fileArgs, spawnOptions));
2727
+ const strategies = platform4 === "win32" ? [
2728
+ { name: "detached-hidden", windowsHide: true },
2729
+ { name: "detached-unhidden", windowsHide: false }
2730
+ ] : [{ name: "detached-hidden", windowsHide: true }];
2731
+ const attempted = [];
2732
+ for (const [index, strategy] of strategies.entries()) {
2733
+ attempted.push(strategy.name);
2734
+ const child = spawnProcess(command, args, {
2735
+ detached: true,
2736
+ env: options.env,
2737
+ stdio: options.stdio,
2738
+ windowsHide: strategy.windowsHide
2739
+ });
2740
+ try {
2741
+ await waitForChildSpawn(child);
2742
+ if (!child.pid) {
2743
+ throw Object.assign(new Error("spawned Connector daemon did not expose a PID"), {
2744
+ code: "NO_PID"
2745
+ });
2746
+ }
2747
+ child.unref();
2748
+ return { pid: child.pid, strategy: strategy.name };
2749
+ } catch (error) {
2750
+ const code = getErrorCode(error);
2751
+ const shouldRetry = platform4 === "win32" && index === 0 && code === "EINVAL";
2752
+ if (shouldRetry) continue;
2753
+ const diagnostics = [
2754
+ `code=${code}`,
2755
+ `strategies=${attempted.join(",")}`,
2756
+ `platform=${platform4}`,
2757
+ `node=${dependencies.nodeVersion ?? process.version}`,
2758
+ `executable=${JSON.stringify(command)}`,
2759
+ `entrypoint=${JSON.stringify(args[0] ?? "")}`,
2760
+ `cwd=${JSON.stringify(dependencies.cwd ?? process.cwd())}`
2761
+ ].join(" ");
2762
+ throw new Error(`Connector daemon process creation failed: ${diagnostics}`, { cause: error });
2763
+ }
2764
+ }
2765
+ throw new Error("Connector daemon process creation failed without a spawn attempt");
2766
+ }
2767
+ var readReadyDaemon = async (launchConfig) => {
2768
+ const config = await readDeviceConfig(launchConfig, getConfigRoot());
2769
+ if (!config) return null;
2770
+ const status = await readLiveStatus(config);
2771
+ return status ? { config, status } : null;
2772
+ };
2599
2773
  var waitForDaemon = async (launchConfig, deadline) => {
2600
2774
  let lastConfig = null;
2601
2775
  while (Date.now() < deadline) {
2776
+ const ready = await readReadyDaemon(launchConfig);
2777
+ if (ready) return ready;
2602
2778
  lastConfig = await readDeviceConfig(launchConfig, getConfigRoot());
2603
- if (lastConfig) {
2604
- const status = await readLiveStatus(lastConfig);
2605
- if (status) return { config: lastConfig, status };
2606
- }
2607
2779
  await wait2(POLL_INTERVAL_MS);
2608
2780
  }
2609
2781
  throw new Error(
@@ -2612,17 +2784,126 @@ var waitForDaemon = async (launchConfig, deadline) => {
2612
2784
  };
2613
2785
  var spawnInternalDaemon = async (launchConfig, mode, preparedRuntime, options = {}) => {
2614
2786
  const runtime = preparedRuntime ?? await ensureStableConnectorRuntime();
2615
- const child = spawn(runtime.nodeExecutable, getRuntimeCommandArgs(runtime, mode, options), {
2616
- detached: true,
2617
- env: {
2618
- ...process.env,
2619
- BUDA_CONNECTOR_INTERNAL_LAUNCH_CONFIG: JSON.stringify(launchConfig ?? {})
2620
- },
2621
- stdio: "ignore",
2622
- windowsHide: true
2623
- });
2624
- child.unref();
2787
+ const debugDaemonStdio = process.env.BUDA_CONNECTOR_DEBUG_DAEMON_STDIO === "1";
2788
+ return spawnDetachedDaemonProcess(
2789
+ runtime.nodeExecutable,
2790
+ getRuntimeCommandArgs(runtime, mode, options),
2791
+ {
2792
+ env: {
2793
+ ...process.env,
2794
+ BUDA_CONNECTOR_INTERNAL_LAUNCH_CONFIG: JSON.stringify(launchConfig ?? {})
2795
+ },
2796
+ stdio: debugDaemonStdio ? ["ignore", "inherit", "inherit"] : "ignore"
2797
+ }
2798
+ );
2799
+ };
2800
+ var waitForProcessExit = async (pid, timeoutMs) => {
2801
+ const deadline = Date.now() + timeoutMs;
2802
+ while (Date.now() < deadline) {
2803
+ if (!isProcessAlive2(pid)) return true;
2804
+ await wait2(50);
2805
+ }
2806
+ return !isProcessAlive2(pid);
2807
+ };
2808
+ var signalProcess = (pid, signal) => {
2809
+ try {
2810
+ process.kill(pid, signal);
2811
+ return true;
2812
+ } catch (error) {
2813
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH") {
2814
+ return false;
2815
+ }
2816
+ throw error;
2817
+ }
2625
2818
  };
2819
+ var stopUnresponsiveDaemonProcess = async (pid) => {
2820
+ if (!isProcessAlive2(pid)) return;
2821
+ if (!signalProcess(pid, "SIGTERM")) return;
2822
+ if (await waitForProcessExit(pid, 2e3)) return;
2823
+ if (!signalProcess(pid, "SIGKILL")) return;
2824
+ if (await waitForProcessExit(pid, 3e3)) return;
2825
+ throw new Error(`Timed-out Connector daemon PID ${pid} could not be stopped safely`);
2826
+ };
2827
+ var waitForDaemonRelease = async (config, status, configRoot = getConfigRoot()) => {
2828
+ const deadline = Date.now() + GRACEFUL_STOP_TIMEOUT_MS;
2829
+ while (Date.now() < deadline) {
2830
+ if (!isProcessAlive2(status.pid)) return true;
2831
+ const current = await readPersistedDaemonStatus(config, configRoot).catch(() => null);
2832
+ if (!current || current.pid !== status.pid || current.instanceId !== status.instanceId) {
2833
+ return true;
2834
+ }
2835
+ await wait2(50);
2836
+ }
2837
+ return !isProcessAlive2(status.pid);
2838
+ };
2839
+ var stopLiveDaemon = async (config, status, configRoot = getConfigRoot(), command = "stop") => {
2840
+ const stopStatus = await requestDaemonControl(config, command, configRoot);
2841
+ if (command === "stop-if-idle" && !stopStatus.idle) return false;
2842
+ if (await waitForDaemonRelease(config, status, configRoot)) return true;
2843
+ const current = await readPersistedDaemonStatus(config, configRoot).catch(() => null);
2844
+ if (!current || current.pid !== status.pid || current.instanceId !== status.instanceId) {
2845
+ return true;
2846
+ }
2847
+ await stopUnresponsiveDaemonProcess(status.pid);
2848
+ return true;
2849
+ };
2850
+ var matchesDaemonIdentity = (config, status) => status.connectorId === config.connectorId && normalizeServerOrigin(status.serverUrl) === normalizeServerOrigin(config.serverUrl) && Number.isSafeInteger(status.pid) && status.pid > 0 && typeof status.instanceId === "string" && status.instanceId.length > 0;
2851
+ var readMatchingPersistedDaemonStatus = async (config, configRoot = getConfigRoot()) => {
2852
+ const status = await readPersistedDaemonStatus(config, configRoot).catch(() => null);
2853
+ return status && matchesDaemonIdentity(config, status) ? status : null;
2854
+ };
2855
+ var stopDaemonForIdentity = async (config, configRoot = getConfigRoot()) => {
2856
+ const liveStatus = await readLiveStatus(config, configRoot);
2857
+ if (liveStatus) {
2858
+ await stopLiveDaemon(config, liveStatus, configRoot);
2859
+ return;
2860
+ }
2861
+ const persistedStatus = await readMatchingPersistedDaemonStatus(config, configRoot);
2862
+ if (!persistedStatus || !isProcessAlive2(persistedStatus.pid)) return;
2863
+ const currentStatus = await readMatchingPersistedDaemonStatus(config, configRoot);
2864
+ if (!currentStatus || currentStatus.pid !== persistedStatus.pid || currentStatus.instanceId !== persistedStatus.instanceId || !isProcessAlive2(currentStatus.pid)) {
2865
+ return;
2866
+ }
2867
+ await stopUnresponsiveDaemonProcess(currentStatus.pid);
2868
+ };
2869
+ var shouldRestartUnhealthyDaemonForEnrollment = (status, explicitEnrollment) => explicitEnrollment && status.idle && status.heartbeatHealthy === false;
2870
+ var readDaemonLockOwnerPid = async (config) => {
2871
+ const lockPath = `${getConnectorDaemonLockTarget(
2872
+ config.serverUrl,
2873
+ config.connectorId,
2874
+ getConfigRoot()
2875
+ )}.lock`;
2876
+ const raw = await readFile6(lockPath, "utf8").catch(() => "");
2877
+ try {
2878
+ const metadata = JSON.parse(raw);
2879
+ const age = typeof metadata.createdAt === "number" && Number.isFinite(metadata.createdAt) ? Date.now() - metadata.createdAt : Number.POSITIVE_INFINITY;
2880
+ return typeof metadata.pid === "number" && Number.isSafeInteger(metadata.pid) && metadata.pid > 0 && age >= 0 && age <= LOCK_ONLY_RECOVERY_MAX_AGE_MS ? metadata.pid : null;
2881
+ } catch {
2882
+ return null;
2883
+ }
2884
+ };
2885
+ async function startDaemonWithRecovery(dependencies, maxAttempts = 2) {
2886
+ let lastError;
2887
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
2888
+ const spawned = await dependencies.spawnDaemon();
2889
+ try {
2890
+ return await dependencies.waitForReady();
2891
+ } catch (error) {
2892
+ lastError = error;
2893
+ const lateReady = await dependencies.readReady().catch(() => null);
2894
+ if (lateReady) return lateReady;
2895
+ await dependencies.stopDaemon(spawned.pid);
2896
+ if (attempt === maxAttempts) throw error;
2897
+ }
2898
+ }
2899
+ throw lastError;
2900
+ }
2901
+ var startInternalDaemonWithRecovery = (launchConfig, mode, preparedRuntime, options = {}) => startDaemonWithRecovery({
2902
+ readReady: () => readReadyDaemon(launchConfig),
2903
+ spawnDaemon: () => spawnInternalDaemon(launchConfig, mode, preparedRuntime, options),
2904
+ stopDaemon: stopUnresponsiveDaemonProcess,
2905
+ waitForReady: () => waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS)
2906
+ });
2626
2907
  var stopOlderIdleDaemon = async (config, status) => {
2627
2908
  if (!shouldReplaceDaemonVersion(status.version, CONNECTOR_RUNTIME_VERSION)) return false;
2628
2909
  if (!status.idle) return false;
@@ -2649,14 +2930,19 @@ async function ensureConnectorDaemon(launchConfig, mode, options = {}) {
2649
2930
  if (existingConfig) {
2650
2931
  const existingStatus = await readLiveStatus(existingConfig);
2651
2932
  if (existingStatus) {
2933
+ if (shouldRestartUnhealthyDaemonForEnrollment(
2934
+ existingStatus,
2935
+ Boolean(options.explicitEnrollment)
2936
+ ) && await stopLiveDaemon(existingConfig, existingStatus, getConfigRoot(), "stop-if-idle")) {
2937
+ return await startInternalDaemonWithRecovery(launchConfig, mode, void 0, options);
2938
+ }
2652
2939
  const preparedRuntime = !shouldReplaceDaemonVersion(existingStatus.version, CONNECTOR_RUNTIME_VERSION) || !existingStatus.idle ? void 0 : await ensureStableConnectorRuntime();
2653
2940
  const replaced = await stopOlderIdleDaemon(existingConfig, existingStatus);
2654
2941
  if (!replaced) {
2655
2942
  const status = mode === "persistent" ? await requestDaemonControl(existingConfig, "persist", getConfigRoot()) : existingStatus;
2656
2943
  return { config: existingConfig, status };
2657
2944
  }
2658
- await spawnInternalDaemon(launchConfig, mode, preparedRuntime, options);
2659
- return await waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS);
2945
+ return await startInternalDaemonWithRecovery(launchConfig, mode, preparedRuntime, options);
2660
2946
  }
2661
2947
  const persistedStatus = await readPersistedDaemonStatus(
2662
2948
  existingConfig,
@@ -2667,9 +2953,12 @@ async function ensureConnectorDaemon(launchConfig, mode, options = {}) {
2667
2953
  `Connector daemon PID ${persistedStatus.pid} is running but its local control channel is not responding. Stop that process and retry.`
2668
2954
  );
2669
2955
  }
2956
+ const lockOwnerPid = await readDaemonLockOwnerPid(existingConfig);
2957
+ if (lockOwnerPid && isProcessAlive2(lockOwnerPid)) {
2958
+ await stopUnresponsiveDaemonProcess(lockOwnerPid);
2959
+ }
2670
2960
  }
2671
- await spawnInternalDaemon(launchConfig, mode, void 0, options);
2672
- return await waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS);
2961
+ return await startInternalDaemonWithRecovery(launchConfig, mode, void 0, options);
2673
2962
  } finally {
2674
2963
  await bootstrapLock.release();
2675
2964
  }
@@ -2689,6 +2978,7 @@ var streamDaemonLogs = (options) => {
2689
2978
  for (const line of lines.slice(seenLines)) {
2690
2979
  try {
2691
2980
  const entry = JSON.parse(line);
2981
+ if (!shouldDisplayConnectorLogEntry(entry.createdAt, options.startedAt)) continue;
2692
2982
  const connectorId = entry.metadata?.connectorId;
2693
2983
  if (options.dashboardActive && typeof connectorId === "string") {
2694
2984
  const output2 = entry.metadata?.source === "daemon-console" ? entry.message : `[buda-connector] ${entry.message}`;
@@ -2764,9 +3054,7 @@ async function stopLocalConnectorDaemon(connectorId, serverUrl, configRoot = get
2764
3054
  (candidate) => candidate.connectorId === connectorId && normalizeServerOrigin(candidate.serverUrl) === serverOrigin
2765
3055
  );
2766
3056
  if (!config) throw new Error(`Local Connector ${connectorId} was not found`);
2767
- const status = await readLiveStatus(config, configRoot);
2768
- if (!status) return;
2769
- await requestDaemonControl(config, "stop", configRoot);
3057
+ await stopDaemonForIdentity(config, configRoot);
2770
3058
  }
2771
3059
  async function stopAllLocalConnectorDaemons(configRoot = getConfigRoot()) {
2772
3060
  const configs = await listDeviceConfigs(configRoot);
@@ -2777,10 +3065,7 @@ async function stopAllLocalConnectorDaemons(configRoot = getConfigRoot()) {
2777
3065
  ${config.connectorId}`, config);
2778
3066
  }
2779
3067
  const results = await Promise.allSettled(
2780
- [...identities.values()].map(async (config) => {
2781
- const status = await readLiveStatus(config, configRoot);
2782
- if (status) await requestDaemonControl(config, "stop", configRoot);
2783
- })
3068
+ [...identities.values()].map((config) => stopDaemonForIdentity(config, configRoot))
2784
3069
  );
2785
3070
  const failures = results.filter((result) => result.status === "rejected");
2786
3071
  if (failures.length) {
@@ -2799,6 +3084,7 @@ var waitForConnectorViewerExit = async (dashboardClosed) => {
2799
3084
  process.off("SIGTERM", finishSignal);
2800
3085
  };
2801
3086
  async function runConnectorDashboard(launchConfig, options = {}) {
3087
+ const startedAt = Date.now();
2802
3088
  let finishDashboard = () => void 0;
2803
3089
  const dashboardClosed = new Promise((resolve) => {
2804
3090
  finishDashboard = resolve;
@@ -2819,7 +3105,8 @@ async function runConnectorDashboard(launchConfig, options = {}) {
2819
3105
  connectorId: config.connectorId,
2820
3106
  dashboardActive: false,
2821
3107
  plain: Boolean(options.plain),
2822
- serverUrl: config.serverUrl
3108
+ serverUrl: config.serverUrl,
3109
+ startedAt
2823
3110
  });
2824
3111
  if (!process.stdin.isTTY) {
2825
3112
  stopLogs2();
@@ -2846,7 +3133,8 @@ async function runConnectorDashboard(launchConfig, options = {}) {
2846
3133
  const stopLogs = streamDaemonLogs({
2847
3134
  dashboardActive: true,
2848
3135
  plain: false,
2849
- serverUrl: getServerUrl(launchConfig)
3136
+ serverUrl: getServerUrl(launchConfig),
3137
+ startedAt
2850
3138
  });
2851
3139
  let refreshing = false;
2852
3140
  const refresh = async () => {
@@ -3394,7 +3682,7 @@ import {
3394
3682
  readdir as readdir5,
3395
3683
  readFile as readFile7,
3396
3684
  readlink,
3397
- realpath,
3685
+ realpath as realpath2,
3398
3686
  rename as rename5,
3399
3687
  rm as rm2,
3400
3688
  symlink,
@@ -3446,7 +3734,7 @@ var migrateLegacyWorkspaceRoot = async (storageRoot, options = {}) => {
3446
3734
  "Legacy migration storage root must be a real directory"
3447
3735
  );
3448
3736
  }
3449
- const canonicalRoot = await realpath(storageRoot);
3737
+ const canonicalRoot = await realpath2(storageRoot);
3450
3738
  const migrationDirectory = path9.join(canonicalRoot, MIGRATION_DIRECTORY);
3451
3739
  await ensurePrivateDirectoryWithin(canonicalRoot, migrationDirectory);
3452
3740
  const journalPath = path9.join(migrationDirectory, JOURNAL_FILENAME);
@@ -3742,6 +4030,7 @@ var writeJournal = async (journalPath, journal) => {
3742
4030
  }
3743
4031
  };
3744
4032
  var syncDirectory2 = async (directory) => {
4033
+ if (process.platform === "win32") return;
3745
4034
  const handle = await open2(directory, constants2.O_RDONLY);
3746
4035
  try {
3747
4036
  await handle.sync();
@@ -4089,7 +4378,7 @@ function isNonEmptyString(value) {
4089
4378
  }
4090
4379
 
4091
4380
  // src/html-preview.ts
4092
- import { readFile as readFile8, realpath as realpath2, stat as stat4 } from "fs/promises";
4381
+ import { readFile as readFile8, realpath as realpath3, stat as stat4 } from "fs/promises";
4093
4382
  import { createServer as createServer2 } from "http";
4094
4383
  import path10 from "path";
4095
4384
  var PREVIEW_HOST = "127.0.0.1";
@@ -4179,7 +4468,7 @@ var resolveWorkspaceFile = async (input) => {
4179
4468
  const virtualPath = input.filePath.replaceAll("\\", "/");
4180
4469
  if (virtualPath.includes("\0")) throw new Error("HTML preview path contains invalid data.");
4181
4470
  const mountPath = path10.resolve(input.mounts[input.rootVolume]);
4182
- const root = await realpath2(mountPath);
4471
+ const root = await realpath3(mountPath);
4183
4472
  let relativePath = virtualPath;
4184
4473
  let explicitVolume;
4185
4474
  for (const volume of ["agent", "space"]) {
@@ -4209,7 +4498,7 @@ var resolveWorkspaceFile = async (input) => {
4209
4498
  }
4210
4499
  const candidate = path10.resolve(root, ...segments);
4211
4500
  assertContained(root, candidate);
4212
- const canonicalFile = await realpath2(candidate);
4501
+ const canonicalFile = await realpath3(candidate);
4213
4502
  assertContained(root, canonicalFile);
4214
4503
  const fileStat = await stat4(canonicalFile);
4215
4504
  if (!fileStat.isFile()) throw new Error("HTML preview path must identify a file.");
@@ -4271,10 +4560,10 @@ var serveStaticFile = async (folder, method, requestUrl) => {
4271
4560
  try {
4272
4561
  const candidate = path10.resolve(folder, ...segments);
4273
4562
  assertContained(folder, candidate);
4274
- let canonicalFile = await realpath2(candidate);
4563
+ let canonicalFile = await realpath3(candidate);
4275
4564
  assertContained(folder, canonicalFile);
4276
4565
  if ((await stat4(canonicalFile)).isDirectory()) {
4277
- canonicalFile = await realpath2(path10.join(canonicalFile, "index.html"));
4566
+ canonicalFile = await realpath3(path10.join(canonicalFile, "index.html"));
4278
4567
  assertContained(folder, canonicalFile);
4279
4568
  }
4280
4569
  if (!(await stat4(canonicalFile)).isFile()) throw new Error("Not a file");
@@ -4309,12 +4598,17 @@ import {
4309
4598
  mkdir as mkdir9,
4310
4599
  readdir as readdir6,
4311
4600
  readFile as readFile9,
4312
- realpath as realpath3,
4601
+ realpath as realpath4,
4313
4602
  rm as rm3,
4314
4603
  symlink as symlink2,
4315
4604
  writeFile as writeFile4
4316
4605
  } from "fs/promises";
4317
4606
  import path11 from "path";
4607
+
4608
+ // src/directory-link.ts
4609
+ var directoryLinkType = (platform4 = process.platform) => platform4 === "win32" ? "junction" : "dir";
4610
+
4611
+ // src/workspace-volume-lifecycle.ts
4318
4612
  var MOUNT_DIRECTORY = "mount";
4319
4613
  var ARCHIVE_MARKER = "archived.json";
4320
4614
  var AGENT_CONTEXT_TEMPLATE_FILES = [
@@ -4447,7 +4741,7 @@ var WorkspaceVolumeLifecycle = class {
4447
4741
  ]) {
4448
4742
  await mkdir9(volume, { recursive: true, mode: 448 });
4449
4743
  const info = await lstat2(volume);
4450
- if (info.isSymbolicLink() || !info.isDirectory() || await realpath3(volume) !== volume) {
4744
+ if (info.isSymbolicLink() || !info.isDirectory() || await realpath4(volume) !== volume) {
4451
4745
  throw new Error("Workspace volume must be a canonical real directory");
4452
4746
  }
4453
4747
  }
@@ -4460,17 +4754,17 @@ var isMissingFile = (error) => error instanceof Error && "code" in error && erro
4460
4754
  var ensureVolumeLink = async (target, linkPath) => {
4461
4755
  try {
4462
4756
  const info = await lstat2(linkPath);
4463
- if (!info.isSymbolicLink() || await realpath3(linkPath) !== target) {
4757
+ if (!info.isSymbolicLink() || await realpath4(linkPath) !== target) {
4464
4758
  throw new Error("Workspace mount link does not match its canonical volume");
4465
4759
  }
4466
4760
  } catch (error) {
4467
4761
  if (!isMissingFile(error)) throw error;
4468
4762
  try {
4469
- await symlink2(target, linkPath, "dir");
4763
+ await symlink2(target, linkPath, directoryLinkType());
4470
4764
  } catch (createError) {
4471
4765
  if (!isAlreadyExists3(createError)) throw createError;
4472
4766
  const info = await lstat2(linkPath);
4473
- if (!info.isSymbolicLink() || await realpath3(linkPath) !== target) {
4767
+ if (!info.isSymbolicLink() || await realpath4(linkPath) !== target) {
4474
4768
  throw new Error("Workspace mount link raced with an invalid target");
4475
4769
  }
4476
4770
  }
@@ -4481,6 +4775,7 @@ var isAlreadyExists3 = (error) => error instanceof Error && "code" in error && e
4481
4775
  // src/relay.ts
4482
4776
  var RELAY_PING_INTERVAL_MS = 2e4;
4483
4777
  var RELAY_PONG_TIMEOUT_MS = 1e4;
4778
+ var RELAY_ACTIVE_REQUEST_PONG_TIMEOUT_LIMIT = 2;
4484
4779
  var RELAY_HANDSHAKE_TIMEOUT_MS = 1e4;
4485
4780
  var RELAY_RECONNECT_MAX_MS = 3e4;
4486
4781
  var RELAY_RECONNECT_JITTER = 0.2;
@@ -4586,6 +4881,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
4586
4881
  socket = ws;
4587
4882
  const inflight = /* @__PURE__ */ new Map();
4588
4883
  currentInflight = inflight;
4884
+ let consecutivePongTimeouts = 0;
4589
4885
  let detached = false;
4590
4886
  const detach = () => {
4591
4887
  if (detached) return;
@@ -4598,6 +4894,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
4598
4894
  void writeLog("info", `relay connected ${relayUrl}`);
4599
4895
  hadOpenConnection = true;
4600
4896
  reconnectDelayMs = RELAY_RECONNECT_INTERVAL_MS;
4897
+ consecutivePongTimeouts = 0;
4601
4898
  pingTimer = setInterval(() => {
4602
4899
  if (ws.readyState !== WebSocket2.OPEN) return;
4603
4900
  try {
@@ -4609,18 +4906,33 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
4609
4906
  }
4610
4907
  if (pongTimer) clearTimeout(pongTimer);
4611
4908
  pongTimer = setTimeout(() => {
4909
+ pongTimer = null;
4910
+ consecutivePongTimeouts += 1;
4911
+ if (inflight.size > 0 && consecutivePongTimeouts < RELAY_ACTIVE_REQUEST_PONG_TIMEOUT_LIMIT) {
4912
+ void writeLog(
4913
+ "warn",
4914
+ `relay pong timeout deferred while ${inflight.size} request(s) are active`
4915
+ );
4916
+ return;
4917
+ }
4612
4918
  void writeLog("warn", "relay pong timeout, terminating");
4613
4919
  ws.terminate();
4614
4920
  }, RELAY_PONG_TIMEOUT_MS);
4615
4921
  }, RELAY_PING_INTERVAL_MS);
4616
4922
  });
4617
4923
  ws.on("pong", () => {
4924
+ consecutivePongTimeouts = 0;
4618
4925
  if (pongTimer) {
4619
4926
  clearTimeout(pongTimer);
4620
4927
  pongTimer = null;
4621
4928
  }
4622
4929
  });
4623
4930
  ws.on("message", (raw) => {
4931
+ consecutivePongTimeouts = 0;
4932
+ if (pongTimer) {
4933
+ clearTimeout(pongTimer);
4934
+ pongTimer = null;
4935
+ }
4624
4936
  void handleRelayMessage(
4625
4937
  ws,
4626
4938
  bunnyDaemon,
@@ -4796,7 +5108,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4796
5108
  });
4797
5109
  executionIdentity = message.executionIdentity;
4798
5110
  } catch (error) {
4799
- sendRelayError(socket, message.requestId, error);
5111
+ await sendRelayError(socket, message.requestId, error).catch(() => void 0);
4800
5112
  return;
4801
5113
  }
4802
5114
  }
@@ -4882,7 +5194,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4882
5194
  durationMs: Date.now() - codingRunStartedAt
4883
5195
  });
4884
5196
  }
4885
- sendRelayMessage(socket, {
5197
+ await sendRelayMessage(socket, {
4886
5198
  type: "start",
4887
5199
  requestId: message.requestId,
4888
5200
  status: response.status,
@@ -4902,7 +5214,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4902
5214
  while (true) {
4903
5215
  const { value, done } = await reader.read();
4904
5216
  if (done) break;
4905
- sendRelayMessage(socket, {
5217
+ await sendRelayMessage(socket, {
4906
5218
  type: "chunk",
4907
5219
  requestId: message.requestId,
4908
5220
  data: binaryResponse ? Buffer.from(value).toString("base64") : decoder?.decode(value, { stream: true }) ?? "",
@@ -4911,7 +5223,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4911
5223
  }
4912
5224
  const tail = decoder?.decode() ?? "";
4913
5225
  if (tail) {
4914
- sendRelayMessage(socket, {
5226
+ await sendRelayMessage(socket, {
4915
5227
  type: "chunk",
4916
5228
  requestId: message.requestId,
4917
5229
  data: tail
@@ -4922,7 +5234,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4922
5234
  reader.releaseLock();
4923
5235
  }
4924
5236
  }
4925
- sendRelayMessage(socket, {
5237
+ await sendRelayMessage(socket, {
4926
5238
  type: "end",
4927
5239
  requestId: message.requestId
4928
5240
  });
@@ -4934,14 +5246,16 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
4934
5246
  });
4935
5247
  }
4936
5248
  } catch (error) {
5249
+ controller.abort();
4937
5250
  if (codingRunTrace && codingRunStartedAt !== void 0) {
4938
5251
  await writeLog("error", "relay request coding run error", {
4939
5252
  ...codingRunTrace,
4940
5253
  status: codingRunStatus,
4941
- durationMs: Date.now() - codingRunStartedAt
5254
+ durationMs: Date.now() - codingRunStartedAt,
5255
+ error: formatConnectorRelayError(error)
4942
5256
  });
4943
5257
  }
4944
- sendRelayError(socket, message.requestId, error);
5258
+ await sendRelayError(socket, message.requestId, error).catch(() => void 0);
4945
5259
  } finally {
4946
5260
  inflight.delete(message.requestId);
4947
5261
  }
@@ -4995,17 +5309,20 @@ var normalizeWorkspaceRequest = (request, mounts) => {
4995
5309
  if (!mount || relative === void 0) {
4996
5310
  throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
4997
5311
  }
4998
- const env = body.env && typeof body.env === "object" && !Array.isArray(body.env) ? body.env : {};
5312
+ const systemEnv = body.systemEnv && typeof body.systemEnv === "object" && !Array.isArray(body.systemEnv) ? body.systemEnv : {};
5313
+ const workspaceEnv = {
5314
+ BUDA_AGENT_ROOT: mounts.agent,
5315
+ BUDA_SPACE_ROOT: mounts.space,
5316
+ BUDA_SYSTEM_ROOT: mounts.system
5317
+ };
4999
5318
  return {
5000
5319
  ...request,
5001
5320
  body: {
5002
5321
  ...body,
5003
5322
  cwd: path12.join(mount, relative),
5004
- env: {
5005
- ...env,
5006
- BUDA_AGENT_ROOT: mounts.agent,
5007
- BUDA_SPACE_ROOT: mounts.space,
5008
- BUDA_SYSTEM_ROOT: mounts.system
5323
+ systemEnv: {
5324
+ ...systemEnv,
5325
+ ...workspaceEnv
5009
5326
  }
5010
5327
  }
5011
5328
  };
@@ -5037,17 +5354,24 @@ var normalizeConnectorFilesystemRequest = (request, mounts) => {
5037
5354
  }
5038
5355
  return request;
5039
5356
  };
5040
- function sendRelayError(socket, requestId, error) {
5041
- sendRelayMessage(socket, {
5357
+ async function sendRelayError(socket, requestId, error) {
5358
+ await sendRelayMessage(socket, {
5042
5359
  type: "error",
5043
5360
  requestId,
5044
5361
  error: error instanceof Error ? error.message : String(error),
5045
5362
  ...error instanceof ConnectorRelayProtocolError ? { code: error.code } : {}
5046
5363
  });
5047
5364
  }
5048
- function sendRelayMessage(socket, message) {
5049
- if (socket.readyState !== WebSocket2.OPEN) return;
5050
- socket.send(JSON.stringify(message));
5365
+ async function sendRelayMessage(socket, message) {
5366
+ if (socket.readyState !== WebSocket2.OPEN) {
5367
+ throw new Error(`Connector relay socket closed before ${message.type} could be sent.`);
5368
+ }
5369
+ await new Promise((resolve, reject) => {
5370
+ socket.send(JSON.stringify(message), (error) => {
5371
+ if (error) reject(error);
5372
+ else resolve();
5373
+ });
5374
+ });
5051
5375
  }
5052
5376
  function headersToRecord(headers) {
5053
5377
  const result = {};
@@ -5106,7 +5430,7 @@ async function ensureSymlink(target, linkPath) {
5106
5430
  } catch {
5107
5431
  }
5108
5432
  await mkdir10(path13.dirname(linkPath), { recursive: true });
5109
- await symlink3(target, linkPath, "dir");
5433
+ await symlink3(target, linkPath, directoryLinkType());
5110
5434
  }
5111
5435
 
5112
5436
  // src/workspace-registry.ts
@@ -5117,7 +5441,7 @@ import {
5117
5441
  mkdir as mkdir11,
5118
5442
  open as open3,
5119
5443
  readFile as readFile10,
5120
- realpath as realpath4,
5444
+ realpath as realpath5,
5121
5445
  rename as rename6,
5122
5446
  statfs,
5123
5447
  unlink as unlink7
@@ -5281,7 +5605,7 @@ var reconcileWorkspace = async (workspace, storageRoot) => {
5281
5605
  for (const volume of volumes) {
5282
5606
  assertContained2(storageRoot, volume);
5283
5607
  await ensurePrivateDirectory2(volume);
5284
- const canonical = await realpath4(volume);
5608
+ const canonical = await realpath5(volume);
5285
5609
  if (canonical !== volume) throw corrupt2("Workspace path resolves through a symbolic link");
5286
5610
  }
5287
5611
  };
@@ -5327,7 +5651,7 @@ var canonicalDirectory = async (directory, label) => {
5327
5651
  if (info.isSymbolicLink() || !info.isDirectory()) {
5328
5652
  throw new WorkspaceRegistryError("STORAGE_UNSAFE", `${label} must be a real directory`);
5329
5653
  }
5330
- return realpath4(directory);
5654
+ return realpath5(directory);
5331
5655
  };
5332
5656
  var rejectUnsafeFilesystem = async (storageRoot) => {
5333
5657
  const stats = await statfs(storageRoot, { bigint: true });
@@ -5563,7 +5887,30 @@ import { mkdir as mkdir12 } from "fs/promises";
5563
5887
  import path15 from "path";
5564
5888
  var RECOVERED_DAEMON_STOP_TIMEOUT_MS = 1e4;
5565
5889
  var RECOVERED_DAEMON_STOP_POLL_MS = 100;
5890
+ var SHUTDOWN_FORCE_EXIT_MS = 1e4;
5891
+ var SHUTDOWN_STEP_TIMEOUT_MS = 2e3;
5566
5892
  var wait3 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
5893
+ var runShutdownStep = async (label, operation) => {
5894
+ let timeout;
5895
+ try {
5896
+ await Promise.race([
5897
+ Promise.resolve().then(operation),
5898
+ new Promise((_, reject) => {
5899
+ timeout = setTimeout(
5900
+ () => reject(new Error(`${label} timed out after ${SHUTDOWN_STEP_TIMEOUT_MS}ms`)),
5901
+ SHUTDOWN_STEP_TIMEOUT_MS
5902
+ );
5903
+ })
5904
+ ]);
5905
+ } catch (error) {
5906
+ await writeLog(
5907
+ "warn",
5908
+ `${label} failed during shutdown: ${error instanceof Error ? error.message : String(error)}`
5909
+ ).catch(() => void 0);
5910
+ } finally {
5911
+ if (timeout) clearTimeout(timeout);
5912
+ }
5913
+ };
5567
5914
  async function stopRecoveredIdleDaemon(config, configRoot) {
5568
5915
  const stopStatus = await requestDaemonControl(config, "stop-if-idle", configRoot).catch(
5569
5916
  () => null
@@ -5683,18 +6030,30 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5683
6030
  const shutdown = async (shutdownOptions = {}) => {
5684
6031
  if (shuttingDown) return;
5685
6032
  shuttingDown = true;
6033
+ const forceExit = options.exitOnShutdown === false ? void 0 : setTimeout(() => process.exit(0), SHUTDOWN_FORCE_EXIT_MS);
5686
6034
  if (interval) clearInterval(interval);
5687
6035
  relay.stop();
5688
6036
  try {
5689
- await closeAllBrowserSessions();
5690
- if (shutdownOptions.unregister !== false) {
5691
- await unregisterConnector(config, launchConfig);
5692
- }
5693
- await stopEmbeddedBunnyDaemon(activeBunnyDaemon);
6037
+ await Promise.all([
6038
+ runShutdownStep("browser session cleanup", closeAllBrowserSessions),
6039
+ ...shutdownOptions.unregister === false ? [] : [
6040
+ runShutdownStep(
6041
+ "connector unregister",
6042
+ () => unregisterConnector(config, launchConfig)
6043
+ )
6044
+ ],
6045
+ runShutdownStep(
6046
+ "embedded Bunny daemon shutdown",
6047
+ () => stopEmbeddedBunnyDaemon(activeBunnyDaemon)
6048
+ )
6049
+ ]);
5694
6050
  } finally {
5695
- await control?.close();
5696
- await daemonLock.release();
5697
- await restoreConsole?.();
6051
+ await Promise.all([
6052
+ runShutdownStep("control channel shutdown", async () => control?.close()),
6053
+ runShutdownStep("daemon lock release", () => daemonLock.release()),
6054
+ runShutdownStep("console capture restore", async () => restoreConsole?.())
6055
+ ]);
6056
+ if (forceExit) clearTimeout(forceExit);
5698
6057
  if (options.exitOnShutdown !== false) process.exit(0);
5699
6058
  }
5700
6059
  };
@@ -5709,8 +6068,11 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5709
6068
  });
5710
6069
  }).catch((error) => {
5711
6070
  heartbeatHealthy = false;
5712
- if (error instanceof ConnectorDeletedError) {
5713
- void writeLog("warn", "connector deleted remotely; stopping daemon");
6071
+ if (error instanceof ConnectorDeletedError || error instanceof ConnectorUnauthorizedError) {
6072
+ void writeLog(
6073
+ "warn",
6074
+ error instanceof ConnectorDeletedError ? "connector deleted remotely; stopping daemon" : "connector authentication rejected remotely; stopping daemon"
6075
+ );
5714
6076
  void shutdown({ unregister: false });
5715
6077
  return;
5716
6078
  }
@@ -5734,6 +6096,10 @@ async function runConnectorDaemon(launchConfig, options = {}) {
5734
6096
  process.once("SIGINT", () => void shutdown());
5735
6097
  process.once("SIGTERM", () => void shutdown());
5736
6098
  } catch (error) {
6099
+ await writeLog(
6100
+ "error",
6101
+ `daemon startup failed: ${error instanceof Error ? error.stack || error.message : String(error)}`
6102
+ ).catch(() => void 0);
5737
6103
  if (bunnyDaemon) await stopEmbeddedBunnyDaemon(bunnyDaemon);
5738
6104
  await daemonLock.release();
5739
6105
  await restoreConsole?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buda-ai/connector",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -18,6 +18,7 @@
18
18
  "dev": "tsx src/cli.ts dev",
19
19
  "daemon": "tsx src/cli.ts daemon",
20
20
  "new": "tsx src/cli.ts new",
21
+ "prepack": "tsup --silent",
21
22
  "start": "node dist/cli.js daemon",
22
23
  "build": "tsup",
23
24
  "test": "vitest run",
@@ -26,7 +27,7 @@
26
27
  "lint:err": "biome format . --write && biome check . --write --diagnostic-level=error"
27
28
  },
28
29
  "dependencies": {
29
- "@bunny-agent/daemon": "0.9.51",
30
+ "@bunny-agent/daemon": "0.9.58",
30
31
  "@clack/prompts": "^1.0.0",
31
32
  "playwright-core": "^1.52.0",
32
33
  "ws": "^8.21.0"