@hua-labs/tap 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,304 +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.replace(/-/g, "_");
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
- const canonicalRecipient = normalizedRecipient.replace(/-/g, "_");
483
- const canonicalAgentId = agentId.trim().replace(/-/g, "_");
484
- return normalizedRecipient === "\uC804\uCCB4" || normalizedRecipient === "all" || canonicalRecipient === canonicalAgentId || normalizedRecipient === agentName;
485
- }
486
- function isOwnMessageSender(sender, agentId, agentName) {
487
- const normalizedSender = sender.trim();
488
- if (!normalizedSender) {
489
- 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;
490
617
  }
491
- const canonicalSender = normalizedSender.replace(/-/g, "_");
492
- const canonicalAgentId = agentId.trim().replace(/-/g, "_");
493
- return canonicalSender === canonicalAgentId || normalizedSender === agentName;
494
- }
495
- function getInboxRoute(fileName) {
496
- const stem = fileName.replace(/\.md$/i, "");
497
- const parts = stem.split("-");
498
- let offset = 0;
499
- if (parts[0] && /^\d{8}$/.test(parts[0])) {
500
- offset = 1;
618
+ if (level === "warn") {
619
+ console.warn(line);
620
+ return;
501
621
  }
622
+ console.log(line);
623
+ }
624
+ function createBridgeLogger(scope) {
625
+ const scopedMessage = (message) => `[${scope}] ${message}`;
502
626
  return {
503
- sender: parts[offset] ?? "",
504
- recipient: parts[offset + 1] ?? "",
505
- 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
+ }
506
639
  };
507
640
  }
641
+
642
+ // ../../scripts/bridge/bridge-candidates.ts
643
+ var routingLogger = createBridgeLogger("routing");
508
644
  function buildMarkerId(filePath, mtimeMs) {
509
645
  return createHash("sha1").update(`${filePath}|${mtimeMs}`).digest("hex");
510
646
  }
511
647
  function getProcessedMarkerPath(stateDir, markerId) {
512
- return join(stateDir, "processed", `${markerId}.done`);
648
+ return join4(stateDir, "processed", `${markerId}.done`);
513
649
  }
514
650
  function loadHeartbeats(commsDir) {
515
651
  try {
516
- return JSON.parse(readFileSync(join(commsDir, "heartbeats.json"), "utf8"));
652
+ return JSON.parse(readFileSync4(join4(commsDir, "heartbeats.json"), "utf8"));
517
653
  } catch {
518
654
  return {};
519
655
  }
520
656
  }
521
- function formatAgentLabel(agentIdOrName, displayName) {
522
- const normalizedId = agentIdOrName.trim();
523
- const normalizedName = displayName?.trim();
524
- if (!normalizedId) {
525
- return normalizedName ?? agentIdOrName;
526
- }
527
- if (!normalizedName || normalizedName === normalizedId) {
528
- return normalizedId;
529
- }
530
- return `${normalizedName} [${normalizedId}]`;
531
- }
532
- function resolveAddressLabel(address, heartbeats) {
533
- const normalized = address.trim();
534
- if (!normalized || normalized === "\uC804\uCCB4" || normalized === "all") {
535
- return address;
536
- }
537
- const direct = heartbeats[normalized];
538
- if (direct?.agent?.trim()) {
539
- return formatAgentLabel(normalized, direct.agent);
540
- }
541
- for (const [agentId, heartbeat] of Object.entries(heartbeats)) {
542
- if (heartbeat.agent?.trim() === normalized) {
543
- return formatAgentLabel(agentId, heartbeat.agent);
544
- }
545
- }
546
- return normalized;
547
- }
548
- function resolveCurrentAgentName(agentId, fallbackAgentName, heartbeats) {
549
- const currentName = heartbeats[agentId]?.agent?.trim();
550
- if (currentName) {
551
- return currentName;
552
- }
553
- for (const heartbeat of Object.values(heartbeats)) {
554
- if (heartbeat.id?.trim() === agentId && heartbeat.agent?.trim()) {
555
- return heartbeat.agent.trim();
556
- }
557
- }
558
- return fallbackAgentName;
559
- }
560
- function refreshAgentIdentity(options, heartbeats) {
561
- const nextAgentName = resolveCurrentAgentName(
562
- options.agentId,
563
- options.agentName,
564
- heartbeats
565
- );
566
- if (nextAgentName !== options.agentName) {
567
- options.agentName = nextAgentName;
568
- persistAgentName(options.stateDir, nextAgentName);
569
- }
570
- return nextAgentName;
571
- }
572
- var HEADLESS_SKIP_PATTERNS = [
573
- /리뷰\s*요청/,
574
- /review[- ]?request/i,
575
- /재리뷰/,
576
- /re-?review/i
577
- ];
578
657
  function shouldSkipInHeadlessMode(fileName, body) {
579
658
  if (process.env.TAP_HEADLESS !== "true") return false;
580
659
  const combined = `${fileName}
581
660
  ${body}`;
582
661
  return HEADLESS_SKIP_PATTERNS.some((p) => p.test(combined));
583
662
  }
