agent-embassy 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/CONTRIBUTING.md +50 -16
  3. package/README.md +57 -25
  4. package/README.zh-CN.md +45 -23
  5. package/SECURITY.md +68 -48
  6. package/dist/src/gateway/claude-helper-protocol.d.ts +1 -0
  7. package/dist/src/gateway/claude-helper-protocol.js +4 -2
  8. package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
  9. package/dist/src/gateway/claude-helper-supervisor.d.ts +1 -0
  10. package/dist/src/gateway/claude-helper-supervisor.js +3 -1
  11. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
  12. package/dist/src/gateway/claude-runtime.d.ts +0 -2
  13. package/dist/src/gateway/claude-runtime.js +6 -17
  14. package/dist/src/gateway/claude-runtime.js.map +1 -1
  15. package/dist/src/gateway/cli-copy.d.ts +1 -1
  16. package/dist/src/gateway/cli-copy.en.d.ts +4 -1
  17. package/dist/src/gateway/cli-copy.en.js +7 -3
  18. package/dist/src/gateway/cli-copy.en.js.map +1 -1
  19. package/dist/src/gateway/cli-copy.js +3 -0
  20. package/dist/src/gateway/cli-copy.js.map +1 -1
  21. package/dist/src/gateway/cli-copy.zh-CN.d.ts +4 -1
  22. package/dist/src/gateway/cli-copy.zh-CN.js +7 -3
  23. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
  24. package/dist/src/gateway/cli.d.ts +2 -5
  25. package/dist/src/gateway/cli.js +41 -67
  26. package/dist/src/gateway/cli.js.map +1 -1
  27. package/dist/src/gateway/codex-app-server.d.ts +2 -8
  28. package/dist/src/gateway/codex-app-server.js +36 -62
  29. package/dist/src/gateway/codex-app-server.js.map +1 -1
  30. package/dist/src/gateway/codex-local-transport.d.ts +4 -10
  31. package/dist/src/gateway/codex-local-transport.js +13 -16
  32. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  33. package/dist/src/gateway/compatibility.d.ts +0 -27
  34. package/dist/src/gateway/compatibility.js +1 -101
  35. package/dist/src/gateway/compatibility.js.map +1 -1
  36. package/dist/src/gateway/config.d.ts +1 -4
  37. package/dist/src/gateway/config.js +0 -10
  38. package/dist/src/gateway/config.js.map +1 -1
  39. package/dist/src/gateway/control.d.ts +2 -21
  40. package/dist/src/gateway/control.js +7 -36
  41. package/dist/src/gateway/control.js.map +1 -1
  42. package/dist/src/gateway/dashboard-copy.d.ts +1 -1
  43. package/dist/src/gateway/dashboard-copy.en.d.ts +21 -8
  44. package/dist/src/gateway/dashboard-copy.en.js +40 -27
  45. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  46. package/dist/src/gateway/dashboard-copy.js +21 -8
  47. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  48. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +21 -8
  49. package/dist/src/gateway/dashboard-copy.zh-CN.js +40 -27
  50. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  51. package/dist/src/gateway/dashboard-model.d.ts +4 -9
  52. package/dist/src/gateway/dashboard-model.js +159 -46
  53. package/dist/src/gateway/dashboard-model.js.map +1 -1
  54. package/dist/src/gateway/dashboard.js +27 -16
  55. package/dist/src/gateway/dashboard.js.map +1 -1
  56. package/dist/src/gateway/instance-lease.d.ts +2 -3
  57. package/dist/src/gateway/instance-lease.js +17 -140
  58. package/dist/src/gateway/instance-lease.js.map +1 -1
  59. package/dist/src/gateway/live-dashboard-app/app.js +62 -63
  60. package/dist/src/gateway/live-dashboard-command.d.ts +5 -1
  61. package/dist/src/gateway/live-dashboard-command.js +8 -6
  62. package/dist/src/gateway/live-dashboard-command.js.map +1 -1
  63. package/dist/src/gateway/live-dashboard-http.d.ts +0 -4
  64. package/dist/src/gateway/live-dashboard-http.js +20 -94
  65. package/dist/src/gateway/live-dashboard-http.js.map +1 -1
  66. package/dist/src/gateway/live-dashboard-protocol.d.ts +1 -5
  67. package/dist/src/gateway/live-dashboard-protocol.js +0 -42
  68. package/dist/src/gateway/live-dashboard-protocol.js.map +1 -1
  69. package/dist/src/gateway/live-dashboard-server.d.ts +2 -2
  70. package/dist/src/gateway/live-dashboard-server.js +18 -4
  71. package/dist/src/gateway/live-dashboard-server.js.map +1 -1
  72. package/dist/src/gateway/live-dashboard.d.ts +3 -6
  73. package/dist/src/gateway/live-dashboard.js +6 -42
  74. package/dist/src/gateway/live-dashboard.js.map +1 -1
  75. package/dist/src/gateway/progress-watch-machine.d.ts +6 -5
  76. package/dist/src/gateway/progress-watch-machine.js +6 -6
  77. package/dist/src/gateway/progress-watch-machine.js.map +1 -1
  78. package/dist/src/gateway/provenance-envelope.d.ts +1 -0
  79. package/dist/src/gateway/provenance-envelope.js +11 -4
  80. package/dist/src/gateway/provenance-envelope.js.map +1 -1
  81. package/dist/src/gateway/providers.d.ts +2 -21
  82. package/dist/src/gateway/providers.js +21 -261
  83. package/dist/src/gateway/providers.js.map +1 -1
  84. package/dist/src/gateway/server.js +0 -3
  85. package/dist/src/gateway/server.js.map +1 -1
  86. package/dist/src/gateway/service.d.ts +5 -15
  87. package/dist/src/gateway/service.js +101 -265
  88. package/dist/src/gateway/service.js.map +1 -1
  89. package/dist/src/gateway/store.d.ts +9 -21
  90. package/dist/src/gateway/store.js +360 -140
  91. package/dist/src/gateway/store.js.map +1 -1
  92. package/dist/src/gateway/types.d.ts +1 -6
  93. package/dist/src/gateway/types.js.map +1 -1
  94. package/docs/CONFIGURATION.md +102 -0
  95. package/docs/CONFIGURATION.zh-CN.md +72 -0
  96. package/docs/DASHBOARD.md +103 -0
  97. package/docs/DASHBOARD.zh-CN.md +47 -0
  98. package/docs/DELIVERY.md +84 -0
  99. package/docs/DELIVERY.zh-CN.md +53 -0
  100. package/docs/GATEWAY-ARCHITECTURE.md +249 -153
  101. package/package.json +8 -4
  102. package/skills/embassy-peer/SKILL.md +25 -10
  103. package/dist/src/gateway/claude-compatibility-scratch.d.ts +0 -18
  104. package/dist/src/gateway/claude-compatibility-scratch.js +0 -158
  105. package/dist/src/gateway/claude-compatibility-scratch.js.map +0 -1
  106. package/dist/src/gateway/live-dashboard-bootstrap.d.ts +0 -36
  107. package/dist/src/gateway/live-dashboard-bootstrap.js +0 -176
  108. package/dist/src/gateway/live-dashboard-bootstrap.js.map +0 -1
  109. 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 }),
