@hua-labs/tap 0.5.2 → 0.6.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.
Files changed (47) hide show
  1. package/AI_GUIDE.md +165 -0
  2. package/CHANGELOG.md +67 -0
  3. package/README.md +204 -18
  4. package/dist/bridges/codex-app-server-auth-gateway.mjs +16 -1
  5. package/dist/bridges/codex-app-server-auth-gateway.mjs.map +1 -1
  6. package/dist/bridges/codex-app-server-bridge.d.mts +105 -12
  7. package/dist/bridges/codex-app-server-bridge.mjs +3149 -251
  8. package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
  9. package/dist/bridges/codex-bridge-runner.d.mts +4 -1
  10. package/dist/bridges/codex-bridge-runner.mjs +512 -58
  11. package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
  12. package/dist/bridges/codex-remote-ipc-relay.d.mts +1 -0
  13. package/dist/bridges/codex-remote-ipc-relay.mjs +1912 -0
  14. package/dist/bridges/codex-remote-ipc-relay.mjs.map +1 -0
  15. package/dist/bridges/gemini-ide-companion-runner.mjs.map +1 -1
  16. package/dist/cli.mjs +30818 -8324
  17. package/dist/cli.mjs.map +1 -1
  18. package/dist/codex-a2a/index.d.mts +2 -0
  19. package/dist/codex-a2a/index.mjs +416 -0
  20. package/dist/codex-a2a/index.mjs.map +1 -0
  21. package/dist/codex-health/index.d.mts +76 -0
  22. package/dist/codex-health/index.mjs +153 -0
  23. package/dist/codex-health/index.mjs.map +1 -0
  24. package/dist/codex-ipc/index.d.mts +2 -0
  25. package/dist/codex-ipc/index.mjs +1834 -0
  26. package/dist/codex-ipc/index.mjs.map +1 -0
  27. package/dist/index-D4Khz2Mh.d.mts +206 -0
  28. package/dist/index-DMToLyGd.d.mts +256 -0
  29. package/dist/index.d.mts +763 -8
  30. package/dist/index.mjs +11586 -3438
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/mcp-server.mjs +8838 -811
  33. package/dist/mcp-server.mjs.map +1 -1
  34. package/dist/types-FWvKrFUt.d.mts +43 -0
  35. package/examples/01-logic-battle-known-broken.md +46 -0
  36. package/examples/02-cross-model-review-root-cause.md +37 -0
  37. package/examples/03-convergence-pattern.md +42 -0
  38. package/examples/04-tower-broadcast.md +41 -0
  39. package/examples/05-self-awareness-paradox.md +49 -0
  40. package/examples/06-session-resurrection.md +37 -0
  41. package/examples/07-ghost-agent.md +31 -0
  42. package/examples/08-naming-creates-identity.md +36 -0
  43. package/examples/09-ceo-as-middleware.md +52 -0
  44. package/examples/10-files-as-interface.md +67 -0
  45. package/examples/README.md +34 -0
  46. package/examples/tap-profile-pack.example.json +71 -0
  47. package/package.json +21 -3
@@ -1,6 +1,6 @@
1
1
  // src/bridges/codex-app-server-bridge.ts
2
2
  import { pathToFileURL as pathToFileURL2 } from "url";
3
- import { resolve as resolve5 } from "path";
3
+ import { basename as basename2, resolve as resolve6 } from "path";
4
4
 
5
5
  // scripts/bridge/bridge-types.ts
6
6
  var DEFAULT_AGENT = String.fromCharCode(50728);
@@ -32,7 +32,7 @@ var STALE_TURN_MS = 5 * 60 * 1e3;
32
32
 
33
33
  // scripts/bridge/bridge-routing.ts
34
34
  import { existsSync, readFileSync, writeFileSync } from "fs";
35
- import { join, resolve } from "path";
35
+ import { join, resolve, win32 } from "path";
36
36
 
37
37
  // packages/tap-plugin/channels/tap-identity.ts
38
38
  var BROADCAST_RECIPIENTS = /* @__PURE__ */ new Set(["\uC804\uCCB4", "all"]);
@@ -40,7 +40,7 @@ function trimAddress(value) {
40
40
  return value?.trim() ?? "";
41
41
  }
42
42
  function canonicalizeAgentId(value) {
43
- return trimAddress(value).replace(/-/g, "_");
43
+ return trimAddress(value).replace(/-/g, "_").toLowerCase();
44
44
  }
