agent-embassy 1.6.1 → 1.7.1

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 (105) hide show
  1. package/CHANGELOG.md +33 -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.d.ts +1 -1
  26. package/dist/src/gateway/cli-copy.en.d.ts +3 -1
  27. package/dist/src/gateway/cli-copy.en.js +5 -2
  28. package/dist/src/gateway/cli-copy.en.js.map +1 -1
  29. package/dist/src/gateway/cli-copy.js +2 -0
  30. package/dist/src/gateway/cli-copy.js.map +1 -1
  31. package/dist/src/gateway/cli-copy.zh-CN.d.ts +3 -1
  32. package/dist/src/gateway/cli-copy.zh-CN.js +5 -2
  33. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
  34. package/dist/src/gateway/cli.d.ts +2 -2
  35. package/dist/src/gateway/cli.js +73 -6
  36. package/dist/src/gateway/cli.js.map +1 -1
  37. package/dist/src/gateway/codex-app-server.d.ts +5 -74
  38. package/dist/src/gateway/codex-app-server.js +9 -599
  39. package/dist/src/gateway/codex-app-server.js.map +1 -1
  40. package/dist/src/gateway/codex-doctor.d.ts +33 -0
  41. package/dist/src/gateway/codex-doctor.js +148 -0
  42. package/dist/src/gateway/codex-doctor.js.map +1 -0
  43. package/dist/src/gateway/codex-local-transport.d.ts +13 -26
  44. package/dist/src/gateway/codex-local-transport.js +31 -76
  45. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  46. package/dist/src/gateway/compatibility.d.ts +6 -66
  47. package/dist/src/gateway/compatibility.js +14 -296
  48. package/dist/src/gateway/compatibility.js.map +1 -1
  49. package/dist/src/gateway/config.d.ts +9 -0
  50. package/dist/src/gateway/config.js +6 -2
  51. package/dist/src/gateway/config.js.map +1 -1
  52. package/dist/src/gateway/control.d.ts +16 -1
  53. package/dist/src/gateway/control.js +128 -59
  54. package/dist/src/gateway/control.js.map +1 -1
  55. package/dist/src/gateway/dashboard-copy.d.ts +1 -1
  56. package/dist/src/gateway/dashboard-copy.en.d.ts +19 -44
  57. package/dist/src/gateway/dashboard-copy.en.js +42 -67
  58. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  59. package/dist/src/gateway/dashboard-copy.js +19 -44
  60. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  61. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +19 -44
  62. package/dist/src/gateway/dashboard-copy.zh-CN.js +42 -67
  63. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  64. package/dist/src/gateway/dashboard-model.d.ts +41 -76
  65. package/dist/src/gateway/dashboard-model.js +167 -257
  66. package/dist/src/gateway/dashboard-model.js.map +1 -1
  67. package/dist/src/gateway/dashboard.d.ts +0 -3
  68. package/dist/src/gateway/dashboard.js +77 -76
  69. package/dist/src/gateway/dashboard.js.map +1 -1
  70. package/dist/src/gateway/deepseek-detect.d.ts +7 -6
  71. package/dist/src/gateway/deepseek-detect.js +31 -115
  72. package/dist/src/gateway/deepseek-detect.js.map +1 -1
  73. package/dist/src/gateway/live-dashboard-app/app.js +175 -621
  74. package/dist/src/gateway/live-dashboard-command.js +2 -4
  75. package/dist/src/gateway/live-dashboard-command.js.map +1 -1
  76. package/dist/src/gateway/live-dashboard-http.d.ts +1 -2
  77. package/dist/src/gateway/live-dashboard-http.js +14 -15
  78. package/dist/src/gateway/live-dashboard-http.js.map +1 -1
  79. package/dist/src/gateway/progress-watch-machine.d.ts +1 -4
  80. package/dist/src/gateway/progress-watch-machine.js.map +1 -1
  81. package/dist/src/gateway/provenance-envelope.d.ts +3 -2
  82. package/dist/src/gateway/provenance-envelope.js +44 -11
  83. package/dist/src/gateway/provenance-envelope.js.map +1 -1
  84. package/dist/src/gateway/providers.d.ts +19 -85
  85. package/dist/src/gateway/providers.js +132 -835
  86. package/dist/src/gateway/providers.js.map +1 -1
  87. package/dist/src/gateway/server.d.ts +8 -9
  88. package/dist/src/gateway/server.js +63 -99
  89. package/dist/src/gateway/server.js.map +1 -1
  90. package/dist/src/gateway/service.d.ts +48 -51
  91. package/dist/src/gateway/service.js +748 -540
  92. package/dist/src/gateway/service.js.map +1 -1
  93. package/dist/src/gateway/store.d.ts +20 -29
  94. package/dist/src/gateway/store.js +306 -838
  95. package/dist/src/gateway/store.js.map +1 -1
  96. package/dist/src/gateway/types.d.ts +63 -54
  97. package/dist/src/gateway/types.js +51 -91
  98. package/dist/src/gateway/types.js.map +1 -1
  99. package/docs/CONFIGURATION.md +11 -10
  100. package/docs/CONFIGURATION.zh-CN.md +10 -9
  101. package/docs/DASHBOARD.md +4 -38
  102. package/docs/DASHBOARD.zh-CN.md +4 -2
  103. package/docs/GATEWAY-ARCHITECTURE.md +61 -119
  104. package/package.json +1 -1
  105. 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;
