agent-embassy 1.2.0 → 1.3.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 (91) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/CONTRIBUTING.md +25 -7
  3. package/README.md +20 -12
  4. package/README.zh-CN.md +14 -13
  5. package/SECURITY.md +55 -41
  6. package/dist/src/gateway/claude-helper-supervisor.js +0 -1
  7. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
  8. package/dist/src/gateway/claude-runtime.d.ts +0 -2
  9. package/dist/src/gateway/claude-runtime.js +6 -17
  10. package/dist/src/gateway/claude-runtime.js.map +1 -1
  11. package/dist/src/gateway/cli-copy.d.ts +1 -1
  12. package/dist/src/gateway/cli-copy.en.d.ts +3 -1
  13. package/dist/src/gateway/cli-copy.en.js +5 -3
  14. package/dist/src/gateway/cli-copy.en.js.map +1 -1
  15. package/dist/src/gateway/cli-copy.js +2 -0
  16. package/dist/src/gateway/cli-copy.js.map +1 -1
  17. package/dist/src/gateway/cli-copy.zh-CN.d.ts +3 -1
  18. package/dist/src/gateway/cli-copy.zh-CN.js +5 -3
  19. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
  20. package/dist/src/gateway/cli.d.ts +2 -5
  21. package/dist/src/gateway/cli.js +30 -67
  22. package/dist/src/gateway/cli.js.map +1 -1
  23. package/dist/src/gateway/codex-app-server.d.ts +1 -7
  24. package/dist/src/gateway/codex-app-server.js +5 -52
  25. package/dist/src/gateway/codex-app-server.js.map +1 -1
  26. package/dist/src/gateway/codex-local-transport.d.ts +4 -10
  27. package/dist/src/gateway/codex-local-transport.js +13 -16
  28. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  29. package/dist/src/gateway/compatibility.d.ts +0 -27
  30. package/dist/src/gateway/compatibility.js +1 -101
  31. package/dist/src/gateway/compatibility.js.map +1 -1
  32. package/dist/src/gateway/config.d.ts +0 -3
  33. package/dist/src/gateway/config.js +0 -10
  34. package/dist/src/gateway/config.js.map +1 -1
  35. package/dist/src/gateway/control.d.ts +1 -20
  36. package/dist/src/gateway/control.js +2 -33
  37. package/dist/src/gateway/control.js.map +1 -1
  38. package/dist/src/gateway/dashboard-copy.d.ts +1 -1
  39. package/dist/src/gateway/dashboard-copy.en.d.ts +6 -7
  40. package/dist/src/gateway/dashboard-copy.en.js +15 -16
  41. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  42. package/dist/src/gateway/dashboard-copy.js +6 -7
  43. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  44. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +6 -7
  45. package/dist/src/gateway/dashboard-copy.zh-CN.js +15 -16
  46. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  47. package/dist/src/gateway/dashboard-model.d.ts +3 -7
  48. package/dist/src/gateway/dashboard-model.js +41 -16
  49. package/dist/src/gateway/dashboard-model.js.map +1 -1
  50. package/dist/src/gateway/dashboard.js +5 -7
  51. package/dist/src/gateway/dashboard.js.map +1 -1
  52. package/dist/src/gateway/live-dashboard-app/app.js +17 -45
  53. package/dist/src/gateway/live-dashboard-command.d.ts +5 -1
  54. package/dist/src/gateway/live-dashboard-command.js +8 -6
  55. package/dist/src/gateway/live-dashboard-command.js.map +1 -1
  56. package/dist/src/gateway/live-dashboard-http.d.ts +0 -4
  57. package/dist/src/gateway/live-dashboard-http.js +19 -93
  58. package/dist/src/gateway/live-dashboard-http.js.map +1 -1
  59. package/dist/src/gateway/live-dashboard-protocol.d.ts +1 -5
  60. package/dist/src/gateway/live-dashboard-protocol.js +0 -42
  61. package/dist/src/gateway/live-dashboard-protocol.js.map +1 -1
  62. package/dist/src/gateway/live-dashboard-server.d.ts +2 -2
  63. package/dist/src/gateway/live-dashboard-server.js +18 -4
  64. package/dist/src/gateway/live-dashboard-server.js.map +1 -1
  65. package/dist/src/gateway/live-dashboard.d.ts +3 -6
  66. package/dist/src/gateway/live-dashboard.js +6 -42
  67. package/dist/src/gateway/live-dashboard.js.map +1 -1
  68. package/dist/src/gateway/provenance-envelope.js +1 -1
  69. package/dist/src/gateway/provenance-envelope.js.map +1 -1
  70. package/dist/src/gateway/providers.d.ts +1 -21
  71. package/dist/src/gateway/providers.js +15 -261
  72. package/dist/src/gateway/providers.js.map +1 -1
  73. package/dist/src/gateway/server.js +0 -3
  74. package/dist/src/gateway/server.js.map +1 -1
  75. package/dist/src/gateway/service.d.ts +3 -15
  76. package/dist/src/gateway/service.js +76 -257
  77. package/dist/src/gateway/service.js.map +1 -1
  78. package/dist/src/gateway/store.js +76 -18
  79. package/dist/src/gateway/store.js.map +1 -1
  80. package/dist/src/gateway/types.d.ts +0 -4
  81. package/dist/src/gateway/types.js.map +1 -1
  82. package/docs/GATEWAY-ARCHITECTURE.md +75 -63
  83. package/package.json +1 -1
  84. package/skills/embassy-peer/SKILL.md +25 -10
  85. package/dist/src/gateway/claude-compatibility-scratch.d.ts +0 -18
  86. package/dist/src/gateway/claude-compatibility-scratch.js +0 -158
  87. package/dist/src/gateway/claude-compatibility-scratch.js.map +0 -1
  88. package/dist/src/gateway/live-dashboard-bootstrap.d.ts +0 -36
  89. package/dist/src/gateway/live-dashboard-bootstrap.js +0 -176
  90. package/dist/src/gateway/live-dashboard-bootstrap.js.map +0 -1
  91. package/docs/DESIGN.md +0 -134
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { BridgeError } from "../errors.js";
3
3
  import { KeyedMutex } from "../mutex.js";
