agent-embassy 1.4.1 → 1.6.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 (86) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/CONTRIBUTING.md +69 -6
  3. package/README.md +8 -5
  4. package/README.zh-CN.md +6 -4
  5. package/SECURITY.md +152 -27
  6. package/dist/src/gateway/claude-peer.d.ts +7 -4
  7. package/dist/src/gateway/claude-peer.js +80 -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 +6 -7
  23. package/dist/src/gateway/cli.js.map +1 -1
  24. package/dist/src/gateway/codex-app-server.d.ts +44 -2
  25. package/dist/src/gateway/codex-app-server.js +536 -2
  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 +42 -3
  31. package/dist/src/gateway/compatibility.js +190 -37
  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 +42 -17
  37. package/dist/src/gateway/dashboard-copy.en.js +56 -31
  38. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  39. package/dist/src/gateway/dashboard-copy.js +42 -17
  40. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  41. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +42 -17
  42. package/dist/src/gateway/dashboard-copy.zh-CN.js +56 -31
  43. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  44. package/dist/src/gateway/dashboard-model.d.ts +199 -11
  45. package/dist/src/gateway/dashboard-model.js +478 -75
  46. package/dist/src/gateway/dashboard-model.js.map +1 -1
  47. package/dist/src/gateway/dashboard.d.ts +3 -0
  48. package/dist/src/gateway/dashboard.js +62 -137
  49. package/dist/src/gateway/dashboard.js.map +1 -1
  50. package/dist/src/gateway/deepseek-detect.d.ts +13 -0
  51. package/dist/src/gateway/deepseek-detect.js +125 -0
  52. package/dist/src/gateway/deepseek-detect.js.map +1 -0
  53. package/dist/src/gateway/live-dashboard-app/app.js +158 -256
  54. package/dist/src/gateway/live-dashboard-assets.js +2 -1
  55. package/dist/src/gateway/live-dashboard-assets.js.map +1 -1
  56. package/dist/src/gateway/live-dashboard-stream.d.ts +2 -2
  57. package/dist/src/gateway/live-dashboard-stream.js +2 -2
  58. package/dist/src/gateway/live-dashboard-stream.js.map +1 -1
  59. package/dist/src/gateway/progress-watch-machine.d.ts +50 -64
  60. package/dist/src/gateway/progress-watch-machine.js +42 -133
  61. package/dist/src/gateway/progress-watch-machine.js.map +1 -1
  62. package/dist/src/gateway/provenance-envelope.d.ts +2 -0
  63. package/dist/src/gateway/provenance-envelope.js +13 -3
  64. package/dist/src/gateway/provenance-envelope.js.map +1 -1
  65. package/dist/src/gateway/providers.d.ts +80 -16
  66. package/dist/src/gateway/providers.js +782 -115
  67. package/dist/src/gateway/providers.js.map +1 -1
  68. package/dist/src/gateway/server.d.ts +10 -5
  69. package/dist/src/gateway/server.js +100 -13
  70. package/dist/src/gateway/server.js.map +1 -1
  71. package/dist/src/gateway/service.d.ts +46 -3
  72. package/dist/src/gateway/service.js +363 -101
  73. package/dist/src/gateway/service.js.map +1 -1
  74. package/dist/src/gateway/store.d.ts +23 -28
  75. package/dist/src/gateway/store.js +479 -467
  76. package/dist/src/gateway/store.js.map +1 -1
  77. package/dist/src/gateway/types.d.ts +40 -12
  78. package/dist/src/gateway/types.js +145 -4
  79. package/dist/src/gateway/types.js.map +1 -1
  80. package/docs/CONFIGURATION.md +8 -8
  81. package/docs/CONFIGURATION.zh-CN.md +8 -8
  82. package/docs/DASHBOARD.md +35 -6
  83. package/docs/DASHBOARD.zh-CN.md +1 -1
  84. package/docs/GATEWAY-ARCHITECTURE.md +173 -61
  85. package/package.json +1 -1
  86. package/skills/embassy-peer/SKILL.md +2 -2