@@ -358,7 +344,8 @@ const CODEX_APP_RECONNECT_GUIDANCE_AFTER_MS = 2_000;
358
344
  function needsCodexAppReconnect(route, connectors, generatedAt) {
359
345
  if (route.provider !== "codex" ||
360
346
  route.state !== "stale" ||
361
- route.safeErrorCode !== "CODEX_ROUTE_STALE" ||
347
+ (route.safeErrorCode !== "CODEX_ROUTE_STALE" &&
348
+ route.safeErrorCode !== "ENDPOINT_GENERATION_CHANGED") ||
362
349
  route.lastSeenAt === undefined ||
363
350
  generatedAt === undefined ||
364
351
  Date.parse(generatedAt) - Date.parse(route.lastSeenAt) <
@@ -367,8 +354,7 @@ function needsCodexAppReconnect(route, connectors, generatedAt) {
367
354
  }
368
355
  return connectors.some((connector) => connector.provider === "codex" &&
369
356
  connector.host === route.host &&
370
- connector.health === "healthy" &&
371
- connector.compatibility === "compatible");
357
+ connector.health === "healthy");
372
358
  }
373
359
  function alertPriority(severity) {
374
360
  return severity === "error" ? 0 : severity === "warning" ? 1 : 2;
@@ -405,14 +391,6 @@ function guidanceFor(code, provider) {
405
391
  case "ADAPTER_DEGRADED":
406
392
  case "ROUTE_DEGRADED":
407
393
  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
394
  default:
417
395
  return "generic";
418
396
  }
@@ -574,35 +552,17 @@ function buildMessageGroups(messages, includeBodies) {
574
552
  activeGroups: all.filter((message) => ["queued", "dispatching", "transport_written", "held"].includes(message.state)).length,
575
553
  };
576
554
  }
