agent-embassy 1.4.1 → 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 +22 -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 +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 +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
@@ -9,7 +9,7 @@ import { createCodexRegistrationSuccession, transitionCodexRegistrationSuccessio
9
9
  import { createCodexRegistrationGeneration, isCodexRegistrationGeneration, } from "./codex-registration-generation.js";
10
10
  import { PROGRESS_WATCH_DEFAULT_IDLE_MS } from "./progress-watch-machine.js";
11
11
  import { GatewayStore, } from "./store.js";
12
- import { GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, arePublicAvailablePeerSnapshots, projectGatewayPublicSnapshot, } from "./types.js";
12
+ import { GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayPublicSnapshotLimits, arePublicAvailablePeerSnapshots, projectPublicCompatibilityCheck, projectGatewayPublicSnapshot, } from "./types.js";
13
13
  const PUBLIC_ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
14
14
  const CLAUDE_SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
15
15
  const SAFE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
@@ -22,10 +22,37 @@ const NATIVE_HELD_THRESHOLD_MS = 1_000;
22
22
  const RUNTIME_ACTIVITY_CAPACITY = 256;
23
23
  const DELIVERY_SETTLEMENT_RETRY_MS = 250;
24
24
  const DELIVERY_DASHBOARD_REFRESH_MS = 15_000;
25
+ const BOOT_REACTIVATION_TRUST_FAILURE_CODES = new Set([
26
+ "HOME_INVALID",
27
+ "MANAGED_CODEX_INVALID",
28
+ "LOCAL_APP_SERVER_ENDPOINT_UNSAFE",
29
+ "APP_SERVER_VERSION_MISMATCH",
30
+ "ENDPOINT_GENERATION_CHANGED",
31
+ "CODEX_ENDPOINT_GENERATION_CHURN",
32
+ "CODEX_FACTORY_ATTESTATION_INVALID",
33
+ "CLEANUP_FAILED",
34
+ "FACTORY_CLOSED",
35
+ "INVALID_CONFIGURATION",
36
+ "CLAUDE_PEER_CALLBACK_UNSAFE",
37
+ "CODEX_ROUTE_CLEANUP_FAILED",
38
+ "CODEX_PROVIDER_CLEANUP_FAILED",
39
+ "CODEX_PROVIDER_CLEANUP_AMBIGUOUS",
40
+ "CODEX_PROVIDER_CLEANUP_TIMEOUT",
41
+ ]);
42
+ function boundedErrorCode(error) {
43
+ if (typeof error !== "object" ||
44
+ error === null ||
45
+ !("code" in error) ||
46
+ typeof error.code !== "string" ||
47
+ !SAFE_CODE.test(error.code)) {
48
+ return undefined;
49
+ }
50
+ return error.code;
51
+ }
25
52
  function progressWatchActivity(text, conversationId, actorAlias, explicitIdleMinutes) {
26
53
  const prefixedOpen = text.startsWith("TRACK:");
27
54
  const completionSignal = text.startsWith("DONE:");
28
- if (completionSignal && (prefixedOpen || explicitIdleMinutes !== undefined)) {
55
+ if (completionSignal && explicitIdleMinutes !== undefined) {
29
56
  throw new BridgeError("PROGRESS_WATCH_SIGNAL_CONFLICT", "A single message cannot both open and complete a progress watch.");
30
57
  }
31
58
  const openIdleMs = explicitIdleMinutes === undefined
@@ -133,6 +160,8 @@ export class GatewayService {
133
160
  bindingAliases = new Map();
134
161
  candidates = new Map();
135
162
  availablePeers = [];
163
+ registryEvidenceByConnector = new Map();
164
+ registryAdaptersWithParseableRecords = new Set();
136
165
  conversations = new Map();
137
166
  messageContexts = new Map();
138
167
  providerTurnContinuations = new Map();
@@ -145,6 +174,8 @@ export class GatewayService {
145
174
  deliveryTokens = new Map();
146
175
  runtimeAlerts = [];
147
176
  activityEvents = [];
177
+ /** Startup probe failures that must preflight cross-provider mutations. */
178
+ incompatibleProviderSurfaces = new Map();
148
179
  activeCodexIncompatibility;
149
180
  activitySequence = 0;
150
181
  detachedReceiptWrites = new Set();
@@ -260,6 +291,12 @@ export class GatewayService {
260
291
  throw new BridgeError("GATEWAY_ADAPTER_NOT_ALLOWED", "A provider adapter is duplicated or outside the host allowlist.");
261
292
  }
262
293
  seen.add(key);
294
+ }
295
+ if (this.adapters.length > 0) {
296
+ await this.runAutomaticCompatibilityProbesLocked();
297
+ assertStartActive();
298
+ }
299
+ for (const adapter of this.adapters) {
263
300
  const observation = await adapter.initialize(this.callbacksFor(adapter));
264
301
  assertStartActive();
265
302
  await this.store.observeConnector({
@@ -274,15 +311,6 @@ export class GatewayService {
274
311
  });
275
312
  assertStartActive();
276
313
  }
277
- if (this.adapters.length > 0 &&
278
- this.adapters.every((adapter) => adapter.compatibilitySurface !== undefined &&
279
- adapter.runCompatibilityProbes !== undefined)) {
280
- const attestations = await this.runAutomaticCompatibilityProbesLocked();
281
- const incompatible = attestations.find((surface) => surface.tier === "incompatible");
282
- if (incompatible !== undefined) {
283
- throw new BridgeError(incompatible.safeErrorCode ?? "COMPATIBILITY_CHECK_FAILED", "A local provider failed bounded compatibility attestation.");
284
- }
285
- }
286
314
  await this.recoverCodexSuccessionAfterRestartLocked();
287
315
  bootReactivatedIdleAliases =
288
316
  await this.reactivateRetainedCodexRoutesAfterRestartLocked();
@@ -338,7 +366,7 @@ export class GatewayService {
338
366
  try {
339
367
  await control.close();
340
368
  }
341
- catch {
369
+ catch (error) {
342
370
  closeFailed = true;
343
371
  }
344
372
  }
@@ -404,7 +432,7 @@ export class GatewayService {
404
432
  await this.drainDetachedReceiptWritesLocked();
405
433
  await this.publish();
406
434
  }
407
- catch {
435
+ catch (error) {
408
436
  closeFailed = true;
409
437
  }
410
438
  const adapterResults = await Promise.allSettled(this.adapters.map(async (adapter) => adapter.close()));
@@ -427,6 +455,9 @@ export class GatewayService {
427
455
  this.deliveryTrackers.clear();
428
456
  this.deliveryTokens.clear();
429
457
  this.runtimeAlerts.length = 0;
458
+ this.incompatibleProviderSurfaces.clear();
459
+ this.registryEvidenceByConnector.clear();
460
+ this.registryAdaptersWithParseableRecords.clear();
430
461
  this.detachedReceiptWrites.clear();
431
462
  this.nativeIngressByConversation.clear();
432
463
  this.callbackQueue.length = 0;
@@ -511,10 +542,7 @@ export class GatewayService {
511
542
  observeSnapshot: async () => await this.observeSnapshot(),
512
543
  deliveryStatus: async (params) => await this.deliveryStatus(params.token),
513
544
  untrack: async (params) => await this.exclusiveDecision(async () => {
514
- const settled = await this.store.settleProgressWatch({
515
- conversationId: params.conversationId,
516
- outcome: "done",
517
- });
545
+ const settled = await this.store.endProgressWatch(params.conversationId);
518
546
  if (!settled) {
519
547
  throw new BridgeError("PROGRESS_WATCH_NOT_FOUND", "No active progress watch matches that conversation token.");
520
548
  }
@@ -559,6 +587,7 @@ export class GatewayService {
559
587
  throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "A required compatibility surface is unavailable.");
560
588
  }
561
589
  const probes = await entry.adapter.runCompatibilityProbes();
590
+ this.captureRegistryObservation(entry.adapter, entry.adapter.latestRegistryObservation?.());
562
591
  const attestation = evaluateCompatibilityAttestation({
563
592
  surface,
564
593
  version: entry.observation.version,
@@ -567,13 +596,82 @@ export class GatewayService {
567
596
  probes,
568
597
  });
569
598
  await this.store.recordCompatibilityAttestation(attestation);
570
- if (attestation.tier !== "incompatible") {
571
- entry.adapter.acceptCompatibilityAttestation?.(attestation);
599
+ entry.adapter.acceptCompatibilityAttestation(attestation);
600
+ if (attestation.tier === "incompatible") {
601
+ const code = attestation.safeErrorCode ?? "COMPATIBILITY_CHECK_FAILED";
602
+ this.incompatibleProviderSurfaces.set(this.providerSurfaceKey(surface, entry.adapter.identity.hostId), code);
603
+ if (surface === "codex") {
604
+ this.activeCodexIncompatibility = structuredClone(attestation);
605
+ }
606
+ }
607
+ else if (surface === "codex" && attestation.tier === "schema_attested") {
608
+ // A live but untested App Server schema is useful for observation only.
609
+ // Fence retained boot reactivation before it can trigger endpoint
610
+ // refresh churn; an explicit registration may still re-probe a later,
611
+ // certified generation through the provider's bounded refresh path.
612
+ this.incompatibleProviderSurfaces.set(this.providerSurfaceKey("codex", entry.adapter.identity.hostId), "CODEX_MONITOR_ONLY");
572
613
  }
573
614
  attestations.push(attestation);
574
615
  }
575
616
  return attestations;
576
617
  }
618
+ captureRegistryObservation(adapter, observation) {
619
+ if (adapter.identity.provider !== "claude" ||
620
+ observation === undefined ||
621
+ !Number.isSafeInteger(observation.entriesScanned) ||
622
+ observation.entriesScanned < 0 ||
623
+ !Number.isSafeInteger(observation.parseableRecords) ||
624
+ observation.parseableRecords < 0 ||
625
+ observation.parseableRecords > observation.entriesScanned ||
626
+ !Array.isArray(observation.rejected)) {
627
+ return;
628
+ }
629
+ const counts = new Map();
630
+ for (const row of observation.rejected) {
631
+ if (!row ||
632
+ typeof row !== "object" ||
633
+ typeof row.safeErrorCode !== "string" ||
634
+ !SAFE_CODE.test(row.safeErrorCode) ||
635
+ !Number.isSafeInteger(row.count) ||
636
+ row.count <= 0) {
637
+ continue;
638
+ }
639
+ const next = (counts.get(row.safeErrorCode) ?? 0) + row.count;
640
+ counts.set(row.safeErrorCode, Math.min(Number.MAX_SAFE_INTEGER, next));
641
+ }
642
+ const ordered = [...counts]
643
+ .sort(([left], [right]) => left.localeCompare(right))
644
+ .map(([safeErrorCode, count]) => ({ safeErrorCode, count }));
645
+ const rejected = ordered.slice(0, gatewayPublicSnapshotLimits.registryRejectionCodes);
646
+ if (observation.parseableRecords > 0) {
647
+ this.registryAdaptersWithParseableRecords.add(adapter);
648
+ }
649
+ const hasObservedParseableRecord = this.registryAdaptersWithParseableRecords.has(adapter);
650
+ const row = {
651
+ entriesScanned: observation.entriesScanned,
652
+ parseableRecords: observation.parseableRecords,
653
+ parseableRecordSeenSinceBoot: hasObservedParseableRecord,
654
+ rejected,
655
+ rejectedCodesOmitted: Math.max(0, ordered.length - rejected.length),
656
+ };
657
+ this.registryEvidenceByConnector.set(`${adapter.identity.provider}\0${adapter.identity.hostId}`, row);
658
+ }
659
+ connectorsWithRegistry(connectors) {
660
+ return connectors.map((connector) => {
661
+ const registry = this.registryEvidenceByConnector.get(`${connector.provider}\0${connector.host}`);
662
+ return {
663
+ ...connector,
664
+ ...(connector.provider !== "claude" || registry === undefined
665
+ ? {}
666
+ : {
667
+ registry: {
668
+ ...registry,
669
+ rejected: registry.rejected.map((row) => ({ ...row })),
670
+ },
671
+ }),
672
+ };
673
+ });
674
+ }
577
675
  async observeSnapshot() {
578
676
  return await this.mutex.run("service", async () => {
579
677
  const changed = await this.processLifecycleLocked();
@@ -1149,6 +1247,23 @@ export class GatewayService {
1149
1247
  throw new BridgeError("PROVIDER_UNAVAILABLE", "The selected provider is unavailable.", true);
1150
1248
  return adapter;
1151
1249
  }
1250
+ providerSurfaceKey(provider, host) {
1251
+ return `${provider}@${host}`;
1252
+ }
1253
+ incompatibleProviderCode(provider, host) {
1254
+ return this.incompatibleProviderSurfaces.get(this.providerSurfaceKey(provider, host));
1255
+ }
1256
+ /** Reject before either half of a cross-provider mutation can acquire state. */
1257
+ assertCrossProviderMutationCompatible(host, options = { allowCodexRefresh: false }) {
1258
+ const claudeCode = this.incompatibleProviderCode("claude", host);
1259
+ if (claudeCode !== undefined) {
1260
+ throw new BridgeError(claudeCode, "The Claude provider is observation-only until Embassy restarts with a compatible runtime.");
1261
+ }
1262
+ const codexCode = this.incompatibleProviderCode("codex", host);
1263
+ if (codexCode !== undefined && !options.allowCodexRefresh) {
1264
+ throw new BridgeError(codexCode, "The Codex provider is observation-only until a compatible endpoint is re-observed.");
1265
+ }
1266
+ }
1152
1267
  async assertClaudeWorkspaceDisjoint(adapter, routeHandle) {
1153
1268
  if (adapter.identity.provider !== "claude") {
1154
1269
  throw new BridgeError("CLAUDE_PROVIDER_MISMATCH", "The selected workspace guard does not belong to Claude.");
@@ -1219,6 +1334,13 @@ export class GatewayService {
1219
1334
  this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
1220
1335
  continue;
1221
1336
  }
1337
+ if (this.incompatibleProviderCode("codex", route.binding.hostId) !==
1338
+ undefined ||
1339
+ this.incompatibleProviderCode("claude", route.binding.hostId) !==
1340
+ undefined) {
1341
+ this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
1342
+ continue;
1343
+ }
1222
1344
  let routeSelected = false;
1223
1345
  let advertiseAttempted = false;
1224
1346
  let storeReactivated = false;
@@ -1308,7 +1430,7 @@ export class GatewayService {
1308
1430
  if (selected.state === "idle")
1309
1431
  idleAliases.push(route.alias);
1310
1432
  }
1311
- catch {
1433
+ catch (error) {
1312
1434
  let cleanupFailed = false;
1313
1435
  if (advertiseAttempted) {
1314
1436
  await claudeAdapter
@@ -1327,6 +1449,11 @@ export class GatewayService {
1327
1449
  if (cleanupFailed || storeReactivated || deferredEndpointRefresh) {
1328
1450
  throw new BridgeError("CODEX_BOOT_REACTIVATION_CLEANUP_FAILED", "A Codex boot reactivation crossed its durable boundary without exact live authority.");
1329
1451
  }
1452
+ const failureCode = boundedErrorCode(error);
1453
+ if (failureCode !== undefined &&
1454
+ BOOT_REACTIVATION_TRUST_FAILURE_CODES.has(failureCode)) {
1455
+ throw error;
1456
+ }
1330
1457
  this.addRuntimeAlert("CODEX_BOOT_REACTIVATION_SKIPPED", "warning", { provider: "codex", host: route.binding.hostId, alias: route.alias });
1331
1458
  }
1332
1459
  }
@@ -2119,6 +2246,9 @@ export class GatewayService {
2119
2246
  if (!params.alias.startsWith("codex-")) {
2120
2247
  throw new BridgeError("INVALID_CODEX_PEER_ALIAS", "A registered Codex alias must start with codex-.");
2121
2248
  }
2249
+ this.assertCrossProviderMutationCompatible(params.hostId, {
2250
+ allowCodexRefresh: params.succeedsAlias === undefined,
2251
+ });
2122
2252
  if (this.codexSuccessionPoisoned.has(params.alias) ||
2123
2253
  this.codexOrphanCleanupPending.has(params.alias) ||
2124
2254
  (params.succeedsAlias !== undefined &&
@@ -2338,7 +2468,7 @@ export class GatewayService {
2338
2468
  unwrittenOutcome: "cancelled",
2339
2469
  safeErrorCode: "ROUTE_UNREGISTERED",
2340
2470
  });
2341
- const settlements = await this.store.unregisterRoute(params.alias, lease, inFlightSettlements);
2471
+ const settlements = await this.store.unregisterRoute(params.alias, lease, inFlightSettlements, "operator");
2342
2472
  for (const settlement of settlements) {
2343
2473
  await this.applyTerminalSettlementLocked(settlement);
2344
2474
  }
@@ -2573,6 +2703,7 @@ export class GatewayService {
2573
2703
  const base = await this.store.publicSnapshot();
2574
2704
  const snapshot = projectGatewayPublicSnapshot({
2575
2705
  ...base,
2706
+ connectors: this.connectorsWithRegistry(base.connectors),
2576
2707
  availablePeers: this.availablePeers.map((peer) => ({ ...peer })),
2577
2708
  });
2578
2709
  return JSON.stringify({
@@ -2607,6 +2738,7 @@ export class GatewayService {
2607
2738
  // authority. The provider still revalidates the exact target before
2608
2739
  // any write.
2609
2740
  }
2741
+ this.captureRegistryObservation(adapter, discovered.registry ?? adapter.latestRegistryObservation?.());
2610
2742
  const grouped = new Map();
2611
2743
  const byHandle = new Map();
2612
2744
  for (const peer of discovered.peers) {
@@ -3094,6 +3226,32 @@ export class GatewayService {
3094
3226
  await this.selectAndPairClaude(params.alias, await this.inferCodexAlias(params.codexThreadId));
3095
3227
  }
3096
3228
  async selectAndPairClaude(selector, codexAlias) {
3229
+ const codexBinding = this.routeBindings.get(codexAlias);
3230
+ const retainedCodexRoute = codexBinding?.provider === "codex"
3231
+ ? undefined
3232
+ : await this.store.inspectPrivateRoute(codexAlias);
3233
+ const codexHost = codexBinding?.provider === "codex"
3234
+ ? codexBinding.hostId
3235
+ : retainedCodexRoute?.binding.provider === "codex"
3236
+ ? retainedCodexRoute.binding.hostId
3237
+ : undefined;
3238
+ if (codexHost !== undefined) {
3239
+ this.assertCrossProviderMutationCompatible(codexHost);
3240
+ }
3241
+ const codexAdapter = codexBinding?.provider === "codex"
3242
+ ? this.adapters.find((adapter) => adapter.identity.provider === "codex" &&
3243
+ adapter.identity.hostId === codexBinding.hostId)
3244
+ : undefined;
3245
+ const codexState = this.routeStates.get(codexAlias);
3246
+ if (codexBinding?.provider !== "codex" ||
3247
+ codexAdapter === undefined ||
3248
+ codexAdapter.identity.endpointGeneration !==
3249
+ codexBinding.endpointGeneration ||
3250
+ (codexState !== "idle" &&
3251
+ codexState !== "busy" &&
3252
+ codexState !== "awaiting_approval")) {
3253
+ throw new BridgeError("CODEX_ROUTE_UNAVAILABLE", "Claude selection requires an exact positively observed live Codex route.");
3254
+ }
3097
3255
  const discoveryChanged = await this.refreshClaudeDiscovery();
3098
3256
  const candidate = this.claudeCandidate(selector);
3099
3257
  if (candidate === undefined)
@@ -3924,14 +4082,7 @@ export class GatewayService {
3924
4082
  return false;
3925
4083
  }
3926
4084
  const actions = await this.store.advanceDueProgressWatches();
3927
- let changed = actions.length > 0;
3928
4085
  for (const action of actions) {
3929
- if (action.type === "settled") {
3930
- this.addRuntimeAlert(action.outcome === "unresponsive"
3931
- ? "PROGRESS_WATCH_UNRESPONSIVE"
3932
- : "PROGRESS_WATCH_ENDPOINT_RETIRED", action.outcome === "unresponsive" ? "error" : "warning", { alias: action.ownerAlias });
3933
- continue;
3934
- }
3935
4086
  const text = `[Embassy automated liveness check — ${action.conversationId} / ` +
3936
4087
  `Embassy 自动活跃检查 — ${action.conversationId}]\n` +
3937
4088
  "Reply with the result or a status. / 请回复结果或状态。";
@@ -3945,20 +4096,13 @@ export class GatewayService {
3945
4096
  nudgeNumber: action.nudgeNumber,
3946
4097
  },
3947
4098
  });
3948
- this.addRuntimeAlert("PROGRESS_WATCH_NUDGE", "warning", {
3949
- alias: action.ownerAlias,
3950
- });
3951
4099
  }
3952
4100
  catch {
3953
- await this.store.deferProgressWatchNudge(action.conversationId);
3954
- this.addRuntimeAlert("PROGRESS_WATCH_NUDGE_DEFERRED", "warning", {
3955
- alias: action.ownerAlias,
3956
- });
3957
- changed = true;
4101
+ await this.store.deferProgressWatchNudge(action.conversationId, action.nudgeNumber);
3958
4102
  }
3959
4103
  }
3960
4104
  await this.refreshNextProgressWatchWakeLocked();
3961
- return changed;
4105
+ return actions.length > 0;
3962
4106
  }
3963
4107
  async processLifecycleLocked() {
3964
4108
  const now = this.now().getTime();
@@ -4253,6 +4397,9 @@ export class GatewayService {
4253
4397
  const conversationId = options.existingConversationId ?? createGatewayConversationId();
4254
4398
  const conversation = this.conversations.get(conversationId) ?? {
4255
4399
  id: conversationId,
4400
+ ...(options.existingConversationId === undefined
4401
+ ? {}
4402
+ : { dedupeScope: createGatewayConversationId() }),
4256
4403
  sourceAlias,
4257
4404
  targetAlias,
4258
4405
  expectsReply,
@@ -4269,7 +4416,7 @@ export class GatewayService {
4269
4416
  sourceAlias,
4270
4417
  targetAlias,
4271
4418
  body: text,
4272
- dedupeKey: `${conversationId}:${sequence}`,
4419
+ dedupeKey: `${conversation.dedupeScope ?? conversationId}:${sequence}`,
4273
4420
  conversationIdSuffix: conversationId.slice(-8),
4274
4421
  deadlineAt,
4275
4422
  ...(options.steer === true ? { steer: true } : {}),
@@ -4348,7 +4495,7 @@ export class GatewayService {
4348
4495
  binding: capability.binding,
4349
4496
  },
4350
4497
  body: text,
4351
- dedupeKey: `${conversation.id}:${sequence}`,
4498
+ dedupeKey: `${conversation.dedupeScope ?? conversation.id}:${sequence}`,
4352
4499
  conversationIdSuffix: conversation.id.slice(-8),
4353
4500
  deadlineAt,
4354
4501
  ...(conversation.pair === true ? { pair: true } : {}),
@@ -4410,7 +4557,7 @@ export class GatewayService {
4410
4557
  },
4411
4558
  targetAlias: conversation.sourceAlias,
4412
4559
  body: text,
4413
- dedupeKey: `${conversation.id}:${sequence}`,
4560
+ dedupeKey: `${conversation.dedupeScope ?? conversation.id}:${sequence}`,
4414
4561
  conversationIdSuffix: conversation.id.slice(-8),
4415
4562
  deadlineAt,
4416
4563
  authorizedPairTeardownReply: true,
@@ -4525,14 +4672,17 @@ export class GatewayService {
4525
4672
  await this.publishNativeHeldLocked(afterCallbacks);
4526
4673
  }
4527
4674
  }
4528
- restoreQueuedMessageContextLocked(item, binding) {
4675
+ restoreQueuedMessageContextLocked(item, binding, canonicalConversationId) {
4529
4676
  this.pruneTransient();
4530
- const generatedConversationId = createGatewayConversationId();
4531
- const conversationId = item.conversationIdSuffix === undefined
4532
- ? generatedConversationId
4533
- : `${generatedConversationId.slice(0, -8)}${item.conversationIdSuffix}`;
4534
- const conversation = {
4677
+ const conversationId = canonicalConversationId ??
4678
+ (item.conversationIdSuffix === undefined
4679
+ ? createGatewayConversationId()
4680
+ : `${createGatewayConversationId().slice(0, -8)}${item.conversationIdSuffix}`);
4681
+ const conversation = this.conversations.get(conversationId) ?? {
4535
4682
  id: conversationId,
4683
+ ...(canonicalConversationId === undefined
4684
+ ? {}
4685
+ : { dedupeScope: createGatewayConversationId() }),
4536
4686
  sourceAlias: item.sourceAlias,
4537
4687
  targetAlias: item.targetAlias,
4538
4688
  expectsReply: false,
@@ -4589,6 +4739,7 @@ export class GatewayService {
4589
4739
  return;
4590
4740
  const selectedBinding = this.routeBindings.get(targetAlias);
4591
4741
  let context = this.messageContexts.get(item.messageId);
4742
+ let progressWatchActive;
4592
4743
  if (context === undefined && selectedBinding === undefined) {
4593
4744
  const result = await this.store.requeueInFlightMessage(item.messageId, item.body);
4594
4745
  if (result.status === "settled") {
@@ -4598,7 +4749,15 @@ export class GatewayService {
4598
4749
  return;
4599
4750
  }
4600
4751
  if (context === undefined && selectedBinding !== undefined) {
4601
- context = this.restoreQueuedMessageContextLocked(item, selectedBinding);
4752
+ const resolution = item.pair === true && item.conversationIdSuffix !== undefined
4753
+ ? await this.store.resolveProgressWatchDispatch({
4754
+ recoveredConversationIdSuffix: item.conversationIdSuffix,
4755
+ sourceAlias: item.sourceAlias,
4756
+ targetAlias: item.targetAlias,
4757
+ })
4758
+ : undefined;
4759
+ context = this.restoreQueuedMessageContextLocked(item, selectedBinding, resolution?.conversationId);
4760
+ progressWatchActive = resolution?.markerActive === true;
4602
4761
  }
4603
4762
  if (context === undefined) {
4604
4763
  const result = await this.store.settleMessage({
@@ -4741,23 +4900,16 @@ export class GatewayService {
4741
4900
  this.scheduleLifecycleWakeLocked();
4742
4901
  return;
4743
4902
  }
4744
- let progressWatches = await this.store.inspectProgressWatches();
4745
- if (context.recovered === true &&
4746
- item.pair === true &&
4747
- !item.body.startsWith("DONE:") &&
4748
- progressWatches.some((watch) => watch.capability === "route" &&
4749
- watch.ownerAlias === item.sourceAlias &&
4750
- watch.workerAlias === item.targetAlias)) {
4751
- await this.store.bindProgressWatchConversation({
4752
- conversationId: context.conversationId,
4753
- ownerAlias: item.sourceAlias,
4754
- workerAlias: item.targetAlias,
4755
- });
4756
- progressWatches = await this.store.inspectProgressWatches();
4903
+ if (progressWatchActive === undefined) {
4904
+ progressWatchActive =
4905
+ item.pair === true
4906
+ ? (await this.store.resolveProgressWatchDispatch({
4907
+ conversationId: context.conversationId,
4908
+ sourceAlias: item.sourceAlias,
4909
+ targetAlias: item.targetAlias,
4910
+ })).markerActive
4911
+ : false;
4757
4912
  }
4758
- const progressWatchActive = progressWatches.some((watch) => watch.capability === "conversation" &&
4759
- watch.workerAlias === item.targetAlias &&
4760
- watch.conversationId === context.conversationId);
4761
4913
  let result;
4762
4914
  try {
4763
4915
  result = await this.awaitDispatchWithNativeHeldLocked(tracker, this.adapter(binding.provider, binding.hostId).dispatch({
@@ -4771,6 +4923,9 @@ export class GatewayService {
4771
4923
  expectsReply: context.expectsReply,
4772
4924
  deadlineAt: item.deadlineAt,
4773
4925
  ...(item.steer === true ? { steer: true } : {}),
4926
+ ...(item.steer === true && item.queuedAhead !== undefined
4927
+ ? { queuedAhead: item.queuedAhead }
4928
+ : {}),
4774
4929
  ...(progressWatchActive
4775
4930
  ? { progressWatchActive: true }
4776
4931
  : {}),
@@ -5404,11 +5559,37 @@ export class GatewayService {
5404
5559
  if (event.outcome === "incompatible") {
5405
5560
  await this.requireEndpointRefreshUnconflicted();
5406
5561
  this.activeCodexIncompatibility = structuredClone(event.attestation);
5562
+ this.incompatibleProviderSurfaces.set(this.providerSurfaceKey("codex", event.previous.hostId), event.attestation.safeErrorCode ?? "CODEX_ENDPOINT_INCOMPATIBLE");
5407
5563
  this.addRuntimeAlert(event.attestation.safeErrorCode ?? "CODEX_ENDPOINT_INCOMPATIBLE", "error", { provider: "codex", host: event.previous.hostId });
5408
5564
  this.handledIncompatibleEndpointRefreshes.set(event.previous.hostId, transitionKey);
5409
5565
  await this.changed();
5410
5566
  return false;
5411
5567
  }
5568
+ if (event.attestation.tier === "schema_attested") {
5569
+ // A passing untested-version schema is useful observation, but it is not
5570
+ // durable route or write authority. Settle the provider's pending probe
5571
+ // so a later generation can be examined, while leaving every G1 route
5572
+ // stale and refusing selector/registration activation.
5573
+ await this.requireEndpointRefreshUnconflicted();
5574
+ await this.store.observeConnector({
5575
+ identity: event.current,
5576
+ health: "degraded",
5577
+ compatibility: "unknown",
5578
+ protocol: adapter.protocol,
5579
+ protocolVersion: adapter.protocolVersion,
5580
+ safeErrorCode: "CODEX_MONITOR_ONLY",
5581
+ });
5582
+ adapter.acceptCompatibilityAttestation(event.attestation);
5583
+ this.activeCodexIncompatibility = undefined;
5584
+ this.incompatibleProviderSurfaces.set(this.providerSurfaceKey("codex", event.current.hostId), "CODEX_MONITOR_ONLY");
5585
+ this.handledIncompatibleEndpointRefreshes.delete(event.current.hostId);
5586
+ await this.changed();
5587
+ if (options.deferActivation === true ||
5588
+ options.requiredRoute !== undefined) {
5589
+ throw new BridgeError("CODEX_MONITOR_ONLY", "This Codex App Server passed read-only schema probes but cannot authorize a task route or message write.");
5590
+ }
5591
+ return false;
5592
+ }
5412
5593
  await this.store.observeConnector({
5413
5594
  identity: event.current,
5414
5595
  health: "healthy",
@@ -5639,11 +5820,7 @@ export class GatewayService {
5639
5820
  // Provider writes remain closed until every durable and native transition
5640
5821
  // above is complete. This acknowledgement is deliberately the final
5641
5822
  // fallible activation step.
5642
- const accept = adapter.acceptCompatibilityAttestation;
5643
- if (accept === undefined) {
5644
- throw new BridgeError("CODEX_ENDPOINT_REFRESH_ACTIVATION_UNAVAILABLE", "The Codex provider cannot acknowledge the validated endpoint transition.");
5645
- }
5646
- accept.call(adapter, event.attestation);
5823
+ adapter.acceptCompatibilityAttestation(event.attestation);
5647
5824
  }
5648
5825
  catch (error) {
5649
5826
  try {
@@ -5665,6 +5842,7 @@ export class GatewayService {
5665
5842
  }
5666
5843
  this.handledIncompatibleEndpointRefreshes.delete(event.current.hostId);
5667
5844
  this.activeCodexIncompatibility = undefined;
5845
+ this.incompatibleProviderSurfaces.delete(this.providerSurfaceKey("codex", event.current.hostId));
5668
5846
  return false;
5669
5847
  }
5670
5848
  async onRouteState(source, event) {
@@ -5891,7 +6069,12 @@ export class GatewayService {
5891
6069
  });
5892
6070
  return projectGatewayPublicSnapshot({
5893
6071
  ...base,
5894
- ...(compatibilityChecks.length === 0 ? {} : { compatibilityChecks }),
6072
+ connectors: this.connectorsWithRegistry(base.connectors),
6073
+ ...(compatibilityChecks.length === 0
6074
+ ? {}
6075
+ : {
6076
+ compatibilityChecks: compatibilityChecks.map(projectPublicCompatibilityCheck),
6077
+ }),
5895
6078
  availablePeers: peers,
5896
6079
  activityEvents: this.activityEvents.map((event) => ({
5897
6080
  ...event,