@hua-labs/tap 0.4.1 → 0.5.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,8 +1,21 @@
1
1
  // src/bridges/codex-app-server-bridge.ts
2
- import { pathToFileURL as pathToFileURL3 } from "url";
3
- import { resolve as resolve6 } from "path";
2
+ import { pathToFileURL as pathToFileURL2 } from "url";
3
+ import { resolve as resolve2 } from "path";
4
4
 
5
- // ../../scripts/bridge/bridge-types.ts
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";
6
19
  var DEFAULT_AGENT = String.fromCharCode(50728);
7
20
  var DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
8
21
  var AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
@@ -20,61 +33,6 @@ var HEADLESS_WARMUP_PROMPT = [
20
33
  var HEADLESS_WARMUP_TIMEOUT_MS = 3e4;
21
34
  var TURN_COMPLETION_POLL_MS = 250;
22
35
  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
- }
78
36
  function normalizeThreadCwd(cwd) {
79
37
  return resolve(cwd).replace(/\\/g, "/").toLowerCase();
80
38
  }
@@ -101,170 +59,6 @@ function chooseLoadedThreadForCwd(cwd, threads) {
101
59
  });
102
60
  return matching[0] ?? null;
103
61
  }
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
- }
268
62
  function printHelp() {
269
63
  console.log(`Codex App Server bridge
270
64
 
@@ -286,7 +80,6 @@ Options:
286
80
  --app-server-url=<ws-url>
287
81
  --gateway-token-file=<path>
288
82
  --busy-mode=wait|steer
289
- --log-level=debug|info|warn|error
290
83
  --thread-id=<id>
291
84
  --ephemeral
292
85
  --help
@@ -445,42 +238,50 @@ function parseArgs(argv) {
445
238
  }
446
239
  continue;
447
240
  }
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
- }
459
241
  throw new Error(`Unknown argument: ${flag}`);
460
242
  }
461
243
  return parsed;
462
244
  }
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
+ }
463
268
  function resolveRepoRoot(explicit) {
464
269
  if (explicit) {
465
- return resolve3(explicit);
270
+ return resolve(explicit);
466
271
  }
467
272
  return process.cwd();
468
273
  }
469
- function resolveTapConfigPath(repoRoot, input) {
470
- const converted = normalizeTapPath(input);
471
- return isAbsolute2(converted) ? resolve3(converted) : resolve3(repoRoot, converted);
472
- }
473
274
  function resolveCommsDir(repoRoot, explicit) {
474
275
  if (explicit) {
475
- return resolve3(normalizeTapPath(explicit));
276
+ return resolve(convertTapPath(explicit));
476
277
  }
477
- const tapConfigPath = join3(repoRoot, ".tap-config");
478
- if (!existsSync3(tapConfigPath)) {
278
+ const tapConfigPath = join(repoRoot, ".tap-config");
279
+ if (!existsSync(tapConfigPath)) {
479
280
  throw new Error(
480
281
  "Unable to resolve comms directory. Pass --comms-dir explicitly."
481
282
  );
482
283
  }
483
- const configText = readFileSync3(tapConfigPath, "utf8");
284
+ const configText = readFileSync(tapConfigPath, "utf8");
484
285
  const match = configText.match(/^TAP_COMMS_DIR="?(.*?)"?$/m);
485
286
  if (!match?.[1]) {
486
287
  throw new Error(
@@ -501,195 +302,352 @@ function resolvePreferredAgentName(requested) {
501
302
  }
502
303
  return null;
503
304
  }
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
+ }
504
315
  function sanitizeStateSegment(agentName) {
505
316
  const normalized = agentName.trim().replace(/[<>:"/\\|?*\x00-\x1f]/g, "-").replace(/[. ]+$/g, "");
506
317
  return normalized || "agent";
507
318
  }
508
319
  function buildDefaultStateDir(repoRoot, preferredAgentName) {
509
320
  const suffix = preferredAgentName?.trim() ? `-${sanitizeStateSegment(preferredAgentName)}` : "";
510
- return resolve3(join3(repoRoot, ".tmp", `codex-app-server-bridge${suffix}`));
321
+ return resolve(join(repoRoot, ".tmp", `codex-app-server-bridge${suffix}`));
511
322
  }
512
323
  function resolveStateDir(repoRoot, explicit, preferredAgentName) {
513
- const root = explicit ? resolve3(explicit) : buildDefaultStateDir(repoRoot, preferredAgentName);
324
+ const root = explicit ? resolve(explicit) : buildDefaultStateDir(repoRoot, preferredAgentName);
514
325
  ensureDir(root);
515
- ensureDir(join3(root, "processed"));
516
- ensureDir(join3(root, "logs"));
326
+ ensureDir(join(root, "processed"));
327
+ ensureDir(join(root, "logs"));
517
328
  return root;
518
329
  }
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
+ }
519
351
  function readGatewayTokenFile(tokenFile) {
520
- const token = readFileSync3(tokenFile, "utf8").trim();
352
+ const token = readFileSync(tokenFile, "utf8").trim();
521
353
  if (!token) {
522
354
  throw new Error(`Gateway token file is empty: ${tokenFile}`);
523
355
  }
524
356
  return token;
525
357
  }
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
- };
563
- }
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];
358
+ function resolveTapConfigPath(repoRoot, input) {
359
+ const converted = convertTapPath(input);
360
+ return isAbsolute(converted) ? resolve(converted) : resolve(repoRoot, converted);
583
361
  }
584
- function formatValue(value) {
585
- if (typeof value === "string") {
586
- return JSON.stringify(value);
362
+ function readThreadState(stateDir) {
363
+ const threadPath = join(stateDir, "thread.json");
364
+ if (!existsSync(threadPath)) {
365
+ return null;
587
366
  }
588
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
589
- return String(value);
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;
590
376
  }
591
- if (value === null) {
592
- return "null";
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;
393
+ }
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;
593
404
  }
594
- return JSON.stringify(value);
405
+ return isAbsolute(normalized) || /^[A-Za-z]:[\\/]/.test(normalized) || normalized.startsWith("\\\\");
595
406
  }
596
- function formatContext(context) {
597
- if (!context) {
598
- return "";
407
+ function loadResumableThreadState(stateDir, fallbackAppServerUrl) {
408
+ const savedThread = readThreadState(stateDir);
409
+ const heartbeat = readHeartbeatState(stateDir);
410
+ const heartbeatThreadId = heartbeat?.threadId?.trim();
411
+ if (!heartbeatThreadId) {
412
+ return savedThread;
413
+ }
414
+ if (!appServerUrlMatches(fallbackAppServerUrl, heartbeat?.appServerUrl)) {
415
+ return savedThread;
416
+ }
417
+ if (!hasValidHeartbeatThreadCwd(heartbeat?.threadCwd)) {
418
+ return savedThread;
419
+ }
420
+ const heartbeatBackedThread = {
421
+ threadId: heartbeatThreadId,
422
+ updatedAt: heartbeat?.updatedAt ?? savedThread?.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
423
+ appServerUrl: heartbeat?.appServerUrl || savedThread?.appServerUrl || fallbackAppServerUrl,
424
+ ephemeral: savedThread?.ephemeral ?? false,
425
+ cwd: heartbeat?.threadCwd ?? (savedThread?.threadId === heartbeatThreadId ? savedThread.cwd ?? null : null)
426
+ };
427
+ let preferred = savedThread;
428
+ if (!savedThread?.threadId) {
429
+ preferred = heartbeatBackedThread;
430
+ } else if (savedThread.threadId === heartbeatThreadId) {
431
+ preferred = {
432
+ ...savedThread,
433
+ updatedAt: heartbeatBackedThread.updatedAt ?? savedThread.updatedAt,
434
+ appServerUrl: heartbeatBackedThread.appServerUrl,
435
+ cwd: heartbeatBackedThread.cwd ?? savedThread.cwd ?? null
436
+ };
437
+ } else if (parseUpdatedAt(heartbeat?.updatedAt) > parseUpdatedAt(savedThread.updatedAt)) {
438
+ preferred = heartbeatBackedThread;
599
439
  }
600
- const entries = Object.entries(context).filter(
601
- ([, value]) => value !== void 0
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"
602
455
  );
603
- if (entries.length === 0) {
604
- return "";
456
+ }
457
+ function getGeneralInboxCutoff(stateDir, lookbackMinutes, processExistingMessages) {
458
+ if (processExistingMessages) {
459
+ return /* @__PURE__ */ new Date(0);
460
+ }
461
+ if (lookbackMinutes > 0) {
462
+ return new Date(Date.now() - lookbackMinutes * 6e4);
463
+ }
464
+ const cutoffPath = join(stateDir, "general-inbox-cutoff.txt");
465
+ if (existsSync(cutoffPath)) {
466
+ try {
467
+ return new Date(readFileSync(cutoffPath, "utf8").trim());
468
+ } catch {
469
+ return /* @__PURE__ */ new Date();
470
+ }
471
+ }
472
+ const cutoff = /* @__PURE__ */ new Date();
473
+ writeFileSync(cutoffPath, `${cutoff.toISOString()}
474
+ `, "utf8");
475
+ return cutoff;
476
+ }
477
+ function recipientMatchesAgent(recipient, agentId, agentName) {
478
+ const normalizedRecipient = recipient.trim();
479
+ if (!normalizedRecipient) {
480
+ return false;
605
481
  }
606
- return ` ${entries.map(([key, value]) => `${key}=${formatValue(value)}`).join(" ")}`;
482
+ const aliases = /* @__PURE__ */ new Set([
483
+ agentId.trim(),
484
+ agentId.trim().replace(/-/g, "_"),
485
+ agentId.trim().replace(/_/g, "-"),
486
+ agentName.trim(),
487
+ agentName.trim().replace(/-/g, "_"),
488
+ agentName.trim().replace(/_/g, "-")
489
+ ]);
490
+ return normalizedRecipient === "\uC804\uCCB4" || normalizedRecipient === "all" || aliases.has(normalizedRecipient);
607
491
  }
608
- function logBridge(level, message, context) {
609
- if (!shouldLog(level)) {
610
- return;
492
+ function isOwnMessageSender(sender, agentId, agentName) {
493
+ const normalizedSender = sender.trim();
494
+ if (!normalizedSender) {
495
+ return false;
611
496
  }
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;
497
+ const aliases = /* @__PURE__ */ new Set([
498
+ agentId.trim(),
499
+ agentId.trim().replace(/-/g, "_"),
500
+ agentId.trim().replace(/_/g, "-"),
501
+ agentName.trim(),
502
+ agentName.trim().replace(/-/g, "_"),
503
+ agentName.trim().replace(/_/g, "-")
504
+ ]);
505
+ return aliases.has(normalizedSender);
506
+ }
507
+ function decodeRouteSegment(value) {
508
+ try {
509
+ return decodeURIComponent(value);
510
+ } catch {
511
+ return value;
617
512
  }
618
- if (level === "warn") {
619
- console.warn(line);
620
- return;
513
+ }
514
+ function parseInboxFrontmatter(body) {
515
+ if (!body) {
516
+ return null;
621
517
  }
622
- console.log(line);
518
+ const frontmatter = body.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
519
+ if (!frontmatter) {
520
+ return null;
521
+ }
522
+ let sender = "";
523
+ let recipient = "";
524
+ let subject = "";
525
+ for (const line of frontmatter[1].split(/\r?\n/)) {
526
+ const separator = line.indexOf(":");
527
+ if (separator <= 0) continue;
528
+ const key = line.slice(0, separator).trim();
529
+ const value = line.slice(separator + 1).trim();
530
+ if (key === "from") sender = value;
531
+ if (key === "to") recipient = value;
532
+ if (key === "subject") subject = value;
533
+ }
534
+ if (!sender || !recipient || !subject) {
535
+ return null;
536
+ }
537
+ return { sender, recipient, subject };
623
538
  }
624
- function createBridgeLogger(scope) {
625
- const scopedMessage = (message) => `[${scope}] ${message}`;
539
+ function getInboxRoute(fileName, body) {
540
+ const frontmatterRoute = parseInboxFrontmatter(body);
541
+ if (frontmatterRoute) {
542
+ return frontmatterRoute;
543
+ }
544
+ const stem = fileName.replace(/\.md$/i, "");
545
+ const parts = stem.split("-");
546
+ let offset = 0;
547
+ if (parts[0] && /^\d{8}$/.test(parts[0])) {
548
+ offset = 1;
549
+ }
626
550
  return {
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
- }
551
+ sender: decodeRouteSegment(parts[offset] ?? ""),
552
+ recipient: decodeRouteSegment(parts[offset + 1] ?? ""),
553
+ subject: decodeRouteSegment(parts.slice(offset + 2).join("-"))
639
554
  };
640
555
  }
641
-
642
- // ../../scripts/bridge/bridge-candidates.ts
643
- var routingLogger = createBridgeLogger("routing");
644
556
  function buildMarkerId(filePath, mtimeMs) {
645
557
  return createHash("sha1").update(`${filePath}|${mtimeMs}`).digest("hex");
646
558
  }
647
559
  function getProcessedMarkerPath(stateDir, markerId) {
648
- return join4(stateDir, "processed", `${markerId}.done`);
560
+ return join(stateDir, "processed", `${markerId}.done`);
649
561
  }
650
562
  function loadHeartbeats(commsDir) {
651
563
  try {
652
- return JSON.parse(readFileSync4(join4(commsDir, "heartbeats.json"), "utf8"));
564
+ return JSON.parse(readFileSync(join(commsDir, "heartbeats.json"), "utf8"));
653
565
  } catch {
654
566
  return {};
655
567
  }
656
568
  }
569
+ function formatAgentLabel(agentIdOrName, displayName) {
570
+ const normalizedId = agentIdOrName.trim();
571
+ const normalizedName = displayName?.trim();
572
+ if (!normalizedId) {
573
+ return normalizedName ?? agentIdOrName;
574
+ }
575
+ if (!normalizedName || normalizedName === normalizedId) {
576
+ return normalizedId;
577
+ }
578
+ return `${normalizedName} [${normalizedId}]`;
579
+ }
580
+ function resolveAddressLabel(address, heartbeats) {
581
+ const normalized = address.trim();
582
+ if (!normalized || normalized === "\uC804\uCCB4" || normalized === "all") {
583
+ return address;
584
+ }
585
+ const direct = heartbeats[normalized];
586
+ if (direct?.agent?.trim()) {
587
+ return formatAgentLabel(normalized, direct.agent);
588
+ }
589
+ for (const [agentId, heartbeat] of Object.entries(heartbeats)) {
590
+ if (heartbeat.agent?.trim() === normalized) {
591
+ return formatAgentLabel(agentId, heartbeat.agent);
592
+ }
593
+ }
594
+ return normalized;
595
+ }
596
+ function resolveCurrentAgentName(agentId, fallbackAgentName, heartbeats) {
597
+ const currentName = heartbeats[agentId]?.agent?.trim();
598
+ if (currentName) {
599
+ return currentName;
600
+ }
601
+ for (const heartbeat of Object.values(heartbeats)) {
602
+ if (heartbeat.id?.trim() === agentId && heartbeat.agent?.trim()) {
603
+ return heartbeat.agent.trim();
604
+ }
605
+ }
606
+ return fallbackAgentName;
607
+ }
608
+ function refreshAgentIdentity(options, heartbeats) {
609
+ const nextAgentName = resolveCurrentAgentName(
610
+ options.agentId,
611
+ options.agentName,
612
+ heartbeats
613
+ );
614
+ if (nextAgentName !== options.agentName) {
615
+ options.agentName = nextAgentName;
616
+ persistAgentName(options.stateDir, nextAgentName);
617
+ }
618
+ return nextAgentName;
619
+ }
620
+ var HEADLESS_SKIP_PATTERNS = [
621
+ /리뷰\s*요청/,
622
+ /review[- ]?request/i,
623
+ /재리뷰/,
624
+ /re-?review/i
625
+ ];
657
626
  function shouldSkipInHeadlessMode(fileName, body) {
658
627
  if (process.env.TAP_HEADLESS !== "true") return false;
659
628
  const combined = `${fileName}
660
629
  ${body}`;
661
630
  return HEADLESS_SKIP_PATTERNS.some((p) => p.test(combined));
662
631
  }
663
- function collectCandidates(inboxDir, agentId, agentName, aliasName) {
632
+ function collectCandidates(inboxDir, agentId, agentName) {
664
633
  const entries = readdirSync(inboxDir, { withFileTypes: true }).filter(
665
634
  (entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md")
666
635
  ).map((entry) => {
667
- const filePath = join4(inboxDir, entry.name);
636
+ const filePath = join(inboxDir, entry.name);
668
637
  const stats = statSync(filePath);
669
638
  return { entry, filePath, stats };
670
639
  }).sort((left, right) => left.stats.mtimeMs - right.stats.mtimeMs);
671
640
  const candidates = [];
672
- let filteredByRecipient = 0;
673
- let filteredBySelf = 0;
674
- let filteredByHeadless = 0;
675
641
  for (const item of entries) {
676
- let body;
677
- try {
678
- body = readFileSync4(item.filePath, "utf8");
679
- } catch {
680
- continue;
681
- }
642
+ const body = readFileSync(item.filePath, "utf8");
682
643
  const route = getInboxRoute(item.entry.name, body);
683
- if (!recipientMatchesAgent(route.recipient, agentId, agentName) && !(aliasName && recipientMatchesAgent(route.recipient, agentId, aliasName))) {
684
- filteredByRecipient += 1;
644
+ if (!recipientMatchesAgent(route.recipient, agentId, agentName)) {
685
645
  continue;
686
646
  }
687
- if (isOwnMessageSender(route.sender, agentId, agentName) || aliasName && isOwnMessageSender(route.sender, agentId, aliasName)) {
688
- filteredBySelf += 1;
647
+ if (isOwnMessageSender(route.sender, agentId, agentName)) {
689
648
  continue;
690
649
  }
691
650
  if (shouldSkipInHeadlessMode(item.entry.name, body)) {
692
- filteredByHeadless += 1;
693
651
  continue;
694
652
  }
695
653
  candidates.push({
@@ -699,58 +657,34 @@ function collectCandidates(inboxDir, agentId, agentName, aliasName) {
699
657
  sender: route.sender,
700
658
  recipient: route.recipient,
701
659
  subject: route.subject,
702
- body: stripBridgeFrontmatter(body),
660
+ body,
703
661
  mtimeMs: item.stats.mtimeMs
704
662
  });
705
663
  }
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
- });
717
664
  return candidates;
718
665
  }
719
666
  function getPendingCandidates(options, cutoff) {
720
- const inboxDir = join4(options.commsDir, "inbox");
721
- if (!existsSync4(inboxDir)) {
667
+ const inboxDir = join(options.commsDir, "inbox");
668
+ if (!existsSync(inboxDir)) {
722
669
  throw new Error(`Inbox directory not found: ${inboxDir}`);
723
670
  }
724
671
  const heartbeats = loadHeartbeats(options.commsDir);
725
- const refreshedName = refreshAgentIdentity(options, heartbeats);
672
+ const agentName = refreshAgentIdentity(options, heartbeats);
726
673
  const cutoffMs = cutoff.getTime();
727
674
  const candidates = collectCandidates(
728
675
  inboxDir,
729
676
  options.agentId,
730
- options.agentName,
731
- // M205: Also accept messages addressed to the heartbeat-refreshed name
732
- refreshedName !== options.agentName ? refreshedName : void 0
677
+ agentName
733
678
  ).filter((candidate) => {
734
679
  if (candidate.mtimeMs < cutoffMs) {
735
680
  return false;
736
681
  }
737
- return !existsSync4(
682
+ return !existsSync(
738
683
  getProcessedMarkerPath(options.stateDir, candidate.markerId)
739
684
  );
740
685
  });
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
- });
748
686
  return { heartbeats, candidates };
749
687
  }
750
-
751
- // ../../scripts/bridge/bridge-format.ts
752
- import { writeFileSync as writeFileSync3 } from "fs";
753
- import { join as join5 } from "path";
754
688
  function buildUserInput(candidate, agentName, heartbeats) {
755
689
  const sender = resolveAddressLabel(candidate.sender || "unknown", heartbeats);
756
690
  const recipient = resolveAddressLabel(
@@ -789,7 +723,7 @@ function writeProcessedMarker(stateDir, candidate, dispatchMode, threadId, turnI
789
723
  turnId,
790
724
  markedAt: (/* @__PURE__ */ new Date()).toISOString()
791
725
  };
792
- writeFileSync3(
726
+ writeFileSync(
793
727
  getProcessedMarkerPath(stateDir, candidate.markerId),
794
728
  `${JSON.stringify(payload, null, 2)}
795
729
  `,
@@ -809,369 +743,83 @@ function writeLastDispatch(stateDir, candidate, dispatchMode, threadId, turnId)
809
743
  turnId,
810
744
  dispatchedAt: (/* @__PURE__ */ new Date()).toISOString()
811
745
  };
812
- writeFileSync3(
813
- join5(stateDir, "last-dispatch.json"),
746
+ writeFileSync(
747
+ join(stateDir, "last-dispatch.json"),
814
748
  `${JSON.stringify(payload, null, 2)}
815
749
  `,
816
750
  "utf8"
817
751
  );
818
752
  }
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, "***");
753
+ function formatJsonRpcError(error) {
754
+ if (!error) {
755
+ return "Unknown App Server error";
756
+ }
757
+ return JSON.stringify(
758
+ {
759
+ code: error.code,
760
+ message: error.message,
761
+ data: error.data
762
+ },
763
+ null,
764
+ 2
765
+ );
835
766
  }
836
767
  function delay(ms) {
837
768
  return new Promise((resolvePromise) => {
838
769
  setTimeout(resolvePromise, ms);
839
770
  });
840
771
  }
841
- function readThreadState(stateDir) {
842
- const threadPath = join6(stateDir, "thread.json");
843
- if (!existsSync5(threadPath)) {
844
- return null;
772
+ async function waitForTurnCompletion(client, turnId, timeoutMs) {
773
+ const deadline = Date.now() + timeoutMs;
774
+ let nextRefreshAt = Date.now();
775
+ while (Date.now() < deadline) {
776
+ if (!client.activeTurnId || client.activeTurnId !== turnId) {
777
+ return client.lastTurnStatus;
778
+ }
779
+ if (Date.now() >= nextRefreshAt) {
780
+ await client.refreshCurrentThreadState().catch(() => void 0);
781
+ if (!client.activeTurnId || client.activeTurnId !== turnId) {
782
+ return client.lastTurnStatus;
783
+ }
784
+ nextRefreshAt = Date.now() + TURN_COMPLETION_REFRESH_MS;
785
+ }
786
+ await delay(
787
+ Math.min(TURN_COMPLETION_POLL_MS, Math.max(deadline - Date.now(), 0))
788
+ );
789
+ }
790
+ await client.refreshCurrentThreadState().catch(() => void 0);
791
+ if (!client.activeTurnId || client.activeTurnId !== turnId) {
792
+ return client.lastTurnStatus;
793
+ }
794
+ throw new Error(`Timed out waiting for turn ${turnId} to complete`);
795
+ }
796
+ async function maybeBootstrapHeadlessTurn(options, cutoff, client) {
797
+ if (process.env.TAP_HEADLESS !== "true" && process.env.TAP_COLD_START_WARMUP !== "true") {
798
+ return false;
799
+ }
800
+ const { candidates } = getPendingCandidates(options, cutoff);
801
+ if (candidates.length > 0 || client.activeTurnId || client.lastTurnStatus !== null) {
802
+ return false;
803
+ }
804
+ logStatus("headless cold-start: sending warmup turn");
805
+ const turnId = await client.startTurn(HEADLESS_WARMUP_PROMPT);
806
+ if (!turnId) {
807
+ throw new Error(
808
+ "Headless cold-start warmup failed: turn/start did not return a turn id. Run: npx @hua-labs/tap doctor"
809
+ );
845
810
  }
846
811
  try {
847
- const parsed = JSON.parse(
848
- readFileSync5(threadPath, "utf8")
812
+ const status = await waitForTurnCompletion(
813
+ client,
814
+ turnId,
815
+ HEADLESS_WARMUP_TIMEOUT_MS
849
816
  );
850
- if (parsed.threadId) {
851
- return parsed;
817
+ if (status !== "completed") {
818
+ throw new Error(
819
+ `turn ${turnId} finished with status ${status ?? "unknown"}`
820
+ );
852
821
  }
853
- } catch {
854
- return null;
855
- }
856
- return null;
857
- }
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") {
1145
- return false;
1146
- }
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
- });
822
+ logStatus(`headless cold-start warmup completed (${status})`);
1175
823
  return true;
1176
824
  } catch (error) {
1177
825
  const reason = error instanceof Error ? error.message : String(error);
@@ -1180,8 +828,13 @@ async function maybeBootstrapHeadlessTurn(options, cutoff, client) {
1180
828
  );
1181
829
  }
1182
830
  }
1183
-
1184
- // ../../scripts/bridge/bridge-ws-client.ts
831
+ function shouldRetrySteerAsStart(error) {
832
+ if (!(error instanceof Error)) {
833
+ return false;
834
+ }
835
+ const message = error.message.toLowerCase();
836
+ return message.includes("no active turn") || message.includes("expectedturnid") || message.includes("turn/steer failed") && (message.includes("active turn") || message.includes("not found"));
837
+ }
1185
838
  async function readSocketData(data) {
1186
839
  if (typeof data === "string") {
1187
840
  return data;
@@ -1199,27 +852,11 @@ async function readSocketData(data) {
1199
852
  }
1200
853
  return String(data);
1201
854
  }
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;
1217
855
  var AppServerClient = class {
1218
856
  socket = null;
1219
857
  url;
1220
858
  gatewayToken;
1221
859
  logger;
1222
- clientId = nextAppServerClientId++;
1223
860
  nextId = 1;
1224
861
  pending = /* @__PURE__ */ new Map();
1225
862
  connected = false;
@@ -1243,12 +880,6 @@ var AppServerClient = class {
1243
880
  if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
1244
881
  return;
1245
882
  }
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
- }
1252
883
  const wsOptions = {};
1253
884
  if (this.gatewayToken) {
1254
885
  wsOptions.protocols = [`${AUTH_SUBPROTOCOL_PREFIX}${this.gatewayToken}`];
@@ -1274,11 +905,7 @@ var AppServerClient = class {
1274
905
  "open",
1275
906
  () => {
1276
907
  this.connected = true;
1277
- this.logger.info("connected to app-server", {
1278
- clientId: this.clientId,
1279
- url: this.url,
1280
- authenticated: Boolean(this.gatewayToken)
1281
- });
908
+ this.logger(`connected to app-server at ${this.url}`);
1282
909
  resolveOnce();
1283
910
  },
1284
911
  { once: true }
@@ -1287,12 +914,7 @@ var AppServerClient = class {
1287
914
  const error = new Error(
1288
915
  `Failed to connect to App Server at ${this.url}`
1289
916
  );
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
- });
917
+ this.lastError = error.message;
1296
918
  rejectOnce(error);
1297
919
  });
1298
920
  this.socket?.addEventListener("close", () => {
@@ -1300,10 +922,7 @@ var AppServerClient = class {
1300
922
  this.initialized = false;
1301
923
  this.activeTurnId = null;
1302
924
  this.turnStartedAt = null;
1303
- this.logger.warn("disconnected from app-server", {
1304
- clientId: this.clientId,
1305
- url: this.url
1306
- });
925
+ this.logger("disconnected from app-server");
1307
926
  this.rejectPending(new Error("App Server connection closed"));
1308
927
  });
1309
928
  this.socket?.addEventListener("message", (event) => {
@@ -1340,20 +959,13 @@ var AppServerClient = class {
1340
959
  });
1341
960
  const resumedThreadId = resumeResponse?.thread?.id ?? explicitThreadId;
1342
961
  await this.refreshThreadState(resumedThreadId);
1343
- this.logger.info("resumed explicit thread", {
1344
- clientId: this.clientId,
1345
- threadId: resumedThreadId,
1346
- activeTurnId: this.activeTurnId
1347
- });
962
+ this.logger(
963
+ `resumed thread ${resumedThreadId}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
964
+ );
1348
965
  return resumedThreadId;
1349
966
  } catch (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
- }
967
+ this.logger(
968
+ `thread resume failed for ${explicitThreadId}; starting a fresh thread (${String(error)})`
1357
969
  );
