@hua-labs/tap 0.5.0 → 0.5.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.
@@ -1,21 +1,8 @@
1
1
  // src/bridges/codex-app-server-bridge.ts
2
2
  import { pathToFileURL as pathToFileURL2 } from "url";
3
- import { resolve as resolve2 } from "path";
3
+ import { resolve as resolve5 } from "path";
4
4
 
5
- // scripts/codex-app-server-bridge.ts
6
- import { createHash } from "crypto";
7
- import {
8
- existsSync,
9
- mkdirSync,
10
- readdirSync,
11
- readFileSync,
12
- renameSync,
13
- statSync,
14
- unlinkSync,
15
- writeFileSync
16
- } from "fs";
17
- import { isAbsolute, join, resolve } from "path";
18
- import { pathToFileURL } from "url";
5
+ // scripts/bridge/bridge-types.ts
19
6
  var DEFAULT_AGENT = String.fromCharCode(50728);
20
7
  var DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
21
8
  var AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
@@ -33,6 +20,61 @@ var HEADLESS_WARMUP_PROMPT = [
33
20
  var HEADLESS_WARMUP_TIMEOUT_MS = 3e4;
34
21
  var TURN_COMPLETION_POLL_MS = 250;
35
22
  var TURN_COMPLETION_REFRESH_MS = 1e3;
23
+ var HEADLESS_SKIP_PATTERNS = [
24
+ /리뷰\s*요청/,
25
+ /review[- ]?request/i,
26
+ /재리뷰/,
27
+ /re-?review/i
28
+ ];
29
+ var COMMS_HEARTBEAT_LOCK_TIMEOUT_MS = 2e3;
30
+ var COMMS_LOCK_STALE_AGE_MS = 1e4;
31
+ var STALE_TURN_MS = 5 * 60 * 1e3;
32
+
33
+ // scripts/bridge/bridge-routing.ts
34
+ import { existsSync, readFileSync, writeFileSync } from "fs";
35
+ import { join, resolve } from "path";
36
+
37
+ // packages/tap-plugin/channels/tap-identity.ts
38
+ var BROADCAST_RECIPIENTS = /* @__PURE__ */ new Set(["\uC804\uCCB4", "all"]);
39
+ function trimAddress(value) {
40
+ return value?.trim() ?? "";
41
+ }
42
+ function canonicalizeAgentId(value) {
43
+ return trimAddress(value).replace(/-/g, "_");
44
+ }
45
+ function isBroadcastRecipient(value) {
46
+ return BROADCAST_RECIPIENTS.has(trimAddress(value));
47
+ }
48
+ function sameRoutingAddress(left, right) {
49
+ const normalizedLeft = trimAddress(left);
50
+ const normalizedRight = trimAddress(right);
51
+ if (!normalizedLeft || !normalizedRight) {
52
+ return false;
53
+ }
54
+ if (isBroadcastRecipient(normalizedLeft) && isBroadcastRecipient(normalizedRight)) {
55
+ return true;
56
+ }
57
+ return normalizedLeft === normalizedRight || canonicalizeAgentId(normalizedLeft) === canonicalizeAgentId(normalizedRight);
58
+ }
59
+ function matchesAgentRecipient(recipient, agentId, agentName) {
60
+ const normalizedRecipient = trimAddress(recipient);
61
+ if (!normalizedRecipient) {
62
+ return false;
63
+ }
64
+ return isBroadcastRecipient(normalizedRecipient) || sameRoutingAddress(normalizedRecipient, agentId) || normalizedRecipient === trimAddress(agentName);
65
+ }
66
+ function isOwnMessageAddress(sender, agentId, agentName) {
67
+ const normalizedSender = trimAddress(sender);
68
+ if (!normalizedSender) {
69
+ return false;
70
+ }
71
+ return sameRoutingAddress(normalizedSender, agentId) || normalizedSender === trimAddress(agentName);
72
+ }
73
+
74
+ // scripts/bridge/bridge-routing.ts
75
+ function canonicalize(id) {
76
+ return canonicalizeAgentId(id);
77
+ }
36
78
  function normalizeThreadCwd(cwd) {
37
79
  return resolve(cwd).replace(/\\/g, "/").toLowerCase();
38
80
  }
@@ -43,9 +85,12 @@ function threadCwdMatches(expectedCwd, actualCwd) {
43
85
  return normalizeThreadCwd(expectedCwd) === normalizeThreadCwd(actualCwd);
44
86
  }
45
87
  function chooseLoadedThreadForCwd(cwd, threads) {
46
- const matching = threads.filter(
47
- (thread) => threadCwdMatches(cwd, thread.cwd)
48
- );
88
+ const matching = threads.filter((thread) => {
89
+ if (!threadCwdMatches(cwd, thread.cwd)) {
90
+ return false;
91
+ }
92
+ return thread.statusType !== "notLoaded";
93
+ });
49
94
  if (matching.length === 0) {
50
95
  return null;
51
96
  }
@@ -59,6 +104,170 @@ function chooseLoadedThreadForCwd(cwd, threads) {
59
104
  });
60
105
  return matching[0] ?? null;
61
106
  }