@@ -103,6 +94,9 @@ function decisionFor(error) {
103
94
  if (!(error instanceof BridgeError)) {
104
95
  return { accepted: false, code: "rejected" };
105
96
  }
97
+ if (error.code === "PROGRESS_WATCH_REPLACEMENT_OWNER_REQUIRED") {
98
+ return { accepted: false, code: "watch_owner_conflict" };
99
+ }
106
100
  if (error.code.includes("NOT_FOUND") || error.code === "ROUTE_UNAVAILABLE") {
107
101
  return { accepted: false, code: "not_found" };
108
102
  }
@@ -145,8 +139,6 @@ export class GatewayService {
145
139
  activeDispatchByTarget = new Map();
146
140
  scheduledDispatchTargets = new Set();
147
141
  dispatchRunnerTargets = new Set();
148
- /** Claude sends that proved no write wait for exact provider-idle evidence. */
149
- claudePrewriteIdleWaitTargets = new Set();
150
142
  heldRedispatchTimers = new Map();
151
143
  pendingClaudeReplies = new Map();
152
144
  deliveryTrackers = new Map();
@@ -285,8 +277,8 @@ export class GatewayService {
285
277
  if (this.adapters.length > 0 &&
286
278
  this.adapters.every((adapter) => adapter.compatibilitySurface !== undefined &&
287
279
  adapter.runCompatibilityProbes !== undefined)) {
288
- const report = await this.compatibilityCheckLocked();
289
- const incompatible = report.surfaces.find((surface) => surface.tier === "incompatible");
280
+ const attestations = await this.runAutomaticCompatibilityProbesLocked();
281
+ const incompatible = attestations.find((surface) => surface.tier === "incompatible");
290
282
  if (incompatible !== undefined) {
291
283
  throw new BridgeError(incompatible.safeErrorCode ?? "COMPATIBILITY_CHECK_FAILED", "A local provider failed bounded compatibility attestation.");
292
284
  }
@@ -427,7 +419,6 @@ export class GatewayService {
427
419
  this.activeDispatchByTarget.clear();
428
420
  this.scheduledDispatchTargets.clear();
429
421
  this.dispatchRunnerTargets.clear();
430
- this.claudePrewriteIdleWaitTargets.clear();
431
422
  for (const timer of this.heldRedispatchTimers.values()) {
432
423
  this.timers.clearTimeout(timer);
433
424
  }
@@ -518,8 +509,6 @@ export class GatewayService {
518
509
  }),
519
510
  listSnapshot: async () => (await this.observeSnapshot()).snapshot,
520
511
  observeSnapshot: async () => await this.observeSnapshot(),
521
- compatibilityCheck: async () => await this.compatibilityCheck(),
522
- compatibilityCertify: async (params) => await this.compatibilityCertify(params),
523
512
  deliveryStatus: async (params) => await this.deliveryStatus(params.token),
524
513
  untrack: async (params) => await this.exclusiveDecision(async () => {
525
514
  const settled = await this.store.settleProgressWatch({
@@ -550,133 +539,7 @@ export class GatewayService {
550
539
  async snapshot() {
551
540
  return (await this.observeSnapshot()).snapshot;
552
541
  }
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() {
542
+ async runAutomaticCompatibilityProbesLocked() {
680
543
  const bySurface = new Map(this.adapters.map((adapter) => {
681
544
  if (adapter.compatibilitySurface === undefined ||
682
545
  adapter.runCompatibilityProbes === undefined) {
@@ -695,39 +558,21 @@ export class GatewayService {
695
558
  if (entry === undefined) {
696
559
  throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "A required compatibility surface is unavailable.");
697
560
  }
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({
561
+ const probes = await entry.adapter.runCompatibilityProbes();
562
+ const attestation = evaluateCompatibilityAttestation({
702
563
  surface,
703
564
  version: entry.observation.version,
704
- checkedAt,
705
- policy: this.config.compatibilityPolicy ?? "observed",
565
+ checkedAt: this.now().toISOString(),
706
566
  certifiedVersions: certifiedCompatibilityVersions[surface],
707
567
  probes,
708
568
  });
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
- }
569
+ await this.store.recordCompatibilityAttestation(attestation);
717
570
  if (attestation.tier !== "incompatible") {
718
571
  entry.adapter.acceptCompatibilityAttestation?.(attestation);
719
572
  }
720
573
  attestations.push(attestation);
721
574
  }
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;
575
+ return attestations;
731
576
  }
732
577
  async observeSnapshot() {
733
578
  return await this.mutex.run("service", async () => {
@@ -2746,13 +2591,22 @@ export class GatewayService {
2746
2591
  const publicBefore = await this.discoveryPublicFingerprint();
2747
2592
  this.candidates.clear();
2748
2593
  const rowsByAlias = new Map();
2594
+ const collisionByRouteHandle = new Map();
2749
2595
  const discoveredCandidates = [];
2750
2596
  const observedSelected = new Set();
2751
2597
  for (const adapter of this.adapters.filter((item) => item.identity.provider === "claude")) {
2752
- const discovered = (await adapter.discoverClaudePeers?.()) ?? {
2598
+ let discovered = {
2753
2599
  peers: [],
2754
2600
  complete: false,
2755
2601
  };
2602
+ try {
2603
+ discovered = (await adapter.discoverClaudePeers?.()) ?? discovered;
2604
+ }
2605
+ catch {
2606
+ // Registry observation is dashboard evidence, not Claude mailbox
2607
+ // authority. The provider still revalidates the exact target before
2608
+ // any write.
2609
+ }
2756
2610
  const grouped = new Map();
2757
2611
  const byHandle = new Map();
2758
2612
  for (const peer of discovered.peers) {
@@ -2772,6 +2626,8 @@ export class GatewayService {
2772
2626
  if (peer === undefined)
2773
2627
  continue;
2774
2628
  if (matches.length !== 1) {
2629
+ for (const match of matches)
2630
+ collisionByRouteHandle.set(match.routeHandle, "PEER_ALIAS_COLLISION");
2775
2631
  rowsByAlias.set(alias, {
2776
2632
  peer,
2777
2633
  adapter,
@@ -2788,6 +2644,7 @@ export class GatewayService {
2788
2644
  continue;
2789
2645
  }
2790
2646
  if ((byHandle.get(peer.routeHandle)?.length ?? 0) !== 1) {
2647
+ collisionByRouteHandle.set(peer.routeHandle, "PEER_SESSION_COLLISION");
2791
2648
  rowsByAlias.set(alias, {
2792
2649
  peer,
2793
2650
  adapter,
@@ -2813,6 +2670,8 @@ export class GatewayService {
2813
2670
  const collision = this.routeBindings.get(alias);
2814
2671
  if (collision !== undefined &&
2815
2672
  bindingKey(collision) !== bindingKey(existing[1])) {
2673
+ collisionByRouteHandle.set(routeHandle, "PEER_ALIAS_COLLISION");
2674
+ collisionByRouteHandle.set(collision.routeHandle, "PEER_ALIAS_COLLISION");
2816
2675
  rowsByAlias.set(alias, {
2817
2676
  peer: candidate,
2818
2677
  adapter,
@@ -2827,10 +2686,16 @@ export class GatewayService {
2827
2686
  catch (error) {
2828
2687
  if (!(error instanceof BridgeError))
2829
2688
  throw error;
2689
+ // Only a real alias collision may wear that receipt; every other
2690
+ // rename failure is an unobserved peer, not a name conflict.
2691
+ const safeErrorCode = error.code === "ROUTE_ALIAS_COLLISION"
2692
+ ? "PEER_ALIAS_COLLISION"
2693
+ : "PEER_NOT_OBSERVED";
2694
+ collisionByRouteHandle.set(routeHandle, safeErrorCode);
2830
2695
  rowsByAlias.set(alias, {
2831
2696
  peer: candidate,
2832
2697
  adapter,
2833
- safeErrorCode: "PEER_ALIAS_COLLISION",
2698
+ safeErrorCode,
2834
2699
  });
2835
2700
  }
2836
2701
  }
@@ -2899,6 +2764,9 @@ export class GatewayService {
2899
2764
  observedSelected.has(bindingKey(binding))) {
2900
2765
  continue;
2901
2766
  }
2767
+ const discoveryError = collisionByRouteHandle.get(binding.routeHandle);
2768
+ if (discoveryError === undefined)
2769
+ continue;
2902
2770
  const inspection = await this.store.inspectPrivateRoute(alias);
2903
2771
  if (inspection !== undefined &&
2904
2772
  inspection.enabled &&
@@ -2906,9 +2774,9 @@ export class GatewayService {
2906
2774
  await this.drainPreDeadlineDeliveryCallbacksLocked();
2907
2775
  const inFlightSettlements = await this.planRouteInFlightSettlementsLocked(alias, {
2908
2776
  unwrittenOutcome: "failed",
2909
- safeErrorCode: "PEER_NOT_OBSERVED",
2777
+ safeErrorCode: discoveryError,
2910
2778
  });
2911
- const settlements = await this.store.invalidateRoute(binding, "PEER_NOT_OBSERVED", inFlightSettlements);
2779
+ const settlements = await this.store.invalidateRoute(binding, discoveryError, inFlightSettlements);
2912
2780
  for (const settlement of settlements) {
2913
2781
  await this.applyTerminalSettlementLocked(settlement);
2914
2782
  }
@@ -3135,6 +3003,11 @@ export class GatewayService {
3135
3003
  async resolveSelectedClaudeDestination(selector) {
3136
3004
  const discoveryChanged = await this.refreshClaudeDiscovery();
3137
3005
  try {
3006
+ const selected = [...this.routeBindings.entries()].find(([alias, binding]) => binding.provider === "claude" &&
3007
+ (alias === selector ||
3008
+ binding.routeHandle.toLowerCase() === selector.toLowerCase()));
3009
+ if (selected !== undefined)
3010
+ return { alias: selected[0], discoveryChanged };
3138
3011
  const candidate = this.claudeCandidate(selector);
3139
3012
  if (candidate === undefined) {
3140
3013
  throw new BridgeError("PEER_NOT_FOUND", "No unique compatible interactive peer matches that current name or session UUID.");
@@ -3327,9 +3200,6 @@ export class GatewayService {
3327
3200
  this.routeBindings.set(alias, binding);
3328
3201
  this.routeStates.set(alias, state);
3329
3202
  this.bindingAliases.set(bindingKey(binding), alias);
3330
- if (binding.provider === "claude" && state === "idle") {
3331
- this.claudePrewriteIdleWaitTargets.delete(alias);
3332
- }
3333
3203
  }
3334
3204
  forgetBinding(alias) {
3335
3205
  const heldTimer = this.heldRedispatchTimers.get(alias);
@@ -3344,7 +3214,6 @@ export class GatewayService {
3344
3214
  }
3345
3215
  this.routeBindings.delete(alias);
3346
3216
  this.routeStates.delete(alias);
3347
- this.claudePrewriteIdleWaitTargets.delete(alias);
3348
3217
  for (const [messageId, continuation] of this.providerTurnContinuations) {
3349
3218
  if (continuation.targetAlias !== alias)
3350
3219
  continue;
@@ -3453,8 +3322,10 @@ export class GatewayService {
3453
3322
  enqueuedAt: input.enqueuedAt,
3454
3323
  stallAt,
3455
3324
  deadlineAt,
3456
- // Deferred means the provider proved no write. Permit one bounded
3457
- // attempt per 500 ms slice until this message's immutable deadline.
3325
+ // Deferred means the provider proved no write. This budgets one
3326
+ // attempt per 500 ms slice of this message's immutable deadline; a
3327
+ // Codex-bound retry spends one slice each, while a Claude-bound retry
3328
+ // backs off exponentially and spends far fewer of them.
3458
3329
  maxCleanPrewriteRetries: Math.max(0, Math.ceil((deadlineAt - input.enqueuedAt) / 500)),
3459
3330
  nativeReceipt: input.nativeReceipt !== undefined,
3460
3331
  maxNativeReceiptCleanPrewriteRetries: DELIVERY_ACK_RETRY_DELAYS_MS.length,
@@ -4061,22 +3932,14 @@ export class GatewayService {
4061
3932
  : "PROGRESS_WATCH_ENDPOINT_RETIRED", action.outcome === "unresponsive" ? "error" : "warning", { alias: action.ownerAlias });
4062
3933
  continue;
4063
3934
  }
4064
- if (action.type === "notify_capability_degraded") {
4065
- this.addRuntimeAlert("PROGRESS_WATCH_CAPABILITY_DEGRADED", "warning", {
4066
- alias: action.ownerAlias,
4067
- });
4068
- continue;
4069
- }
4070
3935
  const text = `[Embassy automated liveness check — ${action.conversationId} / ` +
4071
3936
  `Embassy 自动活跃检查 — ${action.conversationId}]\n` +
4072
3937
  "Reply with the result or a status. / 请回复结果或状态。";
4073
3938
  try {
4074
- await this.enqueue(action.ownerAlias, action.workerAlias, text, false, false, {
3939
+ await this.enqueue(action.ownerAlias, action.workerAlias, text, false, {
4075
3940
  existingConversationId: action.conversationId,
4076
- requestedHopCount: 0,
4077
3941
  exposeDeliveryToken: false,
4078
3942
  skipProgressWatchActivity: true,
4079
- preserveConversationHop: true,
4080
3943
  progressWatchNudge: {
4081
3944
  conversationId: action.conversationId,
4082
3945
  nudgeNumber: action.nudgeNumber,
@@ -4123,8 +3986,7 @@ export class GatewayService {
4123
3986
  }
4124
3987
  if (!isTerminalDeliveryMachine(tracker.machine) &&
4125
3988
  tracker.machine.dispatchRetryAt !== null &&
4126
- tracker.machine.dispatchRetryAt <= now &&
4127
- !this.claudePrewriteIdleWaitTargets.has(tracker.targetAlias)) {
3989
+ tracker.machine.dispatchRetryAt <= now) {
4128
3990
  if (!this.dispatchRunnerTargets.has(tracker.targetAlias)) {
4129
3991
  this.scheduleDispatch(tracker.targetAlias);
4130
3992
  }
@@ -4213,7 +4075,6 @@ export class GatewayService {
4213
4075
  if (wakeups.deadlineAt !== undefined)
4214
4076
  consider(wakeups.deadlineAt);
4215
4077
  if (wakeups.dispatchRetryAt !== undefined &&
4216
- !this.claudePrewriteIdleWaitTargets.has(tracker.targetAlias) &&
4217
4078
  !this.scheduledDispatchTargets.has(tracker.targetAlias) &&
4218
4079
  !this.dispatchRunnerTargets.has(tracker.targetAlias)) {
4219
4080
  consider(wakeups.dispatchRetryAt);
@@ -4311,7 +4172,7 @@ export class GatewayService {
4311
4172
  await this.verifyCodex(params.fromAlias, params.threadId);
4312
4173
  const resolved = await this.resolveSelectedClaudeDestination(params.toAlias);
4313
4174
  discoveryChanged = resolved.discoveryChanged;
4314
- return this.acceptedControlResult(await this.enqueue(params.fromAlias, resolved.alias, params.text, params.expectsReply, false, params.trackIdleMinutes === undefined
4175
+ return this.acceptedControlResult(await this.enqueue(params.fromAlias, resolved.alias, params.text, params.expectsReply, params.trackIdleMinutes === undefined
4315
4176
  ? {}
4316
4177
  : { trackIdleMinutes: params.trackIdleMinutes }));
4317
4178
  }
@@ -4330,7 +4191,7 @@ export class GatewayService {
4330
4191
  try {
4331
4192
  await this.verifyClaude(params.fromAlias, params.replyAddress);
4332
4193
  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, {
4194
+ return this.acceptedControlResult(await this.enqueue(params.fromAlias, params.toAlias, params.text, steer ? false : params.expectsReply, {
4334
4195
  ...(steer ? { steer: true } : {}),
4335
4196
  ...(params.trackIdleMinutes === undefined
4336
4197
  ? {}
@@ -4359,10 +4220,9 @@ export class GatewayService {
4359
4220
  throw new BridgeError("ROUTE_MISMATCH", "The caller is not a conversation participant.");
4360
4221
  const result = caller.kind === "codex" &&
4361
4222
  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, {
4223
+ ? await this.enqueueNativeReply(conversation, from, params.text, true, params.trackIdleMinutes)
4224
+ : await this.enqueue(from, to, params.text, false, {
4364
4225
  existingConversationId: conversation.id,
4365
- requestedHopCount: conversation.lastHopCount + 1,
4366
4226
  ...(params.trackIdleMinutes === undefined
4367
4227
  ? {}
4368
4228
  : { trackIdleMinutes: params.trackIdleMinutes }),
@@ -4374,7 +4234,7 @@ export class GatewayService {
4374
4234
  }
4375
4235
  });
4376
4236
  }
4377
- async enqueue(sourceAlias, targetAlias, text, expectsReply, isReply, options = {}) {
4237
+ async enqueue(sourceAlias, targetAlias, text, expectsReply, options = {}) {
4378
4238
  if (this.codexSuccessionPoisoned.has(sourceAlias) ||
4379
4239
  this.codexSuccessionPoisoned.has(targetAlias)) {
4380
4240
  throw new BridgeError("CODEX_SUCCESSION_RECOVERY_REQUIRED", "No message can be accepted while Codex succession requires manual recovery.");
@@ -4397,11 +4257,8 @@ export class GatewayService {
4397
4257
  targetAlias,
4398
4258
  expectsReply,
4399
4259
  nextSequence: 0,
4400
- lastHopCount: 0,
4401
4260
  lastActivityAt: this.now().toISOString(),
4402
4261
  };
4403
- const hopCount = options.requestedHopCount ??
4404
- (isReply ? conversation.lastHopCount + 1 : 0);
4405
4262
  const sequence = conversation.nextSequence;
4406
4263
  const enqueuedAt = this.now().getTime();
4407
4264
  const deliveryToken = (options.exposeDeliveryToken ?? true)
@@ -4414,7 +4271,6 @@ export class GatewayService {
4414
4271
  body: text,
4415
4272
  dedupeKey: `${conversationId}:${sequence}`,
4416
4273
  conversationIdSuffix: conversationId.slice(-8),
4417
- hopCount,
4418
4274
  deadlineAt,
4419
4275
  ...(options.steer === true ? { steer: true } : {}),
4420
4276
  ...(options.skipProgressWatchActivity === true
@@ -4435,15 +4291,10 @@ export class GatewayService {
4435
4291
  if (queued.pair === true)
4436
4292
  conversation.pair = true;
4437
4293
  conversation.nextSequence += 1;
4438
- if (options.preserveConversationHop !== true) {
4439
- conversation.lastHopCount = hopCount;
4440
- }
4441
4294
  conversation.lastActivityAt = this.now().toISOString();
4442
4295
  this.messageContexts.set(queued.messageId, {
4443
4296
  conversationId,
4444
- isReply,
4445
4297
  expectsReply,
4446
- hopCount,
4447
4298
  sequence,
4448
4299
  targetBindingKey: bindingKey(target),
4449
4300
  authorization: "selected_route",
@@ -4462,7 +4313,6 @@ export class GatewayService {
4462
4313
  this.pendingClaudeReplies.set(bindingKey(target), {
4463
4314
  conversationId,
4464
4315
  bindingKey: bindingKey(target),
4465
- hopCount,
4466
4316
  deadlineAt,
4467
4317
  tainted: false,
4468
4318
  });
@@ -4476,7 +4326,7 @@ export class GatewayService {
4476
4326
  ...(deliveryToken === undefined ? {} : { deliveryToken }),
4477
4327
  };
4478
4328
  }
4479
- async enqueueNativeReply(conversation, sourceAlias, text, requestedHopCount, exposeDeliveryToken = true, trackIdleMinutes) {
4329
+ async enqueueNativeReply(conversation, sourceAlias, text, exposeDeliveryToken = true, trackIdleMinutes) {
4480
4330
  this.pruneTransient();
4481
4331
  const capability = this.nativeIngressByConversation.get(conversation.id);
4482
4332
  if (capability === undefined ||
@@ -4500,7 +4350,6 @@ export class GatewayService {
4500
4350
  body: text,
4501
4351
  dedupeKey: `${conversation.id}:${sequence}`,
4502
4352
  conversationIdSuffix: conversation.id.slice(-8),
4503
- hopCount: requestedHopCount,
4504
4353
  deadlineAt,
4505
4354
  ...(conversation.pair === true ? { pair: true } : {}),
4506
4355
  progressWatch: progressWatchActivity(text, conversation.id, sourceAlias, trackIdleMinutes),
@@ -4509,13 +4358,10 @@ export class GatewayService {
4509
4358
  throw new BridgeError("MESSAGE_REJECTED", "The native reply was not accepted.");
4510
4359
  }
4511
4360
  conversation.nextSequence += 1;
4512
- conversation.lastHopCount = requestedHopCount;
4513
4361
  conversation.lastActivityAt = this.now().toISOString();
4514
4362
  this.messageContexts.set(queued.messageId, {
4515
4363
  conversationId: conversation.id,
4516
- isReply: true,
4517
4364
  expectsReply: false,
4518
- hopCount: requestedHopCount,
4519
4365
  sequence,
4520
4366
  targetBindingKey: bindingKey(capability.binding),
4521
4367
  nativeReplyBinding: { ...capability.binding },
@@ -4547,7 +4393,7 @@ export class GatewayService {
4547
4393
  * native-ingress boundary; this preserves the exact connector generation
4548
4394
  * without recreating route authority or accepting a new unsolicited send.
4549
4395
  */
4550
- async enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, text, requestedHopCount) {
4396
+ async enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, text) {
4551
4397
  const target = await this.store.resolveRoute(conversation.sourceAlias);
4552
4398
  if (sourceBinding.provider !== "claude" ||
4553
4399
  target.provider !== "codex" ||
@@ -4566,7 +4412,6 @@ export class GatewayService {
4566
4412
  body: text,
4567
4413
  dedupeKey: `${conversation.id}:${sequence}`,
4568
4414
  conversationIdSuffix: conversation.id.slice(-8),
4569
- hopCount: requestedHopCount,
4570
4415
  deadlineAt,
4571
4416
  authorizedPairTeardownReply: true,
4572
4417
  progressWatch: progressWatchActivity(text, conversation.id, conversation.targetAlias),
@@ -4575,16 +4420,13 @@ export class GatewayService {
4575
4420
  throw new BridgeError("MESSAGE_REJECTED", "The retained provider reply was not accepted.");
4576
4421
  }
4577
4422
  conversation.nextSequence += 1;
4578
- conversation.lastHopCount = requestedHopCount;
4579
4423
  conversation.lastActivityAt = this.now().toISOString();
4580
4424
  // The old edge has already been removed. Retain only this already-observed
4581
4425
  // reply attempt; the conversation no longer grants pair reply authority.
4582
4426
  delete conversation.pair;
4583
4427
  this.messageContexts.set(queued.messageId, {
4584
4428
  conversationId: conversation.id,
4585
- isReply: true,
4586
4429
  expectsReply: false,
4587
- hopCount: requestedHopCount,
4588
4430
  sequence,
4589
4431
  targetBindingKey: bindingKey(target),
4590
4432
  authorization: "selected_route",
@@ -4695,15 +4537,12 @@ export class GatewayService {
4695
4537
  targetAlias: item.targetAlias,
4696
4538
  expectsReply: false,
4697
4539
  nextSequence: 1,
4698
- lastHopCount: item.hopCount,
4699
4540
  lastActivityAt: this.now().toISOString(),
4700
4541
  ...(item.pair === true ? { pair: true } : {}),
4701
4542
  };
4702
4543
  const context = {
4703
4544
  conversationId,
4704
- isReply: false,
4705
4545
  expectsReply: false,
4706
- hopCount: item.hopCount,
4707
4546
  sequence: 0,
4708
4547
  targetBindingKey: bindingKey(binding),
4709
4548
  authorization: "selected_route",
@@ -4726,8 +4565,7 @@ export class GatewayService {
4726
4565
  if (!this.running ||
4727
4566
  this.closing ||
4728
4567
  this.codexSuccessionDispatchFrozen.has(targetAlias) ||
4729
- this.codexSuccessionPoisoned.has(targetAlias) ||
4730
- this.claudePrewriteIdleWaitTargets.has(targetAlias)) {
4568
+ this.codexSuccessionPoisoned.has(targetAlias)) {
4731
4569
  return;
4732
4570
  }
4733
4571
  if (await this.processLifecycleLocked()) {
@@ -4762,7 +4600,6 @@ export class GatewayService {
4762
4600
  if (context === undefined && selectedBinding !== undefined) {
4763
4601
  context = this.restoreQueuedMessageContextLocked(item, selectedBinding);
4764
4602
  }
4765
- const binding = context?.nativeReplyBinding ?? selectedBinding;
4766
4603
  if (context === undefined) {
4767
4604
  const result = await this.store.settleMessage({
4768
4605
  messageId: item.messageId,
@@ -4775,6 +4612,7 @@ export class GatewayService {
4775
4612
  }
4776
4613
  return;
4777
4614
  }
4615
+ const binding = context.nativeReplyBinding ?? selectedBinding;
4778
4616
  if (binding === undefined) {
4779
4617
  const result = await this.store.requeueInFlightMessage(item.messageId, item.body);
4780
4618
  if (result.status === "settled") {
@@ -4785,8 +4623,8 @@ export class GatewayService {
4785
4623
  }
4786
4624
  if (context.authorization === "selected_route") {
4787
4625
  const routeState = this.routeStates.get(targetAlias);
4788
- const dispatchable = routeState === "idle" ||
4789
- (binding.provider === "claude" && routeState === "busy") ||
4626
+ const dispatchable = binding.provider === "claude" ||
4627
+ routeState === "idle" ||
4790
4628
  (binding.provider === "codex" &&
4791
4629
  item.steer === true &&
4792
4630
  routeState === "busy");
@@ -4814,8 +4652,8 @@ export class GatewayService {
4814
4652
  this.scheduleDispatch(targetAlias);
4815
4653
  return;
4816
4654
  }
4817
- const storedDispatchable = inspection?.state === "idle" ||
4818
- (binding.provider === "claude" && inspection?.state === "busy") ||
4655
+ const storedDispatchable = binding.provider === "claude" ||
4656
+ inspection?.state === "idle" ||
4819
4657
  (binding.provider === "codex" &&
4820
4658
  item.steer === true &&
4821
4659
  inspection?.state === "busy");
@@ -4903,6 +4741,23 @@ export class GatewayService {
4903
4741
  this.scheduleLifecycleWakeLocked();
4904
4742
  return;
4905
4743
  }
4744
+ let progressWatches = await this.store.inspectProgressWatches();
4745
+ if (context.recovered === true &&
4746
+ item.pair === true &&
4747
+ !item.body.startsWith("DONE:") &&
4748
+ progressWatches.some((watch) => watch.capability === "route" &&
4749
+ watch.ownerAlias === item.sourceAlias &&
4750
+ watch.workerAlias === item.targetAlias)) {
4751
+ await this.store.bindProgressWatchConversation({
4752
+ conversationId: context.conversationId,
4753
+ ownerAlias: item.sourceAlias,
4754
+ workerAlias: item.targetAlias,
4755
+ });
4756
+ progressWatches = await this.store.inspectProgressWatches();
4757
+ }
4758
+ const progressWatchActive = progressWatches.some((watch) => watch.capability === "conversation" &&
4759
+ watch.workerAlias === item.targetAlias &&
4760
+ watch.conversationId === context.conversationId);
4906
4761
  let result;
4907
4762
  try {
4908
4763
  result = await this.awaitDispatchWithNativeHeldLocked(tracker, this.adapter(binding.provider, binding.hostId).dispatch({
@@ -4913,9 +4768,12 @@ export class GatewayService {
4913
4768
  authorization: context.authorization,
4914
4769
  messageId: item.messageId,
4915
4770
  text: item.body,
4916
- expectsReply: context?.expectsReply ?? false,
4771
+ expectsReply: context.expectsReply,
4917
4772
  deadlineAt: item.deadlineAt,
4918
4773
  ...(item.steer === true ? { steer: true } : {}),
4774
+ ...(progressWatchActive
4775
+ ? { progressWatchActive: true }
4776
+ : {}),
4919
4777
  }));
4920
4778
  }
4921
4779
  catch {
@@ -4948,10 +4806,19 @@ export class GatewayService {
4948
4806
  const currentTargetAlias = this.bindingAliases.get(bindingKey(binding)) ?? targetAlias;
4949
4807
  const now = this.now().getTime();
4950
4808
  const deferredSafeErrorCode = safeCode(result.safeErrorCode, "PROVIDER_DISPATCH_DEFERRED");
4809
+ const retryDelayMs = binding.provider === "claude"
4810
+ ? Math.min(500 * 2 ** (tracker.machine.dispatchAttempt - 1), 300_000)
4811
+ : 500;
4812
+ // Only Claude's backoff is clamped inside the deadline, so a long delay
4813
+ // still buys one last attempt. A Codex-bound retry keeps its exact
4814
+ // unclamped slice: a slice past the deadline must settle now, not wait.
4815
+ const retryAt = binding.provider === "claude"
4816
+ ? Math.min(now + retryDelayMs, tracker.deadlineAt - 1)
4817
+ : now + retryDelayMs;
4951
4818
  await this.advanceDeliveryLocked(item.messageId, {
4952
4819
  type: "dispatch_clean_prewrite_failed",
4953
4820
  at: now,
4954
- retryAt: now + 500,
4821
+ retryAt,
4955
4822
  safeErrorCode: deferredSafeErrorCode,
4956
4823
  });
4957
4824
  tracker.deliverySafeErrorCode = deferredSafeErrorCode;
@@ -4979,12 +4846,7 @@ export class GatewayService {
4979
4846
  }
4980
4847
  if (requeued.status === "requeued") {
4981
4848
  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) {
4849
+ if (binding.provider === "codex" && !steeringWhileBusy) {
4988
4850
  this.routeStates.set(currentTargetAlias, "idle");
4989
4851
  await this.store.observeRoute({
4990
4852
  binding,
@@ -4992,7 +4854,7 @@ export class GatewayService {
4992
4854
  compatibility: "compatible",
4993
4855
  });
4994
4856
  }
4995
- else {
4857
+ else if (binding.provider === "codex") {
4996
4858
  this.scheduleHeldRedispatch(currentTargetAlias);
4997
4859
  }
4998
4860
  await this.setNativeCodexStatus(currentTargetAlias, "waiting");
@@ -5024,8 +4886,7 @@ export class GatewayService {
5024
4886
  scheduleDispatch(targetAlias) {
5025
4887
  if (this.closing ||
5026
4888
  this.codexSuccessionDispatchFrozen.has(targetAlias) ||
5027
- this.codexSuccessionPoisoned.has(targetAlias) ||
5028
- this.claudePrewriteIdleWaitTargets.has(targetAlias)) {
4889
+ this.codexSuccessionPoisoned.has(targetAlias)) {
5029
4890
  return;
5030
4891
  }
5031
4892
  if (this.scheduledDispatchTargets.has(targetAlias))
@@ -5198,7 +5059,7 @@ export class GatewayService {
5198
5059
  conversation !== undefined) {
5199
5060
  try {
5200
5061
  if (this.nativeIngressByConversation.has(conversation.id)) {
5201
- await this.enqueueNativeReply(conversation, conversation.targetAlias, replyText, context.hopCount + 1, false);
5062
+ await this.enqueueNativeReply(conversation, conversation.targetAlias, replyText, false);
5202
5063
  }
5203
5064
  else {
5204
5065
  const sourceBinding = this.routeBindings.get(conversation.targetAlias);
@@ -5213,12 +5074,11 @@ export class GatewayService {
5213
5074
  }));
5214
5075
  if ((!sourceStillRoutable || !pairStillActive) &&
5215
5076
  sourceBinding?.provider === "claude") {
5216
- await this.enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, replyText, context.hopCount + 1);
5077
+ await this.enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, replyText);
5217
5078
  }
5218
5079
  else {
5219
- await this.enqueue(conversation.targetAlias, conversation.sourceAlias, replyText, false, true, {
5080
+ await this.enqueue(conversation.targetAlias, conversation.sourceAlias, replyText, false, {
5220
5081
  existingConversationId: conversation.id,
5221
- requestedHopCount: context.hopCount + 1,
5222
5082
  exposeDeliveryToken: false,
5223
5083
  });
5224
5084
  }
@@ -5279,9 +5139,8 @@ export class GatewayService {
5279
5139
  if (conversation === undefined)
5280
5140
  return;
5281
5141
  this.pendingClaudeReplies.delete(key);
5282
- await this.enqueue(conversation.targetAlias, conversation.sourceAlias, event.text, false, true, {
5142
+ await this.enqueue(conversation.targetAlias, conversation.sourceAlias, event.text, false, {
5283
5143
  existingConversationId: conversation.id,
5284
- requestedHopCount: pending.hopCount + 1,
5285
5144
  exposeDeliveryToken: false,
5286
5145
  });
5287
5146
  }
@@ -5324,7 +5183,6 @@ export class GatewayService {
5324
5183
  body: event.text,
5325
5184
  dedupeKey: `${conversationId}:0`,
5326
5185
  conversationIdSuffix: conversationId.slice(-8),
5327
- hopCount: 0,
5328
5186
  deadlineAt,
5329
5187
  ...(steer ? { steer: true } : {}),
5330
5188
  progressWatch: progressWatchActivity(event.text, conversationId, event.sourceAlias),
@@ -5342,16 +5200,13 @@ export class GatewayService {
5342
5200
  targetAlias: event.targetAlias,
5343
5201
  expectsReply: !steer,
5344
5202
  nextSequence: 1,
5345
- lastHopCount: 0,
5346
5203
  lastActivityAt: this.now().toISOString(),
5347
5204
  ...(queued.pair === true ? { pair: true } : {}),
5348
5205
  };
5349
5206
  this.conversations.set(conversationId, conversation);
5350
5207
  this.messageContexts.set(queued.messageId, {
5351
5208
  conversationId,
5352
- isReply: false,
5353
5209
  expectsReply: !steer,
5354
- hopCount: 0,
5355
5210
  sequence: 0,
5356
5211
  targetBindingKey: bindingKey(target),
5357
5212
  authorization: "selected_route",
@@ -5824,9 +5679,6 @@ export class GatewayService {
5824
5679
  const [alias, binding] = entry;
5825
5680
  const previousState = this.routeStates.get(alias);
5826
5681
  this.routeStates.set(alias, event.state);
5827
- if (binding.provider === "claude" && event.state === "idle") {
5828
- this.claudePrewriteIdleWaitTargets.delete(alias);
5829
- }
5830
5682
  const staleSafeCode = binding.provider === "codex"
5831
5683
  ? "CODEX_ROUTE_STALE"
5832
5684
  : safeCode(event.safeErrorCode, "ROUTE_STALE");
@@ -6037,21 +5889,6 @@ export class GatewayService {
6037
5889
  },
6038
5890
  ];
6039
5891
  });
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
5892
  return projectGatewayPublicSnapshot({
6056
5893
  ...base,
6057
5894
  ...(compatibilityChecks.length === 0 ? {} : { compatibilityChecks }),
@@ -6067,7 +5904,6 @@ export class GatewayService {
6067
5904
  alerts: [
6068
5905
  ...base.alerts,
6069
5906
  ...this.runtimeAlerts.map((alert) => ({ ...alert })),
6070
- ...certificationAlerts,
6071
5907
  ...stalledAlerts,
6072
5908
  ],
6073
5909
  });