1358
970
  }
1359
971
  }
@@ -1363,12 +975,9 @@ var AppServerClient = class {
1363
975
  }
1364
976
  if (savedThread?.threadId) {
1365
977
  if (savedThread.cwd && !threadCwdMatches(cwd, savedThread.cwd)) {
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
- });
978
+ this.logger(
979
+ `saved thread ${savedThread.threadId} cwd ${savedThread.cwd} does not match ${cwd}; skipping saved thread`
980
+ );
1372
981
  } else {
1373
982
  try {
1374
983
  const resumeResponse = await this.request("thread/resume", {
@@ -1378,33 +987,23 @@ var AppServerClient = class {
1378
987
  const resumedThreadId = resumeResponse?.thread?.id ?? savedThread.threadId;
1379
988
  await this.refreshThreadState(resumedThreadId);
1380
989
  if (!threadCwdMatches(cwd, this.currentThreadCwd)) {
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
- });
990
+ this.logger(
991
+ `saved thread ${resumedThreadId} cwd ${this.currentThreadCwd ?? "unknown"} does not match ${cwd}; starting a fresh thread`
992
+ );
1387
993
  this.threadId = null;
1388
994
  this.currentThreadCwd = null;
1389
995
  this.activeTurnId = null;
1390
996
  this.turnStartedAt = null;
1391
997
  this.lastTurnStatus = null;
1392
998
  } else {
1393
- this.logger.info("resumed saved thread", {
1394
- clientId: this.clientId,
1395
- threadId: resumedThreadId,
1396
- activeTurnId: this.activeTurnId
1397
- });
999
+ this.logger(
1000
+ `resumed saved thread ${resumedThreadId}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
1001
+ );
1398
1002
  return resumedThreadId;
1399
1003
  }
1400
1004
  } catch (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
- }
1005
+ this.logger(
1006
+ `saved thread resume failed for ${savedThread.threadId}; starting a fresh thread (${String(error)})`
1408
1007
  );
1409
1008
  }
1410
1009
  }
@@ -1424,12 +1023,7 @@ var AppServerClient = class {
1424
1023
  this.currentThreadCwd = this.currentThreadCwd ?? cwd;
1425
1024
  this.activeTurnId = null;
1426
1025
  this.lastTurnStatus = null;
1427
- this.logger.info("started thread", {
1428
- clientId: this.clientId,
1429
- threadId: startedThreadId,
1430
- cwd: this.currentThreadCwd,
1431
- ephemeral
1432
- });
1026
+ this.logger(`started thread ${startedThreadId}`);
1433
1027
  return startedThreadId;
1434
1028
  }
1435
1029
  async findLoadedThread(cwd) {
@@ -1467,21 +1061,14 @@ var AppServerClient = class {
1467
1061
  const chosen = chooseLoadedThreadForCwd(cwd, threads);
1468
1062
  if (!chosen) {
1469
1063
  if (threads.length > 0) {
1470
- this.logger.debug("loaded threads exist but none match cwd", {
1471
- clientId: this.clientId,
1472
- cwd,
1473
- loadedThreadCount: threads.length
1474
- });
1064
+ this.logger(`loaded threads exist but none match cwd ${cwd}`);
1475
1065
  }
1476
1066
  return null;
1477
1067
  }
1478
1068
  this.syncThreadStateFromThread(chosen.thread);
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
- });
1069
+ this.logger(
1070
+ `attached to loaded thread ${chosen.id}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
1071
+ );
1485
1072
  return chosen.id;
1486
1073
  }
1487
1074
  async startTurn(inputText) {
@@ -1520,18 +1107,7 @@ var AppServerClient = class {
1520
1107
  return turnId;
1521
1108
  }
1522
1109
  isBusy() {
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;
1110
+ return Boolean(this.activeTurnId);
1535
1111
  }
1536
1112
  async refreshCurrentThreadState() {
1537
1113
  if (!this.threadId) {
@@ -1565,33 +1141,12 @@ var AppServerClient = class {
1565
1141
  this.currentThreadCwd = typeof thread?.cwd === "string" ? thread.cwd : null;
1566
1142
  let activeTurnId = null;
1567
1143
  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
- }
1578
1144
  const turns = Array.isArray(thread?.turns) ? thread.turns : [];
1579
1145
  for (const turn of turns) {
1580
1146
  if (typeof turn?.status === "string") {
1581
1147
  lastTurnStatus = turn.status;
1582
1148
  }
1583
1149
  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
- }
1595
1150
  activeTurnId = turn.id;
1596
1151
  }
1597
1152
  }
@@ -1614,12 +1169,7 @@ var AppServerClient = class {
1614
1169
  this.pending.delete(message.id);
1615
1170
  if (message.error) {
1616
1171
  const errorText = formatJsonRpcError(message.error);
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
- });
1172
+ this.lastError = errorText;
1623
1173
  pending.reject(new Error(`${pending.method} failed: ${errorText}`));
1624
1174
  return;
1625
1175
  }
@@ -1634,10 +1184,6 @@ var AppServerClient = class {
1634
1184
  }
1635
1185
  this.lastNotificationMethod = message.method;
1636
1186
  this.lastNotificationAt = (/* @__PURE__ */ new Date()).toISOString();
1637
- this.logger.debug("received app-server notification", {
1638
- clientId: this.clientId,
1639
- method: message.method
1640
- });
1641
1187
  this.handleNotification(message.method, message.params);
1642
1188
  }
1643
1189
  handleNotification(method, params) {
@@ -1649,28 +1195,18 @@ var AppServerClient = class {
1649
1195
  if (typeof params?.thread?.cwd === "string") {
1650
1196
  this.currentThreadCwd = params.thread.cwd;
1651
1197
  }
1652
- this.logger.info("thread started notification", {
1653
- clientId: this.clientId,
1654
- threadId: params?.thread?.id ?? null,
1655
- cwd: params?.thread?.cwd ?? null
1656
- });
1198
+ this.logger(`thread started ${params?.thread?.id ?? ""}`.trim());
1657
1199
  break;
1658
1200
  case "thread/status/changed":
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
- });
1201
+ this.logger(
1202
+ `thread status changed (${params?.thread?.status?.type ?? params?.status?.type ?? "unknown"})`
1203
+ );
1664
1204
  break;
1665
1205
  case "turn/started":
1666
1206
  if (params?.turn?.id) {
1667
1207
  this.activeTurnId = params.turn.id;
1668
1208
  this.turnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
1669
- this.logger.info("turn started", {
1670
- clientId: this.clientId,
1671
- threadId: this.threadId,
1672
- turnId: params.turn.id
1673
- });
1209
+ this.logger(`turn started ${params.turn.id}`);
1674
1210
  }
1675
1211
  break;
1676
1212
  case "turn/completed": {
@@ -1679,22 +1215,15 @@ var AppServerClient = class {
1679
1215
  this.activeTurnId = null;
1680
1216
  this.turnStartedAt = null;
1681
1217
  const elapsedMs = prevTurnStartedAt ? Date.now() - new Date(prevTurnStartedAt).getTime() : null;
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
- });
1218
+ const elapsedSuffix = elapsedMs !== null ? ` \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed` : "";
1219
+ this.logger(
1220
+ `turn completed (${this.lastTurnStatus ?? "unknown"})${elapsedSuffix}`
1221
+ );
1688
1222
  break;
1689
1223
  }
1690
1224
  case "error":
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
- });
1225
+ this.lastError = JSON.stringify(params ?? {}, null, 2);
1226
+ this.logger(`app-server error notification: ${this.lastError}`);
1698
1227
  break;