577
- function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
578
- const surfaceDefinitions = options.compatibilitySurfaceDefinitions ?? compatibilitySurfaceDefinitions;
555
+ function buildProjectedDashboardViewModel(snapshot, includeBodies) {
579
556
  const generatedAt = normalizedTimestamp(snapshot.generatedAt);
580
557
  const inboundMode = snapshot.inboundMode === "open" ? "open" : "paired";
581
558
  const validPeers = arePublicAvailablePeerSnapshots(snapshot.availablePeers)
582
559
  ? snapshot.availablePeers
583
560
  : [];
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
561
  const peers = validPeers
601
562
  .map((peer) => ({
602
563
  alias: boundedText(peer.alias),
603
564
  host: boundedText(peer.host),
604
565
  state: peer.state,
605
- compatibility: peer.compatibility,
606
566
  validated: peer.validated,
607
567
  selected: peer.selected,
608
568
  selectable: peerIsSelectable(peer),
@@ -654,7 +614,6 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
654
614
  host: boundedText(route.host),
655
615
  enabled: Boolean(route.enabled),
656
616
  state: route.state,
657
- compatibility: route.compatibility,
658
617
  queueDepth: depth,
659
618
  ...(oldestQueuedAt === undefined ? {} : { oldestQueuedAt }),
660
619
  ...(age === undefined ? {} : { queueAgeMs: age }),
@@ -671,33 +630,32 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
671
630
  .sort((left, right) => routePriority(left) - routePriority(right) ||
672
631
  compareText(left.alias, right.alias))
673
632
  .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;
633
+ const routeByEndpoint = new Map(allRoutes.map((route) => [`${route.provider}\0${route.alias}`, route]));
634
+ const allConsentEdges = snapshot.consentEdges
635
+ .map((edge) => {
636
+ const endpoints = edge.endpoints.map((endpoint) => ({
637
+ alias: boundedText(endpoint.alias),
638
+ provider: endpoint.provider,
639
+ }));
640
+ const routes = endpoints.map((endpoint) => routeByEndpoint.get(`${endpoint.provider}\0${endpoint.alias}`));
641
+ const bothPresent = routes.every((route) => route !== undefined);
682
642
  return {
683
- claudeAlias,
684
- codexAlias,
685
- host: boundedText(pair.host),
643
+ endpoints,
644
+ host: boundedText(edge.host),
686
645
  state: bothPresent &&
687
- !quarantinedProviders.has("claude") &&
688
- !quarantinedProviders.has("codex") &&
689
- routeIsReady(claudeRoute) &&
690
- routeIsReady(codexRoute)
646
+ routes.every((route) => route !== undefined && routeIsReady(route))
691
647
  ? "ready"
692
648
  : bothPresent
693
649
  ? "degraded"
694
650
  : "unavailable",
695
- counters: normalizedCounters(pair.counters),
651
+ counters: normalizedCounters(edge.counters),
696
652
  };
697
653
  })
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);
654
+ .sort((left, right) => compareText(left.endpoints[0].provider, right.endpoints[0].provider) ||
655
+ compareText(left.endpoints[0].alias, right.endpoints[0].alias) ||
656
+ compareText(left.endpoints[1].provider, right.endpoints[1].provider) ||
657
+ compareText(left.endpoints[1].alias, right.endpoints[1].alias));
658
+ const consentEdges = allConsentEdges.slice(0, DASHBOARD_MODEL_LIMITS.consentEdges);
701
659
  const allWatches = (snapshot.progressWatches ?? [])
702
660
  .flatMap((watch) => {
703
661
  const lastActivityAt = normalizedTimestamp(watch.lastActivityAt);
@@ -763,7 +721,6 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
763
721
  provider: connector.provider,
764
722
  host: boundedText(connector.host),
765
723
  health: connector.health,
766
- compatibility: connector.compatibility,
767
724
  ...(safeProtocol(connector.protocol) === undefined
768
725
  ? {}
769
726
  : { protocol: safeProtocol(connector.protocol) }),
@@ -773,6 +730,13 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
773
730
  ...(normalizedTimestamp(connector.lastSeenAt) === undefined
774
731
  ? {}
775
732
  : { lastSeenAt: normalizedTimestamp(connector.lastSeenAt) }),
733
+ ...(connector.codexDoctor === undefined
734
+ ? {}
735
+ : {
736
+ codexDoctor: {
737
+ conditions: [...connector.codexDoctor.conditions],
738
+ },
739
+ }),
776
740
  ...(safeCode(connector.safeErrorCode) === undefined
777
741
  ? {}
778
742
  : { safeErrorCode: safeCode(connector.safeErrorCode) }),
@@ -791,38 +755,6 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
791
755
  })
792
756
  .sort((left, right) => compareText(`${left.provider}\0${left.host}`, `${right.provider}\0${right.host}`))
793
757
  .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
758
  const messages = buildMessageGroups(snapshot.messages, includeBodies);
827
759
  const queuedMessages = allRoutes.reduce((total, route) => boundedAdd(total, route.queueDepth), 0);
828
760
  const queuedWithAge = allRoutes
@@ -830,69 +762,109 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
830
762
  .sort((left, right) => right.queueAgeMs - left.queueAgeMs);
831
763
  const oldest = queuedWithAge[0];
832
764
  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;
765
+ const routesByProvider = Object.fromEntries(gatewayProviders.map((provider) => [
766
+ provider,
767
+ allRoutes.filter((route) => route.provider === provider),
768
+ ]));
769
+ const readyByProvider = Object.fromEntries(gatewayProviders.map((provider) => [
770
+ provider,
771
+ routesByProvider[provider].filter(routeIsReady).length,
772
+ ]));
773
+ const pairedAliasesByProvider = Object.fromEntries(gatewayProviders.map((provider) => [provider, new Set()]));
774
+ for (const edge of allConsentEdges) {
775
+ for (const endpoint of edge.endpoints) {
776
+ pairedAliasesByProvider[endpoint.provider].add(endpoint.alias);
777
+ }
778
+ }
779
+ const readyConsentEdges = allConsentEdges.filter((edge) => edge.state === "ready");
780
+ const consentEdgeCountIsLowerBound = (normalizedInteger(snapshot.truncation.consentEdges) ?? 0) > 0;
781
+ const hasConsentEvidence = allConsentEdges.length > 0 || consentEdgeCountIsLowerBound;
846
782
  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,
783
+ consentEdgeCount: allConsentEdges.length,
784
+ readyConsentEdgeCount: readyConsentEdges.length,
785
+ consentEdgeCountIsLowerBound,
786
+ unpairedReadyByProvider: Object.fromEntries(gatewayProviders.map((provider) => [
787
+ provider,
788
+ routesByProvider[provider].filter((route) => routeIsReady(route) &&
789
+ !pairedAliasesByProvider[provider].has(route.alias)).length,
790
+ ])),
852
791
  };
853
792
  const selectedPeers = validPeers.filter((peer) => peer.selected && peerIsSelectable(peer));
854
793
  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";
794
+ const selectedClaudeCount = Math.max(selectedPeers.filter((peer) => peer.provider === "claude").length, readyByProvider.claude);
795
+ const readyPairedByProvider = Object.fromEntries(gatewayProviders.map((provider) => [
796
+ provider,
797
+ routesByProvider[provider].some((route) => routeIsReady(route) &&
798
+ pairedAliasesByProvider[provider].has(route.alias)),
799
+ ]));
800
+ const claudeNextAction = selectedClaudeCount > 0 &&
801
+ (readyPairedByProvider.claude ||
802
+ (consentEdgeCountIsLowerBound && readyByProvider.claude > 0))
803
+ ? "none"
804
+ : readyByProvider.claude > 0
805
+ ? "pair_routes"
806
+ : validPeers.length === 0
807
+ ? "discover_claude"
808
+ : selectablePeers.length > 0
809
+ ? !hasConsentEvidence
810
+ ? "pair_routes"
811
+ : "restore_claude"
812
+ : selectedPeers.length > 0
813
+ ? "restore_claude"
814
+ : "repair_claude_inventory";
815
+ const codexNextAction = readyPairedByProvider.codex ||
816
+ (consentEdgeCountIsLowerBound && readyByProvider.codex > 0)
817
+ ? "none"
818
+ : readyByProvider.codex > 0
819
+ ? "pair_routes"
820
+ : routesByProvider.codex.length === 0
821
+ ? "register_codex"
822
+ : "restore_codex";
823
+ const parties = gatewayProviders.map((provider) => {
824
+ const providerPeers = validPeers.filter((peer) => peer.provider === provider);
825
+ const providerRoutes = routesByProvider[provider];
826
+ const ready = provider === "claude"
827
+ ? selectedClaudeCount
828
+ : readyByProvider[provider];
829
+ const nextAction = provider === "claude"
830
+ ? claudeNextAction
831
+ : provider === "codex"
832
+ ? codexNextAction
833
+ : readyPairedByProvider[provider]
834
+ ? "none"
835
+ : ready > 0
836
+ ? "pair_routes"
837
+ : "none";
838
+ return {
839
+ kind: provider,
840
+ status: provider === "claude" && ready > 0 && partyStatus(providerRoutes, ready) === "missing"
841
+ ? "attention"
842
+ : partyStatus(providerRoutes, ready),
843
+ total: provider === "claude" ? providerPeers.length : providerRoutes.length,
844
+ ready,
845
+ ...(provider === "claude"
846
+ ? { selectable: selectablePeers.filter((peer) => peer.provider === provider).length }
847
+ : {}),
848
+ countIsLowerBound: (normalizedInteger(snapshot.truncation.routes) ?? 0) > 0 ||
849
+ (provider === "claude" &&
850
+ (normalizedInteger(snapshot.truncation.availablePeers) ?? 0) > 0),
851
+ ...(providerPeers.find((peer) => peer.selected)?.alias === undefined
852
+ ? providerRoutes[0]?.alias === undefined
853
+ ? {}
854
+ : { primaryAlias: boundedText(providerRoutes[0].alias) }
855
+ : { primaryAlias: boundedText(providerPeers.find((peer) => peer.selected).alias) }),
856
+ nextAction,
857
+ };
858
+ });
885
859
  const explicitAlerts = coalesceCodexReactivationAlerts(snapshot.alerts
886
- .filter((alert) => alert.code !== "COMPATIBILITY_CERTIFICATION_FAILED")
887
860
  .map((alert) => {
888
861
  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)
862
+ const route = code === "QUEUE_STALLED" && typeof alert.alias === "string" &&
863
+ alert.provider !== undefined
864
+ ? routeByEndpoint.get(`${alert.provider}\0${alert.alias}`)
893
865
  : undefined;
894
- const alertRoute = typeof alert.alias === "string"
895
- ? routeByAlias.get(alert.alias)
866
+ const alertRoute = typeof alert.alias === "string" && alert.provider !== undefined
867
+ ? routeByEndpoint.get(`${alert.provider}\0${alert.alias}`)
896
868
  : undefined;
897
869
  return {
898
870
  kind: "alert",
@@ -905,9 +877,7 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
905
877
  ...(normalizedTimestamp(alert.timestamp) === undefined
906
878
  ? {}
907
879
  : { timestamp: normalizedTimestamp(alert.timestamp) }),
908
- ...(alert.provider === "claude" || alert.provider === "codex"
909
- ? { provider: alert.provider }
910
- : {}),
880
+ ...(alert.provider === undefined ? {} : { provider: alert.provider }),
911
881
  ...(typeof alert.alias === "string"
912
882
  ? { alias: boundedText(alert.alias) }
913
883
  : {}),
@@ -926,29 +896,19 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
926
896
  guidance: alertRoute !== undefined &&
927
897
  needsCodexAppReconnect(alertRoute, connectors, generatedAt)
928
898
  ? "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),
899
+ : guidanceFor(code, alert.provider),
937
900
  };
938
901
  }), allRoutes).sort((left, right) => alertPriority(left.severity) - alertPriority(right.severity) ||
939
902
  compareText(right.timestamp ?? "", left.timestamp ?? ""));
940
903
  const attentionCandidates = [...explicitAlerts];
941
904
  const unavailableConsentEdgeScopes = new Set();
942
- for (const pair of allPairs) {
943
- if (pair.state !== "unavailable")
905
+ for (const edge of allConsentEdges) {
906
+ if (edge.state !== "unavailable")
944
907
  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))
908
+ for (const endpoint of edge.endpoints) {
909
+ if (routeByEndpoint.has(`${endpoint.provider}\0${endpoint.alias}`))
950
910
  continue;
951
- const scope = `${endpoint.provider}\0${endpoint.alias}\0${pair.host}`;
911
+ const scope = `${endpoint.provider}\0${endpoint.alias}\0${edge.host}`;
952
912
  if (unavailableConsentEdgeScopes.has(scope))
953
913
  continue;
954
914
  unavailableConsentEdgeScopes.add(scope);
@@ -957,13 +917,13 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
957
917
  severity: "warning",
958
918
  provider: endpoint.provider,
959
919
  alias: endpoint.alias,
960
- host: pair.host,
920
+ host: edge.host,
961
921
  guidance: "consent_edge_unavailable",
962
922
  });
