@hua-labs/tap 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -71,8 +71,8 @@ function findRepoRoot(startDir = process.cwd()) {
71
71
  function createAdapterContext(commsDir, repoRoot) {
72
72
  const { config: config2 } = resolveConfig({}, repoRoot);
73
73
  return {
74
- commsDir: path.resolve(commsDir),
75
- repoRoot: path.resolve(repoRoot),
74
+ commsDir: path.resolve(normalizeTapPath(commsDir)),
75
+ repoRoot: path.resolve(normalizeTapPath(repoRoot)),
76
76
  stateDir: config2.stateDir,
77
77
  platform: detectPlatform()
78
78
  };
@@ -7414,7 +7414,7 @@ var init_bridge_port_network = __esm({
7414
7414
  });
7415
7415
 
7416
7416
  // src/engine/bridge-process-control.ts
7417
- import { execSync } from "child_process";
7417
+ import { execSync, spawnSync } from "child_process";
7418
7418
  function isProcessAlive(pid) {
7419
7419
  try {
7420
7420
  process.kill(pid, 0);
@@ -7423,6 +7423,25 @@ function isProcessAlive(pid) {
7423
7423
  return false;
7424
7424
  }
7425
7425
  }
7426
+ function getUnixProcessGroupId(pid) {
7427
+ const result = spawnSync("ps", ["-o", "pgid=", "-p", String(pid)], {
7428
+ encoding: "utf-8",
7429
+ windowsHide: true
7430
+ });
7431
+ if (!result || result.status !== 0) {
7432
+ return null;
7433
+ }
7434
+ const parsed = Number.parseInt((result.stdout ?? "").trim(), 10);
7435
+ return Number.isFinite(parsed) ? parsed : null;
7436
+ }
7437
+ function isUnixProcessGroupAlive(processGroupId) {
7438
+ try {
7439
+ process.kill(-processGroupId, 0);
7440
+ return true;
7441
+ } catch {
7442
+ return false;
7443
+ }
7444
+ }
7426
7445
  async function terminateProcess(pid, platform) {
7427
7446
  if (!isProcessAlive(pid)) {
7428
7447
  return false;
@@ -7431,11 +7450,16 @@ async function terminateProcess(pid, platform) {
7431
7450
  if (platform === "win32") {
7432
7451
  execSync(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
7433
7452
  } else {
7434
- process.kill(pid, "SIGTERM");
7453
+ const processGroupId = getUnixProcessGroupId(pid);
7454
+ const signalTarget = processGroupId != null ? -processGroupId : pid;
7455
+ const isTargetAlive = () => processGroupId != null ? isUnixProcessGroupAlive(processGroupId) : isProcessAlive(pid);
7456
+ process.kill(signalTarget, "SIGTERM");
7435
7457
  await delay(2e3);
7436
- if (isProcessAlive(pid)) {
7437
- process.kill(pid, "SIGKILL");
7458
+ if (isTargetAlive()) {
7459
+ process.kill(signalTarget, "SIGKILL");
7460
+ await delay(500);
7438
7461
  }
7462
+ return !isTargetAlive();
7439
7463
  }
7440
7464
  } catch {
7441
7465
  }
@@ -7619,11 +7643,11 @@ var init_bridge_observability = __esm({
7619
7643
  import * as fs9 from "fs";
7620
7644
  import * as os3 from "os";
7621
7645
  import * as path9 from "path";
7622
- import { spawnSync } from "child_process";
7646
+ import { spawnSync as spawnSync2 } from "child_process";
7623
7647
  import { fileURLToPath as fileURLToPath2 } from "url";
7624
7648
  function probeCommand(candidates) {
7625
7649
  for (const candidate of candidates) {
7626
- const result = spawnSync(candidate, ["--version"], {
7650
+ const result = spawnSync2(candidate, ["--version"], {
7627
7651
  encoding: "utf-8",
7628
7652
  shell: process.platform === "win32"
7629
7653
  });
@@ -7639,7 +7663,7 @@ function resolveCommandPath(command) {
7639
7663
  if (path9.isAbsolute(command)) return command;
7640
7664
  const whichCmd = process.platform === "win32" ? "where.exe" : "which";
7641
7665
  try {
7642
- const result = spawnSync(whichCmd, [command], {
7666
+ const result = spawnSync2(whichCmd, [command], {
7643
7667
  encoding: "utf-8",
7644
7668
  windowsHide: true
7645
7669
  });
@@ -7732,7 +7756,7 @@ function findPreferredBunCommand() {
7732
7756
  const candidates = process.platform === "win32" ? [path9.join(home, ".bun", "bin", "bun.exe"), "bun", "bun.cmd"] : [path9.join(home, ".bun", "bin", "bun"), "bun"];
7733
7757
  for (const candidate of candidates) {
7734
7758
  if (path9.isAbsolute(candidate) && !fs9.existsSync(candidate)) continue;
7735
- const result = spawnSync(candidate, ["--version"], {
7759
+ const result = spawnSync2(candidate, ["--version"], {
7736
7760
  encoding: "utf-8",
7737
7761
  shell: process.platform === "win32"
7738
7762
  });
@@ -7894,7 +7918,7 @@ import * as fs11 from "fs";
7894
7918
  import * as os4 from "os";
7895
7919
  import * as path11 from "path";
7896
7920
  import { randomBytes } from "crypto";
7897
- import { spawnSync as spawnSync2 } from "child_process";
7921
+ import { spawnSync as spawnSync3 } from "child_process";
7898
7922
  function cleanupStaleWindowsSpawnWrappers(now = Date.now()) {
7899
7923
  let entries;
7900
7924
  try {
@@ -7970,7 +7994,7 @@ function startWindowsDetachedProcess(command, args, repoRoot, logPath, env = pro
7970
7994
  "-PassThru",
7971
7995
  "; Write-Output $p.Id"
7972
7996
  ].join(" ");
7973
- const result = spawnSync2(
7997
+ const result = spawnSync3(
7974
7998
  powerShellCommand,
7975
7999
  ["-NoLogo", "-NoProfile", "-Command", psCommand],
7976
8000
  {
@@ -8012,7 +8036,7 @@ function findListeningProcessId(url2, platform) {
8012
8036
  if (port == null || !Number.isFinite(port)) {
8013
8037
  return null;
8014
8038
  }
8015
- const result = spawnSync2(
8039
+ const result = spawnSync3(
8016
8040
  resolvePowerShellCommand(),
8017
8041
  [
8018
8042
  "-NoLogo",
@@ -8049,15 +8073,55 @@ var init_bridge_windows_spawn = __esm({
8049
8073
 
8050
8074
  // src/engine/bridge-unix-spawn.ts
8051
8075
  import * as fs12 from "fs";
8052
- import { spawn, spawnSync as spawnSync3 } from "child_process";
8053
- function startUnixDetachedProcess(command, args, repoRoot, logPath, env = process.env) {
8076
+ import { spawn, spawnSync as spawnSync4 } from "child_process";
8077
+ function resolveUnixSpawnCommand(command, args, platform) {
8078
+ if (platform === "linux") {
8079
+ return {
8080
+ command: "nohup",
8081
+ args: [command, ...args]
8082
+ };
8083
+ }
8084
+ return { command, args };
8085
+ }
8086
+ function findListeningPidWithLsof(port) {
8087
+ const result = spawnSync4(
8088
+ "lsof",
8089
+ ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"],
8090
+ {
8091
+ encoding: "utf-8",
8092
+ windowsHide: true
8093
+ }
8094
+ );
8095
+ if (!result || result.status !== 0) {
8096
+ return null;
8097
+ }
8098
+ const parsedPid = Number.parseInt((result.stdout ?? "").trim(), 10);
8099
+ return Number.isFinite(parsedPid) ? parsedPid : null;
8100
+ }
8101
+ function findListeningPidWithSs(port) {
8102
+ const result = spawnSync4("ss", ["-ltnpH", `sport = :${port}`], {
8103
+ encoding: "utf-8",
8104
+ windowsHide: true
8105
+ });
8106
+ if (!result || result.status !== 0) {
8107
+ return null;
8108
+ }
8109
+ const match = (result.stdout ?? "").match(/\bpid=(\d+)\b/);
8110
+ if (!match) {
8111
+ return null;
8112
+ }
8113
+ const parsedPid = Number.parseInt(match[1], 10);
8114
+ return Number.isFinite(parsedPid) ? parsedPid : null;
8115
+ }
8116
+ function startUnixDetachedProcess(command, args, repoRoot, logPath, env = process.env, platform = DEFAULT_UNIX_PLATFORM) {
8054
8117
  const stderrPath = stderrLogFilePath(logPath);
8055
8118
  let logFd = null;
8056
8119
  let stderrFd = null;
8057
8120
  try {
8058
8121
  logFd = fs12.openSync(logPath, "a");
8059
8122
  stderrFd = fs12.openSync(stderrPath, "a");
8060
- const child = spawn(command, args, {
8123
+ const launch = resolveUnixSpawnCommand(command, args, platform);
8124
+ const child = spawn(launch.command, launch.args, {
8061
8125
  cwd: repoRoot,
8062
8126
  detached: true,
8063
8127
  stdio: ["ignore", logFd, stderrFd],
@@ -8075,13 +8139,15 @@ function startUnixDetachedProcess(command, args, repoRoot, logPath, env = proces
8075
8139
  }
8076
8140
  }
8077
8141
  }
8078
- function startUnixCodexAppServer(command, url2, repoRoot, logPath) {
8142
+ function startUnixCodexAppServer(command, url2, repoRoot, logPath, platform = DEFAULT_UNIX_PLATFORM) {
8079
8143
  const { command: exe, prefixArgs } = splitResolvedCommand(command);
8080
8144
  return startUnixDetachedProcess(
8081
8145
  exe,
8082
8146
  [...prefixArgs, "app-server", "--listen", url2],
8083
8147
  repoRoot,
8084
- logPath
8148
+ logPath,
8149
+ process.env,
8150
+ platform
8085
8151
  );
8086
8152
  }
8087
8153
  function findUnixListeningProcessId(url2, platform) {
@@ -8098,25 +8164,21 @@ function findUnixListeningProcessId(url2, platform) {
8098
8164
  if (port == null || !Number.isFinite(port)) {
8099
8165
  return null;
8100
8166
  }
8101
- const result = spawnSync3(
8102
- "lsof",
8103
- ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"],
8104
- {
8105
- encoding: "utf-8",
8106
- windowsHide: true
8167
+ if (platform === "linux") {
8168
+ const ssPid = findListeningPidWithSs(port);
8169
+ if (ssPid != null) {
8170
+ return ssPid;
8107
8171
  }
8108
- );
8109
- if (!result || result.status !== 0) {
8110
- return null;
8111
8172
  }
8112
- const parsedPid = Number.parseInt((result.stdout ?? "").trim(), 10);
8113
- return Number.isFinite(parsedPid) ? parsedPid : null;
8173
+ return findListeningPidWithLsof(port);
8114
8174
  }
8175
+ var DEFAULT_UNIX_PLATFORM;
8115
8176
  var init_bridge_unix_spawn = __esm({
8116
8177
  "src/engine/bridge-unix-spawn.ts"() {
8117
8178
  "use strict";
8118
8179
  init_bridge_codex_command();
8119
8180
  init_bridge_paths();
8181
+ DEFAULT_UNIX_PLATFORM = process.platform === "darwin" ? "darwin" : "linux";
8120
8182
  }
8121
8183
  });
8122
8184
 
@@ -8165,6 +8227,7 @@ var init_bridge_config = __esm({
8165
8227
  });
8166
8228
 
8167
8229
  // src/engine/bridge-app-server-health.ts
8230
+ import * as net2 from "net";
8168
8231
  async function checkAppServerHealth(url2, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS, gatewayToken) {
8169
8232
  const WebSocket = getWebSocketCtor();
8170
8233
  if (!WebSocket) {
@@ -8197,10 +8260,100 @@ async function checkAppServerHealth(url2, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_
8197
8260
  }
8198
8261
  });
8199
8262
  }
8200
- async function waitForAppServerHealth(url2, timeoutMs, gatewayToken) {
8263
+ function buildAppServerReadyzUrl(url2) {
8264
+ let parsed;
8265
+ try {
8266
+ parsed = new URL(url2);
8267
+ } catch {
8268
+ return null;
8269
+ }
8270
+ if (parsed.protocol === "ws:") {
8271
+ parsed.protocol = "http:";
8272
+ } else if (parsed.protocol === "wss:") {
8273
+ parsed.protocol = "https:";
8274
+ } else if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
8275
+ return null;
8276
+ }
8277
+ parsed.pathname = APP_SERVER_READYZ_PATH;
8278
+ parsed.search = "";
8279
+ parsed.hash = "";
8280
+ return parsed.toString();
8281
+ }
8282
+ async function checkAppServerReadyz(url2, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
8283
+ const readyzUrl = buildAppServerReadyzUrl(url2);
8284
+ if (!readyzUrl) {
8285
+ return "unsupported";
8286
+ }
8287
+ const controller = new AbortController();
8288
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
8289
+ try {
8290
+ const response = await fetch(readyzUrl, {
8291
+ method: "GET",
8292
+ signal: controller.signal,
8293
+ headers: {
8294
+ accept: "application/json"
8295
+ }
8296
+ });
8297
+ if (response.ok) {
8298
+ return "ready";
8299
+ }
8300
+ if (response.status === 400 || response.status === 404 || response.status === 405 || response.status === 426 || response.status === 501) {
8301
+ return "unsupported";
8302
+ }
8303
+ return "not-ready";
8304
+ } catch {
8305
+ return "not-ready";
8306
+ } finally {
8307
+ clearTimeout(timer);
8308
+ }
8309
+ }
8310
+ async function checkTcpPortListening(url2, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
8311
+ let hostname3;
8312
+ let port;
8313
+ try {
8314
+ const parsed = new URL(url2.replace(/^ws/, "http"));
8315
+ hostname3 = parsed.hostname;
8316
+ port = parseInt(parsed.port, 10);
8317
+ } catch {
8318
+ return false;
8319
+ }
8320
+ if (!port || !Number.isFinite(port)) return false;
8321
+ return new Promise((resolve11) => {
8322
+ const socket = net2.createConnection({ host: hostname3, port });
8323
+ const timer = setTimeout(() => {
8324
+ socket.destroy();
8325
+ resolve11(false);
8326
+ }, timeoutMs);
8327
+ socket.once("connect", () => {
8328
+ clearTimeout(timer);
8329
+ socket.destroy();
8330
+ resolve11(true);
8331
+ });
8332
+ socket.once("error", () => {
8333
+ clearTimeout(timer);
8334
+ socket.destroy();
8335
+ resolve11(false);
8336
+ });
8337
+ });
8338
+ }
8339
+ async function checkManagedAppServerReady(url2, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
8340
+ const readyzStatus = await checkAppServerReadyz(url2, timeoutMs);
8341
+ if (readyzStatus === "ready") {
8342
+ return true;
8343
+ }
8344
+ if (readyzStatus === "unsupported") {
8345
+ return checkTcpPortListening(url2, timeoutMs);
8346
+ }
8347
+ return false;
8348
+ }
8349
+ async function waitForManagedAppServerReady(url2, timeoutMs) {
8201
8350
  const deadline = Date.now() + timeoutMs;
8202
8351
  while (Date.now() < deadline) {
8203
- if (await checkAppServerHealth(url2, APP_SERVER_HEALTH_TIMEOUT_MS, gatewayToken)) {
8352
+ const remaining = Math.max(
8353
+ 1,
8354
+ Math.min(APP_SERVER_HEALTH_TIMEOUT_MS, deadline - Date.now())
8355
+ );
8356
+ if (await checkManagedAppServerReady(url2, remaining)) {
8204
8357
  return true;
8205
8358
  }
8206
8359
  await delay(APP_SERVER_HEALTH_RETRY_MS);
@@ -8216,13 +8369,14 @@ function markAppServerHealthy(appServer) {
8216
8369
  lastHealthyAt: checkedAt
8217
8370
  };
8218
8371
  }
8219
- var APP_SERVER_HEALTH_TIMEOUT_MS, APP_SERVER_HEALTH_RETRY_MS, AUTH_SUBPROTOCOL_PREFIX;
8372
+ var APP_SERVER_HEALTH_TIMEOUT_MS, APP_SERVER_HEALTH_RETRY_MS, APP_SERVER_READYZ_PATH, AUTH_SUBPROTOCOL_PREFIX;
8220
8373
  var init_bridge_app_server_health = __esm({
8221
8374
  "src/engine/bridge-app-server-health.ts"() {
8222
8375
  "use strict";
8223
8376
  init_bridge_port_network();
8224
8377
  APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
8225
8378
  APP_SERVER_HEALTH_RETRY_MS = 250;
8379
+ APP_SERVER_READYZ_PATH = "/readyz";
8226
8380
  AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
8227
8381
  }
8228
8382
  });
@@ -8494,7 +8648,8 @@ async function createManagedAppServerAuth(options) {
8494
8648
  gatewayArgs,
8495
8649
  options.repoRoot,
8496
8650
  gatewayLogPath,
8497
- gatewayEnv
8651
+ gatewayEnv,
8652
+ options.platform
8498
8653
  );
8499
8654
  } catch (error2) {
8500
8655
  removeFileIfExists2(tokenPath);
@@ -8680,7 +8835,8 @@ Start it manually:
8680
8835
  resolvedCommand,
8681
8836
  effectiveUrl,
8682
8837
  options.repoRoot,
8683
- logPath
8838
+ logPath,
8839
+ options.platform
8684
8840
  );
8685
8841
  } catch (err) {
8686
8842
  throw new Error(
@@ -8698,7 +8854,7 @@ Start it manually:
8698
8854
  ${manualCommand2}`
8699
8855
  );
8700
8856
  }
8701
- const healthy2 = await waitForAppServerHealth(
8857
+ const healthy2 = await waitForManagedAppServerReady(
8702
8858
  effectiveUrl,
8703
8859
  APP_SERVER_START_TIMEOUT_MS
8704
8860
  );
@@ -8760,7 +8916,8 @@ Start it manually:
8760
8916
  resolvedCommand,
8761
8917
  auth.upstreamUrl,
8762
8918
  options.repoRoot,
8763
- logPath
8919
+ logPath,
8920
+ options.platform
8764
8921
  );
8765
8922
  } catch (err) {
8766
8923
  if (auth.gatewayPid != null) {
@@ -8786,7 +8943,7 @@ Start it manually:
8786
8943
  ${manualCommand}`
8787
8944
  );
8788
8945
  }
8789
- const healthy = await waitForAppServerHealth(
8946
+ const healthy = await waitForManagedAppServerReady(
8790
8947
  auth.upstreamUrl,
8791
8948
  APP_SERVER_START_TIMEOUT_MS
8792
8949
  );
@@ -8812,10 +8969,9 @@ Or start it manually:
8812
8969
  removeFileIfExists2(auth.tokenPath);
8813
8970
  throw new Error("Tap auth gateway token is missing after startup.");
8814
8971
  }
8815
- const gatewayHealthy = await waitForAppServerHealth(
8972
+ const gatewayHealthy = await waitForManagedAppServerReady(
8816
8973
  effectiveUrl,
8817
- APP_SERVER_GATEWAY_START_TIMEOUT_MS,
8818
- gatewayToken
8974
+ APP_SERVER_GATEWAY_START_TIMEOUT_MS
8819
8975
  );
8820
8976
  if (!gatewayHealthy) {
8821
8977
  await terminateProcess(pid, options.platform);
@@ -8981,7 +9137,8 @@ async function startBridge(options) {
8981
9137
  [bridgeScript],
8982
9138
  repoRoot,
8983
9139
  logPath,
8984
- bridgeEnv
9140
+ bridgeEnv,
9141
+ options.platform
8985
9142
  );
8986
9143
  if (!bridgePid) {
8987
9144
  throw new Error(`Failed to spawn bridge process for ${instanceId}`);
@@ -9710,9 +9867,26 @@ function writeTomlFile(filePath, content) {
9710
9867
  fs22.writeFileSync(tmp, content, "utf-8");
9711
9868
  fs22.renameSync(tmp, filePath);
9712
9869
  }
9870
+ function buildSessionNeutralCodexSpec(ctx) {
9871
+ const managed = buildManagedMcpServerSpec(ctx);
9872
+ const env = {
9873
+ ...managed.env,
9874
+ TAP_AGENT_NAME: SESSION_NEUTRAL_AGENT_NAME
9875
+ };
9876
+ delete env.TAP_AGENT_ID;
9877
+ return { ...managed, env };
9878
+ }
9879
+ function buildCodexEnvEntries(existingTable, managedEnv) {
9880
+ const preservedEnv = parseTomlAssignments(existingTable ?? "");
9881
+ delete preservedEnv.TAP_AGENT_ID;
9882
+ return {
9883
+ ...preservedEnv,
9884
+ ...managedEnv
9885
+ };
9886
+ }
9713
9887
  function verifyManagedToml(content, ctx, configPath) {
9714
9888
  const checks = [];
9715
- const managed = buildManagedMcpServerSpec(ctx);
9889
+ const managed = buildSessionNeutralCodexSpec(ctx);
9716
9890
  const mainTable = extractTomlTable(content, MCP_SELECTOR);
9717
9891
  const envTable = extractTomlTable(content, ENV_SELECTOR);
9718
9892
  checks.push({
@@ -9749,9 +9923,22 @@ function verifyManagedToml(content, ctx, configPath) {
9749
9923
  message: "Managed tap command/args do not match expected values"
9750
9924
  });
9751
9925
  }
9926
+ if (envTable) {
9927
+ const envValues = parseTomlAssignments(envTable);
9928
+ checks.push({
9929
+ name: "Managed TAP_AGENT_NAME is session-neutral",
9930
+ passed: envValues.TAP_AGENT_NAME === managed.env.TAP_AGENT_NAME,
9931
+ message: `TAP_AGENT_NAME should be "${SESSION_NEUTRAL_AGENT_NAME}"`
9932
+ });
9933
+ checks.push({
9934
+ name: "Managed TAP_AGENT_ID is omitted",
9935
+ passed: typeof envValues.TAP_AGENT_ID !== "string",
9936
+ message: "TAP_AGENT_ID should not be persisted in Codex config"
9937
+ });
9938
+ }
9752
9939
  return checks;
9753
9940
  }
9754
- var MCP_SELECTOR, ENV_SELECTOR, OLD_MCP_SELECTOR, OLD_ENV_SELECTOR, codexAdapter;
9941
+ var MCP_SELECTOR, ENV_SELECTOR, SESSION_NEUTRAL_AGENT_NAME, OLD_MCP_SELECTOR, OLD_ENV_SELECTOR, codexAdapter;
9755
9942
  var init_codex = __esm({
9756
9943
  "src/adapters/codex.ts"() {
9757
9944
  "use strict";
@@ -9761,6 +9948,7 @@ var init_codex = __esm({
9761
9948
  init_common();
9762
9949
  MCP_SELECTOR = "mcp_servers.tap";
9763
9950
  ENV_SELECTOR = "mcp_servers.tap.env";
9951
+ SESSION_NEUTRAL_AGENT_NAME = "<set-per-session>";
9764
9952
  OLD_MCP_SELECTOR = "mcp_servers.tap-comms";
9765
9953
  OLD_ENV_SELECTOR = "mcp_servers.tap-comms.env";
9766
9954
  codexAdapter = {
@@ -9844,7 +10032,7 @@ var init_codex = __esm({
9844
10032
  const configPath = plan.operations[0]?.path ?? findCodexConfigPath();
9845
10033
  const warnings = [];
9846
10034
  const changedFiles = [];
9847
- const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
10035
+ const managed = buildSessionNeutralCodexSpec(ctx);
9848
10036
  warnings.push(...managed.warnings);
9849
10037
  if (managed.issues.length > 0 || !managed.command) {
9850
10038
  return {
@@ -9901,8 +10089,10 @@ var init_codex = __esm({
9901
10089
  ENV_SELECTOR,
9902
10090
  renderTomlTable(
9903
10091
  ENV_SELECTOR,
9904
- managed.env,
9905
- extractTomlTable(existingContent, ENV_SELECTOR)
10092
+ buildCodexEnvEntries(
10093
+ extractTomlTable(existingContent, ENV_SELECTOR),
10094
+ managed.env
10095
+ )
9906
10096
  )
9907
10097
  );
9908
10098
  for (const target of getTrustTargets(ctx)) {
@@ -10304,12 +10494,73 @@ var init_adapters = __esm({
10304
10494
  });
10305
10495
 
10306
10496
  // src/commands/bridge.ts
10497
+ import { existsSync as existsSync19, readFileSync as readFileSync15, renameSync as renameSync9, writeFileSync as writeFileSync10 } from "fs";
10307
10498
  import * as path23 from "path";
10308
10499
  function formatAge(seconds) {
10309
10500
  if (seconds < 60) return `${seconds}s ago`;
10310
10501
  if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
10311
10502
  return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m ago`;
10312
10503
  }
10504
+ function loadBridgeHeartbeatStore(commsDir) {
10505
+ const heartbeatsPath = path23.join(commsDir, "heartbeats.json");
10506
+ if (!existsSync19(heartbeatsPath)) return {};
10507
+ try {
10508
+ return JSON.parse(readFileSync15(heartbeatsPath, "utf-8"));
10509
+ } catch {
10510
+ return null;
10511
+ }
10512
+ }
10513
+ function saveBridgeHeartbeatStore(commsDir, store) {
10514
+ const heartbeatsPath = path23.join(commsDir, "heartbeats.json");
10515
+ const tmp = `${heartbeatsPath}.tmp.${process.pid}`;
10516
+ writeFileSync10(tmp, JSON.stringify(store, null, 2), "utf-8");
10517
+ renameSync9(tmp, heartbeatsPath);
10518
+ }
10519
+ function parseBridgeHeartbeatAgeMs(record2, now) {
10520
+ const raw = record2.lastActivity ?? record2.timestamp;
10521
+ if (!raw) return Number.POSITIVE_INFINITY;
10522
+ const parsed = new Date(raw).getTime();
10523
+ if (!Number.isFinite(parsed)) return Number.POSITIVE_INFINITY;
10524
+ return Math.max(0, now - parsed);
10525
+ }
10526
+ function resolveBridgeHeartbeatInstanceId(state, heartbeatId) {
10527
+ if (state.instances[heartbeatId]) return heartbeatId;
10528
+ const hyphenated = heartbeatId.replace(/_/g, "-");
10529
+ if (state.instances[hyphenated]) return hyphenated;
10530
+ const underscored = heartbeatId.replace(/-/g, "_");
10531
+ if (state.instances[underscored]) return underscored;
10532
+ return null;
10533
+ }
10534
+ function pruneStaleHeartbeatsForBridgeUp(state, stateDir, commsDir) {
10535
+ const store = loadBridgeHeartbeatStore(commsDir);
10536
+ if (store === null) {
10537
+ return {
10538
+ removed: 0,
10539
+ warning: "Auto-clean skipped \u2014 heartbeats.json unreadable"
10540
+ };
10541
+ }
10542
+ const now = Date.now();
10543
+ let removed = 0;
10544
+ for (const [heartbeatId, heartbeat] of Object.entries(store)) {
10545
+ const ageMs = parseBridgeHeartbeatAgeMs(heartbeat, now);
10546
+ const instanceId = resolveBridgeHeartbeatInstanceId(state, heartbeatId);
10547
+ const instance = instanceId ? state.instances[instanceId] : null;
10548
+ const bridgeBacked = instance?.bridgeMode === "app-server";
10549
+ const bridgeRunning = bridgeBacked && instanceId ? getBridgeStatus(stateDir, instanceId) === "running" : false;
10550
+ const status = heartbeat.status ?? "active";
10551
+ const staleByStatus = status === "signing-off" && ageMs >= BRIDGE_UP_SIGNING_OFF_HEARTBEAT_WINDOW_MS;
10552
+ const staleByDeadBridge = bridgeBacked && !bridgeRunning && ageMs >= BRIDGE_UP_ACTIVE_HEARTBEAT_WINDOW_MS;
10553
+ const staleByAge = !bridgeRunning && ageMs >= BRIDGE_UP_ORPHAN_HEARTBEAT_WINDOW_MS;
10554
+ if (staleByStatus || staleByDeadBridge || staleByAge) {
10555
+ delete store[heartbeatId];
10556
+ removed += 1;
10557
+ }
10558
+ }
10559
+ if (removed > 0) {
10560
+ saveBridgeHeartbeatStore(commsDir, store);
10561
+ }
10562
+ return { removed };
10563
+ }
10313
10564
  function formatAppServerState(appServer) {
10314
10565
  const ownership = appServer.managed ? "managed" : "external";
10315
10566
  const pid = appServer.pid != null ? ` pid:${appServer.pid}` : "";
@@ -10328,6 +10579,18 @@ function redactProtectedUrl(url2) {
10328
10579
  return url2.replace(/[?&]tap_token=[^&]+/g, "");
10329
10580
  }
10330
10581
  }
10582
+ function resolveTuiConnectUrl(appServer) {
10583
+ return appServer.auth?.upstreamUrl ?? appServer.url;
10584
+ }
10585
+ function quoteCliArg(value) {
10586
+ return `"${value.replace(/"/g, '\\"')}"`;
10587
+ }
10588
+ function formatCodexTuiAttachCommand(tuiConnectUrl, cwd) {
10589
+ return `codex --enable tui_app_server --remote ${quoteCliArg(tuiConnectUrl)} --cd ${quoteCliArg(cwd)}`;
10590
+ }
10591
+ function resolveTuiAttachCwd(repoRoot, stateRepoRoot, runtimeThreadCwd, savedThreadCwd) {
10592
+ return runtimeThreadCwd ?? savedThreadCwd ?? stateRepoRoot ?? repoRoot;
10593
+ }
10331
10594
  function loadCurrentBridgeState(stateDir, instanceId, fallback) {
10332
10595
  return loadBridgeState(stateDir, instanceId) ?? fallback ?? null;
10333
10596
  }
@@ -10695,6 +10958,26 @@ async function bridgeStartAll(flags = {}) {
10695
10958
  data: {}
10696
10959
  };
10697
10960
  }
10961
+ const ctx = createAdapterContext(state.commsDir, repoRoot);
10962
+ const warnings = [];
10963
+ let prunedHeartbeats = 0;
10964
+ if (flags["auto-prune-heartbeats"] === true) {
10965
+ const cleanup = pruneStaleHeartbeatsForBridgeUp(
10966
+ state,
10967
+ ctx.stateDir,
10968
+ ctx.commsDir
10969
+ );
10970
+ prunedHeartbeats = cleanup.removed;
10971
+ if (cleanup.warning) {
10972
+ warnings.push(cleanup.warning);
10973
+ log(cleanup.warning);
10974
+ }
10975
+ if (prunedHeartbeats > 0) {
10976
+ log(
10977
+ `Auto-clean: pruned ${prunedHeartbeats} stale heartbeat entr${prunedHeartbeats === 1 ? "y" : "ies"}`
10978
+ );
10979
+ }
10980
+ }
10698
10981
  const instanceIds = Object.keys(state.instances);
10699
10982
  const appServerInstances = instanceIds.filter((id) => {
10700
10983
  const inst = state.instances[id];
@@ -10703,13 +10986,14 @@ async function bridgeStartAll(flags = {}) {
10703
10986
  return adapter.bridgeMode() === "app-server";
10704
10987
  });
10705
10988
  if (appServerInstances.length === 0) {
10989
+ const cleanupSuffix2 = prunedHeartbeats > 0 ? ` Auto-clean pruned ${prunedHeartbeats} stale heartbeat entr${prunedHeartbeats === 1 ? "y" : "ies"}.` : "";
10706
10990
  return {
10707
10991
  ok: true,
10708
10992
  command: "bridge",
10709
10993
  code: "TAP_NO_OP",
10710
- message: "No app-server instances found to start.",
10711
- warnings: [],
10712
- data: {}
10994
+ message: `No app-server instances found to start.${cleanupSuffix2}`,
10995
+ warnings,
10996
+ data: { prunedHeartbeats }
10713
10997
  };
10714
10998
  }
10715
10999
  logHeader("@hua-labs/tap bridge start --all");
@@ -10719,7 +11003,6 @@ async function bridgeStartAll(flags = {}) {
10719
11003
  log("");
10720
11004
  const started = [];
10721
11005
  const failed = [];
10722
- const warnings = [];
10723
11006
  for (const instanceId of appServerInstances) {
10724
11007
  const inst = state.instances[instanceId];
10725
11008
  const storedName = inst?.agentName ?? void 0;
@@ -10729,8 +11012,26 @@ async function bridgeStartAll(flags = {}) {
10729
11012
  warnings.push(msg);
10730
11013
  continue;
10731
11014
  }
11015
+ const stateDir = path23.join(repoRoot, ".tap-comms");
11016
+ const currentBridgeState = loadBridgeState(stateDir, instanceId);
11017
+ const { manageAppServer, noAuth } = inferRestartMode(
11018
+ currentBridgeState,
11019
+ {
11020
+ noServer: flags["no-server"] === true ? true : void 0,
11021
+ noAuth: flags["no-auth"] === true ? true : void 0
11022
+ },
11023
+ {
11024
+ manageAppServer: inst.manageAppServer,
11025
+ noAuth: inst.noAuth
11026
+ }
11027
+ );
11028
+ const mergedFlags = {
11029
+ ...flags,
11030
+ ...manageAppServer === false ? { "no-server": true } : {},
11031
+ ...noAuth === true ? { "no-auth": true } : {}
11032
+ };
10732
11033
  log(`Starting ${instanceId} (agent: ${storedName})...`);
10733
- const result = await bridgeStart(instanceId, storedName, flags);
11034
+ const result = await bridgeStart(instanceId, storedName, mergedFlags);
10734
11035
  if (result.ok) {
10735
11036
  started.push(instanceId);
10736
11037
  logSuccess(`${instanceId} started`);
@@ -10741,13 +11042,14 @@ async function bridgeStartAll(flags = {}) {
10741
11042
  log("");
10742
11043
  }
10743
11044
  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(", ")}`;
11045
+ const cleanupSuffix = prunedHeartbeats > 0 ? ` Auto-clean pruned ${prunedHeartbeats} stale heartbeat entr${prunedHeartbeats === 1 ? "y" : "ies"}.` : "";
10744
11046
  return {
10745
11047
  ok: failed.length === 0 && started.length > 0,
10746
11048
  command: "bridge",
10747
11049
  code: started.length > 0 ? "TAP_BRIDGE_START_OK" : "TAP_BRIDGE_START_FAILED",
10748
- message,
11050
+ message: `${message}${cleanupSuffix}`,
10749
11051
  warnings,
10750
- data: { started, failed }
11052
+ data: { started, failed, prunedHeartbeats }
10751
11053
  };
10752
11054
  }
10753
11055
  async function bridgeStopOne(identifier) {
@@ -11329,6 +11631,122 @@ function bridgeStatusOne(identifier) {
11329
11631
  }
11330
11632
  };
11331
11633
  }
11634
+ function bridgeTuiOne(identifier) {
11635
+ const repoRoot = findRepoRoot();
11636
+ const state = loadState(repoRoot);
11637
+ if (!state) {
11638
+ return {
11639
+ ok: false,
11640
+ command: "bridge",
11641
+ code: "TAP_NOT_INITIALIZED",
11642
+ message: "Not initialized. Run: npx @hua-labs/tap init",
11643
+ warnings: [],
11644
+ data: {}
11645
+ };
11646
+ }
11647
+ const resolved = resolveInstanceId(identifier, state);
11648
+ if (!resolved.ok) {
11649
+ return {
11650
+ ok: false,
11651
+ command: "bridge",
11652
+ code: resolved.code,
11653
+ message: resolved.message,
11654
+ warnings: [],
11655
+ data: {}
11656
+ };
11657
+ }
11658
+ const instanceId = resolved.instanceId;
11659
+ const inst = state.instances[instanceId];
11660
+ if (!inst?.installed) {
11661
+ return {
11662
+ ok: false,
11663
+ command: "bridge",
11664
+ instanceId,
11665
+ code: "TAP_INSTANCE_NOT_FOUND",
11666
+ message: `${instanceId} is not installed.`,
11667
+ warnings: [],
11668
+ data: {}
11669
+ };
11670
+ }
11671
+ if (inst.runtime !== "codex" || inst.bridgeMode !== "app-server") {
11672
+ return {
11673
+ ok: false,
11674
+ command: "bridge",
11675
+ instanceId,
11676
+ runtime: inst.runtime,
11677
+ code: "TAP_INVALID_ARGUMENT",
11678
+ message: `${instanceId} does not support Codex TUI attach. Use a Codex app-server bridge instance.`,
11679
+ warnings: [],
11680
+ data: {}
11681
+ };
11682
+ }
11683
+ const { config: resolvedConfig } = resolveConfig({}, repoRoot);
11684
+ const stateDir = resolvedConfig.stateDir;
11685
+ const status = getBridgeStatus(stateDir, instanceId);
11686
+ if (status !== "running") {
11687
+ return {
11688
+ ok: false,
11689
+ command: "bridge",
11690
+ instanceId,
11691
+ runtime: inst.runtime,
11692
+ code: "TAP_BRIDGE_NOT_RUNNING",
11693
+ message: `${instanceId} bridge is ${status}. Start it first with: npx @hua-labs/tap bridge start ${instanceId}`,
11694
+ warnings: [],
11695
+ data: { status }
11696
+ };
11697
+ }
11698
+ const bridgeState = loadBridgeState(stateDir, instanceId);
11699
+ const appServer = bridgeState?.appServer;
11700
+ const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
11701
+ const savedThread = loadRuntimeBridgeThreadState(bridgeState);
11702
+ if (!appServer) {
11703
+ return {
11704
+ ok: false,
11705
+ command: "bridge",
11706
+ instanceId,
11707
+ runtime: inst.runtime,
11708
+ code: "TAP_BRIDGE_NOT_RUNNING",
11709
+ message: `${instanceId} app-server state is missing. Restart the bridge first.`,
11710
+ warnings: [],
11711
+ data: { status }
11712
+ };
11713
+ }
11714
+ const tuiConnectUrl = resolveTuiConnectUrl(appServer);
11715
+ const attachCwd = resolveTuiAttachCwd(
11716
+ repoRoot,
11717
+ state.repoRoot,
11718
+ runtimeHeartbeat?.threadCwd,
11719
+ savedThread?.cwd
11720
+ );
11721
+ const attachCommand = formatCodexTuiAttachCommand(tuiConnectUrl, attachCwd);
11722
+ const warnings = appServer.auth != null ? [
11723
+ "Use the upstream TUI URL, not the protected gateway URL. The protected URL is bridge-only."
11724
+ ] : [];
11725
+ logHeader(`@hua-labs/tap bridge tui ${instanceId}`);
11726
+ if (appServer.auth) {
11727
+ log(`Protected: ${redactProtectedUrl(appServer.auth.protectedUrl)}`);
11728
+ log(`Upstream: ${appServer.auth.upstreamUrl}`);
11729
+ }
11730
+ log(`Using: ${tuiConnectUrl}`);
11731
+ log(`Attach: ${attachCommand}`);
11732
+ log("");
11733
+ return {
11734
+ ok: true,
11735
+ command: "bridge",
11736
+ instanceId,
11737
+ runtime: inst.runtime,
11738
+ code: "TAP_BRIDGE_STATUS_OK",
11739
+ message: `${instanceId} TUI attach command ready`,
11740
+ warnings,
11741
+ data: {
11742
+ status,
11743
+ tuiConnectUrl,
11744
+ attachCwd,
11745
+ attachCommand,
11746
+ appServer
11747
+ }
11748
+ };
11749
+ }
11332
11750
  async function bridgeRestart(identifier, flags) {
11333
11751
  const repoRoot = findRepoRoot();
11334
11752
  const state = loadState(repoRoot);
@@ -11528,6 +11946,19 @@ async function bridgeCommand(args) {
11528
11946
  }
11529
11947
  return bridgeStatusAll();
11530
11948
  }
11949
+ case "tui": {
11950
+ if (!identifierArg) {
11951
+ return {
11952
+ ok: false,
11953
+ command: "bridge",
11954
+ code: "TAP_INVALID_ARGUMENT",
11955
+ message: "Missing instance. Usage: npx @hua-labs/tap bridge tui <instance>",
11956
+ warnings: [],
11957
+ data: {}
11958
+ };
11959
+ }
11960
+ return bridgeTuiOne(identifierArg);
11961
+ }
11531
11962
  case "watch": {
11532
11963
  const intervalStr = typeof flags["interval"] === "string" ? flags["interval"] : void 0;
11533
11964
  const interval = intervalStr ? parseInt(intervalStr, 10) : 30;
@@ -11553,13 +11984,13 @@ async function bridgeCommand(args) {
11553
11984
  ok: false,
11554
11985
  command: "bridge",
11555
11986
  code: "TAP_INVALID_ARGUMENT",
11556
- message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, restart, status`,
11987
+ message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, restart, status, tui`,
11557
11988
  warnings: [],
11558
11989
  data: {}
11559
11990
  };
11560
11991
  }
11561
11992
  }
11562
- var BRIDGE_HELP;
11993
+ var BRIDGE_UP_ACTIVE_HEARTBEAT_WINDOW_MS, BRIDGE_UP_ORPHAN_HEARTBEAT_WINDOW_MS, BRIDGE_UP_SIGNING_OFF_HEARTBEAT_WINDOW_MS, BRIDGE_HELP;
11563
11994
  var init_bridge2 = __esm({
11564
11995
  "src/commands/bridge.ts"() {
11565
11996
  "use strict";
@@ -11568,6 +11999,9 @@ var init_bridge2 = __esm({
11568
11999
  init_config();
11569
12000
  init_adapters();
11570
12001
  init_utils();
12002
+ BRIDGE_UP_ACTIVE_HEARTBEAT_WINDOW_MS = 10 * 60 * 1e3;
12003
+ BRIDGE_UP_ORPHAN_HEARTBEAT_WINDOW_MS = 24 * 60 * 60 * 1e3;
12004
+ BRIDGE_UP_SIGNING_OFF_HEARTBEAT_WINDOW_MS = 5 * 60 * 1e3;
11571
12005
  BRIDGE_HELP = `
11572
12006
  Usage:
11573
12007
  tap bridge <subcommand> [instance] [options]
@@ -11579,6 +12013,7 @@ Subcommands:
11579
12013
  stop Stop all running bridges
11580
12014
  status Show bridge status for all instances
11581
12015
  status <instance> Show bridge status for a specific instance
12016
+ tui <instance> Show the safe Codex TUI attach command for a running bridge
11582
12017
  watch Monitor bridges and auto-restart stuck/stale ones
11583
12018
 
11584
12019
  Options:
@@ -11607,6 +12042,7 @@ Examples:
11607
12042
  npx @hua-labs/tap bridge stop codex
11608
12043
  npx @hua-labs/tap bridge stop
11609
12044
  npx @hua-labs/tap bridge status
12045
+ npx @hua-labs/tap bridge tui codex
11610
12046
  `.trim();
11611
12047
  }
11612
12048
  });
@@ -11633,7 +12069,12 @@ async function upCommand(args) {
11633
12069
  process.env.TAP_COLD_START_WARMUP = "true";
11634
12070
  let result;
11635
12071
  try {
11636
- result = await bridgeCommand(["start", "--all", ...args]);
12072
+ result = await bridgeCommand([
12073
+ "start",
12074
+ "--all",
12075
+ "--auto-prune-heartbeats",
12076
+ ...args
12077
+ ]);
11637
12078
  } finally {
11638
12079
  if (previousColdStartWarmup === void 0) {
11639
12080
  delete process.env.TAP_COLD_START_WARMUP;
@@ -11681,6 +12122,7 @@ Usage:
11681
12122
  Description:
11682
12123
  Start all registered app-server bridge daemons with one command.
11683
12124
  This is the orchestration entrypoint for headless/background TAP operation.
12125
+ tap up auto-prunes stale heartbeat entries before bridge startup.
11684
12126
 
11685
12127
  Examples:
11686
12128
  npx @hua-labs/tap up
@@ -27719,8 +28161,10 @@ async function startHttpServer(options) {
27719
28161
  resolve11();
27720
28162
  });
27721
28163
  });
28164
+ const addr = server.address();
28165
+ const actualPort = typeof addr === "object" && addr ? addr.port : port;
27722
28166
  return {
27723
- port,
28167
+ port: actualPort,
27724
28168
  token,
27725
28169
  close: () => new Promise((resolve11, reject) => {
27726
28170
  server.close((err) => err ? reject(err) : resolve11());
@@ -27744,6 +28188,7 @@ export {
27744
28188
  loadLocalConfig,
27745
28189
  loadSharedConfig,
27746
28190
  loadState,
28191
+ normalizeTapPath,
27747
28192
  probeFnmNode,
27748
28193
  readNodeVersion,
27749
28194
  resolveConfig,