1699
1228
  default:
1700
1229
  break;
@@ -1727,110 +1256,252 @@ var AppServerClient = class {
1727
1256
  }
1728
1257
  this.pending.clear();
1729
1258
  }
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
- });
1259
+ };
1260
+ var heartbeatCount = 0;
1261
+ function writeHeartbeat(options, client, health) {
1262
+ if (client?.threadId) {
1263
+ const savedThread = readThreadState(options.stateDir);
1264
+ persistThreadState(
1265
+ options.stateDir,
1266
+ client.threadId,
1267
+ options.appServerUrl,
1268
+ options.ephemeral,
1269
+ client.currentThreadCwd ?? savedThread?.cwd ?? null
1270
+ );
1271
+ }
1272
+ const payload = {
1273
+ pid: process.pid,
1274
+ agent: options.agentName,
1275
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1276
+ pollSeconds: options.pollSeconds,
1277
+ appServerUrl: options.appServerUrl,
1278
+ connected: client?.connected ?? false,
1279
+ initialized: client?.initialized ?? false,
1280
+ threadId: client?.threadId ?? null,
1281
+ threadCwd: client?.currentThreadCwd ?? null,
1282
+ activeTurnId: client?.activeTurnId ?? null,
1283
+ turnStartedAt: client?.turnStartedAt ?? null,
1284
+ lastTurnStatus: client?.lastTurnStatus ?? null,
1285
+ lastNotificationMethod: client?.lastNotificationMethod ?? null,
1286
+ lastNotificationAt: client?.lastNotificationAt ?? null,
1287
+ lastError: sanitizeErrorForPersistence(client?.lastError ?? null),
1288
+ lastSuccessfulAppServerAt: client?.lastSuccessfulAppServerAt ?? null,
1289
+ lastSuccessfulAppServerMethod: client?.lastSuccessfulAppServerMethod ?? null,
1290
+ consecutiveFailureCount: health.consecutiveFailureCount,
1291
+ busyMode: options.busyMode
1292
+ };
1293
+ writeFileSync(
1294
+ join(options.stateDir, "heartbeat.json"),
1295
+ `${JSON.stringify(payload, null, 2)}
1296
+ `,
1297
+ "utf8"
1298
+ );
1299
+ heartbeatCount += 1;
1300
+ if (heartbeatCount % 5 === 0) {
1301
+ logStatus(
1302
+ `heartbeat: connected=${payload.connected}, thread=${payload.threadId ?? "null"}, turns=${payload.activeTurnId ? "active" : "0"}`
1303
+ );
1304
+ }
1305
+ const status = client?.connected ? "active" : "idle";
1306
+ updateCommsHeartbeat(options, status);
1740
1307
  }
