agent-embassy 1.4.0 → 1.5.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 (82) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/CONTRIBUTING.md +61 -6
  3. package/README.md +6 -5
  4. package/README.zh-CN.md +4 -4
  5. package/SECURITY.md +128 -24
  6. package/dist/src/gateway/claude-peer.d.ts +8 -5
  7. package/dist/src/gateway/claude-peer.js +81 -36
  8. package/dist/src/gateway/claude-peer.js.map +1 -1
  9. package/dist/src/gateway/claude-runtime.d.ts +4 -0
  10. package/dist/src/gateway/claude-runtime.js +50 -39
  11. package/dist/src/gateway/claude-runtime.js.map +1 -1
  12. package/dist/src/gateway/cli-copy.d.ts +1 -1
  13. package/dist/src/gateway/cli-copy.en.d.ts +1 -1
  14. package/dist/src/gateway/cli-copy.en.js +1 -1
  15. package/dist/src/gateway/cli-copy.en.js.map +1 -1
  16. package/dist/src/gateway/cli-copy.js +1 -1
  17. package/dist/src/gateway/cli-copy.js.map +1 -1
  18. package/dist/src/gateway/cli-copy.zh-CN.d.ts +1 -1
  19. package/dist/src/gateway/cli-copy.zh-CN.js +1 -1
  20. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
  21. package/dist/src/gateway/cli.d.ts +1 -1
  22. package/dist/src/gateway/cli.js +3 -7
  23. package/dist/src/gateway/cli.js.map +1 -1
  24. package/dist/src/gateway/codex-app-server.d.ts +2 -1
  25. package/dist/src/gateway/codex-app-server.js +1 -1
  26. package/dist/src/gateway/codex-app-server.js.map +1 -1
  27. package/dist/src/gateway/codex-local-transport.d.ts +10 -6
  28. package/dist/src/gateway/codex-local-transport.js +101 -56
  29. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  30. package/dist/src/gateway/compatibility.d.ts +11 -0
  31. package/dist/src/gateway/compatibility.js +104 -22
  32. package/dist/src/gateway/compatibility.js.map +1 -1
  33. package/dist/src/gateway/control.js +28 -30
  34. package/dist/src/gateway/control.js.map +1 -1
  35. package/dist/src/gateway/dashboard-copy.d.ts +1 -1
  36. package/dist/src/gateway/dashboard-copy.en.d.ts +36 -17
  37. package/dist/src/gateway/dashboard-copy.en.js +50 -31
  38. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  39. package/dist/src/gateway/dashboard-copy.js +36 -17
  40. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  41. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +36 -17
  42. package/dist/src/gateway/dashboard-copy.zh-CN.js +50 -31
  43. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  44. package/dist/src/gateway/dashboard-model.d.ts +176 -8
  45. package/dist/src/gateway/dashboard-model.js +420 -70
  46. package/dist/src/gateway/dashboard-model.js.map +1 -1
  47. package/dist/src/gateway/dashboard.js +48 -135
  48. package/dist/src/gateway/dashboard.js.map +1 -1
  49. package/dist/src/gateway/live-dashboard-app/app.js +148 -247
  50. package/dist/src/gateway/live-dashboard-assets.js +2 -1
  51. package/dist/src/gateway/live-dashboard-assets.js.map +1 -1
  52. package/dist/src/gateway/live-dashboard-stream.d.ts +2 -2
  53. package/dist/src/gateway/live-dashboard-stream.js +2 -2
  54. package/dist/src/gateway/live-dashboard-stream.js.map +1 -1
  55. package/dist/src/gateway/progress-watch-machine.d.ts +50 -64
  56. package/dist/src/gateway/progress-watch-machine.js +42 -133
  57. package/dist/src/gateway/progress-watch-machine.js.map +1 -1
  58. package/dist/src/gateway/provenance-envelope.d.ts +2 -0
  59. package/dist/src/gateway/provenance-envelope.js +13 -3
  60. package/dist/src/gateway/provenance-envelope.js.map +1 -1
  61. package/dist/src/gateway/providers.d.ts +69 -15
  62. package/dist/src/gateway/providers.js +456 -109
  63. package/dist/src/gateway/providers.js.map +1 -1
  64. package/dist/src/gateway/server.d.ts +8 -5
  65. package/dist/src/gateway/server.js +94 -13
  66. package/dist/src/gateway/server.js.map +1 -1
  67. package/dist/src/gateway/service.d.ts +25 -1
  68. package/dist/src/gateway/service.js +252 -69
  69. package/dist/src/gateway/service.js.map +1 -1
  70. package/dist/src/gateway/store.d.ts +23 -28
  71. package/dist/src/gateway/store.js +479 -467
  72. package/dist/src/gateway/store.js.map +1 -1
  73. package/dist/src/gateway/types.d.ts +38 -11
  74. package/dist/src/gateway/types.js +141 -3
  75. package/dist/src/gateway/types.js.map +1 -1
  76. package/docs/CONFIGURATION.md +8 -8
  77. package/docs/CONFIGURATION.zh-CN.md +8 -8
  78. package/docs/DASHBOARD.md +29 -6
  79. package/docs/DASHBOARD.zh-CN.md +1 -1
  80. package/docs/GATEWAY-ARCHITECTURE.md +143 -57
  81. package/package.json +1 -1
  82. package/skills/embassy-peer/SKILL.md +2 -2
