@hua-labs/tap 0.2.3 → 0.2.5

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.
@@ -16,7 +16,7 @@ import { isAbsolute, join, resolve } from "path";
16
16
  import { pathToFileURL } from "url";
17
17
  var DEFAULT_AGENT = String.fromCharCode(50728);
18
18
  var DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
19
- var APP_SERVER_AUTH_QUERY_PARAM = "tap_token";
19
+ var AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
20
20
  var PLACEHOLDER_AGENT_VALUES = /* @__PURE__ */ new Set([
21
21
  "unknown",
22
22
  "unnamed",
@@ -31,6 +31,30 @@ var HEADLESS_WARMUP_PROMPT = [
31
31
  var HEADLESS_WARMUP_TIMEOUT_MS = 3e4;
32
32
  var TURN_COMPLETION_POLL_MS = 250;
33
33
  var TURN_COMPLETION_REFRESH_MS = 1e3;
34
+ function normalizeThreadCwd(cwd) {
35
+ return resolve(cwd).replace(/\\/g, "/").toLowerCase();
36
+ }
37
+ function threadCwdMatches(expectedCwd, actualCwd) {
38
+ if (!actualCwd) {
39
+ return false;
40
+ }
41
+ return normalizeThreadCwd(expectedCwd) === normalizeThreadCwd(actualCwd);
42
+ }
43
+ function chooseLoadedThreadForCwd(cwd, threads) {
44
+ const matching = threads.filter((thread) => threadCwdMatches(cwd, thread.cwd));
45
+ if (matching.length === 0) {
46
+ return null;
47
+ }
48
+ matching.sort((left, right) => {
49
+ const leftActive = left.statusType === "active" ? 1 : 0;
50
+ const rightActive = right.statusType === "active" ? 1 : 0;
51
+ if (leftActive !== rightActive) {
52
+ return rightActive - leftActive;
53
+ }
54
+ return right.updatedAt - left.updatedAt;
55
+ });
56
+ return matching[0] ?? null;
57
+ }
34
58
  function printHelp() {
35
59
  console.log(`Codex App Server bridge
36
60
 
@@ -316,11 +340,6 @@ function persistAgentName(stateDir, agentName) {
316
340
  writeFileSync(join(stateDir, "agent-name.txt"), `${agentName}
317
341
  `, "utf8");
318
342
  }
319
- function buildProtectedAppServerUrl(appServerUrl, token) {
320
- const url = new URL(appServerUrl);
321
- url.searchParams.set(APP_SERVER_AUTH_QUERY_PARAM, token);
322
- return url.toString().replace(/\/(?=\?|$)/, "");
323
- }
324
343
  function readGatewayTokenFile(tokenFile) {
325
344
  const token = readFileSync(tokenFile, "utf8").trim();
326
345
  if (!token) {
@@ -349,12 +368,13 @@ function readThreadState(stateDir) {
349
368
  }
350
369
  return null;
351
370
  }
352
- function persistThreadState(stateDir, threadId, appServerUrl, ephemeral) {
371
+ function persistThreadState(stateDir, threadId, appServerUrl, ephemeral, cwd) {
353
372
  const payload = {
354
373
  threadId,
355
374
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
356
375
  appServerUrl,
357
- ephemeral
376
+ ephemeral,
377
+ cwd
358
378
  };
359
379
  writeFileSync(
360
380
  join(stateDir, "thread.json"),
@@ -651,7 +671,7 @@ async function waitForTurnCompletion(client, turnId, timeoutMs) {
651
671
  throw new Error(`Timed out waiting for turn ${turnId} to complete`);
652
672
  }
653
673
  async function maybeBootstrapHeadlessTurn(options, cutoff, client) {
654
- if (process.env.TAP_HEADLESS !== "true") {
674
+ if (process.env.TAP_HEADLESS !== "true" && process.env.TAP_COLD_START_WARMUP !== "true") {
655
675
  return false;
656
676
  }
657
677
  const { candidates } = getPendingCandidates(options, cutoff);
@@ -712,12 +732,14 @@ async function readSocketData(data) {
712
732
  var AppServerClient = class {
713
733
  socket = null;
714
734
  url;
735
+ gatewayToken;
715
736
  logger;
716
737
  nextId = 1;
717
738
  pending = /* @__PURE__ */ new Map();
718
739
  connected = false;
719
740
  initialized = false;
720
741
  threadId = null;
742
+ currentThreadCwd = null;
721
743
  activeTurnId = null;
722
744
  lastTurnStatus = null;
723
745
  lastNotificationMethod = null;
@@ -725,15 +747,20 @@ var AppServerClient = class {
725
747
  lastError = null;
726
748
  lastSuccessfulAppServerAt = null;
727
749
  lastSuccessfulAppServerMethod = null;
728
- constructor(url, logger) {
750
+ constructor(url, logger, gatewayToken) {
729
751
  this.url = url;
730
752
  this.logger = logger;
753
+ this.gatewayToken = gatewayToken ?? null;
731
754
  }
732
755
  async connect() {
733
756
  if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
734
757
  return;
735
758
  }
736
- this.socket = new WebSocket(this.url);
759
+ const wsOptions = {};
760
+ if (this.gatewayToken) {
761
+ wsOptions.protocols = [`${AUTH_SUBPROTOCOL_PREFIX}${this.gatewayToken}`];
762
+ }
763
+ this.socket = new WebSocket(this.url, wsOptions);
737
764
  await new Promise((resolvePromise, rejectPromise) => {
738
765
  let settled = false;
739
766
  const resolveOnce = () => {
@@ -796,7 +823,7 @@ var AppServerClient = class {
796
823
  this.initialized = false;
797
824
  this.socket = null;
798
825
  }
799
- async ensureThread(explicitThreadId, resumeThreadId, cwd, ephemeral) {
826
+ async ensureThread(explicitThreadId, savedThread, cwd, ephemeral) {
800
827
  if (explicitThreadId) {
801
828
  try {
802
829
  const resumeResponse = await this.request("thread/resume", {
@@ -819,22 +846,38 @@ var AppServerClient = class {
819
846
  if (loadedThreadId) {
820
847
  return loadedThreadId;
821
848
  }
822
- if (resumeThreadId) {
823
- try {
824
- const resumeResponse = await this.request("thread/resume", {
825
- threadId: resumeThreadId,
826
- persistExtendedHistory: false
827
- });
828
- const resumedThreadId = resumeResponse?.thread?.id ?? resumeThreadId;
829
- await this.refreshThreadState(resumedThreadId);
849
+ if (savedThread?.threadId) {
850
+ if (savedThread.cwd && !threadCwdMatches(cwd, savedThread.cwd)) {
830
851
  this.logger(
831
- `resumed saved thread ${resumedThreadId}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
832
- );
833
- return resumedThreadId;
834
- } catch (error) {
835
- this.logger(
836
- `saved thread resume failed for ${resumeThreadId}; starting a fresh thread (${String(error)})`
852
+ `saved thread ${savedThread.threadId} cwd ${savedThread.cwd} does not match ${cwd}; skipping saved thread`
837
853
  );
854
+ } else {
855
+ try {
856
+ const resumeResponse = await this.request("thread/resume", {
857
+ threadId: savedThread.threadId,
858
+ persistExtendedHistory: false
859
+ });
860
+ const resumedThreadId = resumeResponse?.thread?.id ?? savedThread.threadId;
861
+ await this.refreshThreadState(resumedThreadId);
862
+ if (!threadCwdMatches(cwd, this.currentThreadCwd)) {
863
+ this.logger(
864
+ `saved thread ${resumedThreadId} cwd ${this.currentThreadCwd ?? "unknown"} does not match ${cwd}; starting a fresh thread`
865
+ );
866
+ this.threadId = null;
867
+ this.currentThreadCwd = null;
868
+ this.activeTurnId = null;
869
+ this.lastTurnStatus = null;
870
+ } else {
871
+ this.logger(
872
+ `resumed saved thread ${resumedThreadId}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
873
+ );
874
+ return resumedThreadId;
875
+ }
876
+ } catch (error) {
877
+ this.logger(
878
+ `saved thread resume failed for ${savedThread.threadId}; starting a fresh thread (${String(error)})`
879
+ );
880
+ }
838
881
  }
839
882
  }
840
883
  const startResponse = await this.request("thread/start", {
@@ -847,7 +890,9 @@ var AppServerClient = class {
847
890
  if (!startedThreadId) {
848
891
  throw new Error("thread/start did not return a thread id");
849
892
  }
893
+ this.syncThreadStateFromThread(startResponse?.thread);
850
894
  this.threadId = startedThreadId;
895
+ this.currentThreadCwd = this.currentThreadCwd ?? cwd;
851
896
  this.activeTurnId = null;
852
897
  this.lastTurnStatus = null;
853
898
  this.logger(`started thread ${startedThreadId}`);
@@ -885,20 +930,13 @@ var AppServerClient = class {
885
930
  continue;
886
931
  }
887
932
  }
888
- const matching = threads.filter((thread) => thread.cwd === cwd);
889
- const candidates = matching.length > 0 ? matching : threads;
890
- if (candidates.length === 0) {
933
+ const chosen = chooseLoadedThreadForCwd(cwd, threads);
934
+ if (!chosen) {
935
+ if (threads.length > 0) {
936
+ this.logger(`loaded threads exist but none match cwd ${cwd}`);
937
+ }
891
938
  return null;
892
939
  }
893
- candidates.sort((left, right) => {
894
- const leftActive = left.statusType === "active" ? 1 : 0;
895
- const rightActive = right.statusType === "active" ? 1 : 0;
896
- if (leftActive !== rightActive) {
897
- return rightActive - leftActive;
898
- }
899
- return right.updatedAt - left.updatedAt;
900
- });
901
- const chosen = candidates[0];
902
940
  this.syncThreadStateFromThread(chosen.thread);
903
941
  this.logger(
904
942
  `attached to loaded thread ${chosen.id}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
@@ -971,6 +1009,7 @@ var AppServerClient = class {
971
1009
  if (typeof thread?.id === "string") {
972
1010
  this.threadId = thread.id;
973
1011
  }
1012
+ this.currentThreadCwd = typeof thread?.cwd === "string" ? thread.cwd : null;
974
1013
  let activeTurnId = null;
975
1014
  let lastTurnStatus = null;
976
1015
  const turns = Array.isArray(thread?.turns) ? thread.turns : [];
@@ -1019,6 +1058,9 @@ var AppServerClient = class {
1019
1058
  if (params?.thread?.id) {
1020
1059
  this.threadId = params.thread.id;
1021
1060
  }
1061
+ if (typeof params?.thread?.cwd === "string") {
1062
+ this.currentThreadCwd = params.thread.cwd;
1063
+ }
1022
1064
  this.logger(`thread started ${params?.thread?.id ?? ""}`.trim());
1023
1065
  break;
1024
1066
  case "thread/status/changed":
@@ -1074,6 +1116,16 @@ var AppServerClient = class {
1074
1116
  }
1075
1117
  };
1076
1118
  function writeHeartbeat(options, client, health) {
1119
+ if (client?.threadId) {
1120
+ const savedThread = readThreadState(options.stateDir);
1121
+ persistThreadState(
1122
+ options.stateDir,
1123
+ client.threadId,
1124
+ options.appServerUrl,
1125
+ options.ephemeral,
1126
+ client.currentThreadCwd ?? savedThread?.cwd ?? null
1127
+ );
1128
+ }
1077
1129
  const payload = {
1078
1130
  pid: process.pid,
1079
1131
  agent: options.agentName,
@@ -1083,6 +1135,7 @@ function writeHeartbeat(options, client, health) {
1083
1135
  connected: client?.connected ?? false,
1084
1136
  initialized: client?.initialized ?? false,
1085
1137
  threadId: client?.threadId ?? null,
1138
+ threadCwd: client?.currentThreadCwd ?? null,
1086
1139
  activeTurnId: client?.activeTurnId ?? null,
1087
1140
  lastTurnStatus: client?.lastTurnStatus ?? null,
1088
1141
  lastNotificationMethod: client?.lastNotificationMethod ?? null,
@@ -1219,10 +1272,8 @@ function buildOptions(argv) {
1219
1272
  runOnce: parsed.runOnce,
1220
1273
  waitAfterDispatchSeconds: parsed.waitAfterDispatchSeconds ?? 0,
1221
1274
  appServerUrl,
1222
- connectAppServerUrl: gatewayTokenFile ? buildProtectedAppServerUrl(
1223
- appServerUrl,
1224
- readGatewayTokenFile(gatewayTokenFile)
1225
- ) : appServerUrl,
1275
+ connectAppServerUrl: appServerUrl,
1276
+ gatewayToken: gatewayTokenFile ? readGatewayTokenFile(gatewayTokenFile) : null,
1226
1277
  gatewayTokenFile,
1227
1278
  busyMode: parsed.busyMode ?? "steer",
1228
1279
  threadId: parsed.threadId?.trim() || null,
@@ -1236,7 +1287,7 @@ async function main() {
1236
1287
  options.messageLookbackMinutes,
1237
1288
  options.processExistingMessages
1238
1289
  );
1239
- const savedThread = readThreadState(options.stateDir);
1290
+ const initialSavedThread = readThreadState(options.stateDir);
1240
1291
  logStatus("codex app-server bridge ready");
1241
1292
  console.log(` repo: ${options.repoRoot}`);
1242
1293
  console.log(` comms: ${options.commsDir}`);
@@ -1252,14 +1303,15 @@ async function main() {
1252
1303
  console.log(
1253
1304
  ` lookback: ${options.processExistingMessages ? "existing messages" : `${options.messageLookbackMinutes} minute(s)`}`
1254
1305
  );
1255
- if (options.threadId || savedThread?.threadId) {
1256
- console.log(` thread: ${options.threadId ?? savedThread?.threadId}`);
1306
+ if (options.threadId || initialSavedThread?.threadId) {
1307
+ console.log(
1308
+ ` thread: ${options.threadId ?? initialSavedThread?.threadId}`
1309
+ );
1257
1310
  }
1258
1311
  if (options.dryRun) {
1259
1312
  logStatus("dry-run mode enabled");
1260
1313
  }
1261
1314
  let client = null;
1262
- let savedThreadId = savedThread?.threadId ?? null;
1263
1315
  const health = {
1264
1316
  consecutiveFailureCount: 0
1265
1317
  };
@@ -1267,11 +1319,16 @@ async function main() {
1267
1319
  try {
1268
1320
  if (!options.dryRun) {
1269
1321
  if (!client || !client.connected) {
1270
- client = new AppServerClient(options.connectAppServerUrl, logStatus);
1322
+ client = new AppServerClient(
1323
+ options.connectAppServerUrl,
1324
+ logStatus,
1325
+ options.gatewayToken
1326
+ );
1271
1327
  await client.connect();
1328
+ const savedThread = readThreadState(options.stateDir);
1272
1329
  const threadId = await client.ensureThread(
1273
1330
  options.threadId,
1274
- savedThreadId,
1331
+ savedThread,
1275
1332
  options.repoRoot,
1276
1333
  options.ephemeral
1277
1334
  );
@@ -1279,9 +1336,9 @@ async function main() {
1279
1336
  options.stateDir,
1280
1337
  threadId,
1281
1338
  options.appServerUrl,
1282
- options.ephemeral
1339
+ options.ephemeral,
1340
+ client.currentThreadCwd ?? options.repoRoot
1283
1341
  );
1284
- savedThreadId = threadId;
1285
1342
  writeHeartbeat(options, client, health);
1286
1343
  const bootstrapped = await maybeBootstrapHeadlessTurn(
1287
1344
  options,
@@ -1353,6 +1410,7 @@ export {
1353
1410
  HEADLESS_WARMUP_PROMPT,
1354
1411
  buildOptions,
1355
1412
  buildUserInput,
1413
+ chooseLoadedThreadForCwd,
1356
1414
  isOwnMessageSender,
1357
1415
  main,
1358
1416
  maybeBootstrapHeadlessTurn,
@@ -1360,6 +1418,7 @@ export {
1360
1418
  resolveAddressLabel,
1361
1419
  resolveAgentId,
1362
1420
  resolveCurrentAgentName,
1421
+ threadCwdMatches,
1363
1422
  waitForTurnCompletion
1364
1423
  };
1365
1424
  //# sourceMappingURL=codex-app-server-bridge.mjs.map