@@ -1,5 +1,5 @@
1
- import { arePublicAvailablePeerSnapshots } from "./types.js";
2
- import { isCompatibilityAttestation, } from "./compatibility.js";
1
+ import { arePublicAvailablePeerSnapshots, isPublicCompatibilityCheckSnapshot, isPublicRegistryObservationSnapshot, } from "./types.js";
2
+ import { compatibilitySurfaceDefinitions, } from "./compatibility.js";
3
3
  export const DASHBOARD_MODEL_LIMITS = Object.freeze({
4
4
  connectors: 16,
5
5
  availablePeers: 64,
@@ -12,6 +12,237 @@ export const DASHBOARD_MODEL_LIMITS = Object.freeze({
12
12
  activityEvents: 64,
13
13
  alerts: 32,
14
14
  });
15
+ const presentation = (values) => values;
16
+ const statePresentation = {
17
+ delivery: presentation({
18
+ queued: { tone: "info", chip: "progress" },
19
+ duplicate: { tone: "quiet", chip: "inert" },
20
+ dispatching: { tone: "info", chip: "progress" },
21
+ transport_written: { tone: "info", chip: "progress" },
22
+ held: { tone: "warning", chip: "progress" },
23
+ delivered: { tone: "good", chip: "positive" },
24
+ unconfirmed: { tone: "warning", chip: "indeterminate" },
25
+ failed: { tone: "danger", chip: "failure" },
26
+ ambiguous: { tone: "warning", chip: "indeterminate" },
27
+ expired: { tone: "danger", chip: "failure" },
28
+ cancelled: { tone: "danger", chip: "inert" },
29
+ abandoned: { tone: "danger", chip: "inert" },
30
+ rejected: { tone: "danger", chip: "warning" },
31
+ }),
32
+ route: presentation({
33
+ stale: { tone: "danger", chip: "failure" },
34
+ idle: { tone: "good", chip: "progress" },
35
+ busy: { tone: "info", chip: "active" },
36
+ awaiting_approval: { tone: "warning", chip: "warning" },
37
+ offline: { tone: "danger", chip: "failure" },
38
+ incompatible: { tone: "danger", chip: "failure" },
39
+ disabled: { tone: "quiet", chip: "inert" },
40
+ }),
41
+ peer: presentation({
42
+ idle: { tone: "good", chip: "progress" },
43
+ busy: { tone: "info", chip: "active" },
44
+ awaiting_approval: { tone: "warning", chip: "warning" },
45
+ offline: { tone: "danger", chip: "inert" },
46
+ incompatible: { tone: "danger", chip: "failure" },
47
+ }),
48
+ health: presentation({
49
+ healthy: { tone: "good", chip: "positive" },
50
+ connecting: { tone: "info", chip: "progress" },
51
+ degraded: { tone: "warning", chip: "warning" },
52
+ 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
+ }),
61
+ overall: presentation({
62
+ ready: { tone: "good", chip: "positive" },
63
+ setup: { tone: "info", chip: "progress" },
64
+ attention: { tone: "danger", chip: "failure" },
65
+ }),
66
+ party: presentation({
67
+ ready: { tone: "good", chip: "positive" },
68
+ busy: { tone: "info", chip: "active" },
69
+ waiting: { tone: "warning", chip: "warning" },
70
+ missing: { tone: "quiet", chip: "inert" },
71
+ attention: { tone: "danger", chip: "failure" },
72
+ }),
73
+ severity: presentation({
74
+ info: { tone: "info", chip: "active" },
75
+ warning: { tone: "warning", chip: "warning" },
76
+ error: { tone: "danger", chip: "failure" },
77
+ }),
78
+ connection: presentation({
79
+ connected: { tone: "good", chip: "positive" },
80
+ connecting: { tone: "info", chip: "progress" },
81
+ paused: { tone: "quiet", chip: "inert" },
82
+ unavailable: { tone: "warning", chip: "warning" },
83
+ capacity: { tone: "warning", chip: "warning" },
84
+ disconnected: { tone: "warning", chip: "warning" },
85
+ stopped: { tone: "warning", chip: "warning" },
86
+ }),
87
+ };
88
+ const guidanceCopyKeys = {
89
+ reobserve_claude: "reobserveClaude",
90
+ reobserve_codex: "reobserveCodex",
91
+ codex_reactivation_required: "codexReactivationRequired",
92
+ consent_edge_unavailable: "consentEdgeUnavailable",
93
+ claude_not_observed: "claudeNotObserved",
94
+ codex_stale: "codexStale",
95
+ codex_app_reconnect_required: "codexAppReconnectRequired",
96
+ connector_offline: "connectorOffline",
97
+ route_stale: "routeStale",
98
+ queue_stalled: "queueStalled",
99
+ recipient_waiting_input: "recipientWaitingInput",
100
+ unconfirmed: "unconfirmed",
101
+ degraded: "degraded",
102
+ codex_succession_busy: "codexSuccessionBusy",
103
+ codex_succession_recovery: "codexSuccessionRecovery",
104
+ progress_watch: "progressWatch",
105
+ registry_empty: "registryEmpty",
106
+ registry_rejected: "registryRejected",
107
+ provider_incompatible: "providerIncompatible",
108
+ generic: "generic",
109
+ };
110
+ const nextActionCopyKeys = {
111
+ discover_claude: "next.discoverClaude",
112
+ select_claude: "next.selectClaude",
113
+ pair_routes: "next.pairRoutes",
114
+ restore_claude: "next.restoreClaude",
115
+ repair_claude_inventory: "next.repairClaude",
116
+ register_codex: "next.registerCodex",
117
+ restore_codex: "next.restoreCodex",
118
+ review_compatibility: "next.reviewCompatibility",
119
+ none: "next.none",
120
+ };
121
+ const attentionCommands = {
122
+ reobserve_claude: "embassy select-claude --alias {alias}",
123
+ reobserve_codex: "embassy register-codex --alias {alias}",
124
+ codex_reactivation_required: "embassy register-codex --alias {alias}",
125
+ consent_edge_unavailable: "embassy refresh-dashboard",
126
+ claude_not_observed: "embassy select-claude --alias {alias}",
127
+ codex_stale: "embassy register-codex --alias {alias}",
128
+ codex_app_reconnect_required: "/usr/bin/open --env CODEX_APP_SERVER_USE_LOCAL_DAEMON=1 -a ChatGPT",
129
+ connector_offline: "embassy status",
130
+ route_stale: "embassy status",
131
+ queue_stalled: "embassy status",
132
+ recipient_waiting_input: "embassy status",
133
+ unconfirmed: "embassy status",
134
+ degraded: "embassy status",
135
+ codex_succession_busy: "embassy register-codex --alias <new> --succeeds {alias}",
136
+ codex_succession_recovery: "embassy register-codex --alias <new> --succeeds {alias}",
137
+ progress_watch: "embassy status",
138
+ registry_empty: "embassy refresh-dashboard",
139
+ registry_rejected: "embassy status",
140
+ provider_incompatible: "embassy refresh-dashboard",
141
+ generic: "embassy status",
142
+ };
143
+ const deliveryMeaningKeys = {
144
+ queued: "activity.meaning.queued",
145
+ duplicate: "activity.meaning.duplicate",
146
+ dispatching: "activity.meaning.dispatching",
147
+ transport_written: "activity.meaning.transportWritten",
148
+ held: "activity.meaning.held",
149
+ delivered: "activity.meaning.delivered",
150
+ unconfirmed: "activity.meaning.unconfirmed",
151
+ failed: "activity.meaning.failed",
152
+ ambiguous: "activity.meaning.ambiguous",
153
+ expired: "activity.meaning.expired",
154
+ cancelled: "activity.meaning.cancelled",
155
+ abandoned: "activity.meaning.abandoned.generic",
156
+ rejected: "activity.meaning.rejected",
157
+ };
158
+ /** JSON-safe semantics embedded once in the live dashboard boot payload. */
159
+ export const DASHBOARD_SEMANTICS = Object.freeze({
160
+ statePresentation,
161
+ deliveryChipByDirection: {
162
+ delivered: { codex_to_claude: "qualified" },
163
+ },
164
+ deliveryChipBySafeErrorCode: {
165
+ rejected: { SENDER_NOT_PAIRED: "inert" },
166
+ },
167
+ deliveryToneBySafeErrorCode: {
168
+ rejected: { SENDER_NOT_PAIRED: "quiet" },
169
+ },
170
+ guidanceCopyKeys,
171
+ nextActionCopyKeys,
172
+ attentionCommands,
173
+ attentionCommandFallbacks: {
174
+ codex_succession_busy: "<old>",
175
+ codex_succession_recovery: "<old>",
176
+ },
177
+ deliveryMeaningKeys,
178
+ deliveryMeaningByDirection: {
179
+ codex_to_claude: "activity.meaning.delivered.codexToClaude",
180
+ claude_to_codex: "activity.meaning.delivered.claudeToCodex",
181
+ },
182
+ deliveryMeaningBySafeErrorCode: {
183
+ SENDER_NOT_PAIRED: "activity.meaning.senderNotPaired",
184
+ CONTROLLER_RESTARTED: "activity.meaning.abandoned.controllerRestarted",
185
+ TRANSIENT_BODY_UNAVAILABLE: "activity.meaning.abandoned.transientBody",
186
+ ROUTE_UNREGISTERED: "activity.meaning.abandoned.routeTerminated",
187
+ MESSAGE_EXPIRED: "activity.meaning.abandoned.routeTerminated",
188
+ },
189
+ });
190
+ function semanticPresentation(domain, state) {
191
+ const table = DASHBOARD_SEMANTICS.statePresentation[domain];
192
+ return Object.prototype.hasOwnProperty.call(table, state)
193
+ ? table[state]
194
+ : undefined;
195
+ }
196
+ export function dashboardChipKind(domain, state, direction, safeErrorCode) {
197
+ if (domain === "delivery") {
198
+ const byCode = DASHBOARD_SEMANTICS.deliveryChipBySafeErrorCode;
199
+ const byDirection = DASHBOARD_SEMANTICS.deliveryChipByDirection;
200
+ if (safeErrorCode !== undefined) {
201
+ const override = byCode[state]?.[safeErrorCode];
202
+ if (override !== undefined)
203
+ return override;
204
+ }
205
+ if (direction !== undefined) {
206
+ const override = byDirection[state]?.[direction];
207
+ if (override !== undefined)
208
+ return override;
209
+ }
210
+ }
211
+ return semanticPresentation(domain, state)?.chip ?? "unknown";
212
+ }
213
+ export function dashboardTone(domain, state, safeErrorCode) {
214
+ if (domain === "delivery" && safeErrorCode !== undefined) {
215
+ const byCode = DASHBOARD_SEMANTICS.deliveryToneBySafeErrorCode;
216
+ const override = byCode[state]?.[safeErrorCode];
217
+ if (override !== undefined)
218
+ return override;
219
+ }
220
+ return semanticPresentation(domain, state)?.tone ?? "danger";
221
+ }
222
+ export function deliveryMeaningKey(state, direction, safeErrorCode) {
223
+ if (state === "rejected" && safeErrorCode === "SENDER_NOT_PAIRED") {
224
+ return DASHBOARD_SEMANTICS.deliveryMeaningBySafeErrorCode.SENDER_NOT_PAIRED;
225
+ }
226
+ if (state === "delivered" && direction !== undefined) {
227
+ return DASHBOARD_SEMANTICS.deliveryMeaningByDirection[direction];
228
+ }
229
+ if (state === "abandoned" && safeErrorCode !== undefined) {
230
+ const byCode = DASHBOARD_SEMANTICS.deliveryMeaningBySafeErrorCode;
231
+ return byCode[safeErrorCode] ?? deliveryMeaningKeys.abandoned;
232
+ }
233
+ const byState = DASHBOARD_SEMANTICS.deliveryMeaningKeys;
234
+ return byState[state] ?? "activity.meaning.other";
235
+ }
236
+ export function guidanceCopyKey(guidance) {
237
+ return DASHBOARD_SEMANTICS.guidanceCopyKeys[guidance];
238
+ }
239
+ export function nextActionCopyKey(action) {
240
+ return DASHBOARD_SEMANTICS.nextActionCopyKeys[action];
241
+ }
242
+ export function attentionCommand(item) {
243
+ const fallbacks = DASHBOARD_SEMANTICS.attentionCommandFallbacks;
244
+ return DASHBOARD_SEMANTICS.attentionCommands[item.guidance].replaceAll("{alias}", item.alias ?? fallbacks[item.guidance] ?? "<alias>");
245
+ }
15
246
  const SAFE_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
16
247
  const SAFE_PROTOCOL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+:/-]{0,47}$/;
17
248
  const OPAQUE_SUFFIX_PATTERN = /^[a-f0-9]{6,12}$/i;
@@ -69,8 +300,15 @@ function normalizedCounters(counters) {
69
300
  bytesAccepted: normalizedInteger(counters?.bytesAccepted) ?? 0,
70
301
  };
71
302
  }