963
923
  }
964
924
  }
965
925
  for (const route of allRoutes) {
966
- const writes = messages.groups.filter(({ direction, targetAlias, state, timestamp }) => direction === "codex_to_claude" &&
926
+ const writes = messages.groups.filter(({ direction, targetAlias, state, timestamp }) => parseDirection(direction)?.targetProvider === "claude" &&
967
927
  targetAlias === route.alias && state === "delivered" &&
968
928
  timestamp !== undefined);
969
929
  // Groups arrive newest-first. The oldest unconsumed write owns the notice:
@@ -1008,9 +968,7 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1008
968
  }
1009
969
  const representedScopes = new Set(attentionCandidates.map((item) => `${item.provider ?? ""}\0${item.alias ?? ""}\0${item.host ?? ""}`));
1010
970
  for (const route of allRoutes) {
1011
- if (routeIsReady(route) ||
1012
- quarantinedProviders.has(route.provider) ||
1013
- route.safeErrorCode === "CODEX_WRITES_DISABLED") {
971
+ if (routeIsReady(route)) {
1014
972
  continue;
1015
973
  }
1016
974
  const scope = `${route.provider}\0${route.alias}\0${route.host}`;
@@ -1019,9 +977,7 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1019
977
  attentionCandidates.push({
1020
978
  kind: "route",
1021
979
  ...(route.safeErrorCode === undefined ? {} : { code: route.safeErrorCode }),
1022
- severity: route.state === "offline" || route.state === "incompatible"
1023
- ? "error"
1024
- : "warning",
980
+ severity: route.state === "offline" ? "error" : "warning",
1025
981
  ...(route.lastSeenAt === undefined ? {} : { timestamp: route.lastSeenAt }),
1026
982
  provider: route.provider,
1027
983
  alias: route.alias,
@@ -1039,10 +995,8 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1039
995
  representedScopes.add(scope);
1040
996
  }
1041
997
  for (const connector of connectors) {
1042
- if ((connector.health === "healthy" &&
1043
- connector.compatibility === "compatible")) {
998
+ if (connector.health === "healthy")
1044
999
  continue;
1045
- }
1046
1000
  const scope = `${connector.provider}\0\0${connector.host}`;
1047
1001
  if (representedScopes.has(scope))
1048
1002
  continue;
@@ -1059,17 +1013,7 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1059
1013
  host: connector.host,
1060
1014
  guidance: connector.health === "offline"
1061
1015
  ? "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",
1016
+ : "degraded",
1073
1017
  });
1074
1018
  representedScopes.add(scope);
1075
1019
  }
@@ -1112,10 +1056,7 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1112
1056
  guidance: "registry_empty",
1113
1057
  });
