agent-embassy 1.2.0 → 1.4.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.
- package/CHANGELOG.md +41 -0
- package/CONTRIBUTING.md +50 -16
- package/README.md +57 -25
- package/README.zh-CN.md +45 -23
- package/SECURITY.md +68 -48
- package/dist/src/gateway/claude-helper-protocol.d.ts +1 -0
- package/dist/src/gateway/claude-helper-protocol.js +4 -2
- package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
- package/dist/src/gateway/claude-helper-supervisor.d.ts +1 -0
- package/dist/src/gateway/claude-helper-supervisor.js +3 -1
- package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
- package/dist/src/gateway/claude-runtime.d.ts +0 -2
- package/dist/src/gateway/claude-runtime.js +6 -17
- package/dist/src/gateway/claude-runtime.js.map +1 -1
- package/dist/src/gateway/cli-copy.d.ts +1 -1
- package/dist/src/gateway/cli-copy.en.d.ts +4 -1
- package/dist/src/gateway/cli-copy.en.js +7 -3
- package/dist/src/gateway/cli-copy.en.js.map +1 -1
- package/dist/src/gateway/cli-copy.js +3 -0
- package/dist/src/gateway/cli-copy.js.map +1 -1
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +4 -1
- package/dist/src/gateway/cli-copy.zh-CN.js +7 -3
- package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/cli.d.ts +2 -5
- package/dist/src/gateway/cli.js +41 -67
- package/dist/src/gateway/cli.js.map +1 -1
- package/dist/src/gateway/codex-app-server.d.ts +2 -8
- package/dist/src/gateway/codex-app-server.js +36 -62
- package/dist/src/gateway/codex-app-server.js.map +1 -1
- package/dist/src/gateway/codex-local-transport.d.ts +4 -10
- package/dist/src/gateway/codex-local-transport.js +13 -16
- package/dist/src/gateway/codex-local-transport.js.map +1 -1
- package/dist/src/gateway/compatibility.d.ts +0 -27
- package/dist/src/gateway/compatibility.js +1 -101
- package/dist/src/gateway/compatibility.js.map +1 -1
- package/dist/src/gateway/config.d.ts +1 -4
- package/dist/src/gateway/config.js +0 -10
- package/dist/src/gateway/config.js.map +1 -1
- package/dist/src/gateway/control.d.ts +2 -21
- package/dist/src/gateway/control.js +7 -36
- package/dist/src/gateway/control.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.d.ts +1 -1
- package/dist/src/gateway/dashboard-copy.en.d.ts +21 -8
- package/dist/src/gateway/dashboard-copy.en.js +40 -27
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.js +21 -8
- package/dist/src/gateway/dashboard-copy.js.map +1 -1
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +21 -8
- package/dist/src/gateway/dashboard-copy.zh-CN.js +40 -27
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
- package/dist/src/gateway/dashboard-model.d.ts +4 -9
- package/dist/src/gateway/dashboard-model.js +159 -46
- package/dist/src/gateway/dashboard-model.js.map +1 -1
- package/dist/src/gateway/dashboard.js +27 -16
- package/dist/src/gateway/dashboard.js.map +1 -1
- package/dist/src/gateway/instance-lease.d.ts +2 -3
- package/dist/src/gateway/instance-lease.js +17 -140
- package/dist/src/gateway/instance-lease.js.map +1 -1
- package/dist/src/gateway/live-dashboard-app/app.js +62 -63
- package/dist/src/gateway/live-dashboard-command.d.ts +5 -1
- package/dist/src/gateway/live-dashboard-command.js +8 -6
- package/dist/src/gateway/live-dashboard-command.js.map +1 -1
- package/dist/src/gateway/live-dashboard-http.d.ts +0 -4
- package/dist/src/gateway/live-dashboard-http.js +20 -94
- package/dist/src/gateway/live-dashboard-http.js.map +1 -1
- package/dist/src/gateway/live-dashboard-protocol.d.ts +1 -5
- package/dist/src/gateway/live-dashboard-protocol.js +0 -42
- package/dist/src/gateway/live-dashboard-protocol.js.map +1 -1
- package/dist/src/gateway/live-dashboard-server.d.ts +2 -2
- package/dist/src/gateway/live-dashboard-server.js +18 -4
- package/dist/src/gateway/live-dashboard-server.js.map +1 -1
- package/dist/src/gateway/live-dashboard.d.ts +3 -6
- package/dist/src/gateway/live-dashboard.js +6 -42
- package/dist/src/gateway/live-dashboard.js.map +1 -1
- package/dist/src/gateway/progress-watch-machine.d.ts +6 -5
- package/dist/src/gateway/progress-watch-machine.js +6 -6
- package/dist/src/gateway/progress-watch-machine.js.map +1 -1
- package/dist/src/gateway/provenance-envelope.d.ts +1 -0
- package/dist/src/gateway/provenance-envelope.js +11 -4
- package/dist/src/gateway/provenance-envelope.js.map +1 -1
- package/dist/src/gateway/providers.d.ts +2 -21
- package/dist/src/gateway/providers.js +21 -261
- package/dist/src/gateway/providers.js.map +1 -1
- package/dist/src/gateway/server.js +0 -3
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +5 -15
- package/dist/src/gateway/service.js +101 -265
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/store.d.ts +9 -21
- package/dist/src/gateway/store.js +360 -140
- package/dist/src/gateway/store.js.map +1 -1
- package/dist/src/gateway/types.d.ts +1 -6
- package/dist/src/gateway/types.js.map +1 -1
- package/docs/CONFIGURATION.md +102 -0
- package/docs/CONFIGURATION.zh-CN.md +72 -0
- package/docs/DASHBOARD.md +103 -0
- package/docs/DASHBOARD.zh-CN.md +47 -0
- package/docs/DELIVERY.md +84 -0
- package/docs/DELIVERY.zh-CN.md +53 -0
- package/docs/GATEWAY-ARCHITECTURE.md +249 -153
- package/package.json +8 -4
- package/skills/embassy-peer/SKILL.md +25 -10
- package/dist/src/gateway/claude-compatibility-scratch.d.ts +0 -18
- package/dist/src/gateway/claude-compatibility-scratch.js +0 -158
- package/dist/src/gateway/claude-compatibility-scratch.js.map +0 -1
- package/dist/src/gateway/live-dashboard-bootstrap.d.ts +0 -36
- package/dist/src/gateway/live-dashboard-bootstrap.js +0 -176
- package/dist/src/gateway/live-dashboard-bootstrap.js.map +0 -1
- package/docs/DESIGN.md +0 -134
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { BridgeError } from "../errors.js";
|
|
3
3
|
import { KeyedMutex } from "../mutex.js";
|
|
4
|
-
import { certifiedCompatibilityVersions, compatibilitySurfaces,
|
|
4
|
+
import { certifiedCompatibilityVersions, compatibilitySurfaces, evaluateCompatibilityAttestation, isCompatibilityAttestation, } from "./compatibility.js";
|
|
5
5
|
import { createGatewayConversationId, isGatewaySnapshot, startGatewayControlServer, } from "./control.js";
|
|
6
6
|
import { publishGatewayDashboard } from "./dashboard.js";
|
|
7
7
|
import { createDeliveryMachine, isTerminalDeliveryMachine, projectDelivery, projectDeliveryWakeups, transitionDelivery, } from "./delivery-machine.js";
|
|
@@ -22,12 +22,6 @@ 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 CLAUDE_CLEAN_PREWRITE_ROUTE_CODES = new Set([
|
|
26
|
-
"CLAUDE_PEER_TARGET_UNKNOWN",
|
|
27
|
-
"CLAUDE_PEER_TARGET_STALE",
|
|
28
|
-
"CLAUDE_PEER_TARGET_CHANGED",
|
|
29
|
-
"CLAUDE_PEER_WORKSPACE_UNATTESTED",
|
|
30
|
-
]);
|
|
31
25
|
function progressWatchActivity(text, conversationId, actorAlias, explicitIdleMinutes) {
|
|
32
26
|
const prefixedOpen = text.startsWith("TRACK:");
|
|
33
27
|
const completionSignal = text.startsWith("DONE:");
|
|
@@ -69,9 +63,6 @@ function endpointRefreshCallbackKey(event) {
|
|
|
69
63
|
? {}
|
|
70
64
|
: { safeErrorCode: probe.safeErrorCode }),
|
|
71
65
|
})),
|
|
72
|
-
...(event.attestation.certification === undefined
|
|
73
|
-
? {}
|
|
74
|
-
: { certification: { ...event.attestation.certification } }),
|
|
75
66
|
...(event.attestation.safeErrorCode === undefined
|
|
76
67
|
? {}
|
|
77
68
|
: { safeErrorCode: event.attestation.safeErrorCode }),
|
|
@@ -103,6 +94,9 @@ function decisionFor(error) {
|
|
|
103
94
|
if (!(error instanceof BridgeError)) {
|
|
104
95
|
return { accepted: false, code: "rejected" };
|
|
105
96
|
}
|
|
97
|
+
if (error.code === "PROGRESS_WATCH_REPLACEMENT_OWNER_REQUIRED") {
|
|
98
|
+
return { accepted: false, code: "watch_owner_conflict" };
|
|
99
|
+
}
|
|
106
100
|
if (error.code.includes("NOT_FOUND") || error.code === "ROUTE_UNAVAILABLE") {
|
|
107
101
|
return { accepted: false, code: "not_found" };
|
|
108
102
|
}
|
|
@@ -145,8 +139,6 @@ export class GatewayService {
|
|
|
145
139
|
activeDispatchByTarget = new Map();
|
|
146
140
|
scheduledDispatchTargets = new Set();
|
|
147
141
|
dispatchRunnerTargets = new Set();
|
|
148
|
-
/** Claude sends that proved no write wait for exact provider-idle evidence. */
|
|
149
|
-
claudePrewriteIdleWaitTargets = new Set();
|
|
150
142
|
heldRedispatchTimers = new Map();
|
|
151
143
|
pendingClaudeReplies = new Map();
|
|
152
144
|
deliveryTrackers = new Map();
|
|
@@ -285,8 +277,8 @@ export class GatewayService {
|
|
|
285
277
|
if (this.adapters.length > 0 &&
|
|
286
278
|
this.adapters.every((adapter) => adapter.compatibilitySurface !== undefined &&
|
|
287
279
|
adapter.runCompatibilityProbes !== undefined)) {
|
|
288
|
-
const
|
|
289
|
-
const incompatible =
|
|
280
|
+
const attestations = await this.runAutomaticCompatibilityProbesLocked();
|
|
281
|
+
const incompatible = attestations.find((surface) => surface.tier === "incompatible");
|
|
290
282
|
if (incompatible !== undefined) {
|
|
291
283
|
throw new BridgeError(incompatible.safeErrorCode ?? "COMPATIBILITY_CHECK_FAILED", "A local provider failed bounded compatibility attestation.");
|
|
292
284
|
}
|
|
@@ -427,7 +419,6 @@ export class GatewayService {
|
|
|
427
419
|
this.activeDispatchByTarget.clear();
|
|
428
420
|
this.scheduledDispatchTargets.clear();
|
|
429
421
|
this.dispatchRunnerTargets.clear();
|
|
430
|
-
this.claudePrewriteIdleWaitTargets.clear();
|
|
431
422
|
for (const timer of this.heldRedispatchTimers.values()) {
|
|
432
423
|
this.timers.clearTimeout(timer);
|
|
433
424
|
}
|
|
@@ -518,8 +509,6 @@ export class GatewayService {
|
|
|
518
509
|
}),
|
|
519
510
|
listSnapshot: async () => (await this.observeSnapshot()).snapshot,
|
|
520
511
|
observeSnapshot: async () => await this.observeSnapshot(),
|
|
521
|
-
compatibilityCheck: async () => await this.compatibilityCheck(),
|
|
522
|
-
compatibilityCertify: async (params) => await this.compatibilityCertify(params),
|
|
523
512
|
deliveryStatus: async (params) => await this.deliveryStatus(params.token),
|
|
524
513
|
untrack: async (params) => await this.exclusiveDecision(async () => {
|
|
525
514
|
const settled = await this.store.settleProgressWatch({
|
|
@@ -550,133 +539,7 @@ export class GatewayService {
|
|
|
550
539
|
async snapshot() {
|
|
551
540
|
return (await this.observeSnapshot()).snapshot;
|
|
552
541
|
}
|
|
553
|
-
async
|
|
554
|
-
return await this.mutex.run("service", async () => await this.compatibilityCheckLocked());
|
|
555
|
-
}
|
|
556
|
-
async compatibilityCertify(params) {
|
|
557
|
-
const plan = await this.mutex.run("service", async () => {
|
|
558
|
-
if (!this.running || this.closing) {
|
|
559
|
-
throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "Compatibility certification requires one running broker.");
|
|
560
|
-
}
|
|
561
|
-
const report = await this.compatibilityCheckLocked();
|
|
562
|
-
const codexRoutes = (await this.store.inspectPrivateCodexRoutes()).filter((route) => route.enabled);
|
|
563
|
-
const codexRoute = params.codexAlias === undefined
|
|
564
|
-
? codexRoutes.length === 1
|
|
565
|
-
? codexRoutes[0]
|
|
566
|
-
: undefined
|
|
567
|
-
: codexRoutes.find((route) => route.alias === params.codexAlias);
|
|
568
|
-
if (codexRoute === undefined) {
|
|
569
|
-
throw new BridgeError("COMPAT_CERTIFY_CODEX_ROUTE_REQUIRED", "Choose one exact registered Codex route for live certification.");
|
|
570
|
-
}
|
|
571
|
-
const adapters = new Map(this.adapters.map((adapter) => [adapter.identity.provider, adapter]));
|
|
572
|
-
return { adapters, codexRoute, report };
|
|
573
|
-
});
|
|
574
|
-
const certifiedAt = this.now().toISOString();
|
|
575
|
-
const updated = [];
|
|
576
|
-
for (const attestation of plan.report.surfaces) {
|
|
577
|
-
const adapter = plan.adapters.get(attestation.surface);
|
|
578
|
-
let certification;
|
|
579
|
-
if (attestation.tier === "incompatible" ||
|
|
580
|
-
adapter?.runCompatibilityCertification === undefined) {
|
|
581
|
-
certification = {
|
|
582
|
-
depth: attestation.surface === "claude"
|
|
583
|
-
? "wire"
|
|
584
|
-
: params.withTurn
|
|
585
|
-
? "turn"
|
|
586
|
-
: "thread_ops",
|
|
587
|
-
outcome: "fail",
|
|
588
|
-
certifiedAt,
|
|
589
|
-
safeErrorCode: attestation.safeErrorCode ??
|
|
590
|
-
`${attestation.surface.toUpperCase()}_CERTIFICATION_UNAVAILABLE`,
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
else {
|
|
594
|
-
try {
|
|
595
|
-
certification = await adapter.runCompatibilityCertification({
|
|
596
|
-
controllerStateRoot: this.store.rootDir,
|
|
597
|
-
withTurn: params.withTurn,
|
|
598
|
-
...(attestation.surface === "codex"
|
|
599
|
-
? { routeHandle: plan.codexRoute.binding.routeHandle }
|
|
600
|
-
: {}),
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
catch {
|
|
604
|
-
certification = {
|
|
605
|
-
depth: attestation.surface === "claude"
|
|
606
|
-
? "wire"
|
|
607
|
-
: params.withTurn
|
|
608
|
-
? "turn"
|
|
609
|
-
: "thread_ops",
|
|
610
|
-
outcome: "fail",
|
|
611
|
-
certifiedAt: this.now().toISOString(),
|
|
612
|
-
safeErrorCode: `${attestation.surface.toUpperCase()}_CERTIFICATION_FAILED`,
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
const expectedDepth = attestation.surface === "claude"
|
|
617
|
-
? "wire"
|
|
618
|
-
: params.withTurn
|
|
619
|
-
? "turn"
|
|
620
|
-
: "thread_ops";
|
|
621
|
-
if (!isCompatibilityCertification(certification) ||
|
|
622
|
-
certification.depth !== expectedDepth) {
|
|
623
|
-
certification = {
|
|
624
|
-
depth: expectedDepth,
|
|
625
|
-
outcome: "fail",
|
|
626
|
-
certifiedAt: this.now().toISOString(),
|
|
627
|
-
safeErrorCode: `${attestation.surface.toUpperCase()}_CERTIFICATION_EVIDENCE_INVALID`,
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
updated.push(attachCompatibilityCertification(attestation, certification));
|
|
631
|
-
}
|
|
632
|
-
return await this.mutex.run("service", async () => {
|
|
633
|
-
const currentCodexRoute = (await this.store.inspectPrivateCodexRoutes()).find((route) => route.enabled &&
|
|
634
|
-
route.alias === plan.codexRoute.alias &&
|
|
635
|
-
bindingKey(route.binding) === bindingKey(plan.codexRoute.binding));
|
|
636
|
-
const reconciled = updated.map((attestation) => {
|
|
637
|
-
if (attestation.surface === "codex" && currentCodexRoute === undefined) {
|
|
638
|
-
return attachCompatibilityCertification(attestation, {
|
|
639
|
-
depth: params.withTurn ? "turn" : "thread_ops",
|
|
640
|
-
outcome: "fail",
|
|
641
|
-
certifiedAt: this.now().toISOString(),
|
|
642
|
-
safeErrorCode: "CODEX_CERTIFICATION_ROUTE_CHANGED",
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
const observation = plan.adapters
|
|
646
|
-
.get(attestation.surface)
|
|
647
|
-
?.compatibilitySurface?.();
|
|
648
|
-
if (observation?.surface === attestation.surface &&
|
|
649
|
-
observation.version === attestation.version) {
|
|
650
|
-
return attestation;
|
|
651
|
-
}
|
|
652
|
-
return attachCompatibilityCertification(attestation, {
|
|
653
|
-
depth: attestation.surface === "claude"
|
|
654
|
-
? "wire"
|
|
655
|
-
: params.withTurn
|
|
656
|
-
? "turn"
|
|
657
|
-
: "thread_ops",
|
|
658
|
-
outcome: "fail",
|
|
659
|
-
certifiedAt: this.now().toISOString(),
|
|
660
|
-
safeErrorCode: `${attestation.surface.toUpperCase()}_CERTIFICATION_VERSION_CHANGED`,
|
|
661
|
-
});
|
|
662
|
-
});
|
|
663
|
-
const result = {
|
|
664
|
-
policy: plan.report.policy,
|
|
665
|
-
certified: reconciled.every((attestation) => attestation.certification?.outcome === "pass"),
|
|
666
|
-
withTurn: params.withTurn,
|
|
667
|
-
surfaces: reconciled,
|
|
668
|
-
};
|
|
669
|
-
if (!isCompatibilityCertificationReport(result)) {
|
|
670
|
-
throw new BridgeError("COMPAT_CERTIFICATION_REPORT_INVALID", "Compatibility certification evidence failed its closed report schema.");
|
|
671
|
-
}
|
|
672
|
-
for (const attestation of reconciled) {
|
|
673
|
-
await this.store.recordCompatibilityAttestation(attestation);
|
|
674
|
-
}
|
|
675
|
-
await this.changed();
|
|
676
|
-
return result;
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
async compatibilityCheckLocked() {
|
|
542
|
+
async runAutomaticCompatibilityProbesLocked() {
|
|
680
543
|
const bySurface = new Map(this.adapters.map((adapter) => {
|
|
681
544
|
if (adapter.compatibilitySurface === undefined ||
|
|
682
545
|
adapter.runCompatibilityProbes === undefined) {
|
|
@@ -695,39 +558,21 @@ export class GatewayService {
|
|
|
695
558
|
if (entry === undefined) {
|
|
696
559
|
throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "A required compatibility surface is unavailable.");
|
|
697
560
|
}
|
|
698
|
-
const
|
|
699
|
-
const
|
|
700
|
-
const checkedAt = cached?.checkedAt ?? this.now().toISOString();
|
|
701
|
-
let attestation = evaluateCompatibilityAttestation({
|
|
561
|
+
const probes = await entry.adapter.runCompatibilityProbes();
|
|
562
|
+
const attestation = evaluateCompatibilityAttestation({
|
|
702
563
|
surface,
|
|
703
564
|
version: entry.observation.version,
|
|
704
|
-
checkedAt,
|
|
705
|
-
policy: this.config.compatibilityPolicy ?? "observed",
|
|
565
|
+
checkedAt: this.now().toISOString(),
|
|
706
566
|
certifiedVersions: certifiedCompatibilityVersions[surface],
|
|
707
567
|
probes,
|
|
708
568
|
});
|
|
709
|
-
|
|
710
|
-
attestation.tier !== "incompatible") {
|
|
711
|
-
attestation = attachCompatibilityCertification(attestation, cached.certification);
|
|
712
|
-
}
|
|
713
|
-
if (cached === undefined ||
|
|
714
|
-
JSON.stringify(cached) !== JSON.stringify(attestation)) {
|
|
715
|
-
await this.store.recordCompatibilityAttestation(attestation);
|
|
716
|
-
}
|
|
569
|
+
await this.store.recordCompatibilityAttestation(attestation);
|
|
717
570
|
if (attestation.tier !== "incompatible") {
|
|
718
571
|
entry.adapter.acceptCompatibilityAttestation?.(attestation);
|
|
719
572
|
}
|
|
720
573
|
attestations.push(attestation);
|
|
721
574
|
}
|
|
722
|
-
|
|
723
|
-
policy: this.config.compatibilityPolicy ?? "observed",
|
|
724
|
-
compatible: attestations.every((attestation) => attestation.tier !== "incompatible"),
|
|
725
|
-
surfaces: attestations,
|
|
726
|
-
};
|
|
727
|
-
if (!isCompatibilityCheckReport(report)) {
|
|
728
|
-
throw new BridgeError("COMPAT_REPORT_INVALID", "Compatibility evidence failed its closed report schema.");
|
|
729
|
-
}
|
|
730
|
-
return report;
|
|
575
|
+
return attestations;
|
|
731
576
|
}
|
|
732
577
|
async observeSnapshot() {
|
|
733
578
|
return await this.mutex.run("service", async () => {
|
|
@@ -2746,13 +2591,22 @@ export class GatewayService {
|
|
|
2746
2591
|
const publicBefore = await this.discoveryPublicFingerprint();
|
|
2747
2592
|
this.candidates.clear();
|
|
2748
2593
|
const rowsByAlias = new Map();
|
|
2594
|
+
const collisionByRouteHandle = new Map();
|
|
2749
2595
|
const discoveredCandidates = [];
|
|
2750
2596
|
const observedSelected = new Set();
|
|
2751
2597
|
for (const adapter of this.adapters.filter((item) => item.identity.provider === "claude")) {
|
|
2752
|
-
|
|
2598
|
+
let discovered = {
|
|
2753
2599
|
peers: [],
|
|
2754
2600
|
complete: false,
|
|
2755
2601
|
};
|
|
2602
|
+
try {
|
|
2603
|
+
discovered = (await adapter.discoverClaudePeers?.()) ?? discovered;
|
|
2604
|
+
}
|
|
2605
|
+
catch {
|
|
2606
|
+
// Registry observation is dashboard evidence, not Claude mailbox
|
|
2607
|
+
// authority. The provider still revalidates the exact target before
|
|
2608
|
+
// any write.
|
|
2609
|
+
}
|
|
2756
2610
|
const grouped = new Map();
|
|
2757
2611
|
const byHandle = new Map();
|
|
2758
2612
|
for (const peer of discovered.peers) {
|
|
@@ -2772,6 +2626,8 @@ export class GatewayService {
|
|
|
2772
2626
|
if (peer === undefined)
|
|
2773
2627
|
continue;
|
|
2774
2628
|
if (matches.length !== 1) {
|
|
2629
|
+
for (const match of matches)
|
|
2630
|
+
collisionByRouteHandle.set(match.routeHandle, "PEER_ALIAS_COLLISION");
|
|
2775
2631
|
rowsByAlias.set(alias, {
|
|
2776
2632
|
peer,
|
|
2777
2633
|
adapter,
|
|
@@ -2788,6 +2644,7 @@ export class GatewayService {
|
|
|
2788
2644
|
continue;
|
|
2789
2645
|
}
|
|
2790
2646
|
if ((byHandle.get(peer.routeHandle)?.length ?? 0) !== 1) {
|
|
2647
|
+
collisionByRouteHandle.set(peer.routeHandle, "PEER_SESSION_COLLISION");
|
|
2791
2648
|
rowsByAlias.set(alias, {
|
|
2792
2649
|
peer,
|
|
2793
2650
|
adapter,
|
|
@@ -2813,6 +2670,8 @@ export class GatewayService {
|
|
|
2813
2670
|
const collision = this.routeBindings.get(alias);
|
|
2814
2671
|
if (collision !== undefined &&
|
|
2815
2672
|
bindingKey(collision) !== bindingKey(existing[1])) {
|
|
2673
|
+
collisionByRouteHandle.set(routeHandle, "PEER_ALIAS_COLLISION");
|
|
2674
|
+
collisionByRouteHandle.set(collision.routeHandle, "PEER_ALIAS_COLLISION");
|
|
2816
2675
|
rowsByAlias.set(alias, {
|
|
2817
2676
|
peer: candidate,
|
|
2818
2677
|
adapter,
|
|
@@ -2827,10 +2686,16 @@ export class GatewayService {
|
|
|
2827
2686
|
catch (error) {
|
|
2828
2687
|
if (!(error instanceof BridgeError))
|
|
2829
2688
|
throw error;
|
|
2689
|
+
// Only a real alias collision may wear that receipt; every other
|
|
2690
|
+
// rename failure is an unobserved peer, not a name conflict.
|
|
2691
|
+
const safeErrorCode = error.code === "ROUTE_ALIAS_COLLISION"
|
|
2692
|
+
? "PEER_ALIAS_COLLISION"
|
|
2693
|
+
: "PEER_NOT_OBSERVED";
|
|
2694
|
+
collisionByRouteHandle.set(routeHandle, safeErrorCode);
|
|
2830
2695
|
rowsByAlias.set(alias, {
|
|
2831
2696
|
peer: candidate,
|
|
2832
2697
|
adapter,
|
|
2833
|
-
safeErrorCode
|
|
2698
|
+
safeErrorCode,
|
|
2834
2699
|
});
|
|
2835
2700
|
}
|
|
2836
2701
|
}
|
|
@@ -2899,6 +2764,9 @@ export class GatewayService {
|
|
|
2899
2764
|
observedSelected.has(bindingKey(binding))) {
|
|
2900
2765
|
continue;
|
|
2901
2766
|
}
|
|
2767
|
+
const discoveryError = collisionByRouteHandle.get(binding.routeHandle);
|
|
2768
|
+
if (discoveryError === undefined)
|
|
2769
|
+
continue;
|
|
2902
2770
|
const inspection = await this.store.inspectPrivateRoute(alias);
|
|
2903
2771
|
if (inspection !== undefined &&
|
|
2904
2772
|
inspection.enabled &&
|
|
@@ -2906,9 +2774,9 @@ export class GatewayService {
|
|
|
2906
2774
|
await this.drainPreDeadlineDeliveryCallbacksLocked();
|
|
2907
2775
|
const inFlightSettlements = await this.planRouteInFlightSettlementsLocked(alias, {
|
|
2908
2776
|
unwrittenOutcome: "failed",
|
|
2909
|
-
safeErrorCode:
|
|
2777
|
+
safeErrorCode: discoveryError,
|
|
2910
2778
|
});
|
|
2911
|
-
const settlements = await this.store.invalidateRoute(binding,
|
|
2779
|
+
const settlements = await this.store.invalidateRoute(binding, discoveryError, inFlightSettlements);
|
|
2912
2780
|
for (const settlement of settlements) {
|
|
2913
2781
|
await this.applyTerminalSettlementLocked(settlement);
|
|
2914
2782
|
}
|
|
@@ -3135,6 +3003,11 @@ export class GatewayService {
|
|
|
3135
3003
|
async resolveSelectedClaudeDestination(selector) {
|
|
3136
3004
|
const discoveryChanged = await this.refreshClaudeDiscovery();
|
|
3137
3005
|
try {
|
|
3006
|
+
const selected = [...this.routeBindings.entries()].find(([alias, binding]) => binding.provider === "claude" &&
|
|
3007
|
+
(alias === selector ||
|
|
3008
|
+
binding.routeHandle.toLowerCase() === selector.toLowerCase()));
|
|
3009
|
+
if (selected !== undefined)
|
|
3010
|
+
return { alias: selected[0], discoveryChanged };
|
|
3138
3011
|
const candidate = this.claudeCandidate(selector);
|
|
3139
3012
|
if (candidate === undefined) {
|
|
3140
3013
|
throw new BridgeError("PEER_NOT_FOUND", "No unique compatible interactive peer matches that current name or session UUID.");
|
|
@@ -3327,9 +3200,6 @@ export class GatewayService {
|
|
|
3327
3200
|
this.routeBindings.set(alias, binding);
|
|
3328
3201
|
this.routeStates.set(alias, state);
|
|
3329
3202
|
this.bindingAliases.set(bindingKey(binding), alias);
|
|
3330
|
-
if (binding.provider === "claude" && state === "idle") {
|
|
3331
|
-
this.claudePrewriteIdleWaitTargets.delete(alias);
|
|
3332
|
-
}
|
|
3333
3203
|
}
|
|
3334
3204
|
forgetBinding(alias) {
|
|
3335
3205
|
const heldTimer = this.heldRedispatchTimers.get(alias);
|
|
@@ -3344,7 +3214,6 @@ export class GatewayService {
|
|
|
3344
3214
|
}
|
|
3345
3215
|
this.routeBindings.delete(alias);
|
|
3346
3216
|
this.routeStates.delete(alias);
|
|
3347
|
-
this.claudePrewriteIdleWaitTargets.delete(alias);
|
|
3348
3217
|
for (const [messageId, continuation] of this.providerTurnContinuations) {
|
|
3349
3218
|
if (continuation.targetAlias !== alias)
|
|
3350
3219
|
continue;
|
|
@@ -3453,8 +3322,10 @@ export class GatewayService {
|
|
|
3453
3322
|
enqueuedAt: input.enqueuedAt,
|
|
3454
3323
|
stallAt,
|
|
3455
3324
|
deadlineAt,
|
|
3456
|
-
// Deferred means the provider proved no write.
|
|
3457
|
-
// attempt per 500 ms slice
|
|
3325
|
+
// Deferred means the provider proved no write. This budgets one
|
|
3326
|
+
// attempt per 500 ms slice of this message's immutable deadline; a
|
|
3327
|
+
// Codex-bound retry spends one slice each, while a Claude-bound retry
|
|
3328
|
+
// backs off exponentially and spends far fewer of them.
|
|
3458
3329
|
maxCleanPrewriteRetries: Math.max(0, Math.ceil((deadlineAt - input.enqueuedAt) / 500)),
|
|
3459
3330
|
nativeReceipt: input.nativeReceipt !== undefined,
|
|
3460
3331
|
maxNativeReceiptCleanPrewriteRetries: DELIVERY_ACK_RETRY_DELAYS_MS.length,
|
|
@@ -4061,22 +3932,14 @@ export class GatewayService {
|
|
|
4061
3932
|
: "PROGRESS_WATCH_ENDPOINT_RETIRED", action.outcome === "unresponsive" ? "error" : "warning", { alias: action.ownerAlias });
|
|
4062
3933
|
continue;
|
|
4063
3934
|
}
|
|
4064
|
-
if (action.type === "notify_capability_degraded") {
|
|
4065
|
-
this.addRuntimeAlert("PROGRESS_WATCH_CAPABILITY_DEGRADED", "warning", {
|
|
4066
|
-
alias: action.ownerAlias,
|
|
4067
|
-
});
|
|
4068
|
-
continue;
|
|
4069
|
-
}
|
|
4070
3935
|
const text = `[Embassy automated liveness check — ${action.conversationId} / ` +
|
|
4071
3936
|
`Embassy 自动活跃检查 — ${action.conversationId}]\n` +
|
|
4072
3937
|
"Reply with the result or a status. / 请回复结果或状态。";
|
|
4073
3938
|
try {
|
|
4074
|
-
await this.enqueue(action.ownerAlias, action.workerAlias, text, false,
|
|
3939
|
+
await this.enqueue(action.ownerAlias, action.workerAlias, text, false, {
|
|
4075
3940
|
existingConversationId: action.conversationId,
|
|
4076
|
-
requestedHopCount: 0,
|
|
4077
3941
|
exposeDeliveryToken: false,
|
|
4078
3942
|
skipProgressWatchActivity: true,
|
|
4079
|
-
preserveConversationHop: true,
|
|
4080
3943
|
progressWatchNudge: {
|
|
4081
3944
|
conversationId: action.conversationId,
|
|
4082
3945
|
nudgeNumber: action.nudgeNumber,
|
|
@@ -4123,8 +3986,7 @@ export class GatewayService {
|
|
|
4123
3986
|
}
|
|
4124
3987
|
if (!isTerminalDeliveryMachine(tracker.machine) &&
|
|
4125
3988
|
tracker.machine.dispatchRetryAt !== null &&
|
|
4126
|
-
tracker.machine.dispatchRetryAt <= now
|
|
4127
|
-
!this.claudePrewriteIdleWaitTargets.has(tracker.targetAlias)) {
|
|
3989
|
+
tracker.machine.dispatchRetryAt <= now) {
|
|
4128
3990
|
if (!this.dispatchRunnerTargets.has(tracker.targetAlias)) {
|
|
4129
3991
|
this.scheduleDispatch(tracker.targetAlias);
|
|
4130
3992
|
}
|
|
@@ -4213,7 +4075,6 @@ export class GatewayService {
|
|
|
4213
4075
|
if (wakeups.deadlineAt !== undefined)
|
|
4214
4076
|
consider(wakeups.deadlineAt);
|
|
4215
4077
|
if (wakeups.dispatchRetryAt !== undefined &&
|
|
4216
|
-
!this.claudePrewriteIdleWaitTargets.has(tracker.targetAlias) &&
|
|
4217
4078
|
!this.scheduledDispatchTargets.has(tracker.targetAlias) &&
|
|
4218
4079
|
!this.dispatchRunnerTargets.has(tracker.targetAlias)) {
|
|
4219
4080
|
consider(wakeups.dispatchRetryAt);
|
|
@@ -4311,7 +4172,7 @@ export class GatewayService {
|
|
|
4311
4172
|
await this.verifyCodex(params.fromAlias, params.threadId);
|
|
4312
4173
|
const resolved = await this.resolveSelectedClaudeDestination(params.toAlias);
|
|
4313
4174
|
discoveryChanged = resolved.discoveryChanged;
|
|
4314
|
-
return this.acceptedControlResult(await this.enqueue(params.fromAlias, resolved.alias, params.text, params.expectsReply,
|
|
4175
|
+
return this.acceptedControlResult(await this.enqueue(params.fromAlias, resolved.alias, params.text, params.expectsReply, params.trackIdleMinutes === undefined
|
|
4315
4176
|
? {}
|
|
4316
4177
|
: { trackIdleMinutes: params.trackIdleMinutes }));
|
|
4317
4178
|
}
|
|
@@ -4330,7 +4191,7 @@ export class GatewayService {
|
|
|
4330
4191
|
try {
|
|
4331
4192
|
await this.verifyClaude(params.fromAlias, params.replyAddress);
|
|
4332
4193
|
const steer = this.config.steeringEnabled && params.text.startsWith("STEER:");
|
|
4333
|
-
return this.acceptedControlResult(await this.enqueue(params.fromAlias, params.toAlias, params.text, steer ? false : params.expectsReply,
|
|
4194
|
+
return this.acceptedControlResult(await this.enqueue(params.fromAlias, params.toAlias, params.text, steer ? false : params.expectsReply, {
|
|
4334
4195
|
...(steer ? { steer: true } : {}),
|
|
4335
4196
|
...(params.trackIdleMinutes === undefined
|
|
4336
4197
|
? {}
|
|
@@ -4359,10 +4220,9 @@ export class GatewayService {
|
|
|
4359
4220
|
throw new BridgeError("ROUTE_MISMATCH", "The caller is not a conversation participant.");
|
|
4360
4221
|
const result = caller.kind === "codex" &&
|
|
4361
4222
|
this.nativeIngressByConversation.has(conversation.id)
|
|
4362
|
-
? await this.enqueueNativeReply(conversation, from, params.text,
|
|
4363
|
-
: await this.enqueue(from, to, params.text, false,
|
|
4223
|
+
? await this.enqueueNativeReply(conversation, from, params.text, true, params.trackIdleMinutes)
|
|
4224
|
+
: await this.enqueue(from, to, params.text, false, {
|
|
4364
4225
|
existingConversationId: conversation.id,
|
|
4365
|
-
requestedHopCount: conversation.lastHopCount + 1,
|
|
4366
4226
|
...(params.trackIdleMinutes === undefined
|
|
4367
4227
|
? {}
|
|
4368
4228
|
: { trackIdleMinutes: params.trackIdleMinutes }),
|
|
@@ -4374,7 +4234,7 @@ export class GatewayService {
|
|
|
4374
4234
|
}
|
|
4375
4235
|
});
|
|
4376
4236
|
}
|
|
4377
|
-
async enqueue(sourceAlias, targetAlias, text, expectsReply,
|
|
4237
|
+
async enqueue(sourceAlias, targetAlias, text, expectsReply, options = {}) {
|
|
4378
4238
|
if (this.codexSuccessionPoisoned.has(sourceAlias) ||
|
|
4379
4239
|
this.codexSuccessionPoisoned.has(targetAlias)) {
|
|
4380
4240
|
throw new BridgeError("CODEX_SUCCESSION_RECOVERY_REQUIRED", "No message can be accepted while Codex succession requires manual recovery.");
|
|
@@ -4397,11 +4257,8 @@ export class GatewayService {
|
|
|
4397
4257
|
targetAlias,
|
|
4398
4258
|
expectsReply,
|
|
4399
4259
|
nextSequence: 0,
|
|
4400
|
-
lastHopCount: 0,
|
|
4401
4260
|
lastActivityAt: this.now().toISOString(),
|
|
4402
4261
|
};
|
|
4403
|
-
const hopCount = options.requestedHopCount ??
|
|
4404
|
-
(isReply ? conversation.lastHopCount + 1 : 0);
|
|
4405
4262
|
const sequence = conversation.nextSequence;
|
|
4406
4263
|
const enqueuedAt = this.now().getTime();
|
|
4407
4264
|
const deliveryToken = (options.exposeDeliveryToken ?? true)
|
|
@@ -4414,7 +4271,6 @@ export class GatewayService {
|
|
|
4414
4271
|
body: text,
|
|
4415
4272
|
dedupeKey: `${conversationId}:${sequence}`,
|
|
4416
4273
|
conversationIdSuffix: conversationId.slice(-8),
|
|
4417
|
-
hopCount,
|
|
4418
4274
|
deadlineAt,
|
|
4419
4275
|
...(options.steer === true ? { steer: true } : {}),
|
|
4420
4276
|
...(options.skipProgressWatchActivity === true
|
|
@@ -4435,15 +4291,10 @@ export class GatewayService {
|
|
|
4435
4291
|
if (queued.pair === true)
|
|
4436
4292
|
conversation.pair = true;
|
|
4437
4293
|
conversation.nextSequence += 1;
|
|
4438
|
-
if (options.preserveConversationHop !== true) {
|
|
4439
|
-
conversation.lastHopCount = hopCount;
|
|
4440
|
-
}
|
|
4441
4294
|
conversation.lastActivityAt = this.now().toISOString();
|
|
4442
4295
|
this.messageContexts.set(queued.messageId, {
|
|
4443
4296
|
conversationId,
|
|
4444
|
-
isReply,
|
|
4445
4297
|
expectsReply,
|
|
4446
|
-
hopCount,
|
|
4447
4298
|
sequence,
|
|
4448
4299
|
targetBindingKey: bindingKey(target),
|
|
4449
4300
|
authorization: "selected_route",
|
|
@@ -4462,7 +4313,6 @@ export class GatewayService {
|
|
|
4462
4313
|
this.pendingClaudeReplies.set(bindingKey(target), {
|
|
4463
4314
|
conversationId,
|
|
4464
4315
|
bindingKey: bindingKey(target),
|
|
4465
|
-
hopCount,
|
|
4466
4316
|
deadlineAt,
|
|
4467
4317
|
tainted: false,
|
|
4468
4318
|
});
|
|
@@ -4476,7 +4326,7 @@ export class GatewayService {
|
|
|
4476
4326
|
...(deliveryToken === undefined ? {} : { deliveryToken }),
|
|
4477
4327
|
};
|
|
4478
4328
|
}
|
|
4479
|
-
async enqueueNativeReply(conversation, sourceAlias, text,
|
|
4329
|
+
async enqueueNativeReply(conversation, sourceAlias, text, exposeDeliveryToken = true, trackIdleMinutes) {
|
|
4480
4330
|
this.pruneTransient();
|
|
4481
4331
|
const capability = this.nativeIngressByConversation.get(conversation.id);
|
|
4482
4332
|
if (capability === undefined ||
|
|
@@ -4500,7 +4350,6 @@ export class GatewayService {
|
|
|
4500
4350
|
body: text,
|
|
4501
4351
|
dedupeKey: `${conversation.id}:${sequence}`,
|
|
4502
4352
|
conversationIdSuffix: conversation.id.slice(-8),
|
|
4503
|
-
hopCount: requestedHopCount,
|
|
4504
4353
|
deadlineAt,
|
|
4505
4354
|
...(conversation.pair === true ? { pair: true } : {}),
|
|
4506
4355
|
progressWatch: progressWatchActivity(text, conversation.id, sourceAlias, trackIdleMinutes),
|
|
@@ -4509,13 +4358,10 @@ export class GatewayService {
|
|
|
4509
4358
|
throw new BridgeError("MESSAGE_REJECTED", "The native reply was not accepted.");
|
|
4510
4359
|
}
|
|
4511
4360
|
conversation.nextSequence += 1;
|
|
4512
|
-
conversation.lastHopCount = requestedHopCount;
|
|
4513
4361
|
conversation.lastActivityAt = this.now().toISOString();
|
|
4514
4362
|
this.messageContexts.set(queued.messageId, {
|
|
4515
4363
|
conversationId: conversation.id,
|
|
4516
|
-
isReply: true,
|
|
4517
4364
|
expectsReply: false,
|
|
4518
|
-
hopCount: requestedHopCount,
|
|
4519
4365
|
sequence,
|
|
4520
4366
|
targetBindingKey: bindingKey(capability.binding),
|
|
4521
4367
|
nativeReplyBinding: { ...capability.binding },
|
|
@@ -4547,7 +4393,7 @@ export class GatewayService {
|
|
|
4547
4393
|
* native-ingress boundary; this preserves the exact connector generation
|
|
4548
4394
|
* without recreating route authority or accepting a new unsolicited send.
|
|
4549
4395
|
*/
|
|
4550
|
-
async enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, text
|
|
4396
|
+
async enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, text) {
|
|
4551
4397
|
const target = await this.store.resolveRoute(conversation.sourceAlias);
|
|
4552
4398
|
if (sourceBinding.provider !== "claude" ||
|
|
4553
4399
|
target.provider !== "codex" ||
|
|
@@ -4566,7 +4412,6 @@ export class GatewayService {
|
|
|
4566
4412
|
body: text,
|
|
4567
4413
|
dedupeKey: `${conversation.id}:${sequence}`,
|
|
4568
4414
|
conversationIdSuffix: conversation.id.slice(-8),
|
|
4569
|
-
hopCount: requestedHopCount,
|
|
4570
4415
|
deadlineAt,
|
|
4571
4416
|
authorizedPairTeardownReply: true,
|
|
4572
4417
|
progressWatch: progressWatchActivity(text, conversation.id, conversation.targetAlias),
|
|
@@ -4575,16 +4420,13 @@ export class GatewayService {
|
|
|
4575
4420
|
throw new BridgeError("MESSAGE_REJECTED", "The retained provider reply was not accepted.");
|
|
4576
4421
|
}
|
|
4577
4422
|
conversation.nextSequence += 1;
|
|
4578
|
-
conversation.lastHopCount = requestedHopCount;
|
|
4579
4423
|
conversation.lastActivityAt = this.now().toISOString();
|
|
4580
4424
|
// The old edge has already been removed. Retain only this already-observed
|
|
4581
4425
|
// reply attempt; the conversation no longer grants pair reply authority.
|
|
4582
4426
|
delete conversation.pair;
|
|
4583
4427
|
this.messageContexts.set(queued.messageId, {
|
|
4584
4428
|
conversationId: conversation.id,
|
|
4585
|
-
isReply: true,
|
|
4586
4429
|
expectsReply: false,
|
|
4587
|
-
hopCount: requestedHopCount,
|
|
4588
4430
|
sequence,
|
|
4589
4431
|
targetBindingKey: bindingKey(target),
|
|
4590
4432
|
authorization: "selected_route",
|
|
@@ -4695,15 +4537,12 @@ export class GatewayService {
|
|
|
4695
4537
|
targetAlias: item.targetAlias,
|
|
4696
4538
|
expectsReply: false,
|
|
4697
4539
|
nextSequence: 1,
|
|
4698
|
-
lastHopCount: item.hopCount,
|
|
4699
4540
|
lastActivityAt: this.now().toISOString(),
|
|
4700
4541
|
...(item.pair === true ? { pair: true } : {}),
|
|
4701
4542
|
};
|
|
4702
4543
|
const context = {
|
|
4703
4544
|
conversationId,
|
|
4704
|
-
isReply: false,
|
|
4705
4545
|
expectsReply: false,
|
|
4706
|
-
hopCount: item.hopCount,
|
|
4707
4546
|
sequence: 0,
|
|
4708
4547
|
targetBindingKey: bindingKey(binding),
|
|
4709
4548
|
authorization: "selected_route",
|
|
@@ -4726,8 +4565,7 @@ export class GatewayService {
|
|
|
4726
4565
|
if (!this.running ||
|
|
4727
4566
|
this.closing ||
|
|
4728
4567
|
this.codexSuccessionDispatchFrozen.has(targetAlias) ||
|
|
4729
|
-
this.codexSuccessionPoisoned.has(targetAlias)
|
|
4730
|
-
this.claudePrewriteIdleWaitTargets.has(targetAlias)) {
|
|
4568
|
+
this.codexSuccessionPoisoned.has(targetAlias)) {
|
|
4731
4569
|
return;
|
|
4732
4570
|
}
|
|
4733
4571
|
if (await this.processLifecycleLocked()) {
|
|
@@ -4762,7 +4600,6 @@ export class GatewayService {
|
|
|
4762
4600
|
if (context === undefined && selectedBinding !== undefined) {
|
|
4763
4601
|
context = this.restoreQueuedMessageContextLocked(item, selectedBinding);
|
|
4764
4602
|
}
|
|
4765
|
-
const binding = context?.nativeReplyBinding ?? selectedBinding;
|
|
4766
4603
|
if (context === undefined) {
|
|
4767
4604
|
const result = await this.store.settleMessage({
|
|
4768
4605
|
messageId: item.messageId,
|
|
@@ -4775,6 +4612,7 @@ export class GatewayService {
|
|
|
4775
4612
|
}
|
|
4776
4613
|
return;
|
|
4777
4614
|
}
|
|
4615
|
+
const binding = context.nativeReplyBinding ?? selectedBinding;
|
|
4778
4616
|
if (binding === undefined) {
|
|
4779
4617
|
const result = await this.store.requeueInFlightMessage(item.messageId, item.body);
|
|
4780
4618
|
if (result.status === "settled") {
|
|
@@ -4785,8 +4623,8 @@ export class GatewayService {
|
|
|
4785
4623
|
}
|
|
4786
4624
|
if (context.authorization === "selected_route") {
|
|
4787
4625
|
const routeState = this.routeStates.get(targetAlias);
|
|
4788
|
-
const dispatchable =
|
|
4789
|
-
|
|
4626
|
+
const dispatchable = binding.provider === "claude" ||
|
|
4627
|
+
routeState === "idle" ||
|
|
4790
4628
|
(binding.provider === "codex" &&
|
|
4791
4629
|
item.steer === true &&
|
|
4792
4630
|
routeState === "busy");
|
|
@@ -4814,8 +4652,8 @@ export class GatewayService {
|
|
|
4814
4652
|
this.scheduleDispatch(targetAlias);
|
|
4815
4653
|
return;
|
|
4816
4654
|
}
|
|
4817
|
-
const storedDispatchable =
|
|
4818
|
-
|
|
4655
|
+
const storedDispatchable = binding.provider === "claude" ||
|
|
4656
|
+
inspection?.state === "idle" ||
|
|
4819
4657
|
(binding.provider === "codex" &&
|
|
4820
4658
|
item.steer === true &&
|
|
4821
4659
|
inspection?.state === "busy");
|
|
@@ -4903,6 +4741,23 @@ export class GatewayService {
|
|
|
4903
4741
|
this.scheduleLifecycleWakeLocked();
|
|
4904
4742
|
return;
|
|
4905
4743
|
}
|
|
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();
|
|
4757
|
+
}
|
|
4758
|
+
const progressWatchActive = progressWatches.some((watch) => watch.capability === "conversation" &&
|
|
4759
|
+
watch.workerAlias === item.targetAlias &&
|
|
4760
|
+
watch.conversationId === context.conversationId);
|
|
4906
4761
|
let result;
|
|
4907
4762
|
try {
|
|
4908
4763
|
result = await this.awaitDispatchWithNativeHeldLocked(tracker, this.adapter(binding.provider, binding.hostId).dispatch({
|
|
@@ -4913,9 +4768,12 @@ export class GatewayService {
|
|
|
4913
4768
|
authorization: context.authorization,
|
|
4914
4769
|
messageId: item.messageId,
|
|
4915
4770
|
text: item.body,
|
|
4916
|
-
expectsReply: context
|
|
4771
|
+
expectsReply: context.expectsReply,
|
|
4917
4772
|
deadlineAt: item.deadlineAt,
|
|
4918
4773
|
...(item.steer === true ? { steer: true } : {}),
|
|
4774
|
+
...(progressWatchActive
|
|
4775
|
+
? { progressWatchActive: true }
|
|
4776
|
+
: {}),
|
|
4919
4777
|
}));
|
|
4920
4778
|
}
|
|
4921
4779
|
catch {
|
|
@@ -4948,10 +4806,19 @@ export class GatewayService {
|
|
|
4948
4806
|
const currentTargetAlias = this.bindingAliases.get(bindingKey(binding)) ?? targetAlias;
|
|
4949
4807
|
const now = this.now().getTime();
|
|
4950
4808
|
const deferredSafeErrorCode = safeCode(result.safeErrorCode, "PROVIDER_DISPATCH_DEFERRED");
|
|
4809
|
+
const retryDelayMs = binding.provider === "claude"
|
|
4810
|
+
? Math.min(500 * 2 ** (tracker.machine.dispatchAttempt - 1), 300_000)
|
|
4811
|
+
: 500;
|
|
4812
|
+
// Only Claude's backoff is clamped inside the deadline, so a long delay
|
|
4813
|
+
// still buys one last attempt. A Codex-bound retry keeps its exact
|
|
4814
|
+
// unclamped slice: a slice past the deadline must settle now, not wait.
|
|
4815
|
+
const retryAt = binding.provider === "claude"
|
|
4816
|
+
? Math.min(now + retryDelayMs, tracker.deadlineAt - 1)
|
|
4817
|
+
: now + retryDelayMs;
|
|
4951
4818
|
await this.advanceDeliveryLocked(item.messageId, {
|
|
4952
4819
|
type: "dispatch_clean_prewrite_failed",
|
|
4953
4820
|
at: now,
|
|
4954
|
-
retryAt
|
|
4821
|
+
retryAt,
|
|
4955
4822
|
safeErrorCode: deferredSafeErrorCode,
|
|
4956
4823
|
});
|
|
4957
4824
|
tracker.deliverySafeErrorCode = deferredSafeErrorCode;
|
|
@@ -4979,12 +4846,7 @@ export class GatewayService {
|
|
|
4979
4846
|
}
|
|
4980
4847
|
if (requeued.status === "requeued") {
|
|
4981
4848
|
if (context.authorization === "selected_route") {
|
|
4982
|
-
|
|
4983
|
-
CLAUDE_CLEAN_PREWRITE_ROUTE_CODES.has(deferredSafeErrorCode);
|
|
4984
|
-
if (waitForClaudeIdle) {
|
|
4985
|
-
this.claudePrewriteIdleWaitTargets.add(currentTargetAlias);
|
|
4986
|
-
}
|
|
4987
|
-
else if (!steeringWhileBusy) {
|
|
4849
|
+
if (binding.provider === "codex" && !steeringWhileBusy) {
|
|
4988
4850
|
this.routeStates.set(currentTargetAlias, "idle");
|
|
4989
4851
|
await this.store.observeRoute({
|
|
4990
4852
|
binding,
|
|
@@ -4992,7 +4854,7 @@ export class GatewayService {
|
|
|
4992
4854
|
compatibility: "compatible",
|
|
4993
4855
|
});
|
|
4994
4856
|
}
|
|
4995
|
-
else {
|
|
4857
|
+
else if (binding.provider === "codex") {
|
|
4996
4858
|
this.scheduleHeldRedispatch(currentTargetAlias);
|
|
4997
4859
|
}
|
|
4998
4860
|
await this.setNativeCodexStatus(currentTargetAlias, "waiting");
|
|
@@ -5024,8 +4886,7 @@ export class GatewayService {
|
|
|
5024
4886
|
scheduleDispatch(targetAlias) {
|
|
5025
4887
|
if (this.closing ||
|
|
5026
4888
|
this.codexSuccessionDispatchFrozen.has(targetAlias) ||
|
|
5027
|
-
this.codexSuccessionPoisoned.has(targetAlias)
|
|
5028
|
-
this.claudePrewriteIdleWaitTargets.has(targetAlias)) {
|
|
4889
|
+
this.codexSuccessionPoisoned.has(targetAlias)) {
|
|
5029
4890
|
return;
|
|
5030
4891
|
}
|
|
5031
4892
|
if (this.scheduledDispatchTargets.has(targetAlias))
|
|
@@ -5198,7 +5059,7 @@ export class GatewayService {
|
|
|
5198
5059
|
conversation !== undefined) {
|
|
5199
5060
|
try {
|
|
5200
5061
|
if (this.nativeIngressByConversation.has(conversation.id)) {
|
|
5201
|
-
await this.enqueueNativeReply(conversation, conversation.targetAlias, replyText,
|
|
5062
|
+
await this.enqueueNativeReply(conversation, conversation.targetAlias, replyText, false);
|
|
5202
5063
|
}
|
|
5203
5064
|
else {
|
|
5204
5065
|
const sourceBinding = this.routeBindings.get(conversation.targetAlias);
|
|
@@ -5213,12 +5074,11 @@ export class GatewayService {
|
|
|
5213
5074
|
}));
|
|
5214
5075
|
if ((!sourceStillRoutable || !pairStillActive) &&
|
|
5215
5076
|
sourceBinding?.provider === "claude") {
|
|
5216
|
-
await this.enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, replyText
|
|
5077
|
+
await this.enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, replyText);
|
|
5217
5078
|
}
|
|
5218
5079
|
else {
|
|
5219
|
-
await this.enqueue(conversation.targetAlias, conversation.sourceAlias, replyText, false,
|
|
5080
|
+
await this.enqueue(conversation.targetAlias, conversation.sourceAlias, replyText, false, {
|
|
5220
5081
|
existingConversationId: conversation.id,
|
|
5221
|
-
requestedHopCount: context.hopCount + 1,
|
|
5222
5082
|
exposeDeliveryToken: false,
|
|
5223
5083
|
});
|
|
5224
5084
|
}
|
|
@@ -5279,9 +5139,8 @@ export class GatewayService {
|
|
|
5279
5139
|
if (conversation === undefined)
|
|
5280
5140
|
return;
|
|
5281
5141
|
this.pendingClaudeReplies.delete(key);
|
|
5282
|
-
await this.enqueue(conversation.targetAlias, conversation.sourceAlias, event.text, false,
|
|
5142
|
+
await this.enqueue(conversation.targetAlias, conversation.sourceAlias, event.text, false, {
|
|
5283
5143
|
existingConversationId: conversation.id,
|
|
5284
|
-
requestedHopCount: pending.hopCount + 1,
|
|
5285
5144
|
exposeDeliveryToken: false,
|
|
5286
5145
|
});
|
|
5287
5146
|
}
|
|
@@ -5324,7 +5183,6 @@ export class GatewayService {
|
|
|
5324
5183
|
body: event.text,
|
|
5325
5184
|
dedupeKey: `${conversationId}:0`,
|
|
5326
5185
|
conversationIdSuffix: conversationId.slice(-8),
|
|
5327
|
-
hopCount: 0,
|
|
5328
5186
|
deadlineAt,
|
|
5329
5187
|
...(steer ? { steer: true } : {}),
|
|
5330
5188
|
progressWatch: progressWatchActivity(event.text, conversationId, event.sourceAlias),
|
|
@@ -5342,16 +5200,13 @@ export class GatewayService {
|
|
|
5342
5200
|
targetAlias: event.targetAlias,
|
|
5343
5201
|
expectsReply: !steer,
|
|
5344
5202
|
nextSequence: 1,
|
|
5345
|
-
lastHopCount: 0,
|
|
5346
5203
|
lastActivityAt: this.now().toISOString(),
|
|
5347
5204
|
...(queued.pair === true ? { pair: true } : {}),
|
|
5348
5205
|
};
|
|
5349
5206
|
this.conversations.set(conversationId, conversation);
|
|
5350
5207
|
this.messageContexts.set(queued.messageId, {
|
|
5351
5208
|
conversationId,
|
|
5352
|
-
isReply: false,
|
|
5353
5209
|
expectsReply: !steer,
|
|
5354
|
-
hopCount: 0,
|
|
5355
5210
|
sequence: 0,
|
|
5356
5211
|
targetBindingKey: bindingKey(target),
|
|
5357
5212
|
authorization: "selected_route",
|
|
@@ -5824,9 +5679,6 @@ export class GatewayService {
|
|
|
5824
5679
|
const [alias, binding] = entry;
|
|
5825
5680
|
const previousState = this.routeStates.get(alias);
|
|
5826
5681
|
this.routeStates.set(alias, event.state);
|
|
5827
|
-
if (binding.provider === "claude" && event.state === "idle") {
|
|
5828
|
-
this.claudePrewriteIdleWaitTargets.delete(alias);
|
|
5829
|
-
}
|
|
5830
5682
|
const staleSafeCode = binding.provider === "codex"
|
|
5831
5683
|
? "CODEX_ROUTE_STALE"
|
|
5832
5684
|
: safeCode(event.safeErrorCode, "ROUTE_STALE");
|
|
@@ -6037,21 +5889,6 @@ export class GatewayService {
|
|
|
6037
5889
|
},
|
|
6038
5890
|
];
|
|
6039
5891
|
});
|
|
6040
|
-
const certificationAlerts = compatibilityChecks.flatMap((attestation) => {
|
|
6041
|
-
const certification = attestation.certification;
|
|
6042
|
-
if (certification?.outcome !== "fail")
|
|
6043
|
-
return [];
|
|
6044
|
-
const adapter = this.adapters.find((candidate) => candidate.identity.provider === attestation.surface);
|
|
6045
|
-
return [
|
|
6046
|
-
{
|
|
6047
|
-
code: "COMPATIBILITY_CERTIFICATION_FAILED",
|
|
6048
|
-
severity: "error",
|
|
6049
|
-
timestamp: certification.certifiedAt,
|
|
6050
|
-
provider: attestation.surface,
|
|
6051
|
-
host: adapter?.identity.hostId ?? "this-mac",
|
|
6052
|
-
},
|
|
6053
|
-
];
|
|
6054
|
-
});
|
|
6055
5892
|
return projectGatewayPublicSnapshot({
|
|
6056
5893
|
...base,
|
|
6057
5894
|
...(compatibilityChecks.length === 0 ? {} : { compatibilityChecks }),
|
|
@@ -6067,7 +5904,6 @@ export class GatewayService {
|
|
|
6067
5904
|
alerts: [
|
|
6068
5905
|
...base.alerts,
|
|
6069
5906
|
...this.runtimeAlerts.map((alert) => ({ ...alert })),
|
|
6070
|
-
...certificationAlerts,
|
|
6071
5907
|
...stalledAlerts,
|
|
6072
5908
|
],
|
|
6073
5909
|
});
|