303
+ export function buildDashboardViewModel(snapshot, options = {}) {
304
+ return buildProjectedDashboardViewModel(snapshot, false, options);
305
+ }
306
+ export function buildLiveDashboardViewModel(snapshot, options = {}) {
307
+ return buildProjectedDashboardViewModel(snapshot, true, options);
308
+ }
72
309
  function routeIsReady(route) {
73
310
  return (route.enabled &&
311
+ route.safeErrorCode !== "CODEX_WRITES_DISABLED" &&
74
312
  route.compatibility === "compatible" &&
75
313
  (route.state === "idle" || route.state === "busy"));
76
314
  }
@@ -116,6 +354,22 @@ function routePriority(route) {
116
354
  return 3;
117
355
  return 4;
118
356
  }
357
+ const CODEX_APP_RECONNECT_GUIDANCE_AFTER_MS = 2_000;
358
+ function needsCodexAppReconnect(route, connectors, generatedAt) {
359
+ if (route.provider !== "codex" ||
360
+ route.state !== "stale" ||
361
+ route.safeErrorCode !== "CODEX_ROUTE_STALE" ||
362
+ route.lastSeenAt === undefined ||
363
+ generatedAt === undefined ||
364
+ Date.parse(generatedAt) - Date.parse(route.lastSeenAt) <
365
+ CODEX_APP_RECONNECT_GUIDANCE_AFTER_MS) {
366
+ return false;
367
+ }
368
+ return connectors.some((connector) => connector.provider === "codex" &&
369
+ connector.host === route.host &&
370
+ connector.health === "healthy" &&
371
+ connector.compatibility === "compatible");
372
+ }
119
373
  function alertPriority(severity) {
120
374
  return severity === "error" ? 0 : severity === "warning" ? 1 : 2;
121
375
  }