1114
1058
  }
1115
- if ((snapshot.health === "degraded" ||
1116
- snapshot.health === "offline" ||
1117
- snapshot.health === "incompatible") &&
1118
- !(snapshot.health === "degraded" && quarantinedProviders.size > 0) &&
1059
+ if ((snapshot.health === "degraded" || snapshot.health === "offline") &&
1119
1060
  !attentionCandidates.some((item) => item.kind === "broker")) {
1120
1061
  attentionCandidates.push({
1121
1062
  kind: "broker",
@@ -1124,11 +1065,11 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1124
1065
  });
1125
1066
  }
1126
1067
  const attention = attentionCandidates.slice(0, DASHBOARD_MODEL_LIMITS.alerts);
1127
- const setupComplete = hasPairEvidence;
1128
- const hasDegradedPair = allPairs.some((pair) => pair.state !== "ready");
1068
+ const setupComplete = hasConsentEvidence;
1069
+ const hasDegradedConsentEdge = allConsentEdges.some((edge) => edge.state !== "ready");
1129
1070
  const overall = attentionCandidates.length > 0 ||
1130
1071
  (normalizedInteger(snapshot.truncation.alerts) ?? 0) > 0 ||
1131
- hasDegradedPair
1072
+ hasDegradedConsentEdge
1132
1073
  ? "attention"
1133
1074
  : setupComplete
1134
1075
  ? "ready"
@@ -1137,7 +1078,7 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1137
1078
  connectors: boundedAdd(normalizedInteger(snapshot.truncation.connectors) ?? 0, Math.max(0, snapshot.connectors.length - DASHBOARD_MODEL_LIMITS.connectors)),
1138
1079
  availablePeers: boundedAdd(normalizedInteger(snapshot.truncation.availablePeers) ?? 0, Math.max(0, validPeers.length - DASHBOARD_MODEL_LIMITS.availablePeers)),
1139
1080
  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)),
