@buda-ai/connector 0.1.32 → 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.
- package/agent-templates/shared/AGENTS.md +1 -1
- package/dist/cli.js +401 -76
- package/package.json +3 -2
|
@@ -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`, `
|
|
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(() =>
|
|
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
|
-
|
|
39
|
-
|
|
38
|
+
try {
|
|
39
|
+
await handle.writeFile(
|
|
40
|
+
`${JSON.stringify({ pid: process.pid, ownerToken, createdAt: Date.now() })}
|
|
40
41
|
`
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
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.
|
|
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
|
});
|
|
@@ -2421,11 +2440,19 @@ import path6 from "path";
|
|
|
2421
2440
|
import { promisify } from "util";
|
|
2422
2441
|
var execFileAsync = promisify(execFile);
|
|
2423
2442
|
var RUNTIME_INSTALL_TIMEOUT_MS = 5 * 6e4;
|
|
2443
|
+
var WINDOWS_RENAME_RETRY_DELAYS_MS = [50, 100, 200, 400, 500, 500, 500, 500, 500];
|
|
2424
2444
|
var MACOS_NODE_LIBRARY_PATTERN = /^libnode(?:\.\d+)*\.dylib$/;
|
|
2425
2445
|
var getLoadedSharedObjects = () => {
|
|
2426
2446
|
const report = process.report?.getReport();
|
|
2427
2447
|
return Array.isArray(report?.sharedObjects) ? report.sharedObjects.filter((value) => typeof value === "string") : [];
|
|
2428
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
|
+
};
|
|
2429
2456
|
var copyNodeRuntime = async (sourceNode, destinationNode, options = {}) => {
|
|
2430
2457
|
const resolvedSourceNode = await realpath(sourceNode);
|
|
2431
2458
|
const sourceDirectory = path6.dirname(resolvedSourceNode);
|
|
@@ -2452,8 +2479,7 @@ var copyNodeRuntime = async (sourceNode, destinationNode, options = {}) => {
|
|
|
2452
2479
|
}))
|
|
2453
2480
|
)).flatMap(({ directory, entries }) => entries.map((entry) => ({ directory, entry })));
|
|
2454
2481
|
const companionFiles = /* @__PURE__ */ new Map();
|
|
2455
|
-
const
|
|
2456
|
-
for (const library of loadedLibraries) {
|
|
2482
|
+
for (const library of options.loadedLibraries ?? getLoadedSharedObjects()) {
|
|
2457
2483
|
const name = path6.basename(library);
|
|
2458
2484
|
if (MACOS_NODE_LIBRARY_PATTERN.test(name)) companionFiles.set(name, library);
|
|
2459
2485
|
}
|
|
@@ -2471,7 +2497,44 @@ var copyNodeRuntime = async (sourceNode, destinationNode, options = {}) => {
|
|
|
2471
2497
|
)
|
|
2472
2498
|
);
|
|
2473
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
|
+
};
|
|
2474
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
|
+
};
|
|
2475
2538
|
var runtimeRoot = () => process.env.BUDA_CONNECTOR_RUNTIME_ROOT || path6.join(homedir3(), ".buda", "runtime");
|
|
2476
2539
|
var runtimePlatformKey = `${process.platform}-${process.arch}-node${process.versions.modules}`;
|
|
2477
2540
|
var versionDirectory = () => path6.join(runtimeRoot(), runtimePlatformKey, CONNECTOR_RUNTIME_VERSION);
|
|
@@ -2505,9 +2568,11 @@ var validateRuntime = async (runtime) => {
|
|
|
2505
2568
|
};
|
|
2506
2569
|
var installFromNpm = async (directory) => {
|
|
2507
2570
|
await mkdir5(directory, { recursive: true, mode: 448 });
|
|
2571
|
+
const npm = await resolveNpmInvocation();
|
|
2508
2572
|
await execFileAsync(
|
|
2509
|
-
|
|
2573
|
+
npm.executable,
|
|
2510
2574
|
[
|
|
2575
|
+
...npm.argsPrefix,
|
|
2511
2576
|
"install",
|
|
2512
2577
|
"--ignore-scripts=false",
|
|
2513
2578
|
"--no-audit",
|
|
@@ -2541,10 +2606,10 @@ var installFromBundledRuntime = async (directory, sourceRoot) => {
|
|
|
2541
2606
|
if (relativeEntrypoint.startsWith("..") || path6.isAbsolute(relativeEntrypoint)) {
|
|
2542
2607
|
throw new Error("Bundled Connector entrypoint must be inside its runtime root");
|
|
2543
2608
|
}
|
|
2544
|
-
const relativeNode =
|
|
2609
|
+
const { copyNode, relativeNode } = resolveBundledNodePlacement(sourceRoot, sourceNode);
|
|
2545
2610
|
const entrypoint = path6.join(directory, relativeEntrypoint);
|
|
2546
|
-
const nodeExecutable =
|
|
2547
|
-
if (
|
|
2611
|
+
const nodeExecutable = copyNode ? path6.join(directory, "node", path6.basename(sourceNode)) : path6.join(directory, relativeNode);
|
|
2612
|
+
if (copyNode) {
|
|
2548
2613
|
await copyNodeRuntime(sourceNode, nodeExecutable);
|
|
2549
2614
|
}
|
|
2550
2615
|
if (!await exists(entrypoint))
|
|
@@ -2591,7 +2656,7 @@ async function ensureStableConnectorRuntime() {
|
|
|
2591
2656
|
};
|
|
2592
2657
|
await writeManifest(temporary, rebased);
|
|
2593
2658
|
await rm(finalDirectory, { recursive: true, force: true });
|
|
2594
|
-
await
|
|
2659
|
+
await renameStagedRuntime(temporary, finalDirectory);
|
|
2595
2660
|
return rebased;
|
|
2596
2661
|
} catch (error) {
|
|
2597
2662
|
await rm(temporary, { recursive: true, force: true });
|
|
@@ -2605,6 +2670,8 @@ async function ensureStableConnectorRuntime() {
|
|
|
2605
2670
|
// src/daemon-client.ts
|
|
2606
2671
|
var START_TIMEOUT_MS = 45e3;
|
|
2607
2672
|
var BOOTSTRAP_TIMEOUT_MS = 5 * 6e4;
|
|
2673
|
+
var GRACEFUL_STOP_TIMEOUT_MS = 3e3;
|
|
2674
|
+
var LOCK_ONLY_RECOVERY_MAX_AGE_MS = 5 * 6e4;
|
|
2608
2675
|
var POLL_INTERVAL_MS = 250;
|
|
2609
2676
|
var wait2 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
2610
2677
|
var getConfigRoot = () => process.env.BUDA_CONNECTOR_CONFIG_ROOT;
|
|
@@ -2637,14 +2704,78 @@ var compareVersions = (left, right) => {
|
|
|
2637
2704
|
return 0;
|
|
2638
2705
|
};
|
|
2639
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
|
+
};
|
|
2640
2773
|
var waitForDaemon = async (launchConfig, deadline) => {
|
|
2641
2774
|
let lastConfig = null;
|
|
2642
2775
|
while (Date.now() < deadline) {
|
|
2776
|
+
const ready = await readReadyDaemon(launchConfig);
|
|
2777
|
+
if (ready) return ready;
|
|
2643
2778
|
lastConfig = await readDeviceConfig(launchConfig, getConfigRoot());
|
|
2644
|
-
if (lastConfig) {
|
|
2645
|
-
const status = await readLiveStatus(lastConfig);
|
|
2646
|
-
if (status) return { config: lastConfig, status };
|
|
2647
|
-
}
|
|
2648
2779
|
await wait2(POLL_INTERVAL_MS);
|
|
2649
2780
|
}
|
|
2650
2781
|
throw new Error(
|
|
@@ -2653,17 +2784,126 @@ var waitForDaemon = async (launchConfig, deadline) => {
|
|
|
2653
2784
|
};
|
|
2654
2785
|
var spawnInternalDaemon = async (launchConfig, mode, preparedRuntime, options = {}) => {
|
|
2655
2786
|
const runtime = preparedRuntime ?? await ensureStableConnectorRuntime();
|
|
2656
|
-
const
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
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);
|
|
2666
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
|
+
}
|
|
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
|
+
});
|
|
2667
2907
|
var stopOlderIdleDaemon = async (config, status) => {
|
|
2668
2908
|
if (!shouldReplaceDaemonVersion(status.version, CONNECTOR_RUNTIME_VERSION)) return false;
|
|
2669
2909
|
if (!status.idle) return false;
|
|
@@ -2690,14 +2930,19 @@ async function ensureConnectorDaemon(launchConfig, mode, options = {}) {
|
|
|
2690
2930
|
if (existingConfig) {
|
|
2691
2931
|
const existingStatus = await readLiveStatus(existingConfig);
|
|
2692
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
|
+
}
|
|
2693
2939
|
const preparedRuntime = !shouldReplaceDaemonVersion(existingStatus.version, CONNECTOR_RUNTIME_VERSION) || !existingStatus.idle ? void 0 : await ensureStableConnectorRuntime();
|
|
2694
2940
|
const replaced = await stopOlderIdleDaemon(existingConfig, existingStatus);
|
|
2695
2941
|
if (!replaced) {
|
|
2696
2942
|
const status = mode === "persistent" ? await requestDaemonControl(existingConfig, "persist", getConfigRoot()) : existingStatus;
|
|
2697
2943
|
return { config: existingConfig, status };
|
|
2698
2944
|
}
|
|
2699
|
-
await
|
|
2700
|
-
return await waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS);
|
|
2945
|
+
return await startInternalDaemonWithRecovery(launchConfig, mode, preparedRuntime, options);
|
|
2701
2946
|
}
|
|
2702
2947
|
const persistedStatus = await readPersistedDaemonStatus(
|
|
2703
2948
|
existingConfig,
|
|
@@ -2708,9 +2953,12 @@ async function ensureConnectorDaemon(launchConfig, mode, options = {}) {
|
|
|
2708
2953
|
`Connector daemon PID ${persistedStatus.pid} is running but its local control channel is not responding. Stop that process and retry.`
|
|
2709
2954
|
);
|
|
2710
2955
|
}
|
|
2956
|
+
const lockOwnerPid = await readDaemonLockOwnerPid(existingConfig);
|
|
2957
|
+
if (lockOwnerPid && isProcessAlive2(lockOwnerPid)) {
|
|
2958
|
+
await stopUnresponsiveDaemonProcess(lockOwnerPid);
|
|
2959
|
+
}
|
|
2711
2960
|
}
|
|
2712
|
-
await
|
|
2713
|
-
return await waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS);
|
|
2961
|
+
return await startInternalDaemonWithRecovery(launchConfig, mode, void 0, options);
|
|
2714
2962
|
} finally {
|
|
2715
2963
|
await bootstrapLock.release();
|
|
2716
2964
|
}
|
|
@@ -2730,6 +2978,7 @@ var streamDaemonLogs = (options) => {
|
|
|
2730
2978
|
for (const line of lines.slice(seenLines)) {
|
|
2731
2979
|
try {
|
|
2732
2980
|
const entry = JSON.parse(line);
|
|
2981
|
+
if (!shouldDisplayConnectorLogEntry(entry.createdAt, options.startedAt)) continue;
|
|
2733
2982
|
const connectorId = entry.metadata?.connectorId;
|
|
2734
2983
|
if (options.dashboardActive && typeof connectorId === "string") {
|
|
2735
2984
|
const output2 = entry.metadata?.source === "daemon-console" ? entry.message : `[buda-connector] ${entry.message}`;
|
|
@@ -2805,9 +3054,7 @@ async function stopLocalConnectorDaemon(connectorId, serverUrl, configRoot = get
|
|
|
2805
3054
|
(candidate) => candidate.connectorId === connectorId && normalizeServerOrigin(candidate.serverUrl) === serverOrigin
|
|
2806
3055
|
);
|
|
2807
3056
|
if (!config) throw new Error(`Local Connector ${connectorId} was not found`);
|
|
2808
|
-
|
|
2809
|
-
if (!status) return;
|
|
2810
|
-
await requestDaemonControl(config, "stop", configRoot);
|
|
3057
|
+
await stopDaemonForIdentity(config, configRoot);
|
|
2811
3058
|
}
|
|
2812
3059
|
async function stopAllLocalConnectorDaemons(configRoot = getConfigRoot()) {
|
|
2813
3060
|
const configs = await listDeviceConfigs(configRoot);
|
|
@@ -2818,10 +3065,7 @@ async function stopAllLocalConnectorDaemons(configRoot = getConfigRoot()) {
|
|
|
2818
3065
|
${config.connectorId}`, config);
|
|
2819
3066
|
}
|
|
2820
3067
|
const results = await Promise.allSettled(
|
|
2821
|
-
[...identities.values()].map(
|
|
2822
|
-
const status = await readLiveStatus(config, configRoot);
|
|
2823
|
-
if (status) await requestDaemonControl(config, "stop", configRoot);
|
|
2824
|
-
})
|
|
3068
|
+
[...identities.values()].map((config) => stopDaemonForIdentity(config, configRoot))
|
|
2825
3069
|
);
|
|
2826
3070
|
const failures = results.filter((result) => result.status === "rejected");
|
|
2827
3071
|
if (failures.length) {
|
|
@@ -2840,6 +3084,7 @@ var waitForConnectorViewerExit = async (dashboardClosed) => {
|
|
|
2840
3084
|
process.off("SIGTERM", finishSignal);
|
|
2841
3085
|
};
|
|
2842
3086
|
async function runConnectorDashboard(launchConfig, options = {}) {
|
|
3087
|
+
const startedAt = Date.now();
|
|
2843
3088
|
let finishDashboard = () => void 0;
|
|
2844
3089
|
const dashboardClosed = new Promise((resolve) => {
|
|
2845
3090
|
finishDashboard = resolve;
|
|
@@ -2860,7 +3105,8 @@ async function runConnectorDashboard(launchConfig, options = {}) {
|
|
|
2860
3105
|
connectorId: config.connectorId,
|
|
2861
3106
|
dashboardActive: false,
|
|
2862
3107
|
plain: Boolean(options.plain),
|
|
2863
|
-
serverUrl: config.serverUrl
|
|
3108
|
+
serverUrl: config.serverUrl,
|
|
3109
|
+
startedAt
|
|
2864
3110
|
});
|
|
2865
3111
|
if (!process.stdin.isTTY) {
|
|
2866
3112
|
stopLogs2();
|
|
@@ -2887,7 +3133,8 @@ async function runConnectorDashboard(launchConfig, options = {}) {
|
|
|
2887
3133
|
const stopLogs = streamDaemonLogs({
|
|
2888
3134
|
dashboardActive: true,
|
|
2889
3135
|
plain: false,
|
|
2890
|
-
serverUrl: getServerUrl(launchConfig)
|
|
3136
|
+
serverUrl: getServerUrl(launchConfig),
|
|
3137
|
+
startedAt
|
|
2891
3138
|
});
|
|
2892
3139
|
let refreshing = false;
|
|
2893
3140
|
const refresh = async () => {
|
|
@@ -3783,6 +4030,7 @@ var writeJournal = async (journalPath, journal) => {
|
|
|
3783
4030
|
}
|
|
3784
4031
|
};
|
|
3785
4032
|
var syncDirectory2 = async (directory) => {
|
|
4033
|
+
if (process.platform === "win32") return;
|
|
3786
4034
|
const handle = await open2(directory, constants2.O_RDONLY);
|
|
3787
4035
|
try {
|
|
3788
4036
|
await handle.sync();
|
|
@@ -4356,6 +4604,11 @@ import {
|
|
|
4356
4604
|
writeFile as writeFile4
|
|
4357
4605
|
} from "fs/promises";
|
|
4358
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
|
|
4359
4612
|
var MOUNT_DIRECTORY = "mount";
|
|
4360
4613
|
var ARCHIVE_MARKER = "archived.json";
|
|
4361
4614
|
var AGENT_CONTEXT_TEMPLATE_FILES = [
|
|
@@ -4507,7 +4760,7 @@ var ensureVolumeLink = async (target, linkPath) => {
|
|
|
4507
4760
|
} catch (error) {
|
|
4508
4761
|
if (!isMissingFile(error)) throw error;
|
|
4509
4762
|
try {
|
|
4510
|
-
await symlink2(target, linkPath,
|
|
4763
|
+
await symlink2(target, linkPath, directoryLinkType());
|
|
4511
4764
|
} catch (createError) {
|
|
4512
4765
|
if (!isAlreadyExists3(createError)) throw createError;
|
|
4513
4766
|
const info = await lstat2(linkPath);
|
|
@@ -4522,6 +4775,7 @@ var isAlreadyExists3 = (error) => error instanceof Error && "code" in error && e
|
|
|
4522
4775
|
// src/relay.ts
|
|
4523
4776
|
var RELAY_PING_INTERVAL_MS = 2e4;
|
|
4524
4777
|
var RELAY_PONG_TIMEOUT_MS = 1e4;
|
|
4778
|
+
var RELAY_ACTIVE_REQUEST_PONG_TIMEOUT_LIMIT = 2;
|
|
4525
4779
|
var RELAY_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
4526
4780
|
var RELAY_RECONNECT_MAX_MS = 3e4;
|
|
4527
4781
|
var RELAY_RECONNECT_JITTER = 0.2;
|
|
@@ -4627,6 +4881,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
4627
4881
|
socket = ws;
|
|
4628
4882
|
const inflight = /* @__PURE__ */ new Map();
|
|
4629
4883
|
currentInflight = inflight;
|
|
4884
|
+
let consecutivePongTimeouts = 0;
|
|
4630
4885
|
let detached = false;
|
|
4631
4886
|
const detach = () => {
|
|
4632
4887
|
if (detached) return;
|
|
@@ -4639,6 +4894,7 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
4639
4894
|
void writeLog("info", `relay connected ${relayUrl}`);
|
|
4640
4895
|
hadOpenConnection = true;
|
|
4641
4896
|
reconnectDelayMs = RELAY_RECONNECT_INTERVAL_MS;
|
|
4897
|
+
consecutivePongTimeouts = 0;
|
|
4642
4898
|
pingTimer = setInterval(() => {
|
|
4643
4899
|
if (ws.readyState !== WebSocket2.OPEN) return;
|
|
4644
4900
|
try {
|
|
@@ -4650,18 +4906,33 @@ function startConnectorRelay(config, bunnyDaemon, relayProtocol, workspaceRegist
|
|
|
4650
4906
|
}
|
|
4651
4907
|
if (pongTimer) clearTimeout(pongTimer);
|
|
4652
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
|
+
}
|
|
4653
4918
|
void writeLog("warn", "relay pong timeout, terminating");
|
|
4654
4919
|
ws.terminate();
|
|
4655
4920
|
}, RELAY_PONG_TIMEOUT_MS);
|
|
4656
4921
|
}, RELAY_PING_INTERVAL_MS);
|
|
4657
4922
|
});
|
|
4658
4923
|
ws.on("pong", () => {
|
|
4924
|
+
consecutivePongTimeouts = 0;
|
|
4659
4925
|
if (pongTimer) {
|
|
4660
4926
|
clearTimeout(pongTimer);
|
|
4661
4927
|
pongTimer = null;
|
|
4662
4928
|
}
|
|
4663
4929
|
});
|
|
4664
4930
|
ws.on("message", (raw) => {
|
|
4931
|
+
consecutivePongTimeouts = 0;
|
|
4932
|
+
if (pongTimer) {
|
|
4933
|
+
clearTimeout(pongTimer);
|
|
4934
|
+
pongTimer = null;
|
|
4935
|
+
}
|
|
4665
4936
|
void handleRelayMessage(
|
|
4666
4937
|
ws,
|
|
4667
4938
|
bunnyDaemon,
|
|
@@ -4837,7 +5108,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4837
5108
|
});
|
|
4838
5109
|
executionIdentity = message.executionIdentity;
|
|
4839
5110
|
} catch (error) {
|
|
4840
|
-
sendRelayError(socket, message.requestId, error);
|
|
5111
|
+
await sendRelayError(socket, message.requestId, error).catch(() => void 0);
|
|
4841
5112
|
return;
|
|
4842
5113
|
}
|
|
4843
5114
|
}
|
|
@@ -4923,7 +5194,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4923
5194
|
durationMs: Date.now() - codingRunStartedAt
|
|
4924
5195
|
});
|
|
4925
5196
|
}
|
|
4926
|
-
sendRelayMessage(socket, {
|
|
5197
|
+
await sendRelayMessage(socket, {
|
|
4927
5198
|
type: "start",
|
|
4928
5199
|
requestId: message.requestId,
|
|
4929
5200
|
status: response.status,
|
|
@@ -4943,7 +5214,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4943
5214
|
while (true) {
|
|
4944
5215
|
const { value, done } = await reader.read();
|
|
4945
5216
|
if (done) break;
|
|
4946
|
-
sendRelayMessage(socket, {
|
|
5217
|
+
await sendRelayMessage(socket, {
|
|
4947
5218
|
type: "chunk",
|
|
4948
5219
|
requestId: message.requestId,
|
|
4949
5220
|
data: binaryResponse ? Buffer.from(value).toString("base64") : decoder?.decode(value, { stream: true }) ?? "",
|
|
@@ -4952,7 +5223,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4952
5223
|
}
|
|
4953
5224
|
const tail = decoder?.decode() ?? "";
|
|
4954
5225
|
if (tail) {
|
|
4955
|
-
sendRelayMessage(socket, {
|
|
5226
|
+
await sendRelayMessage(socket, {
|
|
4956
5227
|
type: "chunk",
|
|
4957
5228
|
requestId: message.requestId,
|
|
4958
5229
|
data: tail
|
|
@@ -4963,7 +5234,7 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4963
5234
|
reader.releaseLock();
|
|
4964
5235
|
}
|
|
4965
5236
|
}
|
|
4966
|
-
sendRelayMessage(socket, {
|
|
5237
|
+
await sendRelayMessage(socket, {
|
|
4967
5238
|
type: "end",
|
|
4968
5239
|
requestId: message.requestId
|
|
4969
5240
|
});
|
|
@@ -4975,14 +5246,16 @@ async function handleRelayMessage(socket, bunnyDaemon, raw, inflight, expectedCo
|
|
|
4975
5246
|
});
|
|
4976
5247
|
}
|
|
4977
5248
|
} catch (error) {
|
|
5249
|
+
controller.abort();
|
|
4978
5250
|
if (codingRunTrace && codingRunStartedAt !== void 0) {
|
|
4979
5251
|
await writeLog("error", "relay request coding run error", {
|
|
4980
5252
|
...codingRunTrace,
|
|
4981
5253
|
status: codingRunStatus,
|
|
4982
|
-
durationMs: Date.now() - codingRunStartedAt
|
|
5254
|
+
durationMs: Date.now() - codingRunStartedAt,
|
|
5255
|
+
error: formatConnectorRelayError(error)
|
|
4983
5256
|
});
|
|
4984
5257
|
}
|
|
4985
|
-
sendRelayError(socket, message.requestId, error);
|
|
5258
|
+
await sendRelayError(socket, message.requestId, error).catch(() => void 0);
|
|
4986
5259
|
} finally {
|
|
4987
5260
|
inflight.delete(message.requestId);
|
|
4988
5261
|
}
|
|
@@ -5036,17 +5309,20 @@ var normalizeWorkspaceRequest = (request, mounts) => {
|
|
|
5036
5309
|
if (!mount || relative === void 0) {
|
|
5037
5310
|
throw new ConnectorRelayProtocolError("WORKSPACE_MISMATCH");
|
|
5038
5311
|
}
|
|
5039
|
-
const
|
|
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
|
+
};
|
|
5040
5318
|
return {
|
|
5041
5319
|
...request,
|
|
5042
5320
|
body: {
|
|
5043
5321
|
...body,
|
|
5044
5322
|
cwd: path12.join(mount, relative),
|
|
5045
|
-
|
|
5046
|
-
...
|
|
5047
|
-
|
|
5048
|
-
BUDA_SPACE_ROOT: mounts.space,
|
|
5049
|
-
BUDA_SYSTEM_ROOT: mounts.system
|
|
5323
|
+
systemEnv: {
|
|
5324
|
+
...systemEnv,
|
|
5325
|
+
...workspaceEnv
|
|
5050
5326
|
}
|
|
5051
5327
|
}
|
|
5052
5328
|
};
|
|
@@ -5078,17 +5354,24 @@ var normalizeConnectorFilesystemRequest = (request, mounts) => {
|
|
|
5078
5354
|
}
|
|
5079
5355
|
return request;
|
|
5080
5356
|
};
|
|
5081
|
-
function sendRelayError(socket, requestId, error) {
|
|
5082
|
-
sendRelayMessage(socket, {
|
|
5357
|
+
async function sendRelayError(socket, requestId, error) {
|
|
5358
|
+
await sendRelayMessage(socket, {
|
|
5083
5359
|
type: "error",
|
|
5084
5360
|
requestId,
|
|
5085
5361
|
error: error instanceof Error ? error.message : String(error),
|
|
5086
5362
|
...error instanceof ConnectorRelayProtocolError ? { code: error.code } : {}
|
|
5087
5363
|
});
|
|
5088
5364
|
}
|
|
5089
|
-
function sendRelayMessage(socket, message) {
|
|
5090
|
-
if (socket.readyState !== WebSocket2.OPEN)
|
|
5091
|
-
|
|
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
|
+
});
|
|
5092
5375
|
}
|
|
5093
5376
|
function headersToRecord(headers) {
|
|
5094
5377
|
const result = {};
|
|
@@ -5147,7 +5430,7 @@ async function ensureSymlink(target, linkPath) {
|
|
|
5147
5430
|
} catch {
|
|
5148
5431
|
}
|
|
5149
5432
|
await mkdir10(path13.dirname(linkPath), { recursive: true });
|
|
5150
|
-
await symlink3(target, linkPath,
|
|
5433
|
+
await symlink3(target, linkPath, directoryLinkType());
|
|
5151
5434
|
}
|
|
5152
5435
|
|
|
5153
5436
|
// src/workspace-registry.ts
|
|
@@ -5604,7 +5887,30 @@ import { mkdir as mkdir12 } from "fs/promises";
|
|
|
5604
5887
|
import path15 from "path";
|
|
5605
5888
|
var RECOVERED_DAEMON_STOP_TIMEOUT_MS = 1e4;
|
|
5606
5889
|
var RECOVERED_DAEMON_STOP_POLL_MS = 100;
|
|
5890
|
+
var SHUTDOWN_FORCE_EXIT_MS = 1e4;
|
|
5891
|
+
var SHUTDOWN_STEP_TIMEOUT_MS = 2e3;
|
|
5607
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
|
+
};
|
|
5608
5914
|
async function stopRecoveredIdleDaemon(config, configRoot) {
|
|
5609
5915
|
const stopStatus = await requestDaemonControl(config, "stop-if-idle", configRoot).catch(
|
|
5610
5916
|
() => null
|
|
@@ -5724,18 +6030,30 @@ async function runConnectorDaemon(launchConfig, options = {}) {
|
|
|
5724
6030
|
const shutdown = async (shutdownOptions = {}) => {
|
|
5725
6031
|
if (shuttingDown) return;
|
|
5726
6032
|
shuttingDown = true;
|
|
6033
|
+
const forceExit = options.exitOnShutdown === false ? void 0 : setTimeout(() => process.exit(0), SHUTDOWN_FORCE_EXIT_MS);
|
|
5727
6034
|
if (interval) clearInterval(interval);
|
|
5728
6035
|
relay.stop();
|
|
5729
6036
|
try {
|
|
5730
|
-
await
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
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
|
+
]);
|
|
5735
6050
|
} finally {
|
|
5736
|
-
await
|
|
5737
|
-
|
|
5738
|
-
|
|
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);
|
|
5739
6057
|
if (options.exitOnShutdown !== false) process.exit(0);
|
|
5740
6058
|
}
|
|
5741
6059
|
};
|
|
@@ -5750,8 +6068,11 @@ async function runConnectorDaemon(launchConfig, options = {}) {
|
|
|
5750
6068
|
});
|
|
5751
6069
|
}).catch((error) => {
|
|
5752
6070
|
heartbeatHealthy = false;
|
|
5753
|
-
if (error instanceof ConnectorDeletedError) {
|
|
5754
|
-
void writeLog(
|
|
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
|
+
);
|
|
5755
6076
|
void shutdown({ unregister: false });
|
|
5756
6077
|
return;
|
|
5757
6078
|
}
|
|
@@ -5775,6 +6096,10 @@ async function runConnectorDaemon(launchConfig, options = {}) {
|
|
|
5775
6096
|
process.once("SIGINT", () => void shutdown());
|
|
5776
6097
|
process.once("SIGTERM", () => void shutdown());
|
|
5777
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);
|
|
5778
6103
|
if (bunnyDaemon) await stopEmbeddedBunnyDaemon(bunnyDaemon);
|
|
5779
6104
|
await daemonLock.release();
|
|
5780
6105
|
await restoreConsole?.();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buda-ai/connector",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
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"
|