45
45
  function isBroadcastRecipient(value) {
46
46
  return BROADCAST_RECIPIENTS.has(trimAddress(value));
@@ -75,8 +75,33 @@ function isOwnMessageAddress(sender, agentId, agentName) {
75
75
  function canonicalize(id) {
76
76
  return canonicalizeAgentId(id);
77
77
  }
78
+ var WINDOWS_NAMESPACE_PREFIX = "\\\\?\\";
79
+ var WINDOWS_NAMESPACE_UNC_PREFIX = "\\\\?\\UNC\\";
80
+ function looksLikeWindowsAbsolutePath(value) {
81
+ return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
82
+ }
83
+ function stripWindowsNamespacePrefix(cwd) {
84
+ const trimmed = cwd.trim();
85
+ if (trimmed.startsWith(WINDOWS_NAMESPACE_UNC_PREFIX)) {
86
+ return `\\\\${trimmed.slice(WINDOWS_NAMESPACE_UNC_PREFIX.length)}`;
87
+ }
88
+ if (trimmed.startsWith(WINDOWS_NAMESPACE_PREFIX)) {
89
+ return trimmed.slice(WINDOWS_NAMESPACE_PREFIX.length);
90
+ }
91
+ return trimmed;
92
+ }
93
+ function resolveThreadCwd(cwd) {
94
+ const normalized = stripWindowsNamespacePrefix(cwd);
95
+ return looksLikeWindowsAbsolutePath(normalized) ? win32.resolve(normalized) : resolve(normalized);
96
+ }
78
97
  function normalizeThreadCwd(cwd) {
79
- return resolve(cwd).replace(/\\/g, "/").toLowerCase();
98
+ return resolveThreadCwd(cwd).replace(/\\/g, "/").toLowerCase();
99
+ }
100
+ function normalizePersistedThreadCwd(cwd) {
101
+ if (!cwd?.trim()) {
102
+ return null;
103
+ }
104
+ return resolveThreadCwd(cwd);
80
105
  }
81
106
  function threadCwdMatches(expectedCwd, actualCwd) {
82
107
  if (!actualCwd) {
@@ -85,24 +110,33 @@ function threadCwdMatches(expectedCwd, actualCwd) {
85
110
  return normalizeThreadCwd(expectedCwd) === normalizeThreadCwd(actualCwd);
86
111
  }
87
112
  function chooseLoadedThreadForCwd(cwd, threads) {
88
- const matching = threads.filter((thread) => {
113
+ const reusable = threads.filter((thread) => {
89
114
  if (!threadCwdMatches(cwd, thread.cwd)) {
90
115
  return false;
91
116
  }
92
- return thread.statusType !== "notLoaded";
117
+ if (thread.statusType === "notLoaded") {
118
+ return false;
119
+ }
120
+ if (thread.statusType === "active") {
121
+ return false;
122
+ }
123
+ const threadActiveFlags = Array.isArray(
124
+ thread.thread?.status?.activeFlags
125
+ ) ? thread.thread.status.activeFlags : [];
126
+ if (isTurnStuckOnApproval(threadActiveFlags)) {
127
+ return false;
128
+ }
129
+ const turns = Array.isArray(thread.thread?.turns) ? thread.thread.turns : [];
130
+ return !turns.some((turn) => {
131
+ const activeFlags = Array.isArray(turn?.activeFlags) ? turn.activeFlags : [];
132
+ return turn?.status === "inProgress" && isTurnStuckOnApproval(activeFlags);
133
+ });
93
134
  });
94
- if (matching.length === 0) {
135
+ if (reusable.length === 0) {
95
136
  return null;
96
137
  }
97
- matching.sort((left, right) => {
98
- const leftActive = left.statusType === "active" ? 1 : 0;
99
- const rightActive = right.statusType === "active" ? 1 : 0;
100
- if (leftActive !== rightActive) {
101
- return rightActive - leftActive;
102
- }
103
- return right.updatedAt - left.updatedAt;
104
- });
105
- return matching[0] ?? null;
138
+ reusable.sort((left, right) => right.updatedAt - left.updatedAt);
139
+ return reusable[0] ?? null;
106
140
  }
107
141
  function normalizeAgentToken(value) {
108
142
  const normalized = value?.trim();
@@ -190,6 +224,12 @@ function isOwnMessageSender(sender, agentId, agentName) {
190
224
  function isTurnStuckOnApproval(activeFlags) {
191
225
  return activeFlags.includes("waitingOnApproval");
192
226
  }
227
+ function isWaitingApprovalStatus(status) {
228
+ if (!status) {
229
+ return false;
230
+ }
231
+ return /approval|input-required|confirm|consent/i.test(status);
232
+ }
193
233
  function isTurnStale(turnStartedAt, nowMs = Date.now()) {
194
234
  if (!turnStartedAt) return false;
195
235
  return nowMs - new Date(turnStartedAt).getTime() > STALE_TURN_MS;
@@ -201,6 +241,81 @@ function shouldRetrySteerAsStart(error) {
201
241
  const message = error.message.toLowerCase();
202
242
  return message.includes("no active turn") || message.includes("expectedturnid") || message.includes("turn/steer failed") && (message.includes("active turn") || message.includes("not found"));
203
243
  }
244
+ var FORBIDDEN_RAW_PAIR_TOKEN_REASON = "envelope rejected: forbidden raw pairToken field present (M355 defensive drop)";
245
+ function normalizeFrontmatterValue(value) {
246
+ const normalized = value?.trim();
247
+ return normalized ? normalized : null;
248
+ }
249
+ function parseJsonObject(value) {
250
+ const normalized = normalizeFrontmatterValue(value);
251
+ if (!normalized) {
252
+ return null;
253
+ }
254
+ try {
255
+ const parsed = JSON.parse(normalized);
256
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
257
+ return null;
258
+ }
259
+ return parsed;
260
+ } catch {
261
+ return null;
262
+ }
263
+ }
264
+ function parseFrontmatterScope(value) {
265
+ const normalized = normalizeFrontmatterValue(value);
266
+ if (normalized === "observe" || normalized === "suggest" || normalized === "drive") {
267
+ return normalized;
268
+ }
269
+ return null;
270
+ }
271
+ function parseFrontmatterAliases(value) {
272
+ if (!Array.isArray(value)) {
273
+ return void 0;
274
+ }
275
+ const aliases = [];
276
+ for (const candidate of value) {
277
+ if (typeof candidate !== "string") continue;
278
+ const normalized = candidate.trim();
279
+ if (!normalized || aliases.includes(normalized)) continue;
280
+ aliases.push(normalized);
281
+ }
282
+ return aliases.length > 0 ? aliases : void 0;
283
+ }
284
+ function parseFrontmatterSlot(value) {
285
+ if (typeof value !== "string") {
286
+ return null;
287
+ }
288
+ const normalized = value.trim();
289
+ if (normalized === "tower" || normalized === "reviewer" || /^wt-\d+$/.test(normalized)) {
290
+ return normalized;
291
+ }
292
+ return null;
293
+ }
294
+ function parseFrontmatterAddress(value) {
295
+ const record = parseJsonObject(value);
296
+ if (!record) {
297
+ return null;
298
+ }
299
+ return {
300
+ hostId: normalizeFrontmatterValue(
301
+ typeof record.hostId === "string" ? record.hostId : void 0
302
+ ),
303
+ clientId: normalizeFrontmatterValue(
304
+ typeof record.clientId === "string" ? record.clientId : void 0
305
+ ),
306
+ conversationId: normalizeFrontmatterValue(
307
+ typeof record.conversationId === "string" ? record.conversationId : void 0
308
+ ),
309
+ ownerClientId: normalizeFrontmatterValue(
310
+ typeof record.ownerClientId === "string" ? record.ownerClientId : void 0
311
+ ),
312
+ routingAddress: normalizeFrontmatterValue(
313
+ typeof record.routingAddress === "string" ? record.routingAddress : void 0
314
+ ) ?? void 0,
315
+ slot: parseFrontmatterSlot(record.slot),
316
+ aliases: parseFrontmatterAliases(record.aliases)
317
+ };
318
+ }
204
319
  function parseBridgeFrontmatter(content) {
205
320
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
206
321
  if (!match) return null;
@@ -213,7 +328,14 @@ function parseBridgeFrontmatter(content) {
213
328
  return {
214
329
  sender: fields.from,
215
330
  recipient: fields.to,
216
- subject: fields.subject ?? ""
331
+ subject: fields.subject ?? "",
332
+ messageId: normalizeFrontmatterValue(fields.message_id) ?? normalizeFrontmatterValue(fields.messageId),
333
+ fromAddress: parseFrontmatterAddress(fields.from_address),
334
+ toAddress: parseFrontmatterAddress(fields.to_address),
335
+ scope: parseFrontmatterScope(fields.scope),
336
+ action: normalizeFrontmatterValue(fields.action),
337
+ consentRef: normalizeFrontmatterValue(fields.consent_ref) ?? normalizeFrontmatterValue(fields.consentRef),
338
+ validationError: normalizeFrontmatterValue(fields.pairToken) ?? normalizeFrontmatterValue(fields.pair_token) ? FORBIDDEN_RAW_PAIR_TOKEN_REASON : null
217
339
  };
218
340
  }
219
341
  function stripBridgeFrontmatter(content) {
@@ -236,23 +358,20 @@ function getInboxRouteFromFilename(fileName) {
236
358
  return {
237
359
  sender: parts[offset] ?? "",
238
360
  recipient: parts[offset + 1] ?? "",
239
- subject: parts.slice(offset + 2).join("-")
361
+ subject: parts.slice(offset + 2).join("-"),
362
+ validationError: null
240
363
  };
241
364
  }
242
365
 
243
366
  // scripts/bridge/bridge-config.ts
244
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3 } from "fs";
245
- import { isAbsolute as isAbsolute2, join as join3, resolve as resolve3 } from "path";
246
-
247
- // src/config/resolve.ts
248
- import * as fs from "fs";
249
- import * as path from "path";
250
- function normalizeTapPath(input) {
367
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "fs";
368
+ import { isAbsolute, join as join2, resolve as resolve2 } from "path";
369
+ function normalizeTapPath(input, platform = process.platform) {
251
370
  const trimmed = input.trim().replace(/^["'`]+|["'`]+$/g, "");
252
371
  if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
253
372
  return trimmed;
254
373
  }
255
- if (process.platform === "win32") {
374
+ if (platform === "win32") {
256
375
  const match = trimmed.match(/^\/([A-Za-z])\/(.*)$/);
257
376
  if (match) {
258
377
  return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
@@ -260,19 +379,17 @@ function normalizeTapPath(input) {
260
379
  }
261
380
  return trimmed;
262
381
  }
263
-
264
- // scripts/bridge/bridge-config.ts
265
382
  function ensureDir(target) {
266
- if (!existsSync3(target)) {
383
+ if (!existsSync2(target)) {
267
384
  mkdirSync(target, { recursive: true });
268
385
  }
269
- return resolve3(target);
386
+ return resolve2(target);
270
387
  }
271
388
  function printHelp() {
272
389
  console.log(`Codex App Server bridge
273
390
 
274
391
  Usage:
275
- node --experimental-strip-types scripts/codex-app-server-bridge.ts [options]
392
+ node --experimental-strip-types scripts/codex/codex-app-server-bridge.ts [options]
276
393
 
277
394
  Options:
278
395
  --repo-root=<path>
@@ -465,29 +582,33 @@ function parseArgs(argv) {
465
582
  }
466
583
  function resolveRepoRoot(explicit) {
467
584
  if (explicit) {
468
- return resolve3(explicit);
585
+ return resolve2(explicit);
469
586
  }
470
587
  return process.cwd();
471
588
  }
472
589
  function resolveTapConfigPath(repoRoot, input) {
473
590
  const converted = normalizeTapPath(input);
474
- return isAbsolute2(converted) ? resolve3(converted) : resolve3(repoRoot, converted);
591
+ return isAbsolute(converted) ? resolve2(converted) : resolve2(repoRoot, converted);
475
592
  }
476
593
  function resolveCommsDir(repoRoot, explicit) {
477
594
  if (explicit) {
478
- return resolve3(normalizeTapPath(explicit));
595
+ return resolve2(normalizeTapPath(explicit));
596
+ }
597
+ const envCommsDir = process.env.TAP_COMMS_DIR?.trim();
598
+ if (envCommsDir) {
599
+ return resolveTapConfigPath(repoRoot, envCommsDir);
479
600
  }
480
- const tapConfigPath = join3(repoRoot, ".tap-config");
481
- if (!existsSync3(tapConfigPath)) {
601
+ const tapConfigPath = join2(repoRoot, ".tap-config");
602
+ if (!existsSync2(tapConfigPath)) {
482
603
  throw new Error(
483
- "Unable to resolve comms directory. Pass --comms-dir explicitly."
604
+ "Unable to resolve comms directory. Pass --comms-dir or set TAP_COMMS_DIR."
484
605
  );
485
606
  }
486
- const configText = readFileSync3(tapConfigPath, "utf8");
607
+ const configText = readFileSync2(tapConfigPath, "utf8");
487
608
  const match = configText.match(/^TAP_COMMS_DIR="?(.*?)"?$/m);
488
609
  if (!match?.[1]) {
489
610
  throw new Error(
490
- "Unable to resolve comms directory. Pass --comms-dir explicitly."
611
+ "Unable to resolve comms directory. Pass --comms-dir or set TAP_COMMS_DIR."
491
612
  );
492
613
  }
493
614
  return resolveTapConfigPath(repoRoot, match[1]);
@@ -510,22 +631,33 @@ function sanitizeStateSegment(agentName) {
510
631
  }
511
632
  function buildDefaultStateDir(repoRoot, preferredAgentName) {
512
633
  const suffix = preferredAgentName?.trim() ? `-${sanitizeStateSegment(preferredAgentName)}` : "";
513
- return resolve3(join3(repoRoot, ".tmp", `codex-app-server-bridge${suffix}`));
634
+ return resolve2(join2(repoRoot, ".tmp", `codex-app-server-bridge${suffix}`));
514
635
  }
515
636
  function resolveStateDir(repoRoot, explicit, preferredAgentName) {
516
- const root = explicit ? resolve3(explicit) : buildDefaultStateDir(repoRoot, preferredAgentName);
637
+ const root = explicit ? resolve2(explicit) : buildDefaultStateDir(repoRoot, preferredAgentName);
517
638
  ensureDir(root);
518
- ensureDir(join3(root, "processed"));
519
- ensureDir(join3(root, "logs"));
639
+ ensureDir(join2(root, "processed"));
640
+ ensureDir(join2(root, "logs"));
520
641
  return root;
521
642
  }
522
643
  function readGatewayTokenFile(tokenFile) {
523
- const token = readFileSync3(tokenFile, "utf8").trim();
644
+ const token = readFileSync2(tokenFile, "utf8").trim();
524
645
  if (!token) {
525
646
  throw new Error(`Gateway token file is empty: ${tokenFile}`);
526
647
  }
527
648
  return token;
528
649
  }
650
+ function normalizeRoutingSlotEnv(value) {
651
+ const normalized = value?.trim().toLowerCase();
652
+ if (!normalized) return null;
653
+ if (normalized === "tower") return "tower";
654
+ if (normalized === "reviewer") return "reviewer";
655
+ const worktreeMatch = normalized.match(/^wt[-_]?(\d+)$/);
656
+ if (worktreeMatch) {
657
+ return `wt-${Number.parseInt(worktreeMatch[1], 10)}`;
658
+ }
659
+ return null;
660
+ }
529
661
  function buildOptions(argv) {
530
662
  const parsed = parseArgs(argv);
531
663
  const repoRoot = resolveRepoRoot(parsed.repoRoot);
@@ -541,6 +673,7 @@ function buildOptions(argv) {
541
673
  persistAgentName(stateDir, agentName);
542
674
  const gatewayTokenFile = parsed.gatewayTokenFile?.trim() || process.env.TAP_GATEWAY_TOKEN_FILE?.trim() || null;
543
675
  const appServerUrl = parsed.appServerUrl?.trim() || process.env.CODEX_APP_SERVER_URL || DEFAULT_APP_SERVER_URL;
676
+ const routingSlot = normalizeRoutingSlotEnv(process.env.TAP_ROUTING_SLOT);
544
677
  return {
545
678
  repoRoot,
546
679
  commsDir,
@@ -561,13 +694,20 @@ function buildOptions(argv) {
561
694
  busyMode: parsed.busyMode ?? "steer",
562
695
  logLevel: parsed.logLevel ?? "info",
563
696
  threadId: parsed.threadId?.trim() || null,
564
- ephemeral: parsed.ephemeral
697
+ ephemeral: parsed.ephemeral,
698
+ routingSlot
565
699
  };
566
700
  }
567
701
 
568
702
  // scripts/bridge/bridge-candidates.ts
569
703
  import { createHash } from "crypto";
570
- import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, statSync } from "fs";
704
+ import {
705
+ existsSync as existsSync3,
706
+ readFileSync as readFileSync3,
707
+ readdirSync,
708
+ statSync,
709
+ unlinkSync
710
+ } from "fs";
571
711
  import { join as join4 } from "path";
572
712
 
573
713
  // scripts/bridge/bridge-logging.ts
@@ -642,28 +782,192 @@ function createBridgeLogger(scope) {
642
782
  };
643
783
  }
644
784
 
645
- // scripts/bridge/bridge-candidates.ts
646
- var routingLogger = createBridgeLogger("routing");
647
- function buildMarkerId(filePath, mtimeMs) {
648
- return createHash("sha1").update(`${filePath}|${mtimeMs}`).digest("hex");
785
+ // scripts/bridge/bridge-format.ts
786
+ import { writeFileSync as writeFileSync2 } from "fs";
787
+ import { join as join3 } from "path";
788
+
789
+ // src/routing/tap-message-prompt.ts
790
+ function isValidReplyTarget(value) {
791
+ const normalized = value?.trim().toLowerCase();
792
+ return Boolean(
793
+ normalized && normalized !== "unknown" && normalized !== "unnamed" && normalized !== "null" && normalized !== "undefined" && normalized !== "?"
794
+ );
649
795
  }
650
- function getProcessedMarkerPath(stateDir, markerId) {
651
- return join4(stateDir, "processed", `${markerId}.done`);
796
+ function resolveReplyTarget(options) {
797
+ if (isValidReplyTarget(options.returnAddress?.routingAddress)) {
798
+ return options.returnAddress.routingAddress.trim();
799
+ }
800
+ if (isValidReplyTarget(options.replyTo)) {
801
+ return options.replyTo.trim();
802
+ }
803
+ return null;
652
804
  }
653
- function loadHeartbeats(commsDir) {
654
- try {
655
- return JSON.parse(readFileSync4(join4(commsDir, "heartbeats.json"), "utf8"));
656
- } catch {
657
- return {};
805
+ function formatReturnRoute(options) {
806
+ const address = options.returnAddress;
807
+ const parts = [];
808
+ if (isValidReplyTarget(address?.routingAddress)) {
809
+ parts.push(`routingAddress=${address.routingAddress.trim()}`);
810
+ }
811
+ if (address?.hostId?.trim()) parts.push(`hostId=${address.hostId.trim()}`);
812
+ if (options.runtimeSurface?.trim()) {
813
+ parts.push(`runtimeSurface=${options.runtimeSurface.trim()}`);
658
814
  }
815
+ if (address?.clientId?.trim()) {
816
+ parts.push(`clientId=${address.clientId.trim()}`);
817
+ }
818
+ if (address?.conversationId?.trim()) {
819
+ parts.push(`conversationId=${address.conversationId.trim()}`);
820
+ }
821
+ if (address?.ownerClientId?.trim()) {
822
+ parts.push(`ownerClientId=${address.ownerClientId.trim()}`);
823
+ }
824
+ if (address?.surfaceInstanceId?.trim()) {
825
+ parts.push(`surfaceInstanceId=${address.surfaceInstanceId.trim()}`);
826
+ }
827
+ return parts.length ? parts.join("; ") : null;
659
828
  }
660
- function shouldSkipInHeadlessMode(fileName, body) {
661
- if (process.env.TAP_HEADLESS !== "true") return false;
662
- const combined = `${fileName}
663
- ${body}`;
664
- return HEADLESS_SKIP_PATTERNS.some((p) => p.test(combined));
829
+ function createTapMessageViewModel(options) {
830
+ const body = options.body.trim();
831
+ const replyTo = resolveReplyTarget(options);
832
+ const returnRoute = formatReturnRoute(options);
833
+ return {
834
+ agentName: options.agentName,
835
+ sender: options.sender,
836
+ recipient: options.recipient,
837
+ subject: options.subject,
838
+ body: body || "(empty)",
839
+ replyTarget: replyTo,
840
+ returnRoute,
841
+ missingRoute: !replyTo,
842
+ debugEnvelope: {
843
+ fileName: options.fileName,
844
+ returnAddress: options.returnAddress ?? null,
845
+ runtimeSurface: options.runtimeSurface ?? null
846
+ }
847
+ };
665
848
  }
666
- function collectCandidates(inboxDir, agentId, agentName, aliasName) {
849
+ function renderDebugEnvelope(viewModel) {
850
+ const address = viewModel.debugEnvelope.returnAddress;
851
+ const lines = [
852
+ "",
853
+ "Debug envelope:",
854
+ `- file: ${viewModel.debugEnvelope.fileName}`
855
+ ];
856
+ if (viewModel.replyTarget) {
857
+ lines.push(
858
+ `- replyInstruction: Use tap_reply(to: "${viewModel.replyTarget}", subject: "<your-subject>", content: "<your-response>").`
859
+ );
860
+ } else {
861
+ lines.push("- replyInstruction: unavailable; do not reply to unknown");
862
+ }
863
+ if (viewModel.returnRoute) {
864
+ lines.push(`- returnRoute: ${viewModel.returnRoute}`);
865
+ }
866
+ if (viewModel.debugEnvelope.runtimeSurface?.trim()) {
867
+ lines.push(
868
+ `- runtimeSurface: ${viewModel.debugEnvelope.runtimeSurface.trim()}`
869
+ );
870
+ }
871
+ if (address?.aliases?.length) {
872
+ lines.push(`- aliases: ${address.aliases.join(", ")}`);
873
+ }
874
+ return lines;
875
+ }
876
+ function renderAgentMessagePrompt(viewModel, options = {}) {
877
+ const replyInstructions = viewModel.replyTarget ? ["Reply:", `Reply available: ${viewModel.replyTarget}`] : [
878
+ "Reply:",
879
+ "Reply unavailable: no verified return route.",
880
+ "No valid structured return route was provided; `unknown` is not a valid reply target.",
881
+ "Preserve durable inbox evidence or ask tower/operator for a valid return route before replying.",
882
+ "If the message is a review request, perform the review locally and report that the return route is missing.",
883
+ 'Do not reply to "unknown".'
884
+ ];
885
+ const lines = [
886
+ `Tap message for ${viewModel.agentName}`,
887
+ `From: ${viewModel.sender}`,
888
+ `To: ${viewModel.recipient}`,
889
+ `Subject: ${viewModel.subject}`,
890
+ "",
891
+ "Message:",
892
+ viewModel.body,
893
+ "",
894
+ ...replyInstructions
895
+ ];
896
+ if (options.debugEnvelope) {
897
+ lines.push(...renderDebugEnvelope(viewModel));
898
+ }
899
+ return lines.join("\n");
900
+ }
901
+ function buildTapMessagePrompt(options) {
902
+ return renderAgentMessagePrompt(createTapMessageViewModel(options), {
903
+ debugEnvelope: options.debugEnvelope
904
+ });
905
+ }
906
+
907
+ // scripts/bridge/bridge-format.ts
908
+ function buildUserInput(candidate, agentName, heartbeats) {
909
+ const sender = resolveAddressLabel(candidate.sender || "unknown", heartbeats);
910
+ const recipient = resolveAddressLabel(
911
+ candidate.recipient || agentName,
912
+ heartbeats
913
+ );
914
+ const subject = candidate.subject || "(none)";
915
+ return buildTapMessagePrompt({
916
+ agentName,
917
+ sender,
918
+ recipient,
919
+ subject,
920
+ fileName: candidate.fileName,
921
+ body: candidate.body,
922
+ replyTo: candidate.sender || "unknown",
923
+ returnAddress: candidate.fromAddress
924
+ });
925
+ }
926
+ function writeProcessedMarker(stateDir, candidate, dispatchMode, threadId, turnId, blockedReason) {
927
+ const payload = {
928
+ requestFile: candidate.filePath,
929
+ requestName: candidate.fileName,
930
+ sender: candidate.sender,
931
+ recipient: candidate.recipient,
932
+ subject: candidate.subject,
933
+ dispatchMode,
934
+ threadId,
935
+ turnId,
936
+ blockedReason: blockedReason?.trim() || null,
937
+ markedAt: (/* @__PURE__ */ new Date()).toISOString()
938
+ };
939
+ writeFileSync2(
940
+ getProcessedMarkerPath(stateDir, candidate.markerId),
941
+ `${JSON.stringify(payload, null, 2)}
942
+ `,
943
+ "utf8"
944
+ );
945
+ }
946
+ function writeLastDispatch(stateDir, candidate, dispatchMode, threadId, turnId, blockedReason) {
947
+ const payload = {
948
+ requestFile: candidate.filePath,
949
+ requestName: candidate.fileName,
950
+ markerId: candidate.markerId,
951
+ sender: candidate.sender,
952
+ recipient: candidate.recipient,
953
+ subject: candidate.subject,
954
+ dispatchMode,
955
+ threadId,
956
+ turnId,
957
+ blockedReason: blockedReason?.trim() || null,
958
+ dispatchedAt: (/* @__PURE__ */ new Date()).toISOString()
959
+ };
960
+ writeFileSync2(
961
+ join3(stateDir, "last-dispatch.json"),
962
+ `${JSON.stringify(payload, null, 2)}
963
+ `,
964
+ "utf8"
965
+ );
966
+ }
967
+
968
+ // scripts/bridge/bridge-candidates.ts
969
+ var routingLogger = createBridgeLogger("routing");
970
+ function scanCandidates(inboxDir, agentId, agentName, aliasName) {
667
971
  const entries = readdirSync(inboxDir, { withFileTypes: true }).filter(
668
972
  (entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md")
669
973
  ).map((entry) => {
@@ -672,13 +976,14 @@ function collectCandidates(inboxDir, agentId, agentName, aliasName) {
672
976
  return { entry, filePath, stats };
673
977
  }).sort((left, right) => left.stats.mtimeMs - right.stats.mtimeMs);
674
978
  const candidates = [];
979
+ const rejected = [];
675
980
  let filteredByRecipient = 0;
676
981
  let filteredBySelf = 0;
677
982
  let filteredByHeadless = 0;
678
983
  for (const item of entries) {
679
984
  let body;
680
985
  try {
681
- body = readFileSync4(item.filePath, "utf8");
986
+ body = readFileSync3(item.filePath, "utf8");
682
987
  } catch {
683
988
  continue;
684
989
  }
@@ -695,21 +1000,42 @@ function collectCandidates(inboxDir, agentId, agentName, aliasName) {
695
1000
  filteredByHeadless += 1;
696
1001
  continue;
697
1002
  }
1003
+ const markerId = buildMarkerId(item.filePath, item.stats.mtimeMs);
1004
+ if (route.validationError) {
1005
+ rejected.push({
1006
+ markerId,
1007
+ filePath: item.filePath,
1008
+ fileName: item.entry.name,
1009
+ sender: route.sender,
1010
+ recipient: route.recipient,
1011
+ subject: route.subject,
1012
+ mtimeMs: item.stats.mtimeMs,
1013
+ rejectionReason: route.validationError
1014
+ });
1015
+ continue;
1016
+ }
698
1017
  candidates.push({
699
- markerId: buildMarkerId(item.filePath, item.stats.mtimeMs),
1018
+ markerId,
700
1019
  filePath: item.filePath,
701
1020
  fileName: item.entry.name,
702
1021
  sender: route.sender,
703
1022
  recipient: route.recipient,
704
1023
  subject: route.subject,
705
1024
  body: stripBridgeFrontmatter(body),
706
- mtimeMs: item.stats.mtimeMs
1025
+ mtimeMs: item.stats.mtimeMs,
1026
+ messageId: route.messageId ?? null,
1027
+ fromAddress: route.fromAddress ?? null,
1028
+ toAddress: route.toAddress ?? null,
1029
+ scope: route.scope ?? null,
1030
+ action: route.action ?? null,
1031
+ consentRef: route.consentRef ?? null
707
1032
  });
708
1033
  }
709
1034
  routingLogger.debug("candidate scan completed", {
710
1035
  inboxDir,
711
1036
  scanned: entries.length,
712
1037
  matched: candidates.length,
1038
+ rejected: rejected.length,
713
1039
  filteredByRecipient,
714
1040
  filteredBySelf,
715
1041
  filteredByHeadless,
@@ -717,27 +1043,148 @@ function collectCandidates(inboxDir, agentId, agentName, aliasName) {
717
1043
  agentName,
718
1044
  aliasName
719
1045
  });
720
- return candidates;
1046
+ return { candidates, rejected };
1047
+ }
1048
+ function buildMarkerId(filePath, mtimeMs) {
1049
+ return createHash("sha1").update(`${filePath}|${mtimeMs}`).digest("hex");
1050
+ }
1051
+ function getProcessedMarkerPath(stateDir, markerId) {
1052
+ return join4(stateDir, "processed", `${markerId}.done`);
1053
+ }
1054
+ var PROCESSED_MARKER_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
1055
+ var PROCESSED_MARKER_GRACE_MS = 1e4;
1056
+ function sweepOrphanProcessedMarkers(stateDir, options) {
1057
+ const result = {
1058
+ scanned: 0,
1059
+ removed: 0,
1060
+ kept: 0,
1061
+ errors: 0,
1062
+ removedMarkerIds: []
1063
+ };
1064
+ const dir = join4(stateDir, "processed");
1065
+ if (!existsSync3(dir)) {
1066
+ return result;
1067
+ }
1068
+ const now = options?.nowMs ?? Date.now();
1069
+ const maxAge = options?.maxAgeMs ?? PROCESSED_MARKER_MAX_AGE_MS;
1070
+ const grace = options?.graceMs ?? PROCESSED_MARKER_GRACE_MS;
1071
+ const log = options?.logger ?? (() => void 0);
1072
+ let entries;
1073
+ try {
1074
+ entries = readdirSync(dir);
1075
+ } catch {
1076
+ return result;
1077
+ }
1078
+ for (const file of entries) {
1079
+ if (!file.endsWith(".done")) {
1080
+ continue;
1081
+ }
1082
+ result.scanned += 1;
1083
+ const markerPath = join4(dir, file);
1084
+ let markerMtimeMs = 0;
1085
+ let sourcePath = null;
1086
+ try {
1087
+ markerMtimeMs = statSync(markerPath).mtimeMs;
1088
+ const payload = JSON.parse(readFileSync3(markerPath, "utf8"));
1089
+ sourcePath = typeof payload.requestFile === "string" && payload.requestFile.trim() ? payload.requestFile : null;
1090
+ } catch {
1091
+ result.errors += 1;
1092
+ continue;
1093
+ }
1094
+ const ageMs = now - markerMtimeMs;
1095
+ if (ageMs < grace) {
1096
+ result.kept += 1;
1097
+ continue;
1098
+ }
1099
+ const sourceExists = sourcePath ? existsSync3(sourcePath) : false;
1100
+ const agedOut = ageMs > maxAge;
1101
+ if (sourceExists && !agedOut) {
1102
+ result.kept += 1;
1103
+ continue;
1104
+ }
1105
+ try {
1106
+ unlinkSync(markerPath);
1107
+ result.removed += 1;
1108
+ const markerId = file.slice(0, -".done".length);
1109
+ result.removedMarkerIds.push(markerId);
1110
+ log("processed marker retired", {
1111
+ markerId,
1112
+ reason: !sourceExists ? "source_missing" : "aged_out",
1113
+ sourcePath,
1114
+ ageMs
1115
+ });
1116
+ } catch {
1117
+ result.errors += 1;
1118
+ }
1119
+ }
1120
+ return result;
1121
+ }
1122
+ function loadHeartbeats(commsDir) {
1123
+ try {
1124
+ return JSON.parse(readFileSync3(join4(commsDir, "heartbeats.json"), "utf8"));
1125
+ } catch {
1126
+ return {};
1127
+ }
1128
+ }
1129
+ function shouldSkipInHeadlessMode(fileName, body) {
1130
+ if (process.env.TAP_HEADLESS !== "true") return false;
1131
+ const combined = `${fileName}
1132
+ ${body}`;
1133
+ return HEADLESS_SKIP_PATTERNS.some((p) => p.test(combined));
1134
+ }
1135
+ function collectCandidates(inboxDir, agentId, agentName, aliasName) {
1136
+ return scanCandidates(inboxDir, agentId, agentName, aliasName).candidates;
721
1137
  }
722
1138
  function getPendingCandidates(options, cutoff) {
723
1139
  const inboxDir = join4(options.commsDir, "inbox");
724
- if (!existsSync4(inboxDir)) {
1140
+ if (!existsSync3(inboxDir)) {
725
1141
  throw new Error(`Inbox directory not found: ${inboxDir}`);
726
1142
  }
727
1143
  const heartbeats = loadHeartbeats(options.commsDir);
728
1144
  const refreshedName = refreshAgentIdentity(options, heartbeats);
729
1145
  const cutoffMs = cutoff.getTime();
730
- const candidates = collectCandidates(
1146
+ const scan = scanCandidates(
731
1147
  inboxDir,
732
1148
  options.agentId,
733
1149
  options.agentName,
734
1150
  // M205: Also accept messages addressed to the heartbeat-refreshed name
735
1151
  refreshedName !== options.agentName ? refreshedName : void 0
736
- ).filter((candidate) => {
1152
+ );
1153
+ for (const rejection of scan.rejected) {
1154
+ if (rejection.mtimeMs < cutoffMs) {
1155
+ continue;
1156
+ }
1157
+ const markerPath = getProcessedMarkerPath(
1158
+ options.stateDir,
1159
+ rejection.markerId
1160
+ );
1161
+ if (existsSync3(markerPath)) {
1162
+ continue;
1163
+ }
1164
+ writeProcessedMarker(
1165
+ options.stateDir,
1166
+ {
1167
+ ...rejection,
1168
+ body: ""
1169
+ },
1170
+ "rejected",
1171
+ null,
1172
+ null,
1173
+ rejection.rejectionReason
1174
+ );
1175
+ routingLogger.warn("envelope rejected during candidate scan", {
1176
+ fileName: rejection.fileName,
1177
+ sender: rejection.sender || "unknown",
1178
+ recipient: rejection.recipient || options.agentName,
1179
+ subject: rejection.subject || "(none)",
1180
+ reason: rejection.rejectionReason
1181
+ });
1182
+ }
1183
+ const candidates = scan.candidates.filter((candidate) => {
737
1184
  if (candidate.mtimeMs < cutoffMs) {
738
1185
  return false;
739
1186
  }
740
- return !existsSync4(
1187
+ return !existsSync3(
741
1188
  getProcessedMarkerPath(options.stateDir, candidate.markerId)
742
1189
  );
743
1190
  });
@@ -746,92 +1193,2209 @@ function getPendingCandidates(options, cutoff) {
746
1193
  configuredName: options.agentName,
747
1194
  refreshedName: refreshedName !== options.agentName ? refreshedName : void 0,
748
1195
  candidateCount: candidates.length,
1196
+ rejectedCount: scan.rejected.length,
749
1197
  cutoff: cutoff.toISOString()
750
1198
  });
751
- return { heartbeats, candidates };
1199
+ return { heartbeats, candidates };
1200
+ }
1201
+
1202
+ // scripts/bridge/bridge-elicitation.ts
1203
+ function hasObjectShape(value) {
1204
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1205
+ }
1206
+ function isElicitationParams(value) {
1207
+ if (!hasObjectShape(value)) {
1208
+ return false;
1209
+ }
1210
+ return "requestedSchema" in value || "mode" in value || "url" in value;
1211
+ }
1212
+ function resolveElicitationParams(raw) {
1213
+ if (!hasObjectShape(raw)) {
1214
+ return null;
1215
+ }
1216
+ const queue = [raw];
1217
+ const visited = /* @__PURE__ */ new Set();
1218
+ while (queue.length > 0) {
1219
+ const candidate = queue.shift();
1220
+ if (candidate == null || visited.has(candidate)) {
1221
+ continue;
1222
+ }
1223
+ visited.add(candidate);
1224
+ if (isElicitationParams(candidate)) {
1225
+ return candidate;
1226
+ }
1227
+ if (!hasObjectShape(candidate)) {
1228
+ continue;
1229
+ }
1230
+ for (const key of ["params", "request", "payload", "elicitation"]) {
1231
+ const nested = candidate[key];
1232
+ if (nested != null) {
1233
+ queue.push(nested);
1234
+ }
1235
+ }
1236
+ }
1237
+ return null;
1238
+ }
1239
+ function firstEnumValue(values) {
1240
+ if (!Array.isArray(values)) {
1241
+ return void 0;
1242
+ }
1243
+ for (const entry of values) {
1244
+ if (typeof entry === "string") {
1245
+ return entry;
1246
+ }
1247
+ if (hasObjectShape(entry) && typeof entry.const === "string") {
1248
+ return entry.const;
1249
+ }
1250
+ }
1251
+ return void 0;
1252
+ }
1253
+ function buildRequiredStringValue(schema) {
1254
+ return typeof schema.title === "string" && schema.title.trim() ? schema.title.trim() : "approved";
1255
+ }
1256
+ function buildElicitationFieldValue(schema, required) {
1257
+ const defaultValue = schema.default;
1258
+ if (typeof defaultValue === "string" || typeof defaultValue === "number" || typeof defaultValue === "boolean") {
1259
+ return defaultValue;
1260
+ }
1261
+ if (Array.isArray(defaultValue) && defaultValue.every((entry) => typeof entry === "string")) {
1262
+ return defaultValue;
1263
+ }
1264
+ const type = typeof schema.type === "string" ? schema.type : null;
1265
+ if (type === "boolean") {
1266
+ return true;
1267
+ }
1268
+ if (type === "number" || type === "integer") {
1269
+ return typeof schema.minimum === "number" ? schema.minimum : 0;
1270
+ }
1271
+ if (type === "string") {
1272
+ return firstEnumValue(schema.enum) ?? firstEnumValue(schema.anyOf) ?? (required ? buildRequiredStringValue(schema) : "");
1273
+ }
1274
+ if (type === "array") {
1275
+ const minItems = typeof schema.minItems === "number" ? schema.minItems : void 0;
1276
+ const itemSchema = hasObjectShape(schema.items) ? schema.items : {};
1277
+ const itemValue = firstEnumValue(itemSchema.enum) ?? firstEnumValue(itemSchema.anyOf);
1278
+ if (itemValue) {
1279
+ return required || (minItems ?? 0) > 0 ? [itemValue] : [];
1280
+ }
1281
+ return [];
1282
+ }
1283
+ return firstEnumValue(schema.enum) ?? firstEnumValue(schema.anyOf) ?? void 0;
1284
+ }
1285
+ function isAutoElicitationRequestMethod(method) {
1286
+ return method === "elicitation/create" || method === "mcpServer/elicitation/request";
1287
+ }
1288
+ function buildAutoElicitationResult(rawParams) {
1289
+ const params = resolveElicitationParams(rawParams);
1290
+ if (!params) {
1291
+ return null;
1292
+ }
1293
+ if (params.mode === "url" || typeof params.url === "string") {
1294
+ return { action: "cancel" };
1295
+ }
1296
+ const requestedSchema = hasObjectShape(params.requestedSchema) ? params.requestedSchema : null;
1297
+ if (!requestedSchema) {
1298
+ return { action: "accept" };
1299
+ }
1300
+ const properties = hasObjectShape(requestedSchema.properties) ? requestedSchema.properties : {};
1301
+ const required = new Set(
1302
+ Array.isArray(requestedSchema.required) ? requestedSchema.required.filter(
1303
+ (entry) => typeof entry === "string"
1304
+ ) : []
1305
+ );
1306
+ const content = {};
1307
+ for (const [field, schema] of Object.entries(properties)) {
1308
+ if (!hasObjectShape(schema)) {
1309
+ continue;
1310
+ }
1311
+ const value = buildElicitationFieldValue(
1312
+ schema,
1313
+ required.has(field)
1314
+ );
1315
+ if (value !== void 0) {
1316
+ content[field] = value;
1317
+ }
1318
+ }
1319
+ return Object.keys(content).length > 0 ? { action: "accept", content } : { action: "accept" };
1320
+ }
1321
+
1322
+ // scripts/bridge/bridge-dispatch.ts
1323
+ import { randomUUID as randomUUID5 } from "crypto";
1324
+ import {
1325
+ existsSync as existsSync6,
1326
+ mkdirSync as mkdirSync4,
1327
+ readFileSync as readFileSync5,
1328
+ renameSync as renameSync2,
1329
+ statSync as statSync3,
1330
+ unlinkSync as unlinkSync2,
1331
+ writeFileSync as writeFileSync5
1332
+ } from "fs";
1333
+ import { join as join7 } from "path";
1334
+
1335
+ // src/transport/experimental/codex-ipc-control.ts
1336
+ import { randomUUID as randomUUID4 } from "crypto";
1337
+
1338
+ // src/transport/consent.ts
1339
+ import { createHash as createHash2, randomBytes, randomUUID } from "crypto";
1340
+ import { execFileSync } from "child_process";
1341
+ import * as fs from "fs";
1342
+ import * as os from "os";
1343
+ import * as path from "path";
1344
+ var CONSENT_RECEIPTS_DIRNAME = "tap-codex-a2a-consent";
1345
+ var CONSENT_SECRETS_DIRNAME = "tap-codex-a2a-consent-secrets";
1346
+ var DEFAULT_CONSENT_TTL_SECONDS = 10 * 60;
1347
+ var CONSENT_METADATA_DRIFT_TOLERANCE_MS = 5e3;
1348
+ var CONSENT_RESERVATION_TTL_MS = 3e4;
1349
+ var pendingConsentReservations = /* @__PURE__ */ new Set();
1350
+ var SCOPE_PRIORITY = {
1351
+ observe: 1,
1352
+ suggest: 2,
1353
+ drive: 3
1354
+ };
1355
+ var ConsentReceiptError = class extends Error {
1356
+ constructor(code, message) {
1357
+ super(message);
1358
+ this.code = code;
1359
+ this.name = "ConsentReceiptError";
1360
+ }
1361
+ code;
1362
+ };
1363
+ function normalizeString(value) {
1364
+ const normalized = value?.trim();
1365
+ return normalized ? normalized : null;
1366
+ }
1367
+ function assertPendingReservationAvailable(consentRef) {
1368
+ if (!pendingConsentReservations.has(consentRef)) {
1369
+ return;
1370
+ }
1371
+ throw new ConsentReceiptError(
1372
+ "missing",
1373
+ `Consent receipt "${consentRef}" is already reserved or consumed.`
1374
+ );
1375
+ }
1376
+ function markPendingReservation(consentRef) {
1377
+ pendingConsentReservations.add(consentRef);
1378
+ }
1379
+ function clearPendingReservation(consentRef) {
1380
+ pendingConsentReservations.delete(consentRef);
1381
+ }
1382
+ function normalizeMethods(values) {
1383
+ const methods = /* @__PURE__ */ new Set();
1384
+ for (const value of values ?? []) {
1385
+ const normalized = value.trim();
1386
+ if (!normalized) continue;
1387
+ methods.add(normalized);
1388
+ }
1389
+ return [...methods].sort();
1390
+ }
1391
+ function normalizePathForComparison(value) {
1392
+ return path.resolve(value).replace(/\\/g, "/").toLowerCase();
1393
+ }
1394
+ function resolveReceiptsDir(explicitDir) {
1395
+ const configuredDir = explicitDir?.trim() || process.env.TAP_CONSENT_RECEIPTS_DIR?.trim();
1396
+ return configuredDir ? path.resolve(configuredDir) : path.join(os.tmpdir(), CONSENT_RECEIPTS_DIRNAME);
1397
+ }
1398
+ function resolveSecretsDir(explicitDir) {
1399
+ const configuredDir = explicitDir?.trim() || process.env.TAP_CONSENT_SECRETS_DIR?.trim();
1400
+ return configuredDir ? path.resolve(configuredDir) : path.join(os.tmpdir(), CONSENT_SECRETS_DIRNAME);
1401
+ }
1402
+ function resolveConsentDirs(options) {
1403
+ const receiptsDir = resolveReceiptsDir(options.receiptsDir);
1404
+ const secretsDir = resolveSecretsDir(options.secretsDir);
1405
+ if (normalizePathForComparison(receiptsDir) === normalizePathForComparison(secretsDir)) {
1406
+ throw new ConsentReceiptError(
1407
+ "invalid",
1408
+ "Consent receipts dir and secrets dir must be different paths."
1409
+ );
1410
+ }
1411
+ return { receiptsDir, secretsDir };
1412
+ }
1413
+ function hashPairTokenBinding(options) {
1414
+ return createHash2("sha256").update(
1415
+ [
1416
+ options.pairToken,
1417
+ options.hostId ?? "",
1418
+ options.conversationId,
1419
+ options.ownerClientId ?? ""
1420
+ ].join("\0"),
1421
+ "utf-8"
1422
+ ).digest("hex");
1423
+ }
1424
+ function readUtf8PreservingTimes(filePath) {
1425
+ const originalStats = fs.statSync(filePath);
1426
+ const contents = fs.readFileSync(filePath, "utf-8");
1427
+ try {
1428
+ fs.utimesSync(filePath, originalStats.atime, originalStats.mtime);
1429
+ } catch {
1430
+ }
1431
+ return contents;
1432
+ }
1433
+ function loadConsentReceipt(filePath) {
1434
+ try {
1435
+ const parsed = JSON.parse(
1436
+ readUtf8PreservingTimes(filePath)
1437
+ );
1438
+ if (typeof parsed.id !== "string" || typeof parsed.scope !== "string" || typeof parsed.conversationId !== "string" || typeof parsed.pairTokenHash !== "string" || typeof parsed.createdAt !== "string" || typeof parsed.expiresAt !== "string") {
1439
+ return null;
1440
+ }
1441
+ if (parsed.scope !== "observe" && parsed.scope !== "suggest" && parsed.scope !== "drive") {
1442
+ return null;
1443
+ }
1444
+ return {
1445
+ id: parsed.id,
1446
+ scope: parsed.scope,
1447
+ hostId: normalizeString(parsed.hostId),
1448
+ conversationId: parsed.conversationId,
1449
+ ownerClientId: normalizeString(parsed.ownerClientId),
1450
+ issuedByClientId: normalizeString(parsed.issuedByClientId),
1451
+ allowedMethods: normalizeMethods(parsed.allowedMethods),
1452
+ pairTokenHash: parsed.pairTokenHash,
1453
+ createdAt: parsed.createdAt,
1454
+ expiresAt: parsed.expiresAt
1455
+ };
1456
+ } catch {
1457
+ return null;
1458
+ }
1459
+ }
1460
+ function loadReservedReceiptRecord(filePath) {
1461
+ try {
1462
+ const parsed = JSON.parse(readUtf8PreservingTimes(filePath));
1463
+ return {
1464
+ receipt: loadConsentReceipt(filePath),
1465
+ reservationOwnerId: normalizeString(parsed.reservationOwnerId)
1466
+ };
1467
+ } catch {
1468
+ return {
1469
+ receipt: null,
1470
+ reservationOwnerId: null
1471
+ };
1472
+ }
1473
+ }
1474
+ function isExpired(receipt, now) {
1475
+ const expiresAtMs = new Date(receipt.expiresAt).getTime();
1476
+ return Number.isNaN(expiresAtMs) || expiresAtMs <= now.getTime();
1477
+ }
1478
+ function resolveSecretPath(secretsDir, receiptId) {
1479
+ return path.join(secretsDir, `${receiptId}.token`);
1480
+ }
1481
+ function resolveReservedReceiptPath(receiptsDir, receiptId) {
1482
+ return path.join(receiptsDir, `${receiptId}.reserved.json`);
1483
+ }
1484
+ function extractReceiptIdFromPath(filePath) {
1485
+ return path.basename(filePath).replace(/(?:\.reserved)?\.json$/i, "");
1486
+ }
1487
+ function isReceiptPath(fileName) {
1488
+ return /\.json$/i.test(fileName);
1489
+ }
1490
+ function resolveWindowsAclPrincipals() {
1491
+ const username = process.env.USERNAME?.trim();
1492
+ if (!username) return [];
1493
+ const principals = /* @__PURE__ */ new Set();
1494
+ const userDomain = process.env.USERDOMAIN?.trim();
1495
+ if (userDomain) {
1496
+ principals.add(`${userDomain}\\${username}`);
1497
+ }
1498
+ principals.add(username);
1499
+ return [...principals];
1500
+ }
1501
+ function applyWindowsPrivateAcl(targetPath) {
1502
+ if (process.platform !== "win32") return;
1503
+ const principals = resolveWindowsAclPrincipals();
1504
+ if (principals.length === 0) {
1505
+ throw new ConsentReceiptError(
1506
+ "invalid",
1507
+ `Unable to resolve a Windows principal for "${path.basename(targetPath)}".`
1508
+ );
1509
+ }
1510
+ let lastError = null;
1511
+ for (const principal of principals) {
1512
+ try {
1513
+ execFileSync(
1514
+ "icacls",
1515
+ [targetPath, "/inheritance:r", "/grant:r", `${principal}:F`],
1516
+ {
1517
+ stdio: "pipe",
1518
+ windowsHide: true
1519
+ }
1520
+ );
1521
+ return;
1522
+ } catch (error) {
1523
+ lastError = error;
1524
+ }
1525
+ }
1526
+ throw new ConsentReceiptError(
1527
+ "invalid",
1528
+ `Failed to apply Windows ACL hardening to "${path.basename(targetPath)}": ${lastError instanceof Error ? lastError.message : String(lastError)}`
1529
+ );
1530
+ }
1531
+ function hardenSecretStorePath(targetPath, mode) {
1532
+ try {
1533
+ fs.chmodSync(targetPath, mode);
1534
+ } catch {
1535
+ }
1536
+ applyWindowsPrivateAcl(targetPath);
1537
+ }
1538
+ function hasTimestampDrift(stats, mintedAtMs) {
1539
+ if (!Number.isFinite(mintedAtMs)) {
1540
+ return false;
1541
+ }
1542
+ return Math.abs(stats.mtimeMs - mintedAtMs) > CONSENT_METADATA_DRIFT_TOLERANCE_MS || Math.abs(stats.atimeMs - mintedAtMs) > CONSENT_METADATA_DRIFT_TOLERANCE_MS;
1543
+ }
1544
+ function stampMintedAt(targetPath, mintedAt) {
1545
+ fs.utimesSync(targetPath, mintedAt, mintedAt);
1546
+ }
1547
+ function stampReservationAt(targetPath, reservedAt) {
1548
+ fs.utimesSync(targetPath, reservedAt, reservedAt);
1549
+ }
1550
+ function resolveReceiptCreatedAtMs(receipt) {
1551
+ const createdAtMs = new Date(receipt.createdAt).getTime();
1552
+ if (Number.isNaN(createdAtMs)) {
1553
+ throw new ConsentReceiptError(
1554
+ "invalid",
1555
+ `Consent receipt "${receipt.id}" has an invalid createdAt timestamp.`
1556
+ );
1557
+ }
1558
+ return createdAtMs;
1559
+ }
1560
+ function resolveReceiptCreatedAt(receipt) {
1561
+ return new Date(resolveReceiptCreatedAtMs(receipt));
1562
+ }
1563
+ function isReservationExpired(stats, now) {
1564
+ return now.getTime() - stats.mtimeMs > CONSENT_RESERVATION_TTL_MS;
1565
+ }
1566
+ function assertUntamperedConsentPath(stats, receipt, label) {
1567
+ if (!hasTimestampDrift(stats, resolveReceiptCreatedAtMs(receipt))) {
1568
+ return;
1569
+ }
1570
+ throw new ConsentReceiptError(
1571
+ "invalid",
1572
+ `Consent ${label} "${receipt.id}" showed timestamp drift after mint.`
1573
+ );
1574
+ }
1575
+ function removeSecretPath(secretPath) {
1576
+ try {
1577
+ fs.rmSync(secretPath, { force: true });
1578
+ } catch {
1579
+ }
1580
+ }
1581
+ function removeReceiptPath(receiptPath) {
1582
+ try {
1583
+ fs.rmSync(receiptPath, { force: true });
1584
+ } catch {
1585
+ }
1586
+ }
1587
+ function writeActiveReceiptFile(filePath, receipt) {
1588
+ fs.writeFileSync(filePath, JSON.stringify(receipt, null, 2), "utf-8");
1589
+ stampMintedAt(filePath, resolveReceiptCreatedAt(receipt));
1590
+ }
1591
+ function writeReservedReceiptFile(filePath, receipt, reservationOwnerId, reservedAt) {
1592
+ fs.writeFileSync(
1593
+ filePath,
1594
+ JSON.stringify(
1595
+ {
1596
+ ...receipt,
1597
+ reservationOwnerId
1598
+ },
1599
+ null,
1600
+ 2
1601
+ ),
1602
+ "utf-8"
1603
+ );
1604
+ stampReservationAt(filePath, reservedAt);
1605
+ }
1606
+ function cleanupExpiredReceipts(receiptsDir, secretsDir, now) {
1607
+ if (!fs.existsSync(receiptsDir)) return;
1608
+ for (const entry of fs.readdirSync(receiptsDir, { withFileTypes: true })) {
1609
+ if (!entry.isFile() || !isReceiptPath(entry.name)) continue;
1610
+ const filePath = path.join(receiptsDir, entry.name);
1611
+ const receipt = loadConsentReceipt(filePath);
1612
+ const receiptId = receipt?.id ?? extractReceiptIdFromPath(filePath);
1613
+ if (!receipt || isExpired(receipt, now)) {
1614
+ removeReceiptPath(filePath);
1615
+ removeSecretPath(resolveSecretPath(secretsDir, receiptId));
1616
+ }
1617
+ }
1618
+ }
1619
+ function listReceiptPaths(receiptsDir) {
1620
+ if (!fs.existsSync(receiptsDir)) return [];
1621
+ return fs.readdirSync(receiptsDir, { withFileTypes: true }).filter(
1622
+ (entry) => entry.isFile() && entry.name.endsWith(".json") && !entry.name.endsWith(".reserved.json")
1623
+ ).map((entry) => path.join(receiptsDir, entry.name)).sort();
1624
+ }
1625
+ function scopeSatisfies(actual, required) {
1626
+ return SCOPE_PRIORITY[actual] >= SCOPE_PRIORITY[required];
1627
+ }
1628
+ function resolveReceiptPath(receiptsDir, consentRef) {
1629
+ const normalizedConsentRef = normalizeString(consentRef);
1630
+ if (!normalizedConsentRef) return null;
1631
+ return path.join(receiptsDir, `${normalizedConsentRef}.json`);
1632
+ }
1633
+ function reserveReceiptPath(filePath, receipt, reservationOwnerId, now) {
1634
+ const reservedPath = resolveReservedReceiptPath(
1635
+ path.dirname(filePath),
1636
+ receipt.id
1637
+ );
1638
+ try {
1639
+ fs.renameSync(filePath, reservedPath);
1640
+ } catch (error) {
1641
+ if (error.code === "ENOENT") {
1642
+ throw new ConsentReceiptError(
1643
+ "missing",
1644
+ `Consent receipt "${receipt.id}" is already reserved or consumed.`
1645
+ );
1646
+ }
1647
+ throw error;
1648
+ }
1649
+ writeReservedReceiptFile(reservedPath, receipt, reservationOwnerId, now);
1650
+ return reservedPath;
1651
+ }
1652
+ function mintPairToken() {
1653
+ return randomBytes(32).toString("base64url");
1654
+ }
1655
+ function writeSecretFile(secretPath, pairToken, mintedAt) {
1656
+ fs.writeFileSync(secretPath, pairToken, {
1657
+ encoding: "utf-8",
1658
+ mode: 384
1659
+ });
1660
+ stampMintedAt(secretPath, mintedAt);
1661
+ hardenSecretStorePath(secretPath, 384);
1662
+ }
1663
+ function assertNoLegacyPairTokenInput(options, context) {
1664
+ const legacyPairToken = options.pairToken;
1665
+ if (typeof legacyPairToken !== "undefined") {
1666
+ throw new ConsentReceiptError(
1667
+ "invalid",
1668
+ `${context} no longer accepts a caller-provided pairToken.`
1669
+ );
1670
+ }
1671
+ }
1672
+ function createConsentReceipt(options) {
1673
+ assertNoLegacyPairTokenInput(options, "createConsentReceipt");
1674
+ const now = options.now ?? /* @__PURE__ */ new Date();
1675
+ const { receiptsDir, secretsDir } = resolveConsentDirs(options);
1676
+ const scope = options.scope ?? "drive";
1677
+ const conversationId = options.conversationId.trim();
1678
+ if (!conversationId) {
1679
+ throw new ConsentReceiptError(
1680
+ "invalid",
1681
+ "Consent receipt requires a non-empty conversationId."
1682
+ );
1683
+ }
1684
+ fs.mkdirSync(receiptsDir, { recursive: true });
1685
+ fs.mkdirSync(secretsDir, { recursive: true, mode: 448 });
1686
+ hardenSecretStorePath(secretsDir, 448);
1687
+ cleanupExpiredReceipts(receiptsDir, secretsDir, now);
1688
+ const ttlSeconds = Math.max(
1689
+ 1,
1690
+ options.ttlSeconds ?? DEFAULT_CONSENT_TTL_SECONDS
1691
+ );
1692
+ const receiptId = randomUUID();
1693
+ const hostId = normalizeString(options.hostId);
1694
+ const ownerClientId = normalizeString(options.ownerClientId);
1695
+ const pairToken = mintPairToken();
1696
+ const receipt = {
1697
+ id: receiptId,
1698
+ scope,
1699
+ hostId,
1700
+ conversationId,
1701
+ ownerClientId,
1702
+ issuedByClientId: normalizeString(options.issuedByClientId),
1703
+ allowedMethods: normalizeMethods(options.allowedMethods),
1704
+ pairTokenHash: hashPairTokenBinding({
1705
+ pairToken,
1706
+ hostId,
1707
+ conversationId,
1708
+ ownerClientId
1709
+ }),
1710
+ createdAt: now.toISOString(),
1711
+ expiresAt: new Date(now.getTime() + ttlSeconds * 1e3).toISOString()
1712
+ };
1713
+ const filePath = path.join(receiptsDir, `${receipt.id}.json`);
1714
+ const secretPath = resolveSecretPath(secretsDir, receipt.id);
1715
+ const createdAt = new Date(receipt.createdAt);
1716
+ try {
1717
+ writeSecretFile(secretPath, pairToken, createdAt);
1718
+ fs.writeFileSync(filePath, JSON.stringify(receipt, null, 2), "utf-8");
1719
+ stampMintedAt(filePath, createdAt);
1720
+ } catch (error) {
1721
+ removeSecretPath(secretPath);
1722
+ removeReceiptPath(filePath);
1723
+ throw error;
1724
+ }
1725
+ return { receipt, filePath };
1726
+ }
1727
+ function prepareConsentReceipt(options) {
1728
+ assertNoLegacyPairTokenInput(options, "consumeConsentReceipt");
1729
+ const now = options.now ?? /* @__PURE__ */ new Date();
1730
+ const { receiptsDir, secretsDir } = resolveConsentDirs(options);
1731
+ cleanupExpiredReceipts(receiptsDir, secretsDir, now);
1732
+ const requiredScope = options.requiredScope ?? "drive";
1733
+ const method = normalizeString(options.method);
1734
+ const conversationId = options.conversationId.trim();
1735
+ const ownerClientId = normalizeString(options.ownerClientId);
1736
+ const hostId = normalizeString(options.hostId);
1737
+ const reservationOwnerId = normalizeString(options.reservationOwnerId);
1738
+ const explicitConsentRef = normalizeString(options.consentRef);
1739
+ if (!conversationId) {
1740
+ throw new ConsentReceiptError(
1741
+ "invalid",
1742
+ "Consent receipt consumption requires a conversationId."
1743
+ );
1744
+ }
1745
+ const explicitPath = resolveReceiptPath(receiptsDir, explicitConsentRef);
1746
+ const explicitReservedPath = explicitConsentRef ? resolveReservedReceiptPath(receiptsDir, explicitConsentRef) : null;
1747
+ const reservedConsentRef = explicitConsentRef;
1748
+ if (reservedConsentRef && explicitPath && explicitReservedPath && !fs.existsSync(explicitPath) && fs.existsSync(explicitReservedPath)) {
1749
+ assertPendingReservationAvailable(reservedConsentRef);
1750
+ const reservedRecord = loadReservedReceiptRecord(explicitReservedPath);
1751
+ const reservedReceipt = reservedRecord.receipt;
1752
+ const reservedReceiptId = reservedReceipt?.id ?? extractReceiptIdFromPath(explicitReservedPath);
1753
+ if (!reservedReceipt || isExpired(reservedReceipt, now)) {
1754
+ removeReceiptPath(explicitReservedPath);
1755
+ removeSecretPath(resolveSecretPath(secretsDir, reservedReceiptId));
1756
+ } else if (reservationOwnerId && reservedRecord.reservationOwnerId === reservationOwnerId && isReservationExpired(fs.statSync(explicitReservedPath), now)) {
1757
+ fs.renameSync(explicitReservedPath, explicitPath);
1758
+ writeActiveReceiptFile(explicitPath, reservedReceipt);
1759
+ } else {
1760
+ throw new ConsentReceiptError(
1761
+ "missing",
1762
+ `Consent receipt "${explicitConsentRef}" is already reserved or consumed.`
1763
+ );
1764
+ }
1765
+ }
1766
+ const candidatePaths = explicitPath ? [explicitPath] : listReceiptPaths(receiptsDir);
1767
+ let deferredError = null;
1768
+ for (const filePath of candidatePaths) {
1769
+ if (!fs.existsSync(filePath)) {
1770
+ if (explicitPath && explicitReservedPath && fs.existsSync(explicitReservedPath)) {
1771
+ throw new ConsentReceiptError(
1772
+ "missing",
1773
+ `Consent receipt "${explicitConsentRef}" is already reserved or consumed.`
1774
+ );
1775
+ }
1776
+ continue;
1777
+ }
1778
+ const receiptStats = fs.statSync(filePath);
1779
+ const receipt = loadConsentReceipt(filePath);
1780
+ if (!receipt) {
1781
+ removeReceiptPath(filePath);
1782
+ removeSecretPath(
1783
+ resolveSecretPath(secretsDir, extractReceiptIdFromPath(filePath))
1784
+ );
1785
+ continue;
1786
+ }
1787
+ if (isExpired(receipt, now)) {
1788
+ removeReceiptPath(filePath);
1789
+ removeSecretPath(resolveSecretPath(secretsDir, receipt.id));
1790
+ if (explicitPath) {
1791
+ throw new ConsentReceiptError(
1792
+ "expired",
1793
+ `Consent receipt "${receipt.id}" expired at ${receipt.expiresAt}.`
1794
+ );
1795
+ }
1796
+ continue;
1797
+ }
1798
+ const secretPath = resolveSecretPath(secretsDir, receipt.id);
1799
+ if (!fs.existsSync(secretPath)) {
1800
+ if (explicitPath) {
1801
+ throw new ConsentReceiptError(
1802
+ "missing",
1803
+ `Consent secret "${receipt.id}" was not found.`
1804
+ );
1805
+ }
1806
+ continue;
1807
+ }
1808
+ let receiptPrepared = false;
1809
+ let cleanupSecretOnFailure = true;
1810
+ try {
1811
+ assertUntamperedConsentPath(receiptStats, receipt, "receipt");
1812
+ const secretStats = fs.statSync(secretPath);
1813
+ assertUntamperedConsentPath(secretStats, receipt, "secret");
1814
+ const pairToken = readUtf8PreservingTimes(secretPath).trim();
1815
+ if (!pairToken) {
1816
+ throw new ConsentReceiptError(
1817
+ "invalid",
1818
+ `Consent secret "${receipt.id}" was empty.`
1819
+ );
1820
+ }
1821
+ const expectedHash = hashPairTokenBinding({
1822
+ pairToken,
1823
+ hostId,
1824
+ conversationId,
1825
+ ownerClientId
1826
+ });
1827
+ if (receipt.conversationId !== conversationId || receipt.ownerClientId !== ownerClientId || receipt.hostId !== hostId || receipt.pairTokenHash !== expectedHash) {
1828
+ if (explicitPath) {
1829
+ throw new ConsentReceiptError(
1830
+ "binding-mismatch",
1831
+ `Consent receipt "${receipt.id}" did not match the requested conversation binding.`
1832
+ );
1833
+ }
1834
+ continue;
1835
+ }
1836
+ if (!scopeSatisfies(receipt.scope, requiredScope)) {
1837
+ deferredError = new ConsentReceiptError(
1838
+ "scope-mismatch",
1839
+ `Consent receipt "${receipt.id}" grants ${receipt.scope}, not ${requiredScope}.`
1840
+ );
1841
+ if (explicitPath) throw deferredError;
1842
+ continue;
1843
+ }
1844
+ if (method && receipt.allowedMethods.length > 0 && !receipt.allowedMethods.includes(method)) {
1845
+ deferredError = new ConsentReceiptError(
1846
+ "method-mismatch",
1847
+ `Consent receipt "${receipt.id}" does not allow method "${method}".`
1848
+ );
1849
+ if (explicitPath) throw deferredError;
1850
+ continue;
1851
+ }
1852
+ let reservedReceiptPath;
1853
+ try {
1854
+ assertPendingReservationAvailable(receipt.id);
1855
+ reservedReceiptPath = reserveReceiptPath(
1856
+ filePath,
1857
+ receipt,
1858
+ reservationOwnerId,
1859
+ now
1860
+ );
1861
+ } catch (error) {
1862
+ cleanupSecretOnFailure = false;
1863
+ throw error;
1864
+ }
1865
+ markPendingReservation(receipt.id);
1866
+ receiptPrepared = true;
1867
+ return {
1868
+ receipt,
1869
+ commit() {
1870
+ if (!receiptPrepared) {
1871
+ return;
1872
+ }
1873
+ receiptPrepared = false;
1874
+ try {
1875
+ fs.rmSync(reservedReceiptPath, { force: false });
1876
+ } finally {
1877
+ clearPendingReservation(receipt.id);
1878
+ removeSecretPath(secretPath);
1879
+ }
1880
+ },
1881
+ abort() {
1882
+ if (!receiptPrepared) {
1883
+ return;
1884
+ }
1885
+ receiptPrepared = false;
1886
+ try {
1887
+ fs.renameSync(reservedReceiptPath, filePath);
1888
+ writeActiveReceiptFile(filePath, receipt);
1889
+ } finally {
1890
+ clearPendingReservation(receipt.id);
1891
+ }
1892
+ }
1893
+ };
1894
+ } finally {
1895
+ if (!receiptPrepared && cleanupSecretOnFailure) {
1896
+ removeSecretPath(secretPath);
1897
+ }
1898
+ }
1899
+ }
1900
+ if (deferredError) {
1901
+ throw deferredError;
1902
+ }
1903
+ throw new ConsentReceiptError(
1904
+ "missing",
1905
+ explicitPath ? `Consent receipt "${options.consentRef}" was not found.` : "No matching consent receipt was found for the requested drive action."
1906
+ );
1907
+ }
1908
+
1909
+ // src/transport/consent-ledger.ts
1910
+ import { randomUUID as randomUUID2 } from "crypto";
1911
+ import * as fs2 from "fs";
1912
+ import * as path2 from "path";
1913
+ function normalizeString2(value) {
1914
+ const normalized = value?.trim();
1915
+ return normalized ? normalized : null;
1916
+ }
1917
+ function normalizeAddress(value) {
1918
+ if (!value) {
1919
+ return null;
1920
+ }
1921
+ const address = {
1922
+ hostId: normalizeString2(value.hostId),
1923
+ clientId: normalizeString2(value.clientId),
1924
+ conversationId: normalizeString2(value.conversationId),
1925
+ ownerClientId: normalizeString2(value.ownerClientId)
1926
+ };
1927
+ return Object.values(address).some((field) => field) ? address : null;
1928
+ }
1929
+ function isConsentLedgerEnabled() {
1930
+ const normalized = process.env.TAP_CONSENT_LEDGER?.trim().toLowerCase();
1931
+ if (!normalized) {
1932
+ return true;
1933
+ }
1934
+ return !["0", "false", "no", "off"].includes(normalized);
1935
+ }
1936
+ function resolveConsentLedgerDir(commsDir) {
1937
+ const resolvedCommsDir = normalizeString2(commsDir) ?? normalizeString2(process.env.TAP_COMMS_DIR);
1938
+ if (!resolvedCommsDir) {
1939
+ return null;
1940
+ }
1941
+ return path2.join(
1942
+ path2.resolve(resolvedCommsDir),
1943
+ "receipts",
1944
+ "consent-ledger"
1945
+ );
1946
+ }
1947
+ var MISSING_CONSENT_REF_ORPHAN_REASON = "missing_consent_ref";
1948
+ function resolveGrantId(event, grantId) {
1949
+ if (grantId) {
1950
+ return {
1951
+ grantId,
1952
+ orphanReason: null
1953
+ };
1954
+ }
1955
+ if (event !== "rejected") {
1956
+ return {
1957
+ grantId: null,
1958
+ orphanReason: null
1959
+ };
1960
+ }
1961
+ return {
1962
+ grantId: `orphan-${Date.now().toString(36)}-${randomUUID2().slice(0, 8)}`,
1963
+ orphanReason: MISSING_CONSENT_REF_ORPHAN_REASON
1964
+ };
1965
+ }
1966
+ function formatLedgerTimestamp(value) {
1967
+ return value.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
1968
+ }
1969
+ function buildLedgerFilePath(ledgerDir, record) {
1970
+ const timestamp = formatLedgerTimestamp(record.recordedAt);
1971
+ const shortGrantId = record.grantId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "unknown";
1972
+ const baseName = `${timestamp}-${record.event}-${shortGrantId}`;
1973
+ const preferredPath = path2.join(ledgerDir, `${baseName}.md`);
1974
+ if (!fs2.existsSync(preferredPath)) {
1975
+ return preferredPath;
1976
+ }
1977
+ return path2.join(
1978
+ ledgerDir,
1979
+ `${baseName}-${randomUUID2().replace(/-/g, "").slice(0, 6)}.md`
1980
+ );
1981
+ }
1982
+ function buildFrontmatter(record) {
1983
+ const fields = [
1984
+ ["type", "consent-ledger"],
1985
+ ["event", record.event],
1986
+ ["grant_id", record.grantId],
1987
+ ["orphan_reason", record.orphanReason],
1988
+ ["scope", record.scope],
1989
+ ["method", record.method],
1990
+ ["host_id", record.hostId],
1991
+ ["conversation_id", record.conversationId],
1992
+ ["issued_at", record.issuedAt],
1993
+ ["expires_at", record.expiresAt],
1994
+ ["consumed_at", record.consumedAt],
1995
+ ["recorded_at", record.recordedAt],
1996
+ ["result", record.result],
1997
+ ["issued_by_client_id", record.issuedByClientId],
1998
+ ["requester", record.requester],
1999
+ ["owner", record.owner]
2000
+ ];
2001
+ const lines = fields.map(
2002
+ ([key, value]) => `${key}: ${JSON.stringify(value ?? null)}`
2003
+ );
2004
+ return `---
2005
+ ${lines.join("\n")}
2006
+ ---
2007
+
2008
+ `;
2009
+ }
2010
+ function buildBody(record) {
2011
+ return [
2012
+ "# Consent Ledger Event",
2013
+ "",
2014
+ `- Event: \`${record.event}\``,
2015
+ `- Grant: \`${record.grantId}\``,
2016
+ ...record.orphanReason ? [`- Orphan Reason: \`${record.orphanReason}\``] : [],
2017
+ `- Scope: \`${record.scope}\``,
2018
+ `- Result: \`${record.result}\``,
2019
+ "",
2020
+ "## Owner",
2021
+ "",
2022
+ "```json",
2023
+ JSON.stringify(record.owner, null, 2),
2024
+ "```",
2025
+ "",
2026
+ "## Requester",
2027
+ "",
2028
+ "```json",
2029
+ JSON.stringify(record.requester, null, 2),
2030
+ "```",
2031
+ ""
2032
+ ].join("\n");
2033
+ }
2034
+ function writeConsentLedgerEvent(options) {
2035
+ if (!isConsentLedgerEnabled()) {
2036
+ return null;
2037
+ }
2038
+ const { grantId, orphanReason } = resolveGrantId(
2039
+ options.event,
2040
+ normalizeString2(options.grantId)
2041
+ );
2042
+ const result = normalizeString2(options.result);
2043
+ const ledgerDir = resolveConsentLedgerDir(options.commsDir);
2044
+ if (!grantId || !result || !ledgerDir) {
2045
+ return null;
2046
+ }
2047
+ const record = {
2048
+ event: options.event,
2049
+ grantId,
2050
+ orphanReason,
2051
+ scope: options.scope,
2052
+ method: normalizeString2(options.method),
2053
+ hostId: normalizeString2(options.hostId),
2054
+ conversationId: normalizeString2(options.conversationId),
2055
+ issuedAt: normalizeString2(options.issuedAt),
2056
+ expiresAt: normalizeString2(options.expiresAt),
2057
+ consumedAt: normalizeString2(options.consumedAt),
2058
+ recordedAt: normalizeString2(options.recordedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
2059
+ result,
2060
+ requester: normalizeAddress(options.requester),
2061
+ owner: normalizeAddress(options.owner),
2062
+ issuedByClientId: normalizeString2(options.issuedByClientId)
2063
+ };
2064
+ try {
2065
+ fs2.mkdirSync(ledgerDir, { recursive: true });
2066
+ const filePath = buildLedgerFilePath(ledgerDir, record);
2067
+ fs2.writeFileSync(
2068
+ filePath,
2069
+ buildFrontmatter(record) + buildBody(record),
2070
+ "utf-8"
2071
+ );
2072
+ return filePath;
2073
+ } catch {
2074
+ return null;
2075
+ }
2076
+ }
2077
+
2078
+ // src/transport/experimental/codex-ipc-observe.ts
2079
+ import * as net from "net";
2080
+ import { randomUUID as randomUUID3 } from "crypto";
2081
+
2082
+ // src/transport/experimental/codex-ipc-endpoint.ts
2083
+ import { tmpdir as tmpdir2 } from "os";
2084
+ var DEFAULT_CODEX_IPC_WINDOWS_PIPE_PATH = String.raw`\\.\pipe\codex-ipc`;
2085
+ function normalizeDirectory(value) {
2086
+ return value.replace(/[\\/]+$/, "");
2087
+ }
2088
+ function resolveCodexIpcPath(options = {}) {
2089
+ const env = options.env ?? process.env;
2090
+ const explicit = env.TAP_CODEX_IPC_PATH?.trim();
2091
+ if (explicit) return explicit;
2092
+ const platform = options.platform ?? process.platform;
2093
+ if (platform === "win32") return DEFAULT_CODEX_IPC_WINDOWS_PIPE_PATH;
2094
+ if (platform === "darwin") {
2095
+ const baseTmp = normalizeDirectory(
2096
+ options.tmpDir?.trim() || env.TMPDIR?.trim() || tmpdir2()
2097
+ );
2098
+ const uid = typeof options.uid === "number" && Number.isFinite(options.uid) ? options.uid : typeof process.getuid === "function" ? process.getuid() : null;
2099
+ if (uid == null) {
2100
+ throw new Error("Cannot resolve macOS Codex IPC socket without a uid.");
2101
+ }
2102
+ return `${baseTmp}/codex-ipc/ipc-${uid}.sock`;
2103
+ }
2104
+ return DEFAULT_CODEX_IPC_WINDOWS_PIPE_PATH;
2105
+ }
2106
+
2107
+ // src/transport/experimental/codex-ipc-observe.ts
2108
+ var MAX_FRAME_BYTES = 256 * 1024 * 1024;
2109
+ var DEFAULT_REQUEST_TIMEOUT_MS = 5e3;
2110
+ var DEFAULT_TARGETED_REQUEST_VERSION = 1;
2111
+ function isTapIpcTraceEnabled() {
2112
+ const value = process.env.TAP_IPC_TRACE?.trim().toLowerCase();
2113
+ return value === "1" || value === "true" || value === "yes" || value === "on";
2114
+ }
2115
+ function formatTraceValue(value) {
2116
+ if (typeof value === "string") {
2117
+ return JSON.stringify(value);
2118
+ }
2119
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
2120
+ return String(value);
2121
+ }
2122
+ if (value === null) {
2123
+ return "null";
2124
+ }
2125
+ return JSON.stringify(value);
2126
+ }
2127
+ function formatTraceContext(context) {
2128
+ if (!context) return "";
2129
+ const entries = Object.entries(context).filter(
2130
+ ([, value]) => typeof value !== "undefined"
2131
+ );
2132
+ if (entries.length === 0) return "";
2133
+ return ` ${entries.map(([key, value]) => `${key}=${formatTraceValue(value)}`).join(" ")}`;
2134
+ }
2135
+ function resolveHostId(explicitHostId) {
2136
+ const normalizedExplicit = explicitHostId?.trim();
2137
+ if (normalizedExplicit) return normalizedExplicit;
2138
+ const computerName = process.env.COMPUTERNAME?.trim();
2139
+ if (computerName) return computerName;
2140
+ const hostName = process.env.HOSTNAME?.trim();
2141
+ if (hostName) return hostName;
2142
+ return null;
2143
+ }
2144
+ function asRecord(value) {
2145
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2146
+ return value;
2147
+ }
2148
+ function asString(value) {
2149
+ return typeof value === "string" && value.trim() ? value.trim() : null;
2150
+ }
2151
+ function getStringField(record, ...keys) {
2152
+ if (!record) return null;
2153
+ for (const key of keys) {
2154
+ const value = asString(record[key]);
2155
+ if (value) return value;
2156
+ }
2157
+ return null;
2158
+ }
2159
+ function normalizeTransportAddress(hostId, clientId, conversationId, ownerClientId) {
2160
+ return {
2161
+ hostId,
2162
+ clientId,
2163
+ conversationId,
2164
+ ownerClientId
2165
+ };
2166
+ }
2167
+ function extractConversationId(params) {
2168
+ return getStringField(params, "conversationId", "threadId") ?? getStringField(asRecord(params?.change), "conversationId", "threadId") ?? getStringField(asRecord(params?.thread), "id");
2169
+ }
2170
+ function listRecordKeys(value) {
2171
+ if (!value) return null;
2172
+ return Object.keys(value);
2173
+ }
2174
+ function encodeCodexIpcFrame(message) {
2175
+ const json = JSON.stringify(message);
2176
+ const payload = Buffer.from(json, "utf-8");
2177
+ const frame = Buffer.allocUnsafe(4 + payload.length);
2178
+ frame.writeUInt32LE(payload.length, 0);
2179
+ payload.copy(frame, 4);
2180
+ return frame;
2181
+ }
2182
+ function decodeCodexIpcFrames(buffer) {
2183
+ const messages = [];
2184
+ let offset = 0;
2185
+ while (offset + 4 <= buffer.length) {
2186
+ const frameLength = buffer.readUInt32LE(offset);
2187
+ if (frameLength > MAX_FRAME_BYTES) {
2188
+ throw new Error(
2189
+ `Codex IPC frame exceeds max size (${frameLength} bytes > ${MAX_FRAME_BYTES})`
2190
+ );
2191
+ }
2192
+ if (offset + 4 + frameLength > buffer.length) break;
2193
+ const json = buffer.toString("utf-8", offset + 4, offset + 4 + frameLength);
2194
+ messages.push(JSON.parse(json));
2195
+ offset += 4 + frameLength;
2196
+ }
2197
+ return {
2198
+ messages,
2199
+ remainder: buffer.subarray(offset)
2200
+ };
2201
+ }
2202
+ var ExperimentalCodexIpcObserveTransport = class {
2203
+ constructor(options = {}) {
2204
+ this.options = options;
2205
+ this.pipePath = options.pipePath ?? resolveCodexIpcPath();
2206
+ this.hostId = resolveHostId(options.hostId);
2207
+ this.clientType = options.clientType ?? "tap-observe";
2208
+ this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
2209
+ }
2210
+ options;
2211
+ kind = "experimental-codex-ipc-observe";
2212
+ pipePath;
2213
+ hostId;
2214
+ clientType;
2215
+ requestTimeoutMs;
2216
+ listeners = /* @__PURE__ */ new Set();
2217
+ agents = /* @__PURE__ */ new Map();
2218
+ conversations = /* @__PURE__ */ new Map();
2219
+ pendingRequests = /* @__PURE__ */ new Map();
2220
+ socket = null;
2221
+ remainder = Buffer.alloc(0);
2222
+ connectedAt = null;
2223
+ ownClientId = null;
2224
+ snapshot = {
2225
+ transport: this.kind,
2226
+ connected: false,
2227
+ connectedAt: null,
2228
+ agents: [],
2229
+ conversations: []
2230
+ };
2231
+ handleData = (...args) => {
2232
+ const [chunk] = args;
2233
+ if (!Buffer.isBuffer(chunk)) {
2234
+ return;
2235
+ }
2236
+ this.remainder = Buffer.concat([this.remainder, chunk]);
2237
+ const decoded = decodeCodexIpcFrames(this.remainder);
2238
+ this.remainder = decoded.remainder;
2239
+ for (const message of decoded.messages) {
2240
+ this.handleMessage(message);
2241
+ }
2242
+ };
2243
+ handleError = (...args) => {
2244
+ const [error] = args;
2245
+ this.rejectPendingRequests(
2246
+ error instanceof Error ? error : new Error(String(error ?? "Codex IPC transport error"))
2247
+ );
2248
+ };
2249
+ handleClose = () => {
2250
+ this.rejectPendingRequests(new Error("Codex IPC transport closed"));
2251
+ this.remainder = Buffer.alloc(0);
2252
+ this.emitDisconnected(null);
2253
+ this.detachSocket();
2254
+ };
2255
+ async connect() {
2256
+ if (this.socket) {
2257
+ await this.disconnect();
2258
+ }
2259
+ this.trace("connect:start", {
2260
+ pipePath: this.pipePath,
2261
+ clientType: this.clientType,
2262
+ hostId: this.hostId
2263
+ });
2264
+ const socket = this.options.socketFactory?.(this.pipePath) ?? net.createConnection({
2265
+ path: this.pipePath
2266
+ });
2267
+ this.socket = socket;
2268
+ this.attachSocket(socket);
2269
+ await this.waitForConnect(socket);
2270
+ socket.setNoDelay?.(true);
2271
+ this.trace("connect:open", {
2272
+ pipePath: this.pipePath
2273
+ });
2274
+ const response = await this.sendRequest("initialize", {
2275
+ clientType: this.clientType
2276
+ });
2277
+ const result = asRecord(response.result);
2278
+ const clientId = getStringField(result, "clientId");
2279
+ if (!clientId) {
2280
+ throw new Error("Codex IPC initialize response did not include clientId");
2281
+ }
2282
+ this.ownClientId = clientId;
2283
+ this.connectedAt = (/* @__PURE__ */ new Date()).toISOString();
2284
+ this.snapshot = this.buildSnapshot(true);
2285
+ this.trace("connect:initialized", {
2286
+ clientId,
2287
+ connectedAt: this.connectedAt,
2288
+ handledByClientId: response.handledByClientId ?? null,
2289
+ resultType: response.resultType ?? null,
2290
+ resultKeys: listRecordKeys(result)
2291
+ });
2292
+ this.emit({
2293
+ kind: "transport-connected",
2294
+ receivedAt: this.connectedAt,
2295
+ method: "initialize",
2296
+ sourceAddress: normalizeTransportAddress(
2297
+ this.hostId,
2298
+ this.ownClientId,
2299
+ null,
2300
+ null
2301
+ ),
2302
+ payload: response,
2303
+ snapshot: this.snapshot
2304
+ });
2305
+ return this.snapshot;
2306
+ }
2307
+ async disconnect() {
2308
+ if (!this.socket) return;
2309
+ const socket = this.socket;
2310
+ this.detachSocket();
2311
+ this.rejectPendingRequests(new Error("Codex IPC transport disconnected"));
2312
+ this.remainder = Buffer.alloc(0);
2313
+ this.emitDisconnected({ reason: "disconnect" });
2314
+ socket.end();
2315
+ socket.destroy();
2316
+ }
2317
+ getSnapshot() {
2318
+ return this.snapshot;
2319
+ }
2320
+ subscribe(listener) {
2321
+ this.listeners.add(listener);
2322
+ return () => {
2323
+ this.listeners.delete(listener);
2324
+ };
2325
+ }
2326
+ attachSocket(socket) {
2327
+ socket.on("data", this.handleData);
2328
+ socket.on("error", this.handleError);
2329
+ socket.on("close", this.handleClose);
2330
+ }
2331
+ emitDisconnected(payload) {
2332
+ const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
2333
+ this.connectedAt = null;
2334
+ this.snapshot = this.buildSnapshot(false);
2335
+ this.emit({
2336
+ kind: "transport-disconnected",
2337
+ receivedAt,
2338
+ method: null,
2339
+ sourceAddress: normalizeTransportAddress(
2340
+ this.hostId,
2341
+ this.ownClientId,
2342
+ null,
2343
+ null
2344
+ ),
2345
+ payload,
2346
+ snapshot: this.snapshot
2347
+ });
2348
+ }
2349
+ detachSocket() {
2350
+ if (!this.socket) return;
2351
+ this.socket.removeListener("data", this.handleData);
2352
+ this.socket.removeListener("error", this.handleError);
2353
+ this.socket.removeListener("close", this.handleClose);
2354
+ this.socket = null;
2355
+ }
2356
+ async waitForConnect(socket) {
2357
+ await new Promise((resolve7, reject) => {
2358
+ const cleanup = () => {
2359
+ clearTimeout(timeout);
2360
+ socket.removeListener("connect", onConnect);
2361
+ socket.removeListener("error", onError);
2362
+ };
2363
+ const onConnect = () => {
2364
+ cleanup();
2365
+ resolve7();
2366
+ };
2367
+ const onError = (...args) => {
2368
+ const [error] = args;
2369
+ cleanup();
2370
+ reject(
2371
+ error instanceof Error ? error : new Error(String(error ?? "Codex IPC connection failed"))
2372
+ );
2373
+ };
2374
+ const timeout = setTimeout(() => {
2375
+ cleanup();
2376
+ reject(
2377
+ new Error(
2378
+ `Timed out connecting to Codex IPC transport at ${this.pipePath}`
2379
+ )
2380
+ );
2381
+ }, this.requestTimeoutMs);
2382
+ socket.on("connect", onConnect);
2383
+ socket.on("error", onError);
2384
+ });
2385
+ }
2386
+ getHostId() {
2387
+ return this.hostId;
2388
+ }
2389
+ getOwnClientId() {
2390
+ return this.ownClientId;
2391
+ }
2392
+ trace(message, context) {
2393
+ if (!isTapIpcTraceEnabled()) {
2394
+ return;
2395
+ }
2396
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", " UTC");
2397
+ console.log(
2398
+ `[${timestamp}] TAP_IPC_TRACE [${this.kind}] ${message}${formatTraceContext(context)}`
2399
+ );
2400
+ }
2401
+ resolveRequestVersion(_method, targetClientId) {
2402
+ if (this.options.protocolVersion !== null) {
2403
+ const configuredVersion = this.options.protocolVersion;
2404
+ if (typeof configuredVersion !== "undefined") {
2405
+ return configuredVersion;
2406
+ }
2407
+ }
2408
+ if (targetClientId?.trim()) {
2409
+ return DEFAULT_TARGETED_REQUEST_VERSION;
2410
+ }
2411
+ return null;
2412
+ }
2413
+ async sendRequest(method, params, targetClientId) {
2414
+ if (!this.socket) {
2415
+ throw new Error("Codex IPC observe transport is not connected");
2416
+ }
2417
+ const requestId = randomUUID3();
2418
+ const message = {
2419
+ type: "request",
2420
+ requestId,
2421
+ method,
2422
+ params
2423
+ };
2424
+ if (this.ownClientId) {
2425
+ message.sourceClientId = this.ownClientId;
2426
+ }
2427
+ const requestVersion = this.resolveRequestVersion(method, targetClientId);
2428
+ if (requestVersion !== null) {
2429
+ message.version = requestVersion;
2430
+ }
2431
+ if (targetClientId) {
2432
+ message.targetClientId = targetClientId;
2433
+ }
2434
+ this.trace("request:send", {
2435
+ requestId,
2436
+ method,
2437
+ targetClientId: targetClientId ?? null,
2438
+ version: message.version ?? null,
2439
+ conversationId: extractConversationId(params ?? null),
2440
+ paramKeys: listRecordKeys(params ?? null)
2441
+ });
2442
+ const promise = new Promise((resolve7, reject) => {
2443
+ const timeout = setTimeout(() => {
2444
+ this.pendingRequests.delete(requestId);
2445
+ reject(
2446
+ new Error(
2447
+ `Codex IPC request "${method}" timed out after ${this.requestTimeoutMs}ms`
2448
+ )
2449
+ );
2450
+ }, this.requestTimeoutMs);
2451
+ this.pendingRequests.set(requestId, { resolve: resolve7, reject, timeout });
2452
+ });
2453
+ this.socket.write(encodeCodexIpcFrame(message));
2454
+ return promise;
2455
+ }
2456
+ handleMessage(message) {
2457
+ if (message.type === "response") {
2458
+ this.handleResponse(message);
2459
+ return;
2460
+ }
2461
+ if (message.type === "broadcast") {
2462
+ this.handleBroadcast(message);
2463
+ }
2464
+ }
2465
+ handleResponse(message) {
2466
+ const requestId = asString(message.requestId);
2467
+ if (!requestId) return;
2468
+ const pending = this.pendingRequests.get(requestId);
2469
+ if (!pending) return;
2470
+ clearTimeout(pending.timeout);
2471
+ this.pendingRequests.delete(requestId);
2472
+ this.trace("response:recv", {
2473
+ requestId,
2474
+ method: message.method ?? null,
2475
+ resultType: message.resultType ?? null,
2476
+ handledByClientId: message.handledByClientId ?? null,
2477
+ hasError: message.error != null,
2478
+ hasResult: typeof message.result !== "undefined"
2479
+ });
2480
+ if (message.resultType === "error") {
2481
+ pending.reject(
2482
+ new Error(
2483
+ `Codex IPC request failed: ${JSON.stringify(message.error ?? {})}`
2484
+ )
2485
+ );
2486
+ return;
2487
+ }
2488
+ pending.resolve(message);
2489
+ }
2490
+ handleBroadcast(message) {
2491
+ const method = message.method ?? null;
2492
+ const params = asRecord(message.params);
2493
+ const sourceClientId = asString(message.sourceClientId);
2494
+ const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
2495
+ this.trace("broadcast:recv", {
2496
+ method,
2497
+ sourceClientId,
2498
+ conversationId: extractConversationId(params),
2499
+ version: message.version ?? null
2500
+ });
2501
+ if (method === "client-status-changed") {
2502
+ const clientId = getStringField(params, "clientId");
2503
+ if (clientId) {
2504
+ this.upsertAgent(clientId, {
2505
+ name: getStringField(params, "clientType"),
2506
+ metadata: {
2507
+ status: getStringField(params, "status"),
2508
+ clientType: getStringField(params, "clientType")
2509
+ }
2510
+ });
2511
+ this.snapshot = this.buildSnapshot(true);
2512
+ this.emit({
2513
+ kind: "agent-status",
2514
+ receivedAt,
2515
+ method,
2516
+ sourceAddress: normalizeTransportAddress(
2517
+ this.hostId,
2518
+ clientId,
2519
+ null,
2520
+ null
2521
+ ),
2522
+ payload: message,
2523
+ snapshot: this.snapshot
2524
+ });
2525
+ }
2526
+ return;
2527
+ }
2528
+ if (method === "thread-stream-state-changed") {
2529
+ const conversationId = extractConversationId(params);
2530
+ if (conversationId) {
2531
+ const ownerClientId = sourceClientId;
2532
+ if (ownerClientId) {
2533
+ this.upsertAgent(ownerClientId, {
2534
+ name: null,
2535
+ metadata: {}
2536
+ });
2537
+ }
2538
+ this.conversations.set(conversationId, {
2539
+ id: conversationId,
2540
+ address: normalizeTransportAddress(
2541
+ this.hostId,
2542
+ ownerClientId,
2543
+ conversationId,
2544
+ ownerClientId
2545
+ ),
2546
+ metadata: {
2547
+ change: params?.change ?? null,
2548
+ lastMethod: method,
2549
+ sourceClientId: ownerClientId
2550
+ }
2551
+ });
2552
+ this.snapshot = this.buildSnapshot(true);
2553
+ this.emit({
2554
+ kind: "conversation-state",
2555
+ receivedAt,
2556
+ method,
2557
+ sourceAddress: normalizeTransportAddress(
2558
+ this.hostId,
2559
+ ownerClientId,
2560
+ conversationId,
2561
+ ownerClientId
2562
+ ),
2563
+ payload: message,
2564
+ snapshot: this.snapshot
2565
+ });
2566
+ return;
2567
+ }
2568
+ }
2569
+ this.snapshot = this.buildSnapshot(true);
2570
+ this.emit({
2571
+ kind: "raw",
2572
+ receivedAt,
2573
+ method,
2574
+ sourceAddress: normalizeTransportAddress(
2575
+ this.hostId,
2576
+ sourceClientId,
2577
+ extractConversationId(params),
2578
+ sourceClientId
2579
+ ),
2580
+ payload: message,
2581
+ snapshot: this.snapshot
2582
+ });
2583
+ }
2584
+ upsertAgent(clientId, update) {
2585
+ const existing = this.agents.get(clientId);
2586
+ this.agents.set(clientId, {
2587
+ id: clientId,
2588
+ name: update.name ?? existing?.name ?? null,
2589
+ address: normalizeTransportAddress(this.hostId, clientId, null, null),
2590
+ metadata: {
2591
+ ...existing?.metadata ?? {},
2592
+ ...update.metadata
2593
+ }
2594
+ });
2595
+ }
2596
+ buildSnapshot(connected) {
2597
+ return {
2598
+ transport: this.kind,
2599
+ connected,
2600
+ connectedAt: connected ? this.connectedAt : null,
2601
+ agents: [...this.agents.values()].sort(
2602
+ (a, b) => a.id.localeCompare(b.id)
2603
+ ),
2604
+ conversations: [...this.conversations.values()].sort(
2605
+ (a, b) => a.id.localeCompare(b.id)
2606
+ )
2607
+ };
2608
+ }
2609
+ rejectPendingRequests(error) {
2610
+ for (const [requestId, pending] of this.pendingRequests) {
2611
+ clearTimeout(pending.timeout);
2612
+ pending.reject(error);
2613
+ this.pendingRequests.delete(requestId);
2614
+ }
2615
+ }
2616
+ emit(event) {
2617
+ for (const listener of this.listeners) {
2618
+ void listener(event);
2619
+ }
2620
+ }
2621
+ };
2622
+
2623
+ // src/transport/experimental/codex-ipc-control.ts
2624
+ function asJsonRecord(value) {
2625
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2626
+ return null;
2627
+ }
2628
+ return value;
2629
+ }
2630
+ var CODEX_IPC_DRIVE_METHODS = [
2631
+ "thread-follower-start-turn",
2632
+ "thread-follower-steer-turn",
2633
+ "thread-follower-interrupt-turn",
2634
+ "thread-follower-edit-last-user-turn",
2635
+ "thread-follower-submit-user-input",
2636
+ "thread-follower-submit-mcp-server-elicitation-response",
2637
+ "thread-follower-command-approval-decision",
2638
+ "thread-follower-file-approval-decision",
2639
+ "thread-follower-permissions-request-approval-response",
2640
+ "thread-follower-compact-thread",
2641
+ "thread-follower-set-model-and-reasoning",
2642
+ "thread-follower-set-collaboration-mode",
2643
+ "thread-follower-set-queued-follow-ups-state"
2644
+ ];
2645
+ var STABILITY_GUARDED_METHODS = /* @__PURE__ */ new Set([
2646
+ "thread-follower-start-turn"
2647
+ ]);
2648
+ var globalLocksKey = /* @__PURE__ */ Symbol.for("tap-comms:conversationLocks");
2649
+ var globalDriveTimeKey = /* @__PURE__ */ Symbol.for("tap-comms:conversationLastDriveTime");
2650
+ var globalStabilityGuardStore = globalThis;
2651
+ var sharedConversationLocks = globalStabilityGuardStore[globalLocksKey] ?? /* @__PURE__ */ new Map();
2652
+ if (!globalStabilityGuardStore[globalLocksKey]) {
2653
+ globalStabilityGuardStore[globalLocksKey] = sharedConversationLocks;
2654
+ }
2655
+ var sharedConversationLastDriveTime = globalStabilityGuardStore[globalDriveTimeKey] ?? /* @__PURE__ */ new Map();
2656
+ if (!globalStabilityGuardStore[globalDriveTimeKey]) {
2657
+ globalStabilityGuardStore[globalDriveTimeKey] = sharedConversationLastDriveTime;
2658
+ }
2659
+ function normalizeAddress2(value) {
2660
+ return {
2661
+ hostId: value.hostId?.trim() || null,
2662
+ clientId: value.clientId?.trim() || null,
2663
+ conversationId: value.conversationId?.trim() || null,
2664
+ ownerClientId: value.ownerClientId?.trim() || null
2665
+ };
2666
+ }
2667
+ function isDriveMethod(method) {
2668
+ return CODEX_IPC_DRIVE_METHODS.includes(method);
2669
+ }
2670
+ function normalizeMethod(method) {
2671
+ const normalized = method.trim();
2672
+ if (!isDriveMethod(normalized)) {
2673
+ throw new Error(`Unsupported Codex IPC drive method "${method}".`);
2674
+ }
2675
+ return normalized;
2676
+ }
2677
+ function normalizeActionLabel(action, method) {
2678
+ const normalized = action?.trim();
2679
+ return normalized || method;
2680
+ }
2681
+ function asRecord2(value) {
2682
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2683
+ return null;
2684
+ }
2685
+ return value;
2686
+ }
2687
+ function listRecordKeys2(value) {
2688
+ if (!value) {
2689
+ return null;
2690
+ }
2691
+ return Object.keys(value);
2692
+ }
2693
+ function summarizeDriveParams(params) {
2694
+ const turnStartParams = asRecord2(params?.turnStartParams);
2695
+ const input = Array.isArray(turnStartParams?.input) ? turnStartParams.input : null;
2696
+ const textLength = input?.reduce((total, item) => {
2697
+ const record = asRecord2(item);
2698
+ return total + (typeof record?.text === "string" ? record.text.length : 0);
2699
+ }, 0);
2700
+ return {
2701
+ paramKeys: listRecordKeys2(params),
2702
+ turnStartParamKeys: listRecordKeys2(turnStartParams),
2703
+ inputItemCount: input?.length ?? null,
2704
+ textLength: textLength ?? null
2705
+ };
2706
+ }
2707
+ function extractDriveTurnId(response) {
2708
+ const result = asRecord2(response.result);
2709
+ const nested = asRecord2(result?.result);
2710
+ const turn = asRecord2(result?.turn) ?? asRecord2(nested?.turn);
2711
+ const turnId = turn?.id;
2712
+ return typeof turnId === "string" && turnId.trim() ? turnId.trim() : null;
2713
+ }
2714
+ function extractConversationLastTurnStatus(conversation) {
2715
+ const change = asRecord2(conversation?.metadata.change);
2716
+ const turn = asRecord2(change?.turn);
2717
+ const turnStatus = turn?.status;
2718
+ if (typeof turnStatus === "string" && turnStatus.trim()) {
2719
+ return turnStatus.trim();
2720
+ }
2721
+ const conversationState = asRecord2(change?.conversationState);
2722
+ const turns = Array.isArray(conversationState?.turns) ? conversationState.turns : null;
2723
+ const lastTurn = turns?.length ? asRecord2(turns[turns.length - 1]) : null;
2724
+ const lastStatus = lastTurn?.status;
2725
+ return typeof lastStatus === "string" && lastStatus.trim() ? lastStatus.trim() : null;
2726
+ }
2727
+ function extractRejectionResult(error) {
2728
+ if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
2729
+ return error.code;
2730
+ }
2731
+ return "execution-rejected";
2732
+ }
2733
+ function buildFollowerStartTurnParams(options) {
2734
+ const turnStartParams = { ...options.turnStartParams ?? {} };
2735
+ const text = options.text.trim();
2736
+ if (!text) {
2737
+ throw new Error(
2738
+ "thread-follower-start-turn requires a non-empty text input."
2739
+ );
2740
+ }
2741
+ const existingInput = Array.isArray(turnStartParams.input) ? turnStartParams.input : null;
2742
+ if (!existingInput) {
2743
+ turnStartParams.input = [
2744
+ {
2745
+ type: "text",
2746
+ text,
2747
+ text_elements: []
2748
+ }
2749
+ ];
2750
+ }
2751
+ if (!Array.isArray(turnStartParams.attachments)) {
2752
+ turnStartParams.attachments = [];
2753
+ }
2754
+ if (!Array.isArray(turnStartParams.commentAttachments)) {
2755
+ turnStartParams.commentAttachments = [];
2756
+ }
2757
+ if (typeof turnStartParams.inheritThreadSettings !== "boolean") {
2758
+ turnStartParams.inheritThreadSettings = true;
2759
+ }
2760
+ return {
2761
+ conversationId: options.conversationId,
2762
+ turnStartParams
2763
+ };
2764
+ }
2765
+ var ExperimentalCodexIpcControlTransport = class extends ExperimentalCodexIpcObserveTransport {
2766
+ kind = "experimental-codex-ipc-control";
2767
+ commsDir;
2768
+ receiptsDir;
2769
+ secretsDir;
2770
+ defaultConsentTtlSeconds;
2771
+ reservationOwnerId;
2772
+ conversationLocks = sharedConversationLocks;
2773
+ conversationLastDriveTime = sharedConversationLastDriveTime;
2774
+ COOLDOWN_MS = 1e4;
2775
+ LOCK_TIMEOUT_MS = 6e4;
2776
+ RECIPIENT_STATE_WAIT_MS = 750;
2777
+ constructor(options = {}) {
2778
+ super({
2779
+ ...options,
2780
+ clientType: options.clientType ?? "tap-control"
2781
+ });
2782
+ this.commsDir = options.commsDir;
2783
+ this.receiptsDir = options.receiptsDir;
2784
+ this.secretsDir = options.secretsDir;
2785
+ this.defaultConsentTtlSeconds = options.defaultConsentTtlSeconds ?? DEFAULT_CONSENT_TTL_SECONDS;
2786
+ this.reservationOwnerId = options.reservationOwnerId?.trim() || randomUUID4();
2787
+ this.subscribe((event) => {
2788
+ if (event.kind === "conversation-state") {
2789
+ const conversationId = event.sourceAddress.conversationId;
2790
+ if (!conversationId) return;
2791
+ const payload = asJsonRecord(event.payload);
2792
+ const params = asJsonRecord(payload?.params);
2793
+ const change = asJsonRecord(params?.change);
2794
+ const turn = asJsonRecord(change?.turn);
2795
+ if (turn) {
2796
+ const status = turn.status;
2797
+ this.trace("guard:observe-turn-status", {
2798
+ conversationId,
2799
+ turnId: turn.id,
2800
+ status
2801
+ });
2802
+ if (status === "completed" || status === "failed" || status === "cancelled") {
2803
+ this.trace("guard:release-lock", {
2804
+ conversationId,
2805
+ turnId: turn.id,
2806
+ status
2807
+ });
2808
+ this.releaseLock(conversationId);
2809
+ }
2810
+ }
2811
+ }
2812
+ });
2813
+ }
2814
+ acquireLock(conversationId) {
2815
+ const existing = this.conversationLocks.get(conversationId);
2816
+ if (existing?.timer) {
2817
+ clearTimeout(existing.timer);
2818
+ }
2819
+ const timer = setTimeout(() => {
2820
+ this.trace("guard:lock-timeout", { conversationId });
2821
+ this.conversationLocks.delete(conversationId);
2822
+ }, this.LOCK_TIMEOUT_MS);
2823
+ if (timer && typeof timer.unref === "function") {
2824
+ timer.unref();
2825
+ }
2826
+ this.conversationLocks.set(conversationId, { timer });
2827
+ }
2828
+ releaseLock(conversationId) {
2829
+ const existing = this.conversationLocks.get(conversationId);
2830
+ if (existing) {
2831
+ if (existing.timer) {
2832
+ clearTimeout(existing.timer);
2833
+ }
2834
+ this.conversationLocks.delete(conversationId);
2835
+ }
2836
+ }
2837
+ getConversationSnapshot(conversationId) {
2838
+ return this.getSnapshot().conversations.find(
2839
+ (conversation) => conversation.id === conversationId
2840
+ ) ?? null;
2841
+ }
2842
+ async waitForConversationSnapshot(conversationId) {
2843
+ const existing = this.getConversationSnapshot(conversationId);
2844
+ if (existing) return existing;
2845
+ return await new Promise((resolve7) => {
2846
+ let unsubscribe = null;
2847
+ const timeout = setTimeout(() => {
2848
+ unsubscribe?.();
2849
+ resolve7(this.getConversationSnapshot(conversationId));
2850
+ }, this.RECIPIENT_STATE_WAIT_MS);
2851
+ if (typeof timeout.unref === "function") {
2852
+ timeout.unref();
2853
+ }
2854
+ unsubscribe = this.subscribe((event) => {
2855
+ if (event.kind !== "conversation-state" || event.sourceAddress.conversationId !== conversationId) {
2856
+ return;
2857
+ }
2858
+ clearTimeout(timeout);
2859
+ unsubscribe?.();
2860
+ resolve7(
2861
+ event.snapshot.conversations.find(
2862
+ (conversation) => conversation.id === conversationId
2863
+ ) ?? this.getConversationSnapshot(conversationId)
2864
+ );
2865
+ });
2866
+ });
2867
+ }
2868
+ async assertRecipientCanStartTurn(conversationId, method) {
2869
+ const conversation = await this.waitForConversationSnapshot(conversationId);
2870
+ const lastStatus = extractConversationLastTurnStatus(conversation);
2871
+ if (lastStatus === "inProgress") {
2872
+ this.trace("guard:recipient-active-turn", {
2873
+ conversationId,
2874
+ method,
2875
+ lastStatus
2876
+ });
2877
+ throw new Error(
2878
+ `[Stability Guard] Recipient conversation "${conversationId}" has an active in-progress turn; refusing "${method}" to avoid a stuck nested turn.`
2879
+ );
2880
+ }
2881
+ }
2882
+ createConsentReceipt(options) {
2883
+ const targetAddress = this.resolveConversationTargetAddress(
2884
+ options.conversationId,
2885
+ {
2886
+ hostId: options.hostId ?? null,
2887
+ ownerClientId: options.ownerClientId ?? null
2888
+ }
2889
+ );
2890
+ const createOptions = {
2891
+ receiptsDir: this.receiptsDir,
2892
+ secretsDir: this.secretsDir,
2893
+ scope: options.scope ?? "drive",
2894
+ hostId: targetAddress.hostId,
2895
+ conversationId: options.conversationId,
2896
+ ownerClientId: targetAddress.ownerClientId,
2897
+ issuedByClientId: this.getOwnClientId(),
2898
+ ttlSeconds: options.ttlSeconds ?? this.defaultConsentTtlSeconds,
2899
+ allowedMethods: [...options.allowedMethods ?? []]
2900
+ };
2901
+ const created = createConsentReceipt(createOptions);
2902
+ writeConsentLedgerEvent({
2903
+ commsDir: this.commsDir,
2904
+ event: "issued",
2905
+ grantId: created.receipt.id,
2906
+ scope: created.receipt.scope,
2907
+ method: created.receipt.allowedMethods.length === 1 ? created.receipt.allowedMethods[0] : null,
2908
+ hostId: created.receipt.hostId,
2909
+ conversationId: created.receipt.conversationId,
2910
+ issuedAt: created.receipt.createdAt,
2911
+ expiresAt: created.receipt.expiresAt,
2912
+ result: "granted",
2913
+ requester: this.buildSourceAddress(options.conversationId, targetAddress),
2914
+ owner: targetAddress,
2915
+ issuedByClientId: created.receipt.issuedByClientId
2916
+ });
2917
+ return created;
2918
+ }
2919
+ createStartTurnSuggestion(options) {
2920
+ return this.createSuggestion({
2921
+ conversationId: options.conversationId,
2922
+ method: "thread-follower-start-turn",
2923
+ params: buildFollowerStartTurnParams(options),
2924
+ action: options.action ?? "start-turn",
2925
+ consentRef: options.consentRef ?? null
2926
+ });
2927
+ }
2928
+ createSuggestion(options) {
2929
+ const method = normalizeMethod(options.method);
2930
+ const targetAddress = this.resolveConversationTargetAddress(
2931
+ options.conversationId
2932
+ );
2933
+ const sourceAddress = this.buildSourceAddress(
2934
+ options.conversationId,
2935
+ targetAddress
2936
+ );
2937
+ return {
2938
+ id: randomUUID4(),
2939
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2940
+ status: "pending-owner-approval",
2941
+ scope: "suggest",
2942
+ method,
2943
+ action: normalizeActionLabel(options.action, method),
2944
+ conversationId: options.conversationId,
2945
+ payload: options.params ?? null,
2946
+ sourceAddress,
2947
+ targetAddress,
2948
+ consentRef: options.consentRef?.trim() || null
2949
+ };
2950
+ }
2951
+ async startTurn(options) {
2952
+ return this.driveAction({
2953
+ conversationId: options.conversationId,
2954
+ method: "thread-follower-start-turn",
2955
+ params: buildFollowerStartTurnParams(options),
2956
+ action: options.action ?? "start-turn",
2957
+ consentRef: options.consentRef ?? null,
2958
+ hostId: options.hostId ?? null,
2959
+ ownerClientId: options.ownerClientId ?? null
2960
+ });
2961
+ }
2962
+ async driveAction(options) {
2963
+ const method = normalizeMethod(options.method);
2964
+ const conversationId = options.conversationId.trim();
2965
+ const isGuarded = STABILITY_GUARDED_METHODS.has(method);
2966
+ const targetAddress = this.resolveConversationTargetAddress(
2967
+ conversationId,
2968
+ {
2969
+ hostId: options.hostId ?? null,
2970
+ ownerClientId: options.ownerClientId ?? null
2971
+ }
2972
+ );
2973
+ const ownerClientId = targetAddress.ownerClientId?.trim();
2974
+ if (!ownerClientId) {
2975
+ throw new Error(
2976
+ `Conversation "${conversationId}" does not have a live ownerClientId.`
2977
+ );
2978
+ }
2979
+ const sourceAddress = this.buildSourceAddress(
2980
+ conversationId,
2981
+ targetAddress
2982
+ );
2983
+ this.trace("drive:prepare", {
2984
+ conversationId,
2985
+ method,
2986
+ action: normalizeActionLabel(options.action, method),
2987
+ consentRef: options.consentRef ?? null,
2988
+ hostId: targetAddress.hostId,
2989
+ ownerClientId,
2990
+ ...summarizeDriveParams(options.params)
2991
+ });
2992
+ let preparedReceipt = null;
2993
+ let guardLockAcquired = false;
2994
+ try {
2995
+ preparedReceipt = prepareConsentReceipt({
2996
+ receiptsDir: this.receiptsDir,
2997
+ secretsDir: this.secretsDir,
2998
+ consentRef: options.consentRef ?? null,
2999
+ requiredScope: "drive",
3000
+ method,
3001
+ hostId: targetAddress.hostId,
3002
+ conversationId,
3003
+ ownerClientId,
3004
+ reservationOwnerId: this.reservationOwnerId
3005
+ });
3006
+ if (isGuarded) {
3007
+ await this.assertRecipientCanStartTurn(conversationId, method);
3008
+ if (this.conversationLocks.has(conversationId)) {
3009
+ this.trace("guard:locked", { conversationId, method });
3010
+ throw new Error(
3011
+ `[Stability Guard] Rejecting "${method}". Conversation "${conversationId}" has an active in-progress turn.`
3012
+ );
3013
+ }
3014
+ const now = Date.now();
3015
+ const lastDrive = this.conversationLastDriveTime.get(conversationId) ?? 0;
3016
+ const elapsed = now - lastDrive;
3017
+ if (elapsed < this.COOLDOWN_MS) {
3018
+ const waitTime = this.COOLDOWN_MS - elapsed;
3019
+ this.trace("guard:cooldown", {
3020
+ conversationId,
3021
+ method,
3022
+ remainingMs: waitTime
3023
+ });
3024
+ throw new Error(
3025
+ `[Stability Guard] Cooldown active for "${method}" on conversation "${conversationId}". Wait ${Math.ceil(waitTime / 1e3)}s.`
3026
+ );
3027
+ }
3028
+ this.acquireLock(conversationId);
3029
+ guardLockAcquired = true;
3030
+ }
3031
+ this.trace("drive:request", {
3032
+ conversationId,
3033
+ method,
3034
+ ownerClientId
3035
+ });
3036
+ const response = await this.sendRequest(
3037
+ method,
3038
+ options.params,
3039
+ ownerClientId
3040
+ );
3041
+ this.trace("drive:response", {
3042
+ conversationId,
3043
+ method,
3044
+ ownerClientId,
3045
+ turnId: extractDriveTurnId(response),
3046
+ resultType: response.resultType ?? null
3047
+ });
3048
+ preparedReceipt.commit();
3049
+ if (isGuarded) {
3050
+ this.conversationLastDriveTime.set(conversationId, Date.now());
3051
+ }
3052
+ const executedAt = (/* @__PURE__ */ new Date()).toISOString();
3053
+ writeConsentLedgerEvent({
3054
+ commsDir: this.commsDir,
3055
+ event: "consumed",
3056
+ grantId: preparedReceipt.receipt.id,
3057
+ scope: preparedReceipt.receipt.scope,
3058
+ method,
3059
+ hostId: targetAddress.hostId,
3060
+ conversationId,
3061
+ issuedAt: preparedReceipt.receipt.createdAt,
3062
+ expiresAt: preparedReceipt.receipt.expiresAt,
3063
+ consumedAt: executedAt,
3064
+ recordedAt: executedAt,
3065
+ result: "executed",
3066
+ requester: sourceAddress,
3067
+ owner: targetAddress,
3068
+ issuedByClientId: preparedReceipt.receipt.issuedByClientId
3069
+ });
3070
+ return {
3071
+ executedAt,
3072
+ scope: "drive",
3073
+ method,
3074
+ action: normalizeActionLabel(options.action, method),
3075
+ conversationId,
3076
+ sourceAddress,
3077
+ targetAddress,
3078
+ consentRef: preparedReceipt.receipt.id,
3079
+ receipt: preparedReceipt.receipt,
3080
+ response
3081
+ };
3082
+ } catch (error) {
3083
+ if (guardLockAcquired) this.releaseLock(conversationId);
3084
+ this.trace("drive:error", {
3085
+ conversationId,
3086
+ method,
3087
+ ownerClientId,
3088
+ error: error instanceof Error ? error.stack ?? error.message : String(error)
3089
+ });
3090
+ preparedReceipt?.abort();
3091
+ writeConsentLedgerEvent({
3092
+ commsDir: this.commsDir,
3093
+ event: "rejected",
3094
+ grantId: preparedReceipt?.receipt.id ?? options.consentRef ?? null,
3095
+ scope: preparedReceipt?.receipt.scope ?? "drive",
3096
+ method,
3097
+ hostId: targetAddress.hostId,
3098
+ conversationId,
3099
+ issuedAt: preparedReceipt?.receipt.createdAt ?? null,
3100
+ expiresAt: preparedReceipt?.receipt.expiresAt ?? null,
3101
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
3102
+ result: extractRejectionResult(error),
3103
+ requester: sourceAddress,
3104
+ owner: targetAddress,
3105
+ issuedByClientId: preparedReceipt?.receipt.issuedByClientId ?? this.getOwnClientId()
3106
+ });
3107
+ throw error;
3108
+ }
3109
+ }
3110
+ resolveConversationTargetAddress(conversationId, fallback) {
3111
+ const normalizedConversationId = conversationId.trim();
3112
+ if (!normalizedConversationId) {
3113
+ throw new Error(
3114
+ "Codex IPC control actions require a non-empty conversationId."
3115
+ );
3116
+ }
3117
+ const conversation = this.getSnapshot().conversations.find(
3118
+ (candidate) => candidate.id === normalizedConversationId
3119
+ );
3120
+ if (conversation) {
3121
+ return normalizeAddress2(conversation.address);
3122
+ }
3123
+ const ownerClientId = fallback?.ownerClientId?.trim() || null;
3124
+ const hostId = fallback?.hostId?.trim() || this.getHostId();
3125
+ if (!ownerClientId) {
3126
+ throw new Error(
3127
+ `Conversation "${normalizedConversationId}" is not present in the current observe snapshot.`
3128
+ );
3129
+ }
3130
+ return {
3131
+ hostId,
3132
+ clientId: ownerClientId,
3133
+ conversationId: normalizedConversationId,
3134
+ ownerClientId
3135
+ };
3136
+ }
3137
+ buildSourceAddress(conversationId, targetAddress) {
3138
+ return {
3139
+ hostId: this.getHostId(),
3140
+ clientId: this.getOwnClientId(),
3141
+ conversationId,
3142
+ ownerClientId: targetAddress.ownerClientId
3143
+ };
3144
+ }
3145
+ };
3146
+
3147
+ // scripts/bridge/bridge-dispatch.ts
3148
+ var dispatchLogger = createBridgeLogger("dispatch");
3149
+ var heartbeatLogger = createBridgeLogger("heartbeat");
3150
+ var DRIVE_DISPATCH_RESERVATION_OWNER_ID = randomUUID5();
3151
+ var DRIVE_NOT_YET_WIRED_REASON = "missing pairToken / drive not yet wired (M345 Phase 2 / M355 pending)";
3152
+ var DRIVE_ACTION_NOT_YET_SUPPORTED_REASON = "drive action is not yet wired through bridge dispatch";
3153
+ var DRIVE_START_TURN_ACTIONS = /* @__PURE__ */ new Set([
3154
+ "start-turn",
3155
+ "thread-follower-start-turn"
3156
+ ]);
3157
+ function asRecord3(value) {
3158
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3159
+ return null;
3160
+ }
3161
+ return value;
3162
+ }
3163
+ function extractDriveTurnId2(result) {
3164
+ const response = asRecord3(result);
3165
+ const payload = asRecord3(response?.response);
3166
+ const body = asRecord3(payload?.result);
3167
+ const nestedResult = asRecord3(body?.result);
3168
+ const turn = asRecord3(body?.turn) ?? asRecord3(nestedResult?.turn);
3169
+ const turnId = turn?.id;
3170
+ return typeof turnId === "string" && turnId.trim() ? turnId.trim() : null;
3171
+ }
3172
+ function shouldTraceIpc() {
3173
+ const value = process.env.TAP_IPC_TRACE?.trim().toLowerCase();
3174
+ return value === "1" || value === "true" || value === "yes" || value === "on";
3175
+ }
3176
+ function logIpcTrace(message, context) {
3177
+ if (!shouldTraceIpc()) {
3178
+ return;
3179
+ }
3180
+ dispatchLogger.info(`[ipc-trace] ${message}`, context);
3181
+ }
3182
+ function createDriveDispatchTransport(options) {
3183
+ return new ExperimentalCodexIpcControlTransport({
3184
+ commsDir: options.commsDir,
3185
+ hostId: resolveBridgeHostId(options),
3186
+ clientType: "tap-bridge-dispatch",
3187
+ reservationOwnerId: DRIVE_DISPATCH_RESERVATION_OWNER_ID
3188
+ });
752
3189
  }
753
-
754
- // scripts/bridge/bridge-format.ts
755
- import { writeFileSync as writeFileSync3 } from "fs";
756
- import { join as join5 } from "path";
757
- function buildUserInput(candidate, agentName, heartbeats) {
758
- const sender = resolveAddressLabel(candidate.sender || "unknown", heartbeats);
759
- const recipient = resolveAddressLabel(
760
- candidate.recipient || agentName,
761
- heartbeats
762
- );
763
- const subject = candidate.subject || "(none)";
764
- const body = candidate.body.trim();
765
- return [
766
- `Tap-comms inbox message for ${agentName}.`,
767
- `Sender: ${sender}`,
768
- `Recipient: ${recipient}`,
769
- `Subject: ${subject}`,
770
- `File: ${candidate.fileName}`,
771
- "",
772
- "Message body:",
773
- body || "(empty)",
774
- "",
775
- "---",
776
- "Instructions: Read the message above and respond using the tap_reply tool.",
777
- `Use tap_reply(to: "${candidate.sender || "unknown"}", subject: "<your-subject>", content: "<your-response>") to send your response.`,
778
- "If the message is a review request, perform the review and reply with your findings.",
779
- "If the message is informational, acknowledge briefly via tap_reply.",
780
- "Do NOT respond with plain text only \u2014 you MUST use the tap_reply tool."
781
- ].join("\n");
3190
+ function buildInvalidDriveEnvelopeReason(reason) {
3191
+ return `invalid drive envelope: ${reason}`;
782
3192
  }
783
- function writeProcessedMarker(stateDir, candidate, dispatchMode, threadId, turnId) {
784
- const payload = {
785
- requestFile: candidate.filePath,
786
- requestName: candidate.fileName,
787
- sender: candidate.sender,
788
- recipient: candidate.recipient,
789
- subject: candidate.subject,
790
- dispatchMode,
3193
+ function normalizeDriveStartTurnAction(action) {
3194
+ const normalized = action?.trim() || null;
3195
+ if (!normalized) return null;
3196
+ return DRIVE_START_TURN_ACTIONS.has(normalized) ? "thread-follower-start-turn" : null;
3197
+ }
3198
+ function rejectDriveEnvelope(options, candidate, threadId, reason) {
3199
+ writeProcessedMarker(
3200
+ options.stateDir,
3201
+ candidate,
3202
+ "rejected",
791
3203
  threadId,
792
- turnId,
793
- markedAt: (/* @__PURE__ */ new Date()).toISOString()
794
- };
795
- writeFileSync3(
796
- getProcessedMarkerPath(stateDir, candidate.markerId),
797
- `${JSON.stringify(payload, null, 2)}
798
- `,
799
- "utf8"
3204
+ null,
3205
+ reason
3206
+ );
3207
+ writeLastDispatch(
3208
+ options.stateDir,
3209
+ candidate,
3210
+ "rejected",
3211
+ threadId,
3212
+ null,
3213
+ reason
800
3214
  );
3215
+ writeConsentLedgerEvent({
3216
+ commsDir: options.commsDir,
3217
+ event: "rejected",
3218
+ grantId: candidate.consentRef?.trim() || null,
3219
+ scope: "drive",
3220
+ method: candidate.action ?? null,
3221
+ hostId: candidate.toAddress?.hostId ?? null,
3222
+ conversationId: candidate.toAddress?.conversationId ?? threadId,
3223
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
3224
+ result: reason,
3225
+ requester: candidate.fromAddress ?? null,
3226
+ owner: candidate.toAddress ?? null
3227
+ });
3228
+ dispatchLogger.warn("rejected malformed drive envelope", {
3229
+ fileName: candidate.fileName,
3230
+ messageId: candidate.messageId ?? null,
3231
+ conversationId: candidate.toAddress?.conversationId ?? null,
3232
+ action: candidate.action ?? null,
3233
+ consentRef: candidate.consentRef ?? null,
3234
+ reason
3235
+ });
3236
+ return true;
801
3237
  }
802
- function writeLastDispatch(stateDir, candidate, dispatchMode, threadId, turnId) {
803
- const payload = {
804
- requestFile: candidate.filePath,
805
- requestName: candidate.fileName,
806
- markerId: candidate.markerId,
807
- sender: candidate.sender,
808
- recipient: candidate.recipient,
809
- subject: candidate.subject,
810
- dispatchMode,
3238
+ function blockDriveEnvelope(options, candidate, threadId, reason) {
3239
+ writeLastDispatch(
3240
+ options.stateDir,
3241
+ candidate,
3242
+ "blocked",
811
3243
  threadId,
812
- turnId,
813
- dispatchedAt: (/* @__PURE__ */ new Date()).toISOString()
814
- };
815
- writeFileSync3(
816
- join5(stateDir, "last-dispatch.json"),
817
- `${JSON.stringify(payload, null, 2)}
818
- `,
819
- "utf8"
3244
+ null,
3245
+ reason
820
3246
  );
3247
+ writeConsentLedgerEvent({
3248
+ commsDir: options.commsDir,
3249
+ event: "rejected",
3250
+ grantId: candidate.consentRef?.trim() || null,
3251
+ scope: "drive",
3252
+ method: candidate.action ?? null,
3253
+ hostId: candidate.toAddress?.hostId ?? null,
3254
+ conversationId: candidate.toAddress?.conversationId ?? threadId,
3255
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
3256
+ result: reason,
3257
+ requester: candidate.fromAddress ?? null,
3258
+ owner: candidate.toAddress ?? null
3259
+ });
3260
+ dispatchLogger.warn("blocked drive envelope", {
3261
+ fileName: candidate.fileName,
3262
+ messageId: candidate.messageId ?? null,
3263
+ subject: candidate.subject || "(none)",
3264
+ conversationId: candidate.toAddress?.conversationId ?? null,
3265
+ action: candidate.action ?? null,
3266
+ consentRef: candidate.consentRef ?? null,
3267
+ reason
3268
+ });
3269
+ return false;
3270
+ }
3271
+ async function dispatchDriveEnvelope(options, candidate, driveTransportFactory) {
3272
+ const conversationId = candidate.toAddress?.conversationId?.trim() || null;
3273
+ if (!conversationId) {
3274
+ return rejectDriveEnvelope(
3275
+ options,
3276
+ candidate,
3277
+ null,
3278
+ buildInvalidDriveEnvelopeReason(
3279
+ "drive scope requires target conversationId metadata."
3280
+ )
3281
+ );
3282
+ }
3283
+ const consentRef = candidate.consentRef?.trim() || null;
3284
+ if (!consentRef) {
3285
+ return rejectDriveEnvelope(
3286
+ options,
3287
+ candidate,
3288
+ conversationId,
3289
+ buildInvalidDriveEnvelopeReason(
3290
+ "drive scope requires a non-empty consentRef."
3291
+ )
3292
+ );
3293
+ }
3294
+ const method = normalizeDriveStartTurnAction(candidate.action);
3295
+ if (!method) {
3296
+ const action = candidate.action?.trim() || "(missing)";
3297
+ return blockDriveEnvelope(
3298
+ options,
3299
+ candidate,
3300
+ conversationId,
3301
+ `${DRIVE_ACTION_NOT_YET_SUPPORTED_REASON}: ${action}`
3302
+ );
3303
+ }
3304
+ const text = candidate.body.trim();
3305
+ if (!text) {
3306
+ return rejectDriveEnvelope(
3307
+ options,
3308
+ candidate,
3309
+ conversationId,
3310
+ buildInvalidDriveEnvelopeReason(
3311
+ `${method} requires a non-empty message body.`
3312
+ )
3313
+ );
3314
+ }
3315
+ const transport = driveTransportFactory(options);
3316
+ const targetHostId = candidate.toAddress?.hostId?.trim() || null;
3317
+ const targetOwnerClientId = candidate.toAddress?.ownerClientId?.trim() || candidate.toAddress?.clientId?.trim() || null;
3318
+ logIpcTrace("drive envelope prepared", {
3319
+ fileName: candidate.fileName,
3320
+ conversationId,
3321
+ action: candidate.action ?? null,
3322
+ consentRef,
3323
+ targetHostId,
3324
+ targetOwnerClientId
3325
+ });
3326
+ try {
3327
+ logIpcTrace("transport connect start", {
3328
+ fileName: candidate.fileName,
3329
+ conversationId
3330
+ });
3331
+ await transport.connect();
3332
+ logIpcTrace("transport connect success", {
3333
+ fileName: candidate.fileName,
3334
+ conversationId
3335
+ });
3336
+ logIpcTrace("transport startTurn start", {
3337
+ fileName: candidate.fileName,
3338
+ conversationId,
3339
+ textLength: text.length
3340
+ });
3341
+ const result = await transport.startTurn({
3342
+ conversationId,
3343
+ text,
3344
+ action: candidate.action?.trim() || null,
3345
+ consentRef,
3346
+ hostId: targetHostId,
3347
+ ownerClientId: targetOwnerClientId
3348
+ });
3349
+ const turnId = extractDriveTurnId2(result);
3350
+ logIpcTrace("transport startTurn success", {
3351
+ fileName: candidate.fileName,
3352
+ conversationId,
3353
+ turnId,
3354
+ result
3355
+ });
3356
+ writeProcessedMarker(
3357
+ options.stateDir,
3358
+ candidate,
3359
+ "drive",
3360
+ conversationId,
3361
+ turnId
3362
+ );
3363
+ writeLastDispatch(
3364
+ options.stateDir,
3365
+ candidate,
3366
+ "drive",
3367
+ conversationId,
3368
+ turnId,
3369
+ null
3370
+ );
3371
+ markBridgeActivity();
3372
+ dispatchLogger.info("handed drive envelope to control transport", {
3373
+ fileName: candidate.fileName,
3374
+ messageId: candidate.messageId ?? null,
3375
+ conversationId,
3376
+ action: candidate.action ?? null,
3377
+ consentRef,
3378
+ turnId
3379
+ });
3380
+ return true;
3381
+ } catch (error) {
3382
+ logIpcTrace("transport startTurn error", {
3383
+ fileName: candidate.fileName,
3384
+ conversationId,
3385
+ error: error instanceof Error ? error.stack ?? error.message : String(error)
3386
+ });
3387
+ return blockDriveEnvelope(
3388
+ options,
3389
+ candidate,
3390
+ conversationId,
3391
+ sanitizeErrorForPersistence(
3392
+ error instanceof Error ? error.stack ?? error.message : String(error)
3393
+ ) ?? "drive handoff failed"
3394
+ );
3395
+ } finally {
3396
+ await transport.disconnect().catch(() => void 0);
3397
+ }
821
3398
  }
822
-
823
- // scripts/bridge/bridge-dispatch.ts
824
- import {
825
- existsSync as existsSync5,
826
- readFileSync as readFileSync5,
827
- renameSync as renameSync2,
828
- statSync as statSync2,
829
- unlinkSync,
830
- writeFileSync as writeFileSync4
831
- } from "fs";
832
- import { join as join6 } from "path";
833
- var dispatchLogger = createBridgeLogger("dispatch");
834
- var heartbeatLogger = createBridgeLogger("heartbeat");
835
3399
  function sanitizeErrorForPersistence(error) {
836
3400
  if (!error) return null;
837
3401
  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, "***");
@@ -841,9 +3405,58 @@ function delay(ms) {
841
3405
  setTimeout(resolvePromise, ms);
842
3406
  });
843
3407
  }
3408
+ function resolveBridgeRoutingSlot(agentId) {
3409
+ const normalized = agentId.trim().replace(/-/g, "_").toLowerCase();
3410
+ if (!normalized) return null;
3411
+ if (normalized === "tower" || normalized === "claude_main" || normalized === "codex_main") {
3412
+ return "tower";
3413
+ }
3414
+ if (normalized === "reviewer" || normalized === "claude_reviewer" || normalized === "codex_reviewer") {
3415
+ return "reviewer";
3416
+ }
3417
+ const worktreeMatch = normalized.match(/^(?:(?:claude|codex)_)?wt_?(\d+)$/);
3418
+ if (!worktreeMatch) return null;
3419
+ return `wt-${Number.parseInt(worktreeMatch[1], 10)}`;
3420
+ }
3421
+ function resolveBridgeHostId(options) {
3422
+ const explicitHostId = process.env.TAP_HOST_ID?.trim();
3423
+ if (explicitHostId) return explicitHostId;
3424
+ const computerName = process.env.COMPUTERNAME?.trim();
3425
+ if (computerName) return computerName;
3426
+ const hostName = process.env.HOSTNAME?.trim();
3427
+ if (hostName) return hostName;
3428
+ return options.commsDir;
3429
+ }
3430
+ function resolveBridgeAliases(values) {
3431
+ const aliases = [];
3432
+ for (const value of values) {
3433
+ const normalized = value?.trim();
3434
+ if (!normalized || aliases.includes(normalized)) continue;
3435
+ aliases.push(normalized);
3436
+ }
3437
+ return aliases;
3438
+ }
3439
+ function buildBridgeAddress(options, conversationId) {
3440
+ const slot = options.routingSlot ?? resolveBridgeRoutingSlot(options.agentId);
3441
+ const routingAddress = slot ?? options.agentId;
3442
+ return {
3443
+ hostId: resolveBridgeHostId(options),
3444
+ clientId: options.agentId,
3445
+ conversationId,
3446
+ ownerClientId: conversationId ? options.agentId : null,
3447
+ routingAddress,
3448
+ slot,
3449
+ aliases: resolveBridgeAliases([
3450
+ routingAddress,
3451
+ slot,
3452
+ options.agentId,
3453
+ options.agentName
3454
+ ])
3455
+ };
3456
+ }
844
3457
  function readThreadState(stateDir) {
845
- const threadPath = join6(stateDir, "thread.json");
846
- if (!existsSync5(threadPath)) {
3458
+ const threadPath = join7(stateDir, "thread.json");
3459
+ if (!existsSync6(threadPath)) {
847
3460
  return null;
848
3461
  }
849
3462
  try {
@@ -851,7 +3464,10 @@ function readThreadState(stateDir) {
851
3464
  readFileSync5(threadPath, "utf8")
852
3465
  );
853
3466
  if (parsed.threadId) {
854
- return parsed;
3467
+ return {
3468
+ ...parsed,
3469
+ cwd: normalizePersistedThreadCwd(parsed.cwd)
3470
+ };
855
3471
  }
856
3472
  } catch {
857
3473
  return null;
@@ -864,10 +3480,10 @@ function persistThreadState(stateDir, threadId, appServerUrl, ephemeral, cwd) {
864
3480
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
865
3481
  appServerUrl,
866
3482
  ephemeral,
867
- cwd
3483
+ cwd: normalizePersistedThreadCwd(cwd)
868
3484
  };
869
- writeFileSync4(
870
- join6(stateDir, "thread.json"),
3485
+ writeFileSync5(
3486
+ join7(stateDir, "thread.json"),
871
3487
  `${JSON.stringify(payload, null, 2)}
872
3488
  `,
873
3489
  "utf8"
@@ -877,15 +3493,15 @@ function acquireCommsLock(lockPath) {
877
3493
  const deadline = Date.now() + COMMS_HEARTBEAT_LOCK_TIMEOUT_MS;
878
3494
  while (Date.now() < deadline) {
879
3495
  try {
880
- writeFileSync4(lockPath, String(process.pid), { flag: "wx" });
3496
+ writeFileSync5(lockPath, String(process.pid), { flag: "wx" });
881
3497
  return true;
882
3498
  } catch {
883
3499
  try {
884
- const lockAge = Date.now() - statSync2(lockPath).mtimeMs;
3500
+ const lockAge = Date.now() - statSync3(lockPath).mtimeMs;
885
3501
  if (lockAge > COMMS_LOCK_STALE_AGE_MS) {
886
- unlinkSync(lockPath);
3502
+ unlinkSync2(lockPath);
887
3503
  try {
888
- writeFileSync4(lockPath, String(process.pid), { flag: "wx" });
3504
+ writeFileSync5(lockPath, String(process.pid), { flag: "wx" });
889
3505
  return true;
890
3506
  } catch {
891
3507
  }
@@ -901,49 +3517,94 @@ function acquireCommsLock(lockPath) {
901
3517
  }
902
3518
  function releaseCommsLock(lockPath) {
903
3519
  try {
904
- unlinkSync(lockPath);
3520
+ unlinkSync2(lockPath);
905
3521
  } catch {
906
3522
  }
907
3523
  }
908
- function updateCommsHeartbeat(options, status) {
909
- const heartbeatsPath = join6(options.commsDir, "heartbeats.json");
910
- const lockPath = join6(options.commsDir, ".heartbeats.lock");
3524
+ function heartbeatStoreKey(record) {
3525
+ for (const field of ["id", "instanceId", "agent"]) {
3526
+ const value = record[field];
3527
+ if (typeof value === "string" && value.trim()) {
3528
+ return value.trim();
3529
+ }
3530
+ }
3531
+ return null;
3532
+ }
3533
+ function normalizeHeartbeatStore(raw) {
3534
+ if (Array.isArray(raw)) {
3535
+ const normalized = {};
3536
+ for (const entry of raw) {
3537
+ const record = asRecord3(entry);
3538
+ if (!record) continue;
3539
+ const key = heartbeatStoreKey(record);
3540
+ if (key) normalized[key] = record;
3541
+ }
3542
+ return normalized;
3543
+ }
3544
+ return asRecord3(raw) ?? {};
3545
+ }
3546
+ function updateCommsHeartbeat(options, status, conversationId) {
3547
+ const heartbeatsPath = join7(options.commsDir, "heartbeats.json");
3548
+ const lockPath = join7(options.commsDir, ".heartbeats.lock");
911
3549
  if (!acquireCommsLock(lockPath)) {
912
3550
  return;
913
3551
  }
914
3552
  try {
915
3553
  let store = {};
916
3554
  try {
917
- store = JSON.parse(readFileSync5(heartbeatsPath, "utf-8"));
3555
+ store = normalizeHeartbeatStore(
3556
+ JSON.parse(readFileSync5(heartbeatsPath, "utf-8"))
3557
+ );
918
3558
  } catch {
919
3559
  }
920
3560
  const key = options.agentId;
921
3561
  const existing = store[key];
922
3562
  const now = (/* @__PURE__ */ new Date()).toISOString();
3563
+ const lastActivity = _lastBridgeActivityAt ?? existing?.lastActivity ?? now;
3564
+ const resolvedConversationId = conversationId ?? readThreadState(options.stateDir)?.threadId ?? null;
923
3565
  store[key] = {
924
3566
  id: options.agentId,
925
3567
  agent: options.agentName,
926
3568
  timestamp: now,
927
- lastActivity: now,
3569
+ lastActivity,
928
3570
  joinedAt: existing?.joinedAt ?? now,
929
3571
  status,
930
3572
  source: "bridge-dispatch",
931
3573
  instanceId: options.agentId,
932
3574
  bridgePid: process.pid,
933
- connectHash: `instance:${options.agentId}`
3575
+ connectHash: `instance:${options.agentId}`,
3576
+ receiveTransports: ["consent-drive"],
3577
+ address: buildBridgeAddress(options, resolvedConversationId)
934
3578
  };
935
3579
  const tmpPath = heartbeatsPath + ".tmp." + process.pid;
936
- writeFileSync4(tmpPath, JSON.stringify(store, null, 2), "utf-8");
3580
+ writeFileSync5(tmpPath, JSON.stringify(store, null, 2), "utf-8");
937
3581
  renameSync2(tmpPath, heartbeatsPath);
3582
+ try {
3583
+ const presenceDir = join7(options.commsDir, "presence");
3584
+ mkdirSync4(presenceDir, { recursive: true });
3585
+ const sanitizedId = key.replace(/[/\\:]/g, "_");
3586
+ const presPath = join7(presenceDir, `${sanitizedId}.json`);
3587
+ const presTmp = presPath + ".tmp." + process.pid;
3588
+ writeFileSync5(presTmp, JSON.stringify(store[key], null, 2), "utf-8");
3589
+ renameSync2(presTmp, presPath);
3590
+ } catch {
3591
+ }
938
3592
  } catch {
939
3593
  } finally {
940
3594
  releaseCommsLock(lockPath);
941
3595
  }
942
3596
  }
943
3597
  var heartbeatCount = 0;
3598
+ var _lastBridgeActivityAt = null;
3599
+ function markBridgeActivity() {
3600
+ _lastBridgeActivityAt = (/* @__PURE__ */ new Date()).toISOString();
3601
+ }
3602
+ function getLastBridgeActivityAt() {
3603
+ return _lastBridgeActivityAt;
3604
+ }
944
3605
  function readPreviousHeartbeat(stateDir) {
945
- const heartbeatPath = join6(stateDir, "heartbeat.json");
946
- if (!existsSync5(heartbeatPath)) {
3606
+ const heartbeatPath = join7(stateDir, "heartbeat.json");
3607
+ if (!existsSync6(heartbeatPath)) {
947
3608
  return null;
948
3609
  }
949
3610
  try {
@@ -955,8 +3616,8 @@ function readPreviousHeartbeat(stateDir) {
955
3616
  }
956
3617
  }
957
3618
  function readLastDispatchAt(stateDir) {
958
- const dispatchPath = join6(stateDir, "last-dispatch.json");
959
- if (!existsSync5(dispatchPath)) {
3619
+ const dispatchPath = join7(stateDir, "last-dispatch.json");
3620
+ if (!existsSync6(dispatchPath)) {
960
3621
  return null;
961
3622
  }
962
3623
  try {
@@ -968,10 +3629,6 @@ function readLastDispatchAt(stateDir) {
968
3629
  return null;
969
3630
  }
970
3631
  }
971
- function isWaitingApprovalStatus(status) {
972
- if (!status) return false;
973
- return /approval|input-required|confirm|consent/i.test(status);
974
- }
975
3632
  function resolveTurnState(client) {
976
3633
  if (!client) return null;
977
3634
  if (client.activeTurnId) return "active";
@@ -987,7 +3644,11 @@ function writeHeartbeat(options, client, health) {
987
3644
  const previousHeartbeat = readPreviousHeartbeat(options.stateDir);
988
3645
  const lastDispatchAt = readLastDispatchAt(options.stateDir);
989
3646
  const turnState = resolveTurnState(client);
990
- const lastTurnAt = previousHeartbeat?.activeTurnId && !client?.activeTurnId ? nowIso : previousHeartbeat?.lastTurnAt ?? null;
3647
+ const turnJustCompleted = previousHeartbeat?.activeTurnId && !client?.activeTurnId;
3648
+ if (turnJustCompleted) {
3649
+ markBridgeActivity();
3650
+ }
3651
+ const lastTurnAt = turnJustCompleted ? nowIso : previousHeartbeat?.lastTurnAt ?? null;
991
3652
  const idleSince = turnState === "idle" || turnState === "waiting-approval" ? previousHeartbeat?.turnState === turnState && previousHeartbeat.idleSince ? previousHeartbeat.idleSince : lastTurnAt ?? lastDispatchAt ?? nowIso : null;
992
3653
  if (client?.threadId) {
993
3654
  const savedThread = readThreadState(options.stateDir);
@@ -1025,8 +3686,8 @@ function writeHeartbeat(options, client, health) {
1025
3686
  consecutiveFailureCount: health.consecutiveFailureCount,
1026
3687
  busyMode: options.busyMode
1027
3688
  };
1028
- writeFileSync4(
1029
- join6(options.stateDir, "heartbeat.json"),
3689
+ writeFileSync5(
3690
+ join7(options.stateDir, "heartbeat.json"),
1030
3691
  `${JSON.stringify(payload, null, 2)}
1031
3692
  `,
1032
3693
  "utf8"
@@ -1041,19 +3702,38 @@ function writeHeartbeat(options, client, health) {
1041
3702
  });
1042
3703
  }
1043
3704
  const status = turnState === "active" ? "active" : "idle";
1044
- updateCommsHeartbeat(options, status);
3705
+ updateCommsHeartbeat(
3706
+ options,
3707
+ status,
3708
+ payload.threadId ?? readThreadState(options.stateDir)?.threadId ?? null
3709
+ );
1045
3710
  }
1046
- async function dispatchCandidate(client, options, candidate, heartbeats) {
1047
- const input = buildUserInput(candidate, options.agentName, heartbeats);
3711
+ async function dispatchCandidate(client, options, candidate, heartbeats, driveTransportFactory = createDriveDispatchTransport) {
1048
3712
  dispatchLogger.info("dispatching candidate", {
1049
3713
  sender: candidate.sender || "unknown",
1050
3714
  recipient: candidate.recipient || options.agentName,
1051
3715
  subject: candidate.subject || "(none)",
1052
3716
  fileName: candidate.fileName,
3717
+ messageId: candidate.messageId ?? null,
3718
+ scope: candidate.scope ?? null,
3719
+ action: candidate.action ?? null,
3720
+ hasConsentRef: Boolean(candidate.consentRef),
1053
3721
  threadId: client.threadId,
1054
3722
  activeTurnId: client.activeTurnId,
1055
3723
  busyMode: options.busyMode
1056
3724
  });
3725
+ if (candidate.scope === "drive") {
3726
+ return dispatchDriveEnvelope(options, candidate, driveTransportFactory);
3727
+ }
3728
+ const input = buildUserInput(candidate, options.agentName, heartbeats);
3729
+ if (client.isWaitingOnApproval()) {
3730
+ dispatchLogger.warn("thread waiting on approval; skipping dispatch", {
3731
+ fileName: candidate.fileName,
3732
+ threadId: client.threadId,
3733
+ lastTurnStatus: client.lastTurnStatus
3734
+ });
3735
+ return false;
3736
+ }
1057
3737
  if (client.isBusy()) {
1058
3738
  if (options.busyMode !== "steer") {
1059
3739
  dispatchLogger.debug("bridge busy and steer disabled", {
@@ -1076,8 +3756,10 @@ async function dispatchCandidate(client, options, candidate, heartbeats) {
1076
3756
  candidate,
1077
3757
  "steer",
1078
3758
  client.threadId,
1079
- turnId2
3759
+ turnId2,
3760
+ null
1080
3761
  );
3762
+ markBridgeActivity();
1081
3763
  dispatchLogger.info("steered active turn", {
1082
3764
  fileName: candidate.fileName,
1083
3765
  threadId: client.threadId,
@@ -1087,7 +3769,13 @@ async function dispatchCandidate(client, options, candidate, heartbeats) {
1087
3769
  } catch (error) {
1088
3770
  await client.refreshCurrentThreadState().catch(() => void 0);
1089
3771
  if (!client.isBusy()) {
1090
- return dispatchCandidate(client, options, candidate, heartbeats);
3772
+ return dispatchCandidate(
3773
+ client,
3774
+ options,
3775
+ candidate,
3776
+ heartbeats,
3777
+ driveTransportFactory
3778
+ );
1091
3779
  }
1092
3780
  if (shouldRetrySteerAsStart(error)) {
1093
3781
  client.activeTurnId = null;
@@ -1097,7 +3785,13 @@ async function dispatchCandidate(client, options, candidate, heartbeats) {
1097
3785
  threadId: client.threadId,
1098
3786
  error: sanitizeErrorForPersistence(String(error))
1099
3787
  });
1100
- return dispatchCandidate(client, options, candidate, heartbeats);
3788
+ return dispatchCandidate(
3789
+ client,
3790
+ options,
3791
+ candidate,
3792
+ heartbeats,
3793
+ driveTransportFactory
3794
+ );
1101
3795
  }
1102
3796
  throw error;
1103
3797
  }
@@ -1115,8 +3809,10 @@ async function dispatchCandidate(client, options, candidate, heartbeats) {
1115
3809
  candidate,
1116
3810
  "start",
1117
3811
  client.threadId,
1118
- turnId
3812
+ turnId,
3813
+ null
1119
3814
  );
3815
+ markBridgeActivity();
1120
3816
  dispatchLogger.info("started turn for candidate", {
1121
3817
  fileName: candidate.fileName,
1122
3818
  threadId: client.threadId,
@@ -1152,7 +3848,7 @@ async function runScan(options, cutoff, client) {
1152
3848
  candidate,
1153
3849
  heartbeats
1154
3850
  );
1155
- if (!dispatched && options.busyMode === "wait") {
3851
+ if (!dispatched) {
1156
3852
  return { dispatched: false, maxMtimeMs };
1157
3853
  }
1158
3854
  maxMtimeMs = Math.max(maxMtimeMs, candidate.mtimeMs);
@@ -1165,6 +3861,7 @@ async function waitForTurnDrain(options, client, health) {
1165
3861
  while (Date.now() < deadline) {
1166
3862
  writeHeartbeat(options, client, health);
1167
3863
  if (!client.activeTurnId) {
3864
+ markBridgeActivity();
1168
3865
  return;
1169
3866
  }
1170
3867
  await delay(1e3);
@@ -1235,7 +3932,8 @@ async function maybeBootstrapHeadlessTurn(options, cutoff, client) {
1235
3932
  } catch (error) {
1236
3933
  const reason = error instanceof Error ? error.message : String(error);
1237
3934
  throw new Error(
1238
- `Headless cold-start warmup failed: ${reason}. Run: npx @hua-labs/tap doctor`
3935
+ `Headless cold-start warmup failed: ${reason}. Run: npx @hua-labs/tap doctor`,
3936
+ { cause: error }
1239
3937
  );
1240
3938
  }
1241
3939
  }
@@ -1272,7 +3970,11 @@ function formatJsonRpcError(error) {
1272
3970
  2
1273
3971
  );
1274
3972
  }
3973
+ var DEFAULT_APP_SERVER_REQUEST_TIMEOUT_MS = 3e4;
1275
3974
  var nextAppServerClientId = 1;
3975
+ function getProcessRssMb() {
3976
+ return Math.round(process.memoryUsage().rss / (1024 * 1024));
3977
+ }
1276
3978
  var AppServerClient = class {
1277
3979
  socket = null;
1278
3980
  url;
@@ -1280,7 +3982,9 @@ var AppServerClient = class {
1280
3982
  logger;
1281
3983
  clientId = nextAppServerClientId++;
1282
3984
  nextId = 1;
3985
+ requestTimeoutMs;
1283
3986
  pending = /* @__PURE__ */ new Map();
3987
+ socketListeners = /* @__PURE__ */ new Map();
1284
3988
  connected = false;
1285
3989
  initialized = false;
1286
3990
  threadId = null;
@@ -1293,10 +3997,14 @@ var AppServerClient = class {
1293
3997
  lastError = null;
1294
3998
  lastSuccessfulAppServerAt = null;
1295
3999
  lastSuccessfulAppServerMethod = null;
1296
- constructor(url, logger, gatewayToken) {
4000
+ constructor(url, logger, gatewayToken, requestTimeoutMs = DEFAULT_APP_SERVER_REQUEST_TIMEOUT_MS) {
1297
4001
  this.url = url;
1298
4002
  this.logger = logger;
1299
4003
  this.gatewayToken = gatewayToken ?? null;
4004
+ this.requestTimeoutMs = requestTimeoutMs;
4005
+ }
4006
+ getPendingRequestCount() {
4007
+ return this.pending.size;
1300
4008
  }
1301
4009
  async connect() {
1302
4010
  if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
@@ -1313,6 +4021,10 @@ var AppServerClient = class {
1313
4021
  wsOptions.protocols = [`${AUTH_SUBPROTOCOL_PREFIX}${this.gatewayToken}`];
1314
4022
  }
1315
4023
  this.socket = new WebSocket(this.url, wsOptions);
4024
+ const socket = this.socket;
4025
+ if (!socket) {
4026
+ throw new Error(`Failed to create App Server socket for ${this.url}`);
4027
+ }
1316
4028
  await new Promise((resolvePromise, rejectPromise) => {
1317
4029
  let settled = false;
1318
4030
  const resolveOnce = () => {
@@ -1329,45 +4041,59 @@ var AppServerClient = class {
1329
4041
  settled = true;
1330
4042
  rejectPromise(error);
1331
4043
  };
1332
- this.socket?.addEventListener(
1333
- "open",
1334
- () => {
4044
+ const listeners = {
4045
+ open: () => {
1335
4046
  this.connected = true;
1336
- this.logger.info("connected to app-server", {
1337
- clientId: this.clientId,
1338
- url: this.url,
1339
- authenticated: Boolean(this.gatewayToken)
1340
- });
4047
+ this.logger.info(
4048
+ "connected to app-server",
4049
+ this.buildMetricsContext({
4050
+ url: this.url,
4051
+ authenticated: Boolean(this.gatewayToken)
4052
+ })
4053
+ );
1341
4054
  resolveOnce();
1342
4055
  },
1343
- { once: true }
1344
- );
1345
- this.socket?.addEventListener("error", () => {
1346
- const error = new Error(
1347
- `Failed to connect to App Server at ${this.url}`
1348
- );
1349
- this.lastError = sanitizeErrorForPersistence(error.message);
1350
- this.logger.error("failed to connect to app-server", {
1351
- clientId: this.clientId,
1352
- url: this.url,
1353
- error: this.lastError
1354
- });
1355
- rejectOnce(error);
1356
- });
1357
- this.socket?.addEventListener("close", () => {
1358
- this.connected = false;
1359
- this.initialized = false;
1360
- this.activeTurnId = null;
1361
- this.turnStartedAt = null;
1362
- this.logger.warn("disconnected from app-server", {
1363
- clientId: this.clientId,
1364
- url: this.url
1365
- });
1366
- this.rejectPending(new Error("App Server connection closed"));
1367
- });
1368
- this.socket?.addEventListener("message", (event) => {
1369
- void this.handleMessage(event.data);
1370
- });
4056
+ error: () => {
4057
+ const error = new Error(
4058
+ `Failed to connect to App Server at ${this.url}`
4059
+ );
4060
+ this.lastError = sanitizeErrorForPersistence(error.message);
4061
+ this.logger.error(
4062
+ "failed to connect to app-server",
4063
+ this.buildMetricsContext({
4064
+ url: this.url,
4065
+ error: this.lastError
4066
+ })
4067
+ );
4068
+ rejectOnce(error);
4069
+ },
4070
+ close: () => {
4071
+ this.connected = false;
4072
+ this.initialized = false;
4073
+ this.activeTurnId = null;
4074
+ this.turnStartedAt = null;
4075
+ this.detachSocketListeners(socket);
4076
+ if (this.socket === socket) {
4077
+ this.socket = null;
4078
+ }
4079
+ this.logger.warn(
4080
+ "disconnected from app-server",
4081
+ this.buildMetricsContext({
4082
+ url: this.url
4083
+ })
4084
+ );
4085
+ this.rejectPending(new Error("App Server connection closed"));
4086
+ },
4087
+ message: (event) => {
4088
+ const socketEvent = event;
4089
+ void this.handleMessage(socketEvent.data);
4090
+ }
4091
+ };
4092
+ this.socketListeners.set(socket, listeners);
4093
+ socket.addEventListener("open", listeners.open, { once: true });
4094
+ socket.addEventListener("error", listeners.error);
4095
+ socket.addEventListener("close", listeners.close);
4096
+ socket.addEventListener("message", listeners.message);
1371
4097
  });
1372
4098
  await this.request("initialize", {
1373
4099
  clientInfo: {
@@ -1382,13 +4108,18 @@ var AppServerClient = class {
1382
4108
  this.initialized = true;
1383
4109
  }
1384
4110
  async disconnect() {
1385
- if (!this.socket) {
4111
+ const socket = this.socket;
4112
+ if (!socket) {
1386
4113
  return;
1387
4114
  }
1388
- this.socket.close();
4115
+ this.detachSocketListeners(socket);
4116
+ this.socket = null;
1389
4117
  this.connected = false;
1390
4118
  this.initialized = false;
1391
- this.socket = null;
4119
+ this.activeTurnId = null;
4120
+ this.turnStartedAt = null;
4121
+ this.rejectPending(new Error("App Server connection disconnected"));
4122
+ socket.close();
1392
4123
  }
1393
4124
  async ensureThread(explicitThreadId, savedThread, cwd, ephemeral) {
1394
4125
  if (explicitThreadId) {
@@ -1416,10 +4147,6 @@ var AppServerClient = class {
1416
4147
  );
1417
4148
  }
1418
4149
  }
1419
- const loadedThreadId = await this.findLoadedThread(cwd);
1420
- if (loadedThreadId) {
1421
- return loadedThreadId;
1422
- }
1423
4150
  if (savedThread?.threadId) {
1424
4151
  if (savedThread.cwd && !threadCwdMatches(cwd, savedThread.cwd)) {
1425
4152
  this.logger.warn("saved thread cwd mismatch; skipping saved thread", {
@@ -1436,7 +4163,20 @@ var AppServerClient = class {
1436
4163
  });
1437
4164
  const resumedThreadId = resumeResponse?.thread?.id ?? savedThread.threadId;
1438
4165
  await this.refreshThreadState(resumedThreadId);
1439
- if (!threadCwdMatches(cwd, this.currentThreadCwd)) {
4166
+ if (this.isWaitingOnApproval()) {
4167
+ this.logger.warn(
4168
+ "saved thread is waiting on approval; starting fresh thread",
4169
+ {
4170
+ clientId: this.clientId,
4171
+ threadId: resumedThreadId
4172
+ }
4173
+ );
4174
+ this.threadId = null;
4175
+ this.currentThreadCwd = null;
4176
+ this.activeTurnId = null;
4177
+ this.turnStartedAt = null;
4178
+ this.lastTurnStatus = null;
4179
+ } else if (!threadCwdMatches(cwd, this.currentThreadCwd)) {
1440
4180
  this.logger.warn("saved thread resumed with mismatched cwd", {
1441
4181
  clientId: this.clientId,
1442
4182
  threadId: resumedThreadId,
@@ -1468,6 +4208,10 @@ var AppServerClient = class {
1468
4208
  }
1469
4209
  }
1470
4210
  }
4211
+ const loadedThreadId = await this.findLoadedThread(cwd);
4212
+ if (loadedThreadId) {
4213
+ return loadedThreadId;
4214
+ }
1471
4215
  const startResponse = await this.request("thread/start", {
1472
4216
  cwd,
1473
4217
  ephemeral,
@@ -1480,7 +4224,7 @@ var AppServerClient = class {
1480
4224
  }
1481
4225
  this.syncThreadStateFromThread(startResponse?.thread);
1482
4226
  this.threadId = startedThreadId;
1483
- this.currentThreadCwd = this.currentThreadCwd ?? cwd;
4227
+ this.currentThreadCwd = this.currentThreadCwd ?? normalizePersistedThreadCwd(cwd);
1484
4228
  this.activeTurnId = null;
1485
4229
  this.lastTurnStatus = null;
1486
4230
  this.logger.info("started thread", {
@@ -1592,6 +4336,9 @@ var AppServerClient = class {
1592
4336
  }
1593
4337
  return true;
1594
4338
  }
4339
+ isWaitingOnApproval() {
4340
+ return isWaitingApprovalStatus(this.lastTurnStatus);
4341
+ }
1595
4342
  async refreshCurrentThreadState() {
1596
4343
  if (!this.threadId) {
1597
4344
  return;
@@ -1621,7 +4368,7 @@ var AppServerClient = class {
1621
4368
  if (typeof thread?.id === "string") {
1622
4369
  this.threadId = thread.id;
1623
4370
  }
1624
- this.currentThreadCwd = typeof thread?.cwd === "string" ? thread.cwd : null;
4371
+ this.currentThreadCwd = typeof thread?.cwd === "string" ? normalizePersistedThreadCwd(thread.cwd) : null;
1625
4372
  let activeTurnId = null;
1626
4373
  let lastTurnStatus = null;
1627
4374
  const threadActiveFlags = Array.isArray(
@@ -1629,6 +4376,7 @@ var AppServerClient = class {
1629
4376
  ) ? thread.status.activeFlags : [];
1630
4377
  const threadStuckOnApproval = isTurnStuckOnApproval(threadActiveFlags);
1631
4378
  if (threadStuckOnApproval) {
4379
+ lastTurnStatus = "waitingOnApproval";
1632
4380
  this.logger.warn("thread waitingOnApproval; ignoring in-progress turns", {
1633
4381
  clientId: this.clientId,
1634
4382
  threadId: this.threadId
@@ -1645,6 +4393,7 @@ var AppServerClient = class {
1645
4393
  }
1646
4394
  const turnActiveFlags = Array.isArray(turn.activeFlags) ? turn.activeFlags : [];
1647
4395
  if (isTurnStuckOnApproval(turnActiveFlags)) {
4396
+ lastTurnStatus = "waitingOnApproval";
1648
4397
  this.logger.warn("turn waitingOnApproval; ignoring turn as active", {
1649
4398
  clientId: this.clientId,
1650
4399
  turnId: turn.id
@@ -1671,6 +4420,7 @@ var AppServerClient = class {
1671
4420
  return;
1672
4421
  }
1673
4422
  this.pending.delete(message.id);
4423
+ this.clearPendingTimeout(pending);
1674
4424
  if (message.error) {
1675
4425
  const errorText = formatJsonRpcError(message.error);
1676
4426
  this.lastError = sanitizeErrorForPersistence(errorText);
@@ -1688,6 +4438,31 @@ var AppServerClient = class {
1688
4438
  this.lastError = null;
1689
4439
  return;
1690
4440
  }
4441
+ if ((typeof message.id === "number" || typeof message.id === "string") && typeof message.method === "string") {
4442
+ this.lastNotificationMethod = message.method;
4443
+ this.lastNotificationAt = (/* @__PURE__ */ new Date()).toISOString();
4444
+ if (isAutoElicitationRequestMethod(message.method)) {
4445
+ const result = buildAutoElicitationResult(message.params);
4446
+ if (result) {
4447
+ this.sendJsonRpcResult(message.id, result);
4448
+ this.logger.info("auto-responded to elicitation request", {
4449
+ clientId: this.clientId,
4450
+ method: message.method,
4451
+ action: result.action
4452
+ });
4453
+ } else {
4454
+ this.sendJsonRpcResult(message.id, { action: "cancel" });
4455
+ this.logger.warn(
4456
+ "elicitation request missing usable params; cancelled",
4457
+ {
4458
+ clientId: this.clientId,
4459
+ method: message.method
4460
+ }
4461
+ );
4462
+ }
4463
+ return;
4464
+ }
4465
+ }
1691
4466
  if (!message.method) {
1692
4467
  return;
1693
4468
  }
@@ -1701,12 +4476,24 @@ var AppServerClient = class {
1701
4476
  }
1702
4477
  handleNotification(method, params) {
1703
4478
  switch (method) {
4479
+ case "notifications/claude/channel":
4480
+ this.logger.info("tap channel notification received", {
4481
+ clientId: this.clientId,
4482
+ source: params?.meta?.source ?? null,
4483
+ from: params?.meta?.from ?? null,
4484
+ to: params?.meta?.to ?? null,
4485
+ subject: params?.meta?.subject ?? null,
4486
+ filename: params?.meta?.filename ?? null
4487
+ });
4488
+ break;
1704
4489
  case "thread/started":
1705
4490
  if (params?.thread?.id) {
1706
4491
  this.threadId = params.thread.id;
1707
4492
  }
1708
4493
  if (typeof params?.thread?.cwd === "string") {
1709
- this.currentThreadCwd = params.thread.cwd;
4494
+ this.currentThreadCwd = normalizePersistedThreadCwd(
4495
+ params.thread.cwd
4496
+ );
1710
4497
  }
1711
4498
  this.logger.info("thread started notification", {
1712
4499
  clientId: this.clientId,
@@ -1756,11 +4543,16 @@ var AppServerClient = class {
1756
4543
  });
1757
4544
  break;
1758
4545
  default:
4546
+ this.logger.info("unhandled app-server notification", {
4547
+ clientId: this.clientId,
4548
+ method
4549
+ });
1759
4550
  break;
1760
4551
  }
1761
4552
  }
1762
4553
  request(method, params) {
1763
- if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
4554
+ const socket = this.socket;
4555
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
1764
4556
  throw new Error(`Cannot call ${method}; App Server socket is not open`);
1765
4557
  }
1766
4558
  const id = this.nextId;
@@ -1772,34 +4564,106 @@ var AppServerClient = class {
1772
4564
  params
1773
4565
  };
1774
4566
  return new Promise((resolvePromise, rejectPromise) => {
4567
+ const timeout = setTimeout(() => {
4568
+ const pending = this.pending.get(id);
4569
+ if (!pending) {
4570
+ return;
4571
+ }
4572
+ this.pending.delete(id);
4573
+ const errorText = `${method} timed out after ${this.requestTimeoutMs}ms`;
4574
+ this.lastError = sanitizeErrorForPersistence(errorText);
4575
+ this.logger.warn(
4576
+ "app-server request timed out",
4577
+ this.buildMetricsContext({
4578
+ method,
4579
+ requestId: id,
4580
+ timeoutMs: this.requestTimeoutMs
4581
+ })
4582
+ );
4583
+ pending.reject(new Error(errorText));
4584
+ }, this.requestTimeoutMs);
1775
4585
  this.pending.set(id, {
1776
4586
  resolve: resolvePromise,
1777
4587
  reject: rejectPromise,
1778
- method
4588
+ method,
4589
+ timeout
1779
4590
  });
1780
- this.socket?.send(JSON.stringify(request));
4591
+ try {
4592
+ socket.send(JSON.stringify(request));
4593
+ } catch (error) {
4594
+ const pending = this.pending.get(id);
4595
+ if (pending) {
4596
+ this.clearPendingTimeout(pending);
4597
+ this.pending.delete(id);
4598
+ }
4599
+ rejectPromise(error);
4600
+ }
1781
4601
  });
1782
4602
  }
4603
+ sendJsonRpcResult(id, result) {
4604
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
4605
+ return;
4606
+ }
4607
+ this.socket.send(JSON.stringify({ jsonrpc: "2.0", id, result }));
4608
+ }
1783
4609
  rejectPending(error) {
4610
+ if (this.pending.size > 0) {
4611
+ this.logger.warn(
4612
+ "rejecting pending app-server requests",
4613
+ this.buildMetricsContext({
4614
+ error: sanitizeErrorForPersistence(error.message)
4615
+ })
4616
+ );
4617
+ }
1784
4618
  for (const pending of this.pending.values()) {
4619
+ this.clearPendingTimeout(pending);
1785
4620
  pending.reject(error);
1786
4621
  }
1787
4622
  this.pending.clear();
1788
4623
  }
4624
+ clearPendingTimeout(pending) {
4625
+ if (pending.timeout !== null) {
4626
+ clearTimeout(pending.timeout);
4627
+ pending.timeout = null;
4628
+ }
4629
+ }
4630
+ detachSocketListeners(socket) {
4631
+ const listeners = this.socketListeners.get(socket);
4632
+ if (!listeners) {
4633
+ return;
4634
+ }
4635
+ socket.removeEventListener("open", listeners.open);
4636
+ socket.removeEventListener("error", listeners.error);
4637
+ socket.removeEventListener("close", listeners.close);
4638
+ socket.removeEventListener("message", listeners.message);
4639
+ this.socketListeners.delete(socket);
4640
+ }
4641
+ buildMetricsContext(context) {
4642
+ return {
4643
+ clientId: this.clientId,
4644
+ reconnectCount: Math.max(this.clientId - 1, 0),
4645
+ pendingCount: this.pending.size,
4646
+ rssMb: getProcessRssMb(),
4647
+ ...context
4648
+ };
4649
+ }
1789
4650
  };
1790
4651
 
1791
4652
  // scripts/bridge/bridge-main.ts
1792
- import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1793
- import { isAbsolute as isAbsolute3, join as join7, resolve as resolve4 } from "path";
4653
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
4654
+ import { isAbsolute as isAbsolute2, join as join8, resolve as resolve5 } from "path";
1794
4655
  import { pathToFileURL } from "url";
1795
4656
  function delay2(ms) {
1796
4657
  return new Promise((resolvePromise) => {
1797
4658
  setTimeout(resolvePromise, ms);
1798
4659
  });
1799
4660
  }
4661
+ function getProcessRssMb2() {
4662
+ return Math.round(process.memoryUsage().rss / (1024 * 1024));
4663
+ }
1800
4664
  function readHeartbeatState(stateDir) {
1801
- const heartbeatPath = join7(stateDir, "heartbeat.json");
1802
- if (!existsSync6(heartbeatPath)) {
4665
+ const heartbeatPath = join8(stateDir, "heartbeat.json");
4666
+ if (!existsSync7(heartbeatPath)) {
1803
4667
  return null;
1804
4668
  }
1805
4669
  try {
@@ -1823,7 +4687,7 @@ function hasValidHeartbeatThreadCwd(threadCwd) {
1823
4687
  if (!normalized) {
1824
4688
  return false;
1825
4689
  }
1826
- return isAbsolute3(normalized) || /^[A-Za-z]:[\\/]/.test(normalized) || normalized.startsWith("\\\\");
4690
+ return isAbsolute2(normalized) || /^[A-Za-z]:[\\/]/.test(normalized) || normalized.startsWith("\\\\");
1827
4691
  }
1828
4692
  function loadResumableThreadState(stateDir, fallbackAppServerUrl) {
1829
4693
  const savedThread = readThreadState(stateDir);
@@ -1843,7 +4707,9 @@ function loadResumableThreadState(stateDir, fallbackAppServerUrl) {
1843
4707
  updatedAt: heartbeat?.updatedAt ?? savedThread?.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1844
4708
  appServerUrl: heartbeat?.appServerUrl || savedThread?.appServerUrl || fallbackAppServerUrl,
1845
4709
  ephemeral: savedThread?.ephemeral ?? false,
1846
- cwd: heartbeat?.threadCwd ?? (savedThread?.threadId === heartbeatThreadId ? savedThread.cwd ?? null : null)
4710
+ cwd: normalizePersistedThreadCwd(
4711
+ heartbeat?.threadCwd ?? (savedThread?.threadId === heartbeatThreadId ? savedThread.cwd ?? null : null)
4712
+ )
1847
4713
  };
1848
4714
  let preferred = savedThread;
1849
4715
  if (!savedThread?.threadId) {
@@ -1853,7 +4719,9 @@ function loadResumableThreadState(stateDir, fallbackAppServerUrl) {
1853
4719
  ...savedThread,
1854
4720
  updatedAt: heartbeatBackedThread.updatedAt ?? savedThread.updatedAt,
1855
4721
  appServerUrl: heartbeatBackedThread.appServerUrl,
1856
- cwd: heartbeatBackedThread.cwd ?? savedThread.cwd ?? null
4722
+ cwd: normalizePersistedThreadCwd(
4723
+ heartbeatBackedThread.cwd ?? savedThread.cwd ?? null
4724
+ )
1857
4725
  };
1858
4726
  } else if (parseUpdatedAt(heartbeat?.updatedAt) > parseUpdatedAt(savedThread.updatedAt)) {
1859
4727
  preferred = heartbeatBackedThread;
@@ -1865,8 +4733,8 @@ function getGeneralInboxCutoff(stateDir, lookbackMinutes, processExistingMessage
1865
4733
  return /* @__PURE__ */ new Date(0);
1866
4734
  }
1867
4735
  const lookbackCutoff = lookbackMinutes > 0 ? new Date(Date.now() - lookbackMinutes * 6e4) : null;
1868
- const cutoffPath = join7(stateDir, "general-inbox-cutoff.txt");
1869
- if (existsSync6(cutoffPath)) {
4736
+ const cutoffPath = join8(stateDir, "general-inbox-cutoff.txt");
4737
+ if (existsSync7(cutoffPath)) {
1870
4738
  try {
1871
4739
  const saved = new Date(readFileSync6(cutoffPath, "utf8").trim());
1872
4740
  if (!isNaN(saved.getTime())) {
@@ -1882,7 +4750,7 @@ function getGeneralInboxCutoff(stateDir, lookbackMinutes, processExistingMessage
1882
4750
  return lookbackCutoff;
1883
4751
  }
1884
4752
  const cutoff = /* @__PURE__ */ new Date();
1885
- writeFileSync5(cutoffPath, `${cutoff.toISOString()}
4753
+ writeFileSync6(cutoffPath, `${cutoff.toISOString()}
1886
4754
  `, "utf8");
1887
4755
  return cutoff;
1888
4756
  }
@@ -1890,6 +4758,18 @@ async function main() {
1890
4758
  const options = buildOptions(process.argv.slice(2));
1891
4759
  configureBridgeLogging(options.logLevel);
1892
4760
  const logger = createBridgeLogger("bridge");
4761
+ const sweepLogger = createBridgeLogger("sweep");
4762
+ const sweepResult = sweepOrphanProcessedMarkers(options.stateDir, {
4763
+ logger: (msg, ctx) => sweepLogger.debug(msg, ctx)
4764
+ });
4765
+ if (sweepResult.scanned > 0) {
4766
+ logger.info("processed marker sweep", {
4767
+ scanned: sweepResult.scanned,
4768
+ removed: sweepResult.removed,
4769
+ kept: sweepResult.kept,
4770
+ errors: sweepResult.errors
4771
+ });
4772
+ }
1893
4773
  const cutoff = getGeneralInboxCutoff(
1894
4774
  options.stateDir,
1895
4775
  options.messageLookbackMinutes,
@@ -1962,9 +4842,9 @@ async function main() {
1962
4842
  }
1963
4843
  const scanResult = await runScan(options, cutoff, client);
1964
4844
  if (scanResult.dispatched && scanResult.maxMtimeMs > 0) {
1965
- const cutoffPath = join7(options.stateDir, "general-inbox-cutoff.txt");
4845
+ const cutoffPath = join8(options.stateDir, "general-inbox-cutoff.txt");
1966
4846
  const advancedCutoff = new Date(scanResult.maxMtimeMs);
1967
- writeFileSync5(cutoffPath, `${advancedCutoff.toISOString()}
4847
+ writeFileSync6(cutoffPath, `${advancedCutoff.toISOString()}
1968
4848
  `, "utf8");
1969
4849
  }
1970
4850
  if (scanResult.dispatched && client && options.waitAfterDispatchSeconds > 0) {
@@ -1990,11 +4870,15 @@ async function main() {
1990
4870
  const sanitized = sanitizeErrorForPersistence(message);
1991
4871
  throw new Error(sanitized ?? message);
1992
4872
  }
4873
+ const pendingCount = client?.getPendingRequestCount() ?? 0;
1993
4874
  client?.disconnect().catch(() => void 0);
1994
4875
  client = null;
1995
4876
  logger.warn("reconnecting after bridge error", {
1996
4877
  reconnectSeconds: options.reconnectSeconds,
1997
- consecutiveFailureCount: health.consecutiveFailureCount
4878
+ reconnectCount: health.consecutiveFailureCount,
4879
+ consecutiveFailureCount: health.consecutiveFailureCount,
4880
+ pendingCount,
4881
+ rssMb: getProcessRssMb2()
1998
4882
  });
1999
4883
  await delay2(options.reconnectSeconds * 1e3);
2000
4884
  }
@@ -2004,14 +4888,15 @@ async function main() {
2004
4888
  function isDirectExecution() {
2005
4889
  const entry = process.argv[1];
2006
4890
  if (!entry) return false;
2007
- return import.meta.url === pathToFileURL(resolve4(entry)).href;
4891
+ return import.meta.url === pathToFileURL(resolve5(entry)).href;
2008
4892
  }
2009
4893
 
2010
4894
  // src/bridges/codex-app-server-bridge.ts
2011
4895
  function isDirectExecution2() {
2012
4896
  const entry = process.argv[1];
2013
4897
  if (!entry) return false;
2014
- return import.meta.url === pathToFileURL2(resolve5(entry)).href;
4898
+ if (!basename2(entry).startsWith("codex-app-server-bridge")) return false;
4899
+ return import.meta.url === pathToFileURL2(resolve6(entry)).href;
2015
4900
  }
2016
4901
  if (isDirectExecution2()) {
2017
4902
  main().catch((error) => {
@@ -2027,7 +4912,11 @@ export {
2027
4912
  COMMS_HEARTBEAT_LOCK_TIMEOUT_MS,
2028
4913
  COMMS_LOCK_STALE_AGE_MS,
2029
4914
  DEFAULT_AGENT,
4915
+ DEFAULT_APP_SERVER_REQUEST_TIMEOUT_MS,
2030
4916
  DEFAULT_APP_SERVER_URL,
4917
+ DRIVE_ACTION_NOT_YET_SUPPORTED_REASON,
4918
+ DRIVE_NOT_YET_WIRED_REASON,
4919
+ FORBIDDEN_RAW_PAIR_TOKEN_REASON,
2031
4920
  HEADLESS_SKIP_PATTERNS,
2032
4921
  HEADLESS_WARMUP_PROMPT,
2033
4922
  HEADLESS_WARMUP_TIMEOUT_MS,
@@ -2036,6 +4925,7 @@ export {
2036
4925
  TURN_COMPLETION_POLL_MS,
2037
4926
  TURN_COMPLETION_REFRESH_MS,
2038
4927
  acquireCommsLock,
4928
+ buildAutoElicitationResult,
2039
4929
  buildDefaultStateDir,
2040
4930
  buildMarkerId,
2041
4931
  buildOptions,
@@ -2049,17 +4939,23 @@ export {
2049
4939
  getGeneralInboxCutoff,
2050
4940
  getInboxRoute,
2051
4941
  getInboxRouteFromFilename,
4942
+ getLastBridgeActivityAt,
2052
4943
  getPendingCandidates,
2053
4944
  getProcessedMarkerPath,
4945
+ isAutoElicitationRequestMethod,
2054
4946
  isDirectExecution,
2055
4947
  isOwnMessageSender,
2056
4948
  isTurnStale,
2057
4949
  isTurnStuckOnApproval,
4950
+ isWaitingApprovalStatus,
2058
4951
  loadHeartbeats,
2059
4952
  loadResumableThreadState,
2060
4953
  main,
4954
+ markBridgeActivity,
2061
4955
  maybeBootstrapHeadlessTurn,
2062
4956
  normalizeAgentToken,
4957
+ normalizePersistedThreadCwd,
4958
+ normalizeRoutingSlotEnv,
2063
4959
  normalizeThreadCwd,
2064
4960
  parseArgs,
2065
4961
  parseBridgeFrontmatter,
@@ -2087,6 +4983,8 @@ export {
2087
4983
  shouldRetrySteerAsStart,
2088
4984
  shouldSkipInHeadlessMode,
2089
4985
  stripBridgeFrontmatter,
4986
+ stripWindowsNamespacePrefix,
4987
+ sweepOrphanProcessedMarkers,
2090
4988
  threadCwdMatches,
2091
4989
  updateCommsHeartbeat,
2092
4990
  waitForTurnCompletion,