agent-embassy 1.0.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 +61 -0
- package/CONTRIBUTING.md +122 -0
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/README.zh-CN.md +185 -0
- package/SECURITY.md +224 -0
- package/assets/live-dashboard/app.css +1610 -0
- package/assets/mark-seal.svg +7 -0
- package/assets/mark.svg +7 -0
- package/assets/social-preview.png +0 -0
- package/assets/vendor/react/LICENSE +21 -0
- package/assets/vendor/react/react-dom.production.min.js +267 -0
- package/assets/vendor/react/react.production.min.js +31 -0
- package/dist/src/errors.d.ts +5 -0
- package/dist/src/errors.js +11 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/gateway/claude-compatibility-scratch.d.ts +18 -0
- package/dist/src/gateway/claude-compatibility-scratch.js +158 -0
- package/dist/src/gateway/claude-compatibility-scratch.js.map +1 -0
- package/dist/src/gateway/claude-helper-client.d.ts +43 -0
- package/dist/src/gateway/claude-helper-client.js +212 -0
- package/dist/src/gateway/claude-helper-client.js.map +1 -0
- package/dist/src/gateway/claude-helper-protocol.d.ts +172 -0
- package/dist/src/gateway/claude-helper-protocol.js +373 -0
- package/dist/src/gateway/claude-helper-protocol.js.map +1 -0
- package/dist/src/gateway/claude-helper-supervisor.d.ts +74 -0
- package/dist/src/gateway/claude-helper-supervisor.js +497 -0
- package/dist/src/gateway/claude-helper-supervisor.js.map +1 -0
- package/dist/src/gateway/claude-helper.d.ts +1 -0
- package/dist/src/gateway/claude-helper.js +269 -0
- package/dist/src/gateway/claude-helper.js.map +1 -0
- package/dist/src/gateway/claude-peer.d.ts +273 -0
- package/dist/src/gateway/claude-peer.js +2359 -0
- package/dist/src/gateway/claude-peer.js.map +1 -0
- package/dist/src/gateway/claude-runtime.d.ts +40 -0
- package/dist/src/gateway/claude-runtime.js +302 -0
- package/dist/src/gateway/claude-runtime.js.map +1 -0
- package/dist/src/gateway/cli-copy.d.ts +8 -0
- package/dist/src/gateway/cli-copy.en.d.ts +13 -0
- package/dist/src/gateway/cli-copy.en.js +43 -0
- package/dist/src/gateway/cli-copy.en.js.map +1 -0
- package/dist/src/gateway/cli-copy.js +19 -0
- package/dist/src/gateway/cli-copy.js.map +1 -0
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +13 -0
- package/dist/src/gateway/cli-copy.zh-CN.js +43 -0
- package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -0
- package/dist/src/gateway/cli.d.ts +44 -0
- package/dist/src/gateway/cli.js +969 -0
- package/dist/src/gateway/cli.js.map +1 -0
- package/dist/src/gateway/codex-app-server.d.ts +310 -0
- package/dist/src/gateway/codex-app-server.js +1746 -0
- package/dist/src/gateway/codex-app-server.js.map +1 -0
- package/dist/src/gateway/codex-local-transport.d.ts +81 -0
- package/dist/src/gateway/codex-local-transport.js +617 -0
- package/dist/src/gateway/codex-local-transport.js.map +1 -0
- package/dist/src/gateway/codex-registration-generation.d.ts +5 -0
- package/dist/src/gateway/codex-registration-generation.js +19 -0
- package/dist/src/gateway/codex-registration-generation.js.map +1 -0
- package/dist/src/gateway/codex-registration-succession.d.ts +209 -0
- package/dist/src/gateway/codex-registration-succession.js +588 -0
- package/dist/src/gateway/codex-registration-succession.js.map +1 -0
- package/dist/src/gateway/compatibility.d.ts +70 -0
- package/dist/src/gateway/compatibility.js +252 -0
- package/dist/src/gateway/compatibility.js.map +1 -0
- package/dist/src/gateway/config.d.ts +24 -0
- package/dist/src/gateway/config.js +130 -0
- package/dist/src/gateway/config.js.map +1 -0
- package/dist/src/gateway/control.d.ts +308 -0
- package/dist/src/gateway/control.js +1573 -0
- package/dist/src/gateway/control.js.map +1 -0
- package/dist/src/gateway/dashboard-copy.d.ts +7 -0
- package/dist/src/gateway/dashboard-copy.en.d.ts +506 -0
- package/dist/src/gateway/dashboard-copy.en.js +507 -0
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -0
- package/dist/src/gateway/dashboard-copy.js +516 -0
- package/dist/src/gateway/dashboard-copy.js.map +1 -0
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +506 -0
- package/dist/src/gateway/dashboard-copy.zh-CN.js +507 -0
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -0
- package/dist/src/gateway/dashboard-model.d.ts +207 -0
- package/dist/src/gateway/dashboard-model.js +720 -0
- package/dist/src/gateway/dashboard-model.js.map +1 -0
- package/dist/src/gateway/dashboard.d.ts +26 -0
- package/dist/src/gateway/dashboard.js +941 -0
- package/dist/src/gateway/dashboard.js.map +1 -0
- package/dist/src/gateway/delivery-machine.d.ts +235 -0
- package/dist/src/gateway/delivery-machine.js +540 -0
- package/dist/src/gateway/delivery-machine.js.map +1 -0
- package/dist/src/gateway/instance-lease.d.ts +21 -0
- package/dist/src/gateway/instance-lease.js +549 -0
- package/dist/src/gateway/instance-lease.js.map +1 -0
- package/dist/src/gateway/live-dashboard-app/app.js +2914 -0
- package/dist/src/gateway/live-dashboard-assets.d.ts +10 -0
- package/dist/src/gateway/live-dashboard-assets.js +73 -0
- package/dist/src/gateway/live-dashboard-assets.js.map +1 -0
- package/dist/src/gateway/live-dashboard-bootstrap.d.ts +36 -0
- package/dist/src/gateway/live-dashboard-bootstrap.js +176 -0
- package/dist/src/gateway/live-dashboard-bootstrap.js.map +1 -0
- package/dist/src/gateway/live-dashboard-command.d.ts +54 -0
- package/dist/src/gateway/live-dashboard-command.js +324 -0
- package/dist/src/gateway/live-dashboard-command.js.map +1 -0
- package/dist/src/gateway/live-dashboard-http.d.ts +41 -0
- package/dist/src/gateway/live-dashboard-http.js +445 -0
- package/dist/src/gateway/live-dashboard-http.js.map +1 -0
- package/dist/src/gateway/live-dashboard-protocol.d.ts +38 -0
- package/dist/src/gateway/live-dashboard-protocol.js +156 -0
- package/dist/src/gateway/live-dashboard-protocol.js.map +1 -0
- package/dist/src/gateway/live-dashboard-server.d.ts +33 -0
- package/dist/src/gateway/live-dashboard-server.js +130 -0
- package/dist/src/gateway/live-dashboard-server.js.map +1 -0
- package/dist/src/gateway/live-dashboard-stream.d.ts +46 -0
- package/dist/src/gateway/live-dashboard-stream.js +233 -0
- package/dist/src/gateway/live-dashboard-stream.js.map +1 -0
- package/dist/src/gateway/live-dashboard.d.ts +31 -0
- package/dist/src/gateway/live-dashboard.js +190 -0
- package/dist/src/gateway/live-dashboard.js.map +1 -0
- package/dist/src/gateway/locale.d.ts +4 -0
- package/dist/src/gateway/locale.js +10 -0
- package/dist/src/gateway/locale.js.map +1 -0
- package/dist/src/gateway/progress-watch-machine.d.ts +97 -0
- package/dist/src/gateway/progress-watch-machine.js +172 -0
- package/dist/src/gateway/progress-watch-machine.js.map +1 -0
- package/dist/src/gateway/providers.d.ts +287 -0
- package/dist/src/gateway/providers.js +2451 -0
- package/dist/src/gateway/providers.js.map +1 -0
- package/dist/src/gateway/server.d.ts +57 -0
- package/dist/src/gateway/server.js +264 -0
- package/dist/src/gateway/server.js.map +1 -0
- package/dist/src/gateway/service.d.ts +449 -0
- package/dist/src/gateway/service.js +4831 -0
- package/dist/src/gateway/service.js.map +1 -0
- package/dist/src/gateway/store.d.ts +340 -0
- package/dist/src/gateway/store.js +4047 -0
- package/dist/src/gateway/store.js.map +1 -0
- package/dist/src/gateway/types.d.ts +514 -0
- package/dist/src/gateway/types.js +414 -0
- package/dist/src/gateway/types.js.map +1 -0
- package/dist/src/mutex.d.ts +4 -0
- package/dist/src/mutex.js +23 -0
- package/dist/src/mutex.js.map +1 -0
- package/docs/DESIGN.md +134 -0
- package/docs/GATEWAY-ARCHITECTURE.md +858 -0
- package/package.json +80 -0
- package/skills/embassy-peer/SKILL.md +198 -0
- package/skills/embassy-peer/agents/openai.yaml +4 -0
|
@@ -0,0 +1,4831 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { BridgeError } from "../errors.js";
|
|
3
|
+
import { KeyedMutex } from "../mutex.js";
|
|
4
|
+
import { certifiedCompatibilityVersions, compatibilitySurfaces, attachCompatibilityCertification, evaluateCompatibilityAttestation, isCompatibilityCertification, isCompatibilityCertificationReport, isCompatibilityCheckReport, } from "./compatibility.js";
|
|
5
|
+
import { createGatewayConversationId, isGatewaySnapshot, startGatewayControlServer, } from "./control.js";
|
|
6
|
+
import { publishGatewayDashboard } from "./dashboard.js";
|
|
7
|
+
import { createDeliveryMachine, isTerminalDeliveryMachine, projectDelivery, projectDeliveryWakeups, transitionDelivery, } from "./delivery-machine.js";
|
|
8
|
+
import { createCodexRegistrationSuccession, transitionCodexRegistrationSuccession, } from "./codex-registration-succession.js";
|
|
9
|
+
import { createCodexRegistrationGeneration, isCodexRegistrationGeneration, } from "./codex-registration-generation.js";
|
|
10
|
+
import { PROGRESS_WATCH_DEFAULT_IDLE_MS } from "./progress-watch-machine.js";
|
|
11
|
+
import { GatewayStore, } from "./store.js";
|
|
12
|
+
import { GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, arePublicAvailablePeerSnapshots, projectGatewayPublicSnapshot, } from "./types.js";
|
|
13
|
+
const PUBLIC_ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
|
|
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
|
+
const SAFE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
16
|
+
const MESSAGE_ID = /^msg_[0-9a-f-]{36}$/i;
|
|
17
|
+
const DELIVERY_TOKEN = /^dlv_[A-Za-z0-9_-]{24}$/;
|
|
18
|
+
const PRIVATE_HANDLE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
|
|
19
|
+
const MAX_CONVERSATIONS = 1_024;
|
|
20
|
+
const DELIVERY_ACK_RETRY_DELAYS_MS = [250, 500, 1_000, 2_000];
|
|
21
|
+
const NATIVE_HELD_THRESHOLD_MS = 1_000;
|
|
22
|
+
const RUNTIME_ACTIVITY_CAPACITY = 256;
|
|
23
|
+
const DELIVERY_SETTLEMENT_RETRY_MS = 250;
|
|
24
|
+
const DELIVERY_DASHBOARD_REFRESH_MS = 15_000;
|
|
25
|
+
function progressWatchActivity(text, conversationId, actorAlias, explicitIdleMinutes) {
|
|
26
|
+
const prefixedOpen = text.startsWith("TRACK:");
|
|
27
|
+
const completionSignal = text.startsWith("DONE:");
|
|
28
|
+
if (completionSignal && (prefixedOpen || explicitIdleMinutes !== undefined)) {
|
|
29
|
+
throw new BridgeError("PROGRESS_WATCH_SIGNAL_CONFLICT", "A single message cannot both open and complete a progress watch.");
|
|
30
|
+
}
|
|
31
|
+
const openIdleMs = explicitIdleMinutes === undefined
|
|
32
|
+
? prefixedOpen
|
|
33
|
+
? PROGRESS_WATCH_DEFAULT_IDLE_MS
|
|
34
|
+
: undefined
|
|
35
|
+
: explicitIdleMinutes * 60_000;
|
|
36
|
+
return {
|
|
37
|
+
conversationId,
|
|
38
|
+
actorAlias,
|
|
39
|
+
...(openIdleMs === undefined ? {} : { openIdleMs }),
|
|
40
|
+
...(completionSignal ? { completionSignal: true } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function bindingKey(binding) {
|
|
44
|
+
return [
|
|
45
|
+
binding.provider,
|
|
46
|
+
binding.hostId,
|
|
47
|
+
binding.endpointGeneration,
|
|
48
|
+
binding.routeHandle,
|
|
49
|
+
binding.ownerLease,
|
|
50
|
+
].join("\0");
|
|
51
|
+
}
|
|
52
|
+
function stableLease(provider, value) {
|
|
53
|
+
return `lease_${createHash("sha256").update(provider).update("\0").update(value).digest("base64url")}`;
|
|
54
|
+
}
|
|
55
|
+
function safeCode(value, fallback) {
|
|
56
|
+
return value !== undefined && SAFE_CODE.test(value) ? value : fallback;
|
|
57
|
+
}
|
|
58
|
+
function decisionFor(error) {
|
|
59
|
+
if (!(error instanceof BridgeError)) {
|
|
60
|
+
return { accepted: false, code: "rejected" };
|
|
61
|
+
}
|
|
62
|
+
if (error.code.includes("NOT_FOUND") || error.code === "ROUTE_UNAVAILABLE") {
|
|
63
|
+
return { accepted: false, code: "not_found" };
|
|
64
|
+
}
|
|
65
|
+
if (error.code.includes("COLLISION") ||
|
|
66
|
+
error.code.includes("OWNERSHIP") ||
|
|
67
|
+
error.code.includes("REBIND")) {
|
|
68
|
+
return { accepted: false, code: "conflict" };
|
|
69
|
+
}
|
|
70
|
+
if (error.code.includes("BUSY") || error.code.includes("CAPACITY")) {
|
|
71
|
+
return { accepted: false, code: "busy" };
|
|
72
|
+
}
|
|
73
|
+
if (error.code.includes("UNAVAILABLE") ||
|
|
74
|
+
error.code.includes("OFFLINE") ||
|
|
75
|
+
error.code.includes("STALE")) {
|
|
76
|
+
return { accepted: false, code: "unavailable" };
|
|
77
|
+
}
|
|
78
|
+
if (error.code.includes("MISMATCH") || error.code.includes("ADDRESS")) {
|
|
79
|
+
return { accepted: false, code: "route_mismatch" };
|
|
80
|
+
}
|
|
81
|
+
return { accepted: false, code: "rejected" };
|
|
82
|
+
}
|
|
83
|
+
export class GatewayService {
|
|
84
|
+
config;
|
|
85
|
+
store;
|
|
86
|
+
adapters;
|
|
87
|
+
publishDashboard;
|
|
88
|
+
now;
|
|
89
|
+
timers;
|
|
90
|
+
nativePeerCwd;
|
|
91
|
+
successionGeneration;
|
|
92
|
+
mutex = new KeyedMutex();
|
|
93
|
+
routeBindings = new Map();
|
|
94
|
+
routeStates = new Map();
|
|
95
|
+
bindingAliases = new Map();
|
|
96
|
+
candidates = new Map();
|
|
97
|
+
availablePeers = [];
|
|
98
|
+
conversations = new Map();
|
|
99
|
+
messageContexts = new Map();
|
|
100
|
+
providerTurnContinuations = new Map();
|
|
101
|
+
activeDispatchByTarget = new Map();
|
|
102
|
+
scheduledDispatchTargets = new Set();
|
|
103
|
+
dispatchRunnerTargets = new Set();
|
|
104
|
+
heldRedispatchTimers = new Map();
|
|
105
|
+
pendingClaudeReplies = new Map();
|
|
106
|
+
deliveryTrackers = new Map();
|
|
107
|
+
deliveryTokens = new Map();
|
|
108
|
+
runtimeAlerts = [];
|
|
109
|
+
activityEvents = [];
|
|
110
|
+
activitySequence = 0;
|
|
111
|
+
detachedReceiptWrites = new Set();
|
|
112
|
+
nativeIngressByConversation = new Map();
|
|
113
|
+
callbackQueue = [];
|
|
114
|
+
callbackWorker;
|
|
115
|
+
callbackScheduled = false;
|
|
116
|
+
callbackCapacity;
|
|
117
|
+
deliveryCallbackReserve;
|
|
118
|
+
deliveryTokenCapacity;
|
|
119
|
+
lifecycleTimer;
|
|
120
|
+
nextDashboardRefreshAt;
|
|
121
|
+
nextProgressWatchAt;
|
|
122
|
+
control;
|
|
123
|
+
/** Coarse controller mutation revision exposed by `health`. */
|
|
124
|
+
revision = 0;
|
|
125
|
+
/**
|
|
126
|
+
* Process-local semantic public-snapshot clock. The first observation
|
|
127
|
+
* establishes revision zero; a new GatewayService process resets it.
|
|
128
|
+
*/
|
|
129
|
+
snapshotRevision = 0;
|
|
130
|
+
snapshotFingerprint;
|
|
131
|
+
running = false;
|
|
132
|
+
closing = false;
|
|
133
|
+
closeInFlight;
|
|
134
|
+
acceptingCallbacks = true;
|
|
135
|
+
dashboardHealthy = true;
|
|
136
|
+
/** Route-scoped fences; unrelated Codex registrations remain dispatchable. */
|
|
137
|
+
codexSuccessionDispatchFrozen = new Set();
|
|
138
|
+
codexSuccessionPoisoned = new Set();
|
|
139
|
+
codexSuccessionState;
|
|
140
|
+
pendingCodexSuccessionRecovery;
|
|
141
|
+
/**
|
|
142
|
+
* Exact active identity. It changes only through the journaled succession
|
|
143
|
+
* machine; ordinary unregister/register cannot repurpose one listener.
|
|
144
|
+
*/
|
|
145
|
+
codexRegistrationLocks = new Map();
|
|
146
|
+
constructor(options) {
|
|
147
|
+
this.config = options.config;
|
|
148
|
+
this.adapters = [...(options.adapters ?? [])];
|
|
149
|
+
this.publishDashboard = options.publishDashboard ?? publishGatewayDashboard;
|
|
150
|
+
this.now = options.now ?? (() => new Date());
|
|
151
|
+
this.store =
|
|
152
|
+
options.store ?? new GatewayStore(options.config, { now: this.now });
|
|
153
|
+
this.timers = options.timers ?? {
|
|
154
|
+
setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
155
|
+
clearTimeout: (timer) => clearTimeout(timer),
|
|
156
|
+
};
|
|
157
|
+
this.nativePeerCwd = options.nativePeerCwd ?? process.cwd();
|
|
158
|
+
this.successionGeneration =
|
|
159
|
+
options.successionGeneration ??
|
|
160
|
+
createCodexRegistrationGeneration;
|
|
161
|
+
this.callbackCapacity = Math.max(64, options.config.limits.maxRoutes +
|
|
162
|
+
options.config.limits.maxInFlightMessages * 8);
|
|
163
|
+
// A callback flood must never crowd out write evidence. One selected route
|
|
164
|
+
// may retain one accepted provider continuation, while each store-owned
|
|
165
|
+
// in-flight message may contribute uncertain/written/held plus one first
|
|
166
|
+
// terminal observation. Other callback kinds use only the remaining slots.
|
|
167
|
+
this.deliveryCallbackReserve = Math.min(this.callbackCapacity, options.config.limits.maxRoutes +
|
|
168
|
+
options.config.limits.maxInFlightMessages * 4);
|
|
169
|
+
this.deliveryTokenCapacity = Math.max(64, options.config.limits.maxQueueMessages * 4);
|
|
170
|
+
}
|
|
171
|
+
async start(signal) {
|
|
172
|
+
if (this.running || this.closing)
|
|
173
|
+
return;
|
|
174
|
+
const assertStartActive = () => {
|
|
175
|
+
if (signal?.aborted === true || this.closing) {
|
|
176
|
+
throw new BridgeError("GATEWAY_START_CANCELLED", "Gateway startup was cancelled before it became ready.", true);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
assertStartActive();
|
|
180
|
+
// Keep additive state migrations and startup observations in memory until
|
|
181
|
+
// every provider admission check and the private control-socket bind have
|
|
182
|
+
// succeeded. A failed upgrade must leave the prior on-disk schema usable
|
|
183
|
+
// by the previous binary.
|
|
184
|
+
await this.store.initialize({ deferPersistence: true });
|
|
185
|
+
try {
|
|
186
|
+
assertStartActive();
|
|
187
|
+
const seen = new Set();
|
|
188
|
+
for (const adapter of this.adapters) {
|
|
189
|
+
const key = `${adapter.identity.provider}@${adapter.identity.hostId}`;
|
|
190
|
+
if (seen.has(key) || !this.config.allowedHosts.includes(adapter.identity.hostId)) {
|
|
191
|
+
throw new BridgeError("GATEWAY_ADAPTER_NOT_ALLOWED", "A provider adapter is duplicated or outside the host allowlist.");
|
|
192
|
+
}
|
|
193
|
+
seen.add(key);
|
|
194
|
+
const observation = await adapter.initialize(this.callbacksFor(adapter.identity));
|
|
195
|
+
assertStartActive();
|
|
196
|
+
await this.store.observeConnector({
|
|
197
|
+
identity: adapter.identity,
|
|
198
|
+
health: observation.health,
|
|
199
|
+
compatibility: observation.compatibility,
|
|
200
|
+
protocol: adapter.protocol,
|
|
201
|
+
protocolVersion: adapter.protocolVersion,
|
|
202
|
+
...(observation.safeErrorCode === undefined
|
|
203
|
+
? {}
|
|
204
|
+
: { safeErrorCode: safeCode(observation.safeErrorCode, "ADAPTER_DEGRADED") }),
|
|
205
|
+
});
|
|
206
|
+
assertStartActive();
|
|
207
|
+
}
|
|
208
|
+
if (this.adapters.length > 0 &&
|
|
209
|
+
this.adapters.every((adapter) => adapter.compatibilitySurface !== undefined &&
|
|
210
|
+
adapter.runCompatibilityProbes !== undefined)) {
|
|
211
|
+
const report = await this.compatibilityCheckLocked();
|
|
212
|
+
const incompatible = report.surfaces.find((surface) => surface.tier === "incompatible");
|
|
213
|
+
if (incompatible !== undefined) {
|
|
214
|
+
throw new BridgeError(incompatible.safeErrorCode ?? "COMPATIBILITY_CHECK_FAILED", "A local provider failed bounded compatibility attestation.");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
await this.recoverCodexSuccessionAfterRestartLocked();
|
|
218
|
+
assertStartActive();
|
|
219
|
+
const control = await startGatewayControlServer({
|
|
220
|
+
stateDir: this.store.rootDir,
|
|
221
|
+
socketPath: this.config.controlSocketPath,
|
|
222
|
+
handlers: this.handlers(),
|
|
223
|
+
});
|
|
224
|
+
try {
|
|
225
|
+
assertStartActive();
|
|
226
|
+
await this.store.commitInitialization();
|
|
227
|
+
assertStartActive();
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
await control.close();
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
this.control = control;
|
|
234
|
+
this.running = true;
|
|
235
|
+
this.nextDashboardRefreshAt = undefined;
|
|
236
|
+
await this.refreshNextProgressWatchWakeLocked();
|
|
237
|
+
this.scheduleLifecycleWakeLocked();
|
|
238
|
+
await this.publish();
|
|
239
|
+
assertStartActive();
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
this.running = false;
|
|
243
|
+
// Startup and the server's lease-loss path may observe the same failure
|
|
244
|
+
// from opposite sides. Join the one canonical cleanup so no caller can
|
|
245
|
+
// release host ownership while an adapter/store close is still active.
|
|
246
|
+
await this.close();
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async close() {
|
|
251
|
+
if (this.closeInFlight !== undefined) {
|
|
252
|
+
return await this.closeInFlight;
|
|
253
|
+
}
|
|
254
|
+
this.closing = true;
|
|
255
|
+
const close = this.closeOnce();
|
|
256
|
+
this.closeInFlight = close;
|
|
257
|
+
return await close;
|
|
258
|
+
}
|
|
259
|
+
async closeOnce() {
|
|
260
|
+
const control = this.control;
|
|
261
|
+
this.control = undefined;
|
|
262
|
+
let closeFailed = false;
|
|
263
|
+
if (control !== undefined) {
|
|
264
|
+
try {
|
|
265
|
+
await control.close();
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
closeFailed = true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
await this.mutex.run("service", async () => {
|
|
272
|
+
try {
|
|
273
|
+
this.running = false;
|
|
274
|
+
this.acceptingCallbacks = false;
|
|
275
|
+
if (this.lifecycleTimer !== undefined) {
|
|
276
|
+
this.timers.clearTimeout(this.lifecycleTimer);
|
|
277
|
+
this.lifecycleTimer = undefined;
|
|
278
|
+
}
|
|
279
|
+
const quiesceResults = await Promise.allSettled(this.adapters.map(async (adapter) => await adapter.quiesceNativeInbound?.()));
|
|
280
|
+
if (quiesceResults.some((result) => result.status === "rejected")) {
|
|
281
|
+
closeFailed = true;
|
|
282
|
+
}
|
|
283
|
+
await this.drainCallbackQueueLocked();
|
|
284
|
+
for (const messageId of [...this.providerTurnContinuations.keys()]) {
|
|
285
|
+
await this.finishProviderTurnContinuationLocked(messageId, "cancelled");
|
|
286
|
+
}
|
|
287
|
+
for (const messageId of [...this.messageContexts.keys()]) {
|
|
288
|
+
const queued = await this.store.settleQueuedMessage({
|
|
289
|
+
messageId,
|
|
290
|
+
state: "cancelled",
|
|
291
|
+
safeErrorCode: "GATEWAY_SHUTDOWN",
|
|
292
|
+
});
|
|
293
|
+
if (queued.status === "settled") {
|
|
294
|
+
await this.applyTerminalSettlementLocked(queued.settlement);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
await this.advanceDeliveryLocked(messageId, {
|
|
298
|
+
type: "shutdown",
|
|
299
|
+
at: this.now().getTime(),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
for (const tracker of this.deliveryTrackers.values()) {
|
|
303
|
+
if (!isTerminalDeliveryMachine(tracker.machine)) {
|
|
304
|
+
await this.advanceDeliveryLocked(tracker.messageId, {
|
|
305
|
+
type: "shutdown",
|
|
306
|
+
at: this.now().getTime(),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
for (let attempt = 0; attempt <= DELIVERY_ACK_RETRY_DELAYS_MS.length &&
|
|
310
|
+
tracker.nativeReceipt !== undefined &&
|
|
311
|
+
(tracker.machine.nativeReceipt.phase === "sending" ||
|
|
312
|
+
tracker.machine.nativeReceipt.phase === "retry_wait"); attempt += 1) {
|
|
313
|
+
const native = tracker.machine.nativeReceipt;
|
|
314
|
+
if (native.phase === "retry_wait") {
|
|
315
|
+
await this.advanceDeliveryLocked(tracker.messageId, {
|
|
316
|
+
type: "native_receipt_retry_due",
|
|
317
|
+
at: native.retryAt,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
await this.sendNativeReceiptLocked(tracker);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
await this.drainDetachedReceiptWritesLocked();
|
|
326
|
+
await this.publish();
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
closeFailed = true;
|
|
330
|
+
}
|
|
331
|
+
const adapterResults = await Promise.allSettled(this.adapters.map(async (adapter) => adapter.close()));
|
|
332
|
+
for (const result of adapterResults) {
|
|
333
|
+
if (result.status === "rejected")
|
|
334
|
+
closeFailed = true;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
this.conversations.clear();
|
|
338
|
+
this.messageContexts.clear();
|
|
339
|
+
this.providerTurnContinuations.clear();
|
|
340
|
+
this.activeDispatchByTarget.clear();
|
|
341
|
+
this.scheduledDispatchTargets.clear();
|
|
342
|
+
this.dispatchRunnerTargets.clear();
|
|
343
|
+
for (const timer of this.heldRedispatchTimers.values()) {
|
|
344
|
+
this.timers.clearTimeout(timer);
|
|
345
|
+
}
|
|
346
|
+
this.heldRedispatchTimers.clear();
|
|
347
|
+
this.pendingClaudeReplies.clear();
|
|
348
|
+
this.deliveryTrackers.clear();
|
|
349
|
+
this.deliveryTokens.clear();
|
|
350
|
+
this.runtimeAlerts.length = 0;
|
|
351
|
+
this.detachedReceiptWrites.clear();
|
|
352
|
+
this.nativeIngressByConversation.clear();
|
|
353
|
+
this.callbackQueue.length = 0;
|
|
354
|
+
this.candidates.clear();
|
|
355
|
+
this.routeBindings.clear();
|
|
356
|
+
this.routeStates.clear();
|
|
357
|
+
this.bindingAliases.clear();
|
|
358
|
+
await this.store.close().catch(() => {
|
|
359
|
+
closeFailed = true;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
closeFailed = true;
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
if (closeFailed) {
|
|
367
|
+
throw new BridgeError("GATEWAY_CLEANUP_FAILED", "The gateway could not confirm cleanup of every owned resource.");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
handlers() {
|
|
371
|
+
return {
|
|
372
|
+
health: async () => ({
|
|
373
|
+
status: this.running && this.dashboardHealthy ? "ok" : "degraded",
|
|
374
|
+
revision: this.revision,
|
|
375
|
+
}),
|
|
376
|
+
registerCodex: async (params) => await this.exclusiveDecision(async () => this.registerCodex(params), {
|
|
377
|
+
kind: "registration",
|
|
378
|
+
action: params.succeedsAlias === undefined
|
|
379
|
+
? "codex_registered"
|
|
380
|
+
: "codex_succeeded",
|
|
381
|
+
aliases: [
|
|
382
|
+
...(params.succeedsAlias === undefined
|
|
383
|
+
? []
|
|
384
|
+
: [params.succeedsAlias]),
|
|
385
|
+
params.alias,
|
|
386
|
+
],
|
|
387
|
+
}),
|
|
388
|
+
unregisterCodex: async (params) => await this.exclusiveDecision(async () => this.unregisterCodex(params), {
|
|
389
|
+
kind: "registration",
|
|
390
|
+
action: "codex_unregistered",
|
|
391
|
+
aliases: [params.alias],
|
|
392
|
+
}),
|
|
393
|
+
selectClaude: async (params) => await this.exclusiveDecision(async () => this.selectClaude(params), {
|
|
394
|
+
kind: "selection",
|
|
395
|
+
action: "claude_selected",
|
|
396
|
+
aliases: PUBLIC_ALIAS.test(params.alias) ? [params.alias] : [],
|
|
397
|
+
}),
|
|
398
|
+
unselectClaude: async (params) => await this.exclusiveDecision(async () => this.unselectClaude(params), {
|
|
399
|
+
kind: "selection",
|
|
400
|
+
action: "claude_unselected",
|
|
401
|
+
aliases: PUBLIC_ALIAS.test(params.alias) ? [params.alias] : [],
|
|
402
|
+
}),
|
|
403
|
+
pair: async (params) => await this.exclusiveDecision(async () => this.pairRoutes(params), {
|
|
404
|
+
kind: "pairing",
|
|
405
|
+
action: "routes_paired",
|
|
406
|
+
aliases: [params.claudeAlias, params.codexAlias],
|
|
407
|
+
}),
|
|
408
|
+
unpair: async (params) => await this.exclusiveDecision(async () => this.unpairRoutes(params), {
|
|
409
|
+
kind: "pairing",
|
|
410
|
+
action: "routes_unpaired",
|
|
411
|
+
aliases: [params.claudeAlias, params.codexAlias],
|
|
412
|
+
}),
|
|
413
|
+
listSnapshot: async () => (await this.observeSnapshot()).snapshot,
|
|
414
|
+
observeSnapshot: async () => await this.observeSnapshot(),
|
|
415
|
+
compatibilityCheck: async () => await this.compatibilityCheck(),
|
|
416
|
+
compatibilityCertify: async (params) => await this.compatibilityCertify(params),
|
|
417
|
+
deliveryStatus: async (params) => await this.deliveryStatus(params.token),
|
|
418
|
+
untrack: async (params) => await this.exclusiveDecision(async () => {
|
|
419
|
+
const settled = await this.store.settleProgressWatch({
|
|
420
|
+
conversationId: params.conversationId,
|
|
421
|
+
outcome: "done",
|
|
422
|
+
});
|
|
423
|
+
if (!settled) {
|
|
424
|
+
throw new BridgeError("PROGRESS_WATCH_NOT_FOUND", "No active progress watch matches that conversation token.");
|
|
425
|
+
}
|
|
426
|
+
await this.changed();
|
|
427
|
+
}, { kind: "watch", action: "watch_ended", aliases: [] }),
|
|
428
|
+
sendToClaude: async (params) => await this.acceptToClaude(params),
|
|
429
|
+
sendToCodex: async (params) => await this.acceptToCodex(params),
|
|
430
|
+
reply: async (params) => await this.acceptReply(params),
|
|
431
|
+
refreshDashboard: async () => {
|
|
432
|
+
const result = await this.exclusiveDecision(async () => {
|
|
433
|
+
await this.refreshClaudeDiscovery();
|
|
434
|
+
await this.publish();
|
|
435
|
+
}, {
|
|
436
|
+
kind: "discovery",
|
|
437
|
+
action: "discovery_refreshed",
|
|
438
|
+
aliases: [],
|
|
439
|
+
});
|
|
440
|
+
return { ...result, revision: this.revision };
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
async snapshot() {
|
|
445
|
+
return (await this.observeSnapshot()).snapshot;
|
|
446
|
+
}
|
|
447
|
+
async compatibilityCheck() {
|
|
448
|
+
return await this.mutex.run("service", async () => await this.compatibilityCheckLocked());
|
|
449
|
+
}
|
|
450
|
+
async compatibilityCertify(params) {
|
|
451
|
+
const plan = await this.mutex.run("service", async () => {
|
|
452
|
+
if (!this.running || this.closing) {
|
|
453
|
+
throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "Compatibility certification requires one running broker.");
|
|
454
|
+
}
|
|
455
|
+
const report = await this.compatibilityCheckLocked();
|
|
456
|
+
const codexRoutes = (await this.store.inspectPrivateCodexRoutes()).filter((route) => route.enabled);
|
|
457
|
+
const codexRoute = params.codexAlias === undefined
|
|
458
|
+
? codexRoutes.length === 1
|
|
459
|
+
? codexRoutes[0]
|
|
460
|
+
: undefined
|
|
461
|
+
: codexRoutes.find((route) => route.alias === params.codexAlias);
|
|
462
|
+
if (codexRoute === undefined) {
|
|
463
|
+
throw new BridgeError("COMPAT_CERTIFY_CODEX_ROUTE_REQUIRED", "Choose one exact registered Codex route for live certification.");
|
|
464
|
+
}
|
|
465
|
+
const adapters = new Map(this.adapters.map((adapter) => [adapter.identity.provider, adapter]));
|
|
466
|
+
return { adapters, codexRoute, report };
|
|
467
|
+
});
|
|
468
|
+
const certifiedAt = this.now().toISOString();
|
|
469
|
+
const updated = [];
|
|
470
|
+
for (const attestation of plan.report.surfaces) {
|
|
471
|
+
const adapter = plan.adapters.get(attestation.surface);
|
|
472
|
+
let certification;
|
|
473
|
+
if (attestation.tier === "incompatible" ||
|
|
474
|
+
adapter?.runCompatibilityCertification === undefined) {
|
|
475
|
+
certification = {
|
|
476
|
+
depth: attestation.surface === "claude"
|
|
477
|
+
? "wire"
|
|
478
|
+
: params.withTurn
|
|
479
|
+
? "turn"
|
|
480
|
+
: "thread_ops",
|
|
481
|
+
outcome: "fail",
|
|
482
|
+
certifiedAt,
|
|
483
|
+
safeErrorCode: attestation.safeErrorCode ??
|
|
484
|
+
`${attestation.surface.toUpperCase()}_CERTIFICATION_UNAVAILABLE`,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
try {
|
|
489
|
+
certification = await adapter.runCompatibilityCertification({
|
|
490
|
+
controllerStateRoot: this.store.rootDir,
|
|
491
|
+
withTurn: params.withTurn,
|
|
492
|
+
...(attestation.surface === "codex"
|
|
493
|
+
? { routeHandle: plan.codexRoute.binding.routeHandle }
|
|
494
|
+
: {}),
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
certification = {
|
|
499
|
+
depth: attestation.surface === "claude"
|
|
500
|
+
? "wire"
|
|
501
|
+
: params.withTurn
|
|
502
|
+
? "turn"
|
|
503
|
+
: "thread_ops",
|
|
504
|
+
outcome: "fail",
|
|
505
|
+
certifiedAt: this.now().toISOString(),
|
|
506
|
+
safeErrorCode: `${attestation.surface.toUpperCase()}_CERTIFICATION_FAILED`,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const expectedDepth = attestation.surface === "claude"
|
|
511
|
+
? "wire"
|
|
512
|
+
: params.withTurn
|
|
513
|
+
? "turn"
|
|
514
|
+
: "thread_ops";
|
|
515
|
+
if (!isCompatibilityCertification(certification) ||
|
|
516
|
+
certification.depth !== expectedDepth) {
|
|
517
|
+
certification = {
|
|
518
|
+
depth: expectedDepth,
|
|
519
|
+
outcome: "fail",
|
|
520
|
+
certifiedAt: this.now().toISOString(),
|
|
521
|
+
safeErrorCode: `${attestation.surface.toUpperCase()}_CERTIFICATION_EVIDENCE_INVALID`,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
updated.push(attachCompatibilityCertification(attestation, certification));
|
|
525
|
+
}
|
|
526
|
+
return await this.mutex.run("service", async () => {
|
|
527
|
+
const currentCodexRoute = (await this.store.inspectPrivateCodexRoutes()).find((route) => route.enabled &&
|
|
528
|
+
route.alias === plan.codexRoute.alias &&
|
|
529
|
+
bindingKey(route.binding) === bindingKey(plan.codexRoute.binding));
|
|
530
|
+
const reconciled = updated.map((attestation) => {
|
|
531
|
+
if (attestation.surface === "codex" && currentCodexRoute === undefined) {
|
|
532
|
+
return attachCompatibilityCertification(attestation, {
|
|
533
|
+
depth: params.withTurn ? "turn" : "thread_ops",
|
|
534
|
+
outcome: "fail",
|
|
535
|
+
certifiedAt: this.now().toISOString(),
|
|
536
|
+
safeErrorCode: "CODEX_CERTIFICATION_ROUTE_CHANGED",
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
const observation = plan.adapters
|
|
540
|
+
.get(attestation.surface)
|
|
541
|
+
?.compatibilitySurface?.();
|
|
542
|
+
if (observation?.surface === attestation.surface &&
|
|
543
|
+
observation.version === attestation.version) {
|
|
544
|
+
return attestation;
|
|
545
|
+
}
|
|
546
|
+
return attachCompatibilityCertification(attestation, {
|
|
547
|
+
depth: attestation.surface === "claude"
|
|
548
|
+
? "wire"
|
|
549
|
+
: params.withTurn
|
|
550
|
+
? "turn"
|
|
551
|
+
: "thread_ops",
|
|
552
|
+
outcome: "fail",
|
|
553
|
+
certifiedAt: this.now().toISOString(),
|
|
554
|
+
safeErrorCode: `${attestation.surface.toUpperCase()}_CERTIFICATION_VERSION_CHANGED`,
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
const result = {
|
|
558
|
+
policy: plan.report.policy,
|
|
559
|
+
certified: reconciled.every((attestation) => attestation.certification?.outcome === "pass"),
|
|
560
|
+
withTurn: params.withTurn,
|
|
561
|
+
surfaces: reconciled,
|
|
562
|
+
};
|
|
563
|
+
if (!isCompatibilityCertificationReport(result)) {
|
|
564
|
+
throw new BridgeError("COMPAT_CERTIFICATION_REPORT_INVALID", "Compatibility certification evidence failed its closed report schema.");
|
|
565
|
+
}
|
|
566
|
+
for (const attestation of reconciled) {
|
|
567
|
+
await this.store.recordCompatibilityAttestation(attestation);
|
|
568
|
+
}
|
|
569
|
+
await this.changed();
|
|
570
|
+
return result;
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
async compatibilityCheckLocked() {
|
|
574
|
+
const bySurface = new Map(this.adapters.map((adapter) => {
|
|
575
|
+
if (adapter.compatibilitySurface === undefined ||
|
|
576
|
+
adapter.runCompatibilityProbes === undefined) {
|
|
577
|
+
throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "Every local provider must expose the bounded compatibility probe contract.");
|
|
578
|
+
}
|
|
579
|
+
const observation = adapter.compatibilitySurface();
|
|
580
|
+
return [observation.surface, { adapter, observation }];
|
|
581
|
+
}));
|
|
582
|
+
if (bySurface.size !== compatibilitySurfaces.length ||
|
|
583
|
+
compatibilitySurfaces.some((surface) => !bySurface.has(surface))) {
|
|
584
|
+
throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "The bounded compatibility probe requires one Claude and one Codex surface.");
|
|
585
|
+
}
|
|
586
|
+
const attestations = [];
|
|
587
|
+
for (const surface of compatibilitySurfaces) {
|
|
588
|
+
const entry = bySurface.get(surface);
|
|
589
|
+
if (entry === undefined) {
|
|
590
|
+
throw new BridgeError("COMPAT_PROVIDER_UNAVAILABLE", "A required compatibility surface is unavailable.");
|
|
591
|
+
}
|
|
592
|
+
const cached = await this.store.inspectCompatibilityAttestation(surface, entry.observation.version);
|
|
593
|
+
const probes = cached?.probes ?? (await entry.adapter.runCompatibilityProbes());
|
|
594
|
+
const checkedAt = cached?.checkedAt ?? this.now().toISOString();
|
|
595
|
+
let attestation = evaluateCompatibilityAttestation({
|
|
596
|
+
surface,
|
|
597
|
+
version: entry.observation.version,
|
|
598
|
+
checkedAt,
|
|
599
|
+
policy: this.config.compatibilityPolicy ?? "observed",
|
|
600
|
+
certifiedVersions: certifiedCompatibilityVersions[surface],
|
|
601
|
+
probes,
|
|
602
|
+
});
|
|
603
|
+
if (cached?.certification !== undefined &&
|
|
604
|
+
attestation.tier !== "incompatible") {
|
|
605
|
+
attestation = attachCompatibilityCertification(attestation, cached.certification);
|
|
606
|
+
}
|
|
607
|
+
if (cached === undefined ||
|
|
608
|
+
JSON.stringify(cached) !== JSON.stringify(attestation)) {
|
|
609
|
+
await this.store.recordCompatibilityAttestation(attestation);
|
|
610
|
+
}
|
|
611
|
+
if (attestation.tier !== "incompatible") {
|
|
612
|
+
entry.adapter.acceptCompatibilityAttestation?.(attestation);
|
|
613
|
+
}
|
|
614
|
+
attestations.push(attestation);
|
|
615
|
+
}
|
|
616
|
+
const report = {
|
|
617
|
+
policy: this.config.compatibilityPolicy ?? "observed",
|
|
618
|
+
compatible: attestations.every((attestation) => attestation.tier !== "incompatible"),
|
|
619
|
+
surfaces: attestations,
|
|
620
|
+
};
|
|
621
|
+
if (!isCompatibilityCheckReport(report)) {
|
|
622
|
+
throw new BridgeError("COMPAT_REPORT_INVALID", "Compatibility evidence failed its closed report schema.");
|
|
623
|
+
}
|
|
624
|
+
return report;
|
|
625
|
+
}
|
|
626
|
+
async observeSnapshot() {
|
|
627
|
+
return await this.mutex.run("service", async () => {
|
|
628
|
+
const changed = await this.processLifecycleLocked();
|
|
629
|
+
if (changed) {
|
|
630
|
+
this.revision += 1;
|
|
631
|
+
}
|
|
632
|
+
const snapshot = await this.publicSnapshotLocked();
|
|
633
|
+
if (!isGatewaySnapshot(snapshot)) {
|
|
634
|
+
throw new BridgeError("INVALID_PUBLIC_SNAPSHOT", "The gateway public snapshot failed its closed-schema validation.");
|
|
635
|
+
}
|
|
636
|
+
const fingerprint = this.fingerprintSnapshot(snapshot);
|
|
637
|
+
if (this.snapshotFingerprint === undefined) {
|
|
638
|
+
this.snapshotFingerprint = fingerprint;
|
|
639
|
+
}
|
|
640
|
+
else if (this.snapshotFingerprint !== fingerprint) {
|
|
641
|
+
if (this.snapshotRevision >= Number.MAX_SAFE_INTEGER) {
|
|
642
|
+
throw new BridgeError("SNAPSHOT_REVISION_EXHAUSTED", "The process-local public snapshot revision is exhausted.");
|
|
643
|
+
}
|
|
644
|
+
this.snapshotFingerprint = fingerprint;
|
|
645
|
+
this.snapshotRevision += 1;
|
|
646
|
+
}
|
|
647
|
+
if (changed)
|
|
648
|
+
await this.publish(snapshot);
|
|
649
|
+
return {
|
|
650
|
+
snapshotRevision: this.snapshotRevision,
|
|
651
|
+
snapshot,
|
|
652
|
+
};
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
fingerprintSnapshot(snapshot) {
|
|
656
|
+
// Closed snapshot validation bounds every array and field before this
|
|
657
|
+
// serialization. Omitting generatedAt is the sole semantic exception.
|
|
658
|
+
const { generatedAt: _generatedAt, ...semanticSnapshot } = snapshot;
|
|
659
|
+
const encoded = JSON.stringify(semanticSnapshot);
|
|
660
|
+
if (Buffer.byteLength(encoded, "utf8") >
|
|
661
|
+
GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET) {
|
|
662
|
+
throw new BridgeError("PUBLIC_SNAPSHOT_FINGERPRINT_TOO_LARGE", "The gateway public snapshot exceeds the semantic fingerprint budget.");
|
|
663
|
+
}
|
|
664
|
+
return createHash("sha256").update(encoded, "utf8").digest("base64url");
|
|
665
|
+
}
|
|
666
|
+
callbacksFor(source) {
|
|
667
|
+
return {
|
|
668
|
+
onDelivery: (event) => {
|
|
669
|
+
if (!this.running ||
|
|
670
|
+
!MESSAGE_ID.test(event.messageId) ||
|
|
671
|
+
this.deliveryCorrelation(event.messageId) === undefined ||
|
|
672
|
+
(event.safeErrorCode !== undefined && !SAFE_CODE.test(event.safeErrorCode)) ||
|
|
673
|
+
(event.replyText !== undefined &&
|
|
674
|
+
(event.replyText.length === 0 ||
|
|
675
|
+
Buffer.byteLength(event.replyText, "utf8") >
|
|
676
|
+
this.config.limits.maxMessageBytes))) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const context = this.deliveryCorrelation(event.messageId);
|
|
680
|
+
const target = context === undefined
|
|
681
|
+
? undefined
|
|
682
|
+
: this.contextTargetBinding(context);
|
|
683
|
+
if (target === undefined ||
|
|
684
|
+
target.provider !== source.provider ||
|
|
685
|
+
target.hostId !== source.hostId ||
|
|
686
|
+
target.endpointGeneration !== source.endpointGeneration) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
this.enqueueCallback({
|
|
690
|
+
type: "delivery",
|
|
691
|
+
source: { ...source },
|
|
692
|
+
value: { ...event },
|
|
693
|
+
receivedAt: this.now().getTime(),
|
|
694
|
+
});
|
|
695
|
+
},
|
|
696
|
+
onRouteState: (event) => {
|
|
697
|
+
if (!this.running ||
|
|
698
|
+
event.endpoint.provider !== source.provider ||
|
|
699
|
+
event.endpoint.hostId !== source.hostId ||
|
|
700
|
+
event.endpoint.endpointGeneration !== source.endpointGeneration ||
|
|
701
|
+
!PRIVATE_HANDLE.test(event.endpoint.routeHandle) ||
|
|
702
|
+
(event.safeErrorCode !== undefined &&
|
|
703
|
+
!SAFE_CODE.test(event.safeErrorCode))) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
this.enqueueCallback({
|
|
707
|
+
type: "route",
|
|
708
|
+
source: { ...source },
|
|
709
|
+
value: {
|
|
710
|
+
routeHandle: event.endpoint.routeHandle,
|
|
711
|
+
state: event.state,
|
|
712
|
+
...(event.safeErrorCode === undefined
|
|
713
|
+
? {}
|
|
714
|
+
: { safeErrorCode: event.safeErrorCode }),
|
|
715
|
+
},
|
|
716
|
+
});
|
|
717
|
+
},
|
|
718
|
+
onClaudeReply: (event) => {
|
|
719
|
+
if (!this.running ||
|
|
720
|
+
event.endpoint.provider !== source.provider ||
|
|
721
|
+
event.endpoint.hostId !== source.hostId ||
|
|
722
|
+
event.endpoint.endpointGeneration !== source.endpointGeneration ||
|
|
723
|
+
!PRIVATE_HANDLE.test(event.endpoint.routeHandle) ||
|
|
724
|
+
event.text.length === 0 ||
|
|
725
|
+
Buffer.byteLength(event.text, "utf8") >
|
|
726
|
+
this.config.limits.maxMessageBytes) {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const selected = [...this.routeBindings.values()].filter((binding) => binding.provider === event.endpoint.provider &&
|
|
730
|
+
binding.hostId === event.endpoint.hostId &&
|
|
731
|
+
binding.endpointGeneration ===
|
|
732
|
+
event.endpoint.endpointGeneration &&
|
|
733
|
+
binding.routeHandle === event.endpoint.routeHandle);
|
|
734
|
+
if (selected.length !== 1 ||
|
|
735
|
+
selected[0] === undefined ||
|
|
736
|
+
!this.pendingClaudeReplies.has(bindingKey(selected[0]))) {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
this.enqueueCallback({
|
|
740
|
+
type: "claude_reply",
|
|
741
|
+
value: { endpoint: { ...event.endpoint }, text: event.text },
|
|
742
|
+
});
|
|
743
|
+
},
|
|
744
|
+
onClaudeMessage: (event) => {
|
|
745
|
+
const ingressFailureCode = this.closing
|
|
746
|
+
? "GATEWAY_SHUTDOWN"
|
|
747
|
+
: "NATIVE_INGRESS_INVALID";
|
|
748
|
+
const invalid = !this.running ||
|
|
749
|
+
source.provider !== "claude" ||
|
|
750
|
+
event.endpoint.provider !== source.provider ||
|
|
751
|
+
event.endpoint.hostId !== source.hostId ||
|
|
752
|
+
event.endpoint.endpointGeneration !== source.endpointGeneration ||
|
|
753
|
+
!PRIVATE_HANDLE.test(event.endpoint.routeHandle) ||
|
|
754
|
+
!PUBLIC_ALIAS.test(event.sourceAlias) ||
|
|
755
|
+
!PUBLIC_ALIAS.test(event.targetAlias) ||
|
|
756
|
+
event.text.length === 0 ||
|
|
757
|
+
Buffer.byteLength(event.text, "utf8") >
|
|
758
|
+
this.config.limits.maxMessageBytes;
|
|
759
|
+
if (invalid) {
|
|
760
|
+
if (source.provider === "claude" && event.receiptHandle !== undefined) {
|
|
761
|
+
this.rejectDetachedNativeReceipt(source.hostId, event.receiptHandle, ingressFailureCode);
|
|
762
|
+
}
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const retained = this.enqueueCallback({
|
|
766
|
+
type: "claude_message",
|
|
767
|
+
value: {
|
|
768
|
+
endpoint: { ...event.endpoint },
|
|
769
|
+
sourceAlias: event.sourceAlias,
|
|
770
|
+
targetAlias: event.targetAlias,
|
|
771
|
+
text: event.text,
|
|
772
|
+
...(event.receiptHandle === undefined
|
|
773
|
+
? {}
|
|
774
|
+
: { receiptHandle: event.receiptHandle }),
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
if (!retained && event.receiptHandle !== undefined) {
|
|
778
|
+
this.rejectDetachedNativeReceipt(source.hostId, event.receiptHandle, "GATEWAY_CALLBACK_CAPACITY");
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
onProtocolNotice: (event) => {
|
|
782
|
+
if (!this.running || !SAFE_CODE.test(event.code))
|
|
783
|
+
return;
|
|
784
|
+
this.enqueueCallback({
|
|
785
|
+
type: "protocol_notice",
|
|
786
|
+
source: { ...source },
|
|
787
|
+
value: { code: event.code },
|
|
788
|
+
});
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
enqueueCallback(event) {
|
|
793
|
+
if (!this.acceptingCallbacks)
|
|
794
|
+
return false;
|
|
795
|
+
if (event.type === "delivery") {
|
|
796
|
+
const duplicate = this.callbackQueue.findIndex((candidate) => candidate.type === "delivery" &&
|
|
797
|
+
candidate.value.messageId === event.value.messageId &&
|
|
798
|
+
candidate.value.state === event.value.state);
|
|
799
|
+
if (duplicate >= 0) {
|
|
800
|
+
const retained = this.callbackQueue[duplicate];
|
|
801
|
+
// The first service-boundary observation is authoritative. Replacing
|
|
802
|
+
// delivery evidence with a duplicate observed at/after the cutoff
|
|
803
|
+
// would erase proof that the provider acted before the deadline.
|
|
804
|
+
if (retained?.type !== "delivery" ||
|
|
805
|
+
event.receivedAt < retained.receivedAt) {
|
|
806
|
+
this.callbackQueue[duplicate] = event;
|
|
807
|
+
}
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
if (this.isTerminalAdapterDelivery(event.value)) {
|
|
811
|
+
const firstTerminal = this.callbackQueue.findIndex((candidate) => candidate.type === "delivery" &&
|
|
812
|
+
candidate.value.messageId === event.value.messageId &&
|
|
813
|
+
this.isTerminalAdapterDelivery(candidate.value));
|
|
814
|
+
if (firstTerminal >= 0) {
|
|
815
|
+
const retained = this.callbackQueue[firstTerminal];
|
|
816
|
+
if (retained?.type !== "delivery" ||
|
|
817
|
+
event.receivedAt < retained.receivedAt) {
|
|
818
|
+
this.callbackQueue[firstTerminal] = event;
|
|
819
|
+
}
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
else if (this.callbackQueue.length >=
|
|
825
|
+
this.callbackCapacity - this.deliveryCallbackReserve) {
|
|
826
|
+
// Preserve a fixed authority-bearing delivery reserve. Native ingress
|
|
827
|
+
// gets an immediate terminal receipt from its caller when this returns
|
|
828
|
+
// false; route observations are refreshable and replies remain bounded
|
|
829
|
+
// by their one-owner capability.
|
|
830
|
+
this.dashboardHealthy = false;
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
if (this.callbackQueue.length >= this.callbackCapacity) {
|
|
834
|
+
// Delivery write/approval observations are authoritative evidence for
|
|
835
|
+
// no-replay and deadline arbitration. Never evict them. A stale route
|
|
836
|
+
// observation is safe to replace because discovery re-observes it.
|
|
837
|
+
const replaceable = this.callbackQueue.findIndex((candidate) => candidate.type === "route");
|
|
838
|
+
if (replaceable < 0) {
|
|
839
|
+
this.dashboardHealthy = false;
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
this.callbackQueue.splice(replaceable, 1);
|
|
843
|
+
}
|
|
844
|
+
this.callbackQueue.push(event);
|
|
845
|
+
if (this.callbackScheduled)
|
|
846
|
+
return true;
|
|
847
|
+
this.callbackScheduled = true;
|
|
848
|
+
setImmediate(() => {
|
|
849
|
+
this.callbackScheduled = false;
|
|
850
|
+
const worker = this.mutex.run("service", async () => {
|
|
851
|
+
await this.drainCallbackQueueLocked();
|
|
852
|
+
});
|
|
853
|
+
this.callbackWorker = worker;
|
|
854
|
+
worker
|
|
855
|
+
.catch(() => {
|
|
856
|
+
this.dashboardHealthy = false;
|
|
857
|
+
})
|
|
858
|
+
.finally(() => {
|
|
859
|
+
if (this.callbackWorker === worker)
|
|
860
|
+
this.callbackWorker = undefined;
|
|
861
|
+
if (this.callbackQueue.length > 0 && !this.closing) {
|
|
862
|
+
this.enqueueCallbackWorkerOnly();
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
return true;
|
|
867
|
+
}
|
|
868
|
+
enqueueCallbackWorkerOnly() {
|
|
869
|
+
if (this.callbackScheduled)
|
|
870
|
+
return;
|
|
871
|
+
this.callbackScheduled = true;
|
|
872
|
+
setImmediate(() => {
|
|
873
|
+
this.callbackScheduled = false;
|
|
874
|
+
const worker = this.mutex.run("service", async () => {
|
|
875
|
+
await this.drainCallbackQueueLocked();
|
|
876
|
+
});
|
|
877
|
+
this.callbackWorker = worker;
|
|
878
|
+
worker
|
|
879
|
+
.catch(() => {
|
|
880
|
+
this.dashboardHealthy = false;
|
|
881
|
+
})
|
|
882
|
+
.finally(() => {
|
|
883
|
+
if (this.callbackWorker === worker)
|
|
884
|
+
this.callbackWorker = undefined;
|
|
885
|
+
if (this.callbackQueue.length > 0 && !this.closing) {
|
|
886
|
+
this.enqueueCallbackWorkerOnly();
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
async drainCallbackQueueLocked() {
|
|
892
|
+
const lifecycleChanged = await this.processLifecycleLocked();
|
|
893
|
+
if (lifecycleChanged) {
|
|
894
|
+
this.revision += 1;
|
|
895
|
+
await this.publish();
|
|
896
|
+
}
|
|
897
|
+
this.pruneTransient();
|
|
898
|
+
while (this.callbackQueue.length > 0) {
|
|
899
|
+
const event = this.callbackQueue.shift();
|
|
900
|
+
if (event === undefined)
|
|
901
|
+
continue;
|
|
902
|
+
try {
|
|
903
|
+
if (event.type === "delivery") {
|
|
904
|
+
await this.onDelivery(event.source, event.value, event.receivedAt);
|
|
905
|
+
}
|
|
906
|
+
else if (event.type === "route") {
|
|
907
|
+
await this.onRouteState(event.source, event.value);
|
|
908
|
+
}
|
|
909
|
+
else if (event.type === "claude_reply") {
|
|
910
|
+
await this.onClaudeReply(event.value);
|
|
911
|
+
}
|
|
912
|
+
else if (event.type === "claude_message") {
|
|
913
|
+
await this.onClaudeMessage(event.value);
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
this.addRuntimeAlert(event.value.code, "warning", {
|
|
917
|
+
provider: event.source.provider,
|
|
918
|
+
host: event.source.hostId,
|
|
919
|
+
});
|
|
920
|
+
await this.changed();
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
this.dashboardHealthy = false;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
isTerminalAdapterDelivery(event) {
|
|
929
|
+
return (event.state !== "transport_uncertain" &&
|
|
930
|
+
event.state !== "transport_written" &&
|
|
931
|
+
event.state !== "held");
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Delivery evidence observed strictly before the deadline must reach the
|
|
935
|
+
* reducer before its deadline event, even when callback scheduling is late.
|
|
936
|
+
* Evidence observed at/after the cutoff cannot reopen the terminal machine.
|
|
937
|
+
*/
|
|
938
|
+
async drainPreDeadlineDeliveryCallbacksLocked() {
|
|
939
|
+
while (true) {
|
|
940
|
+
const index = this.callbackQueue.findIndex((candidate) => {
|
|
941
|
+
if (candidate.type !== "delivery") {
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
const context = this.deliveryCorrelation(candidate.value.messageId);
|
|
945
|
+
return (context !== undefined &&
|
|
946
|
+
candidate.receivedAt < Date.parse(context.deadlineAt));
|
|
947
|
+
});
|
|
948
|
+
if (index < 0)
|
|
949
|
+
return;
|
|
950
|
+
const [event] = this.callbackQueue.splice(index, 1);
|
|
951
|
+
if (event?.type === "delivery") {
|
|
952
|
+
await this.onDelivery(event.source, event.value, event.receivedAt);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* A provider may synchronously emit a terminal callback before its dispatch
|
|
958
|
+
* promise resolves to `pending`. Consume those already-retained callbacks
|
|
959
|
+
* before interpreting `pending` as successful provider acceptance, otherwise
|
|
960
|
+
* the sender can observe a false delivered result that wins the terminal
|
|
961
|
+
* race against the queued failure.
|
|
962
|
+
*/
|
|
963
|
+
async drainDeliveryCallbacksForMessageLocked(messageId) {
|
|
964
|
+
while (true) {
|
|
965
|
+
const index = this.callbackQueue.findIndex((candidate) => {
|
|
966
|
+
if (candidate.type !== "delivery" ||
|
|
967
|
+
candidate.value.messageId !== messageId) {
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
const context = this.deliveryCorrelation(messageId);
|
|
971
|
+
return (context !== undefined &&
|
|
972
|
+
candidate.receivedAt < Date.parse(context.deadlineAt));
|
|
973
|
+
});
|
|
974
|
+
if (index < 0)
|
|
975
|
+
return;
|
|
976
|
+
const [event] = this.callbackQueue.splice(index, 1);
|
|
977
|
+
if (event?.type === "delivery") {
|
|
978
|
+
await this.onDelivery(event.source, event.value, event.receivedAt);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
adapter(provider, host) {
|
|
983
|
+
const adapter = this.adapters.find((candidate) => candidate.identity.provider === provider && candidate.identity.hostId === host);
|
|
984
|
+
if (adapter === undefined)
|
|
985
|
+
throw new BridgeError("PROVIDER_UNAVAILABLE", "The selected provider is unavailable.", true);
|
|
986
|
+
return adapter;
|
|
987
|
+
}
|
|
988
|
+
async assertClaudeWorkspaceDisjoint(adapter, routeHandle) {
|
|
989
|
+
if (adapter.identity.provider !== "claude") {
|
|
990
|
+
throw new BridgeError("CLAUDE_PROVIDER_MISMATCH", "The selected workspace guard does not belong to Claude.");
|
|
991
|
+
}
|
|
992
|
+
const assertWorkspaceDisjoint = adapter.assertWorkspaceDisjoint;
|
|
993
|
+
if (assertWorkspaceDisjoint === undefined) {
|
|
994
|
+
throw new BridgeError("CLAUDE_WORKSPACE_ATTESTATION_UNAVAILABLE", "The selected Claude provider cannot revalidate its workspace.", true);
|
|
995
|
+
}
|
|
996
|
+
await assertWorkspaceDisjoint.call(adapter, routeHandle, this.store.rootDir);
|
|
997
|
+
}
|
|
998
|
+
contextTargetBinding(context) {
|
|
999
|
+
if (context.nativeReplyBinding !== undefined) {
|
|
1000
|
+
return context.nativeReplyBinding;
|
|
1001
|
+
}
|
|
1002
|
+
return [...this.routeBindings.values()].find((binding) => bindingKey(binding) === context.targetBindingKey);
|
|
1003
|
+
}
|
|
1004
|
+
async recoverCodexSuccessionAfterRestartLocked() {
|
|
1005
|
+
const authority = await this.store.inspectCodexSuccessionRecoveryAuthority();
|
|
1006
|
+
if (authority.authority === "none")
|
|
1007
|
+
return;
|
|
1008
|
+
const exact = {
|
|
1009
|
+
oldGeneration: authority.journal.old.generation,
|
|
1010
|
+
newGeneration: authority.journal.new.generation,
|
|
1011
|
+
};
|
|
1012
|
+
if (authority.authority === "old") {
|
|
1013
|
+
// A prepared listener is process-owned and cannot survive restart. The
|
|
1014
|
+
// durable store proves publication was never armed, so the stale old
|
|
1015
|
+
// route remains the sole authority and may be explicitly re-observed.
|
|
1016
|
+
await this.store.clearCodexSuccession(exact);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
// Armed or later recovery is canonicalized by the store to the new route.
|
|
1020
|
+
// Only an exact re-registration of that new task may clear the journal;
|
|
1021
|
+
// the old task is never restored after this boundary.
|
|
1022
|
+
this.pendingCodexSuccessionRecovery = {
|
|
1023
|
+
authority: "new",
|
|
1024
|
+
journal: authority.journal,
|
|
1025
|
+
};
|
|
1026
|
+
this.codexRegistrationLocks.set(authority.journal.new.alias, Object.freeze({
|
|
1027
|
+
alias: authority.journal.new.alias,
|
|
1028
|
+
threadId: authority.journal.new.threadId,
|
|
1029
|
+
hostId: authority.journal.new.hostId,
|
|
1030
|
+
generation: authority.journal.new.generation,
|
|
1031
|
+
}));
|
|
1032
|
+
}
|
|
1033
|
+
async completePendingCodexSuccessionRecoveryLocked(params) {
|
|
1034
|
+
const recovery = this.pendingCodexSuccessionRecovery;
|
|
1035
|
+
if (recovery === undefined)
|
|
1036
|
+
return;
|
|
1037
|
+
this.assertPendingCodexSuccessionRecoveryMatch(params, recovery);
|
|
1038
|
+
await this.store.completeCodexSuccession({
|
|
1039
|
+
oldGeneration: recovery.journal.old.generation,
|
|
1040
|
+
newGeneration: recovery.journal.new.generation,
|
|
1041
|
+
});
|
|
1042
|
+
this.pendingCodexSuccessionRecovery = undefined;
|
|
1043
|
+
}
|
|
1044
|
+
assertPendingCodexSuccessionRecoveryMatch(params, recovery = this.pendingCodexSuccessionRecovery) {
|
|
1045
|
+
if (recovery === undefined)
|
|
1046
|
+
return;
|
|
1047
|
+
if (recovery.journal.new.alias !== params.alias ||
|
|
1048
|
+
recovery.journal.new.threadId !== params.threadId ||
|
|
1049
|
+
recovery.journal.new.hostId !== params.hostId) {
|
|
1050
|
+
throw new BridgeError("CODEX_SUCCESSION_RECOVERY_MISMATCH", "Only the exact successor task may recover this Codex registration.");
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
requireCurrentNativeCodexGeneration(adapter, alias) {
|
|
1054
|
+
const current = adapter.currentNativeCodexPeerGeneration;
|
|
1055
|
+
if (current === undefined) {
|
|
1056
|
+
throw new BridgeError("CODEX_SUCCESSION_UNAVAILABLE", "The Claude provider cannot identify its active Codex listener generation.");
|
|
1057
|
+
}
|
|
1058
|
+
const generation = current.call(adapter, alias);
|
|
1059
|
+
if (!isCodexRegistrationGeneration(generation)) {
|
|
1060
|
+
throw new BridgeError("CODEX_SUCCESSION_GENERATION_INVALID", "The active Codex listener generation is malformed.");
|
|
1061
|
+
}
|
|
1062
|
+
return generation;
|
|
1063
|
+
}
|
|
1064
|
+
nextCodexSuccessionGeneration(oldGeneration) {
|
|
1065
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
1066
|
+
const generation = this.successionGeneration();
|
|
1067
|
+
if (isCodexRegistrationGeneration(generation) &&
|
|
1068
|
+
generation !== oldGeneration) {
|
|
1069
|
+
return generation;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
throw new BridgeError("CODEX_SUCCESSION_GENERATION_UNAVAILABLE", "A fresh Codex listener generation could not be allocated.");
|
|
1073
|
+
}
|
|
1074
|
+
successionStoreIdentity(registration, binding) {
|
|
1075
|
+
return {
|
|
1076
|
+
...registration,
|
|
1077
|
+
binding: { ...binding },
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
sameSuccessionStoreIdentity(observed, registration, binding) {
|
|
1081
|
+
return (observed.alias === registration.alias &&
|
|
1082
|
+
observed.threadId === registration.threadId &&
|
|
1083
|
+
observed.hostId === registration.hostId &&
|
|
1084
|
+
observed.generation === registration.generation &&
|
|
1085
|
+
bindingKey(observed.binding) === bindingKey(binding));
|
|
1086
|
+
}
|
|
1087
|
+
async reconcilePreparedCodexSuccessionAfterUnknownCommit(execution) {
|
|
1088
|
+
let authority;
|
|
1089
|
+
try {
|
|
1090
|
+
authority =
|
|
1091
|
+
await this.store.inspectCodexSuccessionRecoveryAuthority();
|
|
1092
|
+
}
|
|
1093
|
+
catch {
|
|
1094
|
+
return false;
|
|
1095
|
+
}
|
|
1096
|
+
if (authority.authority !== "old" ||
|
|
1097
|
+
authority.journal.stage !== "prepared" ||
|
|
1098
|
+
!this.sameSuccessionStoreIdentity(authority.journal.old, execution.oldRegistration, execution.oldBinding) ||
|
|
1099
|
+
!this.sameSuccessionStoreIdentity(authority.journal.new, execution.newRegistration, execution.newBinding)) {
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
execution.storePrepared = true;
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
requireSuccessionMethod(adapter, key) {
|
|
1106
|
+
const method = adapter[key];
|
|
1107
|
+
if (typeof method !== "function") {
|
|
1108
|
+
throw new BridgeError("CODEX_SUCCESSION_UNAVAILABLE", "A provider does not support exact Codex registration succession.");
|
|
1109
|
+
}
|
|
1110
|
+
return method;
|
|
1111
|
+
}
|
|
1112
|
+
async succeedCodexRegistration(params) {
|
|
1113
|
+
const oldAlias = params.succeedsAlias;
|
|
1114
|
+
const lock = oldAlias === undefined
|
|
1115
|
+
? undefined
|
|
1116
|
+
: this.codexRegistrationLocks.get(oldAlias);
|
|
1117
|
+
if (oldAlias === undefined ||
|
|
1118
|
+
lock === undefined ||
|
|
1119
|
+
lock.alias !== oldAlias ||
|
|
1120
|
+
lock.hostId !== params.hostId ||
|
|
1121
|
+
lock.alias === params.alias ||
|
|
1122
|
+
lock.threadId === params.threadId ||
|
|
1123
|
+
this.pendingCodexSuccessionRecovery !== undefined) {
|
|
1124
|
+
throw new BridgeError("CODEX_SUCCESSION_OWNER_MISMATCH", "A successor must name the exact active Codex registration on the same host.");
|
|
1125
|
+
}
|
|
1126
|
+
if (this.codexSuccessionState !== undefined &&
|
|
1127
|
+
this.codexSuccessionState.phase !== "active_old" &&
|
|
1128
|
+
!(this.codexSuccessionState.phase === "active_new" &&
|
|
1129
|
+
this.codexSuccessionState.retired === null)) {
|
|
1130
|
+
throw new BridgeError("CODEX_SUCCESSION_ALREADY_ACTIVE", "Another Codex registration succession is still active.", true);
|
|
1131
|
+
}
|
|
1132
|
+
const oldInspection = await this.store.inspectPrivateRoute(oldAlias);
|
|
1133
|
+
if (oldInspection === undefined ||
|
|
1134
|
+
oldInspection.binding.provider !== "codex" ||
|
|
1135
|
+
oldInspection.binding.hostId !== lock.hostId ||
|
|
1136
|
+
oldInspection.binding.routeHandle !== lock.threadId ||
|
|
1137
|
+
oldInspection.binding.ownerLease !==
|
|
1138
|
+
stableLease("codex", `${lock.hostId}\0${lock.threadId}`) ||
|
|
1139
|
+
!oldInspection.enabled ||
|
|
1140
|
+
oldInspection.compatibility !== "compatible") {
|
|
1141
|
+
throw new BridgeError("CODEX_SUCCESSION_OWNER_MISMATCH", "The active Codex registration is no longer exact and compatible.");
|
|
1142
|
+
}
|
|
1143
|
+
const claudeAdapter = this.adapter("claude", params.hostId);
|
|
1144
|
+
const observedGeneration = this.requireCurrentNativeCodexGeneration(claudeAdapter, oldAlias);
|
|
1145
|
+
if (lock.generation !== observedGeneration) {
|
|
1146
|
+
throw new BridgeError("CODEX_SUCCESSION_GENERATION_MISMATCH", "The active Codex listener generation changed before succession.");
|
|
1147
|
+
}
|
|
1148
|
+
const newGeneration = this.nextCodexSuccessionGeneration(observedGeneration);
|
|
1149
|
+
const oldRegistration = {
|
|
1150
|
+
alias: oldAlias,
|
|
1151
|
+
threadId: lock.threadId,
|
|
1152
|
+
hostId: lock.hostId,
|
|
1153
|
+
generation: observedGeneration,
|
|
1154
|
+
};
|
|
1155
|
+
const newRegistration = {
|
|
1156
|
+
alias: params.alias,
|
|
1157
|
+
threadId: params.threadId,
|
|
1158
|
+
hostId: params.hostId,
|
|
1159
|
+
generation: newGeneration,
|
|
1160
|
+
};
|
|
1161
|
+
const codexAdapter = this.adapter("codex", params.hostId);
|
|
1162
|
+
const newBinding = {
|
|
1163
|
+
...codexAdapter.identity,
|
|
1164
|
+
routeHandle: params.threadId,
|
|
1165
|
+
ownerLease: stableLease("codex", `${params.hostId}\0${params.threadId}`),
|
|
1166
|
+
};
|
|
1167
|
+
const execution = {
|
|
1168
|
+
state: createCodexRegistrationSuccession(oldRegistration),
|
|
1169
|
+
oldRegistration,
|
|
1170
|
+
newRegistration,
|
|
1171
|
+
oldBinding: { ...oldInspection.binding },
|
|
1172
|
+
newBinding,
|
|
1173
|
+
newCodexSelected: false,
|
|
1174
|
+
newListenerPrepared: false,
|
|
1175
|
+
newListenerActivated: false,
|
|
1176
|
+
storePrepared: false,
|
|
1177
|
+
publicationAbsenceConfirmed: false,
|
|
1178
|
+
poisonCleanupReady: false,
|
|
1179
|
+
storePrepareOutcomeUnproven: false,
|
|
1180
|
+
recoveryFailed: false,
|
|
1181
|
+
};
|
|
1182
|
+
this.codexSuccessionState = execution.state;
|
|
1183
|
+
await this.driveCodexSuccessionLocked(execution, {
|
|
1184
|
+
type: "begin",
|
|
1185
|
+
registration: newRegistration,
|
|
1186
|
+
});
|
|
1187
|
+
await this.changed().catch(() => {
|
|
1188
|
+
this.dashboardHealthy = false;
|
|
1189
|
+
});
|
|
1190
|
+
if (execution.requestFailureCode !== undefined) {
|
|
1191
|
+
throw new BridgeError(execution.requestFailureCode, this.codexSuccessionPoisoned.has(oldRegistration.alias) ||
|
|
1192
|
+
this.codexSuccessionPoisoned.has(newRegistration.alias)
|
|
1193
|
+
? "Codex succession requires manual recovery."
|
|
1194
|
+
: "Codex succession did not pass its quiescence or publication boundary.", execution.requestFailureCode.includes("BUSY"));
|
|
1195
|
+
}
|
|
1196
|
+
if (execution.state.phase !== "active_new" ||
|
|
1197
|
+
execution.state.retired !== null) {
|
|
1198
|
+
throw new BridgeError("CODEX_SUCCESSION_INCOMPLETE", "Codex succession did not reach a stable active generation.");
|
|
1199
|
+
}
|
|
1200
|
+
this.codexRegistrationLocks.delete(oldRegistration.alias);
|
|
1201
|
+
this.codexRegistrationLocks.set(newRegistration.alias, Object.freeze({ ...newRegistration }));
|
|
1202
|
+
if (execution.newRouteState === "idle") {
|
|
1203
|
+
this.scheduleDispatch(newRegistration.alias);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
async driveCodexSuccessionLocked(execution, event, recoverFailures = true) {
|
|
1207
|
+
const transition = transitionCodexRegistrationSuccession(execution.state, event);
|
|
1208
|
+
execution.state = transition.state;
|
|
1209
|
+
this.codexSuccessionState = transition.state;
|
|
1210
|
+
for (const effect of transition.effects) {
|
|
1211
|
+
try {
|
|
1212
|
+
const next = await this.executeCodexSuccessionEffectLocked(execution, effect);
|
|
1213
|
+
if (next !== undefined) {
|
|
1214
|
+
if (!recoverFailures && execution.recoveryFailed) {
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
await this.driveCodexSuccessionLocked(execution, next, recoverFailures);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
catch (error) {
|
|
1222
|
+
const phase = this.codexSuccessionFailurePhase(effect);
|
|
1223
|
+
const code = safeCode(error instanceof BridgeError ? error.code : undefined, `CODEX_SUCCESSION_${phase.toUpperCase()}_FAILED`);
|
|
1224
|
+
execution.requestFailureCode ??= code;
|
|
1225
|
+
if (effect.type === "prepare_new_store" &&
|
|
1226
|
+
execution.storePrepareOutcomeUnproven) {
|
|
1227
|
+
this.dashboardHealthy = false;
|
|
1228
|
+
await this.driveCodexSuccessionLocked(execution, {
|
|
1229
|
+
type: "restart_evidence",
|
|
1230
|
+
generation: this.successionNewRegistration(execution).generation,
|
|
1231
|
+
publication: "unknown",
|
|
1232
|
+
safeErrorCode: code,
|
|
1233
|
+
}, false);
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
if (!recoverFailures) {
|
|
1237
|
+
if (phase === "resume" && execution.state.phase === "resuming_old") {
|
|
1238
|
+
execution.recoveryFailed = false;
|
|
1239
|
+
await this.recoverCodexSuccessionLocked(execution, phase, code);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
execution.recoveryFailed = true;
|
|
1243
|
+
this.dashboardHealthy = false;
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
await this.recoverCodexSuccessionLocked(execution, phase, code);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
async recoverCodexSuccessionLocked(execution, phase, safeErrorCode) {
|
|
1252
|
+
await this.driveCodexSuccessionLocked(execution, {
|
|
1253
|
+
type: "phase_failed",
|
|
1254
|
+
generation: this.successionNewRegistration(execution).generation,
|
|
1255
|
+
phase,
|
|
1256
|
+
safeErrorCode,
|
|
1257
|
+
}, false);
|
|
1258
|
+
if (execution.recoveryFailed && execution.state.phase === "recovery_required") {
|
|
1259
|
+
execution.recoveryFailed = false;
|
|
1260
|
+
await this.driveCodexSuccessionLocked(execution, {
|
|
1261
|
+
type: "phase_failed",
|
|
1262
|
+
generation: this.successionNewRegistration(execution).generation,
|
|
1263
|
+
phase: "cleanup",
|
|
1264
|
+
safeErrorCode: "CODEX_SUCCESSION_CLEANUP_FAILED",
|
|
1265
|
+
}, false);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
successionNewRegistration(execution) {
|
|
1269
|
+
return execution.newRegistration;
|
|
1270
|
+
}
|
|
1271
|
+
codexSuccessionFailurePhase(effect) {
|
|
1272
|
+
switch (effect.type) {
|
|
1273
|
+
case "freeze_old_ingress":
|
|
1274
|
+
case "freeze_old_dispatch":
|
|
1275
|
+
case "quiesce_and_join_old":
|
|
1276
|
+
return "freeze";
|
|
1277
|
+
case "verify_full_barrier":
|
|
1278
|
+
return "barrier";
|
|
1279
|
+
case "create_fresh_listener_generation":
|
|
1280
|
+
return "listener";
|
|
1281
|
+
case "purge_old_conversations":
|
|
1282
|
+
case "purge_old_reply_capabilities":
|
|
1283
|
+
case "prepare_new_store":
|
|
1284
|
+
return "store";
|
|
1285
|
+
case "arm_publication_journal":
|
|
1286
|
+
return "publication_arm";
|
|
1287
|
+
case "publish_new_registry":
|
|
1288
|
+
return "registry";
|
|
1289
|
+
case "activate_new_registration":
|
|
1290
|
+
return "activation";
|
|
1291
|
+
case "retire_old_generation":
|
|
1292
|
+
case "close_old_listener":
|
|
1293
|
+
return "retirement";
|
|
1294
|
+
case "cleanup_unpublished_generation":
|
|
1295
|
+
case "poison_new_generation":
|
|
1296
|
+
case "take_registrations_offline":
|
|
1297
|
+
case "cleanup_poisoned_generations":
|
|
1298
|
+
case "manual_recovery_required":
|
|
1299
|
+
return "cleanup";
|
|
1300
|
+
case "resume_old_ingress":
|
|
1301
|
+
case "resume_old_dispatch":
|
|
1302
|
+
return "resume";
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
async executeCodexSuccessionEffectLocked(execution, effect) {
|
|
1306
|
+
const newRegistration = this.successionNewRegistration(execution);
|
|
1307
|
+
const claudeAdapter = this.adapter("claude", newRegistration.hostId);
|
|
1308
|
+
const codexAdapter = this.adapter("codex", newRegistration.hostId);
|
|
1309
|
+
const exact = {
|
|
1310
|
+
oldGeneration: execution.oldRegistration.generation,
|
|
1311
|
+
newGeneration: newRegistration.generation,
|
|
1312
|
+
};
|
|
1313
|
+
switch (effect.type) {
|
|
1314
|
+
case "freeze_old_ingress": {
|
|
1315
|
+
const quiesce = this.requireSuccessionMethod(claudeAdapter, "quiesceNativeCodexPeerGeneration");
|
|
1316
|
+
await quiesce.call(claudeAdapter, effect.registration.generation);
|
|
1317
|
+
return undefined;
|
|
1318
|
+
}
|
|
1319
|
+
case "freeze_old_dispatch":
|
|
1320
|
+
this.codexSuccessionDispatchFrozen.add(execution.oldRegistration.alias);
|
|
1321
|
+
this.codexSuccessionDispatchFrozen.add(execution.newRegistration.alias);
|
|
1322
|
+
this.scheduledDispatchTargets.delete(execution.oldRegistration.alias);
|
|
1323
|
+
this.scheduledDispatchTargets.delete(execution.newRegistration.alias);
|
|
1324
|
+
return undefined;
|
|
1325
|
+
case "quiesce_and_join_old":
|
|
1326
|
+
await this.drainCallbackQueueLocked();
|
|
1327
|
+
await this.drainDetachedReceiptWritesLocked();
|
|
1328
|
+
return undefined;
|
|
1329
|
+
case "verify_full_barrier": {
|
|
1330
|
+
if (await this.codexSuccessionBarrierCleanLocked(effect.registration)) {
|
|
1331
|
+
return { type: "barrier_clean", generation: newRegistration.generation };
|
|
1332
|
+
}
|
|
1333
|
+
execution.requestFailureCode ??= "CODEX_SUCCESSION_BARRIER_BUSY";
|
|
1334
|
+
return {
|
|
1335
|
+
type: "barrier_busy",
|
|
1336
|
+
generation: newRegistration.generation,
|
|
1337
|
+
safeErrorCode: "CODEX_SUCCESSION_BARRIER_BUSY",
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
case "create_fresh_listener_generation": {
|
|
1341
|
+
const prepare = this.requireSuccessionMethod(claudeAdapter, "prepareNativeCodexPeerGeneration");
|
|
1342
|
+
await prepare.call(claudeAdapter, {
|
|
1343
|
+
alias: effect.registration.alias,
|
|
1344
|
+
cwd: this.nativePeerCwd,
|
|
1345
|
+
generation: effect.registration.generation,
|
|
1346
|
+
currentGeneration: execution.oldRegistration.generation,
|
|
1347
|
+
});
|
|
1348
|
+
execution.newListenerPrepared = true;
|
|
1349
|
+
return {
|
|
1350
|
+
type: "listener_prepared",
|
|
1351
|
+
generation: effect.registration.generation,
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
case "purge_old_conversations":
|
|
1355
|
+
// The barrier proves no accepted body or live continuation remains.
|
|
1356
|
+
// Inert conversation identifiers are deliberately not inherited by a
|
|
1357
|
+
// successor identity.
|
|
1358
|
+
this.purgeSuccessionTransientState(new Set([
|
|
1359
|
+
execution.oldRegistration.alias,
|
|
1360
|
+
execution.newRegistration.alias,
|
|
1361
|
+
]));
|
|
1362
|
+
return undefined;
|
|
1363
|
+
case "purge_old_reply_capabilities": {
|
|
1364
|
+
this.purgeSuccessionTransientState(new Set([
|
|
1365
|
+
execution.oldRegistration.alias,
|
|
1366
|
+
execution.newRegistration.alias,
|
|
1367
|
+
]));
|
|
1368
|
+
const purge = this.requireSuccessionMethod(claudeAdapter, "purgeNativeCodexPeerGenerationReplyCapabilities");
|
|
1369
|
+
await purge.call(claudeAdapter, effect.registration.generation);
|
|
1370
|
+
return undefined;
|
|
1371
|
+
}
|
|
1372
|
+
case "prepare_new_store": {
|
|
1373
|
+
const selected = await codexAdapter.selectRoute({
|
|
1374
|
+
alias: effect.registration.alias,
|
|
1375
|
+
routeHandle: effect.registration.threadId,
|
|
1376
|
+
});
|
|
1377
|
+
execution.newCodexSelected = true;
|
|
1378
|
+
if (selected.routeHandle !== effect.registration.threadId) {
|
|
1379
|
+
throw new BridgeError("ROUTE_MISMATCH", "The connector prepared a different successor task.");
|
|
1380
|
+
}
|
|
1381
|
+
execution.newRouteState = selected.state;
|
|
1382
|
+
try {
|
|
1383
|
+
await this.store.prepareCodexSuccession({
|
|
1384
|
+
old: this.successionStoreIdentity(this.successionOldRegistration(execution), execution.oldBinding),
|
|
1385
|
+
new: this.successionStoreIdentity(effect.registration, execution.newBinding),
|
|
1386
|
+
});
|
|
1387
|
+
execution.storePrepared = true;
|
|
1388
|
+
}
|
|
1389
|
+
catch (error) {
|
|
1390
|
+
if (error instanceof BridgeError &&
|
|
1391
|
+
error.code === "GATEWAY_STATE_COMMIT_OUTCOME_UNKNOWN") {
|
|
1392
|
+
const reconciled = await this.reconcilePreparedCodexSuccessionAfterUnknownCommit(execution);
|
|
1393
|
+
if (!reconciled)
|
|
1394
|
+
execution.storePrepareOutcomeUnproven = true;
|
|
1395
|
+
}
|
|
1396
|
+
throw error;
|
|
1397
|
+
}
|
|
1398
|
+
return {
|
|
1399
|
+
type: "store_prepared",
|
|
1400
|
+
generation: effect.registration.generation,
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
case "arm_publication_journal":
|
|
1404
|
+
await this.store.armCodexSuccessionPublication(exact);
|
|
1405
|
+
return {
|
|
1406
|
+
type: "publication_armed",
|
|
1407
|
+
generation: effect.registration.generation,
|
|
1408
|
+
};
|
|
1409
|
+
case "publish_new_registry": {
|
|
1410
|
+
const publish = this.requireSuccessionMethod(claudeAdapter, "publishPreparedNativeCodexPeer");
|
|
1411
|
+
const outcome = await publish.call(claudeAdapter, {
|
|
1412
|
+
currentGeneration: exact.oldGeneration,
|
|
1413
|
+
preparedGeneration: exact.newGeneration,
|
|
1414
|
+
});
|
|
1415
|
+
if (outcome === "not_published") {
|
|
1416
|
+
execution.publicationAbsenceConfirmed = true;
|
|
1417
|
+
execution.requestFailureCode ??=
|
|
1418
|
+
"CODEX_SUCCESSION_PUBLICATION_NOT_PUBLISHED";
|
|
1419
|
+
return {
|
|
1420
|
+
type: "publication_absence_confirmed",
|
|
1421
|
+
generation: exact.newGeneration,
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
if (outcome === "unknown") {
|
|
1425
|
+
throw new BridgeError("CODEX_SUCCESSION_PUBLICATION_UNKNOWN", "The successor registry publication outcome is unknown.");
|
|
1426
|
+
}
|
|
1427
|
+
await this.store.markCodexSuccessionPublished(exact);
|
|
1428
|
+
return {
|
|
1429
|
+
type: "registry_published",
|
|
1430
|
+
generation: exact.newGeneration,
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
case "activate_new_registration": {
|
|
1434
|
+
const state = execution.newRouteState;
|
|
1435
|
+
if (state === undefined) {
|
|
1436
|
+
throw new BridgeError("CODEX_SUCCESSION_STATE_MISMATCH", "The successor task has no prepared route state.");
|
|
1437
|
+
}
|
|
1438
|
+
// Durable route authority changes before the prepared listener is
|
|
1439
|
+
// granted ingress. A published listener cannot accept a body while
|
|
1440
|
+
// the store still authorizes the old task.
|
|
1441
|
+
await this.store.activateCodexSuccession({ ...exact, state });
|
|
1442
|
+
const activate = this.requireSuccessionMethod(claudeAdapter, "activatePreparedNativeCodexPeerGeneration");
|
|
1443
|
+
await activate.call(claudeAdapter, effect.registration.generation);
|
|
1444
|
+
execution.newListenerActivated = true;
|
|
1445
|
+
this.forgetBinding(this.successionOldRegistration(execution).alias);
|
|
1446
|
+
this.rememberBinding(effect.registration.alias, execution.newBinding, state);
|
|
1447
|
+
this.codexRegistrationLocks.delete(this.successionOldRegistration(execution).alias);
|
|
1448
|
+
this.codexRegistrationLocks.set(effect.registration.alias, Object.freeze({ ...effect.registration }));
|
|
1449
|
+
await claudeAdapter.updateNativeCodexPeerStatus?.(effect.registration.alias, state === "idle"
|
|
1450
|
+
? "idle"
|
|
1451
|
+
: state === "awaiting_approval"
|
|
1452
|
+
? "waiting"
|
|
1453
|
+
: "busy");
|
|
1454
|
+
return { type: "activate", generation: effect.registration.generation };
|
|
1455
|
+
}
|
|
1456
|
+
case "retire_old_generation":
|
|
1457
|
+
await codexAdapter.releaseRoute?.(effect.registration.threadId);
|
|
1458
|
+
return undefined;
|
|
1459
|
+
case "close_old_listener": {
|
|
1460
|
+
const retire = this.requireSuccessionMethod(claudeAdapter, "retireNativeCodexPeerGeneration");
|
|
1461
|
+
await retire.call(claudeAdapter, {
|
|
1462
|
+
retiredGeneration: effect.registryUnlink.onlyIfOwnedGeneration,
|
|
1463
|
+
protectedActiveGeneration: effect.registryUnlink.protectedActiveGeneration,
|
|
1464
|
+
});
|
|
1465
|
+
await this.store.completeCodexSuccession(exact);
|
|
1466
|
+
this.codexSuccessionDispatchFrozen.delete(execution.oldRegistration.alias);
|
|
1467
|
+
this.codexSuccessionDispatchFrozen.delete(execution.newRegistration.alias);
|
|
1468
|
+
return {
|
|
1469
|
+
type: "cleanup_confirmed",
|
|
1470
|
+
generation: newRegistration.generation,
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
case "cleanup_unpublished_generation": {
|
|
1474
|
+
let cleanupError;
|
|
1475
|
+
if (execution.newListenerPrepared) {
|
|
1476
|
+
try {
|
|
1477
|
+
const cleanup = this.requireSuccessionMethod(claudeAdapter, "cleanupPreparedNativeCodexPeerGeneration");
|
|
1478
|
+
await cleanup.call(claudeAdapter, effect.registration.generation);
|
|
1479
|
+
execution.newListenerPrepared = false;
|
|
1480
|
+
}
|
|
1481
|
+
catch (error) {
|
|
1482
|
+
cleanupError = error;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
if (execution.newCodexSelected) {
|
|
1486
|
+
try {
|
|
1487
|
+
await codexAdapter.releaseRoute?.(effect.registration.threadId);
|
|
1488
|
+
execution.newCodexSelected = false;
|
|
1489
|
+
}
|
|
1490
|
+
catch (error) {
|
|
1491
|
+
cleanupError ??= error;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
if (execution.storePrepared) {
|
|
1495
|
+
try {
|
|
1496
|
+
await this.store.clearCodexSuccession({
|
|
1497
|
+
...exact,
|
|
1498
|
+
...(execution.publicationAbsenceConfirmed
|
|
1499
|
+
? { publicationAbsenceConfirmed: true }
|
|
1500
|
+
: {}),
|
|
1501
|
+
});
|
|
1502
|
+
execution.storePrepared = false;
|
|
1503
|
+
}
|
|
1504
|
+
catch (error) {
|
|
1505
|
+
cleanupError ??= error;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
if (cleanupError !== undefined)
|
|
1509
|
+
throw cleanupError;
|
|
1510
|
+
return {
|
|
1511
|
+
type: "cleanup_confirmed",
|
|
1512
|
+
generation: effect.registration.generation,
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
case "resume_old_ingress": {
|
|
1516
|
+
const resume = this.requireSuccessionMethod(claudeAdapter, "resumeNativeCodexPeerGeneration");
|
|
1517
|
+
await resume.call(claudeAdapter, effect.registration.generation);
|
|
1518
|
+
return undefined;
|
|
1519
|
+
}
|
|
1520
|
+
case "resume_old_dispatch":
|
|
1521
|
+
this.codexSuccessionDispatchFrozen.delete(execution.oldRegistration.alias);
|
|
1522
|
+
this.codexSuccessionDispatchFrozen.delete(execution.newRegistration.alias);
|
|
1523
|
+
if (this.routeStates.get(execution.oldRegistration.alias) === "idle") {
|
|
1524
|
+
this.scheduleDispatch(execution.oldRegistration.alias);
|
|
1525
|
+
}
|
|
1526
|
+
return {
|
|
1527
|
+
type: "resume_confirmed",
|
|
1528
|
+
generation: newRegistration.generation,
|
|
1529
|
+
};
|
|
1530
|
+
case "poison_new_generation":
|
|
1531
|
+
this.codexSuccessionPoisoned.add(execution.oldRegistration.alias);
|
|
1532
|
+
this.codexSuccessionPoisoned.add(execution.newRegistration.alias);
|
|
1533
|
+
if (execution.state.phase === "offline_poisoned" &&
|
|
1534
|
+
execution.state.rollback === "old_allowed") {
|
|
1535
|
+
return undefined;
|
|
1536
|
+
}
|
|
1537
|
+
await this.store.forbidCodexSuccessionRecovery({
|
|
1538
|
+
...exact,
|
|
1539
|
+
safeErrorCode: execution.requestFailureCode ?? "CODEX_SUCCESSION_RECOVERY_REQUIRED",
|
|
1540
|
+
});
|
|
1541
|
+
return undefined;
|
|
1542
|
+
case "take_registrations_offline": {
|
|
1543
|
+
if (execution.state.phase === "offline_poisoned" &&
|
|
1544
|
+
execution.state.rollback === "old_allowed" &&
|
|
1545
|
+
execution.state.failedPhase !== "resume") {
|
|
1546
|
+
return undefined;
|
|
1547
|
+
}
|
|
1548
|
+
await this.quiesceAndDrainPoisonedSuccessionLocked(execution, claudeAdapter);
|
|
1549
|
+
const route = (await this.store.inspectPrivateRoute(effect.newRegistration.alias)) ??
|
|
1550
|
+
(await this.store.inspectPrivateRoute(effect.oldRegistration.alias));
|
|
1551
|
+
if (route !== undefined && route.enabled) {
|
|
1552
|
+
const invalidated = await this.store.invalidateRoute(route.binding, "CODEX_SUCCESSION_RECOVERY_REQUIRED");
|
|
1553
|
+
for (const settlement of invalidated) {
|
|
1554
|
+
await this.applyTerminalSettlementLocked(settlement);
|
|
1555
|
+
}
|
|
1556
|
+
const settlements = await this.store.disableRoute(route.alias, route.binding.ownerLease);
|
|
1557
|
+
for (const settlement of settlements) {
|
|
1558
|
+
await this.applyTerminalSettlementLocked(settlement);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
this.forgetBinding(effect.oldRegistration.alias);
|
|
1562
|
+
this.forgetBinding(effect.newRegistration.alias);
|
|
1563
|
+
await this.drainCallbackQueueLocked();
|
|
1564
|
+
await this.drainDetachedReceiptWritesLocked();
|
|
1565
|
+
this.purgeSuccessionTransientState(new Set([
|
|
1566
|
+
execution.oldRegistration.alias,
|
|
1567
|
+
execution.newRegistration.alias,
|
|
1568
|
+
]));
|
|
1569
|
+
if (!(await this.codexSuccessionPoisonBarrierCleanLocked(execution, claudeAdapter, codexAdapter))) {
|
|
1570
|
+
throw new BridgeError("CODEX_SUCCESSION_DRAIN_INCOMPLETE", "Interrupted succession work did not reach a clean stopping point. Restart the gateway with embassy serve.");
|
|
1571
|
+
}
|
|
1572
|
+
execution.poisonCleanupReady = true;
|
|
1573
|
+
return undefined;
|
|
1574
|
+
}
|
|
1575
|
+
case "cleanup_poisoned_generations": {
|
|
1576
|
+
if (execution.state.phase === "offline_poisoned" &&
|
|
1577
|
+
execution.state.rollback === "old_allowed" &&
|
|
1578
|
+
execution.state.failedPhase !== "resume") {
|
|
1579
|
+
await this.executeCodexSuccessionEffectLocked(execution, {
|
|
1580
|
+
type: "cleanup_unpublished_generation",
|
|
1581
|
+
registration: effect.newRegistration,
|
|
1582
|
+
});
|
|
1583
|
+
this.codexSuccessionPoisoned.delete(execution.oldRegistration.alias);
|
|
1584
|
+
this.codexSuccessionPoisoned.delete(execution.newRegistration.alias);
|
|
1585
|
+
return {
|
|
1586
|
+
type: "cleanup_confirmed",
|
|
1587
|
+
generation: effect.newRegistration.generation,
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
if (!execution.poisonCleanupReady) {
|
|
1591
|
+
throw new BridgeError("CODEX_SUCCESSION_DRAIN_INCOMPLETE", "Interrupted generations cannot close before pending work and receipts are drained. Restart the gateway with embassy serve.");
|
|
1592
|
+
}
|
|
1593
|
+
const results = await Promise.allSettled([
|
|
1594
|
+
claudeAdapter.unadvertiseNativeCodexPeer?.(execution.oldRegistration.alias),
|
|
1595
|
+
claudeAdapter.unadvertiseNativeCodexPeer?.(execution.newRegistration.alias),
|
|
1596
|
+
codexAdapter.releaseRoute?.(execution.oldRegistration.threadId),
|
|
1597
|
+
codexAdapter.releaseRoute?.(execution.newRegistration.threadId),
|
|
1598
|
+
]);
|
|
1599
|
+
if (results.some((result) => result.status === "rejected")) {
|
|
1600
|
+
throw new BridgeError("CODEX_SUCCESSION_CLEANUP_FAILED", "The poisoned Codex generations could not be fully closed.");
|
|
1601
|
+
}
|
|
1602
|
+
this.codexRegistrationLocks.delete(execution.oldRegistration.alias);
|
|
1603
|
+
this.codexRegistrationLocks.delete(execution.newRegistration.alias);
|
|
1604
|
+
return {
|
|
1605
|
+
type: "cleanup_confirmed",
|
|
1606
|
+
generation: effect.newRegistration.generation,
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
case "manual_recovery_required":
|
|
1610
|
+
this.dashboardHealthy = false;
|
|
1611
|
+
this.addRuntimeAlert(effect.safeErrorCode, "error", {
|
|
1612
|
+
provider: "codex",
|
|
1613
|
+
host: effect.registration.hostId,
|
|
1614
|
+
});
|
|
1615
|
+
return undefined;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
successionOldRegistration(execution) {
|
|
1619
|
+
return execution.oldRegistration;
|
|
1620
|
+
}
|
|
1621
|
+
async quiesceAndDrainPoisonedSuccessionLocked(execution, claudeAdapter) {
|
|
1622
|
+
const current = this.requireSuccessionMethod(claudeAdapter, "currentNativeCodexPeerGeneration");
|
|
1623
|
+
const candidates = execution.newListenerActivated
|
|
1624
|
+
? [execution.newRegistration, execution.oldRegistration]
|
|
1625
|
+
: [execution.oldRegistration, execution.newRegistration];
|
|
1626
|
+
let active;
|
|
1627
|
+
for (const candidate of candidates) {
|
|
1628
|
+
try {
|
|
1629
|
+
if (current.call(claudeAdapter, candidate.alias) === candidate.generation) {
|
|
1630
|
+
active = candidate;
|
|
1631
|
+
break;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
catch {
|
|
1635
|
+
// Exact-alias lookup is intentionally probed for both bounded
|
|
1636
|
+
// generations. A partially failed activation must not be guessed.
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
if (active === undefined) {
|
|
1640
|
+
throw new BridgeError("CODEX_SUCCESSION_ACTIVE_GENERATION_UNKNOWN", "The active Codex listener generation could not be proven before cleanup.");
|
|
1641
|
+
}
|
|
1642
|
+
const quiesce = this.requireSuccessionMethod(claudeAdapter, "quiesceNativeCodexPeerGeneration");
|
|
1643
|
+
await quiesce.call(claudeAdapter, active.generation);
|
|
1644
|
+
// Quiescence joins native ingress. Process every callback admitted before
|
|
1645
|
+
// the join, then join the detached terminal receipt writes those callbacks
|
|
1646
|
+
// may create, before any listener capability is closed.
|
|
1647
|
+
await this.drainCallbackQueueLocked();
|
|
1648
|
+
await this.drainDetachedReceiptWritesLocked();
|
|
1649
|
+
const codexAdapter = this.adapter("codex", active.hostId);
|
|
1650
|
+
if (!(await this.codexSuccessionPoisonBarrierCleanLocked(execution, claudeAdapter, codexAdapter, active))) {
|
|
1651
|
+
throw new BridgeError("CODEX_SUCCESSION_DRAIN_INCOMPLETE", "Interrupted succession work did not reach a clean pre-cleanup point. Restart the gateway with embassy serve.");
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
codexRouteSuccessionQuiet(observation) {
|
|
1655
|
+
return (observation.clean ||
|
|
1656
|
+
(!observation.routePresent &&
|
|
1657
|
+
observation.connection === "absent" &&
|
|
1658
|
+
observation.routeStatus === "absent" &&
|
|
1659
|
+
observation.queueDepth === 0 &&
|
|
1660
|
+
!observation.hasActiveTurn &&
|
|
1661
|
+
!observation.requestInFlight &&
|
|
1662
|
+
!observation.routeCreationInFlight &&
|
|
1663
|
+
!observation.routeReleaseInFlight &&
|
|
1664
|
+
observation.pendingReplyCorrelations === 0 &&
|
|
1665
|
+
observation.pendingCallbacks === 0));
|
|
1666
|
+
}
|
|
1667
|
+
conversationTouchesAliases(conversationId, aliases) {
|
|
1668
|
+
const conversation = this.conversations.get(conversationId);
|
|
1669
|
+
return (conversation !== undefined &&
|
|
1670
|
+
(aliases.has(conversation.sourceAlias) ||
|
|
1671
|
+
aliases.has(conversation.targetAlias)));
|
|
1672
|
+
}
|
|
1673
|
+
successionServiceStateClean(aliases) {
|
|
1674
|
+
const contextTouches = (context) => aliases.has(context.targetAlias) ||
|
|
1675
|
+
this.conversationTouchesAliases(context.conversationId, aliases);
|
|
1676
|
+
return ([...this.messageContexts.values()].every((context) => !contextTouches(context)) &&
|
|
1677
|
+
[...this.providerTurnContinuations.values()].every((context) => !contextTouches(context)) &&
|
|
1678
|
+
[...this.activeDispatchByTarget.keys()].every((alias) => !aliases.has(alias)) &&
|
|
1679
|
+
[...this.scheduledDispatchTargets].every((alias) => !aliases.has(alias)) &&
|
|
1680
|
+
[...this.dispatchRunnerTargets].every((alias) => !aliases.has(alias)) &&
|
|
1681
|
+
[...this.pendingClaudeReplies.values()].every((pending) => !this.conversationTouchesAliases(pending.conversationId, aliases)) &&
|
|
1682
|
+
[...this.nativeIngressByConversation.entries()].every(([conversationId, capability]) => !aliases.has(capability.sourceAlias) &&
|
|
1683
|
+
!this.conversationTouchesAliases(conversationId, aliases)) &&
|
|
1684
|
+
[...this.deliveryTrackers.values()].every((tracker) => (!aliases.has(tracker.targetAlias) &&
|
|
1685
|
+
!this.conversationTouchesAliases(tracker.conversationId, aliases)) ||
|
|
1686
|
+
(isTerminalDeliveryMachine(tracker.machine) &&
|
|
1687
|
+
tracker.pendingTerminalEvent === undefined &&
|
|
1688
|
+
tracker.pendingTerminalReplyText === undefined &&
|
|
1689
|
+
tracker.settlementRetryAt === undefined &&
|
|
1690
|
+
tracker.nativeReceipt === undefined)));
|
|
1691
|
+
}
|
|
1692
|
+
purgeSuccessionTransientState(aliases) {
|
|
1693
|
+
const conversationIds = new Set([...this.conversations.values()]
|
|
1694
|
+
.filter((conversation) => aliases.has(conversation.sourceAlias) ||
|
|
1695
|
+
aliases.has(conversation.targetAlias))
|
|
1696
|
+
.map((conversation) => conversation.id));
|
|
1697
|
+
for (const conversationId of conversationIds) {
|
|
1698
|
+
this.conversations.delete(conversationId);
|
|
1699
|
+
this.nativeIngressByConversation.delete(conversationId);
|
|
1700
|
+
}
|
|
1701
|
+
for (const [key, pending] of this.pendingClaudeReplies) {
|
|
1702
|
+
if (conversationIds.has(pending.conversationId)) {
|
|
1703
|
+
this.pendingClaudeReplies.delete(key);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
async codexSuccessionPoisonBarrierCleanLocked(execution, claudeAdapter, codexAdapter, knownActive) {
|
|
1708
|
+
const active = knownActive ??
|
|
1709
|
+
(execution.newListenerActivated
|
|
1710
|
+
? execution.newRegistration
|
|
1711
|
+
: execution.oldRegistration);
|
|
1712
|
+
const observeClaude = this.requireSuccessionMethod(claudeAdapter, "observeNativeCodexSuccessionBarrier");
|
|
1713
|
+
const claude = await observeClaude.call(claudeAdapter, active.generation);
|
|
1714
|
+
const observeCodex = this.requireSuccessionMethod(codexAdapter, "observeRouteSuccessionBarrier");
|
|
1715
|
+
const codexRoutes = [execution.oldRegistration];
|
|
1716
|
+
if (execution.newCodexSelected)
|
|
1717
|
+
codexRoutes.push(execution.newRegistration);
|
|
1718
|
+
const codexQuiet = codexRoutes.every((registration) => this.codexRouteSuccessionQuiet(observeCodex.call(codexAdapter, registration.threadId)));
|
|
1719
|
+
const persistedRoute = (await this.store.inspectPrivateRoute(execution.newRegistration.alias)) ??
|
|
1720
|
+
(await this.store.inspectPrivateRoute(execution.oldRegistration.alias));
|
|
1721
|
+
if (persistedRoute === undefined)
|
|
1722
|
+
return false;
|
|
1723
|
+
const store = await this.store.inspectCodexSuccessionBarrier(persistedRoute.alias);
|
|
1724
|
+
const aliases = new Set([
|
|
1725
|
+
execution.oldRegistration.alias,
|
|
1726
|
+
execution.newRegistration.alias,
|
|
1727
|
+
]);
|
|
1728
|
+
return ([...aliases].every((alias) => this.codexSuccessionPoisoned.has(alias)) &&
|
|
1729
|
+
[...aliases].every((alias) => this.codexSuccessionDispatchFrozen.has(alias)) &&
|
|
1730
|
+
store.clean &&
|
|
1731
|
+
store.codexRouteCount === 1 &&
|
|
1732
|
+
store.queueCount === 0 &&
|
|
1733
|
+
store.inFlightCount === 0 &&
|
|
1734
|
+
store.transientBodyCount === 0 &&
|
|
1735
|
+
store.codexQueueDepth === 0 &&
|
|
1736
|
+
claude.clean &&
|
|
1737
|
+
claude.generation === active.generation &&
|
|
1738
|
+
claude.activeGenerationMatched &&
|
|
1739
|
+
claude.ingressQuiesced &&
|
|
1740
|
+
claude.monitorFrozen &&
|
|
1741
|
+
!claude.discoveryInFlight &&
|
|
1742
|
+
claude.pendingOutboundReceipts === 0 &&
|
|
1743
|
+
claude.pendingInboundReceipts === 0 &&
|
|
1744
|
+
claude.rejectedInboundSettlements === 0 &&
|
|
1745
|
+
codexQuiet &&
|
|
1746
|
+
this.successionServiceStateClean(aliases) &&
|
|
1747
|
+
this.callbackQueue.length === 0 &&
|
|
1748
|
+
this.detachedReceiptWrites.size === 0);
|
|
1749
|
+
}
|
|
1750
|
+
async codexSuccessionBarrierCleanLocked(oldRegistration) {
|
|
1751
|
+
await this.drainCallbackQueueLocked();
|
|
1752
|
+
await this.drainDetachedReceiptWritesLocked();
|
|
1753
|
+
const lifecycleChanged = await this.processLifecycleLocked();
|
|
1754
|
+
if (lifecycleChanged)
|
|
1755
|
+
await this.publish();
|
|
1756
|
+
const store = await this.store.inspectCodexSuccessionBarrier(oldRegistration.alias);
|
|
1757
|
+
const claudeAdapter = this.adapter("claude", oldRegistration.hostId);
|
|
1758
|
+
const codexAdapter = this.adapter("codex", oldRegistration.hostId);
|
|
1759
|
+
const observeClaude = this.requireSuccessionMethod(claudeAdapter, "observeNativeCodexSuccessionBarrier");
|
|
1760
|
+
const observeCodex = this.requireSuccessionMethod(codexAdapter, "observeRouteSuccessionBarrier");
|
|
1761
|
+
const claude = await observeClaude.call(claudeAdapter, oldRegistration.generation);
|
|
1762
|
+
const codex = observeCodex.call(codexAdapter, oldRegistration.threadId);
|
|
1763
|
+
const aliases = new Set([oldRegistration.alias]);
|
|
1764
|
+
return (!this.closing &&
|
|
1765
|
+
this.codexSuccessionDispatchFrozen.has(oldRegistration.alias) &&
|
|
1766
|
+
store.clean &&
|
|
1767
|
+
claude.clean &&
|
|
1768
|
+
claude.generation === oldRegistration.generation &&
|
|
1769
|
+
claude.activeGenerationMatched &&
|
|
1770
|
+
claude.ingressQuiesced &&
|
|
1771
|
+
claude.monitorFrozen &&
|
|
1772
|
+
codex.clean &&
|
|
1773
|
+
this.successionServiceStateClean(aliases) &&
|
|
1774
|
+
this.callbackQueue.length === 0 &&
|
|
1775
|
+
this.detachedReceiptWrites.size === 0);
|
|
1776
|
+
}
|
|
1777
|
+
rescheduleIdleRoutesLocked() {
|
|
1778
|
+
for (const [alias, state] of this.routeStates) {
|
|
1779
|
+
if (state === "idle")
|
|
1780
|
+
this.scheduleDispatch(alias);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
async registerCodex(params) {
|
|
1784
|
+
if (!params.alias.startsWith("codex-")) {
|
|
1785
|
+
throw new BridgeError("INVALID_CODEX_PEER_ALIAS", "A registered Codex alias must start with codex-.");
|
|
1786
|
+
}
|
|
1787
|
+
if (this.codexSuccessionPoisoned.has(params.alias) ||
|
|
1788
|
+
(params.succeedsAlias !== undefined &&
|
|
1789
|
+
this.codexSuccessionPoisoned.has(params.succeedsAlias))) {
|
|
1790
|
+
throw new BridgeError("CODEX_SUCCESSION_RECOVERY_REQUIRED", "Codex registration is offline until the incomplete succession is manually recovered.");
|
|
1791
|
+
}
|
|
1792
|
+
if (params.succeedsAlias !== undefined) {
|
|
1793
|
+
await this.succeedCodexRegistration(params);
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
const registrationLock = this.codexRegistrationLocks.get(params.alias);
|
|
1797
|
+
if (registrationLock !== undefined &&
|
|
1798
|
+
(registrationLock.threadId !== params.threadId ||
|
|
1799
|
+
registrationLock.hostId !== params.hostId)) {
|
|
1800
|
+
throw new BridgeError("CODEX_REGISTRATION_REBIND_FORBIDDEN", "A Codex registration cannot change alias, task, or host during one Embassy process lifetime.");
|
|
1801
|
+
}
|
|
1802
|
+
if ([...this.codexRegistrationLocks.values()].some((lock) => lock.alias !== params.alias &&
|
|
1803
|
+
lock.hostId === params.hostId &&
|
|
1804
|
+
lock.threadId === params.threadId)) {
|
|
1805
|
+
throw new BridgeError("CODEX_REGISTRATION_REBIND_FORBIDDEN", "One Codex task cannot be registered under two aliases.");
|
|
1806
|
+
}
|
|
1807
|
+
// An irreversible succession restart leaves one exact durable successor
|
|
1808
|
+
// authority. Reject every other identity before it can acquire provisional
|
|
1809
|
+
// connector or native-advertisement state.
|
|
1810
|
+
this.assertPendingCodexSuccessionRecoveryMatch(params);
|
|
1811
|
+
const persistedCodexRoute = (await this.store.inspectPrivateCodexRoutes()).find((route) => route.alias === params.alias);
|
|
1812
|
+
if (persistedCodexRoute !== undefined &&
|
|
1813
|
+
(persistedCodexRoute.binding.hostId !== params.hostId ||
|
|
1814
|
+
persistedCodexRoute.binding.routeHandle !== params.threadId ||
|
|
1815
|
+
persistedCodexRoute.binding.ownerLease !==
|
|
1816
|
+
stableLease("codex", `${params.hostId}\0${params.threadId}`))) {
|
|
1817
|
+
throw new BridgeError("CODEX_REGISTRATION_REBIND_FORBIDDEN", "A retained Codex registration must be explicitly retired before another identity can register.");
|
|
1818
|
+
}
|
|
1819
|
+
const routeBeforeRegistration = await this.store.inspectPrivateRoute(params.alias);
|
|
1820
|
+
const adapter = this.adapter("codex", params.hostId);
|
|
1821
|
+
const registered = await adapter.selectRoute({
|
|
1822
|
+
alias: params.alias,
|
|
1823
|
+
routeHandle: params.threadId,
|
|
1824
|
+
});
|
|
1825
|
+
if (registered.routeHandle !== params.threadId) {
|
|
1826
|
+
throw new BridgeError("ROUTE_MISMATCH", "The connector registered a different task.");
|
|
1827
|
+
}
|
|
1828
|
+
const binding = {
|
|
1829
|
+
...adapter.identity,
|
|
1830
|
+
routeHandle: params.threadId,
|
|
1831
|
+
ownerLease: stableLease("codex", `${params.hostId}\0${params.threadId}`),
|
|
1832
|
+
};
|
|
1833
|
+
try {
|
|
1834
|
+
await this.registerOrRebind(params.alias, binding, "endpoint_reobserved", registered.state);
|
|
1835
|
+
}
|
|
1836
|
+
catch (error) {
|
|
1837
|
+
await adapter.releaseRoute?.(params.threadId).catch(() => undefined);
|
|
1838
|
+
throw error;
|
|
1839
|
+
}
|
|
1840
|
+
let advertiseAttempted = false;
|
|
1841
|
+
let listenerGeneration;
|
|
1842
|
+
try {
|
|
1843
|
+
this.rememberBinding(params.alias, binding, registered.state);
|
|
1844
|
+
const claudeAdapter = this.adapter("claude", params.hostId);
|
|
1845
|
+
const advertise = claudeAdapter.advertiseNativeCodexPeer;
|
|
1846
|
+
if (advertise !== undefined) {
|
|
1847
|
+
advertiseAttempted = true;
|
|
1848
|
+
await advertise.call(claudeAdapter, {
|
|
1849
|
+
alias: params.alias,
|
|
1850
|
+
cwd: this.nativePeerCwd,
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
await claudeAdapter.updateNativeCodexPeerStatus?.(params.alias, registered.state === "idle"
|
|
1854
|
+
? "idle"
|
|
1855
|
+
: registered.state === "awaiting_approval"
|
|
1856
|
+
? "waiting"
|
|
1857
|
+
: "busy");
|
|
1858
|
+
listenerGeneration = this.requireCurrentNativeCodexGeneration(claudeAdapter, params.alias);
|
|
1859
|
+
await this.completePendingCodexSuccessionRecoveryLocked(params);
|
|
1860
|
+
await this.changed();
|
|
1861
|
+
}
|
|
1862
|
+
catch (error) {
|
|
1863
|
+
if (routeBeforeRegistration !== undefined) {
|
|
1864
|
+
// A persisted or live exact route, its queue, and any advertisement
|
|
1865
|
+
// predate this invocation, so a later status/dashboard failure cannot
|
|
1866
|
+
// safely delete them as "rollback". A post-restart reactivation also
|
|
1867
|
+
// establishes the process-lifetime identity even if publication fails.
|
|
1868
|
+
this.lockCodexRegistration(params);
|
|
1869
|
+
this.dashboardHealthy = false;
|
|
1870
|
+
throw error;
|
|
1871
|
+
}
|
|
1872
|
+
try {
|
|
1873
|
+
await this.rollbackCodexRegistration(params, binding, advertiseAttempted);
|
|
1874
|
+
}
|
|
1875
|
+
catch (rollbackError) {
|
|
1876
|
+
// Some exact side effect may remain. Pin the provisional identity so a
|
|
1877
|
+
// second alias or task can never enter the adapter while cleanup is
|
|
1878
|
+
// uncertain.
|
|
1879
|
+
this.lockCodexRegistration(params);
|
|
1880
|
+
throw rollbackError;
|
|
1881
|
+
}
|
|
1882
|
+
throw error;
|
|
1883
|
+
}
|
|
1884
|
+
this.lockCodexRegistration(params, listenerGeneration);
|
|
1885
|
+
// The provider may have reported its initial idle observation before this
|
|
1886
|
+
// binding was remembered. Explicit registration is itself an authoritative
|
|
1887
|
+
// wake-up point, so do not require a second route notification to release a
|
|
1888
|
+
// message that was already held for this exact target.
|
|
1889
|
+
if (registered.state === "idle")
|
|
1890
|
+
this.scheduleDispatch(params.alias);
|
|
1891
|
+
}
|
|
1892
|
+
lockCodexRegistration(params, generation = "unconfirmed") {
|
|
1893
|
+
const existing = this.codexRegistrationLocks.get(params.alias);
|
|
1894
|
+
if (existing !== undefined &&
|
|
1895
|
+
(existing.threadId !== params.threadId ||
|
|
1896
|
+
existing.hostId !== params.hostId)) {
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
const lock = Object.freeze({
|
|
1900
|
+
alias: params.alias,
|
|
1901
|
+
threadId: params.threadId,
|
|
1902
|
+
hostId: params.hostId,
|
|
1903
|
+
generation: generation === "unconfirmed" && existing !== undefined
|
|
1904
|
+
? existing.generation
|
|
1905
|
+
: generation,
|
|
1906
|
+
});
|
|
1907
|
+
this.codexRegistrationLocks.set(params.alias, lock);
|
|
1908
|
+
if (generation !== "unconfirmed") {
|
|
1909
|
+
this.codexSuccessionState = createCodexRegistrationSuccession(lock);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
async rollbackCodexRegistration(params, binding, advertiseAttempted) {
|
|
1913
|
+
let cleanupFailed = false;
|
|
1914
|
+
try {
|
|
1915
|
+
await this.drainPreDeadlineDeliveryCallbacksLocked();
|
|
1916
|
+
const inFlightSettlements = await this.planRouteInFlightSettlementsLocked(params.alias, {
|
|
1917
|
+
unwrittenOutcome: "failed",
|
|
1918
|
+
safeErrorCode: "CODEX_REGISTRATION_ROLLBACK",
|
|
1919
|
+
});
|
|
1920
|
+
const settlements = await this.store.unregisterRoute(params.alias, binding.ownerLease, inFlightSettlements);
|
|
1921
|
+
for (const settlement of settlements) {
|
|
1922
|
+
await this.applyTerminalSettlementLocked(settlement);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
catch {
|
|
1926
|
+
cleanupFailed = true;
|
|
1927
|
+
}
|
|
1928
|
+
this.forgetBinding(params.alias);
|
|
1929
|
+
await this.adapter("codex", params.hostId)
|
|
1930
|
+
.releaseRoute?.(params.threadId)
|
|
1931
|
+
.catch(() => {
|
|
1932
|
+
cleanupFailed = true;
|
|
1933
|
+
});
|
|
1934
|
+
if (advertiseAttempted) {
|
|
1935
|
+
await this.adapter("claude", params.hostId)
|
|
1936
|
+
.unadvertiseNativeCodexPeer?.(params.alias)
|
|
1937
|
+
.catch(() => {
|
|
1938
|
+
cleanupFailed = true;
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
await this.changed().catch(() => {
|
|
1942
|
+
cleanupFailed = true;
|
|
1943
|
+
});
|
|
1944
|
+
if (cleanupFailed) {
|
|
1945
|
+
this.dashboardHealthy = false;
|
|
1946
|
+
throw new BridgeError("CODEX_REGISTRATION_ROLLBACK_FAILED", "The failed Codex registration could not be fully rolled back.");
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
async unregisterCodex(params) {
|
|
1950
|
+
const host = params.alias.slice(params.alias.lastIndexOf("@") + 1);
|
|
1951
|
+
const lease = stableLease("codex", `${host}\0${params.threadId}`);
|
|
1952
|
+
await this.drainPreDeadlineDeliveryCallbacksLocked();
|
|
1953
|
+
const inFlightSettlements = await this.planRouteInFlightSettlementsLocked(params.alias, {
|
|
1954
|
+
unwrittenOutcome: "cancelled",
|
|
1955
|
+
safeErrorCode: "ROUTE_UNREGISTERED",
|
|
1956
|
+
});
|
|
1957
|
+
const settlements = await this.store.unregisterRoute(params.alias, lease, inFlightSettlements);
|
|
1958
|
+
for (const settlement of settlements) {
|
|
1959
|
+
await this.applyTerminalSettlementLocked(settlement);
|
|
1960
|
+
}
|
|
1961
|
+
this.forgetBinding(params.alias);
|
|
1962
|
+
await this.adapter("claude", host)
|
|
1963
|
+
.unadvertiseNativeCodexPeer?.(params.alias)
|
|
1964
|
+
.catch(() => {
|
|
1965
|
+
this.dashboardHealthy = false;
|
|
1966
|
+
});
|
|
1967
|
+
await this.adapter("codex", host)
|
|
1968
|
+
.releaseRoute?.(params.threadId)
|
|
1969
|
+
.catch(() => {
|
|
1970
|
+
this.dashboardHealthy = false;
|
|
1971
|
+
});
|
|
1972
|
+
await this.changed();
|
|
1973
|
+
}
|
|
1974
|
+
async discoveryPublicFingerprint() {
|
|
1975
|
+
const base = await this.store.publicSnapshot();
|
|
1976
|
+
const snapshot = projectGatewayPublicSnapshot({
|
|
1977
|
+
...base,
|
|
1978
|
+
availablePeers: this.availablePeers.map((peer) => ({ ...peer })),
|
|
1979
|
+
});
|
|
1980
|
+
return JSON.stringify({
|
|
1981
|
+
inboundMode: snapshot.inboundMode,
|
|
1982
|
+
health: snapshot.health,
|
|
1983
|
+
connectors: snapshot.connectors.map(({ lastSeenAt: _lastSeenAt, ...connector }) => connector),
|
|
1984
|
+
availablePeers: snapshot.availablePeers.map(({ lastSeenAt: _lastSeenAt, ...peer }) => peer),
|
|
1985
|
+
routes: snapshot.routes.map(({ lastSeenAt: _lastSeenAt, ...route }) => route),
|
|
1986
|
+
messages: snapshot.messages,
|
|
1987
|
+
accounting: snapshot.accounting,
|
|
1988
|
+
alerts: snapshot.alerts.map(({ timestamp: _timestamp, ...alert }) => alert),
|
|
1989
|
+
truncation: snapshot.truncation,
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
async refreshClaudeDiscovery() {
|
|
1993
|
+
const publicBefore = await this.discoveryPublicFingerprint();
|
|
1994
|
+
this.candidates.clear();
|
|
1995
|
+
const rowsByAlias = new Map();
|
|
1996
|
+
const discoveredCandidates = [];
|
|
1997
|
+
const observedSelected = new Set();
|
|
1998
|
+
for (const adapter of this.adapters.filter((item) => item.identity.provider === "claude")) {
|
|
1999
|
+
const discovered = (await adapter.discoverClaudePeers?.()) ?? {
|
|
2000
|
+
peers: [],
|
|
2001
|
+
complete: false,
|
|
2002
|
+
};
|
|
2003
|
+
const grouped = new Map();
|
|
2004
|
+
const byHandle = new Map();
|
|
2005
|
+
for (const peer of discovered.peers) {
|
|
2006
|
+
if (!PUBLIC_ALIAS.test(peer.alias) ||
|
|
2007
|
+
!peer.alias.endsWith(`@${adapter.identity.hostId}`) ||
|
|
2008
|
+
peer.kind !== "interactive") {
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
2011
|
+
grouped.set(peer.alias, [...(grouped.get(peer.alias) ?? []), peer]);
|
|
2012
|
+
byHandle.set(peer.routeHandle, [
|
|
2013
|
+
...(byHandle.get(peer.routeHandle) ?? []),
|
|
2014
|
+
peer,
|
|
2015
|
+
]);
|
|
2016
|
+
}
|
|
2017
|
+
for (const [alias, matches] of grouped) {
|
|
2018
|
+
const peer = matches[0];
|
|
2019
|
+
if (peer === undefined)
|
|
2020
|
+
continue;
|
|
2021
|
+
if (matches.length !== 1) {
|
|
2022
|
+
rowsByAlias.set(alias, {
|
|
2023
|
+
peer,
|
|
2024
|
+
adapter,
|
|
2025
|
+
safeErrorCode: "PEER_ALIAS_COLLISION",
|
|
2026
|
+
});
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
if (!discovered.complete) {
|
|
2030
|
+
rowsByAlias.set(alias, {
|
|
2031
|
+
peer,
|
|
2032
|
+
adapter,
|
|
2033
|
+
safeErrorCode: "PEER_DISCOVERY_INCOMPLETE",
|
|
2034
|
+
});
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
if ((byHandle.get(peer.routeHandle)?.length ?? 0) !== 1) {
|
|
2038
|
+
rowsByAlias.set(alias, {
|
|
2039
|
+
peer,
|
|
2040
|
+
adapter,
|
|
2041
|
+
safeErrorCode: "PEER_SESSION_COLLISION",
|
|
2042
|
+
});
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
rowsByAlias.set(alias, { peer, adapter });
|
|
2046
|
+
discoveredCandidates.push({ ...peer, adapter });
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
for (const candidate of discoveredCandidates) {
|
|
2050
|
+
const { alias, adapter, routeHandle } = candidate;
|
|
2051
|
+
const row = rowsByAlias.get(alias);
|
|
2052
|
+
if (row?.safeErrorCode !== undefined)
|
|
2053
|
+
continue;
|
|
2054
|
+
try {
|
|
2055
|
+
const existing = [...this.routeBindings.entries()].find(([, binding]) => binding.provider === "claude" &&
|
|
2056
|
+
binding.hostId === adapter.identity.hostId &&
|
|
2057
|
+
binding.endpointGeneration === adapter.identity.endpointGeneration &&
|
|
2058
|
+
binding.routeHandle === routeHandle);
|
|
2059
|
+
if (existing !== undefined && existing[0] !== alias) {
|
|
2060
|
+
const collision = this.routeBindings.get(alias);
|
|
2061
|
+
if (collision !== undefined &&
|
|
2062
|
+
bindingKey(collision) !== bindingKey(existing[1])) {
|
|
2063
|
+
rowsByAlias.set(alias, {
|
|
2064
|
+
peer: candidate,
|
|
2065
|
+
adapter,
|
|
2066
|
+
safeErrorCode: "PEER_ALIAS_COLLISION",
|
|
2067
|
+
});
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
await this.renameClaudeRoute(existing[0], alias, existing[1]);
|
|
2071
|
+
}
|
|
2072
|
+
this.candidates.set(alias, candidate);
|
|
2073
|
+
}
|
|
2074
|
+
catch (error) {
|
|
2075
|
+
if (!(error instanceof BridgeError))
|
|
2076
|
+
throw error;
|
|
2077
|
+
rowsByAlias.set(alias, {
|
|
2078
|
+
peer: candidate,
|
|
2079
|
+
adapter,
|
|
2080
|
+
safeErrorCode: "PEER_ALIAS_COLLISION",
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
const persisted = await this.store.inspectPrivateClaudeRoutes();
|
|
2085
|
+
for (const route of persisted) {
|
|
2086
|
+
if (!route.enabled ||
|
|
2087
|
+
route.state !== "stale" ||
|
|
2088
|
+
route.compatibility !== "expired" ||
|
|
2089
|
+
!CLAUDE_SESSION_ID.test(route.binding.routeHandle) ||
|
|
2090
|
+
route.binding.ownerLease !==
|
|
2091
|
+
stableLease("claude", route.binding.routeHandle)) {
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
const matches = [...this.candidates.values()].filter((candidate) => candidate.adapter.identity.provider === route.binding.provider &&
|
|
2095
|
+
candidate.adapter.identity.hostId === route.binding.hostId &&
|
|
2096
|
+
candidate.routeHandle === route.binding.routeHandle);
|
|
2097
|
+
if (matches.length !== 1)
|
|
2098
|
+
continue;
|
|
2099
|
+
const candidate = matches[0];
|
|
2100
|
+
if (candidate === undefined)
|
|
2101
|
+
continue;
|
|
2102
|
+
await this.activatePersistedClaudeCandidate(candidate, route, "peer_identity_reobserved").catch(() => undefined);
|
|
2103
|
+
}
|
|
2104
|
+
const rows = [];
|
|
2105
|
+
for (const [alias, row] of rowsByAlias) {
|
|
2106
|
+
if (row.safeErrorCode !== undefined) {
|
|
2107
|
+
this.candidates.delete(alias);
|
|
2108
|
+
rows.push({
|
|
2109
|
+
alias,
|
|
2110
|
+
provider: "claude",
|
|
2111
|
+
host: row.adapter.identity.hostId,
|
|
2112
|
+
state: "incompatible",
|
|
2113
|
+
compatibility: "incompatible",
|
|
2114
|
+
validated: false,
|
|
2115
|
+
selected: false,
|
|
2116
|
+
safeErrorCode: row.safeErrorCode,
|
|
2117
|
+
});
|
|
2118
|
+
continue;
|
|
2119
|
+
}
|
|
2120
|
+
const selectedBinding = this.routeBindings.get(alias);
|
|
2121
|
+
const selected = selectedBinding?.provider === "claude" &&
|
|
2122
|
+
selectedBinding.hostId === row.adapter.identity.hostId &&
|
|
2123
|
+
selectedBinding.endpointGeneration ===
|
|
2124
|
+
row.adapter.identity.endpointGeneration &&
|
|
2125
|
+
selectedBinding.routeHandle === row.peer.routeHandle;
|
|
2126
|
+
rows.push({
|
|
2127
|
+
alias,
|
|
2128
|
+
provider: "claude",
|
|
2129
|
+
host: row.adapter.identity.hostId,
|
|
2130
|
+
state: row.peer.state,
|
|
2131
|
+
compatibility: row.peer.compatibility,
|
|
2132
|
+
validated: true,
|
|
2133
|
+
selected,
|
|
2134
|
+
});
|
|
2135
|
+
if (selected && selectedBinding !== undefined) {
|
|
2136
|
+
observedSelected.add(bindingKey(selectedBinding));
|
|
2137
|
+
await this.store.observeRoute({
|
|
2138
|
+
binding: selectedBinding,
|
|
2139
|
+
state: row.peer.state,
|
|
2140
|
+
compatibility: "compatible",
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
for (const [alias, binding] of [...this.routeBindings.entries()]) {
|
|
2145
|
+
if (binding.provider !== "claude" ||
|
|
2146
|
+
observedSelected.has(bindingKey(binding))) {
|
|
2147
|
+
continue;
|
|
2148
|
+
}
|
|
2149
|
+
const inspection = await this.store.inspectPrivateRoute(alias);
|
|
2150
|
+
if (inspection !== undefined &&
|
|
2151
|
+
inspection.enabled &&
|
|
2152
|
+
inspection.compatibility === "compatible") {
|
|
2153
|
+
await this.drainPreDeadlineDeliveryCallbacksLocked();
|
|
2154
|
+
const inFlightSettlements = await this.planRouteInFlightSettlementsLocked(alias, {
|
|
2155
|
+
unwrittenOutcome: "failed",
|
|
2156
|
+
safeErrorCode: "PEER_NOT_OBSERVED",
|
|
2157
|
+
});
|
|
2158
|
+
const settlements = await this.store.invalidateRoute(binding, "PEER_NOT_OBSERVED", inFlightSettlements);
|
|
2159
|
+
for (const settlement of settlements) {
|
|
2160
|
+
await this.applyTerminalSettlementLocked(settlement);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
await this.adapter("claude", binding.hostId)
|
|
2164
|
+
.releaseRoute?.(binding.routeHandle)
|
|
2165
|
+
.catch(() => {
|
|
2166
|
+
this.dashboardHealthy = false;
|
|
2167
|
+
});
|
|
2168
|
+
this.forgetBinding(alias);
|
|
2169
|
+
}
|
|
2170
|
+
this.availablePeers = rows.sort((left, right) => left.alias.localeCompare(right.alias));
|
|
2171
|
+
const changed = publicBefore !== (await this.discoveryPublicFingerprint());
|
|
2172
|
+
if (changed)
|
|
2173
|
+
this.revision += 1;
|
|
2174
|
+
return changed;
|
|
2175
|
+
}
|
|
2176
|
+
async renameClaudeRoute(oldAlias, newAlias, binding) {
|
|
2177
|
+
if (oldAlias === newAlias)
|
|
2178
|
+
return;
|
|
2179
|
+
await this.store.renameRoute(oldAlias, newAlias, binding.ownerLease);
|
|
2180
|
+
const state = this.routeStates.get(oldAlias) ?? "busy";
|
|
2181
|
+
this.routeBindings.delete(oldAlias);
|
|
2182
|
+
this.routeStates.delete(oldAlias);
|
|
2183
|
+
this.routeBindings.set(newAlias, binding);
|
|
2184
|
+
this.routeStates.set(newAlias, state);
|
|
2185
|
+
this.bindingAliases.set(bindingKey(binding), newAlias);
|
|
2186
|
+
for (const conversation of this.conversations.values()) {
|
|
2187
|
+
if (conversation.sourceAlias === oldAlias)
|
|
2188
|
+
conversation.sourceAlias = newAlias;
|
|
2189
|
+
if (conversation.targetAlias === oldAlias)
|
|
2190
|
+
conversation.targetAlias = newAlias;
|
|
2191
|
+
}
|
|
2192
|
+
for (const context of this.messageContexts.values()) {
|
|
2193
|
+
if (context.targetAlias === oldAlias)
|
|
2194
|
+
context.targetAlias = newAlias;
|
|
2195
|
+
}
|
|
2196
|
+
for (const continuation of this.providerTurnContinuations.values()) {
|
|
2197
|
+
if (continuation.targetAlias === oldAlias) {
|
|
2198
|
+
continuation.targetAlias = newAlias;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
for (const tracker of this.deliveryTrackers.values()) {
|
|
2202
|
+
if (tracker.targetAlias === oldAlias)
|
|
2203
|
+
tracker.targetAlias = newAlias;
|
|
2204
|
+
}
|
|
2205
|
+
const active = this.activeDispatchByTarget.get(oldAlias);
|
|
2206
|
+
if (active !== undefined) {
|
|
2207
|
+
this.activeDispatchByTarget.delete(oldAlias);
|
|
2208
|
+
this.activeDispatchByTarget.set(newAlias, active);
|
|
2209
|
+
}
|
|
2210
|
+
if (this.scheduledDispatchTargets.delete(oldAlias)) {
|
|
2211
|
+
// The already-created old-alias setImmediate may still run, but it can
|
|
2212
|
+
// only observe an empty queue. Give the renamed durable queue its own
|
|
2213
|
+
// runner so a rename between enqueue and dispatch cannot strand it.
|
|
2214
|
+
this.scheduleDispatch(newAlias);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
claudeCandidate(selector) {
|
|
2218
|
+
if (CLAUDE_SESSION_ID.test(selector)) {
|
|
2219
|
+
const normalized = selector.toLowerCase();
|
|
2220
|
+
const matches = [...this.candidates.values()].filter((candidate) => candidate.routeHandle.toLowerCase() === normalized);
|
|
2221
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
2222
|
+
}
|
|
2223
|
+
return this.candidates.get(selector);
|
|
2224
|
+
}
|
|
2225
|
+
async activatePersistedClaudeCandidate(candidate, persisted, reason) {
|
|
2226
|
+
const expectedLease = stableLease("claude", candidate.routeHandle);
|
|
2227
|
+
if (!persisted.enabled ||
|
|
2228
|
+
persisted.state !== "stale" ||
|
|
2229
|
+
persisted.compatibility !== "expired" ||
|
|
2230
|
+
persisted.binding.provider !== "claude" ||
|
|
2231
|
+
candidate.adapter.identity.provider !== "claude" ||
|
|
2232
|
+
persisted.binding.hostId !== candidate.adapter.identity.hostId ||
|
|
2233
|
+
persisted.binding.routeHandle !== candidate.routeHandle ||
|
|
2234
|
+
persisted.binding.ownerLease !== expectedLease) {
|
|
2235
|
+
throw new BridgeError("ROUTE_REBIND_IDENTITY_MISMATCH", "The persisted Claude selection does not match the exact live session identity.");
|
|
2236
|
+
}
|
|
2237
|
+
await this.assertClaudeWorkspaceDisjoint(candidate.adapter, candidate.routeHandle);
|
|
2238
|
+
try {
|
|
2239
|
+
const selected = await candidate.adapter.selectRoute({
|
|
2240
|
+
alias: candidate.alias,
|
|
2241
|
+
routeHandle: candidate.routeHandle,
|
|
2242
|
+
});
|
|
2243
|
+
if (selected.routeHandle !== candidate.routeHandle) {
|
|
2244
|
+
throw new BridgeError("ROUTE_MISMATCH", "The peer identity changed during selection.");
|
|
2245
|
+
}
|
|
2246
|
+
const binding = {
|
|
2247
|
+
...candidate.adapter.identity,
|
|
2248
|
+
routeHandle: candidate.routeHandle,
|
|
2249
|
+
ownerLease: expectedLease,
|
|
2250
|
+
};
|
|
2251
|
+
await this.store.rebindStaleRoute({
|
|
2252
|
+
alias: persisted.alias,
|
|
2253
|
+
newAlias: candidate.alias,
|
|
2254
|
+
currentOwnerLease: persisted.binding.ownerLease,
|
|
2255
|
+
newBinding: binding,
|
|
2256
|
+
reason,
|
|
2257
|
+
state: selected.state,
|
|
2258
|
+
});
|
|
2259
|
+
if (persisted.alias !== candidate.alias) {
|
|
2260
|
+
this.forgetBinding(persisted.alias);
|
|
2261
|
+
}
|
|
2262
|
+
this.rememberBinding(candidate.alias, binding, selected.state);
|
|
2263
|
+
}
|
|
2264
|
+
catch (error) {
|
|
2265
|
+
await candidate.adapter.releaseRoute?.(candidate.routeHandle).catch(() => {
|
|
2266
|
+
this.dashboardHealthy = false;
|
|
2267
|
+
});
|
|
2268
|
+
throw error;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
async selectClaudeCandidate(candidate, persisted) {
|
|
2272
|
+
if (persisted !== undefined) {
|
|
2273
|
+
await this.activatePersistedClaudeCandidate(candidate, persisted, "peer_explicitly_reselected");
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
await this.assertClaudeWorkspaceDisjoint(candidate.adapter, candidate.routeHandle);
|
|
2277
|
+
const binding = {
|
|
2278
|
+
...candidate.adapter.identity,
|
|
2279
|
+
routeHandle: candidate.routeHandle,
|
|
2280
|
+
ownerLease: stableLease("claude", candidate.routeHandle),
|
|
2281
|
+
};
|
|
2282
|
+
try {
|
|
2283
|
+
const selected = await candidate.adapter.selectRoute({
|
|
2284
|
+
alias: candidate.alias,
|
|
2285
|
+
routeHandle: candidate.routeHandle,
|
|
2286
|
+
});
|
|
2287
|
+
if (selected.routeHandle !== candidate.routeHandle) {
|
|
2288
|
+
throw new BridgeError("ROUTE_MISMATCH", "The peer identity changed during selection.");
|
|
2289
|
+
}
|
|
2290
|
+
await this.store.registerRoute({
|
|
2291
|
+
alias: candidate.alias,
|
|
2292
|
+
binding,
|
|
2293
|
+
registrationMode: "selected_live_peer",
|
|
2294
|
+
state: selected.state,
|
|
2295
|
+
compatibility: "compatible",
|
|
2296
|
+
});
|
|
2297
|
+
this.rememberBinding(candidate.alias, binding, selected.state);
|
|
2298
|
+
}
|
|
2299
|
+
catch (error) {
|
|
2300
|
+
await candidate.adapter.releaseRoute?.(candidate.routeHandle).catch(() => {
|
|
2301
|
+
this.dashboardHealthy = false;
|
|
2302
|
+
});
|
|
2303
|
+
throw error;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
async replaceClaudeSelectionCandidate(candidate, retained, retired, alreadyLive) {
|
|
2307
|
+
await this.assertClaudeWorkspaceDisjoint(candidate.adapter, candidate.routeHandle);
|
|
2308
|
+
const binding = {
|
|
2309
|
+
...candidate.adapter.identity,
|
|
2310
|
+
routeHandle: candidate.routeHandle,
|
|
2311
|
+
ownerLease: stableLease("claude", candidate.routeHandle),
|
|
2312
|
+
};
|
|
2313
|
+
const observedState = this.routeStates.get(candidate.alias);
|
|
2314
|
+
let selectedState = observedState === undefined || observedState === "stale"
|
|
2315
|
+
? candidate.state
|
|
2316
|
+
: observedState;
|
|
2317
|
+
let newlySelected = false;
|
|
2318
|
+
let storeCommitted = false;
|
|
2319
|
+
if (!alreadyLive) {
|
|
2320
|
+
const selected = await candidate.adapter.selectRoute({
|
|
2321
|
+
alias: candidate.alias,
|
|
2322
|
+
routeHandle: candidate.routeHandle,
|
|
2323
|
+
});
|
|
2324
|
+
newlySelected = true;
|
|
2325
|
+
if (selected.routeHandle !== candidate.routeHandle) {
|
|
2326
|
+
await candidate.adapter
|
|
2327
|
+
.releaseRoute?.(candidate.routeHandle)
|
|
2328
|
+
.catch(() => {
|
|
2329
|
+
this.dashboardHealthy = false;
|
|
2330
|
+
});
|
|
2331
|
+
throw new BridgeError("ROUTE_MISMATCH", "The peer identity changed during selection.");
|
|
2332
|
+
}
|
|
2333
|
+
selectedState = selected.state;
|
|
2334
|
+
}
|
|
2335
|
+
try {
|
|
2336
|
+
await this.drainPreDeadlineDeliveryCallbacksLocked();
|
|
2337
|
+
const inFlightSettlements = await this.planRoutesInFlightSettlementsLocked(retired.map((route) => route.alias), {
|
|
2338
|
+
unwrittenOutcome: "cancelled",
|
|
2339
|
+
safeErrorCode: "ROUTE_UNREGISTERED",
|
|
2340
|
+
});
|
|
2341
|
+
const settlements = await this.store.replaceClaudeSelection({
|
|
2342
|
+
replacement: {
|
|
2343
|
+
alias: candidate.alias,
|
|
2344
|
+
binding,
|
|
2345
|
+
registrationMode: "selected_live_peer",
|
|
2346
|
+
state: selectedState,
|
|
2347
|
+
compatibility: "compatible",
|
|
2348
|
+
},
|
|
2349
|
+
inFlightSettlements,
|
|
2350
|
+
});
|
|
2351
|
+
storeCommitted = true;
|
|
2352
|
+
for (const settlement of settlements) {
|
|
2353
|
+
await this.applyTerminalSettlementLocked(settlement);
|
|
2354
|
+
}
|
|
2355
|
+
for (const route of retired)
|
|
2356
|
+
this.forgetBinding(route.alias);
|
|
2357
|
+
if (retained !== undefined && retained.alias !== candidate.alias) {
|
|
2358
|
+
this.forgetBinding(retained.alias);
|
|
2359
|
+
}
|
|
2360
|
+
this.rememberBinding(candidate.alias, binding, selectedState);
|
|
2361
|
+
for (const route of retired) {
|
|
2362
|
+
await this.adapters
|
|
2363
|
+
.find((adapter) => adapter.identity.provider === "claude" &&
|
|
2364
|
+
adapter.identity.hostId === route.binding.hostId)
|
|
2365
|
+
?.releaseRoute?.(route.binding.routeHandle)
|
|
2366
|
+
.catch(() => {
|
|
2367
|
+
this.dashboardHealthy = false;
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
catch (error) {
|
|
2372
|
+
if (newlySelected && !storeCommitted) {
|
|
2373
|
+
await candidate.adapter
|
|
2374
|
+
.releaseRoute?.(candidate.routeHandle)
|
|
2375
|
+
.catch(() => {
|
|
2376
|
+
this.dashboardHealthy = false;
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
throw error;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
async resolveSelectedClaudeDestination(selector) {
|
|
2383
|
+
const discoveryChanged = await this.refreshClaudeDiscovery();
|
|
2384
|
+
try {
|
|
2385
|
+
const candidate = this.claudeCandidate(selector);
|
|
2386
|
+
if (candidate === undefined) {
|
|
2387
|
+
throw new BridgeError("PEER_NOT_FOUND", "No unique compatible interactive peer matches that current name or session UUID.");
|
|
2388
|
+
}
|
|
2389
|
+
const existing = this.routeBindings.get(candidate.alias);
|
|
2390
|
+
if (existing === undefined ||
|
|
2391
|
+
existing.provider !== "claude" ||
|
|
2392
|
+
existing.routeHandle !== candidate.routeHandle) {
|
|
2393
|
+
throw new BridgeError("PEER_NOT_SELECTED", "The Claude session must be explicitly selected before messaging.");
|
|
2394
|
+
}
|
|
2395
|
+
return { alias: candidate.alias, discoveryChanged };
|
|
2396
|
+
}
|
|
2397
|
+
catch (error) {
|
|
2398
|
+
if (discoveryChanged)
|
|
2399
|
+
await this.publish();
|
|
2400
|
+
throw error;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
async inferCodexAlias(codexThreadId) {
|
|
2404
|
+
const candidates = (await this.store.inspectPrivateCodexRoutes()).filter(({ binding }) => codexThreadId === undefined || binding.routeHandle === codexThreadId);
|
|
2405
|
+
const candidate = candidates[0];
|
|
2406
|
+
if (candidates.length !== 1 || candidate === undefined) {
|
|
2407
|
+
throw new BridgeError(codexThreadId === undefined
|
|
2408
|
+
? "CODEX_PAIR_INFERENCE_AMBIGUOUS"
|
|
2409
|
+
: "CODEX_CALLER_MISMATCH", codexThreadId === undefined
|
|
2410
|
+
? "Selecting Claude requires an inherited Codex task or exactly one registered Codex route."
|
|
2411
|
+
: "The inherited Codex task does not uniquely own a registered route.");
|
|
2412
|
+
}
|
|
2413
|
+
return candidate.alias;
|
|
2414
|
+
}
|
|
2415
|
+
assertCodexPairCaller(params) {
|
|
2416
|
+
const binding = this.routeBindings.get(params.codexAlias);
|
|
2417
|
+
if (binding?.provider !== "codex" ||
|
|
2418
|
+
(params.codexThreadId !== undefined &&
|
|
2419
|
+
binding.routeHandle !== params.codexThreadId)) {
|
|
2420
|
+
throw new BridgeError("CODEX_CALLER_MISMATCH", "The inherited Codex task does not own the requested pair endpoint.");
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
async pairRoutes(params) {
|
|
2424
|
+
this.assertCodexPairCaller(params);
|
|
2425
|
+
await this.selectAndPairClaude(params.claudeAlias, params.codexAlias);
|
|
2426
|
+
}
|
|
2427
|
+
async unpairRoutes(params) {
|
|
2428
|
+
this.assertCodexPairCaller(params);
|
|
2429
|
+
const selected = await this.selectedClaudeRoute(params.claudeAlias);
|
|
2430
|
+
await this.unpairClaudeRoute(selected, params.codexAlias);
|
|
2431
|
+
}
|
|
2432
|
+
async unpairClaudeRoute(selected, codexAlias) {
|
|
2433
|
+
await this.drainPreDeadlineDeliveryCallbacksLocked();
|
|
2434
|
+
const pair = {
|
|
2435
|
+
claudeAlias: selected.alias,
|
|
2436
|
+
codexAlias,
|
|
2437
|
+
};
|
|
2438
|
+
const inFlightSettlements = await this.planPairInFlightSettlementsLocked(pair, {
|
|
2439
|
+
unwrittenOutcome: "cancelled",
|
|
2440
|
+
safeErrorCode: "PAIR_REMOVED",
|
|
2441
|
+
});
|
|
2442
|
+
const result = await this.store.unpairRoutes({
|
|
2443
|
+
...pair,
|
|
2444
|
+
inFlightSettlements,
|
|
2445
|
+
});
|
|
2446
|
+
for (const settlement of result.settlements) {
|
|
2447
|
+
await this.applyTerminalSettlementLocked(settlement);
|
|
2448
|
+
}
|
|
2449
|
+
this.purgePairCapabilitiesLocked(pair);
|
|
2450
|
+
if (result.claudeRouteUnreferenced) {
|
|
2451
|
+
const settlements = await this.store.unregisterRoute(selected.alias, selected.binding.ownerLease);
|
|
2452
|
+
for (const settlement of settlements) {
|
|
2453
|
+
await this.applyTerminalSettlementLocked(settlement);
|
|
2454
|
+
}
|
|
2455
|
+
this.forgetBinding(selected.alias);
|
|
2456
|
+
await this.adapters
|
|
2457
|
+
.find((adapter) => adapter.identity.provider === "claude" &&
|
|
2458
|
+
adapter.identity.hostId === selected.binding.hostId)
|
|
2459
|
+
?.releaseRoute?.(selected.binding.routeHandle)
|
|
2460
|
+
.catch(() => {
|
|
2461
|
+
this.dashboardHealthy = false;
|
|
2462
|
+
});
|
|
2463
|
+
this.availablePeers = this.availablePeers.map((peer) => peer.alias === selected.alias ? { ...peer, selected: false } : peer);
|
|
2464
|
+
}
|
|
2465
|
+
await this.changed();
|
|
2466
|
+
}
|
|
2467
|
+
async selectClaude(params) {
|
|
2468
|
+
await this.selectAndPairClaude(params.alias, await this.inferCodexAlias(params.codexThreadId));
|
|
2469
|
+
}
|
|
2470
|
+
async selectAndPairClaude(selector, codexAlias) {
|
|
2471
|
+
const discoveryChanged = await this.refreshClaudeDiscovery();
|
|
2472
|
+
const candidate = this.claudeCandidate(selector);
|
|
2473
|
+
if (candidate === undefined)
|
|
2474
|
+
throw new BridgeError("PEER_NOT_FOUND", "No unique compatible interactive peer matches that current name or session UUID.");
|
|
2475
|
+
const persisted = await this.store.inspectPrivateClaudeRoutes();
|
|
2476
|
+
const byIdentity = persisted.find((route) => route.binding.hostId === candidate.adapter.identity.hostId &&
|
|
2477
|
+
route.binding.routeHandle === candidate.routeHandle);
|
|
2478
|
+
const byAlias = persisted.find((route) => route.alias === candidate.alias);
|
|
2479
|
+
const live = this.routeBindings.get(candidate.alias);
|
|
2480
|
+
const alreadyLive = live?.provider === "claude" &&
|
|
2481
|
+
live.hostId === candidate.adapter.identity.hostId &&
|
|
2482
|
+
live.routeHandle === candidate.routeHandle;
|
|
2483
|
+
if (!alreadyLive) {
|
|
2484
|
+
if (byIdentity !== undefined) {
|
|
2485
|
+
await this.selectClaudeCandidate(candidate, byIdentity);
|
|
2486
|
+
}
|
|
2487
|
+
else if (byAlias !== undefined) {
|
|
2488
|
+
await this.replaceClaudeSelectionCandidate(candidate, undefined, [byAlias], false);
|
|
2489
|
+
}
|
|
2490
|
+
else {
|
|
2491
|
+
await this.selectClaudeCandidate(candidate);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
let paired;
|
|
2495
|
+
try {
|
|
2496
|
+
paired = await this.store.pairRoutes({
|
|
2497
|
+
claudeAlias: candidate.alias,
|
|
2498
|
+
codexAlias,
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
catch (error) {
|
|
2502
|
+
if (!alreadyLive &&
|
|
2503
|
+
byIdentity === undefined &&
|
|
2504
|
+
byAlias === undefined) {
|
|
2505
|
+
await this.rollbackFreshClaudeSelection(candidate);
|
|
2506
|
+
}
|
|
2507
|
+
await this.refreshClaudeDiscovery().catch(() => {
|
|
2508
|
+
this.dashboardHealthy = false;
|
|
2509
|
+
});
|
|
2510
|
+
await this.publish().catch(() => {
|
|
2511
|
+
this.dashboardHealthy = false;
|
|
2512
|
+
});
|
|
2513
|
+
throw error;
|
|
2514
|
+
}
|
|
2515
|
+
await this.refreshClaudeDiscovery();
|
|
2516
|
+
if (discoveryChanged || !alreadyLive || paired.created)
|
|
2517
|
+
await this.changed();
|
|
2518
|
+
else
|
|
2519
|
+
await this.publish();
|
|
2520
|
+
}
|
|
2521
|
+
async rollbackFreshClaudeSelection(candidate) {
|
|
2522
|
+
let cleanupFailed = false;
|
|
2523
|
+
try {
|
|
2524
|
+
await this.store.unregisterRoute(candidate.alias, stableLease("claude", candidate.routeHandle));
|
|
2525
|
+
}
|
|
2526
|
+
catch {
|
|
2527
|
+
cleanupFailed = true;
|
|
2528
|
+
}
|
|
2529
|
+
this.forgetBinding(candidate.alias);
|
|
2530
|
+
await candidate.adapter.releaseRoute?.(candidate.routeHandle).catch(() => {
|
|
2531
|
+
cleanupFailed = true;
|
|
2532
|
+
});
|
|
2533
|
+
if (cleanupFailed)
|
|
2534
|
+
this.dashboardHealthy = false;
|
|
2535
|
+
}
|
|
2536
|
+
async unselectClaude(params) {
|
|
2537
|
+
const codexAlias = await this.inferCodexAlias(params.codexThreadId);
|
|
2538
|
+
const selected = await this.selectedClaudeRoute(params.alias);
|
|
2539
|
+
await this.unpairClaudeRoute(selected, codexAlias);
|
|
2540
|
+
}
|
|
2541
|
+
async selectedClaudeRoute(selector) {
|
|
2542
|
+
const persisted = await this.store.inspectPrivateClaudeRoutes();
|
|
2543
|
+
const selected = CLAUDE_SESSION_ID.test(selector)
|
|
2544
|
+
? persisted.find((route) => route.binding.routeHandle.toLowerCase() ===
|
|
2545
|
+
selector.toLowerCase())
|
|
2546
|
+
: persisted.find((route) => route.alias === selector);
|
|
2547
|
+
if (selected === undefined)
|
|
2548
|
+
throw new BridgeError("PEER_NOT_FOUND", "No selected Claude session matches that selector.");
|
|
2549
|
+
return selected;
|
|
2550
|
+
}
|
|
2551
|
+
async registerOrRebind(alias, binding, reason, state, currentOwnerLease = binding.ownerLease) {
|
|
2552
|
+
try {
|
|
2553
|
+
await this.store.registerRoute({
|
|
2554
|
+
alias,
|
|
2555
|
+
binding,
|
|
2556
|
+
registrationMode: binding.provider === "codex" ? "explicit_opt_in" : "selected_live_peer",
|
|
2557
|
+
state,
|
|
2558
|
+
compatibility: "compatible",
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
catch (error) {
|
|
2562
|
+
if (!(error instanceof BridgeError) || error.code !== "ROUTE_ALIAS_COLLISION")
|
|
2563
|
+
throw error;
|
|
2564
|
+
await this.store.rebindStaleRoute({ alias, currentOwnerLease, newBinding: binding, reason, state });
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
rememberBinding(alias, binding, state) {
|
|
2568
|
+
const previous = this.routeBindings.get(alias);
|
|
2569
|
+
if (previous !== undefined &&
|
|
2570
|
+
bindingKey(previous) !== bindingKey(binding)) {
|
|
2571
|
+
this.pendingClaudeReplies.delete(bindingKey(previous));
|
|
2572
|
+
}
|
|
2573
|
+
this.routeBindings.set(alias, binding);
|
|
2574
|
+
this.routeStates.set(alias, state);
|
|
2575
|
+
this.bindingAliases.set(bindingKey(binding), alias);
|
|
2576
|
+
}
|
|
2577
|
+
forgetBinding(alias) {
|
|
2578
|
+
const heldTimer = this.heldRedispatchTimers.get(alias);
|
|
2579
|
+
if (heldTimer !== undefined) {
|
|
2580
|
+
this.timers.clearTimeout(heldTimer);
|
|
2581
|
+
this.heldRedispatchTimers.delete(alias);
|
|
2582
|
+
}
|
|
2583
|
+
const binding = this.routeBindings.get(alias);
|
|
2584
|
+
if (binding !== undefined) {
|
|
2585
|
+
this.bindingAliases.delete(bindingKey(binding));
|
|
2586
|
+
this.pendingClaudeReplies.delete(bindingKey(binding));
|
|
2587
|
+
}
|
|
2588
|
+
this.routeBindings.delete(alias);
|
|
2589
|
+
this.routeStates.delete(alias);
|
|
2590
|
+
for (const [messageId, continuation] of this.providerTurnContinuations) {
|
|
2591
|
+
if (continuation.targetAlias !== alias)
|
|
2592
|
+
continue;
|
|
2593
|
+
this.providerTurnContinuations.delete(messageId);
|
|
2594
|
+
if (this.activeDispatchByTarget.get(alias) === messageId) {
|
|
2595
|
+
this.activeDispatchByTarget.delete(alias);
|
|
2596
|
+
}
|
|
2597
|
+
const conversation = this.conversations.get(continuation.conversationId);
|
|
2598
|
+
this.releasePendingClaude(conversation);
|
|
2599
|
+
this.nativeIngressByConversation.delete(continuation.conversationId);
|
|
2600
|
+
this.addRuntimeAlert("PROVIDER_TURN_ROUTE_REMOVED", "warning", {
|
|
2601
|
+
alias,
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
async verifyCodex(alias, threadId) {
|
|
2606
|
+
const binding = await this.store.resolveRoute(alias);
|
|
2607
|
+
if (binding.provider !== "codex" || binding.routeHandle !== threadId)
|
|
2608
|
+
throw new BridgeError("ROUTE_MISMATCH", "The caller does not own this exact task route.");
|
|
2609
|
+
const state = this.routeStates.get(alias);
|
|
2610
|
+
if (state === undefined || state === "stale") {
|
|
2611
|
+
throw new BridgeError("ROUTE_UNAVAILABLE", "The caller's exact Codex route is not positively observed.", true);
|
|
2612
|
+
}
|
|
2613
|
+
const adapter = this.adapter("codex", binding.hostId);
|
|
2614
|
+
await this.store.observeConnector({
|
|
2615
|
+
identity: adapter.identity,
|
|
2616
|
+
health: "healthy",
|
|
2617
|
+
compatibility: "compatible",
|
|
2618
|
+
protocol: adapter.protocol,
|
|
2619
|
+
protocolVersion: adapter.protocolVersion,
|
|
2620
|
+
});
|
|
2621
|
+
await this.store.observeRoute({
|
|
2622
|
+
binding,
|
|
2623
|
+
state,
|
|
2624
|
+
compatibility: "compatible",
|
|
2625
|
+
});
|
|
2626
|
+
return binding;
|
|
2627
|
+
}
|
|
2628
|
+
async verifyClaude(alias, replyAddress) {
|
|
2629
|
+
if (replyAddress === undefined)
|
|
2630
|
+
throw new BridgeError("REPLY_ADDRESS_MISMATCH", "An exact Claude reply capability is required.");
|
|
2631
|
+
const binding = await this.store.resolveRoute(alias);
|
|
2632
|
+
if (binding.provider !== "claude")
|
|
2633
|
+
throw new BridgeError("ROUTE_MISMATCH", "The caller is not a selected Claude route.");
|
|
2634
|
+
const adapter = this.adapter("claude", binding.hostId);
|
|
2635
|
+
const resolved = await adapter.resolveReplyAddress?.(replyAddress);
|
|
2636
|
+
if (resolved === undefined || resolved.routeHandle !== binding.routeHandle)
|
|
2637
|
+
throw new BridgeError("REPLY_ADDRESS_MISMATCH", "The reply capability does not match the selected peer generation.");
|
|
2638
|
+
await this.assertClaudeWorkspaceDisjoint(adapter, binding.routeHandle);
|
|
2639
|
+
return binding;
|
|
2640
|
+
}
|
|
2641
|
+
acceptedControlResult(enqueued) {
|
|
2642
|
+
if (enqueued.deliveryToken === undefined) {
|
|
2643
|
+
throw new BridgeError("DELIVERY_TOKEN_UNAVAILABLE", "The accepted message is missing its delivery correlation handle.");
|
|
2644
|
+
}
|
|
2645
|
+
return {
|
|
2646
|
+
accepted: true,
|
|
2647
|
+
code: "ok",
|
|
2648
|
+
conversationId: enqueued.conversationId,
|
|
2649
|
+
deliveryToken: enqueued.deliveryToken,
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
reserveDeliveryToken() {
|
|
2653
|
+
this.pruneDeliveryTrackers(this.now().getTime());
|
|
2654
|
+
while (this.deliveryTokens.size >= this.deliveryTokenCapacity) {
|
|
2655
|
+
const oldestTerminal = [...this.deliveryTrackers.values()]
|
|
2656
|
+
.filter((tracker) => tracker.deliveryToken !== undefined &&
|
|
2657
|
+
this.isTerminalDeliveryState(tracker.machine))
|
|
2658
|
+
.sort((left, right) => (isTerminalDeliveryMachine(left.machine)
|
|
2659
|
+
? left.machine.terminalAt
|
|
2660
|
+
: left.updatedAt) -
|
|
2661
|
+
(isTerminalDeliveryMachine(right.machine)
|
|
2662
|
+
? right.machine.terminalAt
|
|
2663
|
+
: right.updatedAt) ||
|
|
2664
|
+
left.messageId.localeCompare(right.messageId))[0];
|
|
2665
|
+
if (oldestTerminal?.deliveryToken === undefined)
|
|
2666
|
+
break;
|
|
2667
|
+
this.deliveryTokens.delete(oldestTerminal.deliveryToken);
|
|
2668
|
+
delete oldestTerminal.deliveryToken;
|
|
2669
|
+
if (oldestTerminal.nativeReceipt === undefined) {
|
|
2670
|
+
this.deliveryTrackers.delete(oldestTerminal.messageId);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
if (this.deliveryTokens.size >= this.deliveryTokenCapacity) {
|
|
2674
|
+
throw new BridgeError("DELIVERY_STATUS_CAPACITY", "The bounded delivery status table is full.", true);
|
|
2675
|
+
}
|
|
2676
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
2677
|
+
const token = `dlv_${randomBytes(18).toString("base64url")}`;
|
|
2678
|
+
if (DELIVERY_TOKEN.test(token) && !this.deliveryTokens.has(token)) {
|
|
2679
|
+
return token;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
throw new BridgeError("DELIVERY_TOKEN_COLLISION", "The gateway could not allocate a unique delivery token.", true);
|
|
2683
|
+
}
|
|
2684
|
+
armDeliveryTracker(input) {
|
|
2685
|
+
const deadlineAt = Date.parse(input.deadlineAt);
|
|
2686
|
+
const stallAt = Math.min(input.enqueuedAt + this.config.stallNoticeMs, deadlineAt - 1);
|
|
2687
|
+
const nativeHeldNextAttemptAt = Math.min(input.enqueuedAt + NATIVE_HELD_THRESHOLD_MS, deadlineAt - 1);
|
|
2688
|
+
const tracker = {
|
|
2689
|
+
messageId: input.messageId,
|
|
2690
|
+
conversationId: input.conversationId,
|
|
2691
|
+
targetAlias: input.targetAlias,
|
|
2692
|
+
enqueuedAt: input.enqueuedAt,
|
|
2693
|
+
deadlineAt,
|
|
2694
|
+
machine: createDeliveryMachine({
|
|
2695
|
+
enqueuedAt: input.enqueuedAt,
|
|
2696
|
+
stallAt,
|
|
2697
|
+
deadlineAt,
|
|
2698
|
+
// Deferred means the provider proved no write. Permit one bounded
|
|
2699
|
+
// attempt per 500 ms slice until this message's immutable deadline.
|
|
2700
|
+
maxCleanPrewriteRetries: Math.max(0, Math.ceil((deadlineAt - input.enqueuedAt) / 500)),
|
|
2701
|
+
nativeReceipt: input.nativeReceipt !== undefined,
|
|
2702
|
+
maxNativeReceiptCleanPrewriteRetries: DELIVERY_ACK_RETRY_DELAYS_MS.length,
|
|
2703
|
+
}),
|
|
2704
|
+
updatedAt: input.enqueuedAt,
|
|
2705
|
+
stallNoticeSent: false,
|
|
2706
|
+
stallNoticeAttempt: 0,
|
|
2707
|
+
stallNoticeNextAttemptAt: stallAt,
|
|
2708
|
+
nativeHeldSent: false,
|
|
2709
|
+
nativeHeldAttempt: 0,
|
|
2710
|
+
nativeHeldNextAttemptAt,
|
|
2711
|
+
...(input.deliveryToken === undefined
|
|
2712
|
+
? {}
|
|
2713
|
+
: { deliveryToken: input.deliveryToken }),
|
|
2714
|
+
...(input.nativeReceipt === undefined
|
|
2715
|
+
? {}
|
|
2716
|
+
: { nativeReceipt: { ...input.nativeReceipt } }),
|
|
2717
|
+
};
|
|
2718
|
+
this.deliveryTrackers.set(input.messageId, tracker);
|
|
2719
|
+
if (input.deliveryToken !== undefined) {
|
|
2720
|
+
this.deliveryTokens.set(input.deliveryToken, input.messageId);
|
|
2721
|
+
}
|
|
2722
|
+
this.nextDashboardRefreshAt ??=
|
|
2723
|
+
input.enqueuedAt + DELIVERY_DASHBOARD_REFRESH_MS;
|
|
2724
|
+
this.scheduleLifecycleWakeLocked();
|
|
2725
|
+
}
|
|
2726
|
+
isTerminalDeliveryState(state) {
|
|
2727
|
+
return isTerminalDeliveryMachine(state);
|
|
2728
|
+
}
|
|
2729
|
+
settlementDeliveryState(settlement) {
|
|
2730
|
+
return settlement.state === "abandoned" ? "failed" : settlement.state;
|
|
2731
|
+
}
|
|
2732
|
+
pruneDeliveryTrackers(now) {
|
|
2733
|
+
const retentionMs = Math.max(60_000, this.config.limits.messageDeadlineMs * 2);
|
|
2734
|
+
for (const [messageId, tracker] of this.deliveryTrackers) {
|
|
2735
|
+
if (!this.isTerminalDeliveryState(tracker.machine))
|
|
2736
|
+
continue;
|
|
2737
|
+
const tokenExpired = tracker.deliveryToken === undefined ||
|
|
2738
|
+
tracker.machine.terminalAt + retentionMs <= now;
|
|
2739
|
+
if (!tokenExpired || tracker.nativeReceipt !== undefined)
|
|
2740
|
+
continue;
|
|
2741
|
+
this.deliveryTrackers.delete(messageId);
|
|
2742
|
+
if (tracker.deliveryToken !== undefined) {
|
|
2743
|
+
this.deliveryTokens.delete(tracker.deliveryToken);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
addRuntimeAlert(code, severity, details = {}) {
|
|
2748
|
+
this.runtimeAlerts.push({
|
|
2749
|
+
code: safeCode(code, "GATEWAY_RUNTIME_ALERT"),
|
|
2750
|
+
severity,
|
|
2751
|
+
timestamp: this.now().toISOString(),
|
|
2752
|
+
...details,
|
|
2753
|
+
});
|
|
2754
|
+
if (this.runtimeAlerts.length > 64) {
|
|
2755
|
+
this.runtimeAlerts.splice(0, this.runtimeAlerts.length - 64);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
async releaseNativeReceiptLocked(tracker) {
|
|
2759
|
+
const receipt = tracker.nativeReceipt;
|
|
2760
|
+
if (receipt === undefined)
|
|
2761
|
+
return;
|
|
2762
|
+
try {
|
|
2763
|
+
await this.adapter("claude", receipt.hostId)
|
|
2764
|
+
.releaseNativeInboundReceipt?.(receipt.receiptHandle);
|
|
2765
|
+
}
|
|
2766
|
+
catch {
|
|
2767
|
+
// The transport is already unconfirmed. Never keep an authority-bearing
|
|
2768
|
+
// receipt forever merely because explicit release also failed.
|
|
2769
|
+
}
|
|
2770
|
+
delete tracker.nativeReceipt;
|
|
2771
|
+
this.addRuntimeAlert("NATIVE_RECEIPT_UNCONFIRMED", "warning", {
|
|
2772
|
+
provider: "claude",
|
|
2773
|
+
host: receipt.hostId,
|
|
2774
|
+
alias: tracker.targetAlias,
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
/**
|
|
2778
|
+
* Native `held` is progress, not terminal delivery or approval. Emit it
|
|
2779
|
+
* only after this exact message is known to be waiting, or after its prompt
|
|
2780
|
+
* dispatch threshold elapses. A clean pre-write may retry; an ambiguous
|
|
2781
|
+
* write is never replayed, and neither path consumes the terminal receipt.
|
|
2782
|
+
*/
|
|
2783
|
+
async publishNativeHeldLocked(tracker) {
|
|
2784
|
+
const receipt = tracker.nativeReceipt;
|
|
2785
|
+
if (receipt === undefined ||
|
|
2786
|
+
tracker.nativeHeldSent ||
|
|
2787
|
+
isTerminalDeliveryMachine(tracker.machine) ||
|
|
2788
|
+
this.now().getTime() >= tracker.deadlineAt) {
|
|
2789
|
+
return false;
|
|
2790
|
+
}
|
|
2791
|
+
const now = this.now().getTime();
|
|
2792
|
+
tracker.nativeHeldAttempt += 1;
|
|
2793
|
+
try {
|
|
2794
|
+
const adapter = this.adapter("claude", receipt.hostId);
|
|
2795
|
+
const update = adapter.updateNativeInboundStatus;
|
|
2796
|
+
if (update === undefined) {
|
|
2797
|
+
throw new BridgeError("NATIVE_RECEIPT_TRANSPORT_UNAVAILABLE", "The Claude adapter cannot publish native held progress.");
|
|
2798
|
+
}
|
|
2799
|
+
await update.call(adapter, receipt.receiptHandle, "held");
|
|
2800
|
+
tracker.nativeHeldSent = true;
|
|
2801
|
+
return true;
|
|
2802
|
+
}
|
|
2803
|
+
catch (error) {
|
|
2804
|
+
const cleanPrewrite = error instanceof BridgeError && error.recoverable;
|
|
2805
|
+
const delay = DELIVERY_ACK_RETRY_DELAYS_MS[Math.min(tracker.nativeHeldAttempt - 1, DELIVERY_ACK_RETRY_DELAYS_MS.length - 1)] ?? DELIVERY_ACK_RETRY_DELAYS_MS.at(-1) ?? 2_000;
|
|
2806
|
+
const retryAt = now + delay;
|
|
2807
|
+
if (cleanPrewrite &&
|
|
2808
|
+
tracker.nativeHeldAttempt <= DELIVERY_ACK_RETRY_DELAYS_MS.length &&
|
|
2809
|
+
retryAt < tracker.deadlineAt) {
|
|
2810
|
+
tracker.nativeHeldNextAttemptAt = retryAt;
|
|
2811
|
+
this.scheduleLifecycleWakeLocked();
|
|
2812
|
+
return true;
|
|
2813
|
+
}
|
|
2814
|
+
// The write may already have reached the sender. Mark this progress
|
|
2815
|
+
// attempt complete locally so no later timer can downgrade a terminal
|
|
2816
|
+
// acknowledgement or replay an ambiguous frame.
|
|
2817
|
+
tracker.nativeHeldSent = true;
|
|
2818
|
+
this.addRuntimeAlert("NATIVE_HELD_NOTICE_UNCONFIRMED", "warning", {
|
|
2819
|
+
provider: "claude",
|
|
2820
|
+
host: receipt.hostId,
|
|
2821
|
+
alias: tracker.targetAlias,
|
|
2822
|
+
});
|
|
2823
|
+
return true;
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
nativeReceiptNotificationFor(tracker) {
|
|
2827
|
+
if (!isTerminalDeliveryMachine(tracker.machine))
|
|
2828
|
+
return undefined;
|
|
2829
|
+
return tracker.machine.outcome === "delivered"
|
|
2830
|
+
? { status: "delivered" }
|
|
2831
|
+
: {
|
|
2832
|
+
status: "expired",
|
|
2833
|
+
diagnosticCode: tracker.deliverySafeErrorCode ??
|
|
2834
|
+
(tracker.machine.outcome === "unconfirmed"
|
|
2835
|
+
? "DELIVERY_UNCONFIRMED"
|
|
2836
|
+
: `DELIVERY_${tracker.machine.outcome.toUpperCase()}`),
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
async sendNativeReceiptLocked(tracker) {
|
|
2840
|
+
const receipt = tracker.nativeReceipt;
|
|
2841
|
+
const native = tracker.machine.nativeReceipt;
|
|
2842
|
+
const notification = this.nativeReceiptNotificationFor(tracker);
|
|
2843
|
+
if (receipt === undefined ||
|
|
2844
|
+
native.phase !== "sending" ||
|
|
2845
|
+
notification === undefined) {
|
|
2846
|
+
return false;
|
|
2847
|
+
}
|
|
2848
|
+
const now = this.now().getTime();
|
|
2849
|
+
try {
|
|
2850
|
+
const adapter = this.adapter("claude", receipt.hostId);
|
|
2851
|
+
const update = adapter.updateNativeInboundStatus;
|
|
2852
|
+
if (update === undefined) {
|
|
2853
|
+
throw new BridgeError("NATIVE_RECEIPT_TRANSPORT_UNAVAILABLE", "The Claude adapter cannot settle a native receipt.");
|
|
2854
|
+
}
|
|
2855
|
+
await update.call(adapter, receipt.receiptHandle, notification.status, notification.status === "expired"
|
|
2856
|
+
? notification.diagnosticCode
|
|
2857
|
+
: undefined);
|
|
2858
|
+
await this.advanceDeliveryLocked(tracker.messageId, {
|
|
2859
|
+
type: "native_receipt_confirmed",
|
|
2860
|
+
at: now,
|
|
2861
|
+
});
|
|
2862
|
+
return true;
|
|
2863
|
+
}
|
|
2864
|
+
catch (error) {
|
|
2865
|
+
const cleanPrewrite = error instanceof BridgeError && error.recoverable;
|
|
2866
|
+
const delay = DELIVERY_ACK_RETRY_DELAYS_MS[Math.min(native.attempt - 1, DELIVERY_ACK_RETRY_DELAYS_MS.length - 1)] ?? DELIVERY_ACK_RETRY_DELAYS_MS.at(-1) ?? 2_000;
|
|
2867
|
+
const nextAttemptAt = now + delay;
|
|
2868
|
+
await this.advanceDeliveryLocked(tracker.messageId, cleanPrewrite
|
|
2869
|
+
? {
|
|
2870
|
+
type: "native_receipt_clean_prewrite_failed",
|
|
2871
|
+
at: now,
|
|
2872
|
+
retryAt: nextAttemptAt,
|
|
2873
|
+
}
|
|
2874
|
+
: { type: "native_receipt_ambiguous", at: now });
|
|
2875
|
+
return true;
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
deliveryCorrelation(messageId) {
|
|
2879
|
+
return (this.messageContexts.get(messageId) ??
|
|
2880
|
+
this.providerTurnContinuations.get(messageId));
|
|
2881
|
+
}
|
|
2882
|
+
eventTime(event) {
|
|
2883
|
+
if ("observedAt" in event)
|
|
2884
|
+
return event.observedAt;
|
|
2885
|
+
return event.at;
|
|
2886
|
+
}
|
|
2887
|
+
normalizeDeliverySafeCode(tracker, outcome, candidate) {
|
|
2888
|
+
const normalized = candidate === undefined
|
|
2889
|
+
? undefined
|
|
2890
|
+
: safeCode(candidate, "PROVIDER_DELIVERY_FAILED");
|
|
2891
|
+
if (outcome !== "unconfirmed")
|
|
2892
|
+
return normalized;
|
|
2893
|
+
const correlation = this.deliveryCorrelation(tracker.messageId);
|
|
2894
|
+
const target = correlation === undefined
|
|
2895
|
+
? undefined
|
|
2896
|
+
: this.contextTargetBinding(correlation);
|
|
2897
|
+
return target?.provider === "claude"
|
|
2898
|
+
? "CLAUDE_RECEIPT_UNCONFIRMED"
|
|
2899
|
+
: (normalized ?? "DELIVERY_UNCONFIRMED");
|
|
2900
|
+
}
|
|
2901
|
+
async planRouteInFlightSettlementsLocked(alias, input) {
|
|
2902
|
+
return await this.planRoutesInFlightSettlementsLocked([alias], input);
|
|
2903
|
+
}
|
|
2904
|
+
async planRoutesInFlightSettlementsLocked(aliases, input) {
|
|
2905
|
+
const affected = await this.store.inspectAffectedInFlightMessages(aliases);
|
|
2906
|
+
const observedAt = this.now().getTime();
|
|
2907
|
+
return affected.map(({ messageId }) => {
|
|
2908
|
+
const tracker = this.deliveryTrackers.get(messageId);
|
|
2909
|
+
if (tracker === undefined) {
|
|
2910
|
+
return {
|
|
2911
|
+
messageId,
|
|
2912
|
+
// In-flight without its reducer means write evidence is unknowable;
|
|
2913
|
+
// never downgrade that corruption to a proven clean cancellation.
|
|
2914
|
+
state: "ambiguous",
|
|
2915
|
+
safeErrorCode: "DELIVERY_TRACKER_MISSING",
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
const event = tracker.pendingTerminalEvent ??
|
|
2919
|
+
{
|
|
2920
|
+
type: "route_terminated",
|
|
2921
|
+
at: observedAt,
|
|
2922
|
+
unwrittenOutcome: input.unwrittenOutcome,
|
|
2923
|
+
safeErrorCode: input.safeErrorCode,
|
|
2924
|
+
};
|
|
2925
|
+
const transition = transitionDelivery(tracker.machine, event);
|
|
2926
|
+
const settlement = transition.effects.find((effect) => effect.type === "settle_delivery");
|
|
2927
|
+
if (settlement === undefined) {
|
|
2928
|
+
throw new BridgeError("ROUTE_TERMINATION_STATE_MISMATCH", "An affected in-flight delivery could not produce an exact terminal route settlement.", true);
|
|
2929
|
+
}
|
|
2930
|
+
const safeErrorCode = this.normalizeDeliverySafeCode(tracker, settlement.outcome, settlement.safeErrorCode);
|
|
2931
|
+
return {
|
|
2932
|
+
messageId,
|
|
2933
|
+
state: settlement.outcome,
|
|
2934
|
+
...(safeErrorCode === undefined ? {} : { safeErrorCode }),
|
|
2935
|
+
};
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2938
|
+
async planPairInFlightSettlementsLocked(pair, input) {
|
|
2939
|
+
const affected = await this.store.inspectAffectedPairInFlightMessages(pair);
|
|
2940
|
+
const observedAt = this.now().getTime();
|
|
2941
|
+
return affected.map(({ messageId }) => {
|
|
2942
|
+
const tracker = this.deliveryTrackers.get(messageId);
|
|
2943
|
+
if (tracker === undefined) {
|
|
2944
|
+
return {
|
|
2945
|
+
messageId,
|
|
2946
|
+
state: "ambiguous",
|
|
2947
|
+
safeErrorCode: "DELIVERY_TRACKER_MISSING",
|
|
2948
|
+
};
|
|
2949
|
+
}
|
|
2950
|
+
const event = tracker.pendingTerminalEvent ??
|
|
2951
|
+
{
|
|
2952
|
+
type: "route_terminated",
|
|
2953
|
+
at: observedAt,
|
|
2954
|
+
unwrittenOutcome: input.unwrittenOutcome,
|
|
2955
|
+
safeErrorCode: input.safeErrorCode,
|
|
2956
|
+
};
|
|
2957
|
+
const transition = transitionDelivery(tracker.machine, event);
|
|
2958
|
+
const settlement = transition.effects.find((effect) => effect.type === "settle_delivery");
|
|
2959
|
+
if (settlement === undefined) {
|
|
2960
|
+
throw new BridgeError("PAIR_TERMINATION_STATE_MISMATCH", "An affected in-flight delivery could not produce an exact terminal pair settlement.", true);
|
|
2961
|
+
}
|
|
2962
|
+
const safeErrorCode = this.normalizeDeliverySafeCode(tracker, settlement.outcome, settlement.safeErrorCode);
|
|
2963
|
+
return {
|
|
2964
|
+
messageId,
|
|
2965
|
+
state: settlement.outcome,
|
|
2966
|
+
...(safeErrorCode === undefined ? {} : { safeErrorCode }),
|
|
2967
|
+
};
|
|
2968
|
+
});
|
|
2969
|
+
}
|
|
2970
|
+
purgePairCapabilitiesLocked(pair) {
|
|
2971
|
+
const matches = (sourceAlias, targetAlias) => (sourceAlias === pair.claudeAlias && targetAlias === pair.codexAlias) ||
|
|
2972
|
+
(sourceAlias === pair.codexAlias && targetAlias === pair.claudeAlias);
|
|
2973
|
+
const conversationIds = new Set();
|
|
2974
|
+
for (const [conversationId, conversation] of this.conversations) {
|
|
2975
|
+
if (conversation.pair !== true ||
|
|
2976
|
+
!matches(conversation.sourceAlias, conversation.targetAlias)) {
|
|
2977
|
+
continue;
|
|
2978
|
+
}
|
|
2979
|
+
conversationIds.add(conversationId);
|
|
2980
|
+
this.conversations.delete(conversationId);
|
|
2981
|
+
}
|
|
2982
|
+
for (const [messageId, context] of this.messageContexts) {
|
|
2983
|
+
if (!conversationIds.has(context.conversationId))
|
|
2984
|
+
continue;
|
|
2985
|
+
this.messageContexts.delete(messageId);
|
|
2986
|
+
if (this.activeDispatchByTarget.get(context.targetAlias) === messageId) {
|
|
2987
|
+
this.activeDispatchByTarget.delete(context.targetAlias);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
for (const [messageId, continuation] of this.providerTurnContinuations) {
|
|
2991
|
+
if (!conversationIds.has(continuation.conversationId))
|
|
2992
|
+
continue;
|
|
2993
|
+
this.providerTurnContinuations.delete(messageId);
|
|
2994
|
+
if (this.activeDispatchByTarget.get(continuation.targetAlias) === messageId) {
|
|
2995
|
+
this.activeDispatchByTarget.delete(continuation.targetAlias);
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
for (const [key, pending] of this.pendingClaudeReplies) {
|
|
2999
|
+
if (conversationIds.has(pending.conversationId)) {
|
|
3000
|
+
this.pendingClaudeReplies.delete(key);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
for (const conversationId of conversationIds) {
|
|
3004
|
+
this.nativeIngressByConversation.delete(conversationId);
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
async settleDeliveryStoreLocked(tracker, outcome, safeErrorCode) {
|
|
3008
|
+
if (outcome === "failed" ||
|
|
3009
|
+
outcome === "expired" ||
|
|
3010
|
+
outcome === "cancelled") {
|
|
3011
|
+
const queued = await this.store.settleQueuedMessage({
|
|
3012
|
+
messageId: tracker.messageId,
|
|
3013
|
+
state: outcome,
|
|
3014
|
+
...(safeErrorCode === undefined ? {} : { safeErrorCode }),
|
|
3015
|
+
});
|
|
3016
|
+
if (queued.status === "settled")
|
|
3017
|
+
return queued.settlement;
|
|
3018
|
+
}
|
|
3019
|
+
const settled = await this.store.settleMessage({
|
|
3020
|
+
messageId: tracker.messageId,
|
|
3021
|
+
state: outcome,
|
|
3022
|
+
...(safeErrorCode === undefined ? {} : { safeErrorCode }),
|
|
3023
|
+
});
|
|
3024
|
+
return settled.status === "settled" ? settled.settlement : undefined;
|
|
3025
|
+
}
|
|
3026
|
+
async publishStallNoticeLocked(tracker, queuedForMs) {
|
|
3027
|
+
const receipt = tracker.nativeReceipt;
|
|
3028
|
+
if (receipt === undefined) {
|
|
3029
|
+
tracker.stallNoticeSent = true;
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
try {
|
|
3033
|
+
const adapter = this.adapter("claude", receipt.hostId);
|
|
3034
|
+
const notify = adapter.notifyNativeInboundProgress;
|
|
3035
|
+
if (notify === undefined) {
|
|
3036
|
+
throw new BridgeError("NATIVE_PROGRESS_TRANSPORT_UNAVAILABLE", "The Claude adapter cannot publish delivery progress.");
|
|
3037
|
+
}
|
|
3038
|
+
await notify.call(adapter, receipt.receiptHandle, {
|
|
3039
|
+
kind: "stall",
|
|
3040
|
+
reason: this.stallReasonFor(tracker),
|
|
3041
|
+
queuedForMs,
|
|
3042
|
+
});
|
|
3043
|
+
tracker.stallNoticeSent = true;
|
|
3044
|
+
}
|
|
3045
|
+
catch (error) {
|
|
3046
|
+
const cleanPrewrite = error instanceof BridgeError && error.recoverable;
|
|
3047
|
+
tracker.stallNoticeAttempt += 1;
|
|
3048
|
+
const delay = DELIVERY_ACK_RETRY_DELAYS_MS[Math.min(tracker.stallNoticeAttempt - 1, DELIVERY_ACK_RETRY_DELAYS_MS.length - 1)] ?? DELIVERY_ACK_RETRY_DELAYS_MS.at(-1) ?? 2_000;
|
|
3049
|
+
const retryAt = this.now().getTime() + delay;
|
|
3050
|
+
if (cleanPrewrite &&
|
|
3051
|
+
tracker.stallNoticeAttempt <= DELIVERY_ACK_RETRY_DELAYS_MS.length &&
|
|
3052
|
+
retryAt < tracker.deadlineAt) {
|
|
3053
|
+
tracker.stallNoticeNextAttemptAt = retryAt;
|
|
3054
|
+
return;
|
|
3055
|
+
}
|
|
3056
|
+
tracker.stallNoticeSent = true;
|
|
3057
|
+
this.addRuntimeAlert("NATIVE_STALL_NOTICE_UNCONFIRMED", "warning", {
|
|
3058
|
+
provider: "claude",
|
|
3059
|
+
host: receipt.hostId,
|
|
3060
|
+
alias: tracker.targetAlias,
|
|
3061
|
+
});
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
async applyDeliveryEffectsLocked(tracker, effects, suppliedSettlement) {
|
|
3065
|
+
let terminalContext;
|
|
3066
|
+
for (const effect of effects) {
|
|
3067
|
+
if (effect.type === "dispatch") {
|
|
3068
|
+
// dispatchOne owns provider I/O. This effect is the reducer's proof
|
|
3069
|
+
// that the exact attempt was authorized, not a second dispatch call.
|
|
3070
|
+
continue;
|
|
3071
|
+
}
|
|
3072
|
+
if (effect.type === "record_progress") {
|
|
3073
|
+
if (effect.progress !== "transport_uncertain") {
|
|
3074
|
+
await this.store
|
|
3075
|
+
.markMessageProgress(tracker.messageId, effect.progress)
|
|
3076
|
+
.catch(() => undefined);
|
|
3077
|
+
}
|
|
3078
|
+
continue;
|
|
3079
|
+
}
|
|
3080
|
+
if (effect.type === "publish_stall") {
|
|
3081
|
+
await this.publishStallNoticeLocked(tracker, effect.pendingForMs);
|
|
3082
|
+
continue;
|
|
3083
|
+
}
|
|
3084
|
+
if (effect.type === "settle_delivery") {
|
|
3085
|
+
const code = this.normalizeDeliverySafeCode(tracker, effect.outcome, effect.safeErrorCode);
|
|
3086
|
+
if (code === undefined)
|
|
3087
|
+
delete tracker.deliverySafeErrorCode;
|
|
3088
|
+
else
|
|
3089
|
+
tracker.deliverySafeErrorCode = code;
|
|
3090
|
+
const settlement = suppliedSettlement ??
|
|
3091
|
+
(await this.settleDeliveryStoreLocked(tracker, effect.outcome, code));
|
|
3092
|
+
if (settlement !== undefined) {
|
|
3093
|
+
const settledCode = this.normalizeDeliverySafeCode(tracker, effect.outcome, settlement.safeErrorCode ?? code);
|
|
3094
|
+
if (settledCode === undefined)
|
|
3095
|
+
delete tracker.deliverySafeErrorCode;
|
|
3096
|
+
else
|
|
3097
|
+
tracker.deliverySafeErrorCode = settledCode;
|
|
3098
|
+
}
|
|
3099
|
+
terminalContext = this.takeMessageContextLocked(tracker.messageId, effect.outcome, this.providerTurnContinuations.has(tracker.messageId));
|
|
3100
|
+
continue;
|
|
3101
|
+
}
|
|
3102
|
+
if (effect.type === "send_native_receipt") {
|
|
3103
|
+
await this.sendNativeReceiptLocked(tracker);
|
|
3104
|
+
continue;
|
|
3105
|
+
}
|
|
3106
|
+
if (effect.type === "release_native_receipt") {
|
|
3107
|
+
await this.releaseNativeReceiptLocked(tracker);
|
|
3108
|
+
}
|
|
3109
|
+
// Retry effects are projected into the one lifecycle timer below.
|
|
3110
|
+
}
|
|
3111
|
+
if (isTerminalDeliveryMachine(tracker.machine) &&
|
|
3112
|
+
tracker.deliveryToken === undefined &&
|
|
3113
|
+
tracker.nativeReceipt === undefined) {
|
|
3114
|
+
this.deliveryTrackers.delete(tracker.messageId);
|
|
3115
|
+
}
|
|
3116
|
+
return terminalContext;
|
|
3117
|
+
}
|
|
3118
|
+
async advanceDeliveryLocked(messageId, event, suppliedSettlement, terminalReplyText) {
|
|
3119
|
+
const tracker = this.deliveryTrackers.get(messageId);
|
|
3120
|
+
if (tracker === undefined)
|
|
3121
|
+
return undefined;
|
|
3122
|
+
// A pre-observed terminal event whose ledger write failed retains
|
|
3123
|
+
// precedence over later lifecycle events. If the store has already
|
|
3124
|
+
// supplied a different authoritative settlement, mirror that proof.
|
|
3125
|
+
const hasSuppliedSettlement = suppliedSettlement !== undefined;
|
|
3126
|
+
const resolvingPendingTerminal = tracker.pendingTerminalEvent !== undefined;
|
|
3127
|
+
const transitionEvent = tracker.pendingTerminalEvent !== undefined && !hasSuppliedSettlement
|
|
3128
|
+
? tracker.pendingTerminalEvent
|
|
3129
|
+
: event;
|
|
3130
|
+
let transition = transitionDelivery(tracker.machine, transitionEvent);
|
|
3131
|
+
if (transition.state === tracker.machine && transition.effects.length === 0) {
|
|
3132
|
+
return undefined;
|
|
3133
|
+
}
|
|
3134
|
+
const settlementEffect = transition.effects.find((effect) => effect.type === "settle_delivery");
|
|
3135
|
+
let authoritativeSettlement = suppliedSettlement;
|
|
3136
|
+
if (settlementEffect !== undefined && authoritativeSettlement === undefined) {
|
|
3137
|
+
const code = this.normalizeDeliverySafeCode(tracker, settlementEffect.outcome, settlementEffect.safeErrorCode);
|
|
3138
|
+
try {
|
|
3139
|
+
authoritativeSettlement = await this.settleDeliveryStoreLocked(tracker, settlementEffect.outcome, code);
|
|
3140
|
+
if (authoritativeSettlement === undefined) {
|
|
3141
|
+
throw new BridgeError("DELIVERY_SETTLEMENT_UNAVAILABLE", "The delivery ledger could not find the accepted message to settle.", true);
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
catch (error) {
|
|
3145
|
+
const firstFailure = tracker.pendingTerminalEvent === undefined;
|
|
3146
|
+
tracker.pendingTerminalEvent ??= transitionEvent;
|
|
3147
|
+
if (firstFailure && terminalReplyText !== undefined) {
|
|
3148
|
+
tracker.pendingTerminalReplyText = terminalReplyText;
|
|
3149
|
+
}
|
|
3150
|
+
tracker.settlementRetryAt =
|
|
3151
|
+
this.now().getTime() + DELIVERY_SETTLEMENT_RETRY_MS;
|
|
3152
|
+
if (firstFailure) {
|
|
3153
|
+
this.addRuntimeAlert("DELIVERY_SETTLEMENT_RETRY", "error", {
|
|
3154
|
+
alias: tracker.targetAlias,
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
this.scheduleLifecycleWakeLocked();
|
|
3158
|
+
throw error;
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
if (settlementEffect !== undefined && authoritativeSettlement !== undefined) {
|
|
3162
|
+
const authoritativeOutcome = this.settlementDeliveryState(authoritativeSettlement);
|
|
3163
|
+
if (authoritativeOutcome !== settlementEffect.outcome ||
|
|
3164
|
+
authoritativeSettlement.safeErrorCode !==
|
|
3165
|
+
settlementEffect.safeErrorCode) {
|
|
3166
|
+
transition = transitionDelivery(tracker.machine, {
|
|
3167
|
+
type: "external_settlement",
|
|
3168
|
+
at: this.eventTime(transitionEvent),
|
|
3169
|
+
outcome: authoritativeOutcome,
|
|
3170
|
+
...(authoritativeSettlement.safeErrorCode === undefined
|
|
3171
|
+
? {}
|
|
3172
|
+
: { safeErrorCode: authoritativeSettlement.safeErrorCode }),
|
|
3173
|
+
});
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
tracker.machine = transition.state;
|
|
3177
|
+
tracker.updatedAt = isTerminalDeliveryMachine(transition.state)
|
|
3178
|
+
? transition.state.terminalAt
|
|
3179
|
+
: this.eventTime(transitionEvent);
|
|
3180
|
+
delete tracker.pendingTerminalEvent;
|
|
3181
|
+
delete tracker.settlementRetryAt;
|
|
3182
|
+
const context = await this.applyDeliveryEffectsLocked(tracker, transition.effects, authoritativeSettlement);
|
|
3183
|
+
if (resolvingPendingTerminal) {
|
|
3184
|
+
const replyText = tracker.pendingTerminalReplyText;
|
|
3185
|
+
delete tracker.pendingTerminalReplyText;
|
|
3186
|
+
if (isTerminalDeliveryMachine(tracker.machine) &&
|
|
3187
|
+
tracker.machine.outcome === "delivered") {
|
|
3188
|
+
await this.finishDeliveredReplyLocked(context, replyText);
|
|
3189
|
+
}
|
|
3190
|
+
return undefined;
|
|
3191
|
+
}
|
|
3192
|
+
if (hasSuppliedSettlement) {
|
|
3193
|
+
delete tracker.pendingTerminalReplyText;
|
|
3194
|
+
}
|
|
3195
|
+
if (tracker.machine.nativeReceipt.phase === "confirmed") {
|
|
3196
|
+
delete tracker.nativeReceipt;
|
|
3197
|
+
if (isTerminalDeliveryMachine(tracker.machine) &&
|
|
3198
|
+
tracker.deliveryToken === undefined) {
|
|
3199
|
+
this.deliveryTrackers.delete(tracker.messageId);
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
return context;
|
|
3203
|
+
}
|
|
3204
|
+
async applyTerminalSettlementLocked(settlement) {
|
|
3205
|
+
const state = this.settlementDeliveryState(settlement);
|
|
3206
|
+
if (!this.deliveryTrackers.has(settlement.messageId)) {
|
|
3207
|
+
const context = this.takeMessageContextLocked(settlement.messageId, state);
|
|
3208
|
+
for (const [token, messageId] of this.deliveryTokens) {
|
|
3209
|
+
if (messageId === settlement.messageId)
|
|
3210
|
+
this.deliveryTokens.delete(token);
|
|
3211
|
+
}
|
|
3212
|
+
this.addRuntimeAlert("DELIVERY_TRACKER_MISSING", "error", {
|
|
3213
|
+
...(context === undefined ? {} : { alias: context.targetAlias }),
|
|
3214
|
+
});
|
|
3215
|
+
return context;
|
|
3216
|
+
}
|
|
3217
|
+
return await this.advanceDeliveryLocked(settlement.messageId, {
|
|
3218
|
+
type: "external_settlement",
|
|
3219
|
+
at: this.now().getTime(),
|
|
3220
|
+
outcome: state,
|
|
3221
|
+
...(settlement.safeErrorCode === undefined
|
|
3222
|
+
? {}
|
|
3223
|
+
: { safeErrorCode: settlement.safeErrorCode }),
|
|
3224
|
+
}, settlement);
|
|
3225
|
+
}
|
|
3226
|
+
takeMessageContextLocked(messageId, state, retainProviderTurn = false) {
|
|
3227
|
+
const context = this.messageContexts.get(messageId);
|
|
3228
|
+
if (context === undefined)
|
|
3229
|
+
return undefined;
|
|
3230
|
+
this.messageContexts.delete(messageId);
|
|
3231
|
+
if (!retainProviderTurn &&
|
|
3232
|
+
this.activeDispatchByTarget.get(context.targetAlias) ===
|
|
3233
|
+
messageId) {
|
|
3234
|
+
this.activeDispatchByTarget.delete(context.targetAlias);
|
|
3235
|
+
if (!this.closing)
|
|
3236
|
+
this.scheduleDispatch(context.targetAlias);
|
|
3237
|
+
}
|
|
3238
|
+
const conversation = this.conversations.get(context.conversationId);
|
|
3239
|
+
// Ambiguity remains active in the reducer until the immutable deadline;
|
|
3240
|
+
// once it becomes terminal there is no remaining reply window to own.
|
|
3241
|
+
if (state !== "delivered") {
|
|
3242
|
+
this.releasePendingClaude(conversation);
|
|
3243
|
+
}
|
|
3244
|
+
if (state !== "delivered") {
|
|
3245
|
+
this.nativeIngressByConversation.delete(context.conversationId);
|
|
3246
|
+
}
|
|
3247
|
+
else if (!context.expectsReply) {
|
|
3248
|
+
this.nativeIngressByConversation.delete(context.conversationId);
|
|
3249
|
+
}
|
|
3250
|
+
return context;
|
|
3251
|
+
}
|
|
3252
|
+
/**
|
|
3253
|
+
* Codex App Server acceptance is a terminal delivery boundary for the
|
|
3254
|
+
* original message body, but not for its later model reply. Settle the
|
|
3255
|
+
* durable ledger/body now and retain only bounded in-memory reply
|
|
3256
|
+
* correlation until completion or the original deadline.
|
|
3257
|
+
*/
|
|
3258
|
+
async acceptProviderDeliveryLocked(messageId) {
|
|
3259
|
+
const context = this.messageContexts.get(messageId);
|
|
3260
|
+
if (context === undefined)
|
|
3261
|
+
return false;
|
|
3262
|
+
if (!context.expectsReply) {
|
|
3263
|
+
this.nativeIngressByConversation.delete(context.conversationId);
|
|
3264
|
+
}
|
|
3265
|
+
this.providerTurnContinuations.set(messageId, { ...context });
|
|
3266
|
+
await this.advanceDeliveryLocked(messageId, {
|
|
3267
|
+
type: "provider_released",
|
|
3268
|
+
observedAt: this.now().getTime(),
|
|
3269
|
+
});
|
|
3270
|
+
if (!this.messageContexts.has(messageId) &&
|
|
3271
|
+
this.providerTurnContinuations.has(messageId)) {
|
|
3272
|
+
return true;
|
|
3273
|
+
}
|
|
3274
|
+
this.providerTurnContinuations.delete(messageId);
|
|
3275
|
+
return false;
|
|
3276
|
+
}
|
|
3277
|
+
stallReasonFor(tracker) {
|
|
3278
|
+
const state = this.routeStates.get(tracker.targetAlias);
|
|
3279
|
+
if (state === "awaiting_approval")
|
|
3280
|
+
return "AWAITING_EXTERNAL_APPROVAL";
|
|
3281
|
+
if (state === "stale")
|
|
3282
|
+
return "CODEX_ROUTE_STALE";
|
|
3283
|
+
if (state === "busy")
|
|
3284
|
+
return "ROUTE_BUSY";
|
|
3285
|
+
return "ROUTE_UNAVAILABLE";
|
|
3286
|
+
}
|
|
3287
|
+
async refreshNextProgressWatchWakeLocked() {
|
|
3288
|
+
const next = await this.store.nextProgressWatchActionAt();
|
|
3289
|
+
const parsed = next === undefined ? Number.NaN : Date.parse(next);
|
|
3290
|
+
this.nextProgressWatchAt = Number.isFinite(parsed) ? parsed : undefined;
|
|
3291
|
+
}
|
|
3292
|
+
async processProgressWatchesLocked(now) {
|
|
3293
|
+
if (this.nextProgressWatchAt === undefined ||
|
|
3294
|
+
this.nextProgressWatchAt > now) {
|
|
3295
|
+
return false;
|
|
3296
|
+
}
|
|
3297
|
+
const actions = await this.store.advanceDueProgressWatches();
|
|
3298
|
+
let changed = actions.length > 0;
|
|
3299
|
+
for (const action of actions) {
|
|
3300
|
+
if (action.type === "settled") {
|
|
3301
|
+
this.addRuntimeAlert(action.outcome === "unresponsive"
|
|
3302
|
+
? "PROGRESS_WATCH_UNRESPONSIVE"
|
|
3303
|
+
: "PROGRESS_WATCH_ENDPOINT_RETIRED", action.outcome === "unresponsive" ? "error" : "warning", { alias: action.ownerAlias });
|
|
3304
|
+
continue;
|
|
3305
|
+
}
|
|
3306
|
+
if (action.type === "notify_capability_degraded") {
|
|
3307
|
+
this.addRuntimeAlert("PROGRESS_WATCH_CAPABILITY_DEGRADED", "warning", {
|
|
3308
|
+
alias: action.ownerAlias,
|
|
3309
|
+
});
|
|
3310
|
+
continue;
|
|
3311
|
+
}
|
|
3312
|
+
const text = `[Embassy automated liveness check — ${action.conversationId} / ` +
|
|
3313
|
+
`Embassy 自动活跃检查 — ${action.conversationId}]\n` +
|
|
3314
|
+
"Reply with the result or a status. / 请回复结果或状态。";
|
|
3315
|
+
try {
|
|
3316
|
+
await this.enqueue(action.ownerAlias, action.workerAlias, text, false, false, {
|
|
3317
|
+
existingConversationId: action.conversationId,
|
|
3318
|
+
requestedHopCount: 0,
|
|
3319
|
+
exposeDeliveryToken: false,
|
|
3320
|
+
skipProgressWatchActivity: true,
|
|
3321
|
+
preserveConversationHop: true,
|
|
3322
|
+
progressWatchNudge: {
|
|
3323
|
+
conversationId: action.conversationId,
|
|
3324
|
+
nudgeNumber: action.nudgeNumber,
|
|
3325
|
+
},
|
|
3326
|
+
});
|
|
3327
|
+
this.addRuntimeAlert("PROGRESS_WATCH_NUDGE", "warning", {
|
|
3328
|
+
alias: action.ownerAlias,
|
|
3329
|
+
});
|
|
3330
|
+
}
|
|
3331
|
+
catch {
|
|
3332
|
+
await this.store.deferProgressWatchNudge(action.conversationId);
|
|
3333
|
+
this.addRuntimeAlert("PROGRESS_WATCH_NUDGE_DEFERRED", "warning", {
|
|
3334
|
+
alias: action.ownerAlias,
|
|
3335
|
+
});
|
|
3336
|
+
changed = true;
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
await this.refreshNextProgressWatchWakeLocked();
|
|
3340
|
+
return changed;
|
|
3341
|
+
}
|
|
3342
|
+
async processLifecycleLocked() {
|
|
3343
|
+
const now = this.now().getTime();
|
|
3344
|
+
let changed = false;
|
|
3345
|
+
if (await this.processProgressWatchesLocked(now))
|
|
3346
|
+
changed = true;
|
|
3347
|
+
await this.drainPreDeadlineDeliveryCallbacksLocked();
|
|
3348
|
+
for (const tracker of this.deliveryTrackers.values()) {
|
|
3349
|
+
if (tracker.pendingTerminalEvent !== undefined) {
|
|
3350
|
+
if ((tracker.settlementRetryAt ?? 0) <= now) {
|
|
3351
|
+
await this.advanceDeliveryLocked(tracker.messageId, tracker.pendingTerminalEvent);
|
|
3352
|
+
changed = true;
|
|
3353
|
+
}
|
|
3354
|
+
if (tracker.pendingTerminalEvent !== undefined)
|
|
3355
|
+
continue;
|
|
3356
|
+
}
|
|
3357
|
+
if (!isTerminalDeliveryMachine(tracker.machine)) {
|
|
3358
|
+
const wakeups = projectDeliveryWakeups(tracker.machine);
|
|
3359
|
+
if (wakeups.stallAt !== undefined && wakeups.stallAt <= now) {
|
|
3360
|
+
await this.advanceDeliveryLocked(tracker.messageId, {
|
|
3361
|
+
type: "stall_due",
|
|
3362
|
+
at: now,
|
|
3363
|
+
});
|
|
3364
|
+
changed = true;
|
|
3365
|
+
}
|
|
3366
|
+
if (!isTerminalDeliveryMachine(tracker.machine) &&
|
|
3367
|
+
tracker.machine.dispatchRetryAt !== null &&
|
|
3368
|
+
tracker.machine.dispatchRetryAt <= now) {
|
|
3369
|
+
if (!this.dispatchRunnerTargets.has(tracker.targetAlias)) {
|
|
3370
|
+
this.scheduleDispatch(tracker.targetAlias);
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
if (!isTerminalDeliveryMachine(tracker.machine) &&
|
|
3374
|
+
tracker.deadlineAt <= now) {
|
|
3375
|
+
await this.advanceDeliveryLocked(tracker.messageId, {
|
|
3376
|
+
type: "deadline_due",
|
|
3377
|
+
at: now,
|
|
3378
|
+
});
|
|
3379
|
+
changed = true;
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
if (tracker.machine.stall === "emitted" &&
|
|
3383
|
+
!tracker.stallNoticeSent &&
|
|
3384
|
+
tracker.stallNoticeNextAttemptAt <= now &&
|
|
3385
|
+
!isTerminalDeliveryMachine(tracker.machine)) {
|
|
3386
|
+
await this.publishStallNoticeLocked(tracker, Math.max(0, now - tracker.enqueuedAt));
|
|
3387
|
+
changed = true;
|
|
3388
|
+
}
|
|
3389
|
+
if (!isTerminalDeliveryMachine(tracker.machine) &&
|
|
3390
|
+
tracker.nativeReceipt !== undefined &&
|
|
3391
|
+
!tracker.nativeHeldSent &&
|
|
3392
|
+
tracker.nativeHeldNextAttemptAt <= now) {
|
|
3393
|
+
if (await this.publishNativeHeldLocked(tracker))
|
|
3394
|
+
changed = true;
|
|
3395
|
+
}
|
|
3396
|
+
const receiptRetryAt = projectDeliveryWakeups(tracker.machine).nativeReceiptRetryAt;
|
|
3397
|
+
if (receiptRetryAt !== undefined && receiptRetryAt <= now) {
|
|
3398
|
+
await this.advanceDeliveryLocked(tracker.messageId, {
|
|
3399
|
+
type: "native_receipt_retry_due",
|
|
3400
|
+
at: now,
|
|
3401
|
+
});
|
|
3402
|
+
changed = true;
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
for (const [messageId, continuation] of this.providerTurnContinuations) {
|
|
3406
|
+
if (Date.parse(continuation.deadlineAt) > now)
|
|
3407
|
+
continue;
|
|
3408
|
+
await this.finishProviderTurnContinuationLocked(messageId, "expired");
|
|
3409
|
+
this.addRuntimeAlert("PROVIDER_REPLY_DEADLINE_EXPIRED", "warning", {
|
|
3410
|
+
alias: continuation.targetAlias,
|
|
3411
|
+
});
|
|
3412
|
+
changed = true;
|
|
3413
|
+
}
|
|
3414
|
+
if (this.nextDashboardRefreshAt !== undefined &&
|
|
3415
|
+
this.nextDashboardRefreshAt <= now) {
|
|
3416
|
+
changed = true;
|
|
3417
|
+
this.nextDashboardRefreshAt = now + DELIVERY_DASHBOARD_REFRESH_MS;
|
|
3418
|
+
}
|
|
3419
|
+
this.pruneTransient();
|
|
3420
|
+
this.pruneDeliveryTrackers(now);
|
|
3421
|
+
this.scheduleLifecycleWakeLocked();
|
|
3422
|
+
return changed;
|
|
3423
|
+
}
|
|
3424
|
+
scheduleLifecycleWakeLocked() {
|
|
3425
|
+
if (this.lifecycleTimer !== undefined) {
|
|
3426
|
+
this.timers.clearTimeout(this.lifecycleTimer);
|
|
3427
|
+
this.lifecycleTimer = undefined;
|
|
3428
|
+
}
|
|
3429
|
+
if (!this.running || this.closing)
|
|
3430
|
+
return;
|
|
3431
|
+
const now = this.now().getTime();
|
|
3432
|
+
let wakeAt;
|
|
3433
|
+
let hasNonterminal = false;
|
|
3434
|
+
const consider = (candidate) => {
|
|
3435
|
+
wakeAt = wakeAt === undefined ? candidate : Math.min(wakeAt, candidate);
|
|
3436
|
+
};
|
|
3437
|
+
for (const tracker of this.deliveryTrackers.values()) {
|
|
3438
|
+
const wakeups = projectDeliveryWakeups(tracker.machine);
|
|
3439
|
+
if (tracker.settlementRetryAt !== undefined) {
|
|
3440
|
+
consider(tracker.settlementRetryAt);
|
|
3441
|
+
}
|
|
3442
|
+
if (!isTerminalDeliveryMachine(tracker.machine)) {
|
|
3443
|
+
hasNonterminal = true;
|
|
3444
|
+
if (wakeups.stallAt !== undefined)
|
|
3445
|
+
consider(wakeups.stallAt);
|
|
3446
|
+
if (tracker.machine.stall === "emitted" &&
|
|
3447
|
+
!tracker.stallNoticeSent) {
|
|
3448
|
+
consider(tracker.stallNoticeNextAttemptAt);
|
|
3449
|
+
}
|
|
3450
|
+
if (tracker.nativeReceipt !== undefined &&
|
|
3451
|
+
!tracker.nativeHeldSent) {
|
|
3452
|
+
consider(tracker.nativeHeldNextAttemptAt);
|
|
3453
|
+
}
|
|
3454
|
+
if (wakeups.deadlineAt !== undefined)
|
|
3455
|
+
consider(wakeups.deadlineAt);
|
|
3456
|
+
if (wakeups.dispatchRetryAt !== undefined &&
|
|
3457
|
+
!this.scheduledDispatchTargets.has(tracker.targetAlias) &&
|
|
3458
|
+
!this.dispatchRunnerTargets.has(tracker.targetAlias)) {
|
|
3459
|
+
consider(wakeups.dispatchRetryAt);
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
if (wakeups.nativeReceiptRetryAt !== undefined) {
|
|
3463
|
+
consider(wakeups.nativeReceiptRetryAt);
|
|
3464
|
+
}
|
|
3465
|
+
if (isTerminalDeliveryMachine(tracker.machine) &&
|
|
3466
|
+
tracker.deliveryToken !== undefined) {
|
|
3467
|
+
consider(tracker.machine.terminalAt +
|
|
3468
|
+
Math.max(60_000, this.config.limits.messageDeadlineMs * 2));
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
// Provider acceptance may settle the original body while its bounded
|
|
3472
|
+
// reply correlation remains live; plain pending stays nonterminal. Keep
|
|
3473
|
+
// every retained reply context's deadline scheduled independently.
|
|
3474
|
+
for (const continuation of this.providerTurnContinuations.values()) {
|
|
3475
|
+
const deadlineAt = Date.parse(continuation.deadlineAt);
|
|
3476
|
+
if (Number.isFinite(deadlineAt))
|
|
3477
|
+
consider(deadlineAt);
|
|
3478
|
+
}
|
|
3479
|
+
if (this.nextProgressWatchAt !== undefined) {
|
|
3480
|
+
consider(this.nextProgressWatchAt);
|
|
3481
|
+
}
|
|
3482
|
+
if (hasNonterminal) {
|
|
3483
|
+
this.nextDashboardRefreshAt ??= now + DELIVERY_DASHBOARD_REFRESH_MS;
|
|
3484
|
+
consider(this.nextDashboardRefreshAt);
|
|
3485
|
+
}
|
|
3486
|
+
else {
|
|
3487
|
+
this.nextDashboardRefreshAt = undefined;
|
|
3488
|
+
}
|
|
3489
|
+
if (wakeAt === undefined)
|
|
3490
|
+
return;
|
|
3491
|
+
const timer = this.timers.setTimeout(() => {
|
|
3492
|
+
if (this.lifecycleTimer !== timer)
|
|
3493
|
+
return;
|
|
3494
|
+
this.lifecycleTimer = undefined;
|
|
3495
|
+
void this.mutex
|
|
3496
|
+
.run("service", async () => {
|
|
3497
|
+
const changed = await this.processLifecycleLocked();
|
|
3498
|
+
if (changed) {
|
|
3499
|
+
this.revision += 1;
|
|
3500
|
+
await this.publish();
|
|
3501
|
+
}
|
|
3502
|
+
})
|
|
3503
|
+
.catch(() => {
|
|
3504
|
+
this.dashboardHealthy = false;
|
|
3505
|
+
});
|
|
3506
|
+
}, Math.max(0, wakeAt - now));
|
|
3507
|
+
this.lifecycleTimer = timer;
|
|
3508
|
+
timer.unref?.();
|
|
3509
|
+
}
|
|
3510
|
+
async deliveryStatus(token) {
|
|
3511
|
+
return await this.mutex.run("service", async () => {
|
|
3512
|
+
if (!DELIVERY_TOKEN.test(token))
|
|
3513
|
+
return { found: false };
|
|
3514
|
+
const changed = await this.processLifecycleLocked();
|
|
3515
|
+
if (changed) {
|
|
3516
|
+
this.revision += 1;
|
|
3517
|
+
await this.publish();
|
|
3518
|
+
}
|
|
3519
|
+
const messageId = this.deliveryTokens.get(token);
|
|
3520
|
+
const tracker = messageId === undefined
|
|
3521
|
+
? undefined
|
|
3522
|
+
: this.deliveryTrackers.get(messageId);
|
|
3523
|
+
if (tracker === undefined || tracker.deliveryToken !== token) {
|
|
3524
|
+
return { found: false };
|
|
3525
|
+
}
|
|
3526
|
+
const projection = projectDelivery(tracker.machine, this.now().getTime());
|
|
3527
|
+
const terminal = projection.terminal;
|
|
3528
|
+
return {
|
|
3529
|
+
found: true,
|
|
3530
|
+
state: terminal
|
|
3531
|
+
? projection.outcome
|
|
3532
|
+
: projection.stalled
|
|
3533
|
+
? "stalled"
|
|
3534
|
+
: "queued",
|
|
3535
|
+
terminal,
|
|
3536
|
+
updatedAt: new Date(tracker.updatedAt).toISOString(),
|
|
3537
|
+
deadlineAt: new Date(tracker.deadlineAt).toISOString(),
|
|
3538
|
+
...(!terminal
|
|
3539
|
+
? { pendingForMs: Math.max(0, this.now().getTime() - tracker.enqueuedAt) }
|
|
3540
|
+
: {}),
|
|
3541
|
+
...(tracker.deliverySafeErrorCode === undefined
|
|
3542
|
+
? {}
|
|
3543
|
+
: { safeErrorCode: tracker.deliverySafeErrorCode }),
|
|
3544
|
+
};
|
|
3545
|
+
});
|
|
3546
|
+
}
|
|
3547
|
+
async acceptToClaude(params) {
|
|
3548
|
+
return await this.mutex.run("service", async () => {
|
|
3549
|
+
let discoveryChanged = false;
|
|
3550
|
+
try {
|
|
3551
|
+
await this.verifyCodex(params.fromAlias, params.threadId);
|
|
3552
|
+
const resolved = await this.resolveSelectedClaudeDestination(params.toAlias);
|
|
3553
|
+
discoveryChanged = resolved.discoveryChanged;
|
|
3554
|
+
return this.acceptedControlResult(await this.enqueue(params.fromAlias, resolved.alias, params.text, params.expectsReply, false, params.trackIdleMinutes === undefined
|
|
3555
|
+
? {}
|
|
3556
|
+
: { trackIdleMinutes: params.trackIdleMinutes }));
|
|
3557
|
+
}
|
|
3558
|
+
catch (error) {
|
|
3559
|
+
// A successful enqueue publishes both the discovery and message
|
|
3560
|
+
// changes once. If the enqueue fails after discovery succeeded, the
|
|
3561
|
+
// dashboard still needs the new peer/route projection.
|
|
3562
|
+
if (discoveryChanged)
|
|
3563
|
+
await this.publish();
|
|
3564
|
+
return decisionFor(error);
|
|
3565
|
+
}
|
|
3566
|
+
});
|
|
3567
|
+
}
|
|
3568
|
+
async acceptToCodex(params) {
|
|
3569
|
+
return await this.mutex.run("service", async () => {
|
|
3570
|
+
try {
|
|
3571
|
+
await this.verifyClaude(params.fromAlias, params.replyAddress);
|
|
3572
|
+
const steer = this.config.steeringEnabled && params.text.startsWith("STEER:");
|
|
3573
|
+
return this.acceptedControlResult(await this.enqueue(params.fromAlias, params.toAlias, params.text, steer ? false : params.expectsReply, false, {
|
|
3574
|
+
...(steer ? { steer: true } : {}),
|
|
3575
|
+
...(params.trackIdleMinutes === undefined
|
|
3576
|
+
? {}
|
|
3577
|
+
: { trackIdleMinutes: params.trackIdleMinutes }),
|
|
3578
|
+
}));
|
|
3579
|
+
}
|
|
3580
|
+
catch (error) {
|
|
3581
|
+
return decisionFor(error);
|
|
3582
|
+
}
|
|
3583
|
+
});
|
|
3584
|
+
}
|
|
3585
|
+
async acceptReply(params) {
|
|
3586
|
+
return await this.mutex.run("service", async () => {
|
|
3587
|
+
try {
|
|
3588
|
+
const conversation = this.conversations.get(params.conversationId);
|
|
3589
|
+
if (conversation === undefined)
|
|
3590
|
+
throw new BridgeError("CONVERSATION_NOT_FOUND", "The conversation is not active.");
|
|
3591
|
+
const caller = params.caller;
|
|
3592
|
+
if (caller.kind === "codex")
|
|
3593
|
+
await this.verifyCodex(caller.alias, caller.threadId);
|
|
3594
|
+
else
|
|
3595
|
+
await this.verifyClaude(caller.alias, caller.replyAddress);
|
|
3596
|
+
const from = caller.alias;
|
|
3597
|
+
const to = from === conversation.sourceAlias ? conversation.targetAlias : from === conversation.targetAlias ? conversation.sourceAlias : undefined;
|
|
3598
|
+
if (to === undefined)
|
|
3599
|
+
throw new BridgeError("ROUTE_MISMATCH", "The caller is not a conversation participant.");
|
|
3600
|
+
const result = caller.kind === "codex" &&
|
|
3601
|
+
this.nativeIngressByConversation.has(conversation.id)
|
|
3602
|
+
? await this.enqueueNativeReply(conversation, from, params.text, conversation.lastHopCount + 1, true, params.trackIdleMinutes)
|
|
3603
|
+
: await this.enqueue(from, to, params.text, false, true, {
|
|
3604
|
+
existingConversationId: conversation.id,
|
|
3605
|
+
requestedHopCount: conversation.lastHopCount + 1,
|
|
3606
|
+
...(params.trackIdleMinutes === undefined
|
|
3607
|
+
? {}
|
|
3608
|
+
: { trackIdleMinutes: params.trackIdleMinutes }),
|
|
3609
|
+
});
|
|
3610
|
+
return this.acceptedControlResult(result);
|
|
3611
|
+
}
|
|
3612
|
+
catch (error) {
|
|
3613
|
+
return decisionFor(error);
|
|
3614
|
+
}
|
|
3615
|
+
});
|
|
3616
|
+
}
|
|
3617
|
+
async enqueue(sourceAlias, targetAlias, text, expectsReply, isReply, options = {}) {
|
|
3618
|
+
if (this.codexSuccessionPoisoned.has(sourceAlias) ||
|
|
3619
|
+
this.codexSuccessionPoisoned.has(targetAlias)) {
|
|
3620
|
+
throw new BridgeError("CODEX_SUCCESSION_RECOVERY_REQUIRED", "No message can be accepted while Codex succession requires manual recovery.");
|
|
3621
|
+
}
|
|
3622
|
+
this.pruneTransient();
|
|
3623
|
+
if (options.existingConversationId === undefined &&
|
|
3624
|
+
this.conversations.size >= MAX_CONVERSATIONS) {
|
|
3625
|
+
throw new BridgeError("CONVERSATION_CAPACITY", "The transient conversation table is full.", true);
|
|
3626
|
+
}
|
|
3627
|
+
const target = await this.store.resolveRoute(targetAlias);
|
|
3628
|
+
if (expectsReply && target.provider === "claude") {
|
|
3629
|
+
const owner = this.pendingClaudeReplies.get(bindingKey(target));
|
|
3630
|
+
if (owner !== undefined)
|
|
3631
|
+
throw new BridgeError("REPLY_ROUTE_BUSY", "That exact peer already owns a pending reply.", true);
|
|
3632
|
+
}
|
|
3633
|
+
const conversationId = options.existingConversationId ?? createGatewayConversationId();
|
|
3634
|
+
const conversation = this.conversations.get(conversationId) ?? {
|
|
3635
|
+
id: conversationId,
|
|
3636
|
+
sourceAlias,
|
|
3637
|
+
targetAlias,
|
|
3638
|
+
expectsReply,
|
|
3639
|
+
nextSequence: 0,
|
|
3640
|
+
lastHopCount: 0,
|
|
3641
|
+
lastActivityAt: this.now().toISOString(),
|
|
3642
|
+
};
|
|
3643
|
+
const hopCount = options.requestedHopCount ??
|
|
3644
|
+
(isReply ? conversation.lastHopCount + 1 : 0);
|
|
3645
|
+
const sequence = conversation.nextSequence;
|
|
3646
|
+
const enqueuedAt = this.now().getTime();
|
|
3647
|
+
const deliveryToken = (options.exposeDeliveryToken ?? true)
|
|
3648
|
+
? this.reserveDeliveryToken()
|
|
3649
|
+
: undefined;
|
|
3650
|
+
const deadlineAt = new Date(enqueuedAt + this.config.limits.messageDeadlineMs).toISOString();
|
|
3651
|
+
const queued = await this.store.enqueueMessage({
|
|
3652
|
+
sourceAlias,
|
|
3653
|
+
targetAlias,
|
|
3654
|
+
body: text,
|
|
3655
|
+
dedupeKey: `${conversationId}:${sequence}`,
|
|
3656
|
+
conversationIdSuffix: conversationId.slice(-8),
|
|
3657
|
+
hopCount,
|
|
3658
|
+
deadlineAt,
|
|
3659
|
+
...(options.steer === true ? { steer: true } : {}),
|
|
3660
|
+
...(options.skipProgressWatchActivity === true
|
|
3661
|
+
? {}
|
|
3662
|
+
: {
|
|
3663
|
+
progressWatch: progressWatchActivity(text, conversationId, sourceAlias, options.trackIdleMinutes),
|
|
3664
|
+
}),
|
|
3665
|
+
...(options.progressWatchNudge === undefined
|
|
3666
|
+
? {}
|
|
3667
|
+
: { progressWatchNudge: options.progressWatchNudge }),
|
|
3668
|
+
});
|
|
3669
|
+
if (!queued.accepted || queued.messageId === undefined)
|
|
3670
|
+
throw new BridgeError("MESSAGE_REJECTED", "The message was not accepted.");
|
|
3671
|
+
if (queued.supersededSettlement !== undefined) {
|
|
3672
|
+
await this.applyTerminalSettlementLocked(queued.supersededSettlement);
|
|
3673
|
+
}
|
|
3674
|
+
this.conversations.set(conversationId, conversation);
|
|
3675
|
+
if (queued.pair === true)
|
|
3676
|
+
conversation.pair = true;
|
|
3677
|
+
conversation.nextSequence += 1;
|
|
3678
|
+
if (options.preserveConversationHop !== true) {
|
|
3679
|
+
conversation.lastHopCount = hopCount;
|
|
3680
|
+
}
|
|
3681
|
+
conversation.lastActivityAt = this.now().toISOString();
|
|
3682
|
+
this.messageContexts.set(queued.messageId, {
|
|
3683
|
+
conversationId,
|
|
3684
|
+
isReply,
|
|
3685
|
+
expectsReply,
|
|
3686
|
+
hopCount,
|
|
3687
|
+
sequence,
|
|
3688
|
+
targetBindingKey: bindingKey(target),
|
|
3689
|
+
authorization: "selected_route",
|
|
3690
|
+
targetAlias,
|
|
3691
|
+
deadlineAt,
|
|
3692
|
+
});
|
|
3693
|
+
this.armDeliveryTracker({
|
|
3694
|
+
messageId: queued.messageId,
|
|
3695
|
+
conversationId,
|
|
3696
|
+
targetAlias,
|
|
3697
|
+
enqueuedAt,
|
|
3698
|
+
deadlineAt,
|
|
3699
|
+
...(deliveryToken === undefined ? {} : { deliveryToken }),
|
|
3700
|
+
});
|
|
3701
|
+
if (expectsReply && target.provider === "claude") {
|
|
3702
|
+
this.pendingClaudeReplies.set(bindingKey(target), {
|
|
3703
|
+
conversationId,
|
|
3704
|
+
bindingKey: bindingKey(target),
|
|
3705
|
+
hopCount,
|
|
3706
|
+
deadlineAt,
|
|
3707
|
+
tainted: false,
|
|
3708
|
+
});
|
|
3709
|
+
}
|
|
3710
|
+
await this.changed();
|
|
3711
|
+
// Provider I/O starts only after the enqueueing handler can return.
|
|
3712
|
+
this.scheduleDispatch(targetAlias);
|
|
3713
|
+
return {
|
|
3714
|
+
conversationId,
|
|
3715
|
+
messageId: queued.messageId,
|
|
3716
|
+
...(deliveryToken === undefined ? {} : { deliveryToken }),
|
|
3717
|
+
};
|
|
3718
|
+
}
|
|
3719
|
+
async enqueueNativeReply(conversation, sourceAlias, text, requestedHopCount, exposeDeliveryToken = true, trackIdleMinutes) {
|
|
3720
|
+
this.pruneTransient();
|
|
3721
|
+
const capability = this.nativeIngressByConversation.get(conversation.id);
|
|
3722
|
+
if (capability === undefined ||
|
|
3723
|
+
sourceAlias !== conversation.targetAlias ||
|
|
3724
|
+
capability.sourceAlias !== conversation.sourceAlias ||
|
|
3725
|
+
Date.parse(capability.deadlineAt) <= this.now().getTime()) {
|
|
3726
|
+
throw new BridgeError("NATIVE_REPLY_CAPABILITY_EXPIRED", "The correlated native Claude reply capability is no longer active.");
|
|
3727
|
+
}
|
|
3728
|
+
const sequence = conversation.nextSequence;
|
|
3729
|
+
const enqueuedAt = this.now().getTime();
|
|
3730
|
+
const deliveryToken = exposeDeliveryToken
|
|
3731
|
+
? this.reserveDeliveryToken()
|
|
3732
|
+
: undefined;
|
|
3733
|
+
const deadlineAt = new Date(Math.min(Date.parse(capability.deadlineAt), this.now().getTime() + this.config.limits.messageDeadlineMs)).toISOString();
|
|
3734
|
+
const queued = await this.store.enqueueNativeReply({
|
|
3735
|
+
sourceAlias,
|
|
3736
|
+
target: {
|
|
3737
|
+
alias: capability.sourceAlias,
|
|
3738
|
+
binding: capability.binding,
|
|
3739
|
+
},
|
|
3740
|
+
body: text,
|
|
3741
|
+
dedupeKey: `${conversation.id}:${sequence}`,
|
|
3742
|
+
conversationIdSuffix: conversation.id.slice(-8),
|
|
3743
|
+
hopCount: requestedHopCount,
|
|
3744
|
+
deadlineAt,
|
|
3745
|
+
...(conversation.pair === true ? { pair: true } : {}),
|
|
3746
|
+
progressWatch: progressWatchActivity(text, conversation.id, sourceAlias, trackIdleMinutes),
|
|
3747
|
+
});
|
|
3748
|
+
if (!queued.accepted || queued.messageId === undefined) {
|
|
3749
|
+
throw new BridgeError("MESSAGE_REJECTED", "The native reply was not accepted.");
|
|
3750
|
+
}
|
|
3751
|
+
conversation.nextSequence += 1;
|
|
3752
|
+
conversation.lastHopCount = requestedHopCount;
|
|
3753
|
+
conversation.lastActivityAt = this.now().toISOString();
|
|
3754
|
+
this.messageContexts.set(queued.messageId, {
|
|
3755
|
+
conversationId: conversation.id,
|
|
3756
|
+
isReply: true,
|
|
3757
|
+
expectsReply: false,
|
|
3758
|
+
hopCount: requestedHopCount,
|
|
3759
|
+
sequence,
|
|
3760
|
+
targetBindingKey: bindingKey(capability.binding),
|
|
3761
|
+
nativeReplyBinding: { ...capability.binding },
|
|
3762
|
+
authorization: "native_reply",
|
|
3763
|
+
targetAlias: capability.sourceAlias,
|
|
3764
|
+
deadlineAt,
|
|
3765
|
+
});
|
|
3766
|
+
this.armDeliveryTracker({
|
|
3767
|
+
messageId: queued.messageId,
|
|
3768
|
+
conversationId: conversation.id,
|
|
3769
|
+
targetAlias: capability.sourceAlias,
|
|
3770
|
+
enqueuedAt,
|
|
3771
|
+
deadlineAt,
|
|
3772
|
+
...(deliveryToken === undefined ? {} : { deliveryToken }),
|
|
3773
|
+
});
|
|
3774
|
+
this.nativeIngressByConversation.delete(conversation.id);
|
|
3775
|
+
await this.changed();
|
|
3776
|
+
this.scheduleDispatch(capability.sourceAlias);
|
|
3777
|
+
return {
|
|
3778
|
+
conversationId: conversation.id,
|
|
3779
|
+
messageId: queued.messageId,
|
|
3780
|
+
...(deliveryToken === undefined ? {} : { deliveryToken }),
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
/**
|
|
3784
|
+
* A provider reply observed before route teardown remains valid evidence
|
|
3785
|
+
* even when the same atomic mutation removes its selected Claude route.
|
|
3786
|
+
* Re-enter that already-observed reply through the existing transient
|
|
3787
|
+
* native-ingress boundary; this preserves the exact connector generation
|
|
3788
|
+
* without recreating route authority or accepting a new unsolicited send.
|
|
3789
|
+
*/
|
|
3790
|
+
async enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, text, requestedHopCount) {
|
|
3791
|
+
const target = await this.store.resolveRoute(conversation.sourceAlias);
|
|
3792
|
+
if (sourceBinding.provider !== "claude" ||
|
|
3793
|
+
target.provider !== "codex" ||
|
|
3794
|
+
sourceBinding.hostId !== target.hostId) {
|
|
3795
|
+
throw new BridgeError("RECOVERED_REPLY_ROUTE_MISMATCH", "The retained provider reply no longer matches an exact same-host Codex target.");
|
|
3796
|
+
}
|
|
3797
|
+
const sequence = conversation.nextSequence;
|
|
3798
|
+
const enqueuedAt = this.now().getTime();
|
|
3799
|
+
const deadlineAt = new Date(enqueuedAt + this.config.limits.messageDeadlineMs).toISOString();
|
|
3800
|
+
const queued = await this.store.enqueueNativeIngress({
|
|
3801
|
+
source: {
|
|
3802
|
+
alias: conversation.targetAlias,
|
|
3803
|
+
binding: sourceBinding,
|
|
3804
|
+
},
|
|
3805
|
+
targetAlias: conversation.sourceAlias,
|
|
3806
|
+
body: text,
|
|
3807
|
+
dedupeKey: `${conversation.id}:${sequence}`,
|
|
3808
|
+
conversationIdSuffix: conversation.id.slice(-8),
|
|
3809
|
+
hopCount: requestedHopCount,
|
|
3810
|
+
deadlineAt,
|
|
3811
|
+
authorizedPairTeardownReply: true,
|
|
3812
|
+
progressWatch: progressWatchActivity(text, conversation.id, conversation.targetAlias),
|
|
3813
|
+
});
|
|
3814
|
+
if (!queued.accepted || queued.messageId === undefined) {
|
|
3815
|
+
throw new BridgeError("MESSAGE_REJECTED", "The retained provider reply was not accepted.");
|
|
3816
|
+
}
|
|
3817
|
+
conversation.nextSequence += 1;
|
|
3818
|
+
conversation.lastHopCount = requestedHopCount;
|
|
3819
|
+
conversation.lastActivityAt = this.now().toISOString();
|
|
3820
|
+
// The old edge has already been removed. Retain only this already-observed
|
|
3821
|
+
// reply attempt; the conversation no longer grants pair reply authority.
|
|
3822
|
+
delete conversation.pair;
|
|
3823
|
+
this.messageContexts.set(queued.messageId, {
|
|
3824
|
+
conversationId: conversation.id,
|
|
3825
|
+
isReply: true,
|
|
3826
|
+
expectsReply: false,
|
|
3827
|
+
hopCount: requestedHopCount,
|
|
3828
|
+
sequence,
|
|
3829
|
+
targetBindingKey: bindingKey(target),
|
|
3830
|
+
authorization: "selected_route",
|
|
3831
|
+
targetAlias: conversation.sourceAlias,
|
|
3832
|
+
deadlineAt,
|
|
3833
|
+
});
|
|
3834
|
+
this.armDeliveryTracker({
|
|
3835
|
+
messageId: queued.messageId,
|
|
3836
|
+
conversationId: conversation.id,
|
|
3837
|
+
targetAlias: conversation.sourceAlias,
|
|
3838
|
+
enqueuedAt,
|
|
3839
|
+
deadlineAt,
|
|
3840
|
+
});
|
|
3841
|
+
await this.changed();
|
|
3842
|
+
await this.setNativeCodexStatus(conversation.sourceAlias, "waiting");
|
|
3843
|
+
this.scheduleDispatch(conversation.sourceAlias);
|
|
3844
|
+
}
|
|
3845
|
+
pruneTransient() {
|
|
3846
|
+
const now = this.now().getTime();
|
|
3847
|
+
// Message deadlines are owned by the service lifecycle sweep. Never drop
|
|
3848
|
+
// correlation here: doing so previously made store expiry sender-silent.
|
|
3849
|
+
for (const [key, pending] of this.pendingClaudeReplies) {
|
|
3850
|
+
if (Date.parse(pending.deadlineAt) <= now) {
|
|
3851
|
+
pending.tainted = true;
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
for (const [conversationId, capability] of this.nativeIngressByConversation) {
|
|
3855
|
+
if (Date.parse(capability.deadlineAt) <= now) {
|
|
3856
|
+
this.nativeIngressByConversation.delete(conversationId);
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
const active = new Set([...this.messageContexts.values()].map((context) => context.conversationId));
|
|
3860
|
+
for (const continuation of this.providerTurnContinuations.values()) {
|
|
3861
|
+
active.add(continuation.conversationId);
|
|
3862
|
+
}
|
|
3863
|
+
for (const pending of this.pendingClaudeReplies.values()) {
|
|
3864
|
+
active.add(pending.conversationId);
|
|
3865
|
+
}
|
|
3866
|
+
for (const conversationId of this.nativeIngressByConversation.keys()) {
|
|
3867
|
+
active.add(conversationId);
|
|
3868
|
+
}
|
|
3869
|
+
const ttl = Math.max(60_000, this.config.limits.messageDeadlineMs * 2);
|
|
3870
|
+
for (const id of this.conversations.keys()) {
|
|
3871
|
+
const conversation = this.conversations.get(id);
|
|
3872
|
+
if (conversation === undefined ||
|
|
3873
|
+
active.has(id) ||
|
|
3874
|
+
(this.conversations.size < MAX_CONVERSATIONS &&
|
|
3875
|
+
Date.parse(conversation.lastActivityAt) + ttl > now)) {
|
|
3876
|
+
continue;
|
|
3877
|
+
}
|
|
3878
|
+
this.conversations.delete(id);
|
|
3879
|
+
if (this.conversations.size < MAX_CONVERSATIONS)
|
|
3880
|
+
break;
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
async awaitDispatchWithNativeHeldLocked(tracker, operation) {
|
|
3884
|
+
const outcome = operation.then((value) => ({ kind: "result", value }), (error) => ({ kind: "error", error }));
|
|
3885
|
+
const unwrap = (settled) => {
|
|
3886
|
+
if (settled.kind === "error")
|
|
3887
|
+
throw settled.error;
|
|
3888
|
+
return settled.value;
|
|
3889
|
+
};
|
|
3890
|
+
while (true) {
|
|
3891
|
+
const current = this.deliveryTrackers.get(tracker.messageId);
|
|
3892
|
+
if (current === undefined ||
|
|
3893
|
+
current.nativeReceipt === undefined ||
|
|
3894
|
+
current.nativeHeldSent ||
|
|
3895
|
+
isTerminalDeliveryMachine(current.machine)) {
|
|
3896
|
+
return unwrap(await outcome);
|
|
3897
|
+
}
|
|
3898
|
+
const now = this.now().getTime();
|
|
3899
|
+
if (now >= current.deadlineAt)
|
|
3900
|
+
return unwrap(await outcome);
|
|
3901
|
+
const delay = Math.max(0, current.nativeHeldNextAttemptAt - now);
|
|
3902
|
+
if (delay > 0) {
|
|
3903
|
+
let timer;
|
|
3904
|
+
const due = new Promise((resolve) => {
|
|
3905
|
+
timer = this.timers.setTimeout(() => resolve({ kind: "due" }), delay);
|
|
3906
|
+
timer.unref?.();
|
|
3907
|
+
});
|
|
3908
|
+
const first = await Promise.race([outcome, due]);
|
|
3909
|
+
if (timer !== undefined)
|
|
3910
|
+
this.timers.clearTimeout(timer);
|
|
3911
|
+
if (first.kind !== "due")
|
|
3912
|
+
return unwrap(first);
|
|
3913
|
+
}
|
|
3914
|
+
// A terminal callback may have arrived before the provider's dispatch
|
|
3915
|
+
// promise resolved. Let that exact evidence win before emitting held.
|
|
3916
|
+
await this.drainDeliveryCallbacksForMessageLocked(tracker.messageId);
|
|
3917
|
+
const afterCallbacks = this.deliveryTrackers.get(tracker.messageId);
|
|
3918
|
+
if (afterCallbacks === undefined ||
|
|
3919
|
+
isTerminalDeliveryMachine(afterCallbacks.machine) ||
|
|
3920
|
+
this.now().getTime() >= afterCallbacks.deadlineAt) {
|
|
3921
|
+
return unwrap(await outcome);
|
|
3922
|
+
}
|
|
3923
|
+
await this.publishNativeHeldLocked(afterCallbacks);
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
async dispatchOne(targetAlias) {
|
|
3927
|
+
if (!this.running ||
|
|
3928
|
+
this.closing ||
|
|
3929
|
+
this.codexSuccessionDispatchFrozen.has(targetAlias) ||
|
|
3930
|
+
this.codexSuccessionPoisoned.has(targetAlias)) {
|
|
3931
|
+
return;
|
|
3932
|
+
}
|
|
3933
|
+
if (await this.processLifecycleLocked()) {
|
|
3934
|
+
await this.changed();
|
|
3935
|
+
}
|
|
3936
|
+
const selectedBeforeDequeue = this.routeBindings.get(targetAlias);
|
|
3937
|
+
const activeDispatch = this.activeDispatchByTarget.get(targetAlias);
|
|
3938
|
+
const steeringWhileBusy = selectedBeforeDequeue?.provider === "codex" &&
|
|
3939
|
+
this.routeStates.get(targetAlias) === "busy";
|
|
3940
|
+
if (activeDispatch !== undefined && !steeringWhileBusy)
|
|
3941
|
+
return;
|
|
3942
|
+
let item = await this.store.dequeueMessage(targetAlias, steeringWhileBusy ? "steer_only" : "any");
|
|
3943
|
+
if (item === undefined &&
|
|
3944
|
+
steeringWhileBusy &&
|
|
3945
|
+
activeDispatch === undefined) {
|
|
3946
|
+
// Preserve the ordinary busy-route waiting transition when no steer can
|
|
3947
|
+
// bypass it. A live provider turn still fences ordinary work.
|
|
3948
|
+
item = await this.store.dequeueMessage(targetAlias, "any");
|
|
3949
|
+
}
|
|
3950
|
+
if (item === undefined)
|
|
3951
|
+
return;
|
|
3952
|
+
const context = this.messageContexts.get(item.messageId);
|
|
3953
|
+
const selectedBinding = this.routeBindings.get(targetAlias);
|
|
3954
|
+
const binding = context?.nativeReplyBinding ?? selectedBinding;
|
|
3955
|
+
if (context === undefined) {
|
|
3956
|
+
const result = await this.store.settleMessage({
|
|
3957
|
+
messageId: item.messageId,
|
|
3958
|
+
state: "failed",
|
|
3959
|
+
safeErrorCode: "MESSAGE_CONTEXT_UNAVAILABLE",
|
|
3960
|
+
});
|
|
3961
|
+
if (result.status === "settled") {
|
|
3962
|
+
await this.applyTerminalSettlementLocked(result.settlement);
|
|
3963
|
+
await this.changed();
|
|
3964
|
+
}
|
|
3965
|
+
return;
|
|
3966
|
+
}
|
|
3967
|
+
if (binding === undefined) {
|
|
3968
|
+
const result = await this.store.requeueInFlightMessage(item.messageId, item.body);
|
|
3969
|
+
if (result.status === "settled") {
|
|
3970
|
+
await this.applyTerminalSettlementLocked(result.settlement);
|
|
3971
|
+
}
|
|
3972
|
+
await this.changed();
|
|
3973
|
+
return;
|
|
3974
|
+
}
|
|
3975
|
+
if (context.authorization === "selected_route") {
|
|
3976
|
+
const routeState = this.routeStates.get(targetAlias);
|
|
3977
|
+
const dispatchable = routeState === "idle" ||
|
|
3978
|
+
(binding.provider === "claude" && routeState === "busy") ||
|
|
3979
|
+
(binding.provider === "codex" &&
|
|
3980
|
+
item.steer === true &&
|
|
3981
|
+
routeState === "busy");
|
|
3982
|
+
const inspection = await this.store.inspectPrivateRoute(targetAlias);
|
|
3983
|
+
const codexRouteStale = binding.provider === "codex" &&
|
|
3984
|
+
(routeState === "stale" ||
|
|
3985
|
+
inspection?.state === "stale" ||
|
|
3986
|
+
inspection?.safeErrorCode === "CODEX_ROUTE_STALE");
|
|
3987
|
+
if (codexRouteStale) {
|
|
3988
|
+
await this.advanceDeliveryLocked(item.messageId, {
|
|
3989
|
+
type: "route_terminated",
|
|
3990
|
+
at: this.now().getTime(),
|
|
3991
|
+
unwrittenOutcome: "failed",
|
|
3992
|
+
safeErrorCode: "CODEX_ROUTE_STALE",
|
|
3993
|
+
});
|
|
3994
|
+
await this.changed();
|
|
3995
|
+
this.scheduleDispatch(targetAlias);
|
|
3996
|
+
return;
|
|
3997
|
+
}
|
|
3998
|
+
const storedDispatchable = inspection?.state === "idle" ||
|
|
3999
|
+
(binding.provider === "claude" && inspection?.state === "busy") ||
|
|
4000
|
+
(binding.provider === "codex" &&
|
|
4001
|
+
item.steer === true &&
|
|
4002
|
+
inspection?.state === "busy");
|
|
4003
|
+
if (selectedBinding === undefined ||
|
|
4004
|
+
!dispatchable ||
|
|
4005
|
+
inspection === undefined ||
|
|
4006
|
+
!inspection.enabled ||
|
|
4007
|
+
inspection.compatibility !== "compatible" ||
|
|
4008
|
+
!storedDispatchable ||
|
|
4009
|
+
bindingKey(inspection.binding) !== bindingKey(binding) ||
|
|
4010
|
+
bindingKey(selectedBinding) !== bindingKey(binding)) {
|
|
4011
|
+
const result = await this.store.requeueInFlightMessage(item.messageId, item.body);
|
|
4012
|
+
if (result.status === "settled") {
|
|
4013
|
+
await this.applyTerminalSettlementLocked(result.settlement);
|
|
4014
|
+
}
|
|
4015
|
+
if (binding.provider === "codex" &&
|
|
4016
|
+
(routeState === "busy" ||
|
|
4017
|
+
routeState === "awaiting_approval" ||
|
|
4018
|
+
inspection?.state === "busy" ||
|
|
4019
|
+
inspection?.state === "awaiting_approval")) {
|
|
4020
|
+
this.scheduleHeldRedispatch(targetAlias);
|
|
4021
|
+
}
|
|
4022
|
+
await this.changed();
|
|
4023
|
+
return;
|
|
4024
|
+
}
|
|
4025
|
+
if (!steeringWhileBusy)
|
|
4026
|
+
this.routeStates.set(targetAlias, "busy");
|
|
4027
|
+
}
|
|
4028
|
+
else if (binding.provider !== "claude" ||
|
|
4029
|
+
bindingKey(binding) !== context.targetBindingKey) {
|
|
4030
|
+
await this.finishDelivery({
|
|
4031
|
+
messageId: item.messageId,
|
|
4032
|
+
state: "failed",
|
|
4033
|
+
safeErrorCode: "NATIVE_REPLY_BINDING_MISMATCH",
|
|
4034
|
+
});
|
|
4035
|
+
return;
|
|
4036
|
+
}
|
|
4037
|
+
if (!steeringWhileBusy) {
|
|
4038
|
+
this.activeDispatchByTarget.set(targetAlias, item.messageId);
|
|
4039
|
+
}
|
|
4040
|
+
const tracker = this.deliveryTrackers.get(item.messageId);
|
|
4041
|
+
if (tracker === undefined) {
|
|
4042
|
+
const settled = await this.store.settleMessage({
|
|
4043
|
+
messageId: item.messageId,
|
|
4044
|
+
state: "failed",
|
|
4045
|
+
safeErrorCode: "DELIVERY_TRACKER_MISSING",
|
|
4046
|
+
});
|
|
4047
|
+
this.takeMessageContextLocked(item.messageId, "failed");
|
|
4048
|
+
if (this.activeDispatchByTarget.get(targetAlias) === item.messageId) {
|
|
4049
|
+
this.activeDispatchByTarget.delete(targetAlias);
|
|
4050
|
+
}
|
|
4051
|
+
for (const [token, messageId] of this.deliveryTokens) {
|
|
4052
|
+
if (messageId === item.messageId)
|
|
4053
|
+
this.deliveryTokens.delete(token);
|
|
4054
|
+
}
|
|
4055
|
+
this.addRuntimeAlert("DELIVERY_TRACKER_MISSING", "error", {
|
|
4056
|
+
alias: targetAlias,
|
|
4057
|
+
});
|
|
4058
|
+
if (settled.status !== "settled") {
|
|
4059
|
+
this.addRuntimeAlert("DELIVERY_LEDGER_RECOVERY_FAILED", "error", {
|
|
4060
|
+
alias: targetAlias,
|
|
4061
|
+
});
|
|
4062
|
+
}
|
|
4063
|
+
await this.changed();
|
|
4064
|
+
return;
|
|
4065
|
+
}
|
|
4066
|
+
const dispatchEvent = tracker.machine.phase !== "terminal" &&
|
|
4067
|
+
tracker.machine.dispatchRetryAt !== null
|
|
4068
|
+
? {
|
|
4069
|
+
type: "dispatch_retry_due",
|
|
4070
|
+
at: this.now().getTime(),
|
|
4071
|
+
}
|
|
4072
|
+
: { type: "dispatch_requested", at: this.now().getTime() };
|
|
4073
|
+
await this.advanceDeliveryLocked(item.messageId, dispatchEvent);
|
|
4074
|
+
if (isTerminalDeliveryMachine(tracker.machine))
|
|
4075
|
+
return;
|
|
4076
|
+
if (tracker.machine.phase !== "dispatching") {
|
|
4077
|
+
const requeued = await this.store.requeueInFlightMessage(item.messageId, item.body);
|
|
4078
|
+
if (this.activeDispatchByTarget.get(targetAlias) === item.messageId) {
|
|
4079
|
+
this.activeDispatchByTarget.delete(targetAlias);
|
|
4080
|
+
}
|
|
4081
|
+
if (requeued.status === "settled") {
|
|
4082
|
+
await this.applyTerminalSettlementLocked(requeued.settlement);
|
|
4083
|
+
}
|
|
4084
|
+
this.scheduleLifecycleWakeLocked();
|
|
4085
|
+
return;
|
|
4086
|
+
}
|
|
4087
|
+
let result;
|
|
4088
|
+
try {
|
|
4089
|
+
result = await this.awaitDispatchWithNativeHeldLocked(tracker, this.adapter(binding.provider, binding.hostId).dispatch({
|
|
4090
|
+
sourceAlias: item.sourceAlias,
|
|
4091
|
+
binding,
|
|
4092
|
+
authorization: context.authorization,
|
|
4093
|
+
messageId: item.messageId,
|
|
4094
|
+
text: item.body,
|
|
4095
|
+
expectsReply: context?.expectsReply ?? false,
|
|
4096
|
+
deadlineAt: item.deadlineAt,
|
|
4097
|
+
...(item.steer === true ? { steer: true } : {}),
|
|
4098
|
+
}));
|
|
4099
|
+
}
|
|
4100
|
+
catch {
|
|
4101
|
+
await this.drainDeliveryCallbacksForMessageLocked(item.messageId);
|
|
4102
|
+
if (await this.processLifecycleLocked()) {
|
|
4103
|
+
await this.changed();
|
|
4104
|
+
}
|
|
4105
|
+
if (!this.messageContexts.has(item.messageId)) {
|
|
4106
|
+
this.scheduleDispatch(this.bindingAliases.get(bindingKey(binding)) ?? targetAlias);
|
|
4107
|
+
return;
|
|
4108
|
+
}
|
|
4109
|
+
await this.advanceDeliveryLocked(item.messageId, {
|
|
4110
|
+
type: "dispatch_ambiguous",
|
|
4111
|
+
at: this.now().getTime(),
|
|
4112
|
+
safeErrorCode: "DISPATCH_OUTCOME_AMBIGUOUS",
|
|
4113
|
+
});
|
|
4114
|
+
await this.changed();
|
|
4115
|
+
this.scheduleDispatch(this.bindingAliases.get(bindingKey(binding)) ?? targetAlias);
|
|
4116
|
+
return;
|
|
4117
|
+
}
|
|
4118
|
+
await this.drainDeliveryCallbacksForMessageLocked(item.messageId);
|
|
4119
|
+
if (await this.processLifecycleLocked()) {
|
|
4120
|
+
await this.changed();
|
|
4121
|
+
}
|
|
4122
|
+
if (!this.messageContexts.has(item.messageId)) {
|
|
4123
|
+
this.scheduleDispatch(this.bindingAliases.get(bindingKey(binding)) ?? targetAlias);
|
|
4124
|
+
return;
|
|
4125
|
+
}
|
|
4126
|
+
if (result.state === "deferred") {
|
|
4127
|
+
const currentTargetAlias = this.bindingAliases.get(bindingKey(binding)) ?? targetAlias;
|
|
4128
|
+
const now = this.now().getTime();
|
|
4129
|
+
await this.advanceDeliveryLocked(item.messageId, {
|
|
4130
|
+
type: "dispatch_clean_prewrite_failed",
|
|
4131
|
+
at: now,
|
|
4132
|
+
retryAt: now + 500,
|
|
4133
|
+
safeErrorCode: safeCode(result.safeErrorCode, "PROVIDER_DISPATCH_DEFERRED"),
|
|
4134
|
+
});
|
|
4135
|
+
const postDeferred = this.deliveryTrackers.get(item.messageId)?.machine;
|
|
4136
|
+
if (postDeferred === undefined ||
|
|
4137
|
+
isTerminalDeliveryMachine(postDeferred)) {
|
|
4138
|
+
await this.changed();
|
|
4139
|
+
return;
|
|
4140
|
+
}
|
|
4141
|
+
// A provider's clean-prewrite return cannot authorize replay after a
|
|
4142
|
+
// callback from the same dispatch has already established write
|
|
4143
|
+
// evidence. Only the reducer's explicit queued retry phase may return
|
|
4144
|
+
// the body to the store queue.
|
|
4145
|
+
if (postDeferred.phase !== "queued") {
|
|
4146
|
+
await this.advanceDeliveryLocked(item.messageId, {
|
|
4147
|
+
type: "await_terminal",
|
|
4148
|
+
at: this.now().getTime(),
|
|
4149
|
+
});
|
|
4150
|
+
await this.changed();
|
|
4151
|
+
return;
|
|
4152
|
+
}
|
|
4153
|
+
const requeued = await this.store.requeueInFlightMessage(item.messageId, item.body);
|
|
4154
|
+
if (this.activeDispatchByTarget.get(currentTargetAlias) === item.messageId) {
|
|
4155
|
+
this.activeDispatchByTarget.delete(currentTargetAlias);
|
|
4156
|
+
}
|
|
4157
|
+
if (requeued.status === "requeued") {
|
|
4158
|
+
if (context.authorization === "selected_route") {
|
|
4159
|
+
if (!steeringWhileBusy) {
|
|
4160
|
+
this.routeStates.set(currentTargetAlias, "idle");
|
|
4161
|
+
await this.store.observeRoute({
|
|
4162
|
+
binding,
|
|
4163
|
+
state: "idle",
|
|
4164
|
+
compatibility: "compatible",
|
|
4165
|
+
});
|
|
4166
|
+
}
|
|
4167
|
+
else {
|
|
4168
|
+
this.scheduleHeldRedispatch(currentTargetAlias);
|
|
4169
|
+
}
|
|
4170
|
+
await this.setNativeCodexStatus(currentTargetAlias, "waiting");
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
else if (requeued.status === "settled") {
|
|
4174
|
+
await this.applyTerminalSettlementLocked(requeued.settlement);
|
|
4175
|
+
}
|
|
4176
|
+
await this.changed();
|
|
4177
|
+
return;
|
|
4178
|
+
}
|
|
4179
|
+
if (result.state === "pending" || result.state === "accepted") {
|
|
4180
|
+
if (result.state === "accepted" &&
|
|
4181
|
+
(await this.acceptProviderDeliveryLocked(item.messageId))) {
|
|
4182
|
+
await this.changed();
|
|
4183
|
+
}
|
|
4184
|
+
else if (result.state === "pending") {
|
|
4185
|
+
await this.advanceDeliveryLocked(item.messageId, {
|
|
4186
|
+
type: "await_terminal",
|
|
4187
|
+
at: this.now().getTime(),
|
|
4188
|
+
});
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
else {
|
|
4192
|
+
await this.finishDelivery({ messageId: item.messageId, state: result.state, ...(result.safeErrorCode === undefined ? {} : { safeErrorCode: result.safeErrorCode }), ...(result.replyText === undefined ? {} : { replyText: result.replyText }) });
|
|
4193
|
+
}
|
|
4194
|
+
this.scheduleDispatch(this.bindingAliases.get(bindingKey(binding)) ?? targetAlias);
|
|
4195
|
+
}
|
|
4196
|
+
scheduleDispatch(targetAlias) {
|
|
4197
|
+
if (this.closing ||
|
|
4198
|
+
this.codexSuccessionDispatchFrozen.has(targetAlias) ||
|
|
4199
|
+
this.codexSuccessionPoisoned.has(targetAlias)) {
|
|
4200
|
+
return;
|
|
4201
|
+
}
|
|
4202
|
+
if (this.scheduledDispatchTargets.has(targetAlias))
|
|
4203
|
+
return;
|
|
4204
|
+
this.scheduledDispatchTargets.add(targetAlias);
|
|
4205
|
+
setImmediate(() => {
|
|
4206
|
+
this.mutex
|
|
4207
|
+
.run("service", async () => {
|
|
4208
|
+
this.scheduledDispatchTargets.delete(targetAlias);
|
|
4209
|
+
if (this.closing ||
|
|
4210
|
+
this.codexSuccessionDispatchFrozen.has(targetAlias) ||
|
|
4211
|
+
this.codexSuccessionPoisoned.has(targetAlias)) {
|
|
4212
|
+
return;
|
|
4213
|
+
}
|
|
4214
|
+
this.dispatchRunnerTargets.add(targetAlias);
|
|
4215
|
+
try {
|
|
4216
|
+
await this.dispatchOne(targetAlias);
|
|
4217
|
+
}
|
|
4218
|
+
finally {
|
|
4219
|
+
this.dispatchRunnerTargets.delete(targetAlias);
|
|
4220
|
+
this.scheduleLifecycleWakeLocked();
|
|
4221
|
+
}
|
|
4222
|
+
})
|
|
4223
|
+
.catch(() => {
|
|
4224
|
+
this.dashboardHealthy = false;
|
|
4225
|
+
});
|
|
4226
|
+
});
|
|
4227
|
+
}
|
|
4228
|
+
scheduleHeldRedispatch(targetAlias) {
|
|
4229
|
+
if (this.closing || this.heldRedispatchTimers.has(targetAlias))
|
|
4230
|
+
return;
|
|
4231
|
+
const timer = this.timers.setTimeout(() => {
|
|
4232
|
+
if (this.heldRedispatchTimers.get(targetAlias) !== timer)
|
|
4233
|
+
return;
|
|
4234
|
+
this.heldRedispatchTimers.delete(targetAlias);
|
|
4235
|
+
this.scheduleDispatch(targetAlias);
|
|
4236
|
+
}, 500);
|
|
4237
|
+
this.heldRedispatchTimers.set(targetAlias, timer);
|
|
4238
|
+
timer.unref?.();
|
|
4239
|
+
}
|
|
4240
|
+
async onDelivery(source, event, receivedAt) {
|
|
4241
|
+
const context = this.deliveryCorrelation(event.messageId);
|
|
4242
|
+
if (context === undefined)
|
|
4243
|
+
return;
|
|
4244
|
+
const target = this.contextTargetBinding(context);
|
|
4245
|
+
if (target === undefined ||
|
|
4246
|
+
target.provider !== source.provider ||
|
|
4247
|
+
target.hostId !== source.hostId ||
|
|
4248
|
+
target.endpointGeneration !== source.endpointGeneration) {
|
|
4249
|
+
return;
|
|
4250
|
+
}
|
|
4251
|
+
if (this.providerTurnContinuations.has(event.messageId) &&
|
|
4252
|
+
!this.messageContexts.has(event.messageId)) {
|
|
4253
|
+
if (event.state === "transport_uncertain" ||
|
|
4254
|
+
event.state === "transport_written" ||
|
|
4255
|
+
event.state === "held") {
|
|
4256
|
+
return;
|
|
4257
|
+
}
|
|
4258
|
+
const state = event.state === "released" || event.state === "completed"
|
|
4259
|
+
? "delivered"
|
|
4260
|
+
: event.state === "expired"
|
|
4261
|
+
? "expired"
|
|
4262
|
+
: event.state === "cancelled"
|
|
4263
|
+
? "cancelled"
|
|
4264
|
+
: event.state === "ambiguous"
|
|
4265
|
+
? "ambiguous"
|
|
4266
|
+
: "failed";
|
|
4267
|
+
await this.finishProviderTurnContinuationLocked(event.messageId, state, event.replyText);
|
|
4268
|
+
await this.changed();
|
|
4269
|
+
return;
|
|
4270
|
+
}
|
|
4271
|
+
const deliveryEvent = event.state === "transport_uncertain"
|
|
4272
|
+
? {
|
|
4273
|
+
type: "dispatch_ambiguous",
|
|
4274
|
+
at: receivedAt,
|
|
4275
|
+
safeErrorCode: safeCode(event.safeErrorCode, "DISPATCH_OUTCOME_AMBIGUOUS"),
|
|
4276
|
+
}
|
|
4277
|
+
: event.state === "transport_written"
|
|
4278
|
+
? { type: "transport_written", observedAt: receivedAt }
|
|
4279
|
+
: event.state === "held"
|
|
4280
|
+
? { type: "provider_held", observedAt: receivedAt }
|
|
4281
|
+
: event.state === "released" || event.state === "completed"
|
|
4282
|
+
? { type: "provider_released", observedAt: receivedAt }
|
|
4283
|
+
: event.state === "unconfirmed"
|
|
4284
|
+
? {
|
|
4285
|
+
type: "provider_unconfirmed",
|
|
4286
|
+
observedAt: receivedAt,
|
|
4287
|
+
safeErrorCode: safeCode(event.safeErrorCode, "DELIVERY_UNCONFIRMED"),
|
|
4288
|
+
}
|
|
4289
|
+
: event.state === "expired"
|
|
4290
|
+
? {
|
|
4291
|
+
type: "provider_expired",
|
|
4292
|
+
observedAt: receivedAt,
|
|
4293
|
+
safeErrorCode: safeCode(event.safeErrorCode, "MESSAGE_EXPIRED"),
|
|
4294
|
+
}
|
|
4295
|
+
: event.state === "denied" || event.state === "failed"
|
|
4296
|
+
? {
|
|
4297
|
+
type: "provider_failed",
|
|
4298
|
+
observedAt: receivedAt,
|
|
4299
|
+
safeErrorCode: safeCode(event.safeErrorCode, "PROVIDER_DELIVERY_FAILED"),
|
|
4300
|
+
}
|
|
4301
|
+
: event.state === "ambiguous"
|
|
4302
|
+
? {
|
|
4303
|
+
type: "dispatch_ambiguous",
|
|
4304
|
+
at: receivedAt,
|
|
4305
|
+
safeErrorCode: safeCode(event.safeErrorCode, "DISPATCH_OUTCOME_AMBIGUOUS"),
|
|
4306
|
+
}
|
|
4307
|
+
: {
|
|
4308
|
+
type: "cancel",
|
|
4309
|
+
at: receivedAt,
|
|
4310
|
+
safeErrorCode: safeCode(event.safeErrorCode, "PROVIDER_DELIVERY_CANCELLED"),
|
|
4311
|
+
};
|
|
4312
|
+
const terminalContext = await this.advanceDeliveryLocked(event.messageId, deliveryEvent, undefined, event.replyText);
|
|
4313
|
+
await this.finishDeliveredReplyLocked(terminalContext, event.replyText);
|
|
4314
|
+
await this.changed();
|
|
4315
|
+
}
|
|
4316
|
+
async finishDelivery(event) {
|
|
4317
|
+
if (this.providerTurnContinuations.has(event.messageId) &&
|
|
4318
|
+
!this.messageContexts.has(event.messageId)) {
|
|
4319
|
+
await this.finishProviderTurnContinuationLocked(event.messageId, event.state, event.replyText);
|
|
4320
|
+
await this.changed();
|
|
4321
|
+
return;
|
|
4322
|
+
}
|
|
4323
|
+
const now = this.now().getTime();
|
|
4324
|
+
const deliveryEvent = event.state === "delivered"
|
|
4325
|
+
? { type: "provider_released", observedAt: now }
|
|
4326
|
+
: event.state === "unconfirmed"
|
|
4327
|
+
? {
|
|
4328
|
+
type: "provider_unconfirmed",
|
|
4329
|
+
observedAt: now,
|
|
4330
|
+
safeErrorCode: safeCode(event.safeErrorCode, "DELIVERY_UNCONFIRMED"),
|
|
4331
|
+
}
|
|
4332
|
+
: event.state === "expired"
|
|
4333
|
+
? {
|
|
4334
|
+
type: "provider_expired",
|
|
4335
|
+
observedAt: now,
|
|
4336
|
+
safeErrorCode: safeCode(event.safeErrorCode, "MESSAGE_EXPIRED"),
|
|
4337
|
+
}
|
|
4338
|
+
: event.state === "failed"
|
|
4339
|
+
? {
|
|
4340
|
+
type: "provider_failed",
|
|
4341
|
+
observedAt: now,
|
|
4342
|
+
safeErrorCode: safeCode(event.safeErrorCode, "PROVIDER_DELIVERY_FAILED"),
|
|
4343
|
+
}
|
|
4344
|
+
: event.state === "ambiguous"
|
|
4345
|
+
? {
|
|
4346
|
+
type: "dispatch_ambiguous",
|
|
4347
|
+
at: now,
|
|
4348
|
+
safeErrorCode: safeCode(event.safeErrorCode, "DISPATCH_OUTCOME_AMBIGUOUS"),
|
|
4349
|
+
}
|
|
4350
|
+
: {
|
|
4351
|
+
type: "cancel",
|
|
4352
|
+
at: now,
|
|
4353
|
+
safeErrorCode: safeCode(event.safeErrorCode, "PROVIDER_DELIVERY_CANCELLED"),
|
|
4354
|
+
};
|
|
4355
|
+
const context = await this.advanceDeliveryLocked(event.messageId, deliveryEvent, undefined, event.replyText);
|
|
4356
|
+
await this.finishDeliveredReplyLocked(context, event.replyText);
|
|
4357
|
+
await this.changed();
|
|
4358
|
+
}
|
|
4359
|
+
async finishDeliveredReplyLocked(context, replyText) {
|
|
4360
|
+
if (context === undefined)
|
|
4361
|
+
return;
|
|
4362
|
+
const conversation = this.conversations.get(context.conversationId);
|
|
4363
|
+
if (!this.closing &&
|
|
4364
|
+
replyText !== undefined &&
|
|
4365
|
+
replyText.length > 0 &&
|
|
4366
|
+
context.expectsReply &&
|
|
4367
|
+
conversation !== undefined) {
|
|
4368
|
+
try {
|
|
4369
|
+
if (this.nativeIngressByConversation.has(conversation.id)) {
|
|
4370
|
+
await this.enqueueNativeReply(conversation, conversation.targetAlias, replyText, context.hopCount + 1, false);
|
|
4371
|
+
}
|
|
4372
|
+
else {
|
|
4373
|
+
const sourceBinding = this.routeBindings.get(conversation.targetAlias);
|
|
4374
|
+
const sourceStillRoutable = await this.store
|
|
4375
|
+
.resolveRoute(conversation.targetAlias)
|
|
4376
|
+
.then(() => true)
|
|
4377
|
+
.catch(() => false);
|
|
4378
|
+
const pairStillActive = conversation.pair !== true ||
|
|
4379
|
+
(await this.store.hasPair({
|
|
4380
|
+
claudeAlias: conversation.targetAlias,
|
|
4381
|
+
codexAlias: conversation.sourceAlias,
|
|
4382
|
+
}));
|
|
4383
|
+
if ((!sourceStillRoutable || !pairStillActive) &&
|
|
4384
|
+
sourceBinding?.provider === "claude") {
|
|
4385
|
+
await this.enqueueObservedClaudeReplyAfterRouteTeardownLocked(conversation, sourceBinding, replyText, context.hopCount + 1);
|
|
4386
|
+
}
|
|
4387
|
+
else {
|
|
4388
|
+
await this.enqueue(conversation.targetAlias, conversation.sourceAlias, replyText, false, true, {
|
|
4389
|
+
existingConversationId: conversation.id,
|
|
4390
|
+
requestedHopCount: context.hopCount + 1,
|
|
4391
|
+
exposeDeliveryToken: false,
|
|
4392
|
+
});
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
catch {
|
|
4397
|
+
this.dashboardHealthy = false;
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
async finishProviderTurnContinuationLocked(messageId, state, replyText) {
|
|
4402
|
+
const continuation = this.providerTurnContinuations.get(messageId);
|
|
4403
|
+
if (continuation === undefined)
|
|
4404
|
+
return false;
|
|
4405
|
+
this.providerTurnContinuations.delete(messageId);
|
|
4406
|
+
if (this.activeDispatchByTarget.get(continuation.targetAlias) === messageId) {
|
|
4407
|
+
this.activeDispatchByTarget.delete(continuation.targetAlias);
|
|
4408
|
+
if (!this.closing)
|
|
4409
|
+
this.scheduleDispatch(continuation.targetAlias);
|
|
4410
|
+
}
|
|
4411
|
+
const conversation = this.conversations.get(continuation.conversationId);
|
|
4412
|
+
if (state !== "delivered") {
|
|
4413
|
+
this.releasePendingClaude(conversation);
|
|
4414
|
+
this.nativeIngressByConversation.delete(continuation.conversationId);
|
|
4415
|
+
this.addRuntimeAlert("PROVIDER_TURN_FAILED_AFTER_ACCEPTANCE", "warning", {
|
|
4416
|
+
alias: continuation.targetAlias,
|
|
4417
|
+
});
|
|
4418
|
+
return true;
|
|
4419
|
+
}
|
|
4420
|
+
await this.finishDeliveredReplyLocked(continuation, replyText);
|
|
4421
|
+
if (replyText === undefined || replyText.length === 0) {
|
|
4422
|
+
this.nativeIngressByConversation.delete(continuation.conversationId);
|
|
4423
|
+
}
|
|
4424
|
+
return true;
|
|
4425
|
+
}
|
|
4426
|
+
async onClaudeReply(event) {
|
|
4427
|
+
if (this.closing)
|
|
4428
|
+
return;
|
|
4429
|
+
const selected = [...this.routeBindings.values()].filter((binding) => binding.provider === "claude" &&
|
|
4430
|
+
binding.hostId === event.endpoint.hostId &&
|
|
4431
|
+
binding.endpointGeneration === event.endpoint.endpointGeneration &&
|
|
4432
|
+
binding.routeHandle === event.endpoint.routeHandle);
|
|
4433
|
+
if (selected.length !== 1)
|
|
4434
|
+
return;
|
|
4435
|
+
const binding = selected[0];
|
|
4436
|
+
if (binding === undefined)
|
|
4437
|
+
return;
|
|
4438
|
+
const key = bindingKey(binding);
|
|
4439
|
+
const pending = this.pendingClaudeReplies.get(key);
|
|
4440
|
+
if (pending === undefined || pending.bindingKey !== key)
|
|
4441
|
+
return;
|
|
4442
|
+
if (pending.tainted ||
|
|
4443
|
+
Date.parse(pending.deadlineAt) <= this.now().getTime()) {
|
|
4444
|
+
this.pendingClaudeReplies.delete(key);
|
|
4445
|
+
return;
|
|
4446
|
+
}
|
|
4447
|
+
const conversation = this.conversations.get(pending.conversationId);
|
|
4448
|
+
if (conversation === undefined)
|
|
4449
|
+
return;
|
|
4450
|
+
this.pendingClaudeReplies.delete(key);
|
|
4451
|
+
await this.enqueue(conversation.targetAlias, conversation.sourceAlias, event.text, false, true, {
|
|
4452
|
+
existingConversationId: conversation.id,
|
|
4453
|
+
requestedHopCount: pending.hopCount + 1,
|
|
4454
|
+
exposeDeliveryToken: false,
|
|
4455
|
+
});
|
|
4456
|
+
}
|
|
4457
|
+
async onClaudeMessage(event) {
|
|
4458
|
+
if (this.closing) {
|
|
4459
|
+
if (event.receiptHandle !== undefined) {
|
|
4460
|
+
await this.settleDetachedNativeReceipt(event.endpoint.hostId, event.receiptHandle, "GATEWAY_SHUTDOWN");
|
|
4461
|
+
}
|
|
4462
|
+
return;
|
|
4463
|
+
}
|
|
4464
|
+
let acceptedMessageId;
|
|
4465
|
+
try {
|
|
4466
|
+
this.pruneTransient();
|
|
4467
|
+
if (this.codexSuccessionPoisoned.has(event.targetAlias)) {
|
|
4468
|
+
throw new BridgeError("CODEX_SUCCESSION_RECOVERY_REQUIRED", "Native ingress is unavailable while Codex succession requires manual recovery.");
|
|
4469
|
+
}
|
|
4470
|
+
if (this.conversations.size >= MAX_CONVERSATIONS) {
|
|
4471
|
+
throw new BridgeError("CONVERSATION_CAPACITY", "The transient conversation table is full.", true);
|
|
4472
|
+
}
|
|
4473
|
+
const target = this.routeBindings.get(event.targetAlias);
|
|
4474
|
+
const sourceBinding = {
|
|
4475
|
+
...event.endpoint,
|
|
4476
|
+
ownerLease: stableLease("claude", event.endpoint.routeHandle),
|
|
4477
|
+
};
|
|
4478
|
+
const collision = this.routeBindings.get(event.sourceAlias);
|
|
4479
|
+
if (target === undefined ||
|
|
4480
|
+
target.provider !== "codex" ||
|
|
4481
|
+
target.hostId !== event.endpoint.hostId ||
|
|
4482
|
+
(collision !== undefined &&
|
|
4483
|
+
bindingKey(collision) !== bindingKey(sourceBinding))) {
|
|
4484
|
+
throw new BridgeError("NATIVE_INGRESS_ROUTE_MISMATCH", "The native sender or registered Codex target no longer matches this exact host generation.");
|
|
4485
|
+
}
|
|
4486
|
+
const conversationId = createGatewayConversationId();
|
|
4487
|
+
const enqueuedAt = this.now().getTime();
|
|
4488
|
+
const deadlineAt = new Date(enqueuedAt + this.config.limits.messageDeadlineMs).toISOString();
|
|
4489
|
+
const steer = this.config.steeringEnabled && event.text.startsWith("STEER:");
|
|
4490
|
+
const queued = await this.store.enqueueNativeIngress({
|
|
4491
|
+
source: { alias: event.sourceAlias, binding: sourceBinding },
|
|
4492
|
+
targetAlias: event.targetAlias,
|
|
4493
|
+
body: event.text,
|
|
4494
|
+
dedupeKey: `${conversationId}:0`,
|
|
4495
|
+
conversationIdSuffix: conversationId.slice(-8),
|
|
4496
|
+
hopCount: 0,
|
|
4497
|
+
deadlineAt,
|
|
4498
|
+
...(steer ? { steer: true } : {}),
|
|
4499
|
+
progressWatch: progressWatchActivity(event.text, conversationId, event.sourceAlias),
|
|
4500
|
+
});
|
|
4501
|
+
if (!queued.accepted || queued.messageId === undefined) {
|
|
4502
|
+
throw new BridgeError("MESSAGE_REJECTED", "The native message was not accepted.");
|
|
4503
|
+
}
|
|
4504
|
+
acceptedMessageId = queued.messageId;
|
|
4505
|
+
if (queued.supersededSettlement !== undefined) {
|
|
4506
|
+
await this.applyTerminalSettlementLocked(queued.supersededSettlement);
|
|
4507
|
+
}
|
|
4508
|
+
const conversation = {
|
|
4509
|
+
id: conversationId,
|
|
4510
|
+
sourceAlias: event.sourceAlias,
|
|
4511
|
+
targetAlias: event.targetAlias,
|
|
4512
|
+
expectsReply: !steer,
|
|
4513
|
+
nextSequence: 1,
|
|
4514
|
+
lastHopCount: 0,
|
|
4515
|
+
lastActivityAt: this.now().toISOString(),
|
|
4516
|
+
...(queued.pair === true ? { pair: true } : {}),
|
|
4517
|
+
};
|
|
4518
|
+
this.conversations.set(conversationId, conversation);
|
|
4519
|
+
this.messageContexts.set(queued.messageId, {
|
|
4520
|
+
conversationId,
|
|
4521
|
+
isReply: false,
|
|
4522
|
+
expectsReply: !steer,
|
|
4523
|
+
hopCount: 0,
|
|
4524
|
+
sequence: 0,
|
|
4525
|
+
targetBindingKey: bindingKey(target),
|
|
4526
|
+
authorization: "selected_route",
|
|
4527
|
+
targetAlias: event.targetAlias,
|
|
4528
|
+
deadlineAt,
|
|
4529
|
+
});
|
|
4530
|
+
this.armDeliveryTracker({
|
|
4531
|
+
messageId: queued.messageId,
|
|
4532
|
+
conversationId,
|
|
4533
|
+
targetAlias: event.targetAlias,
|
|
4534
|
+
enqueuedAt,
|
|
4535
|
+
deadlineAt,
|
|
4536
|
+
...(event.receiptHandle === undefined
|
|
4537
|
+
? {}
|
|
4538
|
+
: {
|
|
4539
|
+
nativeReceipt: {
|
|
4540
|
+
hostId: event.endpoint.hostId,
|
|
4541
|
+
receiptHandle: event.receiptHandle,
|
|
4542
|
+
},
|
|
4543
|
+
}),
|
|
4544
|
+
});
|
|
4545
|
+
this.nativeIngressByConversation.set(conversationId, {
|
|
4546
|
+
sourceAlias: event.sourceAlias,
|
|
4547
|
+
binding: sourceBinding,
|
|
4548
|
+
deadlineAt,
|
|
4549
|
+
});
|
|
4550
|
+
await this.changed();
|
|
4551
|
+
await this.setNativeCodexStatus(event.targetAlias, "waiting");
|
|
4552
|
+
// Native ingress gets one immediate dispatch opportunity. Only an
|
|
4553
|
+
// actual queued/waiting result earns a `held` acknowledgement; an idle
|
|
4554
|
+
// fast path can therefore settle with one terminal acknowledgement.
|
|
4555
|
+
await this.dispatchOne(event.targetAlias);
|
|
4556
|
+
const tracker = this.deliveryTrackers.get(queued.messageId);
|
|
4557
|
+
if (tracker?.machine.phase === "queued") {
|
|
4558
|
+
await this.publishNativeHeldLocked(tracker);
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
catch (error) {
|
|
4562
|
+
const code = safeCode(error instanceof BridgeError ? error.code : undefined, "NATIVE_INGRESS_REJECTED");
|
|
4563
|
+
if (acceptedMessageId !== undefined) {
|
|
4564
|
+
const cancelled = await this.store.settleQueuedMessage({
|
|
4565
|
+
messageId: acceptedMessageId,
|
|
4566
|
+
state: "expired",
|
|
4567
|
+
safeErrorCode: code,
|
|
4568
|
+
});
|
|
4569
|
+
if (cancelled.status === "settled") {
|
|
4570
|
+
await this.applyTerminalSettlementLocked(cancelled.settlement);
|
|
4571
|
+
await this.changed();
|
|
4572
|
+
}
|
|
4573
|
+
return;
|
|
4574
|
+
}
|
|
4575
|
+
if (event.receiptHandle !== undefined) {
|
|
4576
|
+
this.rejectDetachedNativeReceipt(event.endpoint.hostId, event.receiptHandle, code);
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
releasePendingClaude(conversation) {
|
|
4581
|
+
if (conversation === undefined)
|
|
4582
|
+
return;
|
|
4583
|
+
for (const [key, owner] of this.pendingClaudeReplies) {
|
|
4584
|
+
if (owner.conversationId === conversation.id) {
|
|
4585
|
+
this.pendingClaudeReplies.delete(key);
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
async onRouteState(source, event) {
|
|
4590
|
+
if (this.closing)
|
|
4591
|
+
return;
|
|
4592
|
+
const entry = [...this.routeBindings.entries()].find(([, binding]) => binding.provider === source.provider &&
|
|
4593
|
+
binding.hostId === source.hostId &&
|
|
4594
|
+
binding.endpointGeneration === source.endpointGeneration &&
|
|
4595
|
+
binding.routeHandle === event.routeHandle);
|
|
4596
|
+
if (entry === undefined)
|
|
4597
|
+
return;
|
|
4598
|
+
const [alias, binding] = entry;
|
|
4599
|
+
const previousState = this.routeStates.get(alias);
|
|
4600
|
+
this.routeStates.set(alias, event.state);
|
|
4601
|
+
const staleSafeCode = binding.provider === "codex"
|
|
4602
|
+
? "CODEX_ROUTE_STALE"
|
|
4603
|
+
: safeCode(event.safeErrorCode, "ROUTE_STALE");
|
|
4604
|
+
const adapter = this.adapter(source.provider, source.hostId);
|
|
4605
|
+
await this.store.observeConnector({
|
|
4606
|
+
identity: source,
|
|
4607
|
+
health: event.state === "stale" ? "degraded" : "healthy",
|
|
4608
|
+
compatibility: event.state === "stale" ? "expired" : "compatible",
|
|
4609
|
+
protocol: adapter.protocol,
|
|
4610
|
+
protocolVersion: adapter.protocolVersion,
|
|
4611
|
+
...(event.safeErrorCode === undefined
|
|
4612
|
+
? {}
|
|
4613
|
+
: {
|
|
4614
|
+
safeErrorCode: safeCode(event.safeErrorCode, "CONNECTOR_DEGRADED"),
|
|
4615
|
+
}),
|
|
4616
|
+
});
|
|
4617
|
+
if (event.state === "stale") {
|
|
4618
|
+
if (previousState !== "stale") {
|
|
4619
|
+
this.addRuntimeAlert(staleSafeCode, "error", { provider: binding.provider, host: binding.hostId, alias });
|
|
4620
|
+
}
|
|
4621
|
+
await this.store.observeRoute({
|
|
4622
|
+
binding,
|
|
4623
|
+
state: "stale",
|
|
4624
|
+
compatibility: "expired",
|
|
4625
|
+
safeErrorCode: staleSafeCode,
|
|
4626
|
+
});
|
|
4627
|
+
}
|
|
4628
|
+
else {
|
|
4629
|
+
await this.store.observeRoute({
|
|
4630
|
+
binding,
|
|
4631
|
+
state: event.state,
|
|
4632
|
+
compatibility: "compatible",
|
|
4633
|
+
...(event.safeErrorCode === undefined
|
|
4634
|
+
? {}
|
|
4635
|
+
: {
|
|
4636
|
+
safeErrorCode: safeCode(event.safeErrorCode, "ROUTE_DEGRADED"),
|
|
4637
|
+
}),
|
|
4638
|
+
});
|
|
4639
|
+
}
|
|
4640
|
+
await this.store.touchProgressWatchesForAlias(alias);
|
|
4641
|
+
await this.changed();
|
|
4642
|
+
if (binding.provider === "codex") {
|
|
4643
|
+
await this.setNativeCodexStatus(alias, event.state === "idle"
|
|
4644
|
+
? "idle"
|
|
4645
|
+
: event.state === "awaiting_approval"
|
|
4646
|
+
? "waiting"
|
|
4647
|
+
: "busy");
|
|
4648
|
+
}
|
|
4649
|
+
if (event.state === "idle" || event.state === "stale") {
|
|
4650
|
+
const heldTimer = this.heldRedispatchTimers.get(alias);
|
|
4651
|
+
if (heldTimer !== undefined) {
|
|
4652
|
+
this.timers.clearTimeout(heldTimer);
|
|
4653
|
+
this.heldRedispatchTimers.delete(alias);
|
|
4654
|
+
}
|
|
4655
|
+
this.scheduleDispatch(alias);
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4658
|
+
async setNativeCodexStatus(alias, status) {
|
|
4659
|
+
const binding = this.routeBindings.get(alias);
|
|
4660
|
+
if (binding?.provider !== "codex")
|
|
4661
|
+
return;
|
|
4662
|
+
await this.adapter("claude", binding.hostId)
|
|
4663
|
+
.updateNativeCodexPeerStatus?.(alias, status)
|
|
4664
|
+
.catch(() => {
|
|
4665
|
+
this.dashboardHealthy = false;
|
|
4666
|
+
});
|
|
4667
|
+
}
|
|
4668
|
+
rejectDetachedNativeReceipt(hostId, receiptHandle, diagnosticCode) {
|
|
4669
|
+
const work = this.settleDetachedNativeReceipt(hostId, receiptHandle, diagnosticCode).finally(() => {
|
|
4670
|
+
this.detachedReceiptWrites.delete(work);
|
|
4671
|
+
});
|
|
4672
|
+
this.detachedReceiptWrites.add(work);
|
|
4673
|
+
}
|
|
4674
|
+
async drainDetachedReceiptWritesLocked() {
|
|
4675
|
+
while (this.detachedReceiptWrites.size > 0) {
|
|
4676
|
+
await Promise.allSettled([...this.detachedReceiptWrites]);
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
async settleDetachedNativeReceipt(hostId, receiptHandle, diagnosticCode) {
|
|
4680
|
+
try {
|
|
4681
|
+
const adapter = this.adapter("claude", hostId);
|
|
4682
|
+
const update = adapter.updateNativeInboundStatus;
|
|
4683
|
+
if (update === undefined) {
|
|
4684
|
+
throw new BridgeError("NATIVE_RECEIPT_TRANSPORT_UNAVAILABLE", "The Claude adapter cannot settle a native receipt.");
|
|
4685
|
+
}
|
|
4686
|
+
await update.call(adapter, receiptHandle, "expired", safeCode(diagnosticCode, "NATIVE_INGRESS_REJECTED"));
|
|
4687
|
+
}
|
|
4688
|
+
catch {
|
|
4689
|
+
try {
|
|
4690
|
+
await this.adapter("claude", hostId)
|
|
4691
|
+
.releaseNativeInboundReceipt?.(receiptHandle);
|
|
4692
|
+
}
|
|
4693
|
+
catch {
|
|
4694
|
+
// The capability remains owned by the listener only until its bounded
|
|
4695
|
+
// process lifetime. Never retry an outcome-ambiguous status write.
|
|
4696
|
+
}
|
|
4697
|
+
this.addRuntimeAlert("NATIVE_RECEIPT_UNCONFIRMED", "warning", {
|
|
4698
|
+
provider: "claude",
|
|
4699
|
+
host: hostId,
|
|
4700
|
+
});
|
|
4701
|
+
this.dashboardHealthy = false;
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
4704
|
+
async exclusiveDecision(operation, activity) {
|
|
4705
|
+
return await this.mutex.run("service", async () => {
|
|
4706
|
+
let result;
|
|
4707
|
+
let rejectedSafeCode;
|
|
4708
|
+
try {
|
|
4709
|
+
await operation();
|
|
4710
|
+
result = { accepted: true, code: "ok" };
|
|
4711
|
+
}
|
|
4712
|
+
catch (error) {
|
|
4713
|
+
result = decisionFor(error);
|
|
4714
|
+
rejectedSafeCode =
|
|
4715
|
+
error instanceof BridgeError
|
|
4716
|
+
? safeCode(error.code, "OPERATOR_ACTION_REJECTED")
|
|
4717
|
+
: "OPERATOR_ACTION_REJECTED";
|
|
4718
|
+
}
|
|
4719
|
+
if (activity !== undefined) {
|
|
4720
|
+
this.activitySequence += 1;
|
|
4721
|
+
this.activityEvents.push({
|
|
4722
|
+
sequence: this.activitySequence,
|
|
4723
|
+
timestamp: this.now().toISOString(),
|
|
4724
|
+
kind: activity.kind,
|
|
4725
|
+
action: activity.action,
|
|
4726
|
+
outcome: result.accepted ? "accepted" : "rejected",
|
|
4727
|
+
aliases: activity.aliases
|
|
4728
|
+
.filter((alias) => PUBLIC_ALIAS.test(alias))
|
|
4729
|
+
.slice(0, 2),
|
|
4730
|
+
operatorAction: true,
|
|
4731
|
+
...(rejectedSafeCode === undefined
|
|
4732
|
+
? {}
|
|
4733
|
+
: { safeErrorCode: rejectedSafeCode }),
|
|
4734
|
+
});
|
|
4735
|
+
while (this.activityEvents.length > RUNTIME_ACTIVITY_CAPACITY) {
|
|
4736
|
+
this.activityEvents.shift();
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4739
|
+
return result;
|
|
4740
|
+
});
|
|
4741
|
+
}
|
|
4742
|
+
async changed() {
|
|
4743
|
+
this.revision += 1;
|
|
4744
|
+
await this.refreshNextProgressWatchWakeLocked();
|
|
4745
|
+
this.scheduleLifecycleWakeLocked();
|
|
4746
|
+
await this.publish();
|
|
4747
|
+
}
|
|
4748
|
+
async publicSnapshotLocked() {
|
|
4749
|
+
const base = await this.store.publicSnapshot();
|
|
4750
|
+
const compatibilityChecks = [];
|
|
4751
|
+
for (const adapter of this.adapters) {
|
|
4752
|
+
const observation = adapter.compatibilitySurface?.();
|
|
4753
|
+
if (observation === undefined)
|
|
4754
|
+
continue;
|
|
4755
|
+
const attestation = await this.store.inspectCompatibilityAttestation(observation.surface, observation.version);
|
|
4756
|
+
if (attestation !== undefined)
|
|
4757
|
+
compatibilityChecks.push(attestation);
|
|
4758
|
+
}
|
|
4759
|
+
compatibilityChecks.sort((left, right) => left.surface.localeCompare(right.surface));
|
|
4760
|
+
const peers = this.availablePeers.map((peer) => ({ ...peer }));
|
|
4761
|
+
if (!arePublicAvailablePeerSnapshots(peers)) {
|
|
4762
|
+
throw new BridgeError("INVALID_TRANSIENT_INVENTORY", "The transient peer inventory is unsafe.");
|
|
4763
|
+
}
|
|
4764
|
+
const now = this.now().getTime();
|
|
4765
|
+
const stalledAlerts = base.routes.flatMap((route) => {
|
|
4766
|
+
const oldest = route.oldestQueuedAt === undefined
|
|
4767
|
+
? Number.NaN
|
|
4768
|
+
: Date.parse(route.oldestQueuedAt);
|
|
4769
|
+
if (!Number.isFinite(oldest) ||
|
|
4770
|
+
oldest > now ||
|
|
4771
|
+
now - oldest < this.config.stallNoticeMs) {
|
|
4772
|
+
return [];
|
|
4773
|
+
}
|
|
4774
|
+
return [
|
|
4775
|
+
{
|
|
4776
|
+
code: "QUEUE_STALLED",
|
|
4777
|
+
severity: "warning",
|
|
4778
|
+
timestamp: route.oldestQueuedAt ?? base.generatedAt,
|
|
4779
|
+
provider: route.provider,
|
|
4780
|
+
host: route.host,
|
|
4781
|
+
alias: route.alias,
|
|
4782
|
+
},
|
|
4783
|
+
];
|
|
4784
|
+
});
|
|
4785
|
+
const certificationAlerts = compatibilityChecks.flatMap((attestation) => {
|
|
4786
|
+
const certification = attestation.certification;
|
|
4787
|
+
if (certification?.outcome !== "fail")
|
|
4788
|
+
return [];
|
|
4789
|
+
const adapter = this.adapters.find((candidate) => candidate.identity.provider === attestation.surface);
|
|
4790
|
+
return [
|
|
4791
|
+
{
|
|
4792
|
+
code: "COMPATIBILITY_CERTIFICATION_FAILED",
|
|
4793
|
+
severity: "error",
|
|
4794
|
+
timestamp: certification.certifiedAt,
|
|
4795
|
+
provider: attestation.surface,
|
|
4796
|
+
host: adapter?.identity.hostId ?? "this-mac",
|
|
4797
|
+
},
|
|
4798
|
+
];
|
|
4799
|
+
});
|
|
4800
|
+
return projectGatewayPublicSnapshot({
|
|
4801
|
+
...base,
|
|
4802
|
+
...(compatibilityChecks.length === 0 ? {} : { compatibilityChecks }),
|
|
4803
|
+
availablePeers: peers,
|
|
4804
|
+
activityEvents: this.activityEvents.map((event) => ({
|
|
4805
|
+
...event,
|
|
4806
|
+
aliases: [...event.aliases],
|
|
4807
|
+
})),
|
|
4808
|
+
truncation: {
|
|
4809
|
+
...base.truncation,
|
|
4810
|
+
activityEvents: 0,
|
|
4811
|
+
},
|
|
4812
|
+
alerts: [
|
|
4813
|
+
...base.alerts,
|
|
4814
|
+
...this.runtimeAlerts.map((alert) => ({ ...alert })),
|
|
4815
|
+
...certificationAlerts,
|
|
4816
|
+
...stalledAlerts,
|
|
4817
|
+
],
|
|
4818
|
+
});
|
|
4819
|
+
}
|
|
4820
|
+
async publish(snapshot) {
|
|
4821
|
+
try {
|
|
4822
|
+
const current = snapshot ?? (await this.publicSnapshotLocked());
|
|
4823
|
+
await this.publishDashboard(this.store.rootDir, current);
|
|
4824
|
+
this.dashboardHealthy = true;
|
|
4825
|
+
}
|
|
4826
|
+
catch {
|
|
4827
|
+
this.dashboardHealthy = false;
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
//# sourceMappingURL=service.js.map
|