agent-embassy 3.0.0 → 3.1.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 (45) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/CONTRIBUTING.md +4 -2
  3. package/README.md +26 -7
  4. package/SECURITY.md +17 -4
  5. package/dist/src/gateway/claude-helper-supervisor.js +36 -9
  6. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
  7. package/dist/src/gateway/claude-helper.js.map +1 -1
  8. package/dist/src/gateway/claude-peer.js +1 -1
  9. package/dist/src/gateway/claude-peer.js.map +1 -1
  10. package/dist/src/gateway/cli.d.ts +2 -3
  11. package/dist/src/gateway/cli.js +135 -106
  12. package/dist/src/gateway/cli.js.map +1 -1
  13. package/dist/src/gateway/config.js +1 -1
  14. package/dist/src/gateway/config.js.map +1 -1
  15. package/dist/src/gateway/control.d.ts +23 -5
  16. package/dist/src/gateway/control.js +41 -11
  17. package/dist/src/gateway/control.js.map +1 -1
  18. package/dist/src/gateway/peer-protocol.d.ts +5 -0
  19. package/dist/src/gateway/peer-protocol.js +9 -0
  20. package/dist/src/gateway/peer-protocol.js.map +1 -1
  21. package/dist/src/gateway/providers.d.ts +4 -3
  22. package/dist/src/gateway/providers.js +33 -30
  23. package/dist/src/gateway/providers.js.map +1 -1
  24. package/dist/src/gateway/server.d.ts +1 -2
  25. package/dist/src/gateway/server.js +1 -4
  26. package/dist/src/gateway/server.js.map +1 -1
  27. package/dist/src/gateway/service.d.ts +6 -8
  28. package/dist/src/gateway/service.js +313 -252
  29. package/dist/src/gateway/service.js.map +1 -1
  30. package/dist/src/gateway/status-view.d.ts +2 -50
  31. package/dist/src/gateway/status-view.js +66 -70
  32. package/dist/src/gateway/status-view.js.map +1 -1
  33. package/dist/src/gateway/store.d.ts +4 -1
  34. package/dist/src/gateway/store.js +12 -11
  35. package/dist/src/gateway/store.js.map +1 -1
  36. package/dist/src/gateway/types.d.ts +17 -4
  37. package/dist/src/gateway/types.js +27 -36
  38. package/dist/src/gateway/types.js.map +1 -1
  39. package/docs/CONFIGURATION.md +3 -3
  40. package/docs/GATEWAY-ARCHITECTURE.md +27 -21
  41. package/package.json +1 -1
  42. package/skills/embassy-peer/SKILL.md +7 -5
  43. package/dist/src/gateway/claude-helper-client.d.ts +0 -2
  44. package/dist/src/gateway/claude-helper-client.js +0 -2
  45. package/dist/src/gateway/claude-helper-client.js.map +0 -1
@@ -1,10 +1,10 @@
1
1
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
2
  import { BridgeError } from "../errors.js";
3
3
  import { createGatewayConversationId, startGatewayControlServer, GATEWAY_EPHEMERAL_PEER_TTL_DEFAULT_MS, } from "./control.js";
4
- import { PeerConnectionLostError, spawnPeerClient } from "./peer-client.js";
5
- import { peerRouteRef } from "./peer-protocol.js";
4
+ import { PeerConnectionLostError, PeerRequestError, spawnPeerClient } from "./peer-client.js";
5
+ import { isPeerHandoffRefusal, peerRouteRef } from "./peer-protocol.js";
6
6
  import { GatewayStore } from "./store.js";
7
- import { CONNECTOR_OBSERVATION_STALE_AFTER_MS, directionId, GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayPublicSnapshotLimits, gatewayRegistrationIngressPrefixes, parseDirection, projectGatewayPublicSnapshot, } from "./types.js";
7
+ import { CONNECTOR_OBSERVATION_STALE_AFTER_MS, gatewayHealthWord, directionId, GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayPublicSnapshotLimits, parseDirection, projectGatewayPublicSnapshot, } from "./types.js";
8
8
  const SAFE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
9
9
  const PRIVATE_HANDLE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
10
10
  const PUBLIC_ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
@@ -121,9 +121,6 @@ function conversationIdForSuffix(suffix) {
121
121
  const fresh = createGatewayConversationId();
122
122
  return suffix === undefined ? fresh : `${fresh.slice(0, -8)}${suffix}`;
123
123
  }