@@ -1,5 +1,4 @@
1
- import { arePublicAvailablePeerSnapshots } from "./types.js";
2
- import { isCompatibilityAttestation, } from "./compatibility.js";
1
+ import { arePublicAvailablePeerSnapshots, isPublicCompatibilityCheckSnapshot, isPublicRegistryObservationSnapshot, } from "./types.js";
3
2
  export const DASHBOARD_MODEL_LIMITS = Object.freeze({
4
3
  connectors: 16,
5
4
  availablePeers: 64,
@@ -12,6 +11,235 @@ export const DASHBOARD_MODEL_LIMITS = Object.freeze({
12
11
  activityEvents: 64,
13
12
  alerts: 32,
14
13
  });
14
+ const presentation = (values) => values;
15
+ const statePresentation = {
16
+ delivery: presentation({
17
+ queued: { tone: "info", chip: "progress" },
18
+ duplicate: { tone: "quiet", chip: "inert" },
19
+ dispatching: { tone: "info", chip: "progress" },
20
+ transport_written: { tone: "info", chip: "progress" },
21
+ held: { tone: "warning", chip: "progress" },
22
+ delivered: { tone: "good", chip: "positive" },
23
+ unconfirmed: { tone: "warning", chip: "indeterminate" },
24
+ failed: { tone: "danger", chip: "failure" },
25
+ ambiguous: { tone: "warning", chip: "indeterminate" },
26
+ expired: { tone: "danger", chip: "failure" },
27
+ cancelled: { tone: "danger", chip: "inert" },
28
+ abandoned: { tone: "danger", chip: "inert" },
29
+ rejected: { tone: "danger", chip: "warning" },
30
+ }),
31
+ route: presentation({
32
+ stale: { tone: "danger", chip: "failure" },
33
+ idle: { tone: "good", chip: "progress" },
34
+ busy: { tone: "info", chip: "active" },
35
+ awaiting_approval: { tone: "warning", chip: "warning" },
36
+ offline: { tone: "danger", chip: "failure" },
37
+ incompatible: { tone: "danger", chip: "failure" },
38
+ disabled: { tone: "quiet", chip: "inert" },
39
+ }),
40
+ peer: presentation({
41
+ idle: { tone: "good", chip: "progress" },
42
+ busy: { tone: "info", chip: "active" },
43
+ awaiting_approval: { tone: "warning", chip: "warning" },
44
+ offline: { tone: "danger", chip: "inert" },
45
+ incompatible: { tone: "danger", chip: "failure" },
46
+ }),
47
+ health: presentation({
48
+ healthy: { tone: "good", chip: "positive" },
49
+ connecting: { tone: "info", chip: "progress" },
50
+ degraded: { tone: "warning", chip: "warning" },
51
+ offline: { tone: "danger", chip: "failure" },
52
+ incompatible: { tone: "danger", chip: "failure" },
53
+ }),
54
+ compatibility: presentation({
55
+ compatible: { tone: "good", chip: "positive" },
56
+ unknown: { tone: "quiet", chip: "inert" },
57
+ expired: { tone: "warning", chip: "warning" },
58
+ incompatible: { tone: "danger", chip: "failure" },
59
+ }),
60
+ overall: presentation({
61
+ ready: { tone: "good", chip: "positive" },
62
+ setup: { tone: "info", chip: "progress" },
63
+ attention: { tone: "danger", chip: "failure" },
64
+ }),
65
+ party: presentation({
66
+ ready: { tone: "good", chip: "positive" },
67
+ busy: { tone: "info", chip: "active" },
68
+ waiting: { tone: "warning", chip: "warning" },
69
+ missing: { tone: "quiet", chip: "inert" },
70
+ attention: { tone: "danger", chip: "failure" },
71
+ }),
72
+ severity: presentation({
73
+ info: { tone: "info", chip: "active" },
74
+ warning: { tone: "warning", chip: "warning" },
75
+ error: { tone: "danger", chip: "failure" },
76
+ }),
77
+ connection: presentation({
78
+ connected: { tone: "good", chip: "positive" },
79
+ connecting: { tone: "info", chip: "progress" },
80
+ paused: { tone: "quiet", chip: "inert" },
81
+ unavailable: { tone: "warning", chip: "warning" },
82
+ capacity: { tone: "warning", chip: "warning" },
83
+ disconnected: { tone: "warning", chip: "warning" },
84
+ stopped: { tone: "warning", chip: "warning" },
85
+ }),
86
+ };
87
+ const guidanceCopyKeys = {
88
+ reobserve_claude: "reobserveClaude",
89
+ reobserve_codex: "reobserveCodex",
90
+ codex_reactivation_required: "codexReactivationRequired",
91
+ consent_edge_unavailable: "consentEdgeUnavailable",
92
+ claude_not_observed: "claudeNotObserved",
93
+ codex_stale: "codexStale",
94
+ connector_offline: "connectorOffline",
95
+ route_stale: "routeStale",
96
+ queue_stalled: "queueStalled",
97
+ recipient_waiting_input: "recipientWaitingInput",
98
+ unconfirmed: "unconfirmed",
99
+ degraded: "degraded",
100
+ codex_succession_busy: "codexSuccessionBusy",
101
+ codex_succession_recovery: "codexSuccessionRecovery",
102
+ progress_watch: "progressWatch",
103
+ registry_empty: "registryEmpty",
104
+ registry_rejected: "registryRejected",
105
+ provider_incompatible: "providerIncompatible",
106
+ generic: "generic",
107
+ };
108
+ const nextActionCopyKeys = {
109
+ discover_claude: "next.discoverClaude",
110
+ select_claude: "next.selectClaude",
111
+ pair_routes: "next.pairRoutes",
112
+ restore_claude: "next.restoreClaude",
113
+ repair_claude_inventory: "next.repairClaude",
114
+ register_codex: "next.registerCodex",
115
+ restore_codex: "next.restoreCodex",
116
+ review_compatibility: "next.reviewCompatibility",
117
+ none: "next.none",
118
+ };
119
+ const attentionCommands = {
120
+ reobserve_claude: "embassy select-claude --alias {alias}",
121
+ reobserve_codex: "embassy register-codex --alias {alias}",
122
+ codex_reactivation_required: "embassy register-codex --alias {alias}",
123
+ consent_edge_unavailable: "embassy refresh-dashboard",
124
+ claude_not_observed: "embassy select-claude --alias {alias}",
125
+ codex_stale: "embassy register-codex --alias {alias}",
126
+ connector_offline: "embassy status",
127
+ route_stale: "embassy status",
128
+ queue_stalled: "embassy status",
129
+ recipient_waiting_input: "embassy status",
130
+ unconfirmed: "embassy status",
131
+ degraded: "embassy status",
132
+ codex_succession_busy: "embassy register-codex --alias <new> --succeeds {alias}",
133
+ codex_succession_recovery: "embassy register-codex --alias <new> --succeeds {alias}",
134
+ progress_watch: "embassy status",
135
+ registry_empty: "embassy refresh-dashboard",
136
+ registry_rejected: "embassy status",
137
+ provider_incompatible: "embassy refresh-dashboard",
138
+ generic: "embassy status",
139
+ };
140
+ const deliveryMeaningKeys = {
141
+ queued: "activity.meaning.queued",
142
+ duplicate: "activity.meaning.duplicate",
143
+ dispatching: "activity.meaning.dispatching",
144
+ transport_written: "activity.meaning.transportWritten",
145
+ held: "activity.meaning.held",
146
+ delivered: "activity.meaning.delivered",
147
+ unconfirmed: "activity.meaning.unconfirmed",
148
+ failed: "activity.meaning.failed",
149
+ ambiguous: "activity.meaning.ambiguous",
150
+ expired: "activity.meaning.expired",
151
+ cancelled: "activity.meaning.cancelled",
152
+ abandoned: "activity.meaning.abandoned.generic",
153
+ rejected: "activity.meaning.rejected",
154
+ };
155
+ /** JSON-safe semantics embedded once in the live dashboard boot payload. */
156
+ export const DASHBOARD_SEMANTICS = Object.freeze({
157
+ statePresentation,
158
+ deliveryChipByDirection: {
159
+ delivered: { codex_to_claude: "qualified" },
160
+ },
161
+ deliveryChipBySafeErrorCode: {
162
+ rejected: { SENDER_NOT_PAIRED: "inert" },
163
+ },
164
+ deliveryToneBySafeErrorCode: {
165
+ rejected: { SENDER_NOT_PAIRED: "quiet" },
166
+ },
167
+ guidanceCopyKeys,
168
+ nextActionCopyKeys,
169
+ attentionCommands,
170
+ attentionCommandFallbacks: {
171
+ codex_succession_busy: "<old>",
172
+ codex_succession_recovery: "<old>",
173
+ },
174
+ deliveryMeaningKeys,
175
+ deliveryMeaningByDirection: {
176
+ codex_to_claude: "activity.meaning.delivered.codexToClaude",
177
+ claude_to_codex: "activity.meaning.delivered.claudeToCodex",
178
+ },
179
+ deliveryMeaningBySafeErrorCode: {
180
+ SENDER_NOT_PAIRED: "activity.meaning.senderNotPaired",
181
+ CONTROLLER_RESTARTED: "activity.meaning.abandoned.controllerRestarted",
182
+ TRANSIENT_BODY_UNAVAILABLE: "activity.meaning.abandoned.transientBody",
183
+ ROUTE_UNREGISTERED: "activity.meaning.abandoned.routeTerminated",
184
+ MESSAGE_EXPIRED: "activity.meaning.abandoned.routeTerminated",
185
+ },
186
+ });
187
+ function semanticPresentation(domain, state) {
188
+ const table = DASHBOARD_SEMANTICS.statePresentation[domain];
189
+ return Object.prototype.hasOwnProperty.call(table, state)
190
+ ? table[state]
191
+ : undefined;
192
+ }
193
+ export function dashboardChipKind(domain, state, direction, safeErrorCode) {
194
+ if (domain === "delivery") {
195
+ const byCode = DASHBOARD_SEMANTICS.deliveryChipBySafeErrorCode;
196
+ const byDirection = DASHBOARD_SEMANTICS.deliveryChipByDirection;
197
+ if (safeErrorCode !== undefined) {
198
+ const override = byCode[state]?.[safeErrorCode];
199
+ if (override !== undefined)
200
+ return override;
201
+ }
202
+ if (direction !== undefined) {
203
+ const override = byDirection[state]?.[direction];
204
+ if (override !== undefined)
205
+ return override;
206
+ }
207
+ }
208
+ return semanticPresentation(domain, state)?.chip ?? "unknown";
209
+ }
210
+ export function dashboardTone(domain, state, safeErrorCode) {
211
+ if (domain === "delivery" && safeErrorCode !== undefined) {
212
+ const byCode = DASHBOARD_SEMANTICS.deliveryToneBySafeErrorCode;
213
+ const override = byCode[state]?.[safeErrorCode];
214
+ if (override !== undefined)
215
+ return override;
216
+ }
217
+ return semanticPresentation(domain, state)?.tone ?? "danger";
218
+ }
219
+ export function deliveryMeaningKey(state, direction, safeErrorCode) {
220
+ if (state === "rejected" && safeErrorCode === "SENDER_NOT_PAIRED") {
221
+ return DASHBOARD_SEMANTICS.deliveryMeaningBySafeErrorCode.SENDER_NOT_PAIRED;
222
+ }
223
+ if (state === "delivered" && direction !== undefined) {
224
+ return DASHBOARD_SEMANTICS.deliveryMeaningByDirection[direction];
225
+ }
226
+ if (state === "abandoned" && safeErrorCode !== undefined) {
227
+ const byCode = DASHBOARD_SEMANTICS.deliveryMeaningBySafeErrorCode;
228
+ return byCode[safeErrorCode] ?? deliveryMeaningKeys.abandoned;
229
+ }
230
+ const byState = DASHBOARD_SEMANTICS.deliveryMeaningKeys;
231
+ return byState[state] ?? "activity.meaning.other";
232
+ }
233
+ export function guidanceCopyKey(guidance) {
234
+ return DASHBOARD_SEMANTICS.guidanceCopyKeys[guidance];
235
+ }
236
+ export function nextActionCopyKey(action) {
237
+ return DASHBOARD_SEMANTICS.nextActionCopyKeys[action];
238
+ }
239
+ export function attentionCommand(item) {
240
+ const fallbacks = DASHBOARD_SEMANTICS.attentionCommandFallbacks;
241
+ return DASHBOARD_SEMANTICS.attentionCommands[item.guidance].replaceAll("{alias}", item.alias ?? fallbacks[item.guidance] ?? "<alias>");
242
+ }
15
243
  const SAFE_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
16
244
  const SAFE_PROTOCOL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+:/-]{0,47}$/;
17
245
  const OPAQUE_SUFFIX_PATTERN = /^[a-f0-9]{6,12}$/i;
@@ -69,8 +297,15 @@ function normalizedCounters(counters) {
69
297
  bytesAccepted: normalizedInteger(counters?.bytesAccepted) ?? 0,
70
298
  };
71
299
  }
