agent-embassy 1.6.0 → 1.7.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 (97) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +9 -9
  3. package/README.zh-CN.md +9 -9
  4. package/SECURITY.md +34 -77
  5. package/dist/src/gateway/acp-client.d.ts +117 -0
  6. package/dist/src/gateway/acp-client.js +382 -0
  7. package/dist/src/gateway/acp-client.js.map +1 -0
  8. package/dist/src/gateway/acp-provider.d.ts +65 -0
  9. package/dist/src/gateway/acp-provider.js +163 -0
  10. package/dist/src/gateway/acp-provider.js.map +1 -0
  11. package/dist/src/gateway/claude-helper-protocol.d.ts +3 -1
  12. package/dist/src/gateway/claude-helper-protocol.js +15 -5
  13. package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
  14. package/dist/src/gateway/claude-helper-supervisor.d.ts +3 -1
  15. package/dist/src/gateway/claude-helper-supervisor.js +16 -2
  16. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
  17. package/dist/src/gateway/claude-helper.js +4 -3
  18. package/dist/src/gateway/claude-helper.js.map +1 -1
  19. package/dist/src/gateway/claude-peer.d.ts +1 -1
  20. package/dist/src/gateway/claude-peer.js +11 -13
  21. package/dist/src/gateway/claude-peer.js.map +1 -1
  22. package/dist/src/gateway/claude-runtime.d.ts +0 -20
  23. package/dist/src/gateway/claude-runtime.js +6 -127
  24. package/dist/src/gateway/claude-runtime.js.map +1 -1
  25. package/dist/src/gateway/cli-copy.en.d.ts +1 -1
  26. package/dist/src/gateway/cli-copy.en.js +2 -2
  27. package/dist/src/gateway/cli-copy.zh-CN.d.ts +1 -1
  28. package/dist/src/gateway/cli-copy.zh-CN.js +2 -2
  29. package/dist/src/gateway/cli.d.ts +1 -1
  30. package/dist/src/gateway/cli.js +26 -5
  31. package/dist/src/gateway/cli.js.map +1 -1
  32. package/dist/src/gateway/codex-app-server.d.ts +5 -74
  33. package/dist/src/gateway/codex-app-server.js +9 -597
  34. package/dist/src/gateway/codex-app-server.js.map +1 -1
  35. package/dist/src/gateway/codex-local-transport.d.ts +8 -26
  36. package/dist/src/gateway/codex-local-transport.js +26 -76
  37. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  38. package/dist/src/gateway/compatibility.d.ts +6 -66
  39. package/dist/src/gateway/compatibility.js +14 -296
  40. package/dist/src/gateway/compatibility.js.map +1 -1
  41. package/dist/src/gateway/config.d.ts +9 -0
  42. package/dist/src/gateway/config.js +6 -2
  43. package/dist/src/gateway/config.js.map +1 -1
  44. package/dist/src/gateway/control.d.ts +16 -1
  45. package/dist/src/gateway/control.js +89 -57
  46. package/dist/src/gateway/control.js.map +1 -1
  47. package/dist/src/gateway/dashboard-copy.d.ts +1 -1
  48. package/dist/src/gateway/dashboard-copy.en.d.ts +14 -44
  49. package/dist/src/gateway/dashboard-copy.en.js +37 -67
  50. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  51. package/dist/src/gateway/dashboard-copy.js +14 -44
  52. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  53. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +14 -44
  54. package/dist/src/gateway/dashboard-copy.zh-CN.js +37 -67
  55. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  56. package/dist/src/gateway/dashboard-model.d.ts +38 -76
  57. package/dist/src/gateway/dashboard-model.js +158 -256
  58. package/dist/src/gateway/dashboard-model.js.map +1 -1
  59. package/dist/src/gateway/dashboard.d.ts +0 -3
  60. package/dist/src/gateway/dashboard.js +75 -76
  61. package/dist/src/gateway/dashboard.js.map +1 -1
  62. package/dist/src/gateway/deepseek-detect.d.ts +7 -6
  63. package/dist/src/gateway/deepseek-detect.js +31 -115
  64. package/dist/src/gateway/deepseek-detect.js.map +1 -1
  65. package/dist/src/gateway/live-dashboard-app/app.js +166 -621
  66. package/dist/src/gateway/live-dashboard-command.js +2 -4
  67. package/dist/src/gateway/live-dashboard-command.js.map +1 -1
  68. package/dist/src/gateway/live-dashboard-http.d.ts +1 -2
  69. package/dist/src/gateway/live-dashboard-http.js +14 -15
  70. package/dist/src/gateway/live-dashboard-http.js.map +1 -1
  71. package/dist/src/gateway/progress-watch-machine.d.ts +1 -4
  72. package/dist/src/gateway/progress-watch-machine.js.map +1 -1
  73. package/dist/src/gateway/provenance-envelope.d.ts +3 -2
  74. package/dist/src/gateway/provenance-envelope.js +44 -11
  75. package/dist/src/gateway/provenance-envelope.js.map +1 -1
  76. package/dist/src/gateway/providers.d.ts +12 -84
  77. package/dist/src/gateway/providers.js +69 -806
  78. package/dist/src/gateway/providers.js.map +1 -1
  79. package/dist/src/gateway/server.d.ts +8 -9
  80. package/dist/src/gateway/server.js +51 -99
  81. package/dist/src/gateway/server.js.map +1 -1
  82. package/dist/src/gateway/service.d.ts +22 -49
  83. package/dist/src/gateway/service.js +196 -432
  84. package/dist/src/gateway/service.js.map +1 -1
  85. package/dist/src/gateway/store.d.ts +20 -29
  86. package/dist/src/gateway/store.js +281 -827
  87. package/dist/src/gateway/store.js.map +1 -1
  88. package/dist/src/gateway/types.d.ts +55 -55
  89. package/dist/src/gateway/types.js +49 -91
  90. package/dist/src/gateway/types.js.map +1 -1
  91. package/docs/CONFIGURATION.md +11 -10
  92. package/docs/CONFIGURATION.zh-CN.md +10 -9
  93. package/docs/DASHBOARD.md +4 -38
  94. package/docs/DASHBOARD.zh-CN.md +4 -2
  95. package/docs/GATEWAY-ARCHITECTURE.md +61 -119
  96. package/package.json +1 -1
  97. package/skills/embassy-peer/SKILL.md +6 -6