4
- import { certifiedCompatibilityVersions, compatibilitySurfaces, attachCompatibilityCertification, evaluateCompatibilityAttestation, isCompatibilityAttestation, isCompatibilityCertification, isCompatibilityCertificationReport, isCompatibilityCheckReport, } from "./compatibility.js";
4
+ import { certifiedCompatibilityVersions, compatibilitySurfaces, evaluateCompatibilityAttestation, isCompatibilityAttestation, } from "./compatibility.js";
5
5
  import { createGatewayConversationId, isGatewaySnapshot, startGatewayControlServer, } from "./control.js";
6
6
  import { publishGatewayDashboard } from "./dashboard.js";
7
7
  import { createDeliveryMachine, isTerminalDeliveryMachine, projectDelivery, projectDeliveryWakeups, transitionDelivery, } from "./delivery-machine.js";
@@ -22,12 +22,6 @@ const NATIVE_HELD_THRESHOLD_MS = 1_000;
22
22
  const RUNTIME_ACTIVITY_CAPACITY = 256;
23
23
  const DELIVERY_SETTLEMENT_RETRY_MS = 250;
24
24
  const DELIVERY_DASHBOARD_REFRESH_MS = 15_000;
25
- const CLAUDE_CLEAN_PREWRITE_ROUTE_CODES = new Set([
26
- "CLAUDE_PEER_TARGET_UNKNOWN",
27
- "CLAUDE_PEER_TARGET_STALE",
28
- "CLAUDE_PEER_TARGET_CHANGED",
29
- "CLAUDE_PEER_WORKSPACE_UNATTESTED",
30
- ]);
31
25
  function progressWatchActivity(text, conversationId, actorAlias, explicitIdleMinutes) {
32
26
  const prefixedOpen = text.startsWith("TRACK:");
33
27
  const completionSignal = text.startsWith("DONE:");
@@ -69,9 +63,6 @@ function endpointRefreshCallbackKey(event) {
69
63
  ? {}
70
64
  : { safeErrorCode: probe.safeErrorCode }),
71
65
  })),
72
- ...(event.attestation.certification === undefined
73
- ? {}
74
- : { certification: { ...event.attestation.certification } }),
75
66
  ...(event.attestation.safeErrorCode === undefined
76
67
  ? {}
77
68
  : { safeErrorCode: event.attestation.safeErrorCode }),
