@hua-labs/tap 0.3.0 → 0.4.0

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