124
- function bodyHash(body) {
125
- return createHash("sha256").update(body, "utf8").digest("hex");
126
- }
127
124
  function peerCatalogAuthorityChanged(prior, current) {
128
125
  const authority = (catalog) => JSON.stringify(catalog.routes.map(({ state: _state, queueDepth: _depth, lastSeenAt: _seen, safeErrorCode: _code, ...route }) => route).sort((a, b) => a.ref.localeCompare(b.ref)));
129
126
  return prior === undefined || authority(prior) !== authority(current);
@@ -151,7 +148,7 @@ export class GatewayService {
151
148
  spawnPeer;
152
149
  peerClients = new Map();
153
150
  peerCatalogs = new Map();
154
- peerCleanupAliases = new Set();
151
+ routeCleanupAliases = new Set();
155
152
  /** View-only freshness; never consulted by routing or write authorization. */
156
153
  peerRouteViews = new Map();
157
154
  connectors = new Map();
@@ -237,9 +234,6 @@ export class GatewayService {
237
234
  if (registry !== undefined)
238
235
  runtime.registry = publicRegistry(registry, false);
239
236
  this.connectors.set(key, runtime);
240
- if (started.ownedRoute !== undefined) {
241
- await this.installOwnedRoute(adapter, started.ownedRoute);
242
- }
243
237
  }
244
238
  await this.refreshClaudeDiscovery().catch(() => undefined);
245
239
  await this.refreshManagedCodexSocket().catch(() => undefined);
@@ -335,7 +329,9 @@ export class GatewayService {
335
329
  const decide = async (operation, peerPrincipal = false) => {
336
330
  try {
337
331
  this.assertWritable();
338
- await operation();
332
+ const decision = await operation();
333
+ if (decision !== undefined)
334
+ return decision;
339
335
  this.revision += 1;
340
336
  return { accepted: true, code: "ok" };
341
337
  }
@@ -360,6 +356,14 @@ export class GatewayService {
360
356
  unregisterPeer: (params) => decide(async () => this.unregisterPeer(params), true),
361
357
  awaitPeer: (params) => this.awaitPeer(params),
362
358
  peerReceipt: (params) => decide(async () => this.peerReceipt(params), true),
359
+ retireRoute: async ({ alias }) => {
360
+ try {
361
+ return await this.retireRoute(alias);
362
+ }
363
+ catch (error) {
364
+ return sendRefusal(error);
365
+ }
366
+ },
363
367
  unregisterCodex: (params) => decide(async () => this.unregisterCodex(params)),
364
368
  listSnapshot: () => this.snapshot(),
365
369
  observeSnapshot: () => this.observeSnapshot(),
@@ -418,7 +422,17 @@ export class GatewayService {
418
422
  async receivePeerHandoff(peerHost, handoff) {
419
423
  if (!this.config.peerNodes.includes(peerHost))
420
424
  throw new BridgeError("PEER_NOT_CONFIGURED", "The sending peer host is not configured.");
421
- const enqueued = await this.store.enqueuePeerHandoff(peerHost, handoff);
425
+ let enqueued;
426
+ try {
427
+ enqueued = await this.store.enqueuePeerHandoff(peerHost, handoff);
428
+ }
429
+ catch (error) {
430
+ const refusal = { accepted: false, reason: error instanceof BridgeError ? error.code : undefined };
431
+ if (isPeerHandoffRefusal(refusal))
432
+ return refusal;
433
+ throw error;
434
+ }
435
+ // Post-admission bookkeeping is deliberately outside the refusal catch.
422
436
  if (enqueued.messageId !== undefined) {
423
437
  const source = await this.store.inspectPrivateRoute(handoff.source.alias);
424
438
  const target = await this.store.inspectPrivateRoute(handoff.target.alias);
@@ -460,26 +474,8 @@ export class GatewayService {
460
474
  ...(persisted.safeErrorCode === undefined ? {} : { safeErrorCode: persisted.safeErrorCode }),
461
475
  };
462
476
  });
463
- const connectors = [...this.connectors.values()].map((runtime) => {
464
- const observedAt = runtime.observedAt;
465
- const age = observedAt === undefined ? undefined : Math.max(0, now.getTime() - Date.parse(observedAt));
466
- const stale = runtime.health === "healthy" &&
467
- (age === undefined || age > CONNECTOR_OBSERVATION_STALE_AFTER_MS);
468
- const socketHeld = runtime.adapter.identity.provider === "codex" && this.managedCodexSocketHeldOutside;
469
- const connectorCode = stale ? "CONNECTOR_OBSERVATION_STALE" : runtime.safeErrorCode ??
470
- (socketHeld ? "MANAGED_CODEX_UNAVAILABLE" : undefined);
471
- return {
472
- provider: runtime.adapter.identity.provider,
473
- host: runtime.adapter.identity.hostId,
474
- health: stale || socketHeld ? "degraded" : runtime.health,
475
- protocol: runtime.adapter.protocol,
476
- protocolVersion: runtime.adapter.protocolVersion,
477
- ...(observedAt === undefined ? {} : { lastSeenAt: observedAt }),
478
- ...(age === undefined ? {} : { observationAgeMs: age }),
479
- ...(runtime.registry === undefined ? {} : { registry: runtime.registry }),
480
- ...(connectorCode === undefined ? {} : { safeErrorCode: connectorCode }),
481
- };
482
- });
477
+ const connectors = this.publicConnectors(now);
478
+ const word = gatewayHealthWord(connectors);
483
479
  const availablePeers = [...this.candidates.values()]
484
480
  .filter((candidate) => !this.collidingClaudeAliases.has(candidate.alias))
485
481
  .slice(0, gatewayPublicSnapshotLimits.availablePeers)
@@ -495,11 +491,7 @@ export class GatewayService {
495
491
  return projectGatewayPublicSnapshot({
496
492
  ...base,
497
493
  generatedAt: now.toISOString(),
498
- health: connectors.some((connector) => connector.health === "degraded")
499
- ? "degraded"
500
- : connectors.length === 0
501
- ? "offline"
502
- : "healthy",
494
+ health: word === "ok" || word === "stale" ? "healthy" : word,
503
495
  connectors,
504
496
  availablePeers,
505
497
  routes,
@@ -517,30 +509,29 @@ export class GatewayService {
517
509
  return { snapshotRevision: this.snapshotRevision, snapshot };
518
510
  }
519
511
  health() {
520
- return this.connectors.size > 0 &&
521
- [...this.connectors.values()].every((runtime) => runtime.health === "healthy") ? "ok" : "degraded";
522
- }
523
- async installOwnedRoute(adapter, owned) {
524
- const prefix = gatewayRegistrationIngressPrefixes[adapter.identity.provider];
525
- if (!PUBLIC_ALIAS.test(owned.alias) ||
526
- !owned.alias.endsWith(`@${adapter.identity.hostId}`) ||
527
- (prefix !== undefined && !owned.alias.startsWith(prefix)) ||
528
- !PRIVATE_HANDLE.test(owned.routeHandle)) {
529
- throw new BridgeError("GATEWAY_OWNED_ROUTE_INVALID", "The configured route identity is invalid.");
530
- }
531
- const existing = await this.store.inspectPrivateRoute(owned.alias);
532
- if (existing !== undefined) {
533
- if (existing.binding.provider !== adapter.identity.provider ||
534
- existing.binding.hostId !== adapter.identity.hostId ||
535
- existing.binding.routeHandle !== owned.routeHandle) {
536
- throw new BridgeError("ROUTE_ALIAS_ALREADY_REGISTERED", "The configured route conflicts with durable state.");
537
- }
538
- return;
539
- }
540
- await this.store.registerRoute({
541
- alias: owned.alias,
542
- binding: { ...adapter.identity, routeHandle: owned.routeHandle, registrationId: registrationId() },
543
- registrationMode: "explicit_opt_in",
512
+ const word = gatewayHealthWord(this.publicConnectors(this.now()));
513
+ return word === "ok" || word === "stale" ? "ok" : "degraded";
514
+ }
515
+ publicConnectors(now) {
516
+ return [...this.connectors.values()].map((runtime) => {
517
+ const observedAt = runtime.observedAt;
518
+ const age = observedAt === undefined ? undefined : Math.max(0, now.getTime() - Date.parse(observedAt));
519
+ const stale = runtime.health === "healthy" &&
520
+ (age === undefined || age > CONNECTOR_OBSERVATION_STALE_AFTER_MS);
521
+ const socketHeld = runtime.adapter.identity.provider === "codex" && this.managedCodexSocketHeldOutside;
522
+ const connectorCode = runtime.safeErrorCode ??
523
+ (socketHeld ? "MANAGED_CODEX_UNAVAILABLE" : stale ? "CONNECTOR_OBSERVATION_STALE" : undefined);
524
+ return {
525
+ provider: runtime.adapter.identity.provider,
526
+ host: runtime.adapter.identity.hostId,
527
+ health: stale || socketHeld ? "degraded" : runtime.health,
528
+ protocol: runtime.adapter.protocol,
529
+ protocolVersion: runtime.adapter.protocolVersion,
530
+ ...(observedAt === undefined ? {} : { lastSeenAt: observedAt }),
531
+ ...(age === undefined ? {} : { observationAgeMs: age }),
532
+ ...(runtime.registry === undefined ? {} : { registry: runtime.registry }),
533
+ ...(connectorCode === undefined ? {} : { safeErrorCode: connectorCode }),
534
+ };
544
535
  });
545
536
  }
546
537
  async observeLoadedRoutes() {
@@ -584,18 +575,18 @@ export class GatewayService {
584
575
  return this.adapterFor({ provider: "claude", hostId });
585
576
  }
586
577
  async advertise(route) {
587
- const hostId = route.registrationMode === "federated_peer"
588
- ? this.config.hostId : route.binding.hostId;
589
- await this.claudeAdapter(hostId)?.advertiseNativeSourcePeer?.({
578
+ if (route.registrationMode === "federated_peer")
579
+ return;
580
+ await this.claudeAdapter(route.binding.hostId)?.advertiseNativeSourcePeer?.({
590
581
  alias: route.alias,
591
582
  sourceProvider: route.binding.provider,
592
583
  cwd: this.nativePeerCwd,
593
584
  });
594
585
  }
595
586
  async unadvertise(route) {
596
- const hostId = route.registrationMode === "federated_peer"
597
- ? this.config.hostId : route.binding.hostId;
598
- await this.claudeAdapter(hostId)?.unadvertiseNativeSourcePeer?.(route.alias);
587
+ if (route.registrationMode === "federated_peer")
588
+ return;
589
+ await this.claudeAdapter(route.binding.hostId)?.unadvertiseNativeSourcePeer?.(route.alias);
599
590
  }
600
591
  reconcileAdvertisement(route) {
601
592
  void this.advertise(route).catch((error) => this.alert("NATIVE_ADVERTISEMENT_FAILED", route, error));
@@ -603,6 +594,66 @@ export class GatewayService {
603
594
  async reconcileUnadvertisement(route) {
604
595
  await this.unadvertise(route).catch((error) => this.alert("NATIVE_UNADVERTISEMENT_FAILED", route, error));
605
596
  }
597
+ assertRouteNotCleaning(alias) {
598
+ if (this.routeCleanupAliases.has(alias))
599
+ throw new BridgeError("ROUTE_BUSY", "The route is still completing cleanup.", true);
600
+ }
601
+ async retireRoute(alias) {
602
+ this.assertWritable();
603
+ const route = await this.store.inspectPrivateRoute(alias);
604
+ if (!route)
605
+ return { accepted: false, code: "not_found" };
606
+ if (route.registrationMode === "federated_peer" || route.binding.hostId !== this.config.hostId) {
607
+ throw new BridgeError("FEDERATED_ROUTE_READ_ONLY", "A remote route is retired by its owner, not this broker.");
608
+ }
609
+ this.assertRouteNotCleaning(alias);
610
+ this.routeCleanupAliases.add(alias);
611
+ const expiry = this.ephemeralPeerExpiries.get(alias);
612
+ this.ephemeralPeerExpiries.delete(alias);
613
+ this.ephemeralExpiryFailures.delete(alias);
614
+ let removed = false;
615
+ try {
616
+ const result = await this.store.removeOwnedRouteAtomic({ alias, binding: route.binding,
617
+ activity: { operatorAction: true, action: "route_retired" } });
618
+ if (!result.removed)
619
+ return { accepted: false, code: "not_found" };
620
+ removed = true;
621
+ await this.finishSettlements(result.settlements);
622
+ const incident = (row) => row.sourceBinding?.registrationId === route.binding.registrationId || row.targetBinding?.registrationId === route.binding.registrationId;
623
+ for (const [id, conversation] of this.conversations)
624
+ if (incident(conversation))
625
+ this.conversations.delete(id);
626
+ for (const [id, rows] of this.pendingClaudeReplies) {
627
+ const retained = rows.filter((row) => !incident(row));
628
+ if (retained.length)
629
+ this.pendingClaudeReplies.set(id, retained);
630
+ else
631
+ this.pendingClaudeReplies.delete(id);
632
+ }
633
+ this.forgetRoute(route);
634
+ if (route.binding.provider === "claude") {
635
+ try {
636
+ await this.adapterFor(route.binding)?.releaseRoute?.(route.binding.routeHandle);
637
+ }
638
+ catch (error) {
639
+ this.alert("ROUTE_RELEASE_FAILED", route, error);
640
+ }
641
+ }
642
+ else
643
+ await this.reconcileUnadvertisement(route);
644
+ this.revision += 1;
645
+ return { accepted: true, code: "ok", settlements: {
646
+ cancelled: result.settlements.filter((row) => row.state === "cancelled").length,
647
+ ambiguous: result.settlements.filter((row) => row.state === "ambiguous").length,
648
+ unconfirmed: result.settlements.filter((row) => row.state === "unconfirmed").length,
649
+ } };
650
+ }
651
+ finally {
652
+ if (!removed && expiry !== undefined)
653
+ this.ephemeralPeerExpiries.set(alias, expiry);
654
+ this.routeCleanupAliases.delete(alias);
655
+ }
656
+ }
606
657
  async removeOwnedRoute(route) {
607
658
  this.assertWritable();
608
659
  const result = await this.store.removeOwnedRouteAtomic({
@@ -627,6 +678,7 @@ export class GatewayService {
627
678
  }
628
679
  async registerCodex(params) {
629
680
  const existing = await this.store.inspectPrivateRoute(params.alias);
681
+ this.assertRouteNotCleaning(params.alias);
630
682
  if (existing !== undefined && params.succeedsAlias === undefined) {
631
683
  if (existing.binding.provider !== "codex" || existing.binding.routeHandle !== params.threadId) {
632
684
  throw new BridgeError("ROUTE_ALIAS_ALREADY_REGISTERED", "The alias belongs to another registration.");
@@ -647,6 +699,7 @@ export class GatewayService {
647
699
  };
648
700
  if (params.succeedsAlias === undefined) {
649
701
  this.assertWritable();
702
+ this.assertRouteNotCleaning(params.alias);
650
703
  await this.store.registerRoute(replacement);
651
704
  const installed = (await this.store.inspectPrivateRoute(params.alias));
652
705
  await this.recordActivity("registration", "codex_registered", [installed.alias], true);
@@ -668,6 +721,7 @@ export class GatewayService {
668
721
  throw new BridgeError("CODEX_SUCCESSION_OWNER_MISMATCH", "The succeeded registration is absent.");
669
722
  }
670
723
  this.assertWritable();
724
+ this.assertRouteNotCleaning(params.alias);
671
725
  const result = await this.store.replaceCodexRegistrationAtomic({
672
726
  oldAlias: params.succeedsAlias,
673
727
  expectedOldRegistrationId: previous.binding.registrationId,
@@ -685,7 +739,7 @@ export class GatewayService {
685
739
  this.kick(params.alias);
686
740
  }
687
741
  async registerPeer(params) {
688
- if (this.peerCleanupAliases.has(params.alias)) {
742
+ if (this.routeCleanupAliases.has(params.alias)) {
689
743
  throw new BridgeError("ROUTE_BUSY", "The route is still completing cleanup.", true);
690
744
  }
691
745
  const existing = await this.store.inspectPrivateRoute(params.alias);
@@ -702,6 +756,7 @@ export class GatewayService {
702
756
  throw new BridgeError("ROUTE_UNREGISTERED", "The route binding does not match.");
703
757
  }
704
758
  const token = `peer_${randomBytes(24).toString("base64url")}`;
759
+ this.assertRouteNotCleaning(params.alias);
705
760
  await this.store.registerRoute({ alias: params.alias, binding: {
706
761
  provider: "peer", hostId: this.config.hostId,
707
762
  routeHandle: peerHandle(process.getuid(), params.alias, token), registrationId: registrationId(),
@@ -729,9 +784,9 @@ export class GatewayService {
729
784
  }
730
785
  async unregisterPeer(params) {
731
786
  const route = await this.assertPeer(params);
732
- if (this.peerCleanupAliases.has(route.alias))
787
+ if (this.routeCleanupAliases.has(route.alias))
733
788
  throw new BridgeError("ROUTE_BUSY", "The route is still completing cleanup.", true);
734
- this.peerCleanupAliases.add(route.alias);
789
+ this.routeCleanupAliases.add(route.alias);
735
790
  // The expiry leaves the map before the removal, under the same cleanup
736
791
  // guard, so a clock that fires mid-removal has nothing to act on; a
737
792
  // removal that does not happen puts it back, so a refused unregister
@@ -752,7 +807,7 @@ export class GatewayService {
752
807
  finally {
753
808
  if (!removed && expiry !== undefined)
754
809
  this.ephemeralPeerExpiries.set(route.alias, expiry);
755
- this.peerCleanupAliases.delete(route.alias);
810
+ this.routeCleanupAliases.delete(route.alias);
756
811
  }
757
812
  }
758
813
  /**
@@ -774,7 +829,7 @@ export class GatewayService {
774
829
  if (expiresAt > now)
775
830
  continue;
776
831
  // Another owner is mid-removal; the entry is theirs to clear or restore.
777
- if (this.peerCleanupAliases.has(alias))
832
+ if (this.routeCleanupAliases.has(alias))
778
833
  continue;
779
834
  const route = await this.store.inspectPrivateRoute(alias);
780
835
  if (route === undefined || !this.store.isEphemeralRegistration(route.binding.registrationId)) {
@@ -782,7 +837,7 @@ export class GatewayService {
782
837
  this.ephemeralExpiryFailures.delete(alias);
783
838
  continue;
784
839
  }
785
- this.peerCleanupAliases.add(alias);
840
+ this.routeCleanupAliases.add(alias);
786
841
  try {
787
842
  const result = await this.store.removeOwnedRouteAtomic({ alias, binding: route.binding });
788
843
  if (result.removed) {
@@ -808,7 +863,7 @@ export class GatewayService {
808
863
  }
809
864
  }
810
865
  finally {
811
- this.peerCleanupAliases.delete(alias);
866
+ this.routeCleanupAliases.delete(alias);
812
867
  }
813
868
  }
814
869
  }
@@ -826,7 +881,7 @@ export class GatewayService {
826
881
  const route = await this.assertPeer(params);
827
882
  const adapter = this.adapterFor(route.binding);
828
883
  if (adapter?.acknowledgeReceipt({ alias: route.alias, ...route.binding, receipt: params.receipt }) === "rejected") {
829
- throw new BridgeError("ROUTE_UNREGISTERED", "The route binding does not match.");
884
+ return { accepted: false, code: "not_found" };
830
885
  }
831
886
  }
832
887
  async unregisterCodex(params) {
@@ -894,6 +949,7 @@ export class GatewayService {
894
949
  },
895
950
  registrationMode: "selected_live_peer",
896
951
  };
952
+ this.assertRouteNotCleaning(alias);
897
953
  const result = await this.installWithConcurrentFirstSend(input, adapter.identity.hostId, chosen.routeHandle);
898
954
  await this.finishSettlements(result.settlements);
899
955
  const after = (await this.store.listLogicalRoutes()).filter((route) => route.binding.provider === "claude");
@@ -1014,9 +1070,6 @@ export class GatewayService {
1014
1070
  return routed;
1015
1071
  return await this.installClaudeRoute(candidate);
1016
1072
  }
1017
- async resolveClaudeRoute(selector, hostId) {
1018
- return await this.materializeClaudeRoute(await this.lookUpClaudeRoute(selector, hostId));
1019
- }
1020
1073
  async assertThread(alias, threadId) {
1021
1074
  const route = await this.store.inspectPrivateRoute(alias);
1022
1075
  // No registration at all is its own condition — the state every Codex
@@ -1468,190 +1521,198 @@ export class GatewayService {
1468
1521
  await this.resolvePrewrite(attempt.messageId, attempt.attemptId, "failed", "ROUTE_UNREGISTERED");
1469
1522
  return false;
1470
1523
  }
1471
- if (target.registrationMode === "federated_peer") {
1472
- const peer = this.peerClients.get(target.binding.hostId);
1473
- if (peer === undefined || source.registrationMode === "federated_peer") {
1474
- await this.resolvePrewrite(attempt.messageId, attempt.attemptId, "requeue", "PEER_TUNNEL_UNAVAILABLE");
1475
- return false;
1476
- }
1477
- const sourceEndpoint = { alias: source.alias, provider: source.binding.provider,
1478
- host: source.binding.hostId, routeRef: peerRouteRef(source.binding.hostId, source.binding.registrationId) };
1479
- const targetEndpoint = { alias: target.alias, provider: target.binding.provider,
1480
- host: target.binding.hostId, routeRef: target.binding.routeHandle };
1481
- const params = {
1482
- originAttemptId: attempt.attemptId, originMessageId: attempt.messageId,
1483
- source: sourceEndpoint, target: targetEndpoint,
1484
- deadlineAt: attempt.deadlineAt, expectsReply: conversation?.expectsReply ?? true,
1485
- body: attempt.body,
1486
- ...(attempt.steer === true ? { steer: true } : {}),
1487
- ...(attempt.conversationIdSuffix === undefined ? {} : { conversationCorrelation: attempt.conversationIdSuffix }),
1488
- };
1489
- let armed = false, authorizationUncertain = false, acceptanceObserved = false, result;
1490
- try {
1491
- const prepared = peer.prepareHandoff(params);
1492
- let authorized;
1524
+ return target.registrationMode === "federated_peer"
1525
+ ? this.runPeerAttempt(attempt, source, target, conversation)
1526
+ : this.runProviderAttempt(attempt, source, target, conversation, messageContext);
1527
+ }
1528
+ return false;
1529
+ }
1530
+ async runPeerAttempt(attempt, source, target, conversation) {
1531
+ const peer = this.peerClients.get(target.binding.hostId);
1532
+ if (peer === undefined || source.registrationMode === "federated_peer") {
1533
+ await this.resolvePrewrite(attempt.messageId, attempt.attemptId, "requeue", "PEER_TUNNEL_UNAVAILABLE");
1534
+ return false;
1535
+ }
1536
+ const sourceEndpoint = { alias: source.alias, provider: source.binding.provider,
1537
+ host: source.binding.hostId, routeRef: peerRouteRef(source.binding.hostId, source.binding.registrationId) };
1538
+ const targetEndpoint = { alias: target.alias, provider: target.binding.provider,
1539
+ host: target.binding.hostId, routeRef: target.binding.routeHandle };
1540
+ const params = {
1541
+ originAttemptId: attempt.attemptId, originMessageId: attempt.messageId,
1542
+ source: sourceEndpoint, target: targetEndpoint,
1543
+ deadlineAt: attempt.deadlineAt, expectsReply: conversation?.expectsReply ?? true,
1544
+ body: attempt.body,
1545
+ ...(attempt.steer === true ? { steer: true } : {}),
1546
+ ...(attempt.conversationIdSuffix === undefined ? {} : { conversationCorrelation: attempt.conversationIdSuffix }),
1547
+ };
1548
+ let armed = false, authorizationUncertain = false, acceptanceObserved = false, result;
1549
+ try {
1550
+ const prepared = peer.prepareHandoff(params);
1551
+ let authorized;
1552
+ try {
1553
+ authorized = await this.store.authorizeMessage({ messageId: attempt.messageId,
1554
+ attemptId: attempt.attemptId, sourceRegistrationId: attempt.sourceRegistrationId,
1555
+ targetRegistrationId: attempt.targetRegistrationId,
1556
+ prepared: { kind: "peer_handoff", bodyBytes: prepared.bodyBytes,
1557
+ bodySha256: prepared.bodySha256, frameBytes: prepared.frameBytes, sha256: prepared.sha256 } });
1558
+ }
1559
+ catch (error) {
1560
+ authorizationUncertain = true;
1561
+ throw error;
1562
+ }
1563
+ if (authorized.status !== "authorized") {
1564
+ prepared.cancel();
1565
+ if (authorized.status === "terminal")
1566
+ await this.finishSettlement(authorized.settlement);
1567
+ this.activeAttempts.delete(attempt.messageId);
1568
+ return false;
1569
+ }
1570
+ armed = true;
1571
+ await prepared.perform();
1572
+ acceptanceObserved = true;
1573
+ const accepted = await this.store.acceptMessage({ messageId: attempt.messageId,
1574
+ attemptId: attempt.attemptId, lossOutcome: "unconfirmed" });
1575
+ if (accepted.status !== "accepted")
1576
+ throw new BridgeError("ACCEPTANCE_UNCONFIRMED", "The peer acceptance fence is stale.");
1577
+ result = { state: "delivered", safeErrorCode: "PEER_HANDOFF_CONFIRMED" };
1578
+ }
1579
+ catch (error) {
1580
+ if (!armed && !authorizationUncertain) {
1581
+ peer.close();
1582
+ this.peerClients.delete(target.binding.hostId);
1583
+ await this.resolvePrewrite(attempt.messageId, attempt.attemptId, "requeue", "PEER_TUNNEL_UNAVAILABLE");
1584
+ return false;
1585
+ }
1586
+ result = armed && !authorizationUncertain && !acceptanceObserved &&
1587
+ error instanceof PeerRequestError && error.detail.code === -32000 && isPeerHandoffRefusal(error.detail.data)
1588
+ ? { state: "failed", safeErrorCode: error.detail.data.reason }
1589
+ : acceptanceObserved
1590
+ ? { state: "unconfirmed", safeErrorCode: "PEER_HANDOFF_ACCEPTANCE_UNCONFIRMED" }
1591
+ : { state: "ambiguous", safeErrorCode: authorizationUncertain ? "WRITE_AUTHORIZATION_UNCERTAIN" : "PEER_HANDOFF_OUTCOME_UNKNOWN" };
1592
+ }
1593
+ const requeued = await this.applyDispatchResult(attempt.messageId, attempt.attemptId, result, armed, conversation, source.alias, target.alias);
1594
+ this.activeAttempts.delete(attempt.messageId);
1595
+ return requeued;
1596
+ }
1597
+ async runProviderAttempt(attempt, source, target, conversation, messageContext) {
1598
+ const adapter = this.adapterFor(target.binding);
1599
+ if (adapter === undefined) {
1600
+ await this.resolvePrewrite(attempt.messageId, attempt.attemptId, "failed", "PROVIDER_UNAVAILABLE");
1601
+ return false;
1602
+ }
1603
+ const active = this.activeAttempts.get(attempt.messageId);
1604
+ let authorizationUncertain = false;
1605
+ let armed = false;
1606
+ let accepted = false;
1607
+ const conversationId = messageContext?.conversationId ??
1608
+ conversationIdForSuffix(attempt.conversationIdSuffix);
1609
+ const parsed = parseDirection(attempt.direction);
1610
+ if (target.binding.provider === "claude" &&
1611
+ messageContext?.expectsReply === true &&
1612
+ ![...this.pendingClaudeReplies.values()].some((rows) => rows.some((row) => row.messageId === attempt.messageId)) &&
1613
+ [...this.pendingClaudeReplies.values()].reduce((count, rows) => count + rows.length, 0) >= MAX_PENDING_CLAUDE_REPLIES) {
1614
+ await this.resolvePrewrite(attempt.messageId, attempt.attemptId, "requeue", "ROUTE_BUSY");
1615
+ return true;
1616
+ }
1617
+ let result;
1618
+ try {
1619
+ result = await adapter.dispatch({
1620
+ attemptId: attempt.attemptId,
1621
+ sourceAlias: source.alias,
1622
+ sourceProvider: parsed.sourceProvider,
1623
+ // The route's CURRENT alias, not the one captured at reserve. The
1624
+ // provider adapters compare this against the alias they hold for the
1625
+ // binding, so a session renamed between reserve and dispatch would
1626
+ // otherwise fail its own delivery over a stale display name.
1627
+ targetAlias: target.alias,
1628
+ conversationId,
1629
+ binding: target.binding,
1630
+ authorization: "selected_route",
1631
+ messageId: attempt.messageId,
1632
+ text: attempt.body,
1633
+ expectsReply: conversation?.expectsReply ?? true,
1634
+ deadlineAt: attempt.deadlineAt,
1635
+ ...(attempt.steer === true ? { steer: true, queuedAhead: 0 } : {}),
1636
+ authorizeWrite: async (evidence) => {
1637
+ if (evidence.attemptId !== attempt.attemptId)
1638
+ return false;
1493
1639
  try {
1494
- authorized = await this.store.authorizeMessage({ messageId: attempt.messageId,
1495
- attemptId: attempt.attemptId, sourceRegistrationId: attempt.sourceRegistrationId,
1640
+ const authorized = await this.store.authorizeMessage({
1641
+ messageId: attempt.messageId,
1642
+ attemptId: attempt.attemptId,
1643
+ sourceRegistrationId: attempt.sourceRegistrationId,
1496
1644
  targetRegistrationId: attempt.targetRegistrationId,
1497
- prepared: { kind: "peer_handoff", bodyBytes: prepared.bodyBytes,
1498
- bodySha256: prepared.bodySha256, frameBytes: prepared.frameBytes, sha256: prepared.sha256 } });
1645
+ prepared: {
1646
+ kind: evidence.kind,
1647
+ bodyBytes: evidence.bodyBytes,
1648
+ bodySha256: evidence.bodySha256,
1649
+ frameBytes: evidence.frameBytes,
1650
+ sha256: evidence.sha256,
1651
+ },
1652
+ });
1653
+ if (authorized.status === "terminal") {
1654
+ await this.finishSettlement(authorized.settlement);
1655
+ }
1656
+ if (authorized.status === "authorized") {
1657
+ armed = true;
1658
+ if (target.binding.provider === "claude" &&
1659
+ messageContext?.expectsReply === true) {
1660
+ this.installPendingClaudeReply({
1661
+ messageId: attempt.messageId,
1662
+ conversationId,
1663
+ sourceAlias: source.alias,
1664
+ targetAlias: target.alias,
1665
+ sourceBinding: source.binding,
1666
+ targetBinding: target.binding,
1667
+ deadlineAt: attempt.deadlineAt,
1668
+ state: "armed",
1669
+ });
1670
+ }
1671
+ }
1672
+ return authorized.status === "authorized";
1499
1673
  }
1500
1674
  catch (error) {
1501
1675
  authorizationUncertain = true;
1502
1676
  throw error;
1503
1677
  }
1504
- if (authorized.status !== "authorized") {
1505
- prepared.cancel();
1506
- if (authorized.status === "terminal")
1507
- await this.finishSettlement(authorized.settlement);
1508
- this.activeAttempts.delete(attempt.messageId);
1509
- return false;
1678
+ },
1679
+ onAccepted: async (evidence) => {
1680
+ if ((target.binding.provider !== "codex" && target.binding.provider !== "peer") ||
1681
+ evidence.attemptId !== attempt.attemptId) {
1682
+ throw new BridgeError("ACCEPTANCE_UNCONFIRMED", "The provider accepted another attempt.");
1510
1683
  }
1511
- armed = true;
1512
- await prepared.perform();
1513
- acceptanceObserved = true;
1514
- const accepted = await this.store.acceptMessage({ messageId: attempt.messageId,
1515
- attemptId: attempt.attemptId, lossOutcome: "unconfirmed" });
1516
- if (accepted.status !== "accepted")
1517
- throw new BridgeError("ACCEPTANCE_UNCONFIRMED", "The peer acceptance fence is stale.");
1518
- result = { state: "delivered", safeErrorCode: "PEER_HANDOFF_CONFIRMED" };
1519
- }
1520
- catch {
1521
- if (!armed && !authorizationUncertain) {
1522
- peer.close();
1523
- this.peerClients.delete(target.binding.hostId);
1524
- await this.resolvePrewrite(attempt.messageId, attempt.attemptId, "requeue", "PEER_TUNNEL_UNAVAILABLE");
1525
- return false;
1684
+ const result = await this.store.acceptMessage({
1685
+ messageId: attempt.messageId,
1686
+ attemptId: attempt.attemptId,
1687
+ lossOutcome: attempt.steer === true ? "ambiguous" : "unconfirmed",
1688
+ });
1689
+ if (result.status !== "accepted") {
1690
+ throw new BridgeError("ACCEPTANCE_UNCONFIRMED", "The durable acceptance fence is stale.");
1526
1691
  }
1527
- result = acceptanceObserved
1528
- ? { state: "unconfirmed", safeErrorCode: "PEER_HANDOFF_ACCEPTANCE_UNCONFIRMED" }
1529
- : { state: "ambiguous", safeErrorCode: authorizationUncertain ? "WRITE_AUTHORIZATION_UNCERTAIN" : "PEER_HANDOFF_OUTCOME_UNKNOWN" };
1530
- }
1531
- const requeued = await this.applyDispatchResult(attempt.messageId, attempt.attemptId, result, armed, conversation, attempt.sourceAlias, attempt.targetAlias);
1532
- this.activeAttempts.delete(attempt.messageId);
1533
- return requeued;
1534
- }
1535
- const adapter = this.adapterFor(target.binding);
1536
- if (adapter === undefined) {
1537
- await this.resolvePrewrite(attempt.messageId, attempt.attemptId, "failed", "PROVIDER_UNAVAILABLE");
1538
- return false;
1539
- }
1540
- const active = this.activeAttempts.get(attempt.messageId);
1541
- let authorizationUncertain = false;
1542
- let armed = false;
1543
- let accepted = false;
1544
- const conversationId = messageContext?.conversationId ??
1545
- conversationIdForSuffix(attempt.conversationIdSuffix);
1546
- const parsed = parseDirection(attempt.direction);
1547
- if (target.binding.provider === "claude" &&
1548
- messageContext?.expectsReply === true &&
1549
- ![...this.pendingClaudeReplies.values()].some((rows) => rows.some((row) => row.messageId === attempt.messageId)) &&
1550
- [...this.pendingClaudeReplies.values()].reduce((count, rows) => count + rows.length, 0) >= MAX_PENDING_CLAUDE_REPLIES) {
1551
- await this.resolvePrewrite(attempt.messageId, attempt.attemptId, "requeue", "ROUTE_BUSY");
1552
- return true;
1553
- }
1554
- let result;
1555
- try {
1556
- result = await adapter.dispatch({
1557
- attemptId: attempt.attemptId,
1558
- sourceAlias: attempt.sourceAlias,
1559
- sourceProvider: parsed.sourceProvider,
1560
- // The route's CURRENT alias, not the one captured at reserve. The
1561
- // provider adapters compare this against the alias they hold for the
1562
- // binding, so a session renamed between reserve and dispatch would
1563
- // otherwise fail its own delivery over a stale display name.
1564
- targetAlias: target.alias,
1565
- conversationId,
1566
- binding: target.binding,
1567
- authorization: "selected_route",
1568
- messageId: attempt.messageId,
1569
- text: attempt.body,
1570
- expectsReply: conversation?.expectsReply ?? true,
1571
- deadlineAt: attempt.deadlineAt,
1572
- ...(attempt.steer === true ? { steer: true, queuedAhead: 0 } : {}),
1573
- authorizeWrite: async (evidence) => {
1574
- if (evidence.attemptId !== attempt.attemptId)
1575
- return false;
1576
- try {
1577
- const authorized = await this.store.authorizeMessage({
1578
- messageId: attempt.messageId,
1579
- attemptId: attempt.attemptId,
1580
- sourceRegistrationId: attempt.sourceRegistrationId,
1581
- targetRegistrationId: attempt.targetRegistrationId,
1582
- prepared: {
1583
- kind: evidence.kind,
1584
- bodyBytes: evidence.bodyBytes,
1585
- bodySha256: evidence.bodySha256,
1586
- frameBytes: evidence.frameBytes,
1587
- sha256: evidence.sha256,
1588
- },
1589
- });
1590
- if (authorized.status === "terminal") {
1591
- await this.finishSettlement(authorized.settlement);
1592
- }
1593
- if (authorized.status === "authorized") {
1594
- armed = true;
1595
- if (target.binding.provider === "claude" &&
1596
- messageContext?.expectsReply === true) {
1597
- this.installPendingClaudeReply({
1598
- messageId: attempt.messageId,
1599
- conversationId,
1600
- sourceAlias: attempt.sourceAlias,
1601
- targetAlias: attempt.targetAlias,
1602
- sourceBinding: source.binding,
1603
- targetBinding: target.binding,
1604
- deadlineAt: attempt.deadlineAt,
1605
- state: "armed",
1606
- });
1607
- }
1608
- }
1609
- return authorized.status === "authorized";
1610
- }
1611
- catch (error) {
1612
- authorizationUncertain = true;
1613
- throw error;
1614
- }
1615
- },
1616
- onAccepted: async (evidence) => {
1617
- if ((target.binding.provider !== "codex" && target.binding.provider !== "peer") ||
1618
- evidence.attemptId !== attempt.attemptId) {
1619
- throw new BridgeError("ACCEPTANCE_UNCONFIRMED", "The provider accepted another attempt.");
1620
- }
1621
- const result = await this.store.acceptMessage({
1622
- messageId: attempt.messageId,
1623
- attemptId: attempt.attemptId,
1624
- lossOutcome: attempt.steer === true ? "ambiguous" : "unconfirmed",
1625
- });
1626
- if (result.status !== "accepted") {
1627
- throw new BridgeError("ACCEPTANCE_UNCONFIRMED", "The durable acceptance fence is stale.");
1628
- }
1629
- accepted = true;
1630
- },
1631
- });
1632
- }
1633
- catch {
1634
- result = {
1635
- state: accepted
1636
- ? attempt.steer === true
1637
- ? "ambiguous"
1638
- : "unconfirmed"
1639
- : armed || authorizationUncertain
1640
- ? "ambiguous"
1641
- : "failed",
1642
- safeErrorCode: accepted
1643
- ? "DELIVERY_UNCONFIRMED"
1644
- : authorizationUncertain || armed
1645
- ? "WRITE_AUTHORIZATION_UNCERTAIN"
1646
- : "PROVIDER_DISPATCH_FAILED",
1647
- };
1648
- }
1649
- const requeued = await this.applyDispatchResult(attempt.messageId, attempt.attemptId, result, armed, conversation, attempt.sourceAlias, attempt.targetAlias);
1650
- if (this.activeAttempts.get(attempt.messageId) === active)
1651
- this.activeAttempts.delete(attempt.messageId);
1652
- return requeued;
1692
+ accepted = true;
1693
+ },
1694
+ });
1653
1695
  }
1654
- return false;
1696
+ catch {
1697
+ result = {
1698
+ state: accepted
1699
+ ? attempt.steer === true
1700
+ ? "ambiguous"
1701
+ : "unconfirmed"
1702
+ : armed || authorizationUncertain
1703
+ ? "ambiguous"
1704
+ : "failed",
1705
+ safeErrorCode: accepted
1706
+ ? "DELIVERY_UNCONFIRMED"
1707
+ : authorizationUncertain || armed
1708
+ ? "WRITE_AUTHORIZATION_UNCERTAIN"
1709
+ : "PROVIDER_DISPATCH_FAILED",
1710
+ };
1711
+ }
1712
+ const requeued = await this.applyDispatchResult(attempt.messageId, attempt.attemptId, result, armed, conversation, source.alias, target.alias);
1713
+ if (this.activeAttempts.get(attempt.messageId) === active)
1714
+ this.activeAttempts.delete(attempt.messageId);
1715
+ return requeued;
1655
1716
  }
1656
1717
  async settleAttemptForShutdown(messageId, attemptId) {
1657
1718
  const result = await this.store.settleAttemptForShutdown({ messageId, attemptId });