@hua-labs/tap 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -9
- package/dist/bridges/codex-app-server-auth-gateway.d.mts +9 -1
- package/dist/bridges/codex-app-server-auth-gateway.mjs +183 -14
- package/dist/bridges/codex-app-server-auth-gateway.mjs.map +1 -1
- package/dist/bridges/codex-app-server-bridge.d.mts +224 -5
- package/dist/bridges/codex-app-server-bridge.mjs +1138 -687
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
- package/dist/bridges/codex-bridge-runner.mjs +17 -2
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/cli.mjs +703 -95
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +502 -57
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.mjs +327 -70
- package/dist/mcp-server.mjs.map +1 -1
- package/package.json +6 -4
- package/LICENSE +0 -21
package/dist/cli.mjs
CHANGED
|
@@ -61,7 +61,7 @@ function findRepoRoot(startDir = process.cwd()) {
|
|
|
61
61
|
function resolveCommsDir(args, repoRoot) {
|
|
62
62
|
const idx = args.indexOf("--comms-dir");
|
|
63
63
|
if (idx !== -1 && args[idx + 1]) {
|
|
64
|
-
return path.resolve(args[idx + 1]);
|
|
64
|
+
return path.resolve(normalizeTapPath(args[idx + 1]));
|
|
65
65
|
}
|
|
66
66
|
const { config } = resolveConfig({}, repoRoot);
|
|
67
67
|
return config.commsDir;
|
|
@@ -69,8 +69,8 @@ function resolveCommsDir(args, repoRoot) {
|
|
|
69
69
|
function createAdapterContext(commsDir, repoRoot) {
|
|
70
70
|
const { config } = resolveConfig({}, repoRoot);
|
|
71
71
|
return {
|
|
72
|
-
commsDir: path.resolve(commsDir),
|
|
73
|
-
repoRoot: path.resolve(repoRoot),
|
|
72
|
+
commsDir: path.resolve(normalizeTapPath(commsDir)),
|
|
73
|
+
repoRoot: path.resolve(normalizeTapPath(repoRoot)),
|
|
74
74
|
stateDir: config.stateDir,
|
|
75
75
|
platform: detectPlatform()
|
|
76
76
|
};
|
|
@@ -1446,6 +1446,7 @@ function readArtifactBackup(backupPath) {
|
|
|
1446
1446
|
// src/adapters/codex.ts
|
|
1447
1447
|
var MCP_SELECTOR = "mcp_servers.tap";
|
|
1448
1448
|
var ENV_SELECTOR = "mcp_servers.tap.env";
|
|
1449
|
+
var SESSION_NEUTRAL_AGENT_NAME = "<set-per-session>";
|
|
1449
1450
|
var OLD_MCP_SELECTOR = "mcp_servers.tap-comms";
|
|
1450
1451
|
var OLD_ENV_SELECTOR = "mcp_servers.tap-comms.env";
|
|
1451
1452
|
function findCodexConfigPath2() {
|
|
@@ -1490,9 +1491,26 @@ function writeTomlFile(filePath, content) {
|
|
|
1490
1491
|
fs10.writeFileSync(tmp, content, "utf-8");
|
|
1491
1492
|
fs10.renameSync(tmp, filePath);
|
|
1492
1493
|
}
|
|
1494
|
+
function buildSessionNeutralCodexSpec(ctx) {
|
|
1495
|
+
const managed = buildManagedMcpServerSpec(ctx);
|
|
1496
|
+
const env = {
|
|
1497
|
+
...managed.env,
|
|
1498
|
+
TAP_AGENT_NAME: SESSION_NEUTRAL_AGENT_NAME
|
|
1499
|
+
};
|
|
1500
|
+
delete env.TAP_AGENT_ID;
|
|
1501
|
+
return { ...managed, env };
|
|
1502
|
+
}
|
|
1503
|
+
function buildCodexEnvEntries(existingTable, managedEnv) {
|
|
1504
|
+
const preservedEnv = parseTomlAssignments(existingTable ?? "");
|
|
1505
|
+
delete preservedEnv.TAP_AGENT_ID;
|
|
1506
|
+
return {
|
|
1507
|
+
...preservedEnv,
|
|
1508
|
+
...managedEnv
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1493
1511
|
function verifyManagedToml(content, ctx, configPath) {
|
|
1494
1512
|
const checks = [];
|
|
1495
|
-
const managed =
|
|
1513
|
+
const managed = buildSessionNeutralCodexSpec(ctx);
|
|
1496
1514
|
const mainTable = extractTomlTable(content, MCP_SELECTOR);
|
|
1497
1515
|
const envTable = extractTomlTable(content, ENV_SELECTOR);
|
|
1498
1516
|
checks.push({
|
|
@@ -1529,6 +1547,19 @@ function verifyManagedToml(content, ctx, configPath) {
|
|
|
1529
1547
|
message: "Managed tap command/args do not match expected values"
|
|
1530
1548
|
});
|
|
1531
1549
|
}
|
|
1550
|
+
if (envTable) {
|
|
1551
|
+
const envValues = parseTomlAssignments(envTable);
|
|
1552
|
+
checks.push({
|
|
1553
|
+
name: "Managed TAP_AGENT_NAME is session-neutral",
|
|
1554
|
+
passed: envValues.TAP_AGENT_NAME === managed.env.TAP_AGENT_NAME,
|
|
1555
|
+
message: `TAP_AGENT_NAME should be "${SESSION_NEUTRAL_AGENT_NAME}"`
|
|
1556
|
+
});
|
|
1557
|
+
checks.push({
|
|
1558
|
+
name: "Managed TAP_AGENT_ID is omitted",
|
|
1559
|
+
passed: typeof envValues.TAP_AGENT_ID !== "string",
|
|
1560
|
+
message: "TAP_AGENT_ID should not be persisted in Codex config"
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1532
1563
|
return checks;
|
|
1533
1564
|
}
|
|
1534
1565
|
var codexAdapter = {
|
|
@@ -1612,7 +1643,7 @@ var codexAdapter = {
|
|
|
1612
1643
|
const configPath = plan.operations[0]?.path ?? findCodexConfigPath2();
|
|
1613
1644
|
const warnings = [];
|
|
1614
1645
|
const changedFiles = [];
|
|
1615
|
-
const managed =
|
|
1646
|
+
const managed = buildSessionNeutralCodexSpec(ctx);
|
|
1616
1647
|
warnings.push(...managed.warnings);
|
|
1617
1648
|
if (managed.issues.length > 0 || !managed.command) {
|
|
1618
1649
|
return {
|
|
@@ -1669,8 +1700,10 @@ var codexAdapter = {
|
|
|
1669
1700
|
ENV_SELECTOR,
|
|
1670
1701
|
renderTomlTable(
|
|
1671
1702
|
ENV_SELECTOR,
|
|
1672
|
-
|
|
1673
|
-
|
|
1703
|
+
buildCodexEnvEntries(
|
|
1704
|
+
extractTomlTable(existingContent, ENV_SELECTOR),
|
|
1705
|
+
managed.env
|
|
1706
|
+
)
|
|
1674
1707
|
)
|
|
1675
1708
|
);
|
|
1676
1709
|
for (const target of getTrustTargets(ctx)) {
|
|
@@ -2437,14 +2470,55 @@ function findListeningProcessId(url, platform) {
|
|
|
2437
2470
|
// src/engine/bridge-unix-spawn.ts
|
|
2438
2471
|
import * as fs15 from "fs";
|
|
2439
2472
|
import { spawn, spawnSync as spawnSync4 } from "child_process";
|
|
2440
|
-
|
|
2473
|
+
var DEFAULT_UNIX_PLATFORM = process.platform === "darwin" ? "darwin" : "linux";
|
|
2474
|
+
function resolveUnixSpawnCommand(command, args, platform) {
|
|
2475
|
+
if (platform === "linux") {
|
|
2476
|
+
return {
|
|
2477
|
+
command: "nohup",
|
|
2478
|
+
args: [command, ...args]
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
return { command, args };
|
|
2482
|
+
}
|
|
2483
|
+
function findListeningPidWithLsof(port) {
|
|
2484
|
+
const result = spawnSync4(
|
|
2485
|
+
"lsof",
|
|
2486
|
+
["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"],
|
|
2487
|
+
{
|
|
2488
|
+
encoding: "utf-8",
|
|
2489
|
+
windowsHide: true
|
|
2490
|
+
}
|
|
2491
|
+
);
|
|
2492
|
+
if (!result || result.status !== 0) {
|
|
2493
|
+
return null;
|
|
2494
|
+
}
|
|
2495
|
+
const parsedPid = Number.parseInt((result.stdout ?? "").trim(), 10);
|
|
2496
|
+
return Number.isFinite(parsedPid) ? parsedPid : null;
|
|
2497
|
+
}
|
|
2498
|
+
function findListeningPidWithSs(port) {
|
|
2499
|
+
const result = spawnSync4("ss", ["-ltnpH", `sport = :${port}`], {
|
|
2500
|
+
encoding: "utf-8",
|
|
2501
|
+
windowsHide: true
|
|
2502
|
+
});
|
|
2503
|
+
if (!result || result.status !== 0) {
|
|
2504
|
+
return null;
|
|
2505
|
+
}
|
|
2506
|
+
const match = (result.stdout ?? "").match(/\bpid=(\d+)\b/);
|
|
2507
|
+
if (!match) {
|
|
2508
|
+
return null;
|
|
2509
|
+
}
|
|
2510
|
+
const parsedPid = Number.parseInt(match[1], 10);
|
|
2511
|
+
return Number.isFinite(parsedPid) ? parsedPid : null;
|
|
2512
|
+
}
|
|
2513
|
+
function startUnixDetachedProcess(command, args, repoRoot, logPath, env = process.env, platform = DEFAULT_UNIX_PLATFORM) {
|
|
2441
2514
|
const stderrPath = stderrLogFilePath(logPath);
|
|
2442
2515
|
let logFd = null;
|
|
2443
2516
|
let stderrFd = null;
|
|
2444
2517
|
try {
|
|
2445
2518
|
logFd = fs15.openSync(logPath, "a");
|
|
2446
2519
|
stderrFd = fs15.openSync(stderrPath, "a");
|
|
2447
|
-
const
|
|
2520
|
+
const launch = resolveUnixSpawnCommand(command, args, platform);
|
|
2521
|
+
const child = spawn(launch.command, launch.args, {
|
|
2448
2522
|
cwd: repoRoot,
|
|
2449
2523
|
detached: true,
|
|
2450
2524
|
stdio: ["ignore", logFd, stderrFd],
|
|
@@ -2462,13 +2536,15 @@ function startUnixDetachedProcess(command, args, repoRoot, logPath, env = proces
|
|
|
2462
2536
|
}
|
|
2463
2537
|
}
|
|
2464
2538
|
}
|
|
2465
|
-
function startUnixCodexAppServer(command, url, repoRoot, logPath) {
|
|
2539
|
+
function startUnixCodexAppServer(command, url, repoRoot, logPath, platform = DEFAULT_UNIX_PLATFORM) {
|
|
2466
2540
|
const { command: exe, prefixArgs } = splitResolvedCommand(command);
|
|
2467
2541
|
return startUnixDetachedProcess(
|
|
2468
2542
|
exe,
|
|
2469
2543
|
[...prefixArgs, "app-server", "--listen", url],
|
|
2470
2544
|
repoRoot,
|
|
2471
|
-
logPath
|
|
2545
|
+
logPath,
|
|
2546
|
+
process.env,
|
|
2547
|
+
platform
|
|
2472
2548
|
);
|
|
2473
2549
|
}
|
|
2474
2550
|
function findUnixListeningProcessId(url, platform) {
|
|
@@ -2485,23 +2561,17 @@ function findUnixListeningProcessId(url, platform) {
|
|
|
2485
2561
|
if (port == null || !Number.isFinite(port)) {
|
|
2486
2562
|
return null;
|
|
2487
2563
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
encoding: "utf-8",
|
|
2493
|
-
windowsHide: true
|
|
2564
|
+
if (platform === "linux") {
|
|
2565
|
+
const ssPid = findListeningPidWithSs(port);
|
|
2566
|
+
if (ssPid != null) {
|
|
2567
|
+
return ssPid;
|
|
2494
2568
|
}
|
|
2495
|
-
);
|
|
2496
|
-
if (!result || result.status !== 0) {
|
|
2497
|
-
return null;
|
|
2498
2569
|
}
|
|
2499
|
-
|
|
2500
|
-
return Number.isFinite(parsedPid) ? parsedPid : null;
|
|
2570
|
+
return findListeningPidWithLsof(port);
|
|
2501
2571
|
}
|
|
2502
2572
|
|
|
2503
2573
|
// src/engine/bridge-process-control.ts
|
|
2504
|
-
import { execSync as execSync2 } from "child_process";
|
|
2574
|
+
import { execSync as execSync2, spawnSync as spawnSync5 } from "child_process";
|
|
2505
2575
|
function isProcessAlive(pid) {
|
|
2506
2576
|
try {
|
|
2507
2577
|
process.kill(pid, 0);
|
|
@@ -2510,6 +2580,25 @@ function isProcessAlive(pid) {
|
|
|
2510
2580
|
return false;
|
|
2511
2581
|
}
|
|
2512
2582
|
}
|
|
2583
|
+
function getUnixProcessGroupId(pid) {
|
|
2584
|
+
const result = spawnSync5("ps", ["-o", "pgid=", "-p", String(pid)], {
|
|
2585
|
+
encoding: "utf-8",
|
|
2586
|
+
windowsHide: true
|
|
2587
|
+
});
|
|
2588
|
+
if (!result || result.status !== 0) {
|
|
2589
|
+
return null;
|
|
2590
|
+
}
|
|
2591
|
+
const parsed = Number.parseInt((result.stdout ?? "").trim(), 10);
|
|
2592
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2593
|
+
}
|
|
2594
|
+
function isUnixProcessGroupAlive(processGroupId) {
|
|
2595
|
+
try {
|
|
2596
|
+
process.kill(-processGroupId, 0);
|
|
2597
|
+
return true;
|
|
2598
|
+
} catch {
|
|
2599
|
+
return false;
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2513
2602
|
async function terminateProcess(pid, platform) {
|
|
2514
2603
|
if (!isProcessAlive(pid)) {
|
|
2515
2604
|
return false;
|
|
@@ -2518,11 +2607,16 @@ async function terminateProcess(pid, platform) {
|
|
|
2518
2607
|
if (platform === "win32") {
|
|
2519
2608
|
execSync2(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
|
|
2520
2609
|
} else {
|
|
2521
|
-
|
|
2610
|
+
const processGroupId = getUnixProcessGroupId(pid);
|
|
2611
|
+
const signalTarget = processGroupId != null ? -processGroupId : pid;
|
|
2612
|
+
const isTargetAlive = () => processGroupId != null ? isUnixProcessGroupAlive(processGroupId) : isProcessAlive(pid);
|
|
2613
|
+
process.kill(signalTarget, "SIGTERM");
|
|
2522
2614
|
await delay(2e3);
|
|
2523
|
-
if (
|
|
2524
|
-
process.kill(
|
|
2615
|
+
if (isTargetAlive()) {
|
|
2616
|
+
process.kill(signalTarget, "SIGKILL");
|
|
2617
|
+
await delay(500);
|
|
2525
2618
|
}
|
|
2619
|
+
return !isTargetAlive();
|
|
2526
2620
|
}
|
|
2527
2621
|
} catch {
|
|
2528
2622
|
}
|
|
@@ -2712,8 +2806,10 @@ function rotateLog(logPath) {
|
|
|
2712
2806
|
}
|
|
2713
2807
|
|
|
2714
2808
|
// src/engine/bridge-app-server-health.ts
|
|
2809
|
+
import * as net2 from "net";
|
|
2715
2810
|
var APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
|
|
2716
2811
|
var APP_SERVER_HEALTH_RETRY_MS = 250;
|
|
2812
|
+
var APP_SERVER_READYZ_PATH = "/readyz";
|
|
2717
2813
|
var AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
|
|
2718
2814
|
async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS, gatewayToken) {
|
|
2719
2815
|
const WebSocket = getWebSocketCtor();
|
|
@@ -2747,10 +2843,100 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
|
|
|
2747
2843
|
}
|
|
2748
2844
|
});
|
|
2749
2845
|
}
|
|
2750
|
-
|
|
2846
|
+
function buildAppServerReadyzUrl(url) {
|
|
2847
|
+
let parsed;
|
|
2848
|
+
try {
|
|
2849
|
+
parsed = new URL(url);
|
|
2850
|
+
} catch {
|
|
2851
|
+
return null;
|
|
2852
|
+
}
|
|
2853
|
+
if (parsed.protocol === "ws:") {
|
|
2854
|
+
parsed.protocol = "http:";
|
|
2855
|
+
} else if (parsed.protocol === "wss:") {
|
|
2856
|
+
parsed.protocol = "https:";
|
|
2857
|
+
} else if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
2858
|
+
return null;
|
|
2859
|
+
}
|
|
2860
|
+
parsed.pathname = APP_SERVER_READYZ_PATH;
|
|
2861
|
+
parsed.search = "";
|
|
2862
|
+
parsed.hash = "";
|
|
2863
|
+
return parsed.toString();
|
|
2864
|
+
}
|
|
2865
|
+
async function checkAppServerReadyz(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
|
|
2866
|
+
const readyzUrl = buildAppServerReadyzUrl(url);
|
|
2867
|
+
if (!readyzUrl) {
|
|
2868
|
+
return "unsupported";
|
|
2869
|
+
}
|
|
2870
|
+
const controller = new AbortController();
|
|
2871
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2872
|
+
try {
|
|
2873
|
+
const response = await fetch(readyzUrl, {
|
|
2874
|
+
method: "GET",
|
|
2875
|
+
signal: controller.signal,
|
|
2876
|
+
headers: {
|
|
2877
|
+
accept: "application/json"
|
|
2878
|
+
}
|
|
2879
|
+
});
|
|
2880
|
+
if (response.ok) {
|
|
2881
|
+
return "ready";
|
|
2882
|
+
}
|
|
2883
|
+
if (response.status === 400 || response.status === 404 || response.status === 405 || response.status === 426 || response.status === 501) {
|
|
2884
|
+
return "unsupported";
|
|
2885
|
+
}
|
|
2886
|
+
return "not-ready";
|
|
2887
|
+
} catch {
|
|
2888
|
+
return "not-ready";
|
|
2889
|
+
} finally {
|
|
2890
|
+
clearTimeout(timer);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
async function checkTcpPortListening(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
|
|
2894
|
+
let hostname;
|
|
2895
|
+
let port;
|
|
2896
|
+
try {
|
|
2897
|
+
const parsed = new URL(url.replace(/^ws/, "http"));
|
|
2898
|
+
hostname = parsed.hostname;
|
|
2899
|
+
port = parseInt(parsed.port, 10);
|
|
2900
|
+
} catch {
|
|
2901
|
+
return false;
|
|
2902
|
+
}
|
|
2903
|
+
if (!port || !Number.isFinite(port)) return false;
|
|
2904
|
+
return new Promise((resolve14) => {
|
|
2905
|
+
const socket = net2.createConnection({ host: hostname, port });
|
|
2906
|
+
const timer = setTimeout(() => {
|
|
2907
|
+
socket.destroy();
|
|
2908
|
+
resolve14(false);
|
|
2909
|
+
}, timeoutMs);
|
|
2910
|
+
socket.once("connect", () => {
|
|
2911
|
+
clearTimeout(timer);
|
|
2912
|
+
socket.destroy();
|
|
2913
|
+
resolve14(true);
|
|
2914
|
+
});
|
|
2915
|
+
socket.once("error", () => {
|
|
2916
|
+
clearTimeout(timer);
|
|
2917
|
+
socket.destroy();
|
|
2918
|
+
resolve14(false);
|
|
2919
|
+
});
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
async function checkManagedAppServerReady(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
|
|
2923
|
+
const readyzStatus = await checkAppServerReadyz(url, timeoutMs);
|
|
2924
|
+
if (readyzStatus === "ready") {
|
|
2925
|
+
return true;
|
|
2926
|
+
}
|
|
2927
|
+
if (readyzStatus === "unsupported") {
|
|
2928
|
+
return checkTcpPortListening(url, timeoutMs);
|
|
2929
|
+
}
|
|
2930
|
+
return false;
|
|
2931
|
+
}
|
|
2932
|
+
async function waitForManagedAppServerReady(url, timeoutMs) {
|
|
2751
2933
|
const deadline = Date.now() + timeoutMs;
|
|
2752
2934
|
while (Date.now() < deadline) {
|
|
2753
|
-
|
|
2935
|
+
const remaining = Math.max(
|
|
2936
|
+
1,
|
|
2937
|
+
Math.min(APP_SERVER_HEALTH_TIMEOUT_MS, deadline - Date.now())
|
|
2938
|
+
);
|
|
2939
|
+
if (await checkManagedAppServerReady(url, remaining)) {
|
|
2754
2940
|
return true;
|
|
2755
2941
|
}
|
|
2756
2942
|
await delay(APP_SERVER_HEALTH_RETRY_MS);
|
|
@@ -3023,7 +3209,8 @@ async function createManagedAppServerAuth(options) {
|
|
|
3023
3209
|
gatewayArgs,
|
|
3024
3210
|
options.repoRoot,
|
|
3025
3211
|
gatewayLogPath,
|
|
3026
|
-
gatewayEnv
|
|
3212
|
+
gatewayEnv,
|
|
3213
|
+
options.platform
|
|
3027
3214
|
);
|
|
3028
3215
|
} catch (error) {
|
|
3029
3216
|
removeFileIfExists(tokenPath);
|
|
@@ -3198,7 +3385,8 @@ Start it manually:
|
|
|
3198
3385
|
resolvedCommand,
|
|
3199
3386
|
effectiveUrl,
|
|
3200
3387
|
options.repoRoot,
|
|
3201
|
-
logPath
|
|
3388
|
+
logPath,
|
|
3389
|
+
options.platform
|
|
3202
3390
|
);
|
|
3203
3391
|
} catch (err) {
|
|
3204
3392
|
throw new Error(
|
|
@@ -3216,7 +3404,7 @@ Start it manually:
|
|
|
3216
3404
|
${manualCommand2}`
|
|
3217
3405
|
);
|
|
3218
3406
|
}
|
|
3219
|
-
const healthy2 = await
|
|
3407
|
+
const healthy2 = await waitForManagedAppServerReady(
|
|
3220
3408
|
effectiveUrl,
|
|
3221
3409
|
APP_SERVER_START_TIMEOUT_MS
|
|
3222
3410
|
);
|
|
@@ -3278,7 +3466,8 @@ Start it manually:
|
|
|
3278
3466
|
resolvedCommand,
|
|
3279
3467
|
auth.upstreamUrl,
|
|
3280
3468
|
options.repoRoot,
|
|
3281
|
-
logPath
|
|
3469
|
+
logPath,
|
|
3470
|
+
options.platform
|
|
3282
3471
|
);
|
|
3283
3472
|
} catch (err) {
|
|
3284
3473
|
if (auth.gatewayPid != null) {
|
|
@@ -3304,7 +3493,7 @@ Start it manually:
|
|
|
3304
3493
|
${manualCommand}`
|
|
3305
3494
|
);
|
|
3306
3495
|
}
|
|
3307
|
-
const healthy = await
|
|
3496
|
+
const healthy = await waitForManagedAppServerReady(
|
|
3308
3497
|
auth.upstreamUrl,
|
|
3309
3498
|
APP_SERVER_START_TIMEOUT_MS
|
|
3310
3499
|
);
|
|
@@ -3330,10 +3519,9 @@ Or start it manually:
|
|
|
3330
3519
|
removeFileIfExists(auth.tokenPath);
|
|
3331
3520
|
throw new Error("Tap auth gateway token is missing after startup.");
|
|
3332
3521
|
}
|
|
3333
|
-
const gatewayHealthy = await
|
|
3522
|
+
const gatewayHealthy = await waitForManagedAppServerReady(
|
|
3334
3523
|
effectiveUrl,
|
|
3335
|
-
APP_SERVER_GATEWAY_START_TIMEOUT_MS
|
|
3336
|
-
gatewayToken
|
|
3524
|
+
APP_SERVER_GATEWAY_START_TIMEOUT_MS
|
|
3337
3525
|
);
|
|
3338
3526
|
if (!gatewayHealthy) {
|
|
3339
3527
|
await terminateProcess(pid, options.platform);
|
|
@@ -3480,7 +3668,8 @@ async function startBridge(options) {
|
|
|
3480
3668
|
[bridgeScript],
|
|
3481
3669
|
repoRoot,
|
|
3482
3670
|
logPath,
|
|
3483
|
-
bridgeEnv
|
|
3671
|
+
bridgeEnv,
|
|
3672
|
+
options.platform
|
|
3484
3673
|
);
|
|
3485
3674
|
if (!bridgePid) {
|
|
3486
3675
|
throw new Error(`Failed to spawn bridge process for ${instanceId}`);
|
|
@@ -3839,6 +4028,7 @@ async function addCommand(args) {
|
|
|
3839
4028
|
);
|
|
3840
4029
|
}
|
|
3841
4030
|
let bridge = null;
|
|
4031
|
+
let effectivePort = port;
|
|
3842
4032
|
if (mode === "app-server") {
|
|
3843
4033
|
const bridgeScript = adapter.resolveBridgeScript?.(ctx);
|
|
3844
4034
|
if (!bridgeScript) {
|
|
@@ -3846,8 +4036,19 @@ async function addCommand(args) {
|
|
|
3846
4036
|
warnings.push("Bridge script not found. Run bridge manually.");
|
|
3847
4037
|
} else {
|
|
3848
4038
|
const { config: resolvedCfg } = resolveConfig({}, repoRoot);
|
|
4039
|
+
if (effectivePort == null && runtime === "codex") {
|
|
4040
|
+
const currentState = loadState(repoRoot);
|
|
4041
|
+
effectivePort = await findNextAvailableAppServerPort(
|
|
4042
|
+
currentState,
|
|
4043
|
+
resolvedCfg.appServerUrl,
|
|
4044
|
+
4501,
|
|
4045
|
+
instanceId
|
|
4046
|
+
);
|
|
4047
|
+
log(`Auto-assigned port ${effectivePort} for ${instanceId}`);
|
|
4048
|
+
}
|
|
3849
4049
|
log(`Starting bridge: ${bridgeScript}`);
|
|
3850
4050
|
try {
|
|
4051
|
+
const manageAppServer = runtime === "codex";
|
|
3851
4052
|
bridge = await startBridge({
|
|
3852
4053
|
instanceId,
|
|
3853
4054
|
runtime,
|
|
@@ -3859,7 +4060,8 @@ async function addCommand(args) {
|
|
|
3859
4060
|
runtimeCommand: resolvedCfg.runtimeCommand,
|
|
3860
4061
|
appServerUrl: resolvedCfg.appServerUrl,
|
|
3861
4062
|
repoRoot,
|
|
3862
|
-
port:
|
|
4063
|
+
port: effectivePort ?? void 0,
|
|
4064
|
+
manageAppServer,
|
|
3863
4065
|
headless
|
|
3864
4066
|
});
|
|
3865
4067
|
logSuccess(`Bridge started (PID: ${bridge.pid})`);
|
|
@@ -3874,7 +4076,7 @@ async function addCommand(args) {
|
|
|
3874
4076
|
instanceId,
|
|
3875
4077
|
runtime,
|
|
3876
4078
|
agentName: resolvedAgentName,
|
|
3877
|
-
port,
|
|
4079
|
+
port: effectivePort,
|
|
3878
4080
|
installed: true,
|
|
3879
4081
|
configPath: probe.configPath ?? "",
|
|
3880
4082
|
bridgeMode: mode,
|
|
@@ -3884,6 +4086,8 @@ async function addCommand(args) {
|
|
|
3884
4086
|
lastAppliedHash: result.lastAppliedHash,
|
|
3885
4087
|
lastVerifiedAt: verify.ok ? (/* @__PURE__ */ new Date()).toISOString() : null,
|
|
3886
4088
|
bridge,
|
|
4089
|
+
manageAppServer: runtime === "codex",
|
|
4090
|
+
noAuth: false,
|
|
3887
4091
|
headless,
|
|
3888
4092
|
warnings: Array.from(/* @__PURE__ */ new Set([...result.warnings, ...verify.warnings]))
|
|
3889
4093
|
};
|
|
@@ -4318,12 +4522,76 @@ async function removeCommand(args) {
|
|
|
4318
4522
|
}
|
|
4319
4523
|
|
|
4320
4524
|
// src/commands/bridge.ts
|
|
4525
|
+
import { existsSync as existsSync21, readFileSync as readFileSync17, renameSync as renameSync11, writeFileSync as writeFileSync12 } from "fs";
|
|
4321
4526
|
import * as path22 from "path";
|
|
4322
4527
|
function formatAge(seconds) {
|
|
4323
4528
|
if (seconds < 60) return `${seconds}s ago`;
|
|
4324
4529
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
4325
4530
|
return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m ago`;
|
|
4326
4531
|
}
|
|
4532
|
+
var BRIDGE_UP_ACTIVE_HEARTBEAT_WINDOW_MS = 10 * 60 * 1e3;
|
|
4533
|
+
var BRIDGE_UP_ORPHAN_HEARTBEAT_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
4534
|
+
var BRIDGE_UP_SIGNING_OFF_HEARTBEAT_WINDOW_MS = 5 * 60 * 1e3;
|
|
4535
|
+
function loadBridgeHeartbeatStore(commsDir) {
|
|
4536
|
+
const heartbeatsPath = path22.join(commsDir, "heartbeats.json");
|
|
4537
|
+
if (!existsSync21(heartbeatsPath)) return {};
|
|
4538
|
+
try {
|
|
4539
|
+
return JSON.parse(readFileSync17(heartbeatsPath, "utf-8"));
|
|
4540
|
+
} catch {
|
|
4541
|
+
return null;
|
|
4542
|
+
}
|
|
4543
|
+
}
|
|
4544
|
+
function saveBridgeHeartbeatStore(commsDir, store) {
|
|
4545
|
+
const heartbeatsPath = path22.join(commsDir, "heartbeats.json");
|
|
4546
|
+
const tmp = `${heartbeatsPath}.tmp.${process.pid}`;
|
|
4547
|
+
writeFileSync12(tmp, JSON.stringify(store, null, 2), "utf-8");
|
|
4548
|
+
renameSync11(tmp, heartbeatsPath);
|
|
4549
|
+
}
|
|
4550
|
+
function parseBridgeHeartbeatAgeMs(record, now) {
|
|
4551
|
+
const raw = record.lastActivity ?? record.timestamp;
|
|
4552
|
+
if (!raw) return Number.POSITIVE_INFINITY;
|
|
4553
|
+
const parsed = new Date(raw).getTime();
|
|
4554
|
+
if (!Number.isFinite(parsed)) return Number.POSITIVE_INFINITY;
|
|
4555
|
+
return Math.max(0, now - parsed);
|
|
4556
|
+
}
|
|
4557
|
+
function resolveBridgeHeartbeatInstanceId(state, heartbeatId) {
|
|
4558
|
+
if (state.instances[heartbeatId]) return heartbeatId;
|
|
4559
|
+
const hyphenated = heartbeatId.replace(/_/g, "-");
|
|
4560
|
+
if (state.instances[hyphenated]) return hyphenated;
|
|
4561
|
+
const underscored = heartbeatId.replace(/-/g, "_");
|
|
4562
|
+
if (state.instances[underscored]) return underscored;
|
|
4563
|
+
return null;
|
|
4564
|
+
}
|
|
4565
|
+
function pruneStaleHeartbeatsForBridgeUp(state, stateDir, commsDir) {
|
|
4566
|
+
const store = loadBridgeHeartbeatStore(commsDir);
|
|
4567
|
+
if (store === null) {
|
|
4568
|
+
return {
|
|
4569
|
+
removed: 0,
|
|
4570
|
+
warning: "Auto-clean skipped \u2014 heartbeats.json unreadable"
|
|
4571
|
+
};
|
|
4572
|
+
}
|
|
4573
|
+
const now = Date.now();
|
|
4574
|
+
let removed = 0;
|
|
4575
|
+
for (const [heartbeatId, heartbeat] of Object.entries(store)) {
|
|
4576
|
+
const ageMs = parseBridgeHeartbeatAgeMs(heartbeat, now);
|
|
4577
|
+
const instanceId = resolveBridgeHeartbeatInstanceId(state, heartbeatId);
|
|
4578
|
+
const instance = instanceId ? state.instances[instanceId] : null;
|
|
4579
|
+
const bridgeBacked = instance?.bridgeMode === "app-server";
|
|
4580
|
+
const bridgeRunning = bridgeBacked && instanceId ? getBridgeStatus(stateDir, instanceId) === "running" : false;
|
|
4581
|
+
const status = heartbeat.status ?? "active";
|
|
4582
|
+
const staleByStatus = status === "signing-off" && ageMs >= BRIDGE_UP_SIGNING_OFF_HEARTBEAT_WINDOW_MS;
|
|
4583
|
+
const staleByDeadBridge = bridgeBacked && !bridgeRunning && ageMs >= BRIDGE_UP_ACTIVE_HEARTBEAT_WINDOW_MS;
|
|
4584
|
+
const staleByAge = !bridgeRunning && ageMs >= BRIDGE_UP_ORPHAN_HEARTBEAT_WINDOW_MS;
|
|
4585
|
+
if (staleByStatus || staleByDeadBridge || staleByAge) {
|
|
4586
|
+
delete store[heartbeatId];
|
|
4587
|
+
removed += 1;
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
if (removed > 0) {
|
|
4591
|
+
saveBridgeHeartbeatStore(commsDir, store);
|
|
4592
|
+
}
|
|
4593
|
+
return { removed };
|
|
4594
|
+
}
|
|
4327
4595
|
var BRIDGE_HELP = `
|
|
4328
4596
|
Usage:
|
|
4329
4597
|
tap bridge <subcommand> [instance] [options]
|
|
@@ -4335,6 +4603,7 @@ Subcommands:
|
|
|
4335
4603
|
stop Stop all running bridges
|
|
4336
4604
|
status Show bridge status for all instances
|
|
4337
4605
|
status <instance> Show bridge status for a specific instance
|
|
4606
|
+
tui <instance> Show the safe Codex TUI attach command for a running bridge
|
|
4338
4607
|
watch Monitor bridges and auto-restart stuck/stale ones
|
|
4339
4608
|
|
|
4340
4609
|
Options:
|
|
@@ -4363,6 +4632,7 @@ Examples:
|
|
|
4363
4632
|
npx @hua-labs/tap bridge stop codex
|
|
4364
4633
|
npx @hua-labs/tap bridge stop
|
|
4365
4634
|
npx @hua-labs/tap bridge status
|
|
4635
|
+
npx @hua-labs/tap bridge tui codex
|
|
4366
4636
|
`.trim();
|
|
4367
4637
|
function formatAppServerState(appServer) {
|
|
4368
4638
|
const ownership = appServer.managed ? "managed" : "external";
|
|
@@ -4382,6 +4652,18 @@ function redactProtectedUrl(url) {
|
|
|
4382
4652
|
return url.replace(/[?&]tap_token=[^&]+/g, "");
|
|
4383
4653
|
}
|
|
4384
4654
|
}
|
|
4655
|
+
function resolveTuiConnectUrl(appServer) {
|
|
4656
|
+
return appServer.auth?.upstreamUrl ?? appServer.url;
|
|
4657
|
+
}
|
|
4658
|
+
function quoteCliArg(value) {
|
|
4659
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
4660
|
+
}
|
|
4661
|
+
function formatCodexTuiAttachCommand(tuiConnectUrl, cwd) {
|
|
4662
|
+
return `codex --enable tui_app_server --remote ${quoteCliArg(tuiConnectUrl)} --cd ${quoteCliArg(cwd)}`;
|
|
4663
|
+
}
|
|
4664
|
+
function resolveTuiAttachCwd(repoRoot, stateRepoRoot, runtimeThreadCwd, savedThreadCwd) {
|
|
4665
|
+
return runtimeThreadCwd ?? savedThreadCwd ?? stateRepoRoot ?? repoRoot;
|
|
4666
|
+
}
|
|
4385
4667
|
function loadCurrentBridgeState(stateDir, instanceId, fallback) {
|
|
4386
4668
|
return loadBridgeState(stateDir, instanceId) ?? fallback ?? null;
|
|
4387
4669
|
}
|
|
@@ -4749,6 +5031,26 @@ async function bridgeStartAll(flags = {}) {
|
|
|
4749
5031
|
data: {}
|
|
4750
5032
|
};
|
|
4751
5033
|
}
|
|
5034
|
+
const ctx = createAdapterContext(state.commsDir, repoRoot);
|
|
5035
|
+
const warnings = [];
|
|
5036
|
+
let prunedHeartbeats = 0;
|
|
5037
|
+
if (flags["auto-prune-heartbeats"] === true) {
|
|
5038
|
+
const cleanup = pruneStaleHeartbeatsForBridgeUp(
|
|
5039
|
+
state,
|
|
5040
|
+
ctx.stateDir,
|
|
5041
|
+
ctx.commsDir
|
|
5042
|
+
);
|
|
5043
|
+
prunedHeartbeats = cleanup.removed;
|
|
5044
|
+
if (cleanup.warning) {
|
|
5045
|
+
warnings.push(cleanup.warning);
|
|
5046
|
+
log(cleanup.warning);
|
|
5047
|
+
}
|
|
5048
|
+
if (prunedHeartbeats > 0) {
|
|
5049
|
+
log(
|
|
5050
|
+
`Auto-clean: pruned ${prunedHeartbeats} stale heartbeat entr${prunedHeartbeats === 1 ? "y" : "ies"}`
|
|
5051
|
+
);
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
4752
5054
|
const instanceIds = Object.keys(state.instances);
|
|
4753
5055
|
const appServerInstances = instanceIds.filter((id) => {
|
|
4754
5056
|
const inst = state.instances[id];
|
|
@@ -4757,13 +5059,14 @@ async function bridgeStartAll(flags = {}) {
|
|
|
4757
5059
|
return adapter.bridgeMode() === "app-server";
|
|
4758
5060
|
});
|
|
4759
5061
|
if (appServerInstances.length === 0) {
|
|
5062
|
+
const cleanupSuffix2 = prunedHeartbeats > 0 ? ` Auto-clean pruned ${prunedHeartbeats} stale heartbeat entr${prunedHeartbeats === 1 ? "y" : "ies"}.` : "";
|
|
4760
5063
|
return {
|
|
4761
5064
|
ok: true,
|
|
4762
5065
|
command: "bridge",
|
|
4763
5066
|
code: "TAP_NO_OP",
|
|
4764
|
-
message:
|
|
4765
|
-
warnings
|
|
4766
|
-
data: {}
|
|
5067
|
+
message: `No app-server instances found to start.${cleanupSuffix2}`,
|
|
5068
|
+
warnings,
|
|
5069
|
+
data: { prunedHeartbeats }
|
|
4767
5070
|
};
|
|
4768
5071
|
}
|
|
4769
5072
|
logHeader("@hua-labs/tap bridge start --all");
|
|
@@ -4773,7 +5076,6 @@ async function bridgeStartAll(flags = {}) {
|
|
|
4773
5076
|
log("");
|
|
4774
5077
|
const started = [];
|
|
4775
5078
|
const failed = [];
|
|
4776
|
-
const warnings = [];
|
|
4777
5079
|
for (const instanceId of appServerInstances) {
|
|
4778
5080
|
const inst = state.instances[instanceId];
|
|
4779
5081
|
const storedName = inst?.agentName ?? void 0;
|
|
@@ -4783,8 +5085,26 @@ async function bridgeStartAll(flags = {}) {
|
|
|
4783
5085
|
warnings.push(msg);
|
|
4784
5086
|
continue;
|
|
4785
5087
|
}
|
|
5088
|
+
const stateDir = path22.join(repoRoot, ".tap-comms");
|
|
5089
|
+
const currentBridgeState = loadBridgeState(stateDir, instanceId);
|
|
5090
|
+
const { manageAppServer, noAuth } = inferRestartMode(
|
|
5091
|
+
currentBridgeState,
|
|
5092
|
+
{
|
|
5093
|
+
noServer: flags["no-server"] === true ? true : void 0,
|
|
5094
|
+
noAuth: flags["no-auth"] === true ? true : void 0
|
|
5095
|
+
},
|
|
5096
|
+
{
|
|
5097
|
+
manageAppServer: inst.manageAppServer,
|
|
5098
|
+
noAuth: inst.noAuth
|
|
5099
|
+
}
|
|
5100
|
+
);
|
|
5101
|
+
const mergedFlags = {
|
|
5102
|
+
...flags,
|
|
5103
|
+
...manageAppServer === false ? { "no-server": true } : {},
|
|
5104
|
+
...noAuth === true ? { "no-auth": true } : {}
|
|
5105
|
+
};
|
|
4786
5106
|
log(`Starting ${instanceId} (agent: ${storedName})...`);
|
|
4787
|
-
const result = await bridgeStart(instanceId, storedName,
|
|
5107
|
+
const result = await bridgeStart(instanceId, storedName, mergedFlags);
|
|
4788
5108
|
if (result.ok) {
|
|
4789
5109
|
started.push(instanceId);
|
|
4790
5110
|
logSuccess(`${instanceId} started`);
|
|
@@ -4795,13 +5115,14 @@ async function bridgeStartAll(flags = {}) {
|
|
|
4795
5115
|
log("");
|
|
4796
5116
|
}
|
|
4797
5117
|
const message = started.length > 0 ? `Started ${started.length}/${appServerInstances.length} bridge(s): ${started.join(", ")}` + (failed.length > 0 ? `. Failed: ${failed.join(", ")}` : "") : `No bridges started. Failed: ${failed.join(", ")}`;
|
|
5118
|
+
const cleanupSuffix = prunedHeartbeats > 0 ? ` Auto-clean pruned ${prunedHeartbeats} stale heartbeat entr${prunedHeartbeats === 1 ? "y" : "ies"}.` : "";
|
|
4798
5119
|
return {
|
|
4799
5120
|
ok: failed.length === 0 && started.length > 0,
|
|
4800
5121
|
command: "bridge",
|
|
4801
5122
|
code: started.length > 0 ? "TAP_BRIDGE_START_OK" : "TAP_BRIDGE_START_FAILED",
|
|
4802
|
-
message
|
|
5123
|
+
message: `${message}${cleanupSuffix}`,
|
|
4803
5124
|
warnings,
|
|
4804
|
-
data: { started, failed }
|
|
5125
|
+
data: { started, failed, prunedHeartbeats }
|
|
4805
5126
|
};
|
|
4806
5127
|
}
|
|
4807
5128
|
async function bridgeStopOne(identifier) {
|
|
@@ -5383,6 +5704,122 @@ function bridgeStatusOne(identifier) {
|
|
|
5383
5704
|
}
|
|
5384
5705
|
};
|
|
5385
5706
|
}
|
|
5707
|
+
function bridgeTuiOne(identifier) {
|
|
5708
|
+
const repoRoot = findRepoRoot();
|
|
5709
|
+
const state = loadState(repoRoot);
|
|
5710
|
+
if (!state) {
|
|
5711
|
+
return {
|
|
5712
|
+
ok: false,
|
|
5713
|
+
command: "bridge",
|
|
5714
|
+
code: "TAP_NOT_INITIALIZED",
|
|
5715
|
+
message: "Not initialized. Run: npx @hua-labs/tap init",
|
|
5716
|
+
warnings: [],
|
|
5717
|
+
data: {}
|
|
5718
|
+
};
|
|
5719
|
+
}
|
|
5720
|
+
const resolved = resolveInstanceId(identifier, state);
|
|
5721
|
+
if (!resolved.ok) {
|
|
5722
|
+
return {
|
|
5723
|
+
ok: false,
|
|
5724
|
+
command: "bridge",
|
|
5725
|
+
code: resolved.code,
|
|
5726
|
+
message: resolved.message,
|
|
5727
|
+
warnings: [],
|
|
5728
|
+
data: {}
|
|
5729
|
+
};
|
|
5730
|
+
}
|
|
5731
|
+
const instanceId = resolved.instanceId;
|
|
5732
|
+
const inst = state.instances[instanceId];
|
|
5733
|
+
if (!inst?.installed) {
|
|
5734
|
+
return {
|
|
5735
|
+
ok: false,
|
|
5736
|
+
command: "bridge",
|
|
5737
|
+
instanceId,
|
|
5738
|
+
code: "TAP_INSTANCE_NOT_FOUND",
|
|
5739
|
+
message: `${instanceId} is not installed.`,
|
|
5740
|
+
warnings: [],
|
|
5741
|
+
data: {}
|
|
5742
|
+
};
|
|
5743
|
+
}
|
|
5744
|
+
if (inst.runtime !== "codex" || inst.bridgeMode !== "app-server") {
|
|
5745
|
+
return {
|
|
5746
|
+
ok: false,
|
|
5747
|
+
command: "bridge",
|
|
5748
|
+
instanceId,
|
|
5749
|
+
runtime: inst.runtime,
|
|
5750
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
5751
|
+
message: `${instanceId} does not support Codex TUI attach. Use a Codex app-server bridge instance.`,
|
|
5752
|
+
warnings: [],
|
|
5753
|
+
data: {}
|
|
5754
|
+
};
|
|
5755
|
+
}
|
|
5756
|
+
const { config: resolvedConfig } = resolveConfig({}, repoRoot);
|
|
5757
|
+
const stateDir = resolvedConfig.stateDir;
|
|
5758
|
+
const status = getBridgeStatus(stateDir, instanceId);
|
|
5759
|
+
if (status !== "running") {
|
|
5760
|
+
return {
|
|
5761
|
+
ok: false,
|
|
5762
|
+
command: "bridge",
|
|
5763
|
+
instanceId,
|
|
5764
|
+
runtime: inst.runtime,
|
|
5765
|
+
code: "TAP_BRIDGE_NOT_RUNNING",
|
|
5766
|
+
message: `${instanceId} bridge is ${status}. Start it first with: npx @hua-labs/tap bridge start ${instanceId}`,
|
|
5767
|
+
warnings: [],
|
|
5768
|
+
data: { status }
|
|
5769
|
+
};
|
|
5770
|
+
}
|
|
5771
|
+
const bridgeState = loadBridgeState(stateDir, instanceId);
|
|
5772
|
+
const appServer = bridgeState?.appServer;
|
|
5773
|
+
const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
|
|
5774
|
+
const savedThread = loadRuntimeBridgeThreadState(bridgeState);
|
|
5775
|
+
if (!appServer) {
|
|
5776
|
+
return {
|
|
5777
|
+
ok: false,
|
|
5778
|
+
command: "bridge",
|
|
5779
|
+
instanceId,
|
|
5780
|
+
runtime: inst.runtime,
|
|
5781
|
+
code: "TAP_BRIDGE_NOT_RUNNING",
|
|
5782
|
+
message: `${instanceId} app-server state is missing. Restart the bridge first.`,
|
|
5783
|
+
warnings: [],
|
|
5784
|
+
data: { status }
|
|
5785
|
+
};
|
|
5786
|
+
}
|
|
5787
|
+
const tuiConnectUrl = resolveTuiConnectUrl(appServer);
|
|
5788
|
+
const attachCwd = resolveTuiAttachCwd(
|
|
5789
|
+
repoRoot,
|
|
5790
|
+
state.repoRoot,
|
|
5791
|
+
runtimeHeartbeat?.threadCwd,
|
|
5792
|
+
savedThread?.cwd
|
|
5793
|
+
);
|
|
5794
|
+
const attachCommand = formatCodexTuiAttachCommand(tuiConnectUrl, attachCwd);
|
|
5795
|
+
const warnings = appServer.auth != null ? [
|
|
5796
|
+
"Use the upstream TUI URL, not the protected gateway URL. The protected URL is bridge-only."
|
|
5797
|
+
] : [];
|
|
5798
|
+
logHeader(`@hua-labs/tap bridge tui ${instanceId}`);
|
|
5799
|
+
if (appServer.auth) {
|
|
5800
|
+
log(`Protected: ${redactProtectedUrl(appServer.auth.protectedUrl)}`);
|
|
5801
|
+
log(`Upstream: ${appServer.auth.upstreamUrl}`);
|
|
5802
|
+
}
|
|
5803
|
+
log(`Using: ${tuiConnectUrl}`);
|
|
5804
|
+
log(`Attach: ${attachCommand}`);
|
|
5805
|
+
log("");
|
|
5806
|
+
return {
|
|
5807
|
+
ok: true,
|
|
5808
|
+
command: "bridge",
|
|
5809
|
+
instanceId,
|
|
5810
|
+
runtime: inst.runtime,
|
|
5811
|
+
code: "TAP_BRIDGE_STATUS_OK",
|
|
5812
|
+
message: `${instanceId} TUI attach command ready`,
|
|
5813
|
+
warnings,
|
|
5814
|
+
data: {
|
|
5815
|
+
status,
|
|
5816
|
+
tuiConnectUrl,
|
|
5817
|
+
attachCwd,
|
|
5818
|
+
attachCommand,
|
|
5819
|
+
appServer
|
|
5820
|
+
}
|
|
5821
|
+
};
|
|
5822
|
+
}
|
|
5386
5823
|
async function bridgeRestart(identifier, flags) {
|
|
5387
5824
|
const repoRoot = findRepoRoot();
|
|
5388
5825
|
const state = loadState(repoRoot);
|
|
@@ -5582,6 +6019,19 @@ async function bridgeCommand(args) {
|
|
|
5582
6019
|
}
|
|
5583
6020
|
return bridgeStatusAll();
|
|
5584
6021
|
}
|
|
6022
|
+
case "tui": {
|
|
6023
|
+
if (!identifierArg) {
|
|
6024
|
+
return {
|
|
6025
|
+
ok: false,
|
|
6026
|
+
command: "bridge",
|
|
6027
|
+
code: "TAP_INVALID_ARGUMENT",
|
|
6028
|
+
message: "Missing instance. Usage: npx @hua-labs/tap bridge tui <instance>",
|
|
6029
|
+
warnings: [],
|
|
6030
|
+
data: {}
|
|
6031
|
+
};
|
|
6032
|
+
}
|
|
6033
|
+
return bridgeTuiOne(identifierArg);
|
|
6034
|
+
}
|
|
5585
6035
|
case "watch": {
|
|
5586
6036
|
const intervalStr = typeof flags["interval"] === "string" ? flags["interval"] : void 0;
|
|
5587
6037
|
const interval = intervalStr ? parseInt(intervalStr, 10) : 30;
|
|
@@ -5607,7 +6057,7 @@ async function bridgeCommand(args) {
|
|
|
5607
6057
|
ok: false,
|
|
5608
6058
|
command: "bridge",
|
|
5609
6059
|
code: "TAP_INVALID_ARGUMENT",
|
|
5610
|
-
message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, restart, status`,
|
|
6060
|
+
message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, restart, status, tui`,
|
|
5611
6061
|
warnings: [],
|
|
5612
6062
|
data: {}
|
|
5613
6063
|
};
|
|
@@ -5771,6 +6221,7 @@ Usage:
|
|
|
5771
6221
|
Description:
|
|
5772
6222
|
Start all registered app-server bridge daemons with one command.
|
|
5773
6223
|
This is the orchestration entrypoint for headless/background TAP operation.
|
|
6224
|
+
tap up auto-prunes stale heartbeat entries before bridge startup.
|
|
5774
6225
|
|
|
5775
6226
|
Examples:
|
|
5776
6227
|
npx @hua-labs/tap up
|
|
@@ -5794,7 +6245,12 @@ async function upCommand(args) {
|
|
|
5794
6245
|
process.env.TAP_COLD_START_WARMUP = "true";
|
|
5795
6246
|
let result;
|
|
5796
6247
|
try {
|
|
5797
|
-
result = await bridgeCommand([
|
|
6248
|
+
result = await bridgeCommand([
|
|
6249
|
+
"start",
|
|
6250
|
+
"--all",
|
|
6251
|
+
"--auto-prune-heartbeats",
|
|
6252
|
+
...args
|
|
6253
|
+
]);
|
|
5798
6254
|
} finally {
|
|
5799
6255
|
if (previousColdStartWarmup === void 0) {
|
|
5800
6256
|
delete process.env.TAP_COLD_START_WARMUP;
|
|
@@ -5913,10 +6369,10 @@ async function serveCommand(args) {
|
|
|
5913
6369
|
let commsDir;
|
|
5914
6370
|
const commsDirIdx = args.indexOf("--comms-dir");
|
|
5915
6371
|
if (commsDirIdx !== -1 && args[commsDirIdx + 1]) {
|
|
5916
|
-
commsDir = path24.resolve(args[commsDirIdx + 1]);
|
|
6372
|
+
commsDir = path24.resolve(normalizeTapPath(args[commsDirIdx + 1]));
|
|
5917
6373
|
}
|
|
5918
6374
|
if (!commsDir && process.env.TAP_COMMS_DIR) {
|
|
5919
|
-
commsDir = process.env.TAP_COMMS_DIR;
|
|
6375
|
+
commsDir = path24.resolve(normalizeTapPath(process.env.TAP_COMMS_DIR));
|
|
5920
6376
|
}
|
|
5921
6377
|
if (!commsDir) {
|
|
5922
6378
|
const state = loadState(repoRoot);
|
|
@@ -6518,26 +6974,30 @@ async function dashboardCommand(args) {
|
|
|
6518
6974
|
|
|
6519
6975
|
// src/commands/doctor.ts
|
|
6520
6976
|
import {
|
|
6521
|
-
existsSync as
|
|
6977
|
+
existsSync as existsSync24,
|
|
6522
6978
|
mkdirSync as mkdirSync13,
|
|
6523
6979
|
readdirSync as readdirSync6,
|
|
6524
|
-
readFileSync as
|
|
6525
|
-
renameSync as
|
|
6980
|
+
readFileSync as readFileSync19,
|
|
6981
|
+
renameSync as renameSync12,
|
|
6526
6982
|
statSync as statSync3,
|
|
6527
6983
|
unlinkSync as unlinkSync6,
|
|
6528
|
-
writeFileSync as
|
|
6984
|
+
writeFileSync as writeFileSync14
|
|
6529
6985
|
} from "fs";
|
|
6530
6986
|
import { homedir as homedir3 } from "os";
|
|
6531
|
-
import { spawnSync as
|
|
6987
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
6532
6988
|
import { dirname as dirname15, join as join23, resolve as resolve13 } from "path";
|
|
6533
6989
|
var PASS = "pass";
|
|
6534
6990
|
var WARN = "warn";
|
|
6535
6991
|
var FAIL = "fail";
|
|
6992
|
+
var HEARTBEAT_ACTIVE_WINDOW_MS = 10 * 60 * 1e3;
|
|
6993
|
+
var ORPHAN_HEARTBEAT_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
6994
|
+
var SIGNING_OFF_HEARTBEAT_WINDOW_MS = 5 * 60 * 1e3;
|
|
6536
6995
|
var CODEX_ENV_DRIFT_KEYS = [
|
|
6537
6996
|
"TAP_COMMS_DIR",
|
|
6538
6997
|
"TAP_STATE_DIR",
|
|
6539
6998
|
"TAP_REPO_ROOT"
|
|
6540
6999
|
];
|
|
7000
|
+
var CODEX_SESSION_NEUTRAL_NAME = "<set-per-session>";
|
|
6541
7001
|
function normalizeComparablePath2(value) {
|
|
6542
7002
|
return resolve13(value).replace(/\\/g, "/").toLowerCase();
|
|
6543
7003
|
}
|
|
@@ -6574,8 +7034,8 @@ function writeTomlAtomically(filePath, content) {
|
|
|
6574
7034
|
const dir = dirname15(filePath);
|
|
6575
7035
|
mkdirSync13(dir, { recursive: true });
|
|
6576
7036
|
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
6577
|
-
|
|
6578
|
-
|
|
7037
|
+
writeFileSync14(tmp, content, "utf-8");
|
|
7038
|
+
renameSync12(tmp, filePath);
|
|
6579
7039
|
}
|
|
6580
7040
|
function hasInstalledCodexInstance(state) {
|
|
6581
7041
|
return state ? Object.values(state.instances).some(
|
|
@@ -6585,6 +7045,22 @@ function hasInstalledCodexInstance(state) {
|
|
|
6585
7045
|
function getCodexTrustTargets(repoRoot) {
|
|
6586
7046
|
return [...new Set([repoRoot, process.cwd()].map((value) => resolve13(value)))];
|
|
6587
7047
|
}
|
|
7048
|
+
function buildSessionNeutralCodexEnv(env) {
|
|
7049
|
+
const neutralEnv = {
|
|
7050
|
+
...env,
|
|
7051
|
+
TAP_AGENT_NAME: CODEX_SESSION_NEUTRAL_NAME
|
|
7052
|
+
};
|
|
7053
|
+
delete neutralEnv.TAP_AGENT_ID;
|
|
7054
|
+
return neutralEnv;
|
|
7055
|
+
}
|
|
7056
|
+
function buildCodexEnvEntries2(existingTable, managedEnv) {
|
|
7057
|
+
const preservedEnv = parseTomlAssignments(existingTable ?? "");
|
|
7058
|
+
delete preservedEnv.TAP_AGENT_ID;
|
|
7059
|
+
return {
|
|
7060
|
+
...preservedEnv,
|
|
7061
|
+
...managedEnv
|
|
7062
|
+
};
|
|
7063
|
+
}
|
|
6588
7064
|
function buildCodexDoctorSpec(repoRoot, commsDir) {
|
|
6589
7065
|
const state = loadState(repoRoot);
|
|
6590
7066
|
if (!hasInstalledCodexInstance(state)) {
|
|
@@ -6595,7 +7071,10 @@ function buildCodexDoctorSpec(repoRoot, commsDir) {
|
|
|
6595
7071
|
return {
|
|
6596
7072
|
configPath: findCodexConfigPath3(),
|
|
6597
7073
|
trustTargets: getCodexTrustTargets(repoRoot),
|
|
6598
|
-
managed
|
|
7074
|
+
managed: {
|
|
7075
|
+
...managed,
|
|
7076
|
+
env: buildSessionNeutralCodexEnv(managed.env)
|
|
7077
|
+
}
|
|
6599
7078
|
};
|
|
6600
7079
|
}
|
|
6601
7080
|
function repairCodexConfig(repoRoot, commsDir) {
|
|
@@ -6608,7 +7087,7 @@ function repairCodexConfig(repoRoot, commsDir) {
|
|
|
6608
7087
|
spec.managed.issues[0] ?? "Unable to resolve the managed tap MCP server for Codex."
|
|
6609
7088
|
);
|
|
6610
7089
|
}
|
|
6611
|
-
const existingContent =
|
|
7090
|
+
const existingContent = existsSync24(spec.configPath) ? readFileSync19(spec.configPath, "utf-8") : "";
|
|
6612
7091
|
const existingTapEnvTable = extractTomlTable(
|
|
6613
7092
|
existingContent,
|
|
6614
7093
|
"mcp_servers.tap.env"
|
|
@@ -6626,6 +7105,8 @@ function repairCodexConfig(repoRoot, commsDir) {
|
|
|
6626
7105
|
CODEX_ENV_DRIFT_KEYS.map((key) => [key, spec.managed.env[key]])
|
|
6627
7106
|
)
|
|
6628
7107
|
};
|
|
7108
|
+
repairedEnv.TAP_AGENT_NAME = spec.managed.env.TAP_AGENT_NAME;
|
|
7109
|
+
delete repairedEnv.TAP_AGENT_ID;
|
|
6629
7110
|
let nextContent = existingContent;
|
|
6630
7111
|
if (extractTomlTable(nextContent, "mcp_servers.tap-comms.env")) {
|
|
6631
7112
|
nextContent = removeTomlTable(nextContent, "mcp_servers.tap-comms.env");
|
|
@@ -6650,8 +7131,10 @@ function repairCodexConfig(repoRoot, commsDir) {
|
|
|
6650
7131
|
"mcp_servers.tap.env",
|
|
6651
7132
|
renderTomlTable(
|
|
6652
7133
|
"mcp_servers.tap.env",
|
|
6653
|
-
|
|
6654
|
-
|
|
7134
|
+
buildCodexEnvEntries2(
|
|
7135
|
+
existingTapEnvTable ?? existingLegacyEnvTable,
|
|
7136
|
+
repairedEnv
|
|
7137
|
+
)
|
|
6655
7138
|
)
|
|
6656
7139
|
);
|
|
6657
7140
|
for (const trustTarget of spec.trustTargets) {
|
|
@@ -6670,7 +7153,7 @@ function repairCodexConfig(repoRoot, commsDir) {
|
|
|
6670
7153
|
return `Repaired Codex config at ${spec.configPath}. Restart Codex to reload MCP settings.`;
|
|
6671
7154
|
}
|
|
6672
7155
|
function countFiles(dir, ext = ".md") {
|
|
6673
|
-
if (!
|
|
7156
|
+
if (!existsSync24(dir)) return 0;
|
|
6674
7157
|
try {
|
|
6675
7158
|
return readdirSync6(dir).filter((f) => f.endsWith(ext)).length;
|
|
6676
7159
|
} catch {
|
|
@@ -6678,7 +7161,7 @@ function countFiles(dir, ext = ".md") {
|
|
|
6678
7161
|
}
|
|
6679
7162
|
}
|
|
6680
7163
|
function recentFileCount(dir, withinMs) {
|
|
6681
|
-
if (!
|
|
7164
|
+
if (!existsSync24(dir)) return 0;
|
|
6682
7165
|
const cutoff = Date.now() - withinMs;
|
|
6683
7166
|
let count = 0;
|
|
6684
7167
|
try {
|
|
@@ -6693,13 +7176,85 @@ function recentFileCount(dir, withinMs) {
|
|
|
6693
7176
|
}
|
|
6694
7177
|
return count;
|
|
6695
7178
|
}
|
|
7179
|
+
function loadDoctorHeartbeatStore(commsDir) {
|
|
7180
|
+
const heartbeatsPath = join23(commsDir, "heartbeats.json");
|
|
7181
|
+
if (!existsSync24(heartbeatsPath)) return null;
|
|
7182
|
+
try {
|
|
7183
|
+
return JSON.parse(readFileSync19(heartbeatsPath, "utf-8"));
|
|
7184
|
+
} catch {
|
|
7185
|
+
return null;
|
|
7186
|
+
}
|
|
7187
|
+
}
|
|
7188
|
+
function saveDoctorHeartbeatStore(commsDir, store) {
|
|
7189
|
+
const heartbeatsPath = join23(commsDir, "heartbeats.json");
|
|
7190
|
+
const tmp = `${heartbeatsPath}.tmp.${process.pid}`;
|
|
7191
|
+
writeFileSync14(tmp, JSON.stringify(store, null, 2), "utf-8");
|
|
7192
|
+
renameSync12(tmp, heartbeatsPath);
|
|
7193
|
+
}
|
|
7194
|
+
function parseHeartbeatAgeMs(record, now) {
|
|
7195
|
+
const raw = record.lastActivity ?? record.timestamp;
|
|
7196
|
+
if (!raw) return Number.POSITIVE_INFINITY;
|
|
7197
|
+
const parsed = new Date(raw).getTime();
|
|
7198
|
+
if (!Number.isFinite(parsed)) return Number.POSITIVE_INFINITY;
|
|
7199
|
+
return Math.max(0, now - parsed);
|
|
7200
|
+
}
|
|
7201
|
+
function resolveHeartbeatInstanceId(state, heartbeatId) {
|
|
7202
|
+
if (!state) return null;
|
|
7203
|
+
if (state.instances[heartbeatId]) return heartbeatId;
|
|
7204
|
+
const hyphenated = heartbeatId.replace(/_/g, "-");
|
|
7205
|
+
if (state.instances[hyphenated]) return hyphenated;
|
|
7206
|
+
const underscored = heartbeatId.replace(/-/g, "_");
|
|
7207
|
+
if (state.instances[underscored]) return underscored;
|
|
7208
|
+
return null;
|
|
7209
|
+
}
|
|
7210
|
+
function collectStaleHeartbeatIds(commsDir, state, stateDir) {
|
|
7211
|
+
const store = loadDoctorHeartbeatStore(commsDir);
|
|
7212
|
+
if (!store) return [];
|
|
7213
|
+
const now = Date.now();
|
|
7214
|
+
const stale = [];
|
|
7215
|
+
for (const [heartbeatId, heartbeat] of Object.entries(store)) {
|
|
7216
|
+
const ageMs = parseHeartbeatAgeMs(heartbeat, now);
|
|
7217
|
+
const instanceId = resolveHeartbeatInstanceId(state, heartbeatId);
|
|
7218
|
+
const instance = instanceId ? state?.instances[instanceId] : null;
|
|
7219
|
+
const bridgeBacked = instance?.bridgeMode === "app-server";
|
|
7220
|
+
const bridgeRunning = bridgeBacked && instanceId ? isBridgeRunning(stateDir, instanceId) : false;
|
|
7221
|
+
const status = heartbeat.status ?? "active";
|
|
7222
|
+
const staleByStatus = status === "signing-off" && ageMs >= SIGNING_OFF_HEARTBEAT_WINDOW_MS;
|
|
7223
|
+
const staleByDeadBridge = bridgeBacked && !bridgeRunning && ageMs >= HEARTBEAT_ACTIVE_WINDOW_MS;
|
|
7224
|
+
const staleByAge = !bridgeRunning && ageMs >= ORPHAN_HEARTBEAT_WINDOW_MS;
|
|
7225
|
+
if (staleByStatus || staleByDeadBridge || staleByAge) {
|
|
7226
|
+
stale.push({
|
|
7227
|
+
id: heartbeatId,
|
|
7228
|
+
label: heartbeat.agent?.trim() || heartbeatId,
|
|
7229
|
+
ageMs
|
|
7230
|
+
});
|
|
7231
|
+
}
|
|
7232
|
+
}
|
|
7233
|
+
return stale;
|
|
7234
|
+
}
|
|
7235
|
+
function pruneHeartbeatIds(commsDir, heartbeatIds) {
|
|
7236
|
+
if (heartbeatIds.length === 0) return 0;
|
|
7237
|
+
const store = loadDoctorHeartbeatStore(commsDir);
|
|
7238
|
+
if (!store) return 0;
|
|
7239
|
+
let removed = 0;
|
|
7240
|
+
for (const heartbeatId of new Set(heartbeatIds)) {
|
|
7241
|
+
if (heartbeatId in store) {
|
|
7242
|
+
delete store[heartbeatId];
|
|
7243
|
+
removed += 1;
|
|
7244
|
+
}
|
|
7245
|
+
}
|
|
7246
|
+
if (removed > 0) {
|
|
7247
|
+
saveDoctorHeartbeatStore(commsDir, store);
|
|
7248
|
+
}
|
|
7249
|
+
return removed;
|
|
7250
|
+
}
|
|
6696
7251
|
function checkComms(commsDir) {
|
|
6697
7252
|
const checks = [];
|
|
6698
7253
|
checks.push({
|
|
6699
7254
|
name: "comms directory",
|
|
6700
|
-
status:
|
|
6701
|
-
message:
|
|
6702
|
-
fix:
|
|
7255
|
+
status: existsSync24(commsDir) ? PASS : FAIL,
|
|
7256
|
+
message: existsSync24(commsDir) ? commsDir : `Not found: ${commsDir}`,
|
|
7257
|
+
fix: existsSync24(commsDir) ? void 0 : () => {
|
|
6703
7258
|
mkdirSync13(commsDir, { recursive: true });
|
|
6704
7259
|
return `Created ${commsDir}`;
|
|
6705
7260
|
}
|
|
@@ -6710,7 +7265,7 @@ function checkComms(commsDir) {
|
|
|
6710
7265
|
["findings", false]
|
|
6711
7266
|
]) {
|
|
6712
7267
|
const dir = join23(commsDir, subdir);
|
|
6713
|
-
const exists =
|
|
7268
|
+
const exists = existsSync24(dir);
|
|
6714
7269
|
checks.push({
|
|
6715
7270
|
name: `${subdir} directory`,
|
|
6716
7271
|
status: exists ? PASS : required ? FAIL : WARN,
|
|
@@ -6722,14 +7277,14 @@ function checkComms(commsDir) {
|
|
|
6722
7277
|
});
|
|
6723
7278
|
}
|
|
6724
7279
|
const heartbeats = join23(commsDir, "heartbeats.json");
|
|
6725
|
-
if (
|
|
7280
|
+
if (existsSync24(heartbeats)) {
|
|
6726
7281
|
try {
|
|
6727
|
-
const store = JSON.parse(
|
|
7282
|
+
const store = JSON.parse(readFileSync19(heartbeats, "utf-8"));
|
|
6728
7283
|
const agents = Object.keys(store);
|
|
6729
7284
|
const now = Date.now();
|
|
6730
7285
|
const active = agents.filter((a) => {
|
|
6731
7286
|
const ts = store[a]?.lastActivity;
|
|
6732
|
-
return ts && now - new Date(ts).getTime() <
|
|
7287
|
+
return ts && now - new Date(ts).getTime() < HEARTBEAT_ACTIVE_WINDOW_MS;
|
|
6733
7288
|
});
|
|
6734
7289
|
checks.push({
|
|
6735
7290
|
name: "heartbeats",
|
|
@@ -6752,7 +7307,35 @@ function checkComms(commsDir) {
|
|
|
6752
7307
|
}
|
|
6753
7308
|
return checks;
|
|
6754
7309
|
}
|
|
6755
|
-
function
|
|
7310
|
+
function checkStaleHeartbeats(repoRoot, commsDir, stateDir) {
|
|
7311
|
+
const state = loadState(repoRoot);
|
|
7312
|
+
const stale = collectStaleHeartbeatIds(commsDir, state, stateDir);
|
|
7313
|
+
if (stale.length === 0) {
|
|
7314
|
+
return [
|
|
7315
|
+
{
|
|
7316
|
+
name: "stale heartbeats",
|
|
7317
|
+
status: PASS,
|
|
7318
|
+
message: "none"
|
|
7319
|
+
}
|
|
7320
|
+
];
|
|
7321
|
+
}
|
|
7322
|
+
const preview = stale.slice(0, 3).map((entry) => `${entry.label} (${Math.round(entry.ageMs / 6e4)}m)`).join(", ");
|
|
7323
|
+
return [
|
|
7324
|
+
{
|
|
7325
|
+
name: "stale heartbeats",
|
|
7326
|
+
status: WARN,
|
|
7327
|
+
message: stale.length > 3 ? `${stale.length} stale entries: ${preview}, ...` : `${stale.length} stale entr${stale.length === 1 ? "y" : "ies"}: ${preview}`,
|
|
7328
|
+
fix: () => {
|
|
7329
|
+
const removed = pruneHeartbeatIds(
|
|
7330
|
+
commsDir,
|
|
7331
|
+
stale.map((entry) => entry.id)
|
|
7332
|
+
);
|
|
7333
|
+
return `Pruned ${removed} stale heartbeat entr${removed === 1 ? "y" : "ies"}`;
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
7336
|
+
];
|
|
7337
|
+
}
|
|
7338
|
+
function checkInstances(repoRoot, stateDir, commsDir) {
|
|
6756
7339
|
const checks = [];
|
|
6757
7340
|
const state = loadState(repoRoot);
|
|
6758
7341
|
if (!state) {
|
|
@@ -6799,7 +7382,7 @@ function checkInstances(repoRoot, stateDir) {
|
|
|
6799
7382
|
if (pid) {
|
|
6800
7383
|
try {
|
|
6801
7384
|
if (process.platform === "win32") {
|
|
6802
|
-
|
|
7385
|
+
spawnSync6("taskkill", ["/PID", String(pid), "/F", "/T"], {
|
|
6803
7386
|
stdio: "pipe"
|
|
6804
7387
|
});
|
|
6805
7388
|
} else {
|
|
@@ -6821,7 +7404,13 @@ function checkInstances(repoRoot, stateDir) {
|
|
|
6821
7404
|
currentState.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6822
7405
|
saveState(repoRoot, currentState);
|
|
6823
7406
|
}
|
|
6824
|
-
|
|
7407
|
+
const removedHeartbeats = pruneHeartbeatIds(commsDir, [
|
|
7408
|
+
id,
|
|
7409
|
+
id.replace(/-/g, "_"),
|
|
7410
|
+
id.replace(/_/g, "-")
|
|
7411
|
+
]);
|
|
7412
|
+
const suffix = removedHeartbeats > 0 ? `; pruned ${removedHeartbeats} heartbeat entr${removedHeartbeats === 1 ? "y" : "ies"}` : "";
|
|
7413
|
+
return `Cleaned stale bridge + managed processes for ${id}${suffix}`;
|
|
6825
7414
|
};
|
|
6826
7415
|
} else {
|
|
6827
7416
|
status = WARN;
|
|
@@ -6867,7 +7456,7 @@ function checkInstances(repoRoot, stateDir) {
|
|
|
6867
7456
|
function checkMessageLifecycle(commsDir) {
|
|
6868
7457
|
const checks = [];
|
|
6869
7458
|
const inbox = join23(commsDir, "inbox");
|
|
6870
|
-
if (!
|
|
7459
|
+
if (!existsSync24(inbox)) {
|
|
6871
7460
|
checks.push({
|
|
6872
7461
|
name: "message flow",
|
|
6873
7462
|
status: FAIL,
|
|
@@ -6884,9 +7473,9 @@ function checkMessageLifecycle(commsDir) {
|
|
|
6884
7473
|
message: `${total} total, ${recent1h} in last 1h, ${recent10m} in last 10m`
|
|
6885
7474
|
});
|
|
6886
7475
|
const receiptsPath = join23(commsDir, "receipts", "receipts.json");
|
|
6887
|
-
if (
|
|
7476
|
+
if (existsSync24(receiptsPath)) {
|
|
6888
7477
|
try {
|
|
6889
|
-
const receipts = JSON.parse(
|
|
7478
|
+
const receipts = JSON.parse(readFileSync19(receiptsPath, "utf-8"));
|
|
6890
7479
|
const receiptCount = Object.keys(receipts).length;
|
|
6891
7480
|
checks.push({
|
|
6892
7481
|
name: "read receipts",
|
|
@@ -6906,7 +7495,7 @@ function checkMessageLifecycle(commsDir) {
|
|
|
6906
7495
|
function checkMcpServer(repoRoot) {
|
|
6907
7496
|
const checks = [];
|
|
6908
7497
|
const mcpJson = join23(repoRoot, ".mcp.json");
|
|
6909
|
-
if (!
|
|
7498
|
+
if (!existsSync24(mcpJson)) {
|
|
6910
7499
|
checks.push({
|
|
6911
7500
|
name: "MCP config (.mcp.json)",
|
|
6912
7501
|
status: WARN,
|
|
@@ -6916,7 +7505,7 @@ function checkMcpServer(repoRoot) {
|
|
|
6916
7505
|
}
|
|
6917
7506
|
let config;
|
|
6918
7507
|
try {
|
|
6919
|
-
config = JSON.parse(
|
|
7508
|
+
config = JSON.parse(readFileSync19(mcpJson, "utf-8"));
|
|
6920
7509
|
} catch {
|
|
6921
7510
|
checks.push({
|
|
6922
7511
|
name: "MCP config (.mcp.json)",
|
|
@@ -6959,10 +7548,10 @@ function checkMcpServer(repoRoot) {
|
|
|
6959
7548
|
});
|
|
6960
7549
|
if (hasTapComms.command) {
|
|
6961
7550
|
const cmd = hasTapComms.command;
|
|
6962
|
-
let cmdAvailable =
|
|
7551
|
+
let cmdAvailable = existsSync24(cmd);
|
|
6963
7552
|
if (!cmdAvailable) {
|
|
6964
7553
|
try {
|
|
6965
|
-
const result =
|
|
7554
|
+
const result = spawnSync6(cmd, ["--version"], {
|
|
6966
7555
|
stdio: "pipe",
|
|
6967
7556
|
timeout: 5e3,
|
|
6968
7557
|
shell: process.platform === "win32"
|
|
@@ -6981,8 +7570,8 @@ function checkMcpServer(repoRoot) {
|
|
|
6981
7570
|
const mcpScript = hasTapComms.args[0];
|
|
6982
7571
|
checks.push({
|
|
6983
7572
|
name: "MCP server script",
|
|
6984
|
-
status:
|
|
6985
|
-
message:
|
|
7573
|
+
status: existsSync24(mcpScript) ? PASS : FAIL,
|
|
7574
|
+
message: existsSync24(mcpScript) ? mcpScript : `Not found: ${mcpScript}`
|
|
6986
7575
|
});
|
|
6987
7576
|
if (mcpScript.endsWith(".mjs") && hasTapComms.command && !hasTapComms.command.includes("bun")) {
|
|
6988
7577
|
checks.push({
|
|
@@ -7015,8 +7604,8 @@ function checkMcpServer(repoRoot) {
|
|
|
7015
7604
|
} else {
|
|
7016
7605
|
checks.push({
|
|
7017
7606
|
name: "MCP TAP_COMMS_DIR",
|
|
7018
|
-
status:
|
|
7019
|
-
message:
|
|
7607
|
+
status: existsSync24(envCommsDir) ? PASS : FAIL,
|
|
7608
|
+
message: existsSync24(envCommsDir) ? envCommsDir : `Directory not found: ${envCommsDir}`
|
|
7020
7609
|
});
|
|
7021
7610
|
}
|
|
7022
7611
|
checks.push({
|
|
@@ -7033,7 +7622,7 @@ function checkCodexConfig(repoRoot, commsDir) {
|
|
|
7033
7622
|
}
|
|
7034
7623
|
const checks = [];
|
|
7035
7624
|
const fixHint = 'Run "tap doctor --fix" or "tap add codex --force".';
|
|
7036
|
-
if (!
|
|
7625
|
+
if (!existsSync24(spec.configPath)) {
|
|
7037
7626
|
checks.push({
|
|
7038
7627
|
name: "MCP config (~/.codex/config.toml)",
|
|
7039
7628
|
status: WARN,
|
|
@@ -7042,7 +7631,7 @@ function checkCodexConfig(repoRoot, commsDir) {
|
|
|
7042
7631
|
});
|
|
7043
7632
|
return checks;
|
|
7044
7633
|
}
|
|
7045
|
-
const content =
|
|
7634
|
+
const content = readFileSync19(spec.configPath, "utf-8");
|
|
7046
7635
|
const tapTable = extractTomlTable(content, "mcp_servers.tap");
|
|
7047
7636
|
const tapEnvTable = extractTomlTable(content, "mcp_servers.tap.env");
|
|
7048
7637
|
const legacyTable = extractTomlTable(content, "mcp_servers.tap-comms");
|
|
@@ -7084,6 +7673,16 @@ function checkCodexConfig(repoRoot, commsDir) {
|
|
|
7084
7673
|
issues.push(`${key} drift (${actual})`);
|
|
7085
7674
|
}
|
|
7086
7675
|
}
|
|
7676
|
+
const actualAgentName = selectedEnv.TAP_AGENT_NAME;
|
|
7677
|
+
if (typeof actualAgentName !== "string") {
|
|
7678
|
+
issues.push("TAP_AGENT_NAME missing");
|
|
7679
|
+
} else if (actualAgentName !== spec.managed.env.TAP_AGENT_NAME) {
|
|
7680
|
+
issues.push(`non-neutral TAP_AGENT_NAME persisted (${actualAgentName})`);
|
|
7681
|
+
}
|
|
7682
|
+
const actualAgentId = selectedEnv.TAP_AGENT_ID;
|
|
7683
|
+
if (typeof actualAgentId === "string" && actualAgentId.trim()) {
|
|
7684
|
+
issues.push(`concrete TAP_AGENT_ID persisted (${actualAgentId})`);
|
|
7685
|
+
}
|
|
7087
7686
|
for (const trustTarget of spec.trustTargets) {
|
|
7088
7687
|
const trustTable = extractTomlTable(content, trustSelector2(trustTarget));
|
|
7089
7688
|
if (!trustTable || !trustTable.includes('trust_level = "trusted"')) {
|
|
@@ -7109,7 +7708,7 @@ function checkCodexConfig(repoRoot, commsDir) {
|
|
|
7109
7708
|
function checkBridgeTurnHealth(repoRoot) {
|
|
7110
7709
|
const checks = [];
|
|
7111
7710
|
const tmpDir = join23(repoRoot, ".tmp");
|
|
7112
|
-
if (!
|
|
7711
|
+
if (!existsSync24(tmpDir)) return checks;
|
|
7113
7712
|
const state = loadState(repoRoot);
|
|
7114
7713
|
const activeMatchers = /* @__PURE__ */ new Set();
|
|
7115
7714
|
if (state) {
|
|
@@ -7136,10 +7735,10 @@ function checkBridgeTurnHealth(repoRoot) {
|
|
|
7136
7735
|
}
|
|
7137
7736
|
for (const dir of dirs) {
|
|
7138
7737
|
const heartbeatPath = join23(tmpDir, dir, "heartbeat.json");
|
|
7139
|
-
if (!
|
|
7738
|
+
if (!existsSync24(heartbeatPath)) continue;
|
|
7140
7739
|
let heartbeat;
|
|
7141
7740
|
try {
|
|
7142
|
-
heartbeat = JSON.parse(
|
|
7741
|
+
heartbeat = JSON.parse(readFileSync19(heartbeatPath, "utf-8"));
|
|
7143
7742
|
} catch {
|
|
7144
7743
|
checks.push({
|
|
7145
7744
|
name: `turn: ${dir}`,
|
|
@@ -7199,6 +7798,13 @@ function checkBridgeTurnHealth(repoRoot) {
|
|
|
7199
7798
|
});
|
|
7200
7799
|
continue;
|
|
7201
7800
|
}
|
|
7801
|
+
if (heartbeat.authenticated === false) {
|
|
7802
|
+
checks.push({
|
|
7803
|
+
name: `turn: ${dir}`,
|
|
7804
|
+
status: WARN,
|
|
7805
|
+
message: "bridge running without auth \u2014 app-server session is unprotected. Use --gateway-token-file to enable auth."
|
|
7806
|
+
});
|
|
7807
|
+
}
|
|
7202
7808
|
const turnInfo = heartbeat.activeTurnId ? `active turn ${heartbeat.activeTurnId}` : `idle (last: ${heartbeat.lastTurnStatus ?? "none"})`;
|
|
7203
7809
|
checks.push({
|
|
7204
7810
|
name: `turn: ${dir}`,
|
|
@@ -7267,7 +7873,8 @@ async function doctorCommand(args) {
|
|
|
7267
7873
|
function runAllChecks() {
|
|
7268
7874
|
const checks = [];
|
|
7269
7875
|
checks.push(...checkComms(commsDir));
|
|
7270
|
-
checks.push(...
|
|
7876
|
+
checks.push(...checkStaleHeartbeats(repoRoot, commsDir, config.stateDir));
|
|
7877
|
+
checks.push(...checkInstances(repoRoot, config.stateDir, commsDir));
|
|
7271
7878
|
checks.push(...checkMessageLifecycle(commsDir));
|
|
7272
7879
|
checks.push(...checkMcpServer(repoRoot));
|
|
7273
7880
|
checks.push(...checkCodexConfig(repoRoot, commsDir));
|
|
@@ -7289,7 +7896,8 @@ async function doctorCommand(args) {
|
|
|
7289
7896
|
"inbox directory",
|
|
7290
7897
|
"reviews directory",
|
|
7291
7898
|
"findings directory",
|
|
7292
|
-
"heartbeats"
|
|
7899
|
+
"heartbeats",
|
|
7900
|
+
"stale heartbeats"
|
|
7293
7901
|
].includes(c.name)
|
|
7294
7902
|
),
|
|
7295
7903
|
Instances: initialChecks.filter(
|
|
@@ -7362,7 +7970,7 @@ async function doctorCommand(args) {
|
|
|
7362
7970
|
}
|
|
7363
7971
|
|
|
7364
7972
|
// src/commands/comms.ts
|
|
7365
|
-
import { execSync as execSync6, spawnSync as
|
|
7973
|
+
import { execSync as execSync6, spawnSync as spawnSync7 } from "child_process";
|
|
7366
7974
|
import * as fs27 from "fs";
|
|
7367
7975
|
import * as path26 from "path";
|
|
7368
7976
|
var COMMS_HELP = `
|
|
@@ -7454,7 +8062,7 @@ function commsPush(commsDir) {
|
|
|
7454
8062
|
};
|
|
7455
8063
|
}
|
|
7456
8064
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
7457
|
-
const commitResult =
|
|
8065
|
+
const commitResult = spawnSync7(
|
|
7458
8066
|
"git",
|
|
7459
8067
|
["commit", "-m", `chore(comms): sync ${timestamp}`],
|
|
7460
8068
|
{ cwd: commsDir, stdio: "pipe", encoding: "utf-8" }
|
|
@@ -7680,10 +8288,10 @@ function parseMissionsFile(repoRoot) {
|
|
|
7680
8288
|
}
|
|
7681
8289
|
|
|
7682
8290
|
// src/engine/pull-requests.ts
|
|
7683
|
-
import { spawnSync as
|
|
8291
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
7684
8292
|
function runGhPrList(repoRoot, extraArgs) {
|
|
7685
8293
|
try {
|
|
7686
|
-
const result =
|
|
8294
|
+
const result = spawnSync8(
|
|
7687
8295
|
"gh",
|
|
7688
8296
|
[
|
|
7689
8297
|
"pr",
|