@@ -1,10 +1,9 @@
1
- import { arePublicAvailablePeerSnapshots, isPublicCompatibilityCheckSnapshot, isPublicRegistryObservationSnapshot, } from "./types.js";
2
- import { compatibilitySurfaceDefinitions, } from "./compatibility.js";
1
+ import { arePublicAvailablePeerSnapshots, gatewayProviders, isPublicRegistryObservationSnapshot, parseDirection, } from "./types.js";
3
2
  export const DASHBOARD_MODEL_LIMITS = Object.freeze({
4
3
  connectors: 16,
5
4
  availablePeers: 64,
6
5
  routes: 128,
7
- pairs: 64,
6
+ consentEdges: 64,
8
7
  messages: 50,
9
8
  messageEvents: 60,
10
9
  progressWatches: 64,
@@ -35,7 +34,6 @@ const statePresentation = {
35
34
  busy: { tone: "info", chip: "active" },
36
35
  awaiting_approval: { tone: "warning", chip: "warning" },
37
36
  offline: { tone: "danger", chip: "failure" },
38
- incompatible: { tone: "danger", chip: "failure" },
39
37
  disabled: { tone: "quiet", chip: "inert" },
40
38
  }),
41
39
  peer: presentation({
@@ -43,20 +41,12 @@ const statePresentation = {
43
41
  busy: { tone: "info", chip: "active" },
44
42
  awaiting_approval: { tone: "warning", chip: "warning" },
45
43
  offline: { tone: "danger", chip: "inert" },
46
- incompatible: { tone: "danger", chip: "failure" },
47
44
  }),
48
45
  health: presentation({
49
46
  healthy: { tone: "good", chip: "positive" },
50
47
  connecting: { tone: "info", chip: "progress" },
51
48
  degraded: { tone: "warning", chip: "warning" },
52
49
  offline: { tone: "danger", chip: "failure" },
53
- incompatible: { tone: "danger", chip: "failure" },
54
- }),
55
- compatibility: presentation({
56
- compatible: { tone: "good", chip: "positive" },
57
- unknown: { tone: "quiet", chip: "inert" },
58
- expired: { tone: "warning", chip: "warning" },
59
- incompatible: { tone: "danger", chip: "failure" },
60
50
  }),
61
51
  overall: presentation({
62
52
  ready: { tone: "good", chip: "positive" },
@@ -104,7 +94,6 @@ const guidanceCopyKeys = {
104
94
  progress_watch: "progressWatch",
105
95
  registry_empty: "registryEmpty",
106
96
  registry_rejected: "registryRejected",
107
- provider_incompatible: "providerIncompatible",
108
97
  generic: "generic",
109
98
  };
110
99
  const nextActionCopyKeys = {
@@ -115,7 +104,6 @@ const nextActionCopyKeys = {
115
104
  repair_claude_inventory: "next.repairClaude",
116
105
  register_codex: "next.registerCodex",
117
106
  restore_codex: "next.restoreCodex",
118
- review_compatibility: "next.reviewCompatibility",
119
107
  none: "next.none",
120
108
  };
121
109
  const attentionCommands = {
@@ -137,7 +125,6 @@ const attentionCommands = {
137
125
  progress_watch: "embassy status",
138
126
  registry_empty: "embassy refresh-dashboard",
139
127
  registry_rejected: "embassy status",
140
- provider_incompatible: "embassy refresh-dashboard",
141
128
  generic: "embassy status",
142
129
  };
143
130
  const deliveryMeaningKeys = {
@@ -158,8 +145,8 @@ const deliveryMeaningKeys = {
158
145
  /** JSON-safe semantics embedded once in the live dashboard boot payload. */
159
146
  export const DASHBOARD_SEMANTICS = Object.freeze({
160
147
  statePresentation,
161
- deliveryChipByDirection: {
162
- delivered: { codex_to_claude: "qualified" },
148
+ deliveryChipByTargetProvider: {
149
+ delivered: { claude: "qualified" },
163
150
  },
164
151
  deliveryChipBySafeErrorCode: {
165
152
  rejected: { SENDER_NOT_PAIRED: "inert" },
@@ -175,9 +162,11 @@ export const DASHBOARD_SEMANTICS = Object.freeze({
175
162
  codex_succession_recovery: "<old>",
176
163
  },
177
164
  deliveryMeaningKeys,
178
- deliveryMeaningByDirection: {
179
- codex_to_claude: "activity.meaning.delivered.codexToClaude",
180
- claude_to_codex: "activity.meaning.delivered.claudeToCodex",
165
+ deliveryMeaningByTargetProvider: {
166
+ claude: "activity.meaning.delivered.toClaude",
167
+ codex: "activity.meaning.delivered.toCodex",
168
+ deepseek: "activity.meaning.delivered.toDeepSeek",
169
+ grok: "activity.meaning.delivered.toGrok",
181
170
  },
182
171
  deliveryMeaningBySafeErrorCode: {
183
172
  SENDER_NOT_PAIRED: "activity.meaning.senderNotPaired",
@@ -196,14 +185,15 @@ function semanticPresentation(domain, state) {
196
185
  export function dashboardChipKind(domain, state, direction, safeErrorCode) {
197
186
  if (domain === "delivery") {
198
187
  const byCode = DASHBOARD_SEMANTICS.deliveryChipBySafeErrorCode;
199
- const byDirection = DASHBOARD_SEMANTICS.deliveryChipByDirection;
188
+ const byTarget = DASHBOARD_SEMANTICS.deliveryChipByTargetProvider;
200
189
  if (safeErrorCode !== undefined) {
201
190
  const override = byCode[state]?.[safeErrorCode];
202
191
  if (override !== undefined)
203
192
  return override;
204
193
  }
205
194
  if (direction !== undefined) {
206
- const override = byDirection[state]?.[direction];
195
+ const target = parseDirection(direction)?.targetProvider;
196
+ const override = target === undefined ? undefined : byTarget[state]?.[target];
207
197
  if (override !== undefined)
208
198
  return override;
209
199
  }
@@ -224,7 +214,10 @@ export function deliveryMeaningKey(state, direction, safeErrorCode) {
224
214
  return DASHBOARD_SEMANTICS.deliveryMeaningBySafeErrorCode.SENDER_NOT_PAIRED;
225
215
  }
226
216
  if (state === "delivered" && direction !== undefined) {
227
- return DASHBOARD_SEMANTICS.deliveryMeaningByDirection[direction];
217
+ const target = parseDirection(direction)?.targetProvider;
218
+ if (target !== undefined) {
219
+ return DASHBOARD_SEMANTICS.deliveryMeaningByTargetProvider[target];
220
+ }
228
221
  }
229
222
  if (state === "abandoned" && safeErrorCode !== undefined) {
230
223
  const byCode = DASHBOARD_SEMANTICS.deliveryMeaningBySafeErrorCode;
@@ -300,16 +293,14 @@ function normalizedCounters(counters) {
300
293
  bytesAccepted: normalizedInteger(counters?.bytesAccepted) ?? 0,
301
294
  };
302
295
  }
303
- export function buildDashboardViewModel(snapshot, options = {}) {
304
- return buildProjectedDashboardViewModel(snapshot, false, options);
296
+ export function buildDashboardViewModel(snapshot) {
297
+ return buildProjectedDashboardViewModel(snapshot, false);
305
298
  }
306
- export function buildLiveDashboardViewModel(snapshot, options = {}) {
307
- return buildProjectedDashboardViewModel(snapshot, true, options);
299
+ export function buildLiveDashboardViewModel(snapshot) {
300
+ return buildProjectedDashboardViewModel(snapshot, true);
308
301
  }
309
302
  function routeIsReady(route) {
310
303
  return (route.enabled &&
311
- route.safeErrorCode !== "CODEX_WRITES_DISABLED" &&
312
- route.compatibility === "compatible" &&
313
304
  (route.state === "idle" || route.state === "busy"));
314
305
  }
315
306
  function peerSelectionGuidance(peer) {
@@ -322,11 +313,6 @@ function peerSelectionGuidance(peer) {
322
313
  }
323
314
  if (peer.state === "offline")
324
315
  return "offline";
325
- if (peer.state === "incompatible" ||
326
- peer.compatibility !== "compatible" ||
327
- peer.safeErrorCode !== undefined) {
328
- return "incompatible";
329
- }
330
316
  return undefined;
331
317
  }
332
318
  function peerIsSelectable(peer) {
@@ -343,10 +329,10 @@ function partyStatus(routes, ready) {
343
329
  return routes.some((route) => route.state === "busy") ? "busy" : "ready";
344
330
  }
345
331
  function routePriority(route) {
346
- if (!route.enabled || route.state === "offline" || route.state === "incompatible") {
332
+ if (!route.enabled || route.state === "offline") {
347
333
  return 0;
348
334
  }
349
- if (route.state === "stale" || route.compatibility !== "compatible")
335
+ if (route.state === "stale")
350
336
  return 1;
351
337
  if (route.state === "awaiting_approval")
352
338
  return 2;
@@ -367,8 +353,7 @@ function needsCodexAppReconnect(route, connectors, generatedAt) {
367
353
  }
368
354
  return connectors.some((connector) => connector.provider === "codex" &&
369
355
  connector.host === route.host &&
370
- connector.health === "healthy" &&
371
- connector.compatibility === "compatible");
356
+ connector.health === "healthy");
372
357
  }
373
358
  function alertPriority(severity) {
374
359
  return severity === "error" ? 0 : severity === "warning" ? 1 : 2;
@@ -405,14 +390,6 @@ function guidanceFor(code, provider) {
405
390
  case "ADAPTER_DEGRADED":
406
391
  case "ROUTE_DEGRADED":
407
392
  return "degraded";
408
- case "CLAUDE_PEER_VERSION_UNSUPPORTED":
409
- case "CLAUDE_VERSION_UNPARSEABLE":
410
- case "CLAUDE_VERSION_CHECK_FAILED":
411
- case "CLAUDE_VERSION_EVIDENCE_CONFLICT":
412
- case "CLAUDE_VERSION_EVIDENCE_TOO_LARGE":
413
- case "CODEX_APP_SERVER_VERSION_UNSUPPORTED":
414
- case "CODEX_APP_SERVER_VERSION_UNPARSEABLE":
415
- return "provider_incompatible";
416
393
  default:
417
394
  return "generic";
418
395
  }
@@ -574,35 +551,17 @@ function buildMessageGroups(messages, includeBodies) {
574
551
  activeGroups: all.filter((message) => ["queued", "dispatching", "transport_written", "held"].includes(message.state)).length,
575
552
  };
576
553
  }
577
- function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
578
- const surfaceDefinitions = options.compatibilitySurfaceDefinitions ?? compatibilitySurfaceDefinitions;
554
+ function buildProjectedDashboardViewModel(snapshot, includeBodies) {
579
555
  const generatedAt = normalizedTimestamp(snapshot.generatedAt);
580
556
  const inboundMode = snapshot.inboundMode === "open" ? "open" : "paired";
581
557
  const validPeers = arePublicAvailablePeerSnapshots(snapshot.availablePeers)
582
558
  ? snapshot.availablePeers
583
559
  : [];
584
- const writesCoveredProviders = new Set((Array.isArray(snapshot.compatibilityChecks)
585
- ? snapshot.compatibilityChecks.filter(isPublicCompatibilityCheckSnapshot)
586
- : []).filter((check) => check.writesCovered).map((check) => check.surface));
587
- const quarantinedProviders = new Set([
588
- ...snapshot.connectors
589
- .filter((connector) => connector.compatibility === "incompatible")
590
- .map((connector) => connector.provider),
591
- ...(Array.isArray(snapshot.compatibilityChecks)
592
- ? snapshot.compatibilityChecks
593
- .filter(isPublicCompatibilityCheckSnapshot)
594
- .filter((check) => check.tier === "incompatible")
595
- .flatMap((check) => check.surface === "claude" || check.surface === "codex"
596
- ? [check.surface]
597
- : [])
598
- : []),
599
- ]);
600
560
  const peers = validPeers
601
561
  .map((peer) => ({
602
562
  alias: boundedText(peer.alias),
603
563
  host: boundedText(peer.host),
604
564
  state: peer.state,
605
- compatibility: peer.compatibility,
606
565
  validated: peer.validated,
607
566
  selected: peer.selected,
608
567
  selectable: peerIsSelectable(peer),
@@ -654,7 +613,6 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
654
613
  host: boundedText(route.host),
655
614
  enabled: Boolean(route.enabled),
656
615
  state: route.state,
657
- compatibility: route.compatibility,
658
616
  queueDepth: depth,
659
617
  ...(oldestQueuedAt === undefined ? {} : { oldestQueuedAt }),
660
618
  ...(age === undefined ? {} : { queueAgeMs: age }),
@@ -671,33 +629,32 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
671
629
  .sort((left, right) => routePriority(left) - routePriority(right) ||
672
630
  compareText(left.alias, right.alias))
673
631
  .slice(0, DASHBOARD_MODEL_LIMITS.routes);
674
- const routeByAlias = new Map(allRoutes.map((route) => [route.alias, route]));
675
- const allPairs = snapshot.pairs
676
- .map((pair) => {
677
- const claudeAlias = boundedText(pair.claudeAlias);
678
- const codexAlias = boundedText(pair.codexAlias);
679
- const claudeRoute = routeByAlias.get(claudeAlias);
680
- const codexRoute = routeByAlias.get(codexAlias);
681
- const bothPresent = claudeRoute !== undefined && codexRoute !== undefined;
632
+ const routeByEndpoint = new Map(allRoutes.map((route) => [`${route.provider}\0${route.alias}`, route]));
633
+ const allConsentEdges = snapshot.consentEdges
634
+ .map((edge) => {
635
+ const endpoints = edge.endpoints.map((endpoint) => ({
636
+ alias: boundedText(endpoint.alias),
637
+ provider: endpoint.provider,
638
+ }));
639
+ const routes = endpoints.map((endpoint) => routeByEndpoint.get(`${endpoint.provider}\0${endpoint.alias}`));
640
+ const bothPresent = routes.every((route) => route !== undefined);
682
641
  return {
683
- claudeAlias,
684
- codexAlias,
685
- host: boundedText(pair.host),
642
+ endpoints,
643
+ host: boundedText(edge.host),
686
644
  state: bothPresent &&
687
- !quarantinedProviders.has("claude") &&
688
- !quarantinedProviders.has("codex") &&
689
- routeIsReady(claudeRoute) &&
690
- routeIsReady(codexRoute)
645
+ routes.every((route) => route !== undefined && routeIsReady(route))
691
646
  ? "ready"
692
647
  : bothPresent
693
648
  ? "degraded"
694
649
  : "unavailable",
695
- counters: normalizedCounters(pair.counters),
650
+ counters: normalizedCounters(edge.counters),
696
651
  };
697
652
  })
698
- .sort((left, right) => compareText(left.claudeAlias, right.claudeAlias) ||
699
- compareText(left.codexAlias, right.codexAlias));
700
- const pairs = allPairs.slice(0, DASHBOARD_MODEL_LIMITS.pairs);
653
+ .sort((left, right) => compareText(left.endpoints[0].provider, right.endpoints[0].provider) ||
654
+ compareText(left.endpoints[0].alias, right.endpoints[0].alias) ||
655
+ compareText(left.endpoints[1].provider, right.endpoints[1].provider) ||
656
+ compareText(left.endpoints[1].alias, right.endpoints[1].alias));
657
+ const consentEdges = allConsentEdges.slice(0, DASHBOARD_MODEL_LIMITS.consentEdges);
701
658
  const allWatches = (snapshot.progressWatches ?? [])
702
659
  .flatMap((watch) => {
703
660
  const lastActivityAt = normalizedTimestamp(watch.lastActivityAt);
@@ -763,7 +720,6 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
763
720
  provider: connector.provider,
764
721
  host: boundedText(connector.host),
765
722
  health: connector.health,
766
- compatibility: connector.compatibility,
767
723
  ...(safeProtocol(connector.protocol) === undefined
768
724
  ? {}
769
725
  : { protocol: safeProtocol(connector.protocol) }),
@@ -791,38 +747,6 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
791
747
  })
792
748
  .sort((left, right) => compareText(`${left.provider}\0${left.host}`, `${right.provider}\0${right.host}`))
793
749
  .slice(0, DASHBOARD_MODEL_LIMITS.connectors);
794
- const detectedCompatibilityChecks = (Array.isArray(snapshot.compatibilityChecks)
795
- ? snapshot.compatibilityChecks.filter(isPublicCompatibilityCheckSnapshot)
796
- : [])
797
- .map((attestation) => {
798
- const failed = attestation.probes.find((probe) => probe.outcome === "fail");
799
- const issue = safeCode(attestation.safeErrorCode);
800
- return {
801
- surface: attestation.surface,
802
- version: boundedText(attestation.version),
803
- ...(attestation.testedVersion === undefined
804
- ? {}
805
- : { testedVersion: boundedText(attestation.testedVersion) }),
806
- ...(attestation.supportedMajor === undefined
807
- ? {}
808
- : { supportedMajor: boundedText(attestation.supportedMajor) }),
809
- tier: attestation.tier,
810
- writesCovered: attestation.writesCovered,
811
- checkedAt: attestation.checkedAt,
812
- ...(failed === undefined ? {} : { failure: failed.name }),
813
- ...(issue === undefined ? {} : { safeErrorCode: issue }),
814
- };
815
- })
816
- .sort((left, right) => compareText(left.surface, right.surface));
817
- const detectedSurfaces = new Set(detectedCompatibilityChecks.map(({ surface }) => surface));
818
- const compatibilityChecks = [
819
- ...detectedCompatibilityChecks,
820
- ...surfaceDefinitions
821
- .filter(({ required, surface }) => !required && !detectedSurfaces.has(surface))
822
- .map(({ surface }) => ({ surface, notDetected: true })),
823
- ]
824
- .sort((left, right) => compareText(left.surface, right.surface))
825
- .slice(0, surfaceDefinitions.length);
826
750
  const messages = buildMessageGroups(snapshot.messages, includeBodies);
827
751
  const queuedMessages = allRoutes.reduce((total, route) => boundedAdd(total, route.queueDepth), 0);
828
752
  const queuedWithAge = allRoutes
@@ -830,69 +754,109 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
830
754
  .sort((left, right) => right.queueAgeMs - left.queueAgeMs);
831
755
  const oldest = queuedWithAge[0];
832
756
  const activeDeliveries = messages.activeGroups;
833
- const claudeRoutes = allRoutes.filter((route) => route.provider === "claude");
834
- const codexRoutes = allRoutes.filter((route) => route.provider === "codex");
835
- const monitorOnlyCodexRoutes = codexRoutes.filter((route) => route.safeErrorCode === "CODEX_WRITES_DISABLED");
836
- const routeIsExchangeReady = (route) => !quarantinedProviders.has(route.provider) && routeIsReady(route);
837
- const readyClaude = claudeRoutes.filter(routeIsExchangeReady).length;
838
- const readyCodex = codexRoutes.filter(routeIsExchangeReady).length;
839
- const readyPairs = allPairs.filter((pair) => pair.state === "ready");
840
- const pairedClaude = new Set(allPairs.map((pair) => pair.claudeAlias));
841
- const pairedCodex = new Set(allPairs.map((pair) => pair.codexAlias));
842
- const readyPairedClaude = claudeRoutes.some((route) => routeIsExchangeReady(route) && pairedClaude.has(route.alias));
843
- const readyPairedCodex = codexRoutes.some((route) => routeIsExchangeReady(route) && pairedCodex.has(route.alias));
844
- const pairCountIsLowerBound = (normalizedInteger(snapshot.truncation.pairs) ?? 0) > 0;
845
- const hasPairEvidence = allPairs.length > 0 || pairCountIsLowerBound;
757
+ const routesByProvider = Object.fromEntries(gatewayProviders.map((provider) => [
758
+ provider,
759
+ allRoutes.filter((route) => route.provider === provider),
760
+ ]));
761
+ const readyByProvider = Object.fromEntries(gatewayProviders.map((provider) => [
762
+ provider,
763
+ routesByProvider[provider].filter(routeIsReady).length,
764
+ ]));
765
+ const pairedAliasesByProvider = Object.fromEntries(gatewayProviders.map((provider) => [provider, new Set()]));
766
+ for (const edge of allConsentEdges) {
767
+ for (const endpoint of edge.endpoints) {
768
+ pairedAliasesByProvider[endpoint.provider].add(endpoint.alias);
769
+ }
770
+ }
771
+ const readyConsentEdges = allConsentEdges.filter((edge) => edge.state === "ready");
772
+ const consentEdgeCountIsLowerBound = (normalizedInteger(snapshot.truncation.consentEdges) ?? 0) > 0;
773
+ const hasConsentEvidence = allConsentEdges.length > 0 || consentEdgeCountIsLowerBound;
846
774
  const graph = {
847
- pairCount: allPairs.length,
848
- readyPairCount: readyPairs.length,
849
- pairCountIsLowerBound,
850
- unpairedReadyClaude: claudeRoutes.filter((route) => routeIsExchangeReady(route) && !pairedClaude.has(route.alias)).length,
851
- unpairedReadyCodex: codexRoutes.filter((route) => routeIsExchangeReady(route) && !pairedCodex.has(route.alias)).length,
775
+ consentEdgeCount: allConsentEdges.length,
776
+ readyConsentEdgeCount: readyConsentEdges.length,
777
+ consentEdgeCountIsLowerBound,
778
+ unpairedReadyByProvider: Object.fromEntries(gatewayProviders.map((provider) => [
779
+ provider,
780
+ routesByProvider[provider].filter((route) => routeIsReady(route) &&
781
+ !pairedAliasesByProvider[provider].has(route.alias)).length,
782
+ ])),
852
783
  };
853
784
  const selectedPeers = validPeers.filter((peer) => peer.selected && peerIsSelectable(peer));
854
785
  const selectablePeers = validPeers.filter(peerIsSelectable);
855
- const selectedClaudeCount = quarantinedProviders.has("claude")
856
- ? 0
857
- : Math.max(selectedPeers.length, readyClaude);
858
- const claudeStatus = partyStatus(claudeRoutes, readyClaude);
859
- const codexStatus = partyStatus(codexRoutes, readyCodex);
860
- const claudeNextAction = quarantinedProviders.has("claude")
861
- ? "review_compatibility"
862
- : selectedClaudeCount > 0 &&
863
- (readyPairedClaude || (pairCountIsLowerBound && readyClaude > 0))
864
- ? "none"
865
- : readyClaude > 0
866
- ? "pair_routes"
867
- : validPeers.length === 0
868
- ? "discover_claude"
869
- : selectablePeers.length > 0
870
- ? !hasPairEvidence
871
- ? "pair_routes"
872
- : "restore_claude"
873
- : selectedPeers.length > 0
874
- ? "restore_claude"
875
- : "repair_claude_inventory";
876
- const codexNextAction = quarantinedProviders.has("codex") || monitorOnlyCodexRoutes.length > 0
877
- ? "review_compatibility"
878
- : readyPairedCodex || (pairCountIsLowerBound && readyCodex > 0)
879
- ? "none"
880
- : readyCodex > 0
881
- ? "pair_routes"
882
- : codexRoutes.length === 0
883
- ? "register_codex"
884
- : "restore_codex";
786
+ const selectedClaudeCount = Math.max(selectedPeers.filter((peer) => peer.provider === "claude").length, readyByProvider.claude);
787
+ const readyPairedByProvider = Object.fromEntries(gatewayProviders.map((provider) => [
788
+ provider,
789
+ routesByProvider[provider].some((route) => routeIsReady(route) &&
790
+ pairedAliasesByProvider[provider].has(route.alias)),
791
+ ]));
792
+ const claudeNextAction = selectedClaudeCount > 0 &&
793
+ (readyPairedByProvider.claude ||
794
+ (consentEdgeCountIsLowerBound && readyByProvider.claude > 0))
795
+ ? "none"
796
+ : readyByProvider.claude > 0
797
+ ? "pair_routes"
798
+ : validPeers.length === 0
799
+ ? "discover_claude"
800
+ : selectablePeers.length > 0
801
+ ? !hasConsentEvidence
802
+ ? "pair_routes"
803
+ : "restore_claude"
804
+ : selectedPeers.length > 0
805
+ ? "restore_claude"
806
+ : "repair_claude_inventory";
807
+ const codexNextAction = readyPairedByProvider.codex ||
808
+ (consentEdgeCountIsLowerBound && readyByProvider.codex > 0)
809
+ ? "none"
810
+ : readyByProvider.codex > 0
811
+ ? "pair_routes"
812
+ : routesByProvider.codex.length === 0
813
+ ? "register_codex"
814
+ : "restore_codex";
815
+ const parties = gatewayProviders.map((provider) => {
816
+ const providerPeers = validPeers.filter((peer) => peer.provider === provider);
817
+ const providerRoutes = routesByProvider[provider];
818
+ const ready = provider === "claude"
819
+ ? selectedClaudeCount
820
+ : readyByProvider[provider];
821
+ const nextAction = provider === "claude"
822
+ ? claudeNextAction
823
+ : provider === "codex"
824
+ ? codexNextAction
825
+ : readyPairedByProvider[provider]
826
+ ? "none"
827
+ : ready > 0
828
+ ? "pair_routes"
829
+ : "none";
830
+ return {
831
+ kind: provider,
832
+ status: provider === "claude" && ready > 0 && partyStatus(providerRoutes, ready) === "missing"
833
+ ? "attention"
834
+ : partyStatus(providerRoutes, ready),
835
+ total: provider === "claude" ? providerPeers.length : providerRoutes.length,
836
+ ready,
837
+ ...(provider === "claude"
838
+ ? { selectable: selectablePeers.filter((peer) => peer.provider === provider).length }
839
+ : {}),
840
+ countIsLowerBound: (normalizedInteger(snapshot.truncation.routes) ?? 0) > 0 ||
841
+ (provider === "claude" &&
842
+ (normalizedInteger(snapshot.truncation.availablePeers) ?? 0) > 0),
843
+ ...(providerPeers.find((peer) => peer.selected)?.alias === undefined
844
+ ? providerRoutes[0]?.alias === undefined
845
+ ? {}
846
+ : { primaryAlias: boundedText(providerRoutes[0].alias) }
847
+ : { primaryAlias: boundedText(providerPeers.find((peer) => peer.selected).alias) }),
848
+ nextAction,
849
+ };
850
+ });
885
851
  const explicitAlerts = coalesceCodexReactivationAlerts(snapshot.alerts
886
- .filter((alert) => alert.code !== "COMPATIBILITY_CERTIFICATION_FAILED")
887
852
  .map((alert) => {
888
853
  const code = safeCode(alert.code);
889
- const providerWritesCovered = (alert.provider === "claude" || alert.provider === "codex") &&
890
- writesCoveredProviders.has(alert.provider);
891
- const route = code === "QUEUE_STALLED" && typeof alert.alias === "string"
892
- ? routeByAlias.get(alert.alias)
854
+ const route = code === "QUEUE_STALLED" && typeof alert.alias === "string" &&
855
+ alert.provider !== undefined
856
+ ? routeByEndpoint.get(`${alert.provider}\0${alert.alias}`)
893
857
  : undefined;
894
- const alertRoute = typeof alert.alias === "string"
895
- ? routeByAlias.get(alert.alias)
858
+ const alertRoute = typeof alert.alias === "string" && alert.provider !== undefined
859
+ ? routeByEndpoint.get(`${alert.provider}\0${alert.alias}`)
896
860
  : undefined;
897
861
  return {
898
862
  kind: "alert",
@@ -905,9 +869,7 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
905
869
  ...(normalizedTimestamp(alert.timestamp) === undefined
906
870
  ? {}
907
871
  : { timestamp: normalizedTimestamp(alert.timestamp) }),
908
- ...(alert.provider === "claude" || alert.provider === "codex"
909
- ? { provider: alert.provider }
910
- : {}),
872
+ ...(alert.provider === undefined ? {} : { provider: alert.provider }),
911
873
  ...(typeof alert.alias === "string"
912
874
  ? { alias: boundedText(alert.alias) }
913
875
  : {}),
@@ -926,29 +888,19 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
926
888
  guidance: alertRoute !== undefined &&
927
889
  needsCodexAppReconnect(alertRoute, connectors, generatedAt)
928
890
  ? "codex_app_reconnect_required"
929
- : !providerWritesCovered &&
930
- alert.provider !== undefined &&
931
- quarantinedProviders.has(alert.provider)
932
- ? "provider_incompatible"
933
- : providerWritesCovered &&
934
- guidanceFor(code, alert.provider) === "provider_incompatible"
935
- ? "degraded"
936
- : guidanceFor(code, alert.provider),
891
+ : guidanceFor(code, alert.provider),
937
892
  };
938
893
  }), allRoutes).sort((left, right) => alertPriority(left.severity) - alertPriority(right.severity) ||
939
894
  compareText(right.timestamp ?? "", left.timestamp ?? ""));
940
895
  const attentionCandidates = [...explicitAlerts];
941
896
  const unavailableConsentEdgeScopes = new Set();
942
- for (const pair of allPairs) {
943
- if (pair.state !== "unavailable")
897
+ for (const edge of allConsentEdges) {
898
+ if (edge.state !== "unavailable")
944
899
  continue;
945
- for (const endpoint of [
946
- { provider: "claude", alias: pair.claudeAlias },
947
- { provider: "codex", alias: pair.codexAlias },
948
- ]) {
949
- if (routeByAlias.has(endpoint.alias))
900
+ for (const endpoint of edge.endpoints) {
901
+ if (routeByEndpoint.has(`${endpoint.provider}\0${endpoint.alias}`))
950
902
  continue;
951
- const scope = `${endpoint.provider}\0${endpoint.alias}\0${pair.host}`;
903
+ const scope = `${endpoint.provider}\0${endpoint.alias}\0${edge.host}`;
952
904
  if (unavailableConsentEdgeScopes.has(scope))
953
905
  continue;
954
906
  unavailableConsentEdgeScopes.add(scope);
@@ -957,13 +909,13 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
957
909
  severity: "warning",
958
910
  provider: endpoint.provider,
959
911
  alias: endpoint.alias,
960
- host: pair.host,
912
+ host: edge.host,
961
913
  guidance: "consent_edge_unavailable",
962
914
  });
963
915
  }
964
916
  }
965
917
  for (const route of allRoutes) {
966
- const writes = messages.groups.filter(({ direction, targetAlias, state, timestamp }) => direction === "codex_to_claude" &&
918
+ const writes = messages.groups.filter(({ direction, targetAlias, state, timestamp }) => parseDirection(direction)?.targetProvider === "claude" &&
967
919
  targetAlias === route.alias && state === "delivered" &&
968
920
  timestamp !== undefined);
969
921
  // Groups arrive newest-first. The oldest unconsumed write owns the notice:
@@ -1008,9 +960,7 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1008
960
  }
1009
961
  const representedScopes = new Set(attentionCandidates.map((item) => `${item.provider ?? ""}\0${item.alias ?? ""}\0${item.host ?? ""}`));
1010
962
  for (const route of allRoutes) {
1011
- if (routeIsReady(route) ||
1012
- quarantinedProviders.has(route.provider) ||
1013
- route.safeErrorCode === "CODEX_WRITES_DISABLED") {
963
+ if (routeIsReady(route)) {
1014
964
  continue;
1015
965
  }
1016
966
  const scope = `${route.provider}\0${route.alias}\0${route.host}`;
@@ -1019,9 +969,7 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1019
969
  attentionCandidates.push({
1020
970
  kind: "route",
1021
971
  ...(route.safeErrorCode === undefined ? {} : { code: route.safeErrorCode }),
1022
- severity: route.state === "offline" || route.state === "incompatible"
1023
- ? "error"
1024
- : "warning",
972
+ severity: route.state === "offline" ? "error" : "warning",
1025
973
  ...(route.lastSeenAt === undefined ? {} : { timestamp: route.lastSeenAt }),
1026
974
  provider: route.provider,
1027
975
  alias: route.alias,
@@ -1039,10 +987,8 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1039
987
  representedScopes.add(scope);
1040
988
  }
1041
989
  for (const connector of connectors) {
1042
- if ((connector.health === "healthy" &&
1043
- connector.compatibility === "compatible")) {
990
+ if (connector.health === "healthy")
1044
991
  continue;
1045
- }
1046
992
  const scope = `${connector.provider}\0\0${connector.host}`;
1047
993
  if (representedScopes.has(scope))
1048
994
  continue;
@@ -1059,17 +1005,7 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1059
1005
  host: connector.host,
1060
1006
  guidance: connector.health === "offline"
1061
1007
  ? "connector_offline"
1062
- : !writesCoveredProviders.has(connector.provider) &&
1063
- (quarantinedProviders.has(connector.provider) ||
1064
- connector.safeErrorCode === "CLAUDE_PEER_VERSION_UNSUPPORTED" ||
1065
- connector.safeErrorCode === "CLAUDE_VERSION_UNPARSEABLE" ||
1066
- connector.safeErrorCode === "CLAUDE_VERSION_CHECK_FAILED" ||
1067
- connector.safeErrorCode === "CLAUDE_VERSION_EVIDENCE_CONFLICT" ||
1068
- connector.safeErrorCode === "CLAUDE_VERSION_EVIDENCE_TOO_LARGE" ||
1069
- connector.safeErrorCode === "CODEX_APP_SERVER_VERSION_UNSUPPORTED" ||
1070
- connector.safeErrorCode === "CODEX_APP_SERVER_VERSION_UNPARSEABLE")
1071
- ? "provider_incompatible"
1072
- : "degraded",
1008
+ : "degraded",
1073
1009
  });
1074
1010
  representedScopes.add(scope);
1075
1011
  }
@@ -1112,10 +1048,7 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1112
1048
  guidance: "registry_empty",
1113
1049
  });
1114
1050
  }
1115
- if ((snapshot.health === "degraded" ||
1116
- snapshot.health === "offline" ||
1117
- snapshot.health === "incompatible") &&
1118
- !(snapshot.health === "degraded" && quarantinedProviders.size > 0) &&
1051
+ if ((snapshot.health === "degraded" || snapshot.health === "offline") &&
1119
1052
  !attentionCandidates.some((item) => item.kind === "broker")) {
1120
1053
  attentionCandidates.push({
1121
1054
  kind: "broker",
@@ -1124,11 +1057,11 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1124
1057
  });
1125
1058
  }
1126
1059
  const attention = attentionCandidates.slice(0, DASHBOARD_MODEL_LIMITS.alerts);
1127
- const setupComplete = hasPairEvidence;
1128
- const hasDegradedPair = allPairs.some((pair) => pair.state !== "ready");
1060
+ const setupComplete = hasConsentEvidence;
1061
+ const hasDegradedConsentEdge = allConsentEdges.some((edge) => edge.state !== "ready");
1129
1062
  const overall = attentionCandidates.length > 0 ||
1130
1063
  (normalizedInteger(snapshot.truncation.alerts) ?? 0) > 0 ||
1131
- hasDegradedPair
1064
+ hasDegradedConsentEdge
1132
1065
  ? "attention"
1133
1066
  : setupComplete
1134
1067
  ? "ready"
@@ -1137,7 +1070,7 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1137
1070
  connectors: boundedAdd(normalizedInteger(snapshot.truncation.connectors) ?? 0, Math.max(0, snapshot.connectors.length - DASHBOARD_MODEL_LIMITS.connectors)),
1138
1071
  availablePeers: boundedAdd(normalizedInteger(snapshot.truncation.availablePeers) ?? 0, Math.max(0, validPeers.length - DASHBOARD_MODEL_LIMITS.availablePeers)),
1139
1072
  routes: boundedAdd(normalizedInteger(snapshot.truncation.routes) ?? 0, Math.max(0, allRoutes.length - DASHBOARD_MODEL_LIMITS.routes)),
1140
- pairs: boundedAdd(normalizedInteger(snapshot.truncation.pairs) ?? 0, Math.max(0, allPairs.length - DASHBOARD_MODEL_LIMITS.pairs)),
1073
+ consentEdges: boundedAdd(normalizedInteger(snapshot.truncation.consentEdges) ?? 0, Math.max(0, allConsentEdges.length - DASHBOARD_MODEL_LIMITS.consentEdges)),
1141
1074
  progressWatches: boundedAdd(normalizedInteger(snapshot.truncation.progressWatches) ?? 0, Math.max(0, allWatches.length - DASHBOARD_MODEL_LIMITS.progressWatches)),
1142
1075
  upstreamProgressWatchEvents: normalizedInteger(snapshot.truncation.progressWatchEvents) ?? 0,
1143
1076
  progressWatchEvents: Math.max(0, allWatchEvents.length - DASHBOARD_MODEL_LIMITS.progressWatchEvents),
@@ -1150,43 +1083,13 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1150
1083
  activityEvents: Math.max(0, allBrokerActivity.length - DASHBOARD_MODEL_LIMITS.activityEvents),
1151
1084
  };
1152
1085
  return {
1153
- schemaVersion: 1,
1086
+ schemaVersion: 2,
1154
1087
  ...(generatedAt === undefined ? {} : { generatedAt }),
1155
1088
  inboundMode,
1156
1089
  health: snapshot.health,
1157
1090
  overall,
1158
1091
  exchange: {
1159
- claude: {
1160
- kind: "claude",
1161
- status: selectedClaudeCount > 0 && claudeStatus === "missing"
1162
- ? "attention"
1163
- : claudeStatus,
1164
- total: validPeers.length,
1165
- ready: selectedClaudeCount,
1166
- selectable: selectablePeers.length,
1167
- countIsLowerBound: (normalizedInteger(snapshot.truncation.availablePeers) ?? 0) > 0 ||
1168
- (normalizedInteger(snapshot.truncation.routes) ?? 0) > 0,
1169
- ...(selectedPeers[0]?.alias === undefined
1170
- ? claudeRoutes[0]?.alias === undefined
1171
- ? {}
1172
- : { primaryAlias: boundedText(claudeRoutes[0].alias) }
1173
- : { primaryAlias: boundedText(selectedPeers[0].alias) }),
1174
- nextAction: claudeNextAction,
1175
- },
1176
- codex: {
1177
- kind: "codex",
1178
- status: codexStatus,
1179
- total: codexRoutes.length,
1180
- ready: readyCodex,
1181
- ...(monitorOnlyCodexRoutes.length === 0
1182
- ? {}
1183
- : { monitorOnly: monitorOnlyCodexRoutes.length }),
1184
- countIsLowerBound: (normalizedInteger(snapshot.truncation.routes) ?? 0) > 0,
1185
- ...(codexRoutes[0]?.alias === undefined
1186
- ? {}
1187
- : { primaryAlias: codexRoutes[0].alias }),
1188
- nextAction: codexNextAction,
1189
- },
1092
+ parties,
1190
1093
  queuedMessages,
1191
1094
  queueCountIsLowerBound: (normalizedInteger(snapshot.truncation.routes) ?? 0) > 0,
1192
1095
  ...(oldest === undefined
@@ -1213,12 +1116,11 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1213
1116
  brokerActivity,
1214
1117
  peers,
1215
1118
  routes,
1216
- pairs,
1119
+ consentEdges,
1217
1120
  watches,
1218
1121
  watchEvents,
1219
1122
  graph,
1220
1123
  connectors,
1221
- compatibilityChecks,
1222
1124
  accounting: {
1223
1125
  accepted: normalizedInteger(snapshot.accounting.accepted) ?? 0,
1224
1126
  duplicates: normalizedInteger(snapshot.accounting.duplicates) ?? 0,