@@ -145,8 +136,6 @@ export class GatewayService {
145
136
  activeDispatchByTarget = new Map();
146
137
  scheduledDispatchTargets = new Set();
147
138
  dispatchRunnerTargets = new Set();
148
- /** Claude sends that proved no write wait for exact provider-idle evidence. */
149
- claudePrewriteIdleWaitTargets = new Set();
150
139
  heldRedispatchTimers = new Map();
151
140
  pendingClaudeReplies = new Map();
152
141
  deliveryTrackers = new Map();
@@ -285,8 +274,8 @@ export class GatewayService {
285
274
  if (this.adapters.length > 0 &&
286
275
  this.adapters.every((adapter) => adapter.compatibilitySurface !== undefined &&
287
276
  adapter.runCompatibilityProbes !== undefined)) {
288
- const report = await this.compatibilityCheckLocked();
289
- const incompatible = report.surfaces.find((surface) => surface.tier === "incompatible");
277
+ const attestations = await this.runAutomaticCompatibilityProbesLocked();
278
+ const incompatible = attestations.find((surface) => surface.tier === "incompatible");
290
279
  if (incompatible !== undefined) {
291
280
  throw new BridgeError(incompatible.safeErrorCode ?? "COMPATIBILITY_CHECK_FAILED", "A local provider failed bounded compatibility attestation.");
292
281
  }
@@ -427,7 +416,6 @@ export class GatewayService {
427
416
  this.activeDispatchByTarget.clear();
428
417
  this.scheduledDispatchTargets.clear();
429
418
  this.dispatchRunnerTargets.clear();
430
- this.claudePrewriteIdleWaitTargets.clear();
431
419
  for (const timer of this.heldRedispatchTimers.values()) {
432
420
  this.timers.clearTimeout(timer);
433
421
  }
@@ -518,8 +506,6 @@ export class GatewayService {
518
506
  }),
519
507
  listSnapshot: async () => (await this.observeSnapshot()).snapshot,
520
508
  observeSnapshot: async () => await this.observeSnapshot(),
521
- compatibilityCheck: async () => await this.compatibilityCheck(),
522
- compatibilityCertify: async (params) => await this.compatibilityCertify(params),
523
509
  deliveryStatus: async (params) => await this.deliveryStatus(params.token),
524
510
  untrack: async (params) => await this.exclusiveDecision(async () => {
525
511
  const settled = await this.store.settleProgressWatch({
@@ -550,133 +536,7 @@ export class GatewayService {
550
536
  async snapshot() {
551
537
  return (await this.observeSnapshot()).snapshot;
552
538
  }
553
- async compatibilityCheck() {
554
- return await this.mutex.run("service", async () => await this.compatibilityCheckLocked());
555
- }
556
- async compatibilityCertify(params) {
557
- const plan = await this.mutex.run("service", async () => {
558
- if (!this.running || this.closing) {
559
- throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "Compatibility certification requires one running broker.");
560
- }
561
- const report = await this.compatibilityCheckLocked();
562
- const codexRoutes = (await this.store.inspectPrivateCodexRoutes()).filter((route) => route.enabled);
563
- const codexRoute = params.codexAlias === undefined
564
- ? codexRoutes.length === 1
565
- ? codexRoutes[0]
566
- : undefined
567
- : codexRoutes.find((route) => route.alias === params.codexAlias);
568
- if (codexRoute === undefined) {
569
- throw new BridgeError("COMPAT_CERTIFY_CODEX_ROUTE_REQUIRED", "Choose one exact registered Codex route for live certification.");
570
- }
571
- const adapters = new Map(this.adapters.map((adapter) => [adapter.identity.provider, adapter]));
572
- return { adapters, codexRoute, report };
573
- });
574
- const certifiedAt = this.now().toISOString();
575
- const updated = [];
576
- for (const attestation of plan.report.surfaces) {
577
- const adapter = plan.adapters.get(attestation.surface);
578
- let certification;
579
- if (attestation.tier === "incompatible" ||
580
- adapter?.runCompatibilityCertification === undefined) {
581
- certification = {
582
- depth: attestation.surface === "claude"
583
- ? "wire"
584
- : params.withTurn
585
- ? "turn"
586
- : "thread_ops",
587
- outcome: "fail",
588
- certifiedAt,
589
- safeErrorCode: attestation.safeErrorCode ??
590
- `${attestation.surface.toUpperCase()}_CERTIFICATION_UNAVAILABLE`,
591
- };
592
- }
593
- else {
594
- try {
595
- certification = await adapter.runCompatibilityCertification({
596
- controllerStateRoot: this.store.rootDir,
597
- withTurn: params.withTurn,
598
- ...(attestation.surface === "codex"
599
- ? { routeHandle: plan.codexRoute.binding.routeHandle }
600
- : {}),
601
- });
602
- }
603
- catch {
604
- certification = {
605
- depth: attestation.surface === "claude"
606
- ? "wire"
607
- : params.withTurn
608
- ? "turn"
609
- : "thread_ops",
610
- outcome: "fail",
611
- certifiedAt: this.now().toISOString(),
612
- safeErrorCode: `${attestation.surface.toUpperCase()}_CERTIFICATION_FAILED`,
613
- };
614
- }
615
- }
616
- const expectedDepth = attestation.surface === "claude"
617
- ? "wire"
618
- : params.withTurn
619
- ? "turn"
620
- : "thread_ops";
621
- if (!isCompatibilityCertification(certification) ||
622
- certification.depth !== expectedDepth) {
623
- certification = {
624
- depth: expectedDepth,
625
- outcome: "fail",
626
- certifiedAt: this.now().toISOString(),
627
- safeErrorCode: `${attestation.surface.toUpperCase()}_CERTIFICATION_EVIDENCE_INVALID`,
628
- };
629
- }
630
- updated.push(attachCompatibilityCertification(attestation, certification));
631
- }
632
- return await this.mutex.run("service", async () => {
633
- const currentCodexRoute = (await this.store.inspectPrivateCodexRoutes()).find((route) => route.enabled &&
634
- route.alias === plan.codexRoute.alias &&
635
- bindingKey(route.binding) === bindingKey(plan.codexRoute.binding));
636
- const reconciled = updated.map((attestation) => {
637
- if (attestation.surface === "codex" && currentCodexRoute === undefined) {
638
- return attachCompatibilityCertification(attestation, {
639
- depth: params.withTurn ? "turn" : "thread_ops",
640
- outcome: "fail",
641
- certifiedAt: this.now().toISOString(),
642
- safeErrorCode: "CODEX_CERTIFICATION_ROUTE_CHANGED",
643
- });
644
- }
645
- const observation = plan.adapters
646
- .get(attestation.surface)
647
- ?.compatibilitySurface?.();
648
- if (observation?.surface === attestation.surface &&
649
- observation.version === attestation.version) {
650
- return attestation;
651
- }
652
- return attachCompatibilityCertification(attestation, {
653
- depth: attestation.surface === "claude"
654
- ? "wire"
655
- : params.withTurn
656
- ? "turn"
657
- : "thread_ops",
658
- outcome: "fail",
659
- certifiedAt: this.now().toISOString(),
660
- safeErrorCode: `${attestation.surface.toUpperCase()}_CERTIFICATION_VERSION_CHANGED`,
661
- });
662
- });
663
- const result = {
664
- policy: plan.report.policy,
665
- certified: reconciled.every((attestation) => attestation.certification?.outcome === "pass"),
666
- withTurn: params.withTurn,
667
- surfaces: reconciled,
668
- };
669
- if (!isCompatibilityCertificationReport(result)) {
670
- throw new BridgeError("COMPAT_CERTIFICATION_REPORT_INVALID", "Compatibility certification evidence failed its closed report schema.");
671
- }
672
- for (const attestation of reconciled) {
673
- await this.store.recordCompatibilityAttestation(attestation);
674
- }
675
- await this.changed();
676
- return result;
677
- });
678
- }
679
- async compatibilityCheckLocked() {
539
+ async runAutomaticCompatibilityProbesLocked() {
680
540
  const bySurface = new Map(this.adapters.map((adapter) => {
681
541
  if (adapter.compatibilitySurface === undefined ||
682
542
  adapter.runCompatibilityProbes === undefined) {
@@ -695,39 +555,21 @@ export class GatewayService {
695
555
  if (entry === undefined) {
696
556
  throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "A required compatibility surface is unavailable.");
697
557
  }
698
- const cached = await this.store.inspectCompatibilityAttestation(surface, entry.observation.version);
699
- const probes = cached?.probes ?? (await entry.adapter.runCompatibilityProbes());
700
- const checkedAt = cached?.checkedAt ?? this.now().toISOString();
701
- let attestation = evaluateCompatibilityAttestation({
558
+ const probes = await entry.adapter.runCompatibilityProbes();
559
+ const attestation = evaluateCompatibilityAttestation({
702
560
  surface,
703
561
  version: entry.observation.version,
704
- checkedAt,
705
- policy: this.config.compatibilityPolicy ?? "observed",
562
+ checkedAt: this.now().toISOString(),
706
563
  certifiedVersions: certifiedCompatibilityVersions[surface],
707
564
  probes,
708
565
  });
709
- if (cached?.certification !== undefined &&
710
- attestation.tier !== "incompatible") {
711
- attestation = attachCompatibilityCertification(attestation, cached.certification);
712
- }
713
- if (cached === undefined ||
714
- JSON.stringify(cached) !== JSON.stringify(attestation)) {
715
- await this.store.recordCompatibilityAttestation(attestation);
716
- }
566
+ await this.store.recordCompatibilityAttestation(attestation);
717
567
  if (attestation.tier !== "incompatible") {
718
568
  entry.adapter.acceptCompatibilityAttestation?.(attestation);
719
569
  }
720
570
  attestations.push(attestation);
721
571
  }
722
- const report = {
723
- policy: this.config.compatibilityPolicy ?? "observed",
724
- compatible: attestations.every((attestation) => attestation.tier !== "incompatible"),
725
- surfaces: attestations,
726
- };
727
- if (!isCompatibilityCheckReport(report)) {
728
- throw new BridgeError("COMPAT_REPORT_INVALID", "Compatibility evidence failed its closed report schema.");
729
- }
730
- return report;
572
+ return attestations;
731
573
  }
732
574
  async observeSnapshot() {
733
575
  return await this.mutex.run("service", async () => {
@@ -2746,13 +2588,22 @@ export class GatewayService {
2746
2588
  const publicBefore = await this.discoveryPublicFingerprint();
2747
2589
  this.candidates.clear();
2748
2590
  const rowsByAlias = new Map();
2591
+ const collisionByRouteHandle = new Map();
2749
2592
  const discoveredCandidates = [];
2750
2593
  const observedSelected = new Set();
2751
2594
  for (const adapter of this.adapters.filter((item) => item.identity.provider === "claude")) {
2752
- const discovered = (await adapter.discoverClaudePeers?.()) ?? {
2595
+ let discovered = {
2753
2596
  peers: [],
2754
2597
  complete: false,
2755
2598
  };
2599
+ try {
2600
+ discovered = (await adapter.discoverClaudePeers?.()) ?? discovered;
2601
+ }
2602
+ catch {
2603
+ // Registry observation is dashboard evidence, not Claude mailbox
2604
+ // authority. The provider still revalidates the exact target before
2605
+ // any write.
2606
+ }
2756
2607
  const grouped = new Map();
2757
2608
  const byHandle = new Map();
2758
2609
  for (const peer of discovered.peers) {
@@ -2772,6 +2623,8 @@ export class GatewayService {
2772
2623
  if (peer === undefined)
2773
2624
  continue;
2774
2625
  if (matches.length !== 1) {
2626
+ for (const match of matches)
2627
+ collisionByRouteHandle.set(match.routeHandle, "PEER_ALIAS_COLLISION");
2775
2628
  rowsByAlias.set(alias, {
2776
2629
  peer,
2777
2630
  adapter,
@@ -2788,6 +2641,7 @@ export class GatewayService {
2788
2641
  continue;
2789
2642
  }
2790
2643
  if ((byHandle.get(peer.routeHandle)?.length ?? 0) !== 1) {
2644
+ collisionByRouteHandle.set(peer.routeHandle, "PEER_SESSION_COLLISION");
2791
2645
  rowsByAlias.set(alias, {
2792
2646
  peer,
2793
2647
  adapter,
@@ -2813,6 +2667,8 @@ export class GatewayService {
2813
2667
  const collision = this.routeBindings.get(alias);
2814
2668
  if (collision !== undefined &&
2815
2669
  bindingKey(collision) !== bindingKey(existing[1])) {
2670
+ collisionByRouteHandle.set(routeHandle, "PEER_ALIAS_COLLISION");
2671
+ collisionByRouteHandle.set(collision.routeHandle, "PEER_ALIAS_COLLISION");
2816
2672
  rowsByAlias.set(alias, {
2817
2673
  peer: candidate,
2818
2674
  adapter,
@@ -2827,10 +2683,16 @@ export class GatewayService {
2827
2683
  catch (error) {
2828
2684
  if (!(error instanceof BridgeError))
2829
2685
  throw error;
2686
+ // Only a real alias collision may wear that receipt; every other
2687
+ // rename failure is an unobserved peer, not a name conflict.
2688
+ const safeErrorCode = error.code === "ROUTE_ALIAS_COLLISION"
2689
+ ? "PEER_ALIAS_COLLISION"
2690
+ : "PEER_NOT_OBSERVED";
2691
+ collisionByRouteHandle.set(routeHandle, safeErrorCode);
2830
2692
  rowsByAlias.set(alias, {
2831
2693
  peer: candidate,
2832
2694
  adapter,
2833
- safeErrorCode: "PEER_ALIAS_COLLISION",
2695
+ safeErrorCode,
2834
2696
  });
2835
2697
  }
2836
2698
  }
@@ -2899,6 +2761,9 @@ export class GatewayService {
2899
2761
  observedSelected.has(bindingKey(binding))) {
2900
2762
  continue;
2901
2763
  }
2764
+ const discoveryError = collisionByRouteHandle.get(binding.routeHandle);
2765
+ if (discoveryError === undefined)
2766
+ continue;
2902
2767
  const inspection = await this.store.inspectPrivateRoute(alias);
2903
2768
  if (inspection !== undefined &&
2904
2769
  inspection.enabled &&
@@ -2906,9 +2771,9 @@ export class GatewayService {
2906
2771
  await this.drainPreDeadlineDeliveryCallbacksLocked();
2907
2772
  const inFlightSettlements = await this.planRouteInFlightSettlementsLocked(alias, {
2908
2773
  unwrittenOutcome: "failed",
2909
- safeErrorCode: "PEER_NOT_OBSERVED",
2774
+ safeErrorCode: discoveryError,
2910
2775
  });
2911
- const settlements = await this.store.invalidateRoute(binding, "PEER_NOT_OBSERVED", inFlightSettlements);
2776
+ const settlements = await this.store.invalidateRoute(binding, discoveryError, inFlightSettlements);
2912
2777
  for (const settlement of settlements) {
2913
2778
  await this.applyTerminalSettlementLocked(settlement);
2914
2779
  }
@@ -3135,6 +3000,11 @@ export class GatewayService {
3135
3000
  async resolveSelectedClaudeDestination(selector) {
3136
3001
  const discoveryChanged = await this.refreshClaudeDiscovery();
3137
3002
  try {
3003
+ const selected = [...this.routeBindings.entries()].find(([alias, binding]) => binding.provider === "claude" &&
3004
+ (alias === selector ||
3005
+ binding.routeHandle.toLowerCase() === selector.toLowerCase()));
3006
+ if (selected !== undefined)
3007
+ return { alias: selected[0], discoveryChanged };
3138
3008
  const candidate = this.claudeCandidate(selector);
3139
3009
  if (candidate === undefined) {
3140
3010
  throw new BridgeError("PEER_NOT_FOUND", "No unique compatible interactive peer matches that current name or session UUID.");
@@ -3327,9 +3197,6 @@ export class GatewayService {
3327
3197
  this.routeBindings.set(alias, binding);
3328
3198
  this.routeStates.set(alias, state);
3329
3199
  this.bindingAliases.set(bindingKey(binding), alias);
3330
- if (binding.provider === "claude" && state === "idle") {
3331
- this.claudePrewriteIdleWaitTargets.delete(alias);
3332
- }
3333
3200
  }
3334
3201
  forgetBinding(alias) {
3335
3202
  const heldTimer = this.heldRedispatchTimers.get(alias);
@@ -3344,7 +3211,6 @@ export class GatewayService {
3344
3211
  }
3345
3212
  this.routeBindings.delete(alias);
3346
3213
  this.routeStates.delete(alias);
3347
- this.claudePrewriteIdleWaitTargets.delete(alias);
3348
3214
  for (const [messageId, continuation] of this.providerTurnContinuations) {
3349
3215
  if (continuation.targetAlias !== alias)
3350
3216
  continue;
@@ -3453,8 +3319,10 @@ export class GatewayService {
3453
3319
  enqueuedAt: input.enqueuedAt,
3454
3320
  stallAt,
3455
3321
  deadlineAt,
3456
- // Deferred means the provider proved no write. Permit one bounded
3457
- // attempt per 500 ms slice until this message's immutable deadline.
3322
+ // Deferred means the provider proved no write. This budgets one
3323
+ // attempt per 500 ms slice of this message's immutable deadline; a
3324
+ // Codex-bound retry spends one slice each, while a Claude-bound retry
3325
+ // backs off exponentially and spends far fewer of them.
3458
3326
  maxCleanPrewriteRetries: Math.max(0, Math.ceil((deadlineAt - input.enqueuedAt) / 500)),
3459
3327
  nativeReceipt: input.nativeReceipt !== undefined,
3460
3328
  maxNativeReceiptCleanPrewriteRetries: DELIVERY_ACK_RETRY_DELAYS_MS.length,
@@ -4071,12 +3939,10 @@ export class GatewayService {
4071
3939
  `Embassy 自动活跃检查 — ${action.conversationId}]\n` +
4072
3940
  "Reply with the result or a status. / 请回复结果或状态。";
4073
3941
  try {
4074
- await this.enqueue(action.ownerAlias, action.workerAlias, text, false, false, {
3942
+ await this.enqueue(action.ownerAlias, action.workerAlias, text, false, {
4075
3943
  existingConversationId: action.conversationId,
4076
- requestedHopCount: 0,
4077
3944
  exposeDeliveryToken: false,
4078
3945
  skipProgressWatchActivity: true,
4079
- preserveConversationHop: true,
4080
3946
  progressWatchNudge: {
4081
3947
  conversationId: action.conversationId,
4082
3948
  nudgeNumber: action.nudgeNumber,
@@ -4123,8 +3989,7 @@ export class GatewayService {
4123
3989
  }
4124
3990
  if (!isTerminalDeliveryMachine(tracker.machine) &&
4125
3991
  tracker.machine.dispatchRetryAt !== null &&
4126
- tracker.machine.dispatchRetryAt <= now &&
4127
- !this.claudePrewriteIdleWaitTargets.has(tracker.targetAlias)) {
3992
+ tracker.machine.dispatchRetryAt <= now) {
4128
3993
  if (!this.dispatchRunnerTargets.has(tracker.targetAlias)) {
4129
3994
  this.scheduleDispatch(tracker.targetAlias);
4130
3995
  }
@@ -4213,7 +4078,6 @@ export class GatewayService {
4213
4078
  if (wakeups.deadlineAt !== undefined)
4214
4079
  consider(wakeups.deadlineAt);
4215
4080
  if (wakeups.dispatchRetryAt !== undefined &&
4216
- !this.claudePrewriteIdleWaitTargets.has(tracker.targetAlias) &&
4217
4081
  !this.scheduledDispatchTargets.has(tracker.targetAlias) &&
4218
4082
  !this.dispatchRunnerTargets.has(tracker.targetAlias)) {
4219
4083
  consider(wakeups.dispatchRetryAt);
@@ -4311,7 +4175,7 @@ export class GatewayService {
4311
4175
  await this.verifyCodex(params.fromAlias, params.threadId);
4312
4176
  const resolved = await this.resolveSelectedClaudeDestination(params.toAlias);
4313
4177
  discoveryChanged = resolved.discoveryChanged;
4314
- return this.acceptedControlResult(await this.enqueue(params.fromAlias, resolved.alias, params.text, params.expectsReply, false, params.trackIdleMinutes === undefined
4178
+ return this.acceptedControlResult(await this.enqueue(params.fromAlias, resolved.alias, params.text, params.expectsReply, params.trackIdleMinutes === undefined
4315
4179
  ? {}
4316
4180
  : { trackIdleMinutes: params.trackIdleMinutes }));
4317
4181
  }
@@ -4330,7 +4194,7 @@ export class GatewayService {
4330
4194
  try {
4331
4195
  await this.verifyClaude(params.fromAlias, params.replyAddress);
4332
4196
  const steer = this.config.steeringEnabled && params.text.startsWith("STEER:");
4333
- return this.acceptedControlResult(await this.enqueue(params.fromAlias, params.toAlias, params.text, steer ? false : params.expectsReply, false, {
4197
+ return this.acceptedControlResult(await this.enqueue(params.fromAlias, params.toAlias, params.text, steer ? false : params.expectsReply, {
4334
4198
  ...(steer ? { steer: true } : {}),
4335
4199
  ...(params.trackIdleMinutes === undefined
4336
4200
  ? {}
@@ -4359,10 +4223,9 @@ export class GatewayService {
4359
4223
  throw new BridgeError("ROUTE_MISMATCH", "The caller is not a conversation participant.");
4360
4224
  const result = caller.kind === "codex" &&
4361
4225
  this.nativeIngressByConversation.has(conversation.id)
4362
- ? await this.enqueueNativeReply(conversation, from, params.text, conversation.lastHopCount + 1, true, params.trackIdleMinutes)
4363
- : await this.enqueue(from, to, params.text, false, true, {
4226
+ ? await this.enqueueNativeReply(conversation, from, params.text, true, params.trackIdleMinutes)
4227
+ : await this.enqueue(from, to, params.text, false, {
4364
4228
  existingConversationId: conversation.id,
4365
- requestedHopCount: conversation.lastHopCount + 1,
4366
4229
  ...(params.trackIdleMinutes === undefined
4367
4230
  ? {}
4368
4231
  : { trackIdleMinutes: params.trackIdleMinutes }),
@@ -4374,7 +4237,7 @@ export class GatewayService {
4374
4237
  }
4375
4238
  });
4376
4239
  }
4377
- async enqueue(sourceAlias, targetAlias, text, expectsReply, isReply, options = {}) {
4240
+ async enqueue(sourceAlias, targetAlias, text, expectsReply, options = {}) {
4378
4241
  if (this.codexSuccessionPoisoned.has(sourceAlias) ||
4379
4242
  this.codexSuccessionPoisoned.has(targetAlias)) {
4380
4243
  throw new BridgeError("CODEX_SUCCESSION_RECOVERY_REQUIRED", "No message can be accepted while Codex succession requires manual recovery.");
@@ -4397,11 +4260,8 @@ export class GatewayService {
4397
4260
  targetAlias,
4398
4261
  expectsReply,
4399
4262
  nextSequence: 0,
4400
- lastHopCount: 0,
4401
4263
  lastActivityAt: this.now().toISOString(),
4402
4264
  };
4403
- const hopCount = options.requestedHopCount ??
4404
- (isReply ? conversation.lastHopCount + 1 : 0);
4405
4265
  const sequence = conversation.nextSequence;
4406
4266
  const enqueuedAt = this.now().getTime();
4407
4267
  const deliveryToken = (options.exposeDeliveryToken ?? true)
@@ -4414,7 +4274,6 @@ export class GatewayService {
4414
4274
  body: text,
4415
4275
  dedupeKey: `${conversationId}:${sequence}`,
4416
4276
  conversationIdSuffix: conversationId.slice(-8),
4417
- hopCount,
4418
4277
  deadlineAt,
4419
4278
  ...(options.steer === true ? { steer: true } : {}),
4420
4279
  ...(options.skipProgressWatchActivity === true
@@ -4435,15 +4294,10 @@ export class GatewayService {
4435
4294
  if (queued.pair === true)
4436
4295
  conversation.pair = true;
4437
4296
  conversation.nextSequence += 1;
4438
- if (options.preserveConversationHop !== true) {
4439
- conversation.lastHopCount = hopCount;
4440
- }
4441
4297
  conversation.lastActivityAt = this.now().toISOString();
4442
4298
  this.messageContexts.set(queued.messageId, {
4443
4299
  conversationId,
4444
- isReply,
4445
4300
  expectsReply,
4446
- hopCount,
4447
4301
  sequence,
4448
4302
  targetBindingKey: bindingKey(target),
4449
4303
  authorization: "selected_route",
@@ -4462,7 +4316,6 @@ export class GatewayService {
4462
4316
  this.pendingClaudeReplies.set(bindingKey(target), {
4463
4317
  conversationId,
4464
4318
  bindingKey: bindingKey(target),
4465
- hopCount,
4466
4319
  deadlineAt,
4467
4320
  tainted: false,
4468
4321
  });
@@ -4476,7 +4329,7 @@ export class GatewayService {
4476
4329
  ...(deliveryToken === undefined ? {} : { deliveryToken }),
4477
4330
  };
4478
4331
  }
4479
- async enqueueNativeReply(conversation, sourceAlias, text, requestedHopCount, exposeDeliveryToken = true, trackIdleMinutes) {
4332
+ async enqueueNativeReply(conversation, sourceAlias, text, exposeDeliveryToken = true, trackIdleMinutes) {
4480
4333
  this.pruneTransient();
4481
4334
  const capability = this.nativeIngressByConversation.get(conversation.id);
4482
4335
  if (capability === undefined ||
@@ -4500,7 +4353,6 @@ export class GatewayService {
4500
4353
  body: text,
4501
4354
  dedupeKey: `${conversation.id}:${sequence}`,
4502
4355
  conversationIdSuffix: conversation.id.slice(-8),
4503
- hopCount: requestedHopCount,
4504
4356
  deadlineAt,
4505
4357
  ...(conversation.pair === true ? { pair: true } : {}),
4506
4358
  progressWatch: progressWatchActivity(text, conversation.id, sourceAlias, trackIdleMinutes),
@@ -4509,13 +4361,10 @@ export class GatewayService {
4509
4361
  throw new BridgeError("MESSAGE_REJECTED", "The native reply was not accepted.");
4510
4362
  }
4511
4363
  conversation.nextSequence += 1;
4512
- conversation.lastHopCount = requestedHopCount;
4513
4364
  conversation.lastActivityAt = this.now().toISOString();
4514
4365
  this.messageContexts.set(queued.messageId, {
4515
4366
  conversationId: conversation.id,
4516
- isReply: true,
4517
4367
  expectsReply: false,
4518
- hopCount: requestedHopCount,
4519
4368
  sequence,
4520
4369
  targetBindingKey: bindingKey(capability.binding),
4521
4370
  nativeReplyBinding: { ...capability.binding },
@@ -4547,7 +4396,7 @@ export class GatewayService {
4547
4396
  * native-ingress boundary; this preserves the exact connector generation
4548
4397
  * without recreating route authority or accepting a new unsolicited send.
4549
4398
  */
4550
- async enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, text, requestedHopCount) {
4399
+ async enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, text) {
4551
4400
  const target = await this.store.resolveRoute(conversation.sourceAlias);
4552
4401
  if (sourceBinding.provider !== "claude" ||
4553
4402
  target.provider !== "codex" ||
@@ -4566,7 +4415,6 @@ export class GatewayService {
4566
4415
  body: text,
4567
4416
  dedupeKey: `${conversation.id}:${sequence}`,
4568
4417
  conversationIdSuffix: conversation.id.slice(-8),
4569
- hopCount: requestedHopCount,
4570
4418
  deadlineAt,
4571
4419
  authorizedPairTeardownReply: true,
4572
4420
  progressWatch: progressWatchActivity(text, conversation.id, conversation.targetAlias),
@@ -4575,16 +4423,13 @@ export class GatewayService {
4575
4423
  throw new BridgeError("MESSAGE_REJECTED", "The retained provider reply was not accepted.");
4576
4424
  }
4577
4425
  conversation.nextSequence += 1;
4578
- conversation.lastHopCount = requestedHopCount;
4579
4426
  conversation.lastActivityAt = this.now().toISOString();
4580
4427
  // The old edge has already been removed. Retain only this already-observed
4581
4428
  // reply attempt; the conversation no longer grants pair reply authority.
4582
4429
  delete conversation.pair;
4583
4430
  this.messageContexts.set(queued.messageId, {
4584
4431
  conversationId: conversation.id,
4585
- isReply: true,
4586
4432
  expectsReply: false,
4587
- hopCount: requestedHopCount,
4588
4433
  sequence,
4589
4434
  targetBindingKey: bindingKey(target),
4590
4435
  authorization: "selected_route",
@@ -4695,15 +4540,12 @@ export class GatewayService {
4695
4540
  targetAlias: item.targetAlias,
4696
4541
  expectsReply: false,
4697
4542
  nextSequence: 1,
4698
- lastHopCount: item.hopCount,
4699
4543
  lastActivityAt: this.now().toISOString(),
4700
4544
  ...(item.pair === true ? { pair: true } : {}),
4701
4545
  };
4702
4546
  const context = {
4703
4547
  conversationId,
4704
- isReply: false,
4705
4548
  expectsReply: false,
4706
- hopCount: item.hopCount,
4707
4549
  sequence: 0,
4708
4550
  targetBindingKey: bindingKey(binding),
4709
4551
  authorization: "selected_route",
@@ -4726,8 +4568,7 @@ export class GatewayService {
4726
4568
  if (!this.running ||
4727
4569
  this.closing ||
4728
4570
  this.codexSuccessionDispatchFrozen.has(targetAlias) ||
4729
- this.codexSuccessionPoisoned.has(targetAlias) ||
4730
- this.claudePrewriteIdleWaitTargets.has(targetAlias)) {
4571
+ this.codexSuccessionPoisoned.has(targetAlias)) {
4731
4572
  return;
4732
4573
  }
4733
4574
  if (await this.processLifecycleLocked()) {
@@ -4785,8 +4626,8 @@ export class GatewayService {
4785
4626
  }
4786
4627
  if (context.authorization === "selected_route") {
4787
4628
  const routeState = this.routeStates.get(targetAlias);
4788
- const dispatchable = routeState === "idle" ||
4789
- (binding.provider === "claude" && routeState === "busy") ||
4629
+ const dispatchable = binding.provider === "claude" ||
4630
+ routeState === "idle" ||
4790
4631
  (binding.provider === "codex" &&
4791
4632
  item.steer === true &&
4792
4633
  routeState === "busy");
@@ -4814,8 +4655,8 @@ export class GatewayService {
4814
4655
  this.scheduleDispatch(targetAlias);
4815
4656
  return;
4816
4657
  }
4817
- const storedDispatchable = inspection?.state === "idle" ||
4818
- (binding.provider === "claude" && inspection?.state === "busy") ||
4658
+ const storedDispatchable = binding.provider === "claude" ||
4659
+ inspection?.state === "idle" ||
4819
4660
  (binding.provider === "codex" &&
4820
4661
  item.steer === true &&
4821
4662
  inspection?.state === "busy");
@@ -4948,10 +4789,19 @@ export class GatewayService {
4948
4789
  const currentTargetAlias = this.bindingAliases.get(bindingKey(binding)) ?? targetAlias;
4949
4790
  const now = this.now().getTime();
4950
4791
  const deferredSafeErrorCode = safeCode(result.safeErrorCode, "PROVIDER_DISPATCH_DEFERRED");
4792
+ const retryDelayMs = binding.provider === "claude"
4793
+ ? Math.min(500 * 2 ** (tracker.machine.dispatchAttempt - 1), 300_000)
4794
+ : 500;
4795
+ // Only Claude's backoff is clamped inside the deadline, so a long delay
4796
+ // still buys one last attempt. A Codex-bound retry keeps its exact
4797
+ // unclamped slice: a slice past the deadline must settle now, not wait.
4798
+ const retryAt = binding.provider === "claude"
4799
+ ? Math.min(now + retryDelayMs, tracker.deadlineAt - 1)
4800
+ : now + retryDelayMs;
4951
4801
  await this.advanceDeliveryLocked(item.messageId, {
4952
4802
  type: "dispatch_clean_prewrite_failed",
4953
4803
  at: now,
4954
- retryAt: now + 500,
4804
+ retryAt,
4955
4805
  safeErrorCode: deferredSafeErrorCode,
4956
4806
  });
4957
4807
  tracker.deliverySafeErrorCode = deferredSafeErrorCode;
@@ -4979,12 +4829,7 @@ export class GatewayService {
4979
4829
  }
4980
4830
  if (requeued.status === "requeued") {
4981
4831
  if (context.authorization === "selected_route") {
4982
- const waitForClaudeIdle = binding.provider === "claude" &&
4983
- CLAUDE_CLEAN_PREWRITE_ROUTE_CODES.has(deferredSafeErrorCode);
4984
- if (waitForClaudeIdle) {
4985
- this.claudePrewriteIdleWaitTargets.add(currentTargetAlias);
4986
- }
4987
- else if (!steeringWhileBusy) {
4832
+ if (binding.provider === "codex" && !steeringWhileBusy) {
4988
4833
  this.routeStates.set(currentTargetAlias, "idle");
4989
4834
  await this.store.observeRoute({
4990
4835
  binding,
@@ -4992,7 +4837,7 @@ export class GatewayService {
4992
4837
  compatibility: "compatible",
4993
4838
  });
4994
4839
  }
4995
- else {
4840
+ else if (binding.provider === "codex") {
4996
4841
  this.scheduleHeldRedispatch(currentTargetAlias);
4997
4842
  }
4998
4843
  await this.setNativeCodexStatus(currentTargetAlias, "waiting");
@@ -5024,8 +4869,7 @@ export class GatewayService {
5024
4869
  scheduleDispatch(targetAlias) {
5025
4870
  if (this.closing ||
5026
4871
  this.codexSuccessionDispatchFrozen.has(targetAlias) ||
5027
- this.codexSuccessionPoisoned.has(targetAlias) ||
5028
- this.claudePrewriteIdleWaitTargets.has(targetAlias)) {
4872
+ this.codexSuccessionPoisoned.has(targetAlias)) {
5029
4873
  return;
5030
4874
  }
5031
4875
  if (this.scheduledDispatchTargets.has(targetAlias))
@@ -5198,7 +5042,7 @@ export class GatewayService {
5198
5042
  conversation !== undefined) {
5199
5043
  try {
5200
5044
  if (this.nativeIngressByConversation.has(conversation.id)) {
5201
- await this.enqueueNativeReply(conversation, conversation.targetAlias, replyText, context.hopCount + 1, false);
5045
+ await this.enqueueNativeReply(conversation, conversation.targetAlias, replyText, false);
5202
5046
  }
5203
5047
  else {
5204
5048
  const sourceBinding = this.routeBindings.get(conversation.targetAlias);
@@ -5213,12 +5057,11 @@ export class GatewayService {
5213
5057
  }));
5214
5058
  if ((!sourceStillRoutable || !pairStillActive) &&
5215
5059
  sourceBinding?.provider === "claude") {
5216
- await this.enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, replyText, context.hopCount + 1);
5060
+ await this.enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, replyText);
5217
5061
  }
5218
5062
  else {
5219
- await this.enqueue(conversation.targetAlias, conversation.sourceAlias, replyText, false, true, {
5063
+ await this.enqueue(conversation.targetAlias, conversation.sourceAlias, replyText, false, {
5220
5064
  existingConversationId: conversation.id,
5221
- requestedHopCount: context.hopCount + 1,
5222
5065
  exposeDeliveryToken: false,
5223
5066
  });
5224
5067
  }
@@ -5279,9 +5122,8 @@ export class GatewayService {
5279
5122
  if (conversation === undefined)
5280
5123
  return;
5281
5124
  this.pendingClaudeReplies.delete(key);
5282
- await this.enqueue(conversation.targetAlias, conversation.sourceAlias, event.text, false, true, {
5125
+ await this.enqueue(conversation.targetAlias, conversation.sourceAlias, event.text, false, {
5283
5126
  existingConversationId: conversation.id,
5284
- requestedHopCount: pending.hopCount + 1,
5285
5127
  exposeDeliveryToken: false,
5286
5128
  });
5287
5129
  }
@@ -5324,7 +5166,6 @@ export class GatewayService {
5324
5166
  body: event.text,
5325
5167
  dedupeKey: `${conversationId}:0`,
5326
5168
  conversationIdSuffix: conversationId.slice(-8),
5327
- hopCount: 0,
5328
5169
  deadlineAt,
5329
5170
  ...(steer ? { steer: true } : {}),
5330
5171
  progressWatch: progressWatchActivity(event.text, conversationId, event.sourceAlias),
@@ -5342,16 +5183,13 @@ export class GatewayService {
5342
5183
  targetAlias: event.targetAlias,
5343
5184
  expectsReply: !steer,
5344
5185
  nextSequence: 1,
5345
- lastHopCount: 0,
5346
5186
  lastActivityAt: this.now().toISOString(),
5347
5187
  ...(queued.pair === true ? { pair: true } : {}),
5348
5188
  };
5349
5189
  this.conversations.set(conversationId, conversation);
5350
5190
  this.messageContexts.set(queued.messageId, {
5351
5191
  conversationId,
5352
- isReply: false,
5353
5192
  expectsReply: !steer,
5354
- hopCount: 0,
5355
5193
  sequence: 0,
5356
5194
  targetBindingKey: bindingKey(target),
5357
5195
  authorization: "selected_route",
@@ -5824,9 +5662,6 @@ export class GatewayService {
5824
5662
  const [alias, binding] = entry;
5825
5663
  const previousState = this.routeStates.get(alias);
5826
5664
  this.routeStates.set(alias, event.state);
5827
- if (binding.provider === "claude" && event.state === "idle") {
5828
- this.claudePrewriteIdleWaitTargets.delete(alias);
5829
- }
5830
5665
  const staleSafeCode = binding.provider === "codex"
5831
5666
  ? "CODEX_ROUTE_STALE"
5832
5667
  : safeCode(event.safeErrorCode, "ROUTE_STALE");
@@ -6037,21 +5872,6 @@ export class GatewayService {
6037
5872
  },
6038
5873
  ];
6039
5874
  });
6040
- const certificationAlerts = compatibilityChecks.flatMap((attestation) => {
6041
- const certification = attestation.certification;
6042
- if (certification?.outcome !== "fail")
6043
- return [];
6044
- const adapter = this.adapters.find((candidate) => candidate.identity.provider === attestation.surface);
6045
- return [
6046
- {
6047
- code: "COMPATIBILITY_CERTIFICATION_FAILED",
6048
- severity: "error",
6049
- timestamp: certification.certifiedAt,
6050
- provider: attestation.surface,
6051
- host: adapter?.identity.hostId ?? "this-mac",
6052
- },
6053
- ];
6054
- });
6055
5875
  return projectGatewayPublicSnapshot({
6056
5876
  ...base,
6057
5877
  ...(compatibilityChecks.length === 0 ? {} : { compatibilityChecks }),
@@ -6067,7 +5887,6 @@ export class GatewayService {
6067
5887
  alerts: [
6068
5888
  ...base.alerts,
6069
5889
  ...this.runtimeAlerts.map((alert) => ({ ...alert })),
6070
- ...certificationAlerts,
6071
5890
  ...stalledAlerts,
6072
5891
  ],
6073
5892
  });