107
+ function normalizeAgentToken(value) {
108
+ const normalized = value?.trim();
109
+ if (!normalized || PLACEHOLDER_AGENT_VALUES.has(normalized)) {
110
+ return null;
111
+ }
112
+ return canonicalize(normalized);
113
+ }
114
+ function resolveAgentId(preferredAgentName) {
115
+ return normalizeAgentToken(process.env.TAP_AGENT_ID) ?? normalizeAgentToken(preferredAgentName) ?? "unknown";
116
+ }
117
+ function resolveAgentName(preferredAgentName, stateDir) {
118
+ if (preferredAgentName?.trim()) {
119
+ return preferredAgentName.trim();
120
+ }
121
+ const agentFile = join(stateDir, "agent-name.txt");
122
+ if (existsSync(agentFile)) {
123
+ const candidate = readFileSync(agentFile, "utf8").trim();
124
+ if (candidate) {
125
+ return candidate;
126
+ }
127
+ }
128
+ return DEFAULT_AGENT;
129
+ }
130
+ function resolveCurrentAgentName(agentId, fallbackAgentName, heartbeats) {
131
+ const currentName = heartbeats[agentId]?.agent?.trim();
132
+ if (currentName) {
133
+ return currentName;
134
+ }
135
+ for (const heartbeat of Object.values(heartbeats)) {
136
+ if (heartbeat.id?.trim() === agentId && heartbeat.agent?.trim()) {
137
+ return heartbeat.agent.trim();
138
+ }
139
+ }
140
+ return fallbackAgentName;
141
+ }
142
+ function resolveAddressLabel(address, heartbeats) {
143
+ const normalized = address.trim();
144
+ if (!normalized || normalized === "\uC804\uCCB4" || normalized === "all") {
145
+ return address;
146
+ }
147
+ const direct = heartbeats[normalized];
148
+ if (direct?.agent?.trim()) {
149
+ return formatAgentLabel(normalized, direct.agent);
150
+ }
151
+ for (const [agentId, heartbeat] of Object.entries(heartbeats)) {
152
+ if (heartbeat.agent?.trim() === normalized) {
153
+ return formatAgentLabel(agentId, heartbeat.agent);
154
+ }
155
+ }
156
+ return normalized;
157
+ }
158
+ function persistAgentName(stateDir, agentName) {
159
+ writeFileSync(join(stateDir, "agent-name.txt"), `${agentName}
160
+ `, "utf8");
161
+ }
162
+ function formatAgentLabel(agentIdOrName, displayName) {
163
+ const normalizedId = agentIdOrName.trim();
164
+ const normalizedName = displayName?.trim();
165
+ if (!normalizedId) {
166
+ return normalizedName ?? agentIdOrName;
167
+ }
168
+ if (!normalizedName || normalizedName === normalizedId) {
169
+ return normalizedId;
170
+ }
171
+ return `${normalizedName} [${normalizedId}]`;
172
+ }
173
+ function refreshAgentIdentity(options, heartbeats) {
174
+ const nextAgentName = resolveCurrentAgentName(
175
+ options.agentId,
176
+ options.agentName,
177
+ heartbeats
178
+ );
179
+ if (nextAgentName !== options.agentName) {
180
+ persistAgentName(options.stateDir, nextAgentName);
181
+ }
182
+ return nextAgentName;
183
+ }
184
+ function recipientMatchesAgent(recipient, agentId, agentName) {
185
+ return matchesAgentRecipient(recipient, agentId, agentName);
186
+ }
187
+ function isOwnMessageSender(sender, agentId, agentName) {
188
+ return isOwnMessageAddress(sender, agentId, agentName);
189
+ }
190
+ function isTurnStuckOnApproval(activeFlags) {
191
+ return activeFlags.includes("waitingOnApproval");
192
+ }
193
+ function isTurnStale(turnStartedAt, nowMs = Date.now()) {
194
+ if (!turnStartedAt) return false;
195
+ return nowMs - new Date(turnStartedAt).getTime() > STALE_TURN_MS;
196
+ }
197
+ function shouldRetrySteerAsStart(error) {
198
+ if (!(error instanceof Error)) {
199
+ return false;
200
+ }
201
+ const message = error.message.toLowerCase();
202
+ return message.includes("no active turn") || message.includes("expectedturnid") || message.includes("turn/steer failed") && (message.includes("active turn") || message.includes("not found"));
203
+ }
204
+ function parseBridgeFrontmatter(content) {
205
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
206
+ if (!match) return null;
207
+ const fields = {};
208
+ for (const line of match[1].split("\n")) {
209
+ const kv = line.match(/^(\w+):\s*(.+)$/);
210
+ if (kv) fields[kv[1]] = kv[2].trim();
211
+ }
212
+ if (!fields.from || !fields.to) return null;
213
+ return {
214
+ sender: fields.from,
215
+ recipient: fields.to,
216
+ subject: fields.subject ?? ""
217
+ };
218
+ }
219
+ function stripBridgeFrontmatter(content) {
220
+ return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n*/, "");
221
+ }
222
+ function getInboxRoute(fileName, body) {
223
+ if (body) {
224
+ const fm = parseBridgeFrontmatter(body);
225
+ if (fm) return fm;
226
+ }
227
+ return getInboxRouteFromFilename(fileName);
228
+ }
229
+ function getInboxRouteFromFilename(fileName) {
230
+ const stem = fileName.replace(/\.md$/i, "");
231
+ const parts = stem.split("-");
232
+ let offset = 0;
233
+ if (parts[0] && /^\d{8}$/.test(parts[0])) {
234
+ offset = 1;
235
+ }
236
+ return {
237
+ sender: parts[offset] ?? "",
238
+ recipient: parts[offset + 1] ?? "",
239
+ subject: parts.slice(offset + 2).join("-")
240
+ };
241
+ }
242
+
243
+ // scripts/bridge/bridge-config.ts
244
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3 } from "fs";
245
+ import { isAbsolute as isAbsolute2, join as join3, resolve as resolve3 } from "path";
246
+
247
+ // src/config/resolve.ts
248
+ import * as fs from "fs";
249
+ import * as path from "path";
250
+ function normalizeTapPath(input) {
251
+ const trimmed = input.trim().replace(/^["'`]+|["'`]+$/g, "");
252
+ if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
253
+ return trimmed;
254
+ }
255
+ if (process.platform === "win32") {
256
+ const match = trimmed.match(/^\/([A-Za-z])\/(.*)$/);
257
+ if (match) {
258
+ return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
259
+ }
260
+ }
261
+ return trimmed;
262
+ }
263
+
264
+ // scripts/bridge/bridge-config.ts
265
+ function ensureDir(target) {
266
+ if (!existsSync3(target)) {
267
+ mkdirSync(target, { recursive: true });
268
+ }
269
+ return resolve3(target);
270
+ }
62
271
  function printHelp() {
63
272
  console.log(`Codex App Server bridge
64
273
 
@@ -80,6 +289,7 @@ Options:
80
289
  --app-server-url=<ws-url>
81
290
  --gateway-token-file=<path>
82
291
  --busy-mode=wait|steer
292
+ --log-level=debug|info|warn|error
83
293
  --thread-id=<id>
84
294
  --ephemeral
85
295
  --help
@@ -238,50 +448,42 @@ function parseArgs(argv) {
238
448
  }
239
449
  continue;
240
450
  }
451
+ if (flag.startsWith("--log-level")) {
452
+ const value = readFlagValue(argv, index, "--log-level");
453
+ if (value !== "debug" && value !== "info" && value !== "warn" && value !== "error") {
454
+ throw new Error(`Invalid --log-level: ${value}`);
455
+ }
456
+ parsed.logLevel = value;
457
+ if (consumesNext) {
458
+ index += 1;
459
+ }
460
+ continue;
461
+ }
241
462
  throw new Error(`Unknown argument: ${flag}`);
242
463
  }
243
464
  return parsed;
244
465
  }
245
- function timestamp() {
246
- return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", " UTC");
247
- }
248
- function logStatus(message) {
249
- console.log(`[${timestamp()}] ${message}`);
250
- }
251
- function ensureDir(target) {
252
- if (!existsSync(target)) {
253
- mkdirSync(target, { recursive: true });
254
- }
255
- return resolve(target);
256
- }
257
- function convertTapPath(input) {
258
- const trimmed = input.trim().replace(/^["'`]+|["'`]+$/g, "");
259
- if (/^[A-Za-z]:\\/.test(trimmed)) {
260
- return trimmed;
261
- }
262
- const match = trimmed.match(/^\/([A-Za-z])\/(.*)$/);
263
- if (match) {
264
- return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
265
- }
266
- return trimmed;
267
- }
268
466
  function resolveRepoRoot(explicit) {
269
467
  if (explicit) {
270
- return resolve(explicit);
468
+ return resolve3(explicit);
271
469
  }
272
470
  return process.cwd();
273
471
  }
472
+ function resolveTapConfigPath(repoRoot, input) {
473
+ const converted = normalizeTapPath(input);
474
+ return isAbsolute2(converted) ? resolve3(converted) : resolve3(repoRoot, converted);
475
+ }
274
476
  function resolveCommsDir(repoRoot, explicit) {
275
477
  if (explicit) {
276
- return resolve(convertTapPath(explicit));
478
+ return resolve3(normalizeTapPath(explicit));
277
479
  }
278
- const tapConfigPath = join(repoRoot, ".tap-config");
279
- if (!existsSync(tapConfigPath)) {
480
+ const tapConfigPath = join3(repoRoot, ".tap-config");
481
+ if (!existsSync3(tapConfigPath)) {
280
482
  throw new Error(
281
483
  "Unable to resolve comms directory. Pass --comms-dir explicitly."
282
484
  );
283
485
  }
284
- const configText = readFileSync(tapConfigPath, "utf8");
486
+ const configText = readFileSync3(tapConfigPath, "utf8");
285
487
  const match = configText.match(/^TAP_COMMS_DIR="?(.*?)"?$/m);
286
488
  if (!match?.[1]) {
287
489
  throw new Error(
@@ -302,352 +504,195 @@ function resolvePreferredAgentName(requested) {
302
504
  }
303
505
  return null;
304
506
  }
305
- function normalizeAgentToken(value) {
306
- const normalized = value?.trim();
307
- if (!normalized || PLACEHOLDER_AGENT_VALUES.has(normalized)) {
308
- return null;
309
- }
310
- return normalized.replace(/-/g, "_");
311
- }
312
- function resolveAgentId(preferredAgentName) {
313
- return normalizeAgentToken(process.env.TAP_AGENT_ID) ?? normalizeAgentToken(preferredAgentName) ?? "unknown";
314
- }
315
507
  function sanitizeStateSegment(agentName) {
316
508
  const normalized = agentName.trim().replace(/[<>:"/\\|?*\x00-\x1f]/g, "-").replace(/[. ]+$/g, "");
317
509
  return normalized || "agent";
318
510
  }
319
511
  function buildDefaultStateDir(repoRoot, preferredAgentName) {
320
512
  const suffix = preferredAgentName?.trim() ? `-${sanitizeStateSegment(preferredAgentName)}` : "";
321
- return resolve(join(repoRoot, ".tmp", `codex-app-server-bridge${suffix}`));
513
+ return resolve3(join3(repoRoot, ".tmp", `codex-app-server-bridge${suffix}`));
322
514
  }
323
515
  function resolveStateDir(repoRoot, explicit, preferredAgentName) {
324
- const root = explicit ? resolve(explicit) : buildDefaultStateDir(repoRoot, preferredAgentName);
516
+ const root = explicit ? resolve3(explicit) : buildDefaultStateDir(repoRoot, preferredAgentName);
325
517
  ensureDir(root);
326
- ensureDir(join(root, "processed"));
327
- ensureDir(join(root, "logs"));
518
+ ensureDir(join3(root, "processed"));
519
+ ensureDir(join3(root, "logs"));
328
520
  return root;
329
521
  }
330
- function resolveAgentName(preferredAgentName, stateDir) {
331
- if (preferredAgentName?.trim()) {
332
- return preferredAgentName.trim();
333
- }
334
- const agentFile = join(stateDir, "agent-name.txt");
335
- if (existsSync(agentFile)) {
336
- const candidate = readFileSync(agentFile, "utf8").trim();
337
- if (candidate) {
338
- return candidate;
339
- }
340
- }
341
- return DEFAULT_AGENT;
342
- }
343
- function persistAgentName(stateDir, agentName) {
344
- writeFileSync(join(stateDir, "agent-name.txt"), `${agentName}
345
- `, "utf8");
346
- }
347
- function sanitizeErrorForPersistence(error) {
348
- if (!error) return null;
349
- return error.replace(/([?&])tap_token=[^\s&)"'}]+/gi, "$1tap_token=***").replace(/"tap_token"\s*:\s*"[^"]*"/g, '"tap_token":"***"').replace(/tap-auth-[A-Za-z0-9_-]+/g, "tap-auth-***").replace(/Bearer\s+[A-Za-z0-9_.-]+/gi, "Bearer ***");
350
- }
351
522
  function readGatewayTokenFile(tokenFile) {
352
- const token = readFileSync(tokenFile, "utf8").trim();
523
+ const token = readFileSync3(tokenFile, "utf8").trim();
353
524
  if (!token) {
354
525
  throw new Error(`Gateway token file is empty: ${tokenFile}`);
355
526
  }
356
527
  return token;
357
528
  }
358
- function resolveTapConfigPath(repoRoot, input) {
359
- const converted = convertTapPath(input);
360
- return isAbsolute(converted) ? resolve(converted) : resolve(repoRoot, converted);
529
+ function buildOptions(argv) {
530
+ const parsed = parseArgs(argv);
531
+ const repoRoot = resolveRepoRoot(parsed.repoRoot);
532
+ const commsDir = resolveCommsDir(repoRoot, parsed.commsDir);
533
+ const preferredAgentName = resolvePreferredAgentName(parsed.agentName);
534
+ const stateDir = resolveStateDir(
535
+ repoRoot,
536
+ parsed.stateDir,
537
+ preferredAgentName
538
+ );
539
+ const agentName = resolveAgentName(preferredAgentName, stateDir);
540
+ const agentId = resolveAgentId(agentName);
541
+ persistAgentName(stateDir, agentName);
542
+ const gatewayTokenFile = parsed.gatewayTokenFile?.trim() || process.env.TAP_GATEWAY_TOKEN_FILE?.trim() || null;
543
+ const appServerUrl = parsed.appServerUrl?.trim() || process.env.CODEX_APP_SERVER_URL || DEFAULT_APP_SERVER_URL;
544
+ return {
545
+ repoRoot,
546
+ commsDir,
547
+ agentId,
548
+ stateDir,
549
+ agentName,
550
+ pollSeconds: parsed.pollSeconds ?? 5,
551
+ reconnectSeconds: parsed.reconnectSeconds ?? 5,
552
+ messageLookbackMinutes: parsed.messageLookbackMinutes ?? 10,
553
+ processExistingMessages: parsed.processExistingMessages,
554
+ dryRun: parsed.dryRun,
555
+ runOnce: parsed.runOnce,
556
+ waitAfterDispatchSeconds: parsed.waitAfterDispatchSeconds ?? 0,
557
+ appServerUrl,
558
+ connectAppServerUrl: appServerUrl,
559
+ gatewayToken: gatewayTokenFile ? readGatewayTokenFile(gatewayTokenFile) : null,
560
+ gatewayTokenFile,
561
+ busyMode: parsed.busyMode ?? "steer",
562
+ logLevel: parsed.logLevel ?? "info",
563
+ threadId: parsed.threadId?.trim() || null,
564
+ ephemeral: parsed.ephemeral
565
+ };
361
566
  }
362
- function readThreadState(stateDir) {
363
- const threadPath = join(stateDir, "thread.json");
364
- if (!existsSync(threadPath)) {
365
- return null;
567
+
568
+ // scripts/bridge/bridge-candidates.ts
569
+ import { createHash } from "crypto";
570
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, statSync } from "fs";
571
+ import { join as join4 } from "path";
572
+
573
+ // scripts/bridge/bridge-logging.ts
574
+ var LOG_LEVEL_PRIORITY = {
575
+ debug: 10,
576
+ info: 20,
577
+ warn: 30,
578
+ error: 40
579
+ };
580
+ var currentLogLevel = "info";
581
+ function configureBridgeLogging(level) {
582
+ currentLogLevel = level;
583
+ }
584
+ function shouldLog(level) {
585
+ return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[currentLogLevel];
586
+ }
587
+ function formatValue(value) {
588
+ if (typeof value === "string") {
589
+ return JSON.stringify(value);
366
590
  }
367
- try {
368
- const parsed = JSON.parse(
369
- readFileSync(threadPath, "utf8")
370
- );
371
- if (parsed.threadId) {
372
- return parsed;
373
- }
374
- } catch {
375
- return null;
376
- }
377
- return null;
378
- }
379
- function readHeartbeatState(stateDir) {
380
- const heartbeatPath = join(stateDir, "heartbeat.json");
381
- if (!existsSync(heartbeatPath)) {
382
- return null;
383
- }
384
- try {
385
- return JSON.parse(readFileSync(heartbeatPath, "utf8"));
386
- } catch {
387
- return null;
388
- }
389
- }
390
- function parseUpdatedAt(value) {
391
- if (!value) {
392
- return 0;
591
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
592
+ return String(value);
393
593
  }
394
- const parsed = Date.parse(value);
395
- return Number.isFinite(parsed) ? parsed : 0;
396
- }
397
- function appServerUrlMatches(expectedAppServerUrl, actualAppServerUrl) {
398
- return actualAppServerUrl?.trim() === expectedAppServerUrl;
399
- }
400
- function hasValidHeartbeatThreadCwd(threadCwd) {
401
- const normalized = threadCwd?.trim();
402
- if (!normalized) {
403
- return false;
594
+ if (value === null) {
595
+ return "null";
404
596
  }
405
- return isAbsolute(normalized) || /^[A-Za-z]:[\\/]/.test(normalized) || normalized.startsWith("\\\\");
597
+ return JSON.stringify(value);
406
598
  }
407
- function loadResumableThreadState(stateDir, fallbackAppServerUrl) {
408
- const savedThread = readThreadState(stateDir);
409
- const heartbeat = readHeartbeatState(stateDir);
410
- const heartbeatThreadId = heartbeat?.threadId?.trim();
411
- if (!heartbeatThreadId) {
412
- return savedThread;
413
- }
414
- if (!appServerUrlMatches(fallbackAppServerUrl, heartbeat?.appServerUrl)) {
415
- return savedThread;
416
- }
417
- if (!hasValidHeartbeatThreadCwd(heartbeat?.threadCwd)) {
418
- return savedThread;
419
- }
420
- const heartbeatBackedThread = {
421
- threadId: heartbeatThreadId,
422
- updatedAt: heartbeat?.updatedAt ?? savedThread?.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
423
- appServerUrl: heartbeat?.appServerUrl || savedThread?.appServerUrl || fallbackAppServerUrl,
424
- ephemeral: savedThread?.ephemeral ?? false,
425
- cwd: heartbeat?.threadCwd ?? (savedThread?.threadId === heartbeatThreadId ? savedThread.cwd ?? null : null)
426
- };
427
- let preferred = savedThread;
428
- if (!savedThread?.threadId) {
429
- preferred = heartbeatBackedThread;
430
- } else if (savedThread.threadId === heartbeatThreadId) {
431
- preferred = {
432
- ...savedThread,
433
- updatedAt: heartbeatBackedThread.updatedAt ?? savedThread.updatedAt,
434
- appServerUrl: heartbeatBackedThread.appServerUrl,
435
- cwd: heartbeatBackedThread.cwd ?? savedThread.cwd ?? null
436
- };
437
- } else if (parseUpdatedAt(heartbeat?.updatedAt) > parseUpdatedAt(savedThread.updatedAt)) {
438
- preferred = heartbeatBackedThread;
599
+ function formatContext(context) {
600
+ if (!context) {
601
+ return "";
439
602
  }
440
- return preferred;
441
- }
442
- function persistThreadState(stateDir, threadId, appServerUrl, ephemeral, cwd) {
443
- const payload = {
444
- threadId,
445
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
446
- appServerUrl,
447
- ephemeral,
448
- cwd
449
- };
450
- writeFileSync(
451
- join(stateDir, "thread.json"),
452
- `${JSON.stringify(payload, null, 2)}
453
- `,
454
- "utf8"
603
+ const entries = Object.entries(context).filter(
604
+ ([, value]) => value !== void 0
455
605
  );
456
- }
457
- function getGeneralInboxCutoff(stateDir, lookbackMinutes, processExistingMessages) {
458
- if (processExistingMessages) {
459
- return /* @__PURE__ */ new Date(0);
460
- }
461
- if (lookbackMinutes > 0) {
462
- return new Date(Date.now() - lookbackMinutes * 6e4);
463
- }
464
- const cutoffPath = join(stateDir, "general-inbox-cutoff.txt");
465
- if (existsSync(cutoffPath)) {
466
- try {
467
- return new Date(readFileSync(cutoffPath, "utf8").trim());
468
- } catch {
469
- return /* @__PURE__ */ new Date();
470
- }
471
- }
472
- const cutoff = /* @__PURE__ */ new Date();
473
- writeFileSync(cutoffPath, `${cutoff.toISOString()}
474
- `, "utf8");
475
- return cutoff;
476
- }
477
- function recipientMatchesAgent(recipient, agentId, agentName) {
478
- const normalizedRecipient = recipient.trim();
479
- if (!normalizedRecipient) {
480
- return false;
481
- }
482
- const aliases = /* @__PURE__ */ new Set([
483
- agentId.trim(),
484
- agentId.trim().replace(/-/g, "_"),
485
- agentId.trim().replace(/_/g, "-"),
486
- agentName.trim(),
487
- agentName.trim().replace(/-/g, "_"),
488
- agentName.trim().replace(/_/g, "-")
489
- ]);
490
- return normalizedRecipient === "\uC804\uCCB4" || normalizedRecipient === "all" || aliases.has(normalizedRecipient);
491
- }
492
- function isOwnMessageSender(sender, agentId, agentName) {
493
- const normalizedSender = sender.trim();
494
- if (!normalizedSender) {
495
- return false;
496
- }
497
- const aliases = /* @__PURE__ */ new Set([
498
- agentId.trim(),
499
- agentId.trim().replace(/-/g, "_"),
500
- agentId.trim().replace(/_/g, "-"),
501
- agentName.trim(),
502
- agentName.trim().replace(/-/g, "_"),
503
- agentName.trim().replace(/_/g, "-")
504
- ]);
505
- return aliases.has(normalizedSender);
506
- }
507
- function decodeRouteSegment(value) {
508
- try {
509
- return decodeURIComponent(value);
510
- } catch {
511
- return value;
606
+ if (entries.length === 0) {
607
+ return "";
512
608
  }
609
+ return ` ${entries.map(([key, value]) => `${key}=${formatValue(value)}`).join(" ")}`;
513
610
  }
514
- function parseInboxFrontmatter(body) {
515
- if (!body) {
516
- return null;
611
+ function logBridge(level, message, context) {
612
+ if (!shouldLog(level)) {
613
+ return;
517
614
  }
518
- const frontmatter = body.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
519
- if (!frontmatter) {
520
- return null;
615
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", " UTC");
616
+ const line = `[${ts}] ${level.toUpperCase()} ${message}${formatContext(context)}`;
617
+ if (level === "error") {
618
+ console.error(line);
619
+ return;
521
620
  }
522
- let sender = "";
523
- let recipient = "";
524
- let subject = "";
525
- for (const line of frontmatter[1].split(/\r?\n/)) {
526
- const separator = line.indexOf(":");
527
- if (separator <= 0) continue;
528
- const key = line.slice(0, separator).trim();
529
- const value = line.slice(separator + 1).trim();
530
- if (key === "from") sender = value;
531
- if (key === "to") recipient = value;
532
- if (key === "subject") subject = value;
533
- }
534
- if (!sender || !recipient || !subject) {
535
- return null;
621
+ if (level === "warn") {
622
+ console.warn(line);
623
+ return;
536
624
  }
537
- return { sender, recipient, subject };
625
+ console.log(line);
538
626
  }
539
- function getInboxRoute(fileName, body) {
540
- const frontmatterRoute = parseInboxFrontmatter(body);
541
- if (frontmatterRoute) {
542
- return frontmatterRoute;
543
- }
544
- const stem = fileName.replace(/\.md$/i, "");
545
- const parts = stem.split("-");
546
- let offset = 0;
547
- if (parts[0] && /^\d{8}$/.test(parts[0])) {
548
- offset = 1;
549
- }
627
+ function createBridgeLogger(scope) {
628
+ const scopedMessage = (message) => `[${scope}] ${message}`;
550
629
  return {
551
- sender: decodeRouteSegment(parts[offset] ?? ""),
552
- recipient: decodeRouteSegment(parts[offset + 1] ?? ""),
553
- subject: decodeRouteSegment(parts.slice(offset + 2).join("-"))
630
+ debug(message, context) {
631
+ logBridge("debug", scopedMessage(message), context);
632
+ },
633
+ info(message, context) {
634
+ logBridge("info", scopedMessage(message), context);
635
+ },
636
+ warn(message, context) {
637
+ logBridge("warn", scopedMessage(message), context);
638
+ },
639
+ error(message, context) {
640
+ logBridge("error", scopedMessage(message), context);
641
+ }
554
642
  };
555
643
  }
644
+
645
+ // scripts/bridge/bridge-candidates.ts
646
+ var routingLogger = createBridgeLogger("routing");
556
647
  function buildMarkerId(filePath, mtimeMs) {
557
648
  return createHash("sha1").update(`${filePath}|${mtimeMs}`).digest("hex");
558
649
  }
559
650
  function getProcessedMarkerPath(stateDir, markerId) {
560
- return join(stateDir, "processed", `${markerId}.done`);
651
+ return join4(stateDir, "processed", `${markerId}.done`);
561
652
  }
562
653
  function loadHeartbeats(commsDir) {
563
654
  try {
564
- return JSON.parse(readFileSync(join(commsDir, "heartbeats.json"), "utf8"));
655
+ return JSON.parse(readFileSync4(join4(commsDir, "heartbeats.json"), "utf8"));
565
656
  } catch {
566
657
  return {};
567
658
  }
568
659
  }
569
- function formatAgentLabel(agentIdOrName, displayName) {
570
- const normalizedId = agentIdOrName.trim();
571
- const normalizedName = displayName?.trim();
572
- if (!normalizedId) {
573
- return normalizedName ?? agentIdOrName;
574
- }
575
- if (!normalizedName || normalizedName === normalizedId) {
576
- return normalizedId;
577
- }
578
- return `${normalizedName} [${normalizedId}]`;
579
- }
580
- function resolveAddressLabel(address, heartbeats) {
581
- const normalized = address.trim();
582
- if (!normalized || normalized === "\uC804\uCCB4" || normalized === "all") {
583
- return address;
584
- }
585
- const direct = heartbeats[normalized];
586
- if (direct?.agent?.trim()) {
587
- return formatAgentLabel(normalized, direct.agent);
588
- }
589
- for (const [agentId, heartbeat] of Object.entries(heartbeats)) {
590
- if (heartbeat.agent?.trim() === normalized) {
591
- return formatAgentLabel(agentId, heartbeat.agent);
592
- }
593
- }
594
- return normalized;
595
- }
596
- function resolveCurrentAgentName(agentId, fallbackAgentName, heartbeats) {
597
- const currentName = heartbeats[agentId]?.agent?.trim();
598
- if (currentName) {
599
- return currentName;
600
- }
601
- for (const heartbeat of Object.values(heartbeats)) {
602
- if (heartbeat.id?.trim() === agentId && heartbeat.agent?.trim()) {
603
- return heartbeat.agent.trim();
604
- }
605
- }
606
- return fallbackAgentName;
607
- }
608
- function refreshAgentIdentity(options, heartbeats) {
609
- const nextAgentName = resolveCurrentAgentName(
610
- options.agentId,
611
- options.agentName,
612
- heartbeats
613
- );
614
- if (nextAgentName !== options.agentName) {
615
- options.agentName = nextAgentName;
616
- persistAgentName(options.stateDir, nextAgentName);
617
- }
618
- return nextAgentName;
619
- }
620
- var HEADLESS_SKIP_PATTERNS = [
621
- /리뷰\s*요청/,
622
- /review[- ]?request/i,
623
- /재리뷰/,
624
- /re-?review/i
625
- ];
626
660
  function shouldSkipInHeadlessMode(fileName, body) {
627
661
  if (process.env.TAP_HEADLESS !== "true") return false;
628
662
  const combined = `${fileName}
629
663
  ${body}`;
630
664
  return HEADLESS_SKIP_PATTERNS.some((p) => p.test(combined));
631
665
  }
632
- function collectCandidates(inboxDir, agentId, agentName) {
666
+ function collectCandidates(inboxDir, agentId, agentName, aliasName) {
633
667
  const entries = readdirSync(inboxDir, { withFileTypes: true }).filter(
634
668
  (entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md")
635
669
  ).map((entry) => {
636
- const filePath = join(inboxDir, entry.name);
670
+ const filePath = join4(inboxDir, entry.name);
637
671
  const stats = statSync(filePath);
638
672
  return { entry, filePath, stats };
639
673
  }).sort((left, right) => left.stats.mtimeMs - right.stats.mtimeMs);
640
674
  const candidates = [];
675
+ let filteredByRecipient = 0;
676
+ let filteredBySelf = 0;
677
+ let filteredByHeadless = 0;
641
678
  for (const item of entries) {
642
- const body = readFileSync(item.filePath, "utf8");
679
+ let body;
680
+ try {
681
+ body = readFileSync4(item.filePath, "utf8");
682
+ } catch {
683
+ continue;
684
+ }
643
685
  const route = getInboxRoute(item.entry.name, body);
644
- if (!recipientMatchesAgent(route.recipient, agentId, agentName)) {
686
+ if (!recipientMatchesAgent(route.recipient, agentId, agentName) && !(aliasName && recipientMatchesAgent(route.recipient, agentId, aliasName))) {
687
+ filteredByRecipient += 1;
645
688
  continue;
646
689
  }
647
- if (isOwnMessageSender(route.sender, agentId, agentName)) {
690
+ if (isOwnMessageSender(route.sender, agentId, agentName) || aliasName && isOwnMessageSender(route.sender, agentId, aliasName)) {
691
+ filteredBySelf += 1;
648
692
  continue;
649
693
  }
650
694
  if (shouldSkipInHeadlessMode(item.entry.name, body)) {
695
+ filteredByHeadless += 1;
651
696
  continue;
652
697
  }
653
698
  candidates.push({
@@ -657,34 +702,58 @@ function collectCandidates(inboxDir, agentId, agentName) {
657
702
  sender: route.sender,
658
703
  recipient: route.recipient,
659
704
  subject: route.subject,
660
- body,
705
+ body: stripBridgeFrontmatter(body),
661
706
  mtimeMs: item.stats.mtimeMs
662
707
  });
663
708
  }
709
+ routingLogger.debug("candidate scan completed", {
710
+ inboxDir,
711
+ scanned: entries.length,
712
+ matched: candidates.length,
713
+ filteredByRecipient,
714
+ filteredBySelf,
715
+ filteredByHeadless,
716
+ agentId,
717
+ agentName,
718
+ aliasName
719
+ });
664
720
  return candidates;
665
721
  }
666
722
  function getPendingCandidates(options, cutoff) {
667
- const inboxDir = join(options.commsDir, "inbox");
668
- if (!existsSync(inboxDir)) {
723
+ const inboxDir = join4(options.commsDir, "inbox");
724
+ if (!existsSync4(inboxDir)) {
669
725
  throw new Error(`Inbox directory not found: ${inboxDir}`);
670
726
  }
671
727
  const heartbeats = loadHeartbeats(options.commsDir);
672
- const agentName = refreshAgentIdentity(options, heartbeats);
728
+ const refreshedName = refreshAgentIdentity(options, heartbeats);
673
729
  const cutoffMs = cutoff.getTime();
674
730
  const candidates = collectCandidates(
675
731
  inboxDir,
676
732
  options.agentId,
677
- agentName
733
+ options.agentName,
734
+ // M205: Also accept messages addressed to the heartbeat-refreshed name
735
+ refreshedName !== options.agentName ? refreshedName : void 0
678
736
  ).filter((candidate) => {
679
737
  if (candidate.mtimeMs < cutoffMs) {
680
738
  return false;
681
739
  }
682
- return !existsSync(
740
+ return !existsSync4(
683
741
  getProcessedMarkerPath(options.stateDir, candidate.markerId)
684
742
  );
685
743
  });
744
+ routingLogger.debug("pending candidates resolved", {
745
+ agentId: options.agentId,
746
+ configuredName: options.agentName,
747
+ refreshedName: refreshedName !== options.agentName ? refreshedName : void 0,
748
+ candidateCount: candidates.length,
749
+ cutoff: cutoff.toISOString()
750
+ });
686
751
  return { heartbeats, candidates };
687
752
  }
753
+
754
+ // scripts/bridge/bridge-format.ts
755
+ import { writeFileSync as writeFileSync3 } from "fs";
756
+ import { join as join5 } from "path";
688
757
  function buildUserInput(candidate, agentName, heartbeats) {
689
758
  const sender = resolveAddressLabel(candidate.sender || "unknown", heartbeats);
690
759
  const recipient = resolveAddressLabel(
@@ -723,7 +792,7 @@ function writeProcessedMarker(stateDir, candidate, dispatchMode, threadId, turnI
723
792
  turnId,
724
793
  markedAt: (/* @__PURE__ */ new Date()).toISOString()
725
794
  };
726
- writeFileSync(
795
+ writeFileSync3(
727
796
  getProcessedMarkerPath(stateDir, candidate.markerId),
728
797
  `${JSON.stringify(payload, null, 2)}
729
798
  `,
@@ -743,98 +812,435 @@ function writeLastDispatch(stateDir, candidate, dispatchMode, threadId, turnId)
743
812
  turnId,
744
813
  dispatchedAt: (/* @__PURE__ */ new Date()).toISOString()
745
814
  };
746
- writeFileSync(
747
- join(stateDir, "last-dispatch.json"),
815
+ writeFileSync3(
816
+ join5(stateDir, "last-dispatch.json"),
748
817
  `${JSON.stringify(payload, null, 2)}
749
818
  `,
750
819
  "utf8"
751
820
  );
752
821
  }
753
- function formatJsonRpcError(error) {
754
- if (!error) {
755
- return "Unknown App Server error";
756
- }
757
- return JSON.stringify(
758
- {
759
- code: error.code,
760
- message: error.message,
761
- data: error.data
762
- },
763
- null,
764
- 2
765
- );
822
+
823
+ // scripts/bridge/bridge-dispatch.ts
824
+ import {
825
+ existsSync as existsSync5,
826
+ readFileSync as readFileSync5,
827
+ renameSync as renameSync2,
828
+ statSync as statSync2,
829
+ unlinkSync,
830
+ writeFileSync as writeFileSync4
831
+ } from "fs";
832
+ import { join as join6 } from "path";
833
+ var dispatchLogger = createBridgeLogger("dispatch");
834
+ var heartbeatLogger = createBridgeLogger("heartbeat");
835
+ function sanitizeErrorForPersistence(error) {
836
+ if (!error) return null;
837
+ return error.replace(/([?&])tap_token=[^\s&)"'}]+/gi, "$1tap_token=***").replace(/([?&])token=[^\s&)"'}]+/gi, "$1token=***").replace(/([?&])secret=[^\s&)"'}]+/gi, "$1secret=***").replace(/([?&])key=[^\s&)"'}]+/gi, "$1key=***").replace(/"tap_token"\s*:\s*"[^"]*"/g, '"tap_token":"***"').replace(/"token"\s*:\s*"[^"]*"/g, '"token":"***"').replace(/"secret"\s*:\s*"[^"]*"/g, '"secret":"***"').replace(/"password"\s*:\s*"[^"]*"/g, '"password":"***"').replace(/"authorization"\s*:\s*"[^"]*"/gi, '"authorization":"***"').replace(/tap-auth-[A-Za-z0-9_.\-/+=]+/g, "tap-auth-***").replace(/Bearer\s+[A-Za-z0-9_.\-/+=]+/gi, "Bearer ***").replace(/(?<=[=:"\s])[A-Za-z0-9_\-/+=]{40,}(?=["\s&)}'}\],]|$)/g, "***");
766
838
  }
767
839
  function delay(ms) {
768
840
  return new Promise((resolvePromise) => {
769
841
  setTimeout(resolvePromise, ms);
770
842
  });
771
843
  }
772
- async function waitForTurnCompletion(client, turnId, timeoutMs) {
773
- const deadline = Date.now() + timeoutMs;
774
- let nextRefreshAt = Date.now();
775
- while (Date.now() < deadline) {
776
- if (!client.activeTurnId || client.activeTurnId !== turnId) {
777
- return client.lastTurnStatus;
778
- }
779
- if (Date.now() >= nextRefreshAt) {
780
- await client.refreshCurrentThreadState().catch(() => void 0);
781
- if (!client.activeTurnId || client.activeTurnId !== turnId) {
782
- return client.lastTurnStatus;
783
- }
784
- nextRefreshAt = Date.now() + TURN_COMPLETION_REFRESH_MS;
785
- }
786
- await delay(
787
- Math.min(TURN_COMPLETION_POLL_MS, Math.max(deadline - Date.now(), 0))
788
- );
789
- }
790
- await client.refreshCurrentThreadState().catch(() => void 0);
791
- if (!client.activeTurnId || client.activeTurnId !== turnId) {
792
- return client.lastTurnStatus;
793
- }
794
- throw new Error(`Timed out waiting for turn ${turnId} to complete`);
795
- }
796
- async function maybeBootstrapHeadlessTurn(options, cutoff, client) {
797
- if (process.env.TAP_HEADLESS !== "true" && process.env.TAP_COLD_START_WARMUP !== "true") {
798
- return false;
799
- }
800
- const { candidates } = getPendingCandidates(options, cutoff);
801
- if (candidates.length > 0 || client.activeTurnId || client.lastTurnStatus !== null) {
802
- return false;
803
- }
804
- logStatus("headless cold-start: sending warmup turn");
805
- const turnId = await client.startTurn(HEADLESS_WARMUP_PROMPT);
806
- if (!turnId) {
807
- throw new Error(
808
- "Headless cold-start warmup failed: turn/start did not return a turn id. Run: npx @hua-labs/tap doctor"
809
- );
844
+ function readThreadState(stateDir) {
845
+ const threadPath = join6(stateDir, "thread.json");
846
+ if (!existsSync5(threadPath)) {
847
+ return null;
810
848
  }
811
849
  try {
812
- const status = await waitForTurnCompletion(
813
- client,
814
- turnId,
815
- HEADLESS_WARMUP_TIMEOUT_MS
850
+ const parsed = JSON.parse(
851
+ readFileSync5(threadPath, "utf8")
816
852
  );
817
- if (status !== "completed") {
818
- throw new Error(
819
- `turn ${turnId} finished with status ${status ?? "unknown"}`
820
- );
853
+ if (parsed.threadId) {
854
+ return parsed;
821
855
  }
822
- logStatus(`headless cold-start warmup completed (${status})`);
823
- return true;
824
- } catch (error) {
825
- const reason = error instanceof Error ? error.message : String(error);
826
- throw new Error(
827
- `Headless cold-start warmup failed: ${reason}. Run: npx @hua-labs/tap doctor`
856
+ } catch {
857
+ return null;
858
+ }
859
+ return null;
860
+ }
861
+ function persistThreadState(stateDir, threadId, appServerUrl, ephemeral, cwd) {
862
+ const payload = {
863
+ threadId,
864
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
865
+ appServerUrl,
866
+ ephemeral,
867
+ cwd
868
+ };
869
+ writeFileSync4(
870
+ join6(stateDir, "thread.json"),
871
+ `${JSON.stringify(payload, null, 2)}
872
+ `,
873
+ "utf8"
874
+ );
875
+ }
876
+ function acquireCommsLock(lockPath) {
877
+ const deadline = Date.now() + COMMS_HEARTBEAT_LOCK_TIMEOUT_MS;
878
+ while (Date.now() < deadline) {
879
+ try {
880
+ writeFileSync4(lockPath, String(process.pid), { flag: "wx" });
881
+ return true;
882
+ } catch {
883
+ try {
884
+ const lockAge = Date.now() - statSync2(lockPath).mtimeMs;
885
+ if (lockAge > COMMS_LOCK_STALE_AGE_MS) {
886
+ unlinkSync(lockPath);
887
+ try {
888
+ writeFileSync4(lockPath, String(process.pid), { flag: "wx" });
889
+ return true;
890
+ } catch {
891
+ }
892
+ }
893
+ } catch {
894
+ }
895
+ const start = Date.now();
896
+ while (Date.now() - start < 50) {
897
+ }
898
+ }
899
+ }
900
+ return false;
901
+ }
902
+ function releaseCommsLock(lockPath) {
903
+ try {
904
+ unlinkSync(lockPath);
905
+ } catch {
906
+ }
907
+ }
908
+ function updateCommsHeartbeat(options, status) {
909
+ const heartbeatsPath = join6(options.commsDir, "heartbeats.json");
910
+ const lockPath = join6(options.commsDir, ".heartbeats.lock");
911
+ if (!acquireCommsLock(lockPath)) {
912
+ return;
913
+ }
914
+ try {
915
+ let store = {};
916
+ try {
917
+ store = JSON.parse(readFileSync5(heartbeatsPath, "utf-8"));
918
+ } catch {
919
+ }
920
+ const key = options.agentId;
921
+ const existing = store[key];
922
+ const now = (/* @__PURE__ */ new Date()).toISOString();
923
+ store[key] = {
924
+ id: options.agentId,
925
+ agent: options.agentName,
926
+ timestamp: now,
927
+ lastActivity: now,
928
+ joinedAt: existing?.joinedAt ?? now,
929
+ status,
930
+ source: "bridge-dispatch",
931
+ instanceId: options.agentId,
932
+ bridgePid: process.pid,
933
+ connectHash: `instance:${options.agentId}`
934
+ };
935
+ const tmpPath = heartbeatsPath + ".tmp." + process.pid;
936
+ writeFileSync4(tmpPath, JSON.stringify(store, null, 2), "utf-8");
937
+ renameSync2(tmpPath, heartbeatsPath);
938
+ } catch {
939
+ } finally {
940
+ releaseCommsLock(lockPath);
941
+ }
942
+ }
943
+ var heartbeatCount = 0;
944
+ function readPreviousHeartbeat(stateDir) {
945
+ const heartbeatPath = join6(stateDir, "heartbeat.json");
946
+ if (!existsSync5(heartbeatPath)) {
947
+ return null;
948
+ }
949
+ try {
950
+ return JSON.parse(
951
+ readFileSync5(heartbeatPath, "utf8")
828
952
  );
953
+ } catch {
954
+ return null;
829
955
  }
830
956
  }
831
- function shouldRetrySteerAsStart(error) {
832
- if (!(error instanceof Error)) {
957
+ function readLastDispatchAt(stateDir) {
958
+ const dispatchPath = join6(stateDir, "last-dispatch.json");
959
+ if (!existsSync5(dispatchPath)) {
960
+ return null;
961
+ }
962
+ try {
963
+ const parsed = JSON.parse(
964
+ readFileSync5(dispatchPath, "utf8")
965
+ );
966
+ return typeof parsed.dispatchedAt === "string" ? parsed.dispatchedAt : null;
967
+ } catch {
968
+ return null;
969
+ }
970
+ }
971
+ function isWaitingApprovalStatus(status) {
972
+ if (!status) return false;
973
+ return /approval|input-required|confirm|consent/i.test(status);
974
+ }
975
+ function resolveTurnState(client) {
976
+ if (!client) return null;
977
+ if (client.activeTurnId) return "active";
978
+ if (client.connected === false) return "disconnected";
979
+ if (isWaitingApprovalStatus(client.lastTurnStatus)) {
980
+ return "waiting-approval";
981
+ }
982
+ if (client.connected) return "idle";
983
+ return null;
984
+ }
985
+ function writeHeartbeat(options, client, health) {
986
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
987
+ const previousHeartbeat = readPreviousHeartbeat(options.stateDir);
988
+ const lastDispatchAt = readLastDispatchAt(options.stateDir);
989
+ const turnState = resolveTurnState(client);
990
+ const lastTurnAt = previousHeartbeat?.activeTurnId && !client?.activeTurnId ? nowIso : previousHeartbeat?.lastTurnAt ?? null;
991
+ const idleSince = turnState === "idle" || turnState === "waiting-approval" ? previousHeartbeat?.turnState === turnState && previousHeartbeat.idleSince ? previousHeartbeat.idleSince : lastTurnAt ?? lastDispatchAt ?? nowIso : null;
992
+ if (client?.threadId) {
993
+ const savedThread = readThreadState(options.stateDir);
994
+ persistThreadState(
995
+ options.stateDir,
996
+ client.threadId,
997
+ options.appServerUrl,
998
+ options.ephemeral,
999
+ client.currentThreadCwd ?? savedThread?.cwd ?? null
1000
+ );
1001
+ }
1002
+ const payload = {
1003
+ pid: process.pid,
1004
+ agent: options.agentName,
1005
+ updatedAt: nowIso,
1006
+ pollSeconds: options.pollSeconds,
1007
+ appServerUrl: options.appServerUrl,
1008
+ authenticated: Boolean(options.gatewayToken),
1009
+ connected: client?.connected ?? false,
1010
+ initialized: client?.initialized ?? false,
1011
+ threadId: client?.threadId ?? null,
1012
+ threadCwd: client?.currentThreadCwd ?? null,
1013
+ activeTurnId: client?.activeTurnId ?? null,
1014
+ turnStartedAt: client?.turnStartedAt ?? null,
1015
+ lastTurnStatus: client?.lastTurnStatus ?? null,
1016
+ lastTurnAt,
1017
+ lastDispatchAt,
1018
+ idleSince,
1019
+ turnState: turnState ?? void 0,
1020
+ lastNotificationMethod: client?.lastNotificationMethod ?? null,
1021
+ lastNotificationAt: client?.lastNotificationAt ?? null,
1022
+ lastError: sanitizeErrorForPersistence(client?.lastError ?? null),
1023
+ lastSuccessfulAppServerAt: client?.lastSuccessfulAppServerAt ?? null,
1024
+ lastSuccessfulAppServerMethod: client?.lastSuccessfulAppServerMethod ?? null,
1025
+ consecutiveFailureCount: health.consecutiveFailureCount,
1026
+ busyMode: options.busyMode
1027
+ };
1028
+ writeFileSync4(
1029
+ join6(options.stateDir, "heartbeat.json"),
1030
+ `${JSON.stringify(payload, null, 2)}
1031
+ `,
1032
+ "utf8"
1033
+ );
1034
+ heartbeatCount += 1;
1035
+ if (heartbeatCount % 5 === 0) {
1036
+ heartbeatLogger.debug("heartbeat written", {
1037
+ connected: payload.connected,
1038
+ threadId: payload.threadId ?? "null",
1039
+ activeTurnId: payload.activeTurnId ?? null,
1040
+ consecutiveFailureCount: payload.consecutiveFailureCount
1041
+ });
1042
+ }
1043
+ const status = turnState === "active" ? "active" : "idle";
1044
+ updateCommsHeartbeat(options, status);
1045
+ }
1046
+ async function dispatchCandidate(client, options, candidate, heartbeats) {
1047
+ const input = buildUserInput(candidate, options.agentName, heartbeats);
1048
+ dispatchLogger.info("dispatching candidate", {
1049
+ sender: candidate.sender || "unknown",
1050
+ recipient: candidate.recipient || options.agentName,
1051
+ subject: candidate.subject || "(none)",
1052
+ fileName: candidate.fileName,
1053
+ threadId: client.threadId,
1054
+ activeTurnId: client.activeTurnId,
1055
+ busyMode: options.busyMode
1056
+ });
1057
+ if (client.isBusy()) {
1058
+ if (options.busyMode !== "steer") {
1059
+ dispatchLogger.debug("bridge busy and steer disabled", {
1060
+ fileName: candidate.fileName,
1061
+ activeTurnId: client.activeTurnId
1062
+ });
1063
+ return false;
1064
+ }
1065
+ try {
1066
+ const turnId2 = await client.steerTurn(input);
1067
+ writeProcessedMarker(
1068
+ options.stateDir,
1069
+ candidate,
1070
+ "steer",
1071
+ client.threadId,
1072
+ turnId2
1073
+ );
1074
+ writeLastDispatch(
1075
+ options.stateDir,
1076
+ candidate,
1077
+ "steer",
1078
+ client.threadId,
1079
+ turnId2
1080
+ );
1081
+ dispatchLogger.info("steered active turn", {
1082
+ fileName: candidate.fileName,
1083
+ threadId: client.threadId,
1084
+ turnId: turnId2
1085
+ });
1086
+ return true;
1087
+ } catch (error) {
1088
+ await client.refreshCurrentThreadState().catch(() => void 0);
1089
+ if (!client.isBusy()) {
1090
+ return dispatchCandidate(client, options, candidate, heartbeats);
1091
+ }
1092
+ if (shouldRetrySteerAsStart(error)) {
1093
+ client.activeTurnId = null;
1094
+ client.turnStartedAt = null;
1095
+ dispatchLogger.warn("steer fallback to start", {
1096
+ fileName: candidate.fileName,
1097
+ threadId: client.threadId,
1098
+ error: sanitizeErrorForPersistence(String(error))
1099
+ });
1100
+ return dispatchCandidate(client, options, candidate, heartbeats);
1101
+ }
1102
+ throw error;
1103
+ }
1104
+ }
1105
+ const turnId = await client.startTurn(input);
1106
+ writeProcessedMarker(
1107
+ options.stateDir,
1108
+ candidate,
1109
+ "start",
1110
+ client.threadId,
1111
+ turnId
1112
+ );
1113
+ writeLastDispatch(
1114
+ options.stateDir,
1115
+ candidate,
1116
+ "start",
1117
+ client.threadId,
1118
+ turnId
1119
+ );
1120
+ dispatchLogger.info("started turn for candidate", {
1121
+ fileName: candidate.fileName,
1122
+ threadId: client.threadId,
1123
+ turnId
1124
+ });
1125
+ return true;
1126
+ }
1127
+ async function runScan(options, cutoff, client) {
1128
+ const { heartbeats, candidates } = getPendingCandidates(options, cutoff);
1129
+ if (candidates.length === 0) {
1130
+ dispatchLogger.debug("no pending candidates", {
1131
+ cutoff: cutoff.toISOString(),
1132
+ agentName: options.agentName
1133
+ });
1134
+ }
1135
+ let maxMtimeMs = 0;
1136
+ for (const candidate of candidates) {
1137
+ if (options.dryRun) {
1138
+ dispatchLogger.info("dry-run candidate", {
1139
+ fileName: candidate.fileName,
1140
+ sender: candidate.sender,
1141
+ recipient: candidate.recipient
1142
+ });
1143
+ maxMtimeMs = Math.max(maxMtimeMs, candidate.mtimeMs);
1144
+ continue;
1145
+ }
1146
+ if (!client) {
1147
+ throw new Error("App Server client is not available");
1148
+ }
1149
+ const dispatched = await dispatchCandidate(
1150
+ client,
1151
+ options,
1152
+ candidate,
1153
+ heartbeats
1154
+ );
1155
+ if (!dispatched && options.busyMode === "wait") {
1156
+ return { dispatched: false, maxMtimeMs };
1157
+ }
1158
+ maxMtimeMs = Math.max(maxMtimeMs, candidate.mtimeMs);
1159
+ return { dispatched: true, maxMtimeMs };
1160
+ }
1161
+ return { dispatched: false, maxMtimeMs: 0 };
1162
+ }
1163
+ async function waitForTurnDrain(options, client, health) {
1164
+ const deadline = Date.now() + options.waitAfterDispatchSeconds * 1e3;
1165
+ while (Date.now() < deadline) {
1166
+ writeHeartbeat(options, client, health);
1167
+ if (!client.activeTurnId) {
1168
+ return;
1169
+ }
1170
+ await delay(1e3);
1171
+ }
1172
+ dispatchLogger.warn("wait-after-dispatch deadline reached", {
1173
+ threadId: client.threadId,
1174
+ activeTurnId: client.activeTurnId,
1175
+ waitAfterDispatchSeconds: options.waitAfterDispatchSeconds
1176
+ });
1177
+ }
1178
+ async function waitForTurnCompletion(client, turnId, timeoutMs) {
1179
+ const deadline = Date.now() + timeoutMs;
1180
+ let nextRefreshAt = Date.now();
1181
+ while (Date.now() < deadline) {
1182
+ if (!client.activeTurnId || client.activeTurnId !== turnId) {
1183
+ return client.lastTurnStatus;
1184
+ }
1185
+ if (Date.now() >= nextRefreshAt) {
1186
+ await client.refreshCurrentThreadState().catch(() => void 0);
1187
+ if (!client.activeTurnId || client.activeTurnId !== turnId) {
1188
+ return client.lastTurnStatus;
1189
+ }
1190
+ nextRefreshAt = Date.now() + TURN_COMPLETION_REFRESH_MS;
1191
+ }
1192
+ await delay(
1193
+ Math.min(TURN_COMPLETION_POLL_MS, Math.max(deadline - Date.now(), 0))
1194
+ );
1195
+ }
1196
+ await client.refreshCurrentThreadState().catch(() => void 0);
1197
+ if (!client.activeTurnId || client.activeTurnId !== turnId) {
1198
+ return client.lastTurnStatus;
1199
+ }
1200
+ throw new Error(`Timed out waiting for turn ${turnId} to complete`);
1201
+ }
1202
+ async function maybeBootstrapHeadlessTurn(options, cutoff, client) {
1203
+ if (process.env.TAP_HEADLESS !== "true" && process.env.TAP_COLD_START_WARMUP !== "true") {
833
1204
  return false;
834
1205
  }
835
- const message = error.message.toLowerCase();
836
- return message.includes("no active turn") || message.includes("expectedturnid") || message.includes("turn/steer failed") && (message.includes("active turn") || message.includes("not found"));
1206
+ const { candidates } = getPendingCandidates(options, cutoff);
1207
+ if (candidates.length > 0 || client.activeTurnId || client.lastTurnStatus !== null) {
1208
+ return false;
1209
+ }
1210
+ dispatchLogger.info("headless cold-start warmup starting", {
1211
+ threadId: client.activeTurnId
1212
+ });
1213
+ const turnId = await client.startTurn(HEADLESS_WARMUP_PROMPT);
1214
+ if (!turnId) {
1215
+ throw new Error(
1216
+ "Headless cold-start warmup failed: turn/start did not return a turn id. Run: npx @hua-labs/tap doctor"
1217
+ );
1218
+ }
1219
+ try {
1220
+ const status = await waitForTurnCompletion(
1221
+ client,
1222
+ turnId,
1223
+ HEADLESS_WARMUP_TIMEOUT_MS
1224
+ );
1225
+ if (status !== "completed") {
1226
+ throw new Error(
1227
+ `turn ${turnId} finished with status ${status ?? "unknown"}`
1228
+ );
1229
+ }
1230
+ dispatchLogger.info("headless cold-start warmup completed", {
1231
+ turnId,
1232
+ status
1233
+ });
1234
+ return true;
1235
+ } catch (error) {
1236
+ const reason = error instanceof Error ? error.message : String(error);
1237
+ throw new Error(
1238
+ `Headless cold-start warmup failed: ${reason}. Run: npx @hua-labs/tap doctor`
1239
+ );
1240
+ }
837
1241
  }
1242
+
1243
+ // scripts/bridge/bridge-ws-client.ts
838
1244
  async function readSocketData(data) {
839
1245
  if (typeof data === "string") {
840
1246
  return data;
@@ -852,11 +1258,27 @@ async function readSocketData(data) {
852
1258
  }
853
1259
  return String(data);
854
1260
  }
1261
+ function formatJsonRpcError(error) {
1262
+ if (!error) {
1263
+ return "Unknown App Server error";
1264
+ }
1265
+ return JSON.stringify(
1266
+ {
1267
+ code: error.code,
1268
+ message: error.message,
1269
+ data: error.data
1270
+ },
1271
+ null,
1272
+ 2
1273
+ );
1274
+ }
1275
+ var nextAppServerClientId = 1;
855
1276
  var AppServerClient = class {
856
1277
  socket = null;
857
1278
  url;
858
1279
  gatewayToken;
859
1280
  logger;
1281
+ clientId = nextAppServerClientId++;
860
1282
  nextId = 1;
861
1283
  pending = /* @__PURE__ */ new Map();
862
1284
  connected = false;
@@ -880,6 +1302,12 @@ var AppServerClient = class {
880
1302
  if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
881
1303
  return;
882
1304
  }
1305
+ if (!this.gatewayToken) {
1306
+ this.logger.warn(
1307
+ "connecting without auth token \u2014 app-server session is unprotected. Use --gateway-token-file or TAP_GATEWAY_TOKEN_FILE to enable auth.",
1308
+ { url: this.url }
1309
+ );
1310
+ }
883
1311
  const wsOptions = {};
884
1312
  if (this.gatewayToken) {
885
1313
  wsOptions.protocols = [`${AUTH_SUBPROTOCOL_PREFIX}${this.gatewayToken}`];
@@ -905,7 +1333,11 @@ var AppServerClient = class {
905
1333
  "open",
906
1334
  () => {
907
1335
  this.connected = true;
908
- this.logger(`connected to app-server at ${this.url}`);
1336
+ this.logger.info("connected to app-server", {
1337
+ clientId: this.clientId,
1338
+ url: this.url,
1339
+ authenticated: Boolean(this.gatewayToken)
1340
+ });
909
1341
  resolveOnce();
910
1342
  },
911
1343
  { once: true }
@@ -914,7 +1346,12 @@ var AppServerClient = class {
914
1346
  const error = new Error(
915
1347
  `Failed to connect to App Server at ${this.url}`
916
1348
  );
917
- this.lastError = error.message;
1349
+ this.lastError = sanitizeErrorForPersistence(error.message);
1350
+ this.logger.error("failed to connect to app-server", {
1351
+ clientId: this.clientId,
1352
+ url: this.url,
1353
+ error: this.lastError
1354
+ });
918
1355
  rejectOnce(error);
919
1356
  });
920
1357
  this.socket?.addEventListener("close", () => {
@@ -922,7 +1359,10 @@ var AppServerClient = class {
922
1359
  this.initialized = false;
923
1360
  this.activeTurnId = null;
924
1361
  this.turnStartedAt = null;
925
- this.logger("disconnected from app-server");
1362
+ this.logger.warn("disconnected from app-server", {
1363
+ clientId: this.clientId,
1364
+ url: this.url
1365
+ });
926
1366
  this.rejectPending(new Error("App Server connection closed"));
927
1367
  });
928
1368
  this.socket?.addEventListener("message", (event) => {
@@ -959,13 +1399,20 @@ var AppServerClient = class {
959
1399
  });
960
1400
  const resumedThreadId = resumeResponse?.thread?.id ?? explicitThreadId;
961
1401
  await this.refreshThreadState(resumedThreadId);
962
- this.logger(
963
- `resumed thread ${resumedThreadId}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
964
- );
1402
+ this.logger.info("resumed explicit thread", {
1403
+ clientId: this.clientId,
1404
+ threadId: resumedThreadId,
1405
+ activeTurnId: this.activeTurnId
1406
+ });
965
1407
  return resumedThreadId;
966
1408
  } catch (error) {
967
- this.logger(
968
- `thread resume failed for ${explicitThreadId}; starting a fresh thread (${String(error)})`
1409
+ this.logger.warn(
1410
+ "explicit thread resume failed; starting fresh thread",
1411
+ {
1412
+ clientId: this.clientId,
1413
+ threadId: explicitThreadId,
1414
+ error: sanitizeErrorForPersistence(String(error))
1415
+ }
969
1416
  );
970
1417
  }
971
1418
  }
@@ -975,9 +1422,12 @@ var AppServerClient = class {
975
1422
  }
976
1423
  if (savedThread?.threadId) {
977
1424
  if (savedThread.cwd && !threadCwdMatches(cwd, savedThread.cwd)) {
978
- this.logger(
979
- `saved thread ${savedThread.threadId} cwd ${savedThread.cwd} does not match ${cwd}; skipping saved thread`
980
- );
1425
+ this.logger.warn("saved thread cwd mismatch; skipping saved thread", {
1426
+ clientId: this.clientId,
1427
+ threadId: savedThread.threadId,
1428
+ savedCwd: savedThread.cwd,
1429
+ expectedCwd: cwd
1430
+ });
981
1431
  } else {
982
1432
  try {
983
1433
  const resumeResponse = await this.request("thread/resume", {
@@ -987,23 +1437,33 @@ var AppServerClient = class {
987
1437
  const resumedThreadId = resumeResponse?.thread?.id ?? savedThread.threadId;
988
1438
  await this.refreshThreadState(resumedThreadId);
989
1439
  if (!threadCwdMatches(cwd, this.currentThreadCwd)) {
990
- this.logger(
991
- `saved thread ${resumedThreadId} cwd ${this.currentThreadCwd ?? "unknown"} does not match ${cwd}; starting a fresh thread`
992
- );
1440
+ this.logger.warn("saved thread resumed with mismatched cwd", {
1441
+ clientId: this.clientId,
1442
+ threadId: resumedThreadId,
1443
+ expectedCwd: cwd,
1444
+ actualCwd: this.currentThreadCwd ?? "unknown"
1445
+ });
993
1446
  this.threadId = null;
994
1447
  this.currentThreadCwd = null;
995
1448
  this.activeTurnId = null;
996
1449
  this.turnStartedAt = null;
997
1450
  this.lastTurnStatus = null;
998
1451
  } else {
999
- this.logger(
1000
- `resumed saved thread ${resumedThreadId}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
1001
- );
1452
+ this.logger.info("resumed saved thread", {
1453
+ clientId: this.clientId,
1454
+ threadId: resumedThreadId,
1455
+ activeTurnId: this.activeTurnId
1456
+ });
1002
1457
  return resumedThreadId;
1003
1458
  }
1004
1459
  } catch (error) {
1005
- this.logger(
1006
- `saved thread resume failed for ${savedThread.threadId}; starting a fresh thread (${String(error)})`
1460
+ this.logger.warn(
1461
+ "saved thread resume failed; starting fresh thread",
1462
+ {
1463
+ clientId: this.clientId,
1464
+ threadId: savedThread.threadId,
1465
+ error: sanitizeErrorForPersistence(String(error))
1466
+ }
1007
1467
  );
1008
1468
  }
1009
1469
  }
@@ -1023,7 +1483,12 @@ var AppServerClient = class {
1023
1483
  this.currentThreadCwd = this.currentThreadCwd ?? cwd;
1024
1484
  this.activeTurnId = null;
1025
1485
  this.lastTurnStatus = null;
1026
- this.logger(`started thread ${startedThreadId}`);
1486
+ this.logger.info("started thread", {
1487
+ clientId: this.clientId,
1488
+ threadId: startedThreadId,
1489
+ cwd: this.currentThreadCwd,
1490
+ ephemeral
1491
+ });
1027
1492
  return startedThreadId;
1028
1493
  }
1029
1494
  async findLoadedThread(cwd) {
@@ -1061,14 +1526,21 @@ var AppServerClient = class {
1061
1526
  const chosen = chooseLoadedThreadForCwd(cwd, threads);
1062
1527
  if (!chosen) {
1063
1528
  if (threads.length > 0) {
1064
- this.logger(`loaded threads exist but none match cwd ${cwd}`);
1529
+ this.logger.debug("loaded threads exist but none match cwd", {
1530
+ clientId: this.clientId,
1531
+ cwd,
1532
+ loadedThreadCount: threads.length
1533
+ });
1065
1534
  }
1066
1535
  return null;
1067
1536
  }
1068
1537
  this.syncThreadStateFromThread(chosen.thread);
1069
- this.logger(
1070
- `attached to loaded thread ${chosen.id}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
1071
- );
1538
+ this.logger.info("attached to loaded thread", {
1539
+ clientId: this.clientId,
1540
+ threadId: chosen.id,
1541
+ activeTurnId: this.activeTurnId,
1542
+ cwd: chosen.cwd
1543
+ });
1072
1544
  return chosen.id;
1073
1545
  }
1074
1546
  async startTurn(inputText) {
@@ -1107,7 +1579,18 @@ var AppServerClient = class {
1107
1579
  return turnId;
1108
1580
  }
1109
1581
  isBusy() {
1110
- return Boolean(this.activeTurnId);
1582
+ if (!this.activeTurnId) return false;
1583
+ if (isTurnStale(this.turnStartedAt)) {
1584
+ this.logger.warn("active turn is stale; treating bridge as idle", {
1585
+ clientId: this.clientId,
1586
+ turnId: this.activeTurnId,
1587
+ turnStartedAt: this.turnStartedAt
1588
+ });
1589
+ this.activeTurnId = null;
1590
+ this.turnStartedAt = null;
1591
+ return false;
1592
+ }
1593
+ return true;
1111
1594
  }
1112
1595
  async refreshCurrentThreadState() {
1113
1596
  if (!this.threadId) {
@@ -1141,12 +1624,33 @@ var AppServerClient = class {
1141
1624
  this.currentThreadCwd = typeof thread?.cwd === "string" ? thread.cwd : null;
1142
1625
  let activeTurnId = null;
1143
1626
  let lastTurnStatus = null;
1627
+ const threadActiveFlags = Array.isArray(
1628
+ thread?.status?.activeFlags
1629
+ ) ? thread.status.activeFlags : [];
1630
+ const threadStuckOnApproval = isTurnStuckOnApproval(threadActiveFlags);
1631
+ if (threadStuckOnApproval) {
1632
+ this.logger.warn("thread waitingOnApproval; ignoring in-progress turns", {
1633
+ clientId: this.clientId,
1634
+ threadId: this.threadId
1635
+ });
1636
+ }
1144
1637
  const turns = Array.isArray(thread?.turns) ? thread.turns : [];
1145
1638
  for (const turn of turns) {
1146
1639
  if (typeof turn?.status === "string") {
1147
1640
  lastTurnStatus = turn.status;
1148
1641
  }
1149
1642
  if (turn?.status === "inProgress" && typeof turn.id === "string") {
1643
+ if (threadStuckOnApproval) {
1644
+ continue;
1645
+ }
1646
+ const turnActiveFlags = Array.isArray(turn.activeFlags) ? turn.activeFlags : [];
1647
+ if (isTurnStuckOnApproval(turnActiveFlags)) {
1648
+ this.logger.warn("turn waitingOnApproval; ignoring turn as active", {
1649
+ clientId: this.clientId,
1650
+ turnId: turn.id
1651
+ });
1652
+ continue;
1653
+ }
1150
1654
  activeTurnId = turn.id;
1151
1655
  }
1152
1656
  }
@@ -1169,7 +1673,12 @@ var AppServerClient = class {
1169
1673
  this.pending.delete(message.id);
1170
1674
  if (message.error) {
1171
1675
  const errorText = formatJsonRpcError(message.error);
1172
- this.lastError = errorText;
1676
+ this.lastError = sanitizeErrorForPersistence(errorText);
1677
+ this.logger.error("app-server request failed", {
1678
+ clientId: this.clientId,
1679
+ method: pending.method,
1680
+ error: this.lastError
1681
+ });
1173
1682
  pending.reject(new Error(`${pending.method} failed: ${errorText}`));
1174
1683
  return;
1175
1684
  }
@@ -1184,6 +1693,10 @@ var AppServerClient = class {
1184
1693
  }
1185
1694
  this.lastNotificationMethod = message.method;
1186
1695
  this.lastNotificationAt = (/* @__PURE__ */ new Date()).toISOString();
1696
+ this.logger.debug("received app-server notification", {
1697
+ clientId: this.clientId,
1698
+ method: message.method
1699
+ });
1187
1700
  this.handleNotification(message.method, message.params);
1188
1701
  }
1189
1702
  handleNotification(method, params) {
@@ -1195,18 +1708,28 @@ var AppServerClient = class {
1195
1708
  if (typeof params?.thread?.cwd === "string") {
1196
1709
  this.currentThreadCwd = params.thread.cwd;
1197
1710
  }
1198
- this.logger(`thread started ${params?.thread?.id ?? ""}`.trim());
1711
+ this.logger.info("thread started notification", {
1712
+ clientId: this.clientId,
1713
+ threadId: params?.thread?.id ?? null,
1714
+ cwd: params?.thread?.cwd ?? null
1715
+ });
1199
1716
  break;
1200
1717
  case "thread/status/changed":
1201
- this.logger(
1202
- `thread status changed (${params?.thread?.status?.type ?? params?.status?.type ?? "unknown"})`
1203
- );
1718
+ this.logger.debug("thread status changed", {
1719
+ clientId: this.clientId,
1720
+ threadId: params?.thread?.id ?? this.threadId,
1721
+ status: params?.thread?.status?.type ?? params?.status?.type ?? "unknown"
1722
+ });
1204
1723
  break;
1205
1724
  case "turn/started":
1206
1725
  if (params?.turn?.id) {
1207
1726
  this.activeTurnId = params.turn.id;
1208
1727
  this.turnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
1209
- this.logger(`turn started ${params.turn.id}`);
1728
+ this.logger.info("turn started", {
1729
+ clientId: this.clientId,
1730
+ threadId: this.threadId,
1731
+ turnId: params.turn.id
1732
+ });
1210
1733
  }
1211
1734
  break;
1212
1735
  case "turn/completed": {
@@ -1215,15 +1738,22 @@ var AppServerClient = class {
1215
1738
  this.activeTurnId = null;
1216
1739
  this.turnStartedAt = null;
1217
1740
  const elapsedMs = prevTurnStartedAt ? Date.now() - new Date(prevTurnStartedAt).getTime() : null;
1218
- const elapsedSuffix = elapsedMs !== null ? ` \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed` : "";
1219
- this.logger(
1220
- `turn completed (${this.lastTurnStatus ?? "unknown"})${elapsedSuffix}`
1221
- );
1741
+ this.logger.info("turn completed", {
1742
+ clientId: this.clientId,
1743
+ threadId: this.threadId,
1744
+ status: this.lastTurnStatus ?? "unknown",
1745
+ elapsedSeconds: elapsedMs !== null ? Math.round(elapsedMs / 1e3) : void 0
1746
+ });
1222
1747
  break;
1223
1748
  }
1224
1749
  case "error":
1225
- this.lastError = JSON.stringify(params ?? {}, null, 2);
1226
- this.logger(`app-server error notification: ${this.lastError}`);
1750
+ this.lastError = sanitizeErrorForPersistence(
1751
+ JSON.stringify(params ?? {}, null, 2)
1752
+ );
1753
+ this.logger.error("app-server error notification", {
1754
+ clientId: this.clientId,
1755
+ error: this.lastError
1756
+ });
1227
1757
  break;
1228
1758
  default:
1229
1759
  break;
@@ -1244,264 +1774,122 @@ var AppServerClient = class {
1244
1774
  return new Promise((resolvePromise, rejectPromise) => {
1245
1775
  this.pending.set(id, {
1246
1776
  resolve: resolvePromise,
1247
- reject: rejectPromise,
1248
- method
1249
- });
1250
- this.socket?.send(JSON.stringify(request));
1251
- });
1252
- }
1253
- rejectPending(error) {
1254
- for (const pending of this.pending.values()) {
1255
- pending.reject(error);
1256
- }
1257
- this.pending.clear();
1258
- }
1259
- };
1260
- var heartbeatCount = 0;
1261
- function writeHeartbeat(options, client, health) {
1262
- if (client?.threadId) {
1263
- const savedThread = readThreadState(options.stateDir);
1264
- persistThreadState(
1265
- options.stateDir,
1266
- client.threadId,
1267
- options.appServerUrl,
1268
- options.ephemeral,
1269
- client.currentThreadCwd ?? savedThread?.cwd ?? null
1270
- );
1271
- }
1272
- const payload = {
1273
- pid: process.pid,
1274
- agent: options.agentName,
1275
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1276
- pollSeconds: options.pollSeconds,
1277
- appServerUrl: options.appServerUrl,
1278
- connected: client?.connected ?? false,
1279
- initialized: client?.initialized ?? false,
1280
- threadId: client?.threadId ?? null,
1281
- threadCwd: client?.currentThreadCwd ?? null,
1282
- activeTurnId: client?.activeTurnId ?? null,
1283
- turnStartedAt: client?.turnStartedAt ?? null,
1284
- lastTurnStatus: client?.lastTurnStatus ?? null,
1285
- lastNotificationMethod: client?.lastNotificationMethod ?? null,
1286
- lastNotificationAt: client?.lastNotificationAt ?? null,
1287
- lastError: sanitizeErrorForPersistence(client?.lastError ?? null),
1288
- lastSuccessfulAppServerAt: client?.lastSuccessfulAppServerAt ?? null,
1289
- lastSuccessfulAppServerMethod: client?.lastSuccessfulAppServerMethod ?? null,
1290
- consecutiveFailureCount: health.consecutiveFailureCount,
1291
- busyMode: options.busyMode
1292
- };
1293
- writeFileSync(
1294
- join(options.stateDir, "heartbeat.json"),
1295
- `${JSON.stringify(payload, null, 2)}
1296
- `,
1297
- "utf8"
1298
- );
1299
- heartbeatCount += 1;
1300
- if (heartbeatCount % 5 === 0) {
1301
- logStatus(
1302
- `heartbeat: connected=${payload.connected}, thread=${payload.threadId ?? "null"}, turns=${payload.activeTurnId ? "active" : "0"}`
1303
- );
1777
+ reject: rejectPromise,
1778
+ method
1779
+ });
1780
+ this.socket?.send(JSON.stringify(request));
1781
+ });
1304
1782
  }
1305
- const status = client?.connected ? "active" : "idle";
1306
- updateCommsHeartbeat(options, status);
1307
- }
1308
- var COMMS_HEARTBEAT_LOCK_TIMEOUT_MS = 2e3;
1309
- var COMMS_LOCK_STALE_AGE_MS = 1e4;
1310
- function acquireCommsLock(lockPath) {
1311
- const deadline = Date.now() + COMMS_HEARTBEAT_LOCK_TIMEOUT_MS;
1312
- while (Date.now() < deadline) {
1313
- try {
1314
- writeFileSync(lockPath, String(process.pid), { flag: "wx" });
1315
- return true;
1316
- } catch {
1317
- try {
1318
- const lockAge = Date.now() - statSync(lockPath).mtimeMs;
1319
- if (lockAge > COMMS_LOCK_STALE_AGE_MS) {
1320
- unlinkSync(lockPath);
1321
- try {
1322
- writeFileSync(lockPath, String(process.pid), { flag: "wx" });
1323
- return true;
1324
- } catch {
1325
- }
1326
- }
1327
- } catch {
1328
- }
1329
- const start = Date.now();
1330
- while (Date.now() - start < 50) {
1331
- }
1783
+ rejectPending(error) {
1784
+ for (const pending of this.pending.values()) {
1785
+ pending.reject(error);
1332
1786
  }
1787
+ this.pending.clear();
1333
1788
  }
1334
- return false;
1789
+ };
1790
+
1791
+ // scripts/bridge/bridge-main.ts
1792
+ import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1793
+ import { isAbsolute as isAbsolute3, join as join7, resolve as resolve4 } from "path";
1794
+ import { pathToFileURL } from "url";
1795
+ function delay2(ms) {
1796
+ return new Promise((resolvePromise) => {
1797
+ setTimeout(resolvePromise, ms);
1798
+ });
1335
1799
  }
1336
- function releaseCommsLock(lockPath) {
1800
+ function readHeartbeatState(stateDir) {
1801
+ const heartbeatPath = join7(stateDir, "heartbeat.json");
1802
+ if (!existsSync6(heartbeatPath)) {
1803
+ return null;
1804
+ }
1337
1805
  try {
1338
- unlinkSync(lockPath);
1806
+ return JSON.parse(readFileSync6(heartbeatPath, "utf8"));
1339
1807
  } catch {
1808
+ return null;
1340
1809
  }
1341
1810
  }
1342
- function updateCommsHeartbeat(options, status) {
1343
- const heartbeatsPath = join(options.commsDir, "heartbeats.json");
1344
- const lockPath = join(options.commsDir, ".heartbeats.lock");
1345
- if (!acquireCommsLock(lockPath)) {
1346
- return;
1811
+ function parseUpdatedAt(value) {
1812
+ if (!value) {
1813
+ return 0;
1347
1814
  }
1348
- try {
1349
- let store = {};
1350
- try {
1351
- store = JSON.parse(readFileSync(heartbeatsPath, "utf-8"));
1352
- } catch {
1353
- }
1354
- const key = options.agentId;
1355
- const existing = store[key];
1356
- store[key] = {
1357
- id: options.agentId,
1358
- agent: options.agentName,
1359
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1360
- lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
1361
- joinedAt: existing?.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1362
- status
1815
+ const parsed = Date.parse(value);
1816
+ return Number.isFinite(parsed) ? parsed : 0;
1817
+ }
1818
+ function appServerUrlMatches(expectedAppServerUrl, actualAppServerUrl) {
1819
+ return actualAppServerUrl?.trim() === expectedAppServerUrl;
1820
+ }
1821
+ function hasValidHeartbeatThreadCwd(threadCwd) {
1822
+ const normalized = threadCwd?.trim();
1823
+ if (!normalized) {
1824
+ return false;
1825
+ }
1826
+ return isAbsolute3(normalized) || /^[A-Za-z]:[\\/]/.test(normalized) || normalized.startsWith("\\\\");
1827
+ }
1828
+ function loadResumableThreadState(stateDir, fallbackAppServerUrl) {
1829
+ const savedThread = readThreadState(stateDir);
1830
+ const heartbeat = readHeartbeatState(stateDir);
1831
+ const heartbeatThreadId = heartbeat?.threadId?.trim();
1832
+ if (!heartbeatThreadId) {
1833
+ return savedThread;
1834
+ }
1835
+ if (!appServerUrlMatches(fallbackAppServerUrl, heartbeat?.appServerUrl)) {
1836
+ return savedThread;
1837
+ }
1838
+ if (!hasValidHeartbeatThreadCwd(heartbeat?.threadCwd)) {
1839
+ return savedThread;
1840
+ }
1841
+ const heartbeatBackedThread = {
1842
+ threadId: heartbeatThreadId,
1843
+ updatedAt: heartbeat?.updatedAt ?? savedThread?.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1844
+ appServerUrl: heartbeat?.appServerUrl || savedThread?.appServerUrl || fallbackAppServerUrl,
1845
+ ephemeral: savedThread?.ephemeral ?? false,
1846
+ cwd: heartbeat?.threadCwd ?? (savedThread?.threadId === heartbeatThreadId ? savedThread.cwd ?? null : null)
1847
+ };
1848
+ let preferred = savedThread;
1849
+ if (!savedThread?.threadId) {
1850
+ preferred = heartbeatBackedThread;
1851
+ } else if (savedThread.threadId === heartbeatThreadId) {
1852
+ preferred = {
1853
+ ...savedThread,
1854
+ updatedAt: heartbeatBackedThread.updatedAt ?? savedThread.updatedAt,
1855
+ appServerUrl: heartbeatBackedThread.appServerUrl,
1856
+ cwd: heartbeatBackedThread.cwd ?? savedThread.cwd ?? null
1363
1857
  };
1364
- const tmpPath = heartbeatsPath + ".tmp." + process.pid;
1365
- writeFileSync(tmpPath, JSON.stringify(store, null, 2), "utf-8");
1366
- renameSync(tmpPath, heartbeatsPath);
1367
- } catch {
1368
- } finally {
1369
- releaseCommsLock(lockPath);
1858
+ } else if (parseUpdatedAt(heartbeat?.updatedAt) > parseUpdatedAt(savedThread.updatedAt)) {
1859
+ preferred = heartbeatBackedThread;
1370
1860
  }
1861
+ return preferred;
1371
1862
  }
1372
- async function dispatchCandidate(client, options, candidate, heartbeats) {
1373
- const input = buildUserInput(candidate, options.agentName, heartbeats);
1374
- logStatus(
1375
- `dispatching from ${candidate.sender || "unknown"}: ${candidate.subject || "(none)"}`
1376
- );
1377
- if (client.isBusy()) {
1378
- if (options.busyMode !== "steer") {
1379
- return false;
1380
- }
1863
+ function getGeneralInboxCutoff(stateDir, lookbackMinutes, processExistingMessages) {
1864
+ if (processExistingMessages) {
1865
+ return /* @__PURE__ */ new Date(0);
1866
+ }
1867
+ const lookbackCutoff = lookbackMinutes > 0 ? new Date(Date.now() - lookbackMinutes * 6e4) : null;
1868
+ const cutoffPath = join7(stateDir, "general-inbox-cutoff.txt");
1869
+ if (existsSync6(cutoffPath)) {
1381
1870
  try {
1382
- const turnId2 = await client.steerTurn(input);
1383
- writeProcessedMarker(
1384
- options.stateDir,
1385
- candidate,
1386
- "steer",
1387
- client.threadId,
1388
- turnId2
1389
- );
1390
- writeLastDispatch(
1391
- options.stateDir,
1392
- candidate,
1393
- "steer",
1394
- client.threadId,
1395
- turnId2
1396
- );
1397
- logStatus(`steered active turn with ${candidate.fileName}`);
1398
- return true;
1399
- } catch (error) {
1400
- await client.refreshCurrentThreadState().catch(() => void 0);
1401
- if (!client.isBusy()) {
1402
- return dispatchCandidate(client, options, candidate, heartbeats);
1403
- }
1404
- if (shouldRetrySteerAsStart(error)) {
1405
- client.activeTurnId = null;
1406
- client.turnStartedAt = null;
1407
- logStatus(
1408
- `steer fallback -> start for ${candidate.fileName} (${String(error)})`
1409
- );
1410
- return dispatchCandidate(client, options, candidate, heartbeats);
1871
+ const saved = new Date(readFileSync6(cutoffPath, "utf8").trim());
1872
+ if (!isNaN(saved.getTime())) {
1873
+ if (lookbackCutoff && lookbackCutoff > saved) {
1874
+ return lookbackCutoff;
1875
+ }
1876
+ return saved;
1411
1877
  }
1412
- throw error;
1413
- }
1414
- }
1415
- const turnId = await client.startTurn(input);
1416
- writeProcessedMarker(
1417
- options.stateDir,
1418
- candidate,
1419
- "start",
1420
- client.threadId,
1421
- turnId
1422
- );
1423
- writeLastDispatch(
1424
- options.stateDir,
1425
- candidate,
1426
- "start",
1427
- client.threadId,
1428
- turnId
1429
- );
1430
- logStatus(`dispatched ${candidate.fileName} to thread ${client.threadId}`);
1431
- return true;
1432
- }
1433
- async function runScan(options, cutoff, client) {
1434
- const { heartbeats, candidates } = getPendingCandidates(options, cutoff);
1435
- for (const candidate of candidates) {
1436
- if (options.dryRun) {
1437
- logStatus(`dry-run candidate ${candidate.fileName}`);
1438
- continue;
1439
- }
1440
- if (!client) {
1441
- throw new Error("App Server client is not available");
1442
- }
1443
- const dispatched = await dispatchCandidate(
1444
- client,
1445
- options,
1446
- candidate,
1447
- heartbeats
1448
- );
1449
- if (!dispatched && options.busyMode === "wait") {
1450
- return false;
1878
+ } catch {
1451
1879
  }
1452
- return true;
1453
1880
  }
1454
- return false;
1455
- }
1456
- async function waitForTurnDrain(options, client, health) {
1457
- const deadline = Date.now() + options.waitAfterDispatchSeconds * 1e3;
1458
- while (Date.now() < deadline) {
1459
- writeHeartbeat(options, client, health);
1460
- if (!client.activeTurnId) {
1461
- return;
1462
- }
1463
- await delay(1e3);
1881
+ if (lookbackCutoff) {
1882
+ return lookbackCutoff;
1464
1883
  }
1465
- }
1466
- function buildOptions(argv) {
1467
- const parsed = parseArgs(argv);
1468
- const repoRoot = resolveRepoRoot(parsed.repoRoot);
1469
- const commsDir = resolveCommsDir(repoRoot, parsed.commsDir);
1470
- const preferredAgentName = resolvePreferredAgentName(parsed.agentName);
1471
- const stateDir = resolveStateDir(
1472
- repoRoot,
1473
- parsed.stateDir,
1474
- preferredAgentName
1475
- );
1476
- const agentName = resolveAgentName(preferredAgentName, stateDir);
1477
- const agentId = resolveAgentId(agentName);
1478
- persistAgentName(stateDir, agentName);
1479
- const gatewayTokenFile = parsed.gatewayTokenFile?.trim() || process.env.TAP_GATEWAY_TOKEN_FILE?.trim() || null;
1480
- const appServerUrl = parsed.appServerUrl?.trim() || process.env.CODEX_APP_SERVER_URL || DEFAULT_APP_SERVER_URL;
1481
- return {
1482
- repoRoot,
1483
- commsDir,
1484
- agentId,
1485
- stateDir,
1486
- agentName,
1487
- pollSeconds: parsed.pollSeconds ?? 5,
1488
- reconnectSeconds: parsed.reconnectSeconds ?? 5,
1489
- messageLookbackMinutes: parsed.messageLookbackMinutes ?? 10,
1490
- processExistingMessages: parsed.processExistingMessages,
1491
- dryRun: parsed.dryRun,
1492
- runOnce: parsed.runOnce,
1493
- waitAfterDispatchSeconds: parsed.waitAfterDispatchSeconds ?? 0,
1494
- appServerUrl,
1495
- connectAppServerUrl: appServerUrl,
1496
- gatewayToken: gatewayTokenFile ? readGatewayTokenFile(gatewayTokenFile) : null,
1497
- gatewayTokenFile,
1498
- busyMode: parsed.busyMode ?? "steer",
1499
- threadId: parsed.threadId?.trim() || null,
1500
- ephemeral: parsed.ephemeral
1501
- };
1884
+ const cutoff = /* @__PURE__ */ new Date();
1885
+ writeFileSync5(cutoffPath, `${cutoff.toISOString()}
1886
+ `, "utf8");
1887
+ return cutoff;
1502
1888
  }
1503
1889
  async function main() {
1504
1890
  const options = buildOptions(process.argv.slice(2));
1891
+ configureBridgeLogging(options.logLevel);
1892
+ const logger = createBridgeLogger("bridge");
1505
1893
  const cutoff = getGeneralInboxCutoff(
1506
1894
  options.stateDir,
1507
1895
  options.messageLookbackMinutes,
@@ -1511,28 +1899,20 @@ async function main() {
1511
1899
  options.stateDir,
1512
1900
  options.appServerUrl
1513
1901
  );
1514
- logStatus("codex app-server bridge ready");
1515
- console.log(` repo: ${options.repoRoot}`);
1516
- console.log(` comms: ${options.commsDir}`);
1517
- console.log(` agent: ${options.agentName}`);
1518
- console.log(` state: ${options.stateDir}`);
1519
- console.log(` app-server: ${options.appServerUrl}`);
1520
- console.log(` busy-mode: ${options.busyMode}`);
1521
- if (options.waitAfterDispatchSeconds > 0) {
1522
- console.log(
1523
- ` wait: ${options.waitAfterDispatchSeconds}s after dispatch`
1524
- );
1525
- }
1526
- console.log(
1527
- ` lookback: ${options.processExistingMessages ? "existing messages" : `${options.messageLookbackMinutes} minute(s)`}`
1528
- );
1529
- if (options.threadId || initialSavedThread?.threadId) {
1530
- console.log(
1531
- ` thread: ${options.threadId ?? initialSavedThread?.threadId}`
1532
- );
1533
- }
1902
+ logger.info("codex app-server bridge ready", {
1903
+ repoRoot: options.repoRoot,
1904
+ commsDir: options.commsDir,
1905
+ agentName: options.agentName,
1906
+ stateDir: options.stateDir,
1907
+ appServerUrl: options.appServerUrl,
1908
+ busyMode: options.busyMode,
1909
+ logLevel: options.logLevel,
1910
+ waitAfterDispatchSeconds: options.waitAfterDispatchSeconds > 0 ? options.waitAfterDispatchSeconds : void 0,
1911
+ lookback: options.processExistingMessages ? "existing messages" : `${options.messageLookbackMinutes} minute(s)`,
1912
+ threadId: options.threadId ?? initialSavedThread?.threadId
1913
+ });
1534
1914
  if (options.dryRun) {
1535
- logStatus("dry-run mode enabled");
1915
+ logger.info("dry-run mode enabled");
1536
1916
  }
1537
1917
  let client = null;
1538
1918
  const health = {
@@ -1544,7 +1924,7 @@ async function main() {
1544
1924
  if (!client || !client.connected) {
1545
1925
  client = new AppServerClient(
1546
1926
  options.connectAppServerUrl,
1547
- logStatus,
1927
+ createBridgeLogger("app-server"),
1548
1928
  options.gatewayToken
1549
1929
  );
1550
1930
  await client.connect();
@@ -1552,6 +1932,10 @@ async function main() {
1552
1932
  options.stateDir,
1553
1933
  options.appServerUrl
1554
1934
  );
1935
+ logger.debug("resolved resumable thread state", {
1936
+ savedThreadId: savedThread?.threadId,
1937
+ savedThreadCwd: savedThread?.cwd ?? null
1938
+ });
1555
1939
  const threadId = await client.ensureThread(
1556
1940
  options.threadId,
1557
1941
  savedThread,
@@ -1576,8 +1960,14 @@ async function main() {
1576
1960
  }
1577
1961
  }
1578
1962
  }
1579
- const dispatched = await runScan(options, cutoff, client);
1580
- if (dispatched && client && options.waitAfterDispatchSeconds > 0) {
1963
+ const scanResult = await runScan(options, cutoff, client);
1964
+ if (scanResult.dispatched && scanResult.maxMtimeMs > 0) {
1965
+ const cutoffPath = join7(options.stateDir, "general-inbox-cutoff.txt");
1966
+ const advancedCutoff = new Date(scanResult.maxMtimeMs);
1967
+ writeFileSync5(cutoffPath, `${advancedCutoff.toISOString()}
1968
+ `, "utf8");
1969
+ }
1970
+ if (scanResult.dispatched && client && options.waitAfterDispatchSeconds > 0) {
1581
1971
  await waitForTurnDrain(options, client, health);
1582
1972
  }
1583
1973
  health.consecutiveFailureCount = 0;
@@ -1585,22 +1975,28 @@ async function main() {
1585
1975
  if (options.runOnce) {
1586
1976
  break;
1587
1977
  }
1588
- await delay(options.pollSeconds * 1e3);
1978
+ await delay2(options.pollSeconds * 1e3);
1589
1979
  } catch (error) {
1590
1980
  const message = error instanceof Error ? error.message : String(error);
1591
- logStatus(`bridge error: ${message}`);
1981
+ logger.error("bridge error", {
1982
+ error: sanitizeErrorForPersistence(message)
1983
+ });
1592
1984
  if (client) {
1593
- client.lastError = message;
1985
+ client.lastError = sanitizeErrorForPersistence(message);
1594
1986
  }
1595
1987
  health.consecutiveFailureCount += 1;
1596
1988
  writeHeartbeat(options, client, health);
1597
1989
  if (options.runOnce) {
1598
- throw error;
1990
+ const sanitized = sanitizeErrorForPersistence(message);
1991
+ throw new Error(sanitized ?? message);
1599
1992
  }
1600
1993
  client?.disconnect().catch(() => void 0);
1601
1994
  client = null;
1602
- logStatus(`reconnecting in ${options.reconnectSeconds}s...`);
1603
- await delay(options.reconnectSeconds * 1e3);
1995
+ logger.warn("reconnecting after bridge error", {
1996
+ reconnectSeconds: options.reconnectSeconds,
1997
+ consecutiveFailureCount: health.consecutiveFailureCount
1998
+ });
1999
+ await delay2(options.reconnectSeconds * 1e3);
1604
2000
  }
1605
2001
  }
1606
2002
  await client?.disconnect();
@@ -1608,22 +2004,14 @@ async function main() {
1608
2004
  function isDirectExecution() {
1609
2005
  const entry = process.argv[1];
1610
2006
  if (!entry) return false;
1611
- return import.meta.url === pathToFileURL(resolve(entry)).href;
1612
- }
1613
- if (isDirectExecution()) {
1614
- main().catch((error) => {
1615
- console.error(
1616
- error instanceof Error ? error.stack ?? error.message : String(error)
1617
- );
1618
- process.exitCode = 1;
1619
- });
2007
+ return import.meta.url === pathToFileURL(resolve4(entry)).href;
1620
2008
  }
1621
2009
 
1622
2010
  // src/bridges/codex-app-server-bridge.ts
1623
2011
  function isDirectExecution2() {
1624
2012
  const entry = process.argv[1];
1625
2013
  if (!entry) return false;
1626
- return import.meta.url === pathToFileURL2(resolve2(entry)).href;
2014
+ return import.meta.url === pathToFileURL2(resolve5(entry)).href;
1627
2015
  }
1628
2016
  if (isDirectExecution2()) {
1629
2017
  main().catch((error) => {
@@ -1634,19 +2022,77 @@ if (isDirectExecution2()) {
1634
2022
  });
1635
2023
  }
1636
2024
  export {
2025
+ AUTH_SUBPROTOCOL_PREFIX,
2026
+ AppServerClient,
2027
+ COMMS_HEARTBEAT_LOCK_TIMEOUT_MS,
2028
+ COMMS_LOCK_STALE_AGE_MS,
2029
+ DEFAULT_AGENT,
2030
+ DEFAULT_APP_SERVER_URL,
2031
+ HEADLESS_SKIP_PATTERNS,
1637
2032
  HEADLESS_WARMUP_PROMPT,
2033
+ HEADLESS_WARMUP_TIMEOUT_MS,
2034
+ PLACEHOLDER_AGENT_VALUES,
2035
+ STALE_TURN_MS,
2036
+ TURN_COMPLETION_POLL_MS,
2037
+ TURN_COMPLETION_REFRESH_MS,
2038
+ acquireCommsLock,
2039
+ buildDefaultStateDir,
2040
+ buildMarkerId,
1638
2041
  buildOptions,
1639
2042
  buildUserInput,
2043
+ canonicalize,
1640
2044
  chooseLoadedThreadForCwd,
2045
+ collectCandidates,
2046
+ dispatchCandidate,
2047
+ formatAgentLabel,
2048
+ formatJsonRpcError,
2049
+ getGeneralInboxCutoff,
2050
+ getInboxRoute,
2051
+ getInboxRouteFromFilename,
2052
+ getPendingCandidates,
2053
+ getProcessedMarkerPath,
2054
+ isDirectExecution,
1641
2055
  isOwnMessageSender,
2056
+ isTurnStale,
2057
+ isTurnStuckOnApproval,
2058
+ loadHeartbeats,
1642
2059
  loadResumableThreadState,
1643
2060
  main,
1644
2061
  maybeBootstrapHeadlessTurn,
2062
+ normalizeAgentToken,
2063
+ normalizeThreadCwd,
2064
+ parseArgs,
2065
+ parseBridgeFrontmatter,
2066
+ persistAgentName,
2067
+ persistThreadState,
2068
+ readGatewayTokenFile,
2069
+ readHeartbeatState,
2070
+ readSocketData,
2071
+ readThreadState,
1645
2072
  recipientMatchesAgent,
2073
+ refreshAgentIdentity,
2074
+ releaseCommsLock,
1646
2075
  resolveAddressLabel,
1647
2076
  resolveAgentId,
2077
+ resolveAgentName,
2078
+ resolveCommsDir,
1648
2079
  resolveCurrentAgentName,
2080
+ resolvePreferredAgentName,
2081
+ resolveRepoRoot,
2082
+ resolveStateDir,
2083
+ resolveTapConfigPath,
2084
+ runScan,
2085
+ sanitizeErrorForPersistence,
2086
+ sanitizeStateSegment,
2087
+ shouldRetrySteerAsStart,
2088
+ shouldSkipInHeadlessMode,
2089
+ stripBridgeFrontmatter,
1649
2090
  threadCwdMatches,
1650
- waitForTurnCompletion
2091
+ updateCommsHeartbeat,
2092
+ waitForTurnCompletion,
2093
+ waitForTurnDrain,
2094
+ writeHeartbeat,
2095
+ writeLastDispatch,
2096
+ writeProcessedMarker
1651
2097
  };
1652
2098
  //# sourceMappingURL=codex-app-server-bridge.mjs.map