@@ -151,6 +405,14 @@ function guidanceFor(code, provider) {
151
405
  case "ADAPTER_DEGRADED":
152
406
  case "ROUTE_DEGRADED":
153
407
  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";
154
416
  default:
155
417
  return "generic";
156
418
  }
@@ -219,7 +481,7 @@ function messageKey(message) {
219
481
  typeof message.messageIdSuffix === "string" ? message.messageIdSuffix : "",
220
482
  ].join("\0");
221
483
  }
222
- function buildMessageGroups(messages) {
484
+ function buildMessageGroups(messages, includeBodies) {
223
485
  const grouped = new Map();
224
486
  for (const message of messages) {
225
487
  const key = messageKey(message);
@@ -282,7 +544,8 @@ function buildMessageGroups(messages) {
282
544
  ...(latest.steer === true ? { steer: true } : {}),
283
545
  // Bodies are first-class observable data (CO #36); the display copy
284
546
  // is bounded so one 16 KiB message cannot dominate the view model.
285
- ...(typeof latest.body === "string" && latest.body.length > 0
547
+ ...(includeBodies &&
548
+ typeof latest.body === "string" && latest.body.length > 0
286
549
  ? { body: boundedText(latest.body, 4096) }
287
550
  : {}),
288
551
  events: allEvents,
@@ -311,12 +574,29 @@ function buildMessageGroups(messages) {
311
574
  activeGroups: all.filter((message) => ["queued", "dispatching", "transport_written", "held"].includes(message.state)).length,
312
575
  };
313
576
  }
314
- export function buildDashboardViewModel(snapshot) {
577
+ function buildProjectedDashboardViewModel(snapshot, includeBodies, options) {
578
+ const surfaceDefinitions = options.compatibilitySurfaceDefinitions ?? compatibilitySurfaceDefinitions;
315
579
  const generatedAt = normalizedTimestamp(snapshot.generatedAt);
316
580
  const inboundMode = snapshot.inboundMode === "open" ? "open" : "paired";
317
581
  const validPeers = arePublicAvailablePeerSnapshots(snapshot.availablePeers)
318
582
  ? snapshot.availablePeers
319
583
  : [];
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
+ ]);
320
600
  const peers = validPeers
321
601
  .map((peer) => ({
322
602
  alias: boundedText(peer.alias),
@@ -403,7 +683,11 @@ export function buildDashboardViewModel(snapshot) {
403
683
  claudeAlias,
404
684
  codexAlias,
405
685
  host: boundedText(pair.host),
406
- state: bothPresent && routeIsReady(claudeRoute) && routeIsReady(codexRoute)
686
+ state: bothPresent &&
687
+ !quarantinedProviders.has("claude") &&
688
+ !quarantinedProviders.has("codex") &&
689
+ routeIsReady(claudeRoute) &&
690
+ routeIsReady(codexRoute)
407
691
  ? "ready"
408
692
  : bothPresent
409
693
  ? "degraded"
@@ -425,9 +709,9 @@ export function buildDashboardViewModel(snapshot) {
425
709
  generatedAtMs === undefined) {
426
710
  return [];
427
711
  }
428
- const idleMs = normalizedInteger(watch.idleMs);
429
- if (idleMs === undefined ||
430
- (watch.nudgeCount !== 0 && watch.nudgeCount !== 1 && watch.nudgeCount !== 2)) {
712
+ if (watch.nudgeCount !== 0 &&
713
+ watch.nudgeCount !== 1 &&
714
+ watch.nudgeCount !== 2) {
431
715
  return [];
432
716
  }
433
717
  return [
@@ -435,11 +719,8 @@ export function buildDashboardViewModel(snapshot) {
435
719
  conversationIdSuffix: watch.conversationIdSuffix,
436
720
  ownerAlias: boundedText(watch.ownerAlias),
437
721
  workerAlias: boundedText(watch.workerAlias),
438
- phase: watch.phase,
439
- capability: watch.capability,
440
722
  lastActivityAt,
441
723
  nextActionAt,
442
- idleMs,
443
724
  idleForMs: Math.max(0, generatedAtMs - Date.parse(lastActivityAt)),
444
725
  dueInMs: Math.max(0, Date.parse(nextActionAt) - generatedAtMs),
445
726
  nudgeCount: watch.nudgeCount,
@@ -466,37 +747,52 @@ export function buildDashboardViewModel(snapshot) {
466
747
  ownerAlias: boundedText(event.ownerAlias),
467
748
  workerAlias: boundedText(event.workerAlias),
468
749
  kind: event.kind,
469
- ...(event.nudgeNumber === undefined
470
- ? {}
471
- : { nudgeNumber: event.nudgeNumber }),
750
+ actor: event.actor,
751
+ ...(event.reason === undefined ? {} : { reason: event.reason }),
472
752
  },
473
753
  ];
474
754
  })
475
755
  .sort((left, right) => right.sequence - left.sequence);
476
756
  const watchEvents = allWatchEvents.slice(0, DASHBOARD_MODEL_LIMITS.progressWatchEvents);
477
757
  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
- }))
758
+ .map((connector) => {
759
+ const registry = isPublicRegistryObservationSnapshot(connector.registry)
760
+ ? connector.registry
761
+ : undefined;
762
+ return {
763
+ provider: connector.provider,
764
+ host: boundedText(connector.host),
765
+ health: connector.health,
766
+ compatibility: connector.compatibility,
767
+ ...(safeProtocol(connector.protocol) === undefined
768
+ ? {}
769
+ : { protocol: safeProtocol(connector.protocol) }),
770
+ ...(safeProtocol(connector.protocolVersion) === undefined
771
+ ? {}
772
+ : { protocolVersion: safeProtocol(connector.protocolVersion) }),
773
+ ...(normalizedTimestamp(connector.lastSeenAt) === undefined
774
+ ? {}
775
+ : { lastSeenAt: normalizedTimestamp(connector.lastSeenAt) }),
776
+ ...(safeCode(connector.safeErrorCode) === undefined
777
+ ? {}
778
+ : { safeErrorCode: safeCode(connector.safeErrorCode) }),
779
+ ...(registry === undefined
780
+ ? {}
781
+ : {
782
+ registry: {
783
+ entriesScanned: registry.entriesScanned,
784
+ parseableRecords: registry.parseableRecords,
785
+ parseableRecordSeenSinceBoot: registry.parseableRecordSeenSinceBoot,
786
+ rejected: registry.rejected.map((row) => ({ ...row })),
787
+ rejectedCodesOmitted: registry.rejectedCodesOmitted,
788
+ },
789
+ }),
790
+ };
791
+ })
496
792
  .sort((left, right) => compareText(`${left.provider}\0${left.host}`, `${right.provider}\0${right.host}`))
497
793
  .slice(0, DASHBOARD_MODEL_LIMITS.connectors);
498
- const compatibilityChecks = (Array.isArray(snapshot.compatibilityChecks)
499
- ? snapshot.compatibilityChecks.filter(isCompatibilityAttestation)
794
+ const detectedCompatibilityChecks = (Array.isArray(snapshot.compatibilityChecks)
795
+ ? snapshot.compatibilityChecks.filter(isPublicCompatibilityCheckSnapshot)
500
796
  : [])
501
797
  .map((attestation) => {
502
798
  const failed = attestation.probes.find((probe) => probe.outcome === "fail");
@@ -504,15 +800,30 @@ export function buildDashboardViewModel(snapshot) {
504
800
  return {
505
801
  surface: attestation.surface,
506
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) }),
507
809
  tier: attestation.tier,
810
+ writesCovered: attestation.writesCovered,
508
811
  checkedAt: attestation.checkedAt,
509
812
  ...(failed === undefined ? {} : { failure: failed.name }),
510
813
  ...(issue === undefined ? {} : { safeErrorCode: issue }),
511
814
  };
512
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
+ ]
513
824
  .sort((left, right) => compareText(left.surface, right.surface))
514
- .slice(0, 2);
515
- const messages = buildMessageGroups(snapshot.messages);
825
+ .slice(0, surfaceDefinitions.length);
826
+ const messages = buildMessageGroups(snapshot.messages, includeBodies);
516
827
  const queuedMessages = allRoutes.reduce((total, route) => boundedAdd(total, route.queueDepth), 0);
517
828
  const queuedWithAge = allRoutes
518
829
  .filter((route) => route.oldestQueuedAt !== undefined && route.queueAgeMs !== undefined)
@@ -521,52 +832,68 @@ export function buildDashboardViewModel(snapshot) {
521
832
  const activeDeliveries = messages.activeGroups;
522
833
  const claudeRoutes = allRoutes.filter((route) => route.provider === "claude");
523
834
  const codexRoutes = allRoutes.filter((route) => route.provider === "codex");
524
- const readyClaude = claudeRoutes.filter(routeIsReady).length;
525
- const readyCodex = codexRoutes.filter(routeIsReady).length;
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;
526
839
  const readyPairs = allPairs.filter((pair) => pair.state === "ready");
527
840
  const pairedClaude = new Set(allPairs.map((pair) => pair.claudeAlias));
528
841
  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));
842
+ const readyPairedClaude = claudeRoutes.some((route) => routeIsExchangeReady(route) && pairedClaude.has(route.alias));
843
+ const readyPairedCodex = codexRoutes.some((route) => routeIsExchangeReady(route) && pairedCodex.has(route.alias));
531
844
  const pairCountIsLowerBound = (normalizedInteger(snapshot.truncation.pairs) ?? 0) > 0;
532
845
  const hasPairEvidence = allPairs.length > 0 || pairCountIsLowerBound;
533
846
  const graph = {
534
847
  pairCount: allPairs.length,
535
848
  readyPairCount: readyPairs.length,
536
849
  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,
850
+ unpairedReadyClaude: claudeRoutes.filter((route) => routeIsExchangeReady(route) && !pairedClaude.has(route.alias)).length,
851
+ unpairedReadyCodex: codexRoutes.filter((route) => routeIsExchangeReady(route) && !pairedCodex.has(route.alias)).length,
539
852
  };
540
853
  const selectedPeers = validPeers.filter((peer) => peer.selected && peerIsSelectable(peer));
541
854
  const selectablePeers = validPeers.filter(peerIsSelectable);
542
- const selectedClaudeCount = Math.max(selectedPeers.length, readyClaude);
855
+ const selectedClaudeCount = quarantinedProviders.has("claude")
856
+ ? 0
857
+ : Math.max(selectedPeers.length, readyClaude);
543
858
  const claudeStatus = partyStatus(claudeRoutes, readyClaude);
544
859
  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";
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";
566
885
  const explicitAlerts = coalesceCodexReactivationAlerts(snapshot.alerts
567
886
  .filter((alert) => alert.code !== "COMPATIBILITY_CERTIFICATION_FAILED")
568
887
  .map((alert) => {
569
888
  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)
893
+ : undefined;
894
+ const alertRoute = typeof alert.alias === "string"
895
+ ? routeByAlias.get(alert.alias)
896
+ : undefined;
570
897
  return {
571
898
  kind: "alert",
572
899
  ...(code === undefined ? {} : { code }),
@@ -587,7 +914,26 @@ export function buildDashboardViewModel(snapshot) {
587
914
  ...(typeof alert.host === "string"
588
915
  ? { host: boundedText(alert.host) }
589
916
  : {}),
590
- guidance: guidanceFor(code, alert.provider),
917
+ ...(alert.provider === "codex" &&
918
+ typeof alert.host === "string" &&
919
+ route !== undefined &&
920
+ alert.host === route.host &&
921
+ route.provider === "codex" &&
922
+ route.state === "busy" &&
923
+ route.queueDepth > 0
924
+ ? { queueDepth: route.queueDepth }
925
+ : {}),
926
+ guidance: alertRoute !== undefined &&
927
+ needsCodexAppReconnect(alertRoute, connectors, generatedAt)
928
+ ? "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),
591
937
  };
592
938
  }), allRoutes).sort((left, right) => alertPriority(left.severity) - alertPriority(right.severity) ||
593
939
  compareText(right.timestamp ?? "", left.timestamp ?? ""));
@@ -649,13 +995,11 @@ export function buildDashboardViewModel(snapshot) {
649
995
  attentionCandidates[existingIndex] = item;
650
996
  }
651
997
  for (const watch of allWatches) {
652
- if (watch.phase !== "episode" && watch.capability !== "route")
998
+ if (watch.nudgeCount === 0)
653
999
  continue;
654
1000
  attentionCandidates.push({
655
1001
  kind: "watch",
656
- code: watch.capability === "route"
657
- ? "PROGRESS_WATCH_CAPABILITY_DEGRADED"
658
- : "PROGRESS_WATCH_QUIET",
1002
+ code: "PROGRESS_WATCH_QUIET",
659
1003
  severity: "warning",
660
1004
  timestamp: watch.lastActivityAt,
661
1005
  alias: watch.workerAlias,
@@ -664,7 +1008,9 @@ export function buildDashboardViewModel(snapshot) {
664
1008
  }
665
1009
  const representedScopes = new Set(attentionCandidates.map((item) => `${item.provider ?? ""}\0${item.alias ?? ""}\0${item.host ?? ""}`));
666
1010
  for (const route of allRoutes) {
667
- if (routeIsReady(route)) {
1011
+ if (routeIsReady(route) ||
1012
+ quarantinedProviders.has(route.provider) ||
1013
+ route.safeErrorCode === "CODEX_WRITES_DISABLED") {
668
1014
  continue;
669
1015
  }
670
1016
  const scope = `${route.provider}\0${route.alias}\0${route.host}`;
@@ -684,9 +1030,11 @@ export function buildDashboardViewModel(snapshot) {
684
1030
  route.state === "stale" &&
685
1031
  route.safeErrorCode === "REOBSERVATION_REQUIRED"
686
1032
  ? "codex_reactivation_required"
687
- : route.provider === "codex" && route.state === "stale"
688
- ? "codex_stale"
689
- : "route_stale",
1033
+ : needsCodexAppReconnect(route, connectors, generatedAt)
1034
+ ? "codex_app_reconnect_required"
1035
+ : route.provider === "codex" && route.state === "stale"
1036
+ ? "codex_stale"
1037
+ : "route_stale",
690
1038
  });
691
1039
  representedScopes.add(scope);
692
1040
  }
@@ -709,13 +1057,65 @@ export function buildDashboardViewModel(snapshot) {
709
1057
  : { timestamp: connector.lastSeenAt }),
710
1058
  provider: connector.provider,
711
1059
  host: connector.host,
712
- guidance: connector.health === "offline" ? "connector_offline" : "degraded",
1060
+ guidance: connector.health === "offline"
1061
+ ? "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",
713
1073
  });
714
1074
  representedScopes.add(scope);
715
1075
  }
1076
+ for (const connector of connectors) {
1077
+ const observation = connector.registry;
1078
+ if (observation === undefined)
1079
+ continue;
1080
+ if (observation.rejected.length > 0 ||
1081
+ observation.rejectedCodesOmitted > 0) {
1082
+ const item = {
1083
+ kind: "connector",
1084
+ code: observation.rejected.some((row) => row.safeErrorCode === "CLAUDE_REGISTRY_UNAVAILABLE")
1085
+ ? "CLAUDE_REGISTRY_UNAVAILABLE"
1086
+ : "CLAUDE_REGISTRY_RECORDS_REJECTED",
1087
+ severity: "warning",
1088
+ provider: "claude",
1089
+ host: connector.host,
1090
+ guidance: "registry_rejected",
1091
+ };
1092
+ const duplicateIndex = attentionCandidates.findIndex((candidate) => candidate.code === item.code &&
1093
+ candidate.provider === item.provider &&
1094
+ candidate.alias === item.alias &&
1095
+ candidate.host === item.host);
1096
+ if (duplicateIndex === -1)
1097
+ attentionCandidates.push(item);
1098
+ else
1099
+ attentionCandidates[duplicateIndex] = item;
1100
+ continue;
1101
+ }
1102
+ if (observation.parseableRecordSeenSinceBoot)
1103
+ continue;
1104
+ attentionCandidates.push({
1105
+ kind: "connector",
1106
+ code: observation.entriesScanned === 0
1107
+ ? "CLAUDE_REGISTRY_EMPTY_SINCE_BOOT"
1108
+ : "CLAUDE_REGISTRY_NO_PARSEABLE_RECORD_SINCE_BOOT",
1109
+ severity: "warning",
1110
+ provider: "claude",
1111
+ host: connector.host,
1112
+ guidance: "registry_empty",
1113
+ });
1114
+ }
716
1115
  if ((snapshot.health === "degraded" ||
717
1116
  snapshot.health === "offline" ||
718
1117
  snapshot.health === "incompatible") &&
1118
+ !(snapshot.health === "degraded" && quarantinedProviders.size > 0) &&
719
1119
  !attentionCandidates.some((item) => item.kind === "broker")) {
720
1120
  attentionCandidates.push({
721
1121
  kind: "broker",
@@ -778,6 +1178,9 @@ export function buildDashboardViewModel(snapshot) {
778
1178
  status: codexStatus,
779
1179
  total: codexRoutes.length,
780
1180
  ready: readyCodex,
1181
+ ...(monitorOnlyCodexRoutes.length === 0
1182
+ ? {}
1183
+ : { monitorOnly: monitorOnlyCodexRoutes.length }),
781
1184
  countIsLowerBound: (normalizedInteger(snapshot.truncation.routes) ?? 0) > 0,
782
1185
  ...(codexRoutes[0]?.alias === undefined
783
1186
  ? {}