1741
- function readHeartbeatState(stateDir) {
1742
- const heartbeatPath = join7(stateDir, "heartbeat.json");
1743
- if (!existsSync6(heartbeatPath)) {
1744
- return null;
1308
+ var COMMS_HEARTBEAT_LOCK_TIMEOUT_MS = 2e3;
1309
+ var COMMS_LOCK_STALE_AGE_MS = 1e4;
1310
+ function acquireCommsLock(lockPath) {
1311
+ const deadline = Date.now() + COMMS_HEARTBEAT_LOCK_TIMEOUT_MS;
1312
+ while (Date.now() < deadline) {
1313
+ try {
1314
+ writeFileSync(lockPath, String(process.pid), { flag: "wx" });
1315
+ return true;
1316
+ } catch {
1317
+ try {
1318
+ const lockAge = Date.now() - statSync(lockPath).mtimeMs;
1319
+ if (lockAge > COMMS_LOCK_STALE_AGE_MS) {
1320
+ unlinkSync(lockPath);
1321
+ try {
1322
+ writeFileSync(lockPath, String(process.pid), { flag: "wx" });
1323
+ return true;
1324
+ } catch {
1325
+ }
1326
+ }
1327
+ } catch {
1328
+ }
1329
+ const start = Date.now();
1330
+ while (Date.now() - start < 50) {
1331
+ }
1332
+ }
1745
1333
  }
1334
+ return false;
1335
+ }
1336
+ function releaseCommsLock(lockPath) {
1746
1337
  try {
1747
- return JSON.parse(readFileSync6(heartbeatPath, "utf8"));
1338
+ unlinkSync(lockPath);
1748
1339
  } catch {
1749
- return null;
1750
- }
1751
- }
1752
- function parseUpdatedAt(value) {
1753
- if (!value) {
1754
- return 0;
1755
- }
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
1340
  }
1767
- return isAbsolute3(normalized) || /^[A-Za-z]:[\\/]/.test(normalized) || normalized.startsWith("\\\\");
1768
1341
  }
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;
1342
+ function updateCommsHeartbeat(options, status) {
1343
+ const heartbeatsPath = join(options.commsDir, "heartbeats.json");
1344
+ const lockPath = join(options.commsDir, ".heartbeats.lock");
1345
+ if (!acquireCommsLock(lockPath)) {
1346
+ return;
1781
1347
  }
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
1348
+ try {
1349
+ let store = {};
1350
+ try {
1351
+ store = JSON.parse(readFileSync(heartbeatsPath, "utf-8"));
1352
+ } catch {
1353
+ }
1354
+ const key = options.agentId;
1355
+ const existing = store[key];
1356
+ store[key] = {
1357
+ id: options.agentId,
1358
+ agent: options.agentName,
1359
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1360
+ lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
1361
+ joinedAt: existing?.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1362
+ status
1798
1363
  };
1799
- } else if (parseUpdatedAt(heartbeat?.updatedAt) > parseUpdatedAt(savedThread.updatedAt)) {
1800
- preferred = heartbeatBackedThread;
1364
+ const tmpPath = heartbeatsPath + ".tmp." + process.pid;
1365
+ writeFileSync(tmpPath, JSON.stringify(store, null, 2), "utf-8");
1366
+ renameSync(tmpPath, heartbeatsPath);
1367
+ } catch {
1368
+ } finally {
1369
+ releaseCommsLock(lockPath);
1801
1370
  }
1802
- return preferred;
1803
1371
  }
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)) {
1372
+ async function dispatchCandidate(client, options, candidate, heartbeats) {
1373
+ const input = buildUserInput(candidate, options.agentName, heartbeats);
1374
+ logStatus(
1375
+ `dispatching from ${candidate.sender || "unknown"}: ${candidate.subject || "(none)"}`
1376
+ );
1377
+ if (client.isBusy()) {
1378
+ if (options.busyMode !== "steer") {
1379
+ return false;
1380
+ }
1811
1381
  try {
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;
1382
+ const turnId2 = await client.steerTurn(input);
1383
+ writeProcessedMarker(
1384
+ options.stateDir,
1385
+ candidate,
1386
+ "steer",
1387
+ client.threadId,
1388
+ turnId2
1389
+ );
1390
+ writeLastDispatch(
1391
+ options.stateDir,
1392
+ candidate,
1393
+ "steer",
1394
+ client.threadId,
1395
+ turnId2
1396
+ );
1397
+ logStatus(`steered active turn with ${candidate.fileName}`);
1398
+ return true;
1399
+ } catch (error) {
1400
+ await client.refreshCurrentThreadState().catch(() => void 0);
1401
+ if (!client.isBusy()) {
1402
+ return dispatchCandidate(client, options, candidate, heartbeats);
1818
1403
  }
1819
- } catch {
1404
+ if (shouldRetrySteerAsStart(error)) {
1405
+ client.activeTurnId = null;
1406
+ client.turnStartedAt = null;
1407
+ logStatus(
1408
+ `steer fallback -> start for ${candidate.fileName} (${String(error)})`
1409
+ );
1410
+ return dispatchCandidate(client, options, candidate, heartbeats);
1411
+ }
1412
+ throw error;
1820
1413
  }
1821
1414
  }
1822
- if (lookbackCutoff) {
1823
- return lookbackCutoff;
1415
+ const turnId = await client.startTurn(input);
1416
+ writeProcessedMarker(
1417
+ options.stateDir,
1418
+ candidate,
1419
+ "start",
1420
+ client.threadId,
1421
+ turnId
1422
+ );
1423
+ writeLastDispatch(
1424
+ options.stateDir,
1425
+ candidate,
1426
+ "start",
1427
+ client.threadId,
1428
+ turnId
1429
+ );
1430
+ logStatus(`dispatched ${candidate.fileName} to thread ${client.threadId}`);
1431
+ return true;
1432
+ }
1433
+ async function runScan(options, cutoff, client) {
1434
+ const { heartbeats, candidates } = getPendingCandidates(options, cutoff);
1435
+ for (const candidate of candidates) {
1436
+ if (options.dryRun) {
1437
+ logStatus(`dry-run candidate ${candidate.fileName}`);
1438
+ continue;
1439
+ }
1440
+ if (!client) {
1441
+ throw new Error("App Server client is not available");
1442
+ }
1443
+ const dispatched = await dispatchCandidate(
1444
+ client,
1445
+ options,
1446
+ candidate,
1447
+ heartbeats
1448
+ );
1449
+ if (!dispatched && options.busyMode === "wait") {
1450
+ return false;
1451
+ }
1452
+ return true;
1824
1453
  }
1825
- const cutoff = /* @__PURE__ */ new Date();
1826
- writeFileSync5(cutoffPath, `${cutoff.toISOString()}
1827
- `, "utf8");
1828
- return cutoff;
1454
+ return false;
1455
+ }
1456
+ async function waitForTurnDrain(options, client, health) {
1457
+ const deadline = Date.now() + options.waitAfterDispatchSeconds * 1e3;
1458
+ while (Date.now() < deadline) {
1459
+ writeHeartbeat(options, client, health);
1460
+ if (!client.activeTurnId) {
1461
+ return;
1462
+ }
1463
+ await delay(1e3);
1464
+ }
1465
+ }
1466
+ function buildOptions(argv) {
1467
+ const parsed = parseArgs(argv);
1468
+ const repoRoot = resolveRepoRoot(parsed.repoRoot);
1469
+ const commsDir = resolveCommsDir(repoRoot, parsed.commsDir);
1470
+ const preferredAgentName = resolvePreferredAgentName(parsed.agentName);
1471
+ const stateDir = resolveStateDir(
1472
+ repoRoot,
1473
+ parsed.stateDir,
1474
+ preferredAgentName
1475
+ );
1476
+ const agentName = resolveAgentName(preferredAgentName, stateDir);
1477
+ const agentId = resolveAgentId(agentName);
1478
+ persistAgentName(stateDir, agentName);
1479
+ const gatewayTokenFile = parsed.gatewayTokenFile?.trim() || process.env.TAP_GATEWAY_TOKEN_FILE?.trim() || null;
1480
+ const appServerUrl = parsed.appServerUrl?.trim() || process.env.CODEX_APP_SERVER_URL || DEFAULT_APP_SERVER_URL;
1481
+ return {
1482
+ repoRoot,
1483
+ commsDir,
1484
+ agentId,
1485
+ stateDir,
1486
+ agentName,
1487
+ pollSeconds: parsed.pollSeconds ?? 5,
1488
+ reconnectSeconds: parsed.reconnectSeconds ?? 5,
1489
+ messageLookbackMinutes: parsed.messageLookbackMinutes ?? 10,
1490
+ processExistingMessages: parsed.processExistingMessages,
1491
+ dryRun: parsed.dryRun,
1492
+ runOnce: parsed.runOnce,
1493
+ waitAfterDispatchSeconds: parsed.waitAfterDispatchSeconds ?? 0,
1494
+ appServerUrl,
1495
+ connectAppServerUrl: appServerUrl,
1496
+ gatewayToken: gatewayTokenFile ? readGatewayTokenFile(gatewayTokenFile) : null,
1497
+ gatewayTokenFile,
1498
+ busyMode: parsed.busyMode ?? "steer",
1499
+ threadId: parsed.threadId?.trim() || null,
1500
+ ephemeral: parsed.ephemeral
1501
+ };
1829
1502
  }
1830
1503
  async function main() {
1831
1504
  const options = buildOptions(process.argv.slice(2));
1832
- configureBridgeLogging(options.logLevel);
1833
- const logger = createBridgeLogger("bridge");
1834
1505
  const cutoff = getGeneralInboxCutoff(
1835
1506
  options.stateDir,
1836
1507
  options.messageLookbackMinutes,
@@ -1840,20 +1511,28 @@ async function main() {
1840
1511
  options.stateDir,
1841
1512
  options.appServerUrl
1842
1513
  );
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
- });
1514
+ logStatus("codex app-server bridge ready");
1515
+ console.log(` repo: ${options.repoRoot}`);
1516
+ console.log(` comms: ${options.commsDir}`);
1517
+ console.log(` agent: ${options.agentName}`);
1518
+ console.log(` state: ${options.stateDir}`);
1519
+ console.log(` app-server: ${options.appServerUrl}`);
1520
+ console.log(` busy-mode: ${options.busyMode}`);
1521
+ if (options.waitAfterDispatchSeconds > 0) {
1522
+ console.log(
1523
+ ` wait: ${options.waitAfterDispatchSeconds}s after dispatch`
1524
+ );
1525
+ }
1526
+ console.log(
1527
+ ` lookback: ${options.processExistingMessages ? "existing messages" : `${options.messageLookbackMinutes} minute(s)`}`
1528
+ );
1529
+ if (options.threadId || initialSavedThread?.threadId) {
1530
+ console.log(
1531
+ ` thread: ${options.threadId ?? initialSavedThread?.threadId}`
1532
+ );
1533
+ }
1855
1534
  if (options.dryRun) {
1856
- logger.info("dry-run mode enabled");
1535
+ logStatus("dry-run mode enabled");
1857
1536
  }
1858
1537
  let client = null;
1859
1538
  const health = {
@@ -1865,7 +1544,7 @@ async function main() {
1865
1544
  if (!client || !client.connected) {
1866
1545
  client = new AppServerClient(
1867
1546
  options.connectAppServerUrl,
1868
- createBridgeLogger("app-server"),
1547
+ logStatus,
1869
1548
  options.gatewayToken
1870
1549
  );
1871
1550
  await client.connect();
@@ -1873,10 +1552,6 @@ async function main() {
1873
1552
  options.stateDir,
1874
1553
  options.appServerUrl
1875
1554
  );
1876
- logger.debug("resolved resumable thread state", {
1877
- savedThreadId: savedThread?.threadId,
1878
- savedThreadCwd: savedThread?.cwd ?? null
1879
- });
1880
1555
  const threadId = await client.ensureThread(
1881
1556
  options.threadId,
1882
1557
  savedThread,
@@ -1901,14 +1576,8 @@ async function main() {
1901
1576
  }
1902
1577
  }
1903
1578
  }
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) {
1579
+ const dispatched = await runScan(options, cutoff, client);
1580
+ if (dispatched && client && options.waitAfterDispatchSeconds > 0) {
1912
1581
  await waitForTurnDrain(options, client, health);
1913
1582
  }
1914
1583
  health.consecutiveFailureCount = 0;
@@ -1916,28 +1585,22 @@ async function main() {
1916
1585
  if (options.runOnce) {
1917
1586
  break;
1918
1587
  }
1919
- await delay2(options.pollSeconds * 1e3);
1588
+ await delay(options.pollSeconds * 1e3);
1920
1589
  } catch (error) {
1921
1590
  const message = error instanceof Error ? error.message : String(error);
1922
- logger.error("bridge error", {
1923
- error: sanitizeErrorForPersistence(message)
1924
- });
1591
+ logStatus(`bridge error: ${message}`);
1925
1592
  if (client) {
1926
- client.lastError = sanitizeErrorForPersistence(message);
1593
+ client.lastError = message;
1927
1594
  }
1928
1595
  health.consecutiveFailureCount += 1;
1929
1596
  writeHeartbeat(options, client, health);
1930
1597
  if (options.runOnce) {
1931
- const sanitized = sanitizeErrorForPersistence(message);
1932
- throw new Error(sanitized ?? message);
1598
+ throw error;
1933
1599
  }
1934
1600
  client?.disconnect().catch(() => void 0);
1935
1601
  client = null;
1936
- logger.warn("reconnecting after bridge error", {
1937
- reconnectSeconds: options.reconnectSeconds,
1938
- consecutiveFailureCount: health.consecutiveFailureCount
1939
- });
1940
- await delay2(options.reconnectSeconds * 1e3);
1602
+ logStatus(`reconnecting in ${options.reconnectSeconds}s...`);
1603
+ await delay(options.reconnectSeconds * 1e3);
1941
1604
  }
1942
1605
  }
1943
1606
  await client?.disconnect();
@@ -1945,32 +1608,24 @@ async function main() {
1945
1608
  function isDirectExecution() {
1946
1609
  const entry = process.argv[1];
1947
1610
  if (!entry) return false;
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;
1611
+ return import.meta.url === pathToFileURL(resolve(entry)).href;
1958
1612
  }
1959
- if (isDirectExecution2()) {
1613
+ if (isDirectExecution()) {
1960
1614
  main().catch((error) => {
1961
- const raw = error instanceof Error ? error.stack ?? error.message : String(error);
1962
- console.error(sanitizeErrorForPersistence(raw));
1615
+ console.error(
1616
+ error instanceof Error ? error.stack ?? error.message : String(error)
1617
+ );
1963
1618
  process.exitCode = 1;
1964
1619
  });
1965
1620
  }
1966
1621
 
1967
1622
  // src/bridges/codex-app-server-bridge.ts
1968
- function isDirectExecution3() {
1623
+ function isDirectExecution2() {
1969
1624
  const entry = process.argv[1];
1970
1625
  if (!entry) return false;
1971
- return import.meta.url === pathToFileURL3(resolve6(entry)).href;
1626
+ return import.meta.url === pathToFileURL2(resolve2(entry)).href;
1972
1627
  }
1973
- if (isDirectExecution3()) {
1628
+ if (isDirectExecution2()) {
1974
1629
  main().catch((error) => {
1975
1630
  console.error(
1976
1631
  error instanceof Error ? error.stack ?? error.message : String(error)
@@ -1979,77 +1634,19 @@ if (isDirectExecution3()) {
1979
1634
  });
1980
1635
  }
1981
1636
  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,
1989
1637
  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,
1998
1638
  buildOptions,
1999
1639
  buildUserInput,
2000
- canonicalize,
2001
1640
  chooseLoadedThreadForCwd,
2002
- collectCandidates,
2003
- dispatchCandidate,
2004
- formatAgentLabel,
2005
- formatJsonRpcError,
2006
- getGeneralInboxCutoff,
2007
- getInboxRoute,
2008
- getInboxRouteFromFilename,
2009
- getPendingCandidates,
2010
- getProcessedMarkerPath,
2011
- isDirectExecution,
2012
1641
  isOwnMessageSender,
2013
- isTurnStale,
2014
- isTurnStuckOnApproval,
2015
- loadHeartbeats,
2016
1642
  loadResumableThreadState,
2017
1643
  main,
2018
1644
  maybeBootstrapHeadlessTurn,
2019
- normalizeAgentToken,
2020
- normalizeThreadCwd,
2021
- parseArgs,
2022
- parseBridgeFrontmatter,
2023
- persistAgentName,
2024
- persistThreadState,
2025
- readGatewayTokenFile,
2026
- readHeartbeatState,
2027
- readSocketData,
2028
- readThreadState,
2029
1645
  recipientMatchesAgent,
2030
- refreshAgentIdentity,
2031
- releaseCommsLock,
2032
1646
  resolveAddressLabel,
2033
1647
  resolveAgentId,
2034
- resolveAgentName,
2035
- resolveCommsDir,
2036
1648
  resolveCurrentAgentName,
2037
- resolvePreferredAgentName,
2038
- resolveRepoRoot,
2039
- resolveStateDir,
2040
- resolveTapConfigPath,
2041
- runScan,
2042
- sanitizeErrorForPersistence,
2043
- sanitizeStateSegment,
2044
- shouldRetrySteerAsStart,
2045
- shouldSkipInHeadlessMode,
2046
- stripBridgeFrontmatter,
2047
1649
  threadCwdMatches,
2048
- updateCommsHeartbeat,
2049
- waitForTurnCompletion,
2050
- waitForTurnDrain,
2051
- writeHeartbeat,
2052
- writeLastDispatch,
2053
- writeProcessedMarker
1650
+ waitForTurnCompletion
2054
1651
  };
2055
1652
  //# sourceMappingURL=codex-app-server-bridge.mjs.map