300
+ export function buildDashboardViewModel(snapshot) {
301
+ return buildProjectedDashboardViewModel(snapshot, false);
302
+ }
303
+ export function buildLiveDashboardViewModel(snapshot) {
304
+ return buildProjectedDashboardViewModel(snapshot, true);
305
+ }
72
306
  function routeIsReady(route) {
73
307
  return (route.enabled &&
308
+ route.safeErrorCode !== "CODEX_WRITES_DISABLED" &&
74
309
  route.compatibility === "compatible" &&
75
310
  (route.state === "idle" || route.state === "busy"));
76
311
  }
@@ -151,6 +386,14 @@ function guidanceFor(code, provider) {
151
386
  case "ADAPTER_DEGRADED":
152
387
  case "ROUTE_DEGRADED":
153
388
  return "degraded";
389
+ case "CLAUDE_PEER_VERSION_UNSUPPORTED":
390
+ case "CLAUDE_VERSION_UNPARSEABLE":
391
+ case "CLAUDE_VERSION_CHECK_FAILED":
392
+ case "CLAUDE_VERSION_EVIDENCE_CONFLICT":
393
+ case "CLAUDE_VERSION_EVIDENCE_TOO_LARGE":
394
+ case "CODEX_APP_SERVER_VERSION_UNSUPPORTED":
395
+ case "CODEX_APP_SERVER_VERSION_UNPARSEABLE":
396
+ return "provider_incompatible";
154
397
  default:
155
398
  return "generic";
156
399
  }