1081
+ consentEdges: boundedAdd(normalizedInteger(snapshot.truncation.consentEdges) ?? 0, Math.max(0, allConsentEdges.length - DASHBOARD_MODEL_LIMITS.consentEdges)),
1141
1082
  progressWatches: boundedAdd(normalizedInteger(snapshot.truncation.progressWatches) ?? 0, Math.max(0, allWatches.length - DASHBOARD_MODEL_LIMITS.progressWatches)),
1142
1083
  upstreamProgressWatchEvents: normalizedInteger(snapshot.truncation.progressWatchEvents) ?? 0,
1143
1084
  progressWatchEvents: Math.max(0, allWatchEvents.length - DASHBOARD_MODEL_LIMITS.progressWatchEvents),
@@ -1150,43 +1091,13 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1150
1091
  activityEvents: Math.max(0, allBrokerActivity.length - DASHBOARD_MODEL_LIMITS.activityEvents),
1151
1092
  };
1152
1093
  return {
1153
- schemaVersion: 1,
1094
+ schemaVersion: 2,
1154
1095
  ...(generatedAt === undefined ? {} : { generatedAt }),
1155
1096
  inboundMode,
1156
1097
  health: snapshot.health,
1157
1098
  overall,
1158
1099
  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
- },
1100
+ parties,
1190
1101
  queuedMessages,
1191
1102
  queueCountIsLowerBound: (normalizedInteger(snapshot.truncation.routes) ?? 0) > 0,
1192
1103
  ...(oldest === undefined
@@ -1213,12 +1124,11 @@ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
1213
1124
  brokerActivity,
1214
1125
  peers,
1215
1126
  routes,
1216
- pairs,
1127
+ consentEdges,
1217
1128
  watches,
1218
1129
  watchEvents,
1219
1130
  graph,
1220
1131
  connectors,
1221
- compatibilityChecks,
1222
1132
  accounting: {
1223
1133
  accepted: normalizedInteger(snapshot.accounting.accepted) ?? 0,
1224
1134
  duplicates: normalizedInteger(snapshot.accounting.duplicates) ?? 0,