584
- function collectCandidates(inboxDir, agentId, agentName) {
663
+ function collectCandidates(inboxDir, agentId, agentName, aliasName) {
585
664
  const entries = readdirSync(inboxDir, { withFileTypes: true }).filter(
586
665
  (entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md")
587
666
  ).map((entry) => {
588
- const filePath = join(inboxDir, entry.name);
667
+ const filePath = join4(inboxDir, entry.name);
589
668
  const stats = statSync(filePath);
590
669
  return { entry, filePath, stats };
591
670
  }).sort((left, right) => left.stats.mtimeMs - right.stats.mtimeMs);
592
671
  const candidates = [];
672
+ let filteredByRecipient = 0;
673
+ let filteredBySelf = 0;
674
+ let filteredByHeadless = 0;
593
675
  for (const item of entries) {
594
- const route = getInboxRoute(item.entry.name);
595
- if (!recipientMatchesAgent(route.recipient, agentId, agentName)) {
676
+ let body;
677
+ try {
678
+ body = readFileSync4(item.filePath, "utf8");
679
+ } catch {
680
+ continue;
681
+ }
682
+ const route = getInboxRoute(item.entry.name, body);
683
+ if (!recipientMatchesAgent(route.recipient, agentId, agentName) && !(aliasName && recipientMatchesAgent(route.recipient, agentId, aliasName))) {
684
+ filteredByRecipient += 1;
596
685
  continue;
597
686
  }
598
- if (isOwnMessageSender(route.sender, agentId, agentName)) {
687
+ if (isOwnMessageSender(route.sender, agentId, agentName) || aliasName && isOwnMessageSender(route.sender, agentId, aliasName)) {
688
+ filteredBySelf += 1;
599
689
  continue;
600
690
  }
601
- const body = readFileSync(item.filePath, "utf8");
602
691
  if (shouldSkipInHeadlessMode(item.entry.name, body)) {
692
+ filteredByHeadless += 1;
603
693
  continue;
604
694
  }
605
695
  candidates.push({
@@ -609,34 +699,58 @@ function collectCandidates(inboxDir, agentId, agentName) {
609
699
  sender: route.sender,
610
700
  recipient: route.recipient,
611
701
  subject: route.subject,
612
- body,
702
+ body: stripBridgeFrontmatter(body),
613
703
  mtimeMs: item.stats.mtimeMs
614
704
  });
615
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
+ });
616
717
  return candidates;
617
718
  }
618
719
  function getPendingCandidates(options, cutoff) {
619
- const inboxDir = join(options.commsDir, "inbox");
620
- if (!existsSync(inboxDir)) {
720
+ const inboxDir = join4(options.commsDir, "inbox");
721
+ if (!existsSync4(inboxDir)) {
621
722
  throw new Error(`Inbox directory not found: ${inboxDir}`);
622
723
  }
623
724
  const heartbeats = loadHeartbeats(options.commsDir);
624
- const agentName = refreshAgentIdentity(options, heartbeats);
725
+ const refreshedName = refreshAgentIdentity(options, heartbeats);
625
726
  const cutoffMs = cutoff.getTime();
626
727
  const candidates = collectCandidates(
627
728
  inboxDir,
628
729
  options.agentId,
629
- agentName
730
+ options.agentName,
731
+ // M205: Also accept messages addressed to the heartbeat-refreshed name
732
+ refreshedName !== options.agentName ? refreshedName : void 0
630
733
  ).filter((candidate) => {
631
734
  if (candidate.mtimeMs < cutoffMs) {
632
735
  return false;
633
736
  }
634
- return !existsSync(
737
+ return !existsSync4(
635
738
  getProcessedMarkerPath(options.stateDir, candidate.markerId)
636
739
  );
637
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
+ });
638
748
  return { heartbeats, candidates };
639
749
  }
750
+
751
+ // ../../scripts/bridge/bridge-format.ts
752
+ import { writeFileSync as writeFileSync3 } from "fs";
753
+ import { join as join5 } from "path";
640
754
  function buildUserInput(candidate, agentName, heartbeats) {
641
755
  const sender = resolveAddressLabel(candidate.sender || "unknown", heartbeats);
642
756
  const recipient = resolveAddressLabel(
@@ -675,7 +789,7 @@ function writeProcessedMarker(stateDir, candidate, dispatchMode, threadId, turnI
675
789
  turnId,
676
790
  markedAt: (/* @__PURE__ */ new Date()).toISOString()
677
791
  };
678
- writeFileSync(
792
+ writeFileSync3(
679
793
  getProcessedMarkerPath(stateDir, candidate.markerId),
680
794
  `${JSON.stringify(payload, null, 2)}
681
795
  `,
@@ -695,98 +809,379 @@ function writeLastDispatch(stateDir, candidate, dispatchMode, threadId, turnId)
695
809
  turnId,
696
810
  dispatchedAt: (/* @__PURE__ */ new Date()).toISOString()
697
811
  };
698
- writeFileSync(
699
- join(stateDir, "last-dispatch.json"),
812
+ writeFileSync3(
813
+ join5(stateDir, "last-dispatch.json"),
700
814
  `${JSON.stringify(payload, null, 2)}
701
815
  `,
702
816
  "utf8"
703
817
  );
704
818
  }
705
- function formatJsonRpcError(error) {
706
- if (!error) {
707
- return "Unknown App Server error";
708
- }
709
- return JSON.stringify(
710
- {
711
- code: error.code,
712
- message: error.message,
713
- data: error.data
714
- },
715
- null,
716
- 2
717
- );
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, "***");
718
835
  }
719
836
  function delay(ms) {
720
837
  return new Promise((resolvePromise) => {
721
838
  setTimeout(resolvePromise, ms);
722
839
  });
723
840
  }
724
- async function waitForTurnCompletion(client, turnId, timeoutMs) {
725
- const deadline = Date.now() + timeoutMs;
726
- let nextRefreshAt = Date.now();
727
- while (Date.now() < deadline) {
728
- if (!client.activeTurnId || client.activeTurnId !== turnId) {
729
- return client.lastTurnStatus;
730
- }
731
- if (Date.now() >= nextRefreshAt) {
732
- await client.refreshCurrentThreadState().catch(() => void 0);
733
- if (!client.activeTurnId || client.activeTurnId !== turnId) {
734
- return client.lastTurnStatus;
735
- }
736
- nextRefreshAt = Date.now() + TURN_COMPLETION_REFRESH_MS;
737
- }
738
- await delay(
739
- Math.min(TURN_COMPLETION_POLL_MS, Math.max(deadline - Date.now(), 0))
740
- );
741
- }
742
- await client.refreshCurrentThreadState().catch(() => void 0);
743
- if (!client.activeTurnId || client.activeTurnId !== turnId) {
744
- return client.lastTurnStatus;
745
- }
746
- throw new Error(`Timed out waiting for turn ${turnId} to complete`);
747
- }
748
- async function maybeBootstrapHeadlessTurn(options, cutoff, client) {
749
- if (process.env.TAP_HEADLESS !== "true" && process.env.TAP_COLD_START_WARMUP !== "true") {
750
- return false;
751
- }
752
- const { candidates } = getPendingCandidates(options, cutoff);
753
- if (candidates.length > 0 || client.activeTurnId || client.lastTurnStatus !== null) {
754
- return false;
755
- }
756
- logStatus("headless cold-start: sending warmup turn");
757
- const turnId = await client.startTurn(HEADLESS_WARMUP_PROMPT);
758
- if (!turnId) {
759
- throw new Error(
760
- "Headless cold-start warmup failed: turn/start did not return a turn id. Run: npx @hua-labs/tap doctor"
761
- );
841
+ function readThreadState(stateDir) {
842
+ const threadPath = join6(stateDir, "thread.json");
843
+ if (!existsSync5(threadPath)) {
844
+ return null;
762
845
  }
763
846
  try {
764
- const status = await waitForTurnCompletion(
765
- client,
766
- turnId,
767
- HEADLESS_WARMUP_TIMEOUT_MS
847
+ const parsed = JSON.parse(
848
+ readFileSync5(threadPath, "utf8")
768
849
  );
769
- if (status !== "completed") {
770
- throw new Error(
771
- `turn ${turnId} finished with status ${status ?? "unknown"}`
772
- );
850
+ if (parsed.threadId) {
851
+ return parsed;
773
852
  }
774
- logStatus(`headless cold-start warmup completed (${status})`);
775
- return true;
776
- } catch (error) {
777
- const reason = error instanceof Error ? error.message : String(error);
778
- throw new Error(
779
- `Headless cold-start warmup failed: ${reason}. Run: npx @hua-labs/tap doctor`
780
- );
853
+ } catch {
854
+ return null;
781
855
  }
856
+ return null;
782
857
  }
783
- function shouldRetrySteerAsStart(error) {
784
- 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") {
785
1145
  return false;
786
1146
  }
787
- const message = error.message.toLowerCase();
788
- 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
+ }
789
1182
  }
1183
+
1184
+ // ../../scripts/bridge/bridge-ws-client.ts
790
1185
  async function readSocketData(data) {
791
1186
  if (typeof data === "string") {
792
1187
  return data;
@@ -804,11 +1199,27 @@ async function readSocketData(data) {
804
1199
  }
805
1200
  return String(data);
806
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;
807
1217
  var AppServerClient = class {
808
1218
  socket = null;
809
1219
  url;
810
1220
  gatewayToken;
811
1221
  logger;
1222
+ clientId = nextAppServerClientId++;
812
1223
  nextId = 1;
813
1224
  pending = /* @__PURE__ */ new Map();
814
1225
  connected = false;
@@ -832,6 +1243,12 @@ var AppServerClient = class {
832
1243
  if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
833
1244
  return;
834
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
+ }
835
1252
  const wsOptions = {};
836
1253
  if (this.gatewayToken) {
837
1254
  wsOptions.protocols = [`${AUTH_SUBPROTOCOL_PREFIX}${this.gatewayToken}`];
@@ -857,7 +1274,11 @@ var AppServerClient = class {
857
1274
  "open",
858
1275
  () => {
859
1276
  this.connected = true;
860
- 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
+ });
861
1282
  resolveOnce();
862
1283
  },
863
1284
  { once: true }
@@ -866,7 +1287,12 @@ var AppServerClient = class {
866
1287
  const error = new Error(
867
1288
  `Failed to connect to App Server at ${this.url}`
868
1289
  );
869
- 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
+ });
870
1296
  rejectOnce(error);
871
1297
  });
872
1298
  this.socket?.addEventListener("close", () => {
@@ -874,7 +1300,10 @@ var AppServerClient = class {
874
1300
  this.initialized = false;
875
1301
  this.activeTurnId = null;
876
1302
  this.turnStartedAt = null;
877
- this.logger("disconnected from app-server");
1303
+ this.logger.warn("disconnected from app-server", {
1304
+ clientId: this.clientId,
1305
+ url: this.url
1306
+ });
878
1307
  this.rejectPending(new Error("App Server connection closed"));
879
1308
  });
880
1309
  this.socket?.addEventListener("message", (event) => {
@@ -911,13 +1340,20 @@ var AppServerClient = class {
911
1340
  });
912
1341
  const resumedThreadId = resumeResponse?.thread?.id ?? explicitThreadId;
913
1342
  await this.refreshThreadState(resumedThreadId);
914
- this.logger(
915
- `resumed thread ${resumedThreadId}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
916
- );
1343
+ this.logger.info("resumed explicit thread", {
1344
+ clientId: this.clientId,
1345
+ threadId: resumedThreadId,
1346
+ activeTurnId: this.activeTurnId
1347
+ });
917
1348
  return resumedThreadId;
918
1349
  } catch (error) {
919
- this.logger(
920
- `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
+ }
921
1357
  );
922
1358
  }
923
1359
  }
@@ -927,9 +1363,12 @@ var AppServerClient = class {
927
1363
  }
928
1364
  if (savedThread?.threadId) {
929
1365
  if (savedThread.cwd && !threadCwdMatches(cwd, savedThread.cwd)) {
930
- this.logger(
931
- `saved thread ${savedThread.threadId} cwd ${savedThread.cwd} does not match ${cwd}; skipping saved thread`
932
- );
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
+ });
933
1372
  } else {
934
1373
  try {
935
1374
  const resumeResponse = await this.request("thread/resume", {
@@ -939,23 +1378,33 @@ var AppServerClient = class {
939
1378
  const resumedThreadId = resumeResponse?.thread?.id ?? savedThread.threadId;
940
1379
  await this.refreshThreadState(resumedThreadId);
941
1380
  if (!threadCwdMatches(cwd, this.currentThreadCwd)) {
942
- this.logger(
943
- `saved thread ${resumedThreadId} cwd ${this.currentThreadCwd ?? "unknown"} does not match ${cwd}; starting a fresh thread`
944
- );
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
+ });
945
1387
  this.threadId = null;
946
1388
  this.currentThreadCwd = null;
947
1389
  this.activeTurnId = null;
948
1390
  this.turnStartedAt = null;
949
1391
  this.lastTurnStatus = null;
950
1392
  } else {
951
- this.logger(
952
- `resumed saved thread ${resumedThreadId}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
953
- );
1393
+ this.logger.info("resumed saved thread", {
1394
+ clientId: this.clientId,
1395
+ threadId: resumedThreadId,
1396
+ activeTurnId: this.activeTurnId
1397
+ });
954
1398
  return resumedThreadId;
955
1399
  }
956
1400
  } catch (error) {
957
- this.logger(
958
- `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
+ }
959
1408
  );
960
1409
  }
961
1410
  }
@@ -975,7 +1424,12 @@ var AppServerClient = class {
975
1424
  this.currentThreadCwd = this.currentThreadCwd ?? cwd;
976
1425
  this.activeTurnId = null;
977
1426
  this.lastTurnStatus = null;
978
- 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
+ });
979
1433
  return startedThreadId;
980
1434
  }
981
1435
  async findLoadedThread(cwd) {
@@ -1013,14 +1467,21 @@ var AppServerClient = class {
1013
1467
  const chosen = chooseLoadedThreadForCwd(cwd, threads);
1014
1468
  if (!chosen) {
1015
1469
  if (threads.length > 0) {
1016
- 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
+ });
1017
1475
  }
1018
1476
  return null;
1019
1477
  }
1020
1478
  this.syncThreadStateFromThread(chosen.thread);
1021
- this.logger(
1022
- `attached to loaded thread ${chosen.id}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
1023
- );
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
+ });
1024
1485
  return chosen.id;
1025
1486
  }
1026
1487
  async startTurn(inputText) {
@@ -1059,7 +1520,18 @@ var AppServerClient = class {
1059
1520
  return turnId;
1060
1521
  }
1061
1522
  isBusy() {
1062
- 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;
1063
1535
  }
1064
1536
  async refreshCurrentThreadState() {
1065
1537
  if (!this.threadId) {
@@ -1093,12 +1565,33 @@ var AppServerClient = class {
1093
1565
  this.currentThreadCwd = typeof thread?.cwd === "string" ? thread.cwd : null;
1094
1566
  let activeTurnId = null;
1095
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
+ }
1096
1578
  const turns = Array.isArray(thread?.turns) ? thread.turns : [];
1097
1579
  for (const turn of turns) {
1098
1580
  if (typeof turn?.status === "string") {
1099
1581
  lastTurnStatus = turn.status;
1100
1582
  }
1101
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
+ }
1102
1595
  activeTurnId = turn.id;
1103
1596
  }
1104
1597
  }
@@ -1121,7 +1614,12 @@ var AppServerClient = class {
1121
1614
  this.pending.delete(message.id);
1122
1615
  if (message.error) {
1123
1616
  const errorText = formatJsonRpcError(message.error);
1124
- 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
+ });
1125
1623
  pending.reject(new Error(`${pending.method} failed: ${errorText}`));
1126
1624
  return;
1127
1625
  }
@@ -1136,6 +1634,10 @@ var AppServerClient = class {
1136
1634
  }
1137
1635
  this.lastNotificationMethod = message.method;
1138
1636
  this.lastNotificationAt = (/* @__PURE__ */ new Date()).toISOString();
1637
+ this.logger.debug("received app-server notification", {
1638
+ clientId: this.clientId,
1639
+ method: message.method
1640
+ });
1139
1641
  this.handleNotification(message.method, message.params);
1140
1642
  }
1141
1643
  handleNotification(method, params) {
@@ -1147,18 +1649,28 @@ var AppServerClient = class {
1147
1649
  if (typeof params?.thread?.cwd === "string") {
1148
1650
  this.currentThreadCwd = params.thread.cwd;
1149
1651
  }
1150
- 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
+ });
1151
1657
  break;
1152
1658
  case "thread/status/changed":
1153
- this.logger(
1154
- `thread status changed (${params?.thread?.status?.type ?? params?.status?.type ?? "unknown"})`
1155
- );
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
+ });
1156
1664
  break;
1157
1665
  case "turn/started":
1158
1666
  if (params?.turn?.id) {
1159
1667
  this.activeTurnId = params.turn.id;
1160
1668
  this.turnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
1161
- 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
+ });
1162
1674
  }
1163
1675
  break;
1164
1676
  case "turn/completed": {
@@ -1167,15 +1679,22 @@ var AppServerClient = class {
1167
1679
  this.activeTurnId = null;
1168
1680
  this.turnStartedAt = null;
1169
1681
  const elapsedMs = prevTurnStartedAt ? Date.now() - new Date(prevTurnStartedAt).getTime() : null;
1170
- const elapsedSuffix = elapsedMs !== null ? ` \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed` : "";
1171
- this.logger(
1172
- `turn completed (${this.lastTurnStatus ?? "unknown"})${elapsedSuffix}`
1173
- );
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
+ });
1174
1688
  break;
1175
1689
  }
1176
1690
  case "error":
1177
- this.lastError = JSON.stringify(params ?? {}, null, 2);
1178
- 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
+ });
1179
1698
  break;
1180
1699
  default:
1181
1700
  break;
@@ -1183,277 +1702,135 @@ var AppServerClient = class {
1183
1702
  }
1184
1703
  request(method, params) {
1185
1704
  if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
1186
- throw new Error(`Cannot call ${method}; App Server socket is not open`);
1187
- }
1188
- const id = this.nextId;
1189
- this.nextId += 1;
1190
- const request = {
1191
- jsonrpc: "2.0",
1192
- id,
1193
- method,
1194
- params
1195
- };
1196
- return new Promise((resolvePromise, rejectPromise) => {
1197
- this.pending.set(id, {
1198
- resolve: resolvePromise,
1199
- reject: rejectPromise,
1200
- method
1201
- });
1202
- this.socket?.send(JSON.stringify(request));
1203
- });
1204
- }
1205
- rejectPending(error) {
1206
- for (const pending of this.pending.values()) {
1207
- pending.reject(error);
1208
- }
1209
- this.pending.clear();
1210
- }
1211
- };
1212
- var heartbeatCount = 0;
1213
- function writeHeartbeat(options, client, health) {
1214
- if (client?.threadId) {
1215
- const savedThread = readThreadState(options.stateDir);
1216
- persistThreadState(
1217
- options.stateDir,
1218
- client.threadId,
1219
- options.appServerUrl,
1220
- options.ephemeral,
1221
- client.currentThreadCwd ?? savedThread?.cwd ?? null
1222
- );
1223
- }
1224
- const payload = {
1225
- pid: process.pid,
1226
- agent: options.agentName,
1227
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1228
- pollSeconds: options.pollSeconds,
1229
- appServerUrl: options.appServerUrl,
1230
- connected: client?.connected ?? false,
1231
- initialized: client?.initialized ?? false,
1232
- threadId: client?.threadId ?? null,
1233
- threadCwd: client?.currentThreadCwd ?? null,
1234
- activeTurnId: client?.activeTurnId ?? null,
1235
- turnStartedAt: client?.turnStartedAt ?? null,
1236
- lastTurnStatus: client?.lastTurnStatus ?? null,
1237
- lastNotificationMethod: client?.lastNotificationMethod ?? null,
1238
- lastNotificationAt: client?.lastNotificationAt ?? null,
1239
- lastError: sanitizeErrorForPersistence(client?.lastError ?? null),
1240
- lastSuccessfulAppServerAt: client?.lastSuccessfulAppServerAt ?? null,
1241
- lastSuccessfulAppServerMethod: client?.lastSuccessfulAppServerMethod ?? null,
1242
- consecutiveFailureCount: health.consecutiveFailureCount,
1243
- busyMode: options.busyMode
1244
- };
1245
- writeFileSync(
1246
- join(options.stateDir, "heartbeat.json"),
1247
- `${JSON.stringify(payload, null, 2)}
1248
- `,
1249
- "utf8"
1250
- );
1251
- heartbeatCount += 1;
1252
- if (heartbeatCount % 5 === 0) {
1253
- logStatus(
1254
- `heartbeat: connected=${payload.connected}, thread=${payload.threadId ?? "null"}, turns=${payload.activeTurnId ? "active" : "0"}`
1255
- );
1256
- }
1257
- const status = client?.connected ? "active" : "idle";
1258
- updateCommsHeartbeat(options, status);
1259
- }
1260
- var COMMS_HEARTBEAT_LOCK_TIMEOUT_MS = 2e3;
1261
- var COMMS_LOCK_STALE_AGE_MS = 1e4;
1262
- function acquireCommsLock(lockPath) {
1263
- const deadline = Date.now() + COMMS_HEARTBEAT_LOCK_TIMEOUT_MS;
1264
- while (Date.now() < deadline) {
1265
- try {
1266
- writeFileSync(lockPath, String(process.pid), { flag: "wx" });
1267
- return true;
1268
- } catch {
1269
- try {
1270
- const lockAge = Date.now() - statSync(lockPath).mtimeMs;
1271
- if (lockAge > COMMS_LOCK_STALE_AGE_MS) {
1272
- unlinkSync(lockPath);
1273
- try {
1274
- writeFileSync(lockPath, String(process.pid), { flag: "wx" });
1275
- return true;
1276
- } catch {
1277
- }
1278
- }
1279
- } catch {
1280
- }
1281
- const start = Date.now();
1282
- while (Date.now() - start < 50) {
1283
- }
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);
1284
1727
  }
1728
+ this.pending.clear();
1285
1729
  }
1286
- 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
+ });
1287
1740
  }
1288
- function releaseCommsLock(lockPath) {
1741
+ function readHeartbeatState(stateDir) {
1742
+ const heartbeatPath = join7(stateDir, "heartbeat.json");
1743
+ if (!existsSync6(heartbeatPath)) {
1744
+ return null;
1745
+ }
1289
1746
  try {
1290
- unlinkSync(lockPath);
1747
+ return JSON.parse(readFileSync6(heartbeatPath, "utf8"));
1291
1748
  } catch {
1749
+ return null;
1292
1750
  }
1293
1751
  }
1294
- function updateCommsHeartbeat(options, status) {
1295
- const heartbeatsPath = join(options.commsDir, "heartbeats.json");
1296
- const lockPath = join(options.commsDir, ".heartbeats.lock");
1297
- if (!acquireCommsLock(lockPath)) {
1298
- return;
1752
+ function parseUpdatedAt(value) {
1753
+ if (!value) {
1754
+ return 0;
1299
1755
  }
1300
- try {
1301
- let store = {};
1302
- try {
1303
- store = JSON.parse(readFileSync(heartbeatsPath, "utf-8"));
1304
- } catch {
1305
- }
1306
- const key = options.agentId;
1307
- const existing = store[key];
1308
- store[key] = {
1309
- id: options.agentId,
1310
- agent: options.agentName,
1311
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1312
- lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
1313
- joinedAt: existing?.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1314
- 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
1315
1798
  };
1316
- const tmpPath = heartbeatsPath + ".tmp." + process.pid;
1317
- writeFileSync(tmpPath, JSON.stringify(store, null, 2), "utf-8");
1318
- renameSync(tmpPath, heartbeatsPath);
1319
- } catch {
1320
- } finally {
1321
- releaseCommsLock(lockPath);
1799
+ } else if (parseUpdatedAt(heartbeat?.updatedAt) > parseUpdatedAt(savedThread.updatedAt)) {
1800
+ preferred = heartbeatBackedThread;
1322
1801
  }
1802
+ return preferred;
1323
1803
  }
1324
- async function dispatchCandidate(client, options, candidate, heartbeats) {
1325
- const input = buildUserInput(candidate, options.agentName, heartbeats);
1326
- logStatus(
1327
- `dispatching from ${candidate.sender || "unknown"}: ${candidate.subject || "(none)"}`
1328
- );
1329
- if (client.isBusy()) {
1330
- if (options.busyMode !== "steer") {
1331
- return false;
1332
- }
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)) {
1333
1811
  try {
1334
- const turnId2 = await client.steerTurn(input);
1335
- writeProcessedMarker(
1336
- options.stateDir,
1337
- candidate,
1338
- "steer",
1339
- client.threadId,
1340
- turnId2
1341
- );
1342
- writeLastDispatch(
1343
- options.stateDir,
1344
- candidate,
1345
- "steer",
1346
- client.threadId,
1347
- turnId2
1348
- );
1349
- logStatus(`steered active turn with ${candidate.fileName}`);
1350
- return true;
1351
- } catch (error) {
1352
- await client.refreshCurrentThreadState().catch(() => void 0);
1353
- if (!client.isBusy()) {
1354
- return dispatchCandidate(client, options, candidate, heartbeats);
1355
- }
1356
- if (shouldRetrySteerAsStart(error)) {
1357
- client.activeTurnId = null;
1358
- client.turnStartedAt = null;
1359
- logStatus(
1360
- `steer fallback -> start for ${candidate.fileName} (${String(error)})`
1361
- );
1362
- 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;
1363
1818
  }
1364
- throw error;
1365
- }
1366
- }
1367
- const turnId = await client.startTurn(input);
1368
- writeProcessedMarker(
1369
- options.stateDir,
1370
- candidate,
1371
- "start",
1372
- client.threadId,
1373
- turnId
1374
- );
1375
- writeLastDispatch(
1376
- options.stateDir,
1377
- candidate,
1378
- "start",
1379
- client.threadId,
1380
- turnId
1381
- );
1382
- logStatus(`dispatched ${candidate.fileName} to thread ${client.threadId}`);
1383
- return true;
1384
- }
1385
- async function runScan(options, cutoff, client) {
1386
- const { heartbeats, candidates } = getPendingCandidates(options, cutoff);
1387
- for (const candidate of candidates) {
1388
- if (options.dryRun) {
1389
- logStatus(`dry-run candidate ${candidate.fileName}`);
1390
- continue;
1391
- }
1392
- if (!client) {
1393
- throw new Error("App Server client is not available");
1394
- }
1395
- const dispatched = await dispatchCandidate(
1396
- client,
1397
- options,
1398
- candidate,
1399
- heartbeats
1400
- );
1401
- if (!dispatched && options.busyMode === "wait") {
1402
- return false;
1819
+ } catch {
1403
1820
  }
1404
- return true;
1405
1821
  }
1406
- return false;
1407
- }
1408
- async function waitForTurnDrain(options, client, health) {
1409
- const deadline = Date.now() + options.waitAfterDispatchSeconds * 1e3;
1410
- while (Date.now() < deadline) {
1411
- writeHeartbeat(options, client, health);
1412
- if (!client.activeTurnId) {
1413
- return;
1414
- }
1415
- await delay(1e3);
1822
+ if (lookbackCutoff) {
1823
+ return lookbackCutoff;
1416
1824
  }
1417
- }
1418
- function buildOptions(argv) {
1419
- const parsed = parseArgs(argv);
1420
- const repoRoot = resolveRepoRoot(parsed.repoRoot);
1421
- const commsDir = resolveCommsDir(repoRoot, parsed.commsDir);
1422
- const preferredAgentName = resolvePreferredAgentName(parsed.agentName);
1423
- const stateDir = resolveStateDir(
1424
- repoRoot,
1425
- parsed.stateDir,
1426
- preferredAgentName
1427
- );
1428
- const agentName = resolveAgentName(preferredAgentName, stateDir);
1429
- const agentId = resolveAgentId(agentName);
1430
- persistAgentName(stateDir, agentName);
1431
- const gatewayTokenFile = parsed.gatewayTokenFile?.trim() || process.env.TAP_GATEWAY_TOKEN_FILE?.trim() || null;
1432
- const appServerUrl = parsed.appServerUrl?.trim() || process.env.CODEX_APP_SERVER_URL || DEFAULT_APP_SERVER_URL;
1433
- return {
1434
- repoRoot,
1435
- commsDir,
1436
- agentId,
1437
- stateDir,
1438
- agentName,
1439
- pollSeconds: parsed.pollSeconds ?? 5,
1440
- reconnectSeconds: parsed.reconnectSeconds ?? 5,
1441
- messageLookbackMinutes: parsed.messageLookbackMinutes ?? 10,
1442
- processExistingMessages: parsed.processExistingMessages,
1443
- dryRun: parsed.dryRun,
1444
- runOnce: parsed.runOnce,
1445
- waitAfterDispatchSeconds: parsed.waitAfterDispatchSeconds ?? 0,
1446
- appServerUrl,
1447
- connectAppServerUrl: appServerUrl,
1448
- gatewayToken: gatewayTokenFile ? readGatewayTokenFile(gatewayTokenFile) : null,
1449
- gatewayTokenFile,
1450
- busyMode: parsed.busyMode ?? "steer",
1451
- threadId: parsed.threadId?.trim() || null,
1452
- ephemeral: parsed.ephemeral
1453
- };
1825
+ const cutoff = /* @__PURE__ */ new Date();
1826
+ writeFileSync5(cutoffPath, `${cutoff.toISOString()}
1827
+ `, "utf8");
1828
+ return cutoff;
1454
1829
  }
1455
1830
  async function main() {
1456
1831
  const options = buildOptions(process.argv.slice(2));
1832
+ configureBridgeLogging(options.logLevel);
1833
+ const logger = createBridgeLogger("bridge");
1457
1834
  const cutoff = getGeneralInboxCutoff(
1458
1835
  options.stateDir,
1459
1836
  options.messageLookbackMinutes,
@@ -1463,28 +1840,20 @@ async function main() {
1463
1840
  options.stateDir,
1464
1841
  options.appServerUrl
1465
1842
  );
1466
- logStatus("codex app-server bridge ready");
1467
- console.log(` repo: ${options.repoRoot}`);
1468
- console.log(` comms: ${options.commsDir}`);
1469
- console.log(` agent: ${options.agentName}`);
1470
- console.log(` state: ${options.stateDir}`);
1471
- console.log(` app-server: ${options.appServerUrl}`);
1472
- console.log(` busy-mode: ${options.busyMode}`);
1473
- if (options.waitAfterDispatchSeconds > 0) {
1474
- console.log(
1475
- ` wait: ${options.waitAfterDispatchSeconds}s after dispatch`
1476
- );
1477
- }
1478
- console.log(
1479
- ` lookback: ${options.processExistingMessages ? "existing messages" : `${options.messageLookbackMinutes} minute(s)`}`
1480
- );
1481
- if (options.threadId || initialSavedThread?.threadId) {
1482
- console.log(
1483
- ` thread: ${options.threadId ?? initialSavedThread?.threadId}`
1484
- );
1485
- }
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
+ });
1486
1855
  if (options.dryRun) {
1487
- logStatus("dry-run mode enabled");
1856
+ logger.info("dry-run mode enabled");
1488
1857
  }
1489
1858
  let client = null;
1490
1859
  const health = {
@@ -1496,7 +1865,7 @@ async function main() {
1496
1865
  if (!client || !client.connected) {
1497
1866
  client = new AppServerClient(
1498
1867
  options.connectAppServerUrl,
1499
- logStatus,
1868
+ createBridgeLogger("app-server"),
1500
1869
  options.gatewayToken
1501
1870
  );
1502
1871
  await client.connect();
@@ -1504,6 +1873,10 @@ async function main() {
1504
1873
  options.stateDir,
1505
1874
  options.appServerUrl
1506
1875
  );
1876
+ logger.debug("resolved resumable thread state", {
1877
+ savedThreadId: savedThread?.threadId,
1878
+ savedThreadCwd: savedThread?.cwd ?? null
1879
+ });
1507
1880
  const threadId = await client.ensureThread(
1508
1881
  options.threadId,
1509
1882
  savedThread,
@@ -1528,8 +1901,14 @@ async function main() {
1528
1901
  }
1529
1902
  }
1530
1903
  }
1531
- const dispatched = await runScan(options, cutoff, client);
1532
- 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) {
1533
1912
  await waitForTurnDrain(options, client, health);
1534
1913
  }
1535
1914
  health.consecutiveFailureCount = 0;
@@ -1537,22 +1916,28 @@ async function main() {
1537
1916
  if (options.runOnce) {
1538
1917
  break;
1539
1918
  }
1540
- await delay(options.pollSeconds * 1e3);
1919
+ await delay2(options.pollSeconds * 1e3);
1541
1920
  } catch (error) {
1542
1921
  const message = error instanceof Error ? error.message : String(error);
1543
- logStatus(`bridge error: ${message}`);
1922
+ logger.error("bridge error", {
1923
+ error: sanitizeErrorForPersistence(message)
1924
+ });
1544
1925
  if (client) {
1545
- client.lastError = message;
1926
+ client.lastError = sanitizeErrorForPersistence(message);
1546
1927
  }
1547
1928
  health.consecutiveFailureCount += 1;
1548
1929
  writeHeartbeat(options, client, health);
1549
1930
  if (options.runOnce) {
1550
- throw error;
1931
+ const sanitized = sanitizeErrorForPersistence(message);
1932
+ throw new Error(sanitized ?? message);
1551
1933
  }
1552
1934
  client?.disconnect().catch(() => void 0);
1553
1935
  client = null;
1554
- logStatus(`reconnecting in ${options.reconnectSeconds}s...`);
1555
- 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);
1556
1941
  }
1557
1942
  }
1558
1943
  await client?.disconnect();
@@ -1560,24 +1945,32 @@ async function main() {
1560
1945
  function isDirectExecution() {
1561
1946
  const entry = process.argv[1];
1562
1947
  if (!entry) return false;
1563
- 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;
1564
1958
  }
1565
- if (isDirectExecution()) {
1959
+ if (isDirectExecution2()) {
1566
1960
  main().catch((error) => {
1567
- console.error(
1568
- error instanceof Error ? error.stack ?? error.message : String(error)
1569
- );
1961
+ const raw = error instanceof Error ? error.stack ?? error.message : String(error);
1962
+ console.error(sanitizeErrorForPersistence(raw));
1570
1963
  process.exitCode = 1;
1571
1964
  });
1572
1965
  }
1573
1966
 
1574
1967
  // src/bridges/codex-app-server-bridge.ts
1575
- function isDirectExecution2() {
1968
+ function isDirectExecution3() {
1576
1969
  const entry = process.argv[1];
1577
1970
  if (!entry) return false;
1578
- return import.meta.url === pathToFileURL2(resolve2(entry)).href;
1971
+ return import.meta.url === pathToFileURL3(resolve6(entry)).href;
1579
1972
  }
1580
- if (isDirectExecution2()) {
1973
+ if (isDirectExecution3()) {
1581
1974
  main().catch((error) => {
1582
1975
  console.error(
1583
1976
  error instanceof Error ? error.stack ?? error.message : String(error)
@@ -1586,19 +1979,77 @@ if (isDirectExecution2()) {
1586
1979
  });
1587
1980
  }
1588
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,
1589
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,
1590
1998
  buildOptions,
1591
1999
  buildUserInput,
2000
+ canonicalize,
1592
2001
  chooseLoadedThreadForCwd,
2002
+ collectCandidates,
2003
+ dispatchCandidate,
2004
+ formatAgentLabel,
2005
+ formatJsonRpcError,
2006
+ getGeneralInboxCutoff,
2007
+ getInboxRoute,
2008
+ getInboxRouteFromFilename,
2009
+ getPendingCandidates,
2010
+ getProcessedMarkerPath,
2011
+ isDirectExecution,
1593
2012
  isOwnMessageSender,
2013
+ isTurnStale,
2014
+ isTurnStuckOnApproval,
2015
+ loadHeartbeats,
1594
2016
  loadResumableThreadState,
1595
2017
  main,
1596
2018
  maybeBootstrapHeadlessTurn,
2019
+ normalizeAgentToken,
2020
+ normalizeThreadCwd,
2021
+ parseArgs,
2022
+ parseBridgeFrontmatter,
2023
+ persistAgentName,
2024
+ persistThreadState,
2025
+ readGatewayTokenFile,
2026
+ readHeartbeatState,
2027
+ readSocketData,
2028
+ readThreadState,
1597
2029
  recipientMatchesAgent,
2030
+ refreshAgentIdentity,
2031
+ releaseCommsLock,
1598
2032
  resolveAddressLabel,
1599
2033
  resolveAgentId,
2034
+ resolveAgentName,
2035
+ resolveCommsDir,
1600
2036
  resolveCurrentAgentName,
2037
+ resolvePreferredAgentName,
2038
+ resolveRepoRoot,
2039
+ resolveStateDir,
2040
+ resolveTapConfigPath,
2041
+ runScan,
2042
+ sanitizeErrorForPersistence,
2043
+ sanitizeStateSegment,
2044
+ shouldRetrySteerAsStart,
2045
+ shouldSkipInHeadlessMode,
2046
+ stripBridgeFrontmatter,
1601
2047
  threadCwdMatches,
1602
- waitForTurnCompletion
2048
+ updateCommsHeartbeat,
2049
+ waitForTurnCompletion,
2050
+ waitForTurnDrain,
2051
+ writeHeartbeat,
2052
+ writeLastDispatch,
2053
+ writeProcessedMarker
1603
2054
  };
1604
2055
  //# sourceMappingURL=codex-app-server-bridge.mjs.map