@@ -219,7 +462,7 @@ function messageKey(message) {
219
462
  typeof message.messageIdSuffix === "string" ? message.messageIdSuffix : "",
220
463
  ].join("\0");
221
464
  }
222
- function buildMessageGroups(messages) {
465
+ function buildMessageGroups(messages, includeBodies) {
223
466
  const grouped = new Map();
224
467
  for (const message of messages) {
225
468
  const key = messageKey(message);
@@ -282,7 +525,8 @@ function buildMessageGroups(messages) {
282
525
  ...(latest.steer === true ? { steer: true } : {}),
283
526
  // Bodies are first-class observable data (CO #36); the display copy
284
527
  // is bounded so one 16 KiB message cannot dominate the view model.
285
- ...(typeof latest.body === "string" && latest.body.length > 0
528
+ ...(includeBodies &&
529
+ typeof latest.body === "string" && latest.body.length > 0
286
530
  ? { body: boundedText(latest.body, 4096) }
287
531
  : {}),
288
532
  events: allEvents,
@@ -311,12 +555,23 @@ function buildMessageGroups(messages) {
311
555
  activeGroups: all.filter((message) => ["queued", "dispatching", "transport_written", "held"].includes(message.state)).length,
312
556
  };
313
557
  }
314
- export function buildDashboardViewModel(snapshot) {
558
+ function buildProjectedDashboardViewModel(snapshot, includeBodies) {
315
559
  const generatedAt = normalizedTimestamp(snapshot.generatedAt);
316
560
  const inboundMode = snapshot.inboundMode === "open" ? "open" : "paired";
317
561
  const validPeers = arePublicAvailablePeerSnapshots(snapshot.availablePeers)
318
562
  ? snapshot.availablePeers
319
563
  : [];
564
+ const quarantinedProviders = new Set([
565
+ ...snapshot.connectors
566
+ .filter((connector) => connector.compatibility === "incompatible")
567
+ .map((connector) => connector.provider),
568
+ ...(Array.isArray(snapshot.compatibilityChecks)
569
+ ? snapshot.compatibilityChecks
570
+ .filter(isPublicCompatibilityCheckSnapshot)
571
+ .filter((check) => check.tier === "incompatible")
572
+ .map((check) => check.surface)
573
+ : []),
574
+ ]);
320
575
  const peers = validPeers
321
576
  .map((peer) => ({
322
577
  alias: boundedText(peer.alias),
@@ -403,7 +658,11 @@ export function buildDashboardViewModel(snapshot) {
403
658
  claudeAlias,
404
659
  codexAlias,
405
660
  host: boundedText(pair.host),
406
- state: bothPresent && routeIsReady(claudeRoute) && routeIsReady(codexRoute)
661
+ state: bothPresent &&
662
+ !quarantinedProviders.has("claude") &&
663
+ !quarantinedProviders.has("codex") &&
664
+ routeIsReady(claudeRoute) &&
665
+ routeIsReady(codexRoute)
407
666
  ? "ready"
408
667
  : bothPresent
409
668
  ? "degraded"
@@ -425,9 +684,9 @@ export function buildDashboardViewModel(snapshot) {
425
684
  generatedAtMs === undefined) {
426
685
  return [];
427
686
  }
428
- const idleMs = normalizedInteger(watch.idleMs);
429
- if (idleMs === undefined ||
430
- (watch.nudgeCount !== 0 && watch.nudgeCount !== 1 && watch.nudgeCount !== 2)) {
687
+ if (watch.nudgeCount !== 0 &&
688
+ watch.nudgeCount !== 1 &&
689
+ watch.nudgeCount !== 2) {
431
690
  return [];
432
691
  }
433
692
  return [
@@ -435,11 +694,8 @@ export function buildDashboardViewModel(snapshot) {
435
694
  conversationIdSuffix: watch.conversationIdSuffix,
436
695
  ownerAlias: boundedText(watch.ownerAlias),
437
696
  workerAlias: boundedText(watch.workerAlias),
438
- phase: watch.phase,
439
- capability: watch.capability,
440
697
  lastActivityAt,
441
698
  nextActionAt,
442
- idleMs,
443
699
  idleForMs: Math.max(0, generatedAtMs - Date.parse(lastActivityAt)),
444
700
  dueInMs: Math.max(0, Date.parse(nextActionAt) - generatedAtMs),
445
701
  nudgeCount: watch.nudgeCount,
@@ -466,37 +722,52 @@ export function buildDashboardViewModel(snapshot) {
466
722
  ownerAlias: boundedText(event.ownerAlias),
467
723
  workerAlias: boundedText(event.workerAlias),
468
724
  kind: event.kind,
469
- ...(event.nudgeNumber === undefined
470
- ? {}
471
- : { nudgeNumber: event.nudgeNumber }),
725
+ actor: event.actor,
726
+ ...(event.reason === undefined ? {} : { reason: event.reason }),
472
727
  },
473
728
  ];
474
729
  })
475
730
  .sort((left, right) => right.sequence - left.sequence);
476
731
  const watchEvents = allWatchEvents.slice(0, DASHBOARD_MODEL_LIMITS.progressWatchEvents);
477
732
  const connectors = snapshot.connectors
478
- .map((connector) => ({
479
- provider: connector.provider,
480
- host: boundedText(connector.host),
481
- health: connector.health,
482
- compatibility: connector.compatibility,
483
- ...(safeProtocol(connector.protocol) === undefined
484
- ? {}
485
- : { protocol: safeProtocol(connector.protocol) }),
486
- ...(safeProtocol(connector.protocolVersion) === undefined
487
- ? {}
488
- : { protocolVersion: safeProtocol(connector.protocolVersion) }),
489
- ...(normalizedTimestamp(connector.lastSeenAt) === undefined
490
- ? {}
491
- : { lastSeenAt: normalizedTimestamp(connector.lastSeenAt) }),
492
- ...(safeCode(connector.safeErrorCode) === undefined
493
- ? {}
494
- : { safeErrorCode: safeCode(connector.safeErrorCode) }),
495
- }))
733
+ .map((connector) => {
734
+ const registry = isPublicRegistryObservationSnapshot(connector.registry)
735
+ ? connector.registry
736
+ : undefined;
737
+ return {
738
+ provider: connector.provider,
739
+ host: boundedText(connector.host),
740
+ health: connector.health,
741
+ compatibility: connector.compatibility,
742
+ ...(safeProtocol(connector.protocol) === undefined
743
+ ? {}
744
+ : { protocol: safeProtocol(connector.protocol) }),
745
+ ...(safeProtocol(connector.protocolVersion) === undefined
746
+ ? {}
747
+ : { protocolVersion: safeProtocol(connector.protocolVersion) }),
748
+ ...(normalizedTimestamp(connector.lastSeenAt) === undefined
749
+ ? {}
750
+ : { lastSeenAt: normalizedTimestamp(connector.lastSeenAt) }),
751
+ ...(safeCode(connector.safeErrorCode) === undefined
752
+ ? {}
753
+ : { safeErrorCode: safeCode(connector.safeErrorCode) }),
754
+ ...(registry === undefined
755
+ ? {}
756
+ : {
757
+ registry: {
758
+ entriesScanned: registry.entriesScanned,
759
+ parseableRecords: registry.parseableRecords,
760
+ parseableRecordSeenSinceBoot: registry.parseableRecordSeenSinceBoot,
761
+ rejected: registry.rejected.map((row) => ({ ...row })),
762
+ rejectedCodesOmitted: registry.rejectedCodesOmitted,
763
+ },
764
+ }),
765
+ };
766
+ })
496
767
  .sort((left, right) => compareText(`${left.provider}\0${left.host}`, `${right.provider}\0${right.host}`))
497
768
  .slice(0, DASHBOARD_MODEL_LIMITS.connectors);
498
769
  const compatibilityChecks = (Array.isArray(snapshot.compatibilityChecks)
499
- ? snapshot.compatibilityChecks.filter(isCompatibilityAttestation)
770
+ ? snapshot.compatibilityChecks.filter(isPublicCompatibilityCheckSnapshot)
500
771
  : [])
501
772
  .map((attestation) => {
502
773
  const failed = attestation.probes.find((probe) => probe.outcome === "fail");
@@ -504,6 +775,8 @@ export function buildDashboardViewModel(snapshot) {
504
775
  return {
505
776
  surface: attestation.surface,
506
777
  version: boundedText(attestation.version),
778
+ testedVersion: boundedText(attestation.testedVersion),
779
+ supportedMajor: boundedText(attestation.supportedMajor),
507
780
  tier: attestation.tier,
508
781
  checkedAt: attestation.checkedAt,
509
782
  ...(failed === undefined ? {} : { failure: failed.name }),
@@ -512,7 +785,7 @@ export function buildDashboardViewModel(snapshot) {
512
785
  })
513
786
  .sort((left, right) => compareText(left.surface, right.surface))
514
787
  .slice(0, 2);
515
- const messages = buildMessageGroups(snapshot.messages);
788
+ const messages = buildMessageGroups(snapshot.messages, includeBodies);
516
789
  const queuedMessages = allRoutes.reduce((total, route) => boundedAdd(total, route.queueDepth), 0);
517
790
  const queuedWithAge = allRoutes
518
791
  .filter((route) => route.oldestQueuedAt !== undefined && route.queueAgeMs !== undefined)
@@ -521,52 +794,63 @@ export function buildDashboardViewModel(snapshot) {
521
794
  const activeDeliveries = messages.activeGroups;
522
795
  const claudeRoutes = allRoutes.filter((route) => route.provider === "claude");
523
796
  const codexRoutes = allRoutes.filter((route) => route.provider === "codex");
524
- const readyClaude = claudeRoutes.filter(routeIsReady).length;
525
- const readyCodex = codexRoutes.filter(routeIsReady).length;
797
+ const monitorOnlyCodexRoutes = codexRoutes.filter((route) => route.safeErrorCode === "CODEX_WRITES_DISABLED");
798
+ const routeIsExchangeReady = (route) => !quarantinedProviders.has(route.provider) && routeIsReady(route);
799
+ const readyClaude = claudeRoutes.filter(routeIsExchangeReady).length;
800
+ const readyCodex = codexRoutes.filter(routeIsExchangeReady).length;
526
801
  const readyPairs = allPairs.filter((pair) => pair.state === "ready");
527
802
  const pairedClaude = new Set(allPairs.map((pair) => pair.claudeAlias));
528
803
  const pairedCodex = new Set(allPairs.map((pair) => pair.codexAlias));
529
- const readyPairedClaude = claudeRoutes.some((route) => routeIsReady(route) && pairedClaude.has(route.alias));
530
- const readyPairedCodex = codexRoutes.some((route) => routeIsReady(route) && pairedCodex.has(route.alias));
804
+ const readyPairedClaude = claudeRoutes.some((route) => routeIsExchangeReady(route) && pairedClaude.has(route.alias));
805
+ const readyPairedCodex = codexRoutes.some((route) => routeIsExchangeReady(route) && pairedCodex.has(route.alias));
531
806
  const pairCountIsLowerBound = (normalizedInteger(snapshot.truncation.pairs) ?? 0) > 0;
532
807
  const hasPairEvidence = allPairs.length > 0 || pairCountIsLowerBound;
533
808
  const graph = {
534
809
  pairCount: allPairs.length,
535
810
  readyPairCount: readyPairs.length,
536
811
  pairCountIsLowerBound,
537
- unpairedReadyClaude: claudeRoutes.filter((route) => routeIsReady(route) && !pairedClaude.has(route.alias)).length,
538
- unpairedReadyCodex: codexRoutes.filter((route) => routeIsReady(route) && !pairedCodex.has(route.alias)).length,
812
+ unpairedReadyClaude: claudeRoutes.filter((route) => routeIsExchangeReady(route) && !pairedClaude.has(route.alias)).length,
813
+ unpairedReadyCodex: codexRoutes.filter((route) => routeIsExchangeReady(route) && !pairedCodex.has(route.alias)).length,
539
814
  };
540
815
  const selectedPeers = validPeers.filter((peer) => peer.selected && peerIsSelectable(peer));
541
816
  const selectablePeers = validPeers.filter(peerIsSelectable);
542
- const selectedClaudeCount = Math.max(selectedPeers.length, readyClaude);
817
+ const selectedClaudeCount = quarantinedProviders.has("claude")
818
+ ? 0
819
+ : Math.max(selectedPeers.length, readyClaude);
543
820
  const claudeStatus = partyStatus(claudeRoutes, readyClaude);
544
821
  const codexStatus = partyStatus(codexRoutes, readyCodex);
545
- const claudeNextAction = selectedClaudeCount > 0 &&
546
- (readyPairedClaude || (pairCountIsLowerBound && readyClaude > 0))
547
- ? "none"
548
- : readyClaude > 0
549
- ? "pair_routes"
550
- : validPeers.length === 0
551
- ? "discover_claude"
552
- : selectablePeers.length > 0
553
- ? !hasPairEvidence
554
- ? "pair_routes"
555
- : "restore_claude"
556
- : selectedPeers.length > 0
557
- ? "restore_claude"
558
- : "repair_claude_inventory";
559
- const codexNextAction = readyPairedCodex || (pairCountIsLowerBound && readyCodex > 0)
560
- ? "none"
561
- : readyCodex > 0
562
- ? "pair_routes"
563
- : codexRoutes.length === 0
564
- ? "register_codex"
565
- : "restore_codex";
822
+ const claudeNextAction = quarantinedProviders.has("claude")
823
+ ? "review_compatibility"
824
+ : selectedClaudeCount > 0 &&
825
+ (readyPairedClaude || (pairCountIsLowerBound && readyClaude > 0))
826
+ ? "none"
827
+ : readyClaude > 0
828
+ ? "pair_routes"
829
+ : validPeers.length === 0
830
+ ? "discover_claude"
831
+ : selectablePeers.length > 0
832
+ ? !hasPairEvidence
833
+ ? "pair_routes"
834
+ : "restore_claude"
835
+ : selectedPeers.length > 0
836
+ ? "restore_claude"
837
+ : "repair_claude_inventory";
838
+ const codexNextAction = quarantinedProviders.has("codex") || monitorOnlyCodexRoutes.length > 0
839
+ ? "review_compatibility"
840
+ : readyPairedCodex || (pairCountIsLowerBound && readyCodex > 0)
841
+ ? "none"
842
+ : readyCodex > 0
843
+ ? "pair_routes"
844
+ : codexRoutes.length === 0
845
+ ? "register_codex"
846
+ : "restore_codex";
566
847
  const explicitAlerts = coalesceCodexReactivationAlerts(snapshot.alerts
567
848
  .filter((alert) => alert.code !== "COMPATIBILITY_CERTIFICATION_FAILED")
568
849
  .map((alert) => {
569
850
  const code = safeCode(alert.code);
851
+ const route = code === "QUEUE_STALLED" && typeof alert.alias === "string"
852
+ ? routeByAlias.get(alert.alias)
853
+ : undefined;
570
854
  return {
571
855
  kind: "alert",
572
856
  ...(code === undefined ? {} : { code }),
@@ -587,7 +871,19 @@ export function buildDashboardViewModel(snapshot) {
587
871
  ...(typeof alert.host === "string"
588
872
  ? { host: boundedText(alert.host) }
589
873
  : {}),
590
- guidance: guidanceFor(code, alert.provider),
874
+ ...(alert.provider === "codex" &&
875
+ typeof alert.host === "string" &&
876
+ route !== undefined &&
877
+ alert.host === route.host &&
878
+ route.provider === "codex" &&
879
+ route.state === "busy" &&
880
+ route.queueDepth > 0
881
+ ? { queueDepth: route.queueDepth }
882
+ : {}),
883
+ guidance: alert.provider !== undefined &&
884
+ quarantinedProviders.has(alert.provider)
885
+ ? "provider_incompatible"
886
+ : guidanceFor(code, alert.provider),
591
887
  };
592
888
  }), allRoutes).sort((left, right) => alertPriority(left.severity) - alertPriority(right.severity) ||
593
889
  compareText(right.timestamp ?? "", left.timestamp ?? ""));
@@ -649,13 +945,11 @@ export function buildDashboardViewModel(snapshot) {
649
945
  attentionCandidates[existingIndex] = item;
650
946
  }
651
947
  for (const watch of allWatches) {
652
- if (watch.phase !== "episode" && watch.capability !== "route")
948
+ if (watch.nudgeCount === 0)
653
949
  continue;
654
950
  attentionCandidates.push({
655
951
  kind: "watch",
656
- code: watch.capability === "route"
657
- ? "PROGRESS_WATCH_CAPABILITY_DEGRADED"
658
- : "PROGRESS_WATCH_QUIET",
952
+ code: "PROGRESS_WATCH_QUIET",
659
953
  severity: "warning",
660
954
  timestamp: watch.lastActivityAt,
661
955
  alias: watch.workerAlias,
@@ -664,7 +958,9 @@ export function buildDashboardViewModel(snapshot) {
664
958
  }
665
959
  const representedScopes = new Set(attentionCandidates.map((item) => `${item.provider ?? ""}\0${item.alias ?? ""}\0${item.host ?? ""}`));
666
960
  for (const route of allRoutes) {
667
- if (routeIsReady(route)) {
961
+ if (routeIsReady(route) ||
962
+ quarantinedProviders.has(route.provider) ||
963
+ route.safeErrorCode === "CODEX_WRITES_DISABLED") {
668
964
  continue;
669
965
  }
670
966
  const scope = `${route.provider}\0${route.alias}\0${route.host}`;
@@ -709,13 +1005,64 @@ export function buildDashboardViewModel(snapshot) {
709
1005
  : { timestamp: connector.lastSeenAt }),
710
1006
  provider: connector.provider,
711
1007
  host: connector.host,
712
- guidance: connector.health === "offline" ? "connector_offline" : "degraded",
1008
+ guidance: connector.health === "offline"
1009
+ ? "connector_offline"
1010
+ : quarantinedProviders.has(connector.provider) ||
1011
+ connector.safeErrorCode === "CLAUDE_PEER_VERSION_UNSUPPORTED" ||
1012
+ connector.safeErrorCode === "CLAUDE_VERSION_UNPARSEABLE" ||
1013
+ connector.safeErrorCode === "CLAUDE_VERSION_CHECK_FAILED" ||
1014
+ connector.safeErrorCode === "CLAUDE_VERSION_EVIDENCE_CONFLICT" ||
1015
+ connector.safeErrorCode === "CLAUDE_VERSION_EVIDENCE_TOO_LARGE" ||
1016
+ connector.safeErrorCode === "CODEX_APP_SERVER_VERSION_UNSUPPORTED" ||
1017
+ connector.safeErrorCode === "CODEX_APP_SERVER_VERSION_UNPARSEABLE"
1018
+ ? "provider_incompatible"
1019
+ : "degraded",
713
1020
  });
714
1021
  representedScopes.add(scope);
715
1022
  }
1023
+ for (const connector of connectors) {
1024
+ const observation = connector.registry;
1025
+ if (observation === undefined)
1026
+ continue;
1027
+ if (observation.rejected.length > 0 ||
1028
+ observation.rejectedCodesOmitted > 0) {
1029
+ const item = {
1030
+ kind: "connector",
1031
+ code: observation.rejected.some((row) => row.safeErrorCode === "CLAUDE_REGISTRY_UNAVAILABLE")
1032
+ ? "CLAUDE_REGISTRY_UNAVAILABLE"
1033
+ : "CLAUDE_REGISTRY_RECORDS_REJECTED",
1034
+ severity: "warning",
1035
+ provider: "claude",
1036
+ host: connector.host,
1037
+ guidance: "registry_rejected",
1038
+ };
1039
+ const duplicateIndex = attentionCandidates.findIndex((candidate) => candidate.code === item.code &&
1040
+ candidate.provider === item.provider &&
1041
+ candidate.alias === item.alias &&
1042
+ candidate.host === item.host);
1043
+ if (duplicateIndex === -1)
1044
+ attentionCandidates.push(item);
1045
+ else
1046
+ attentionCandidates[duplicateIndex] = item;
1047
+ continue;
1048
+ }
1049
+ if (observation.parseableRecordSeenSinceBoot)
1050
+ continue;
1051
+ attentionCandidates.push({
1052
+ kind: "connector",
1053
+ code: observation.entriesScanned === 0
1054
+ ? "CLAUDE_REGISTRY_EMPTY_SINCE_BOOT"
1055
+ : "CLAUDE_REGISTRY_NO_PARSEABLE_RECORD_SINCE_BOOT",
1056
+ severity: "warning",
1057
+ provider: "claude",
1058
+ host: connector.host,
1059
+ guidance: "registry_empty",
1060
+ });
1061
+ }
716
1062
  if ((snapshot.health === "degraded" ||
717
1063
  snapshot.health === "offline" ||
718
1064
  snapshot.health === "incompatible") &&
1065
+ !(snapshot.health === "degraded" && quarantinedProviders.size > 0) &&
719
1066
  !attentionCandidates.some((item) => item.kind === "broker")) {
720
1067
  attentionCandidates.push({
721
1068
  kind: "broker",
@@ -778,6 +1125,9 @@ export function buildDashboardViewModel(snapshot) {
778
1125
  status: codexStatus,
779
1126
  total: codexRoutes.length,
780
1127
  ready: readyCodex,
1128
+ ...(monitorOnlyCodexRoutes.length === 0
1129
+ ? {}
1130
+ : { monitorOnly: monitorOnlyCodexRoutes.length }),
781
1131
  countIsLowerBound: (normalizedInteger(snapshot.truncation.routes) ?? 0) > 0,
782
1132
  ...(codexRoutes[0]?.alias === undefined
783
1133
  ? {}