agent-embassy 3.0.0 → 4.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 +42 -0
- package/CONTRIBUTING.md +92 -200
- package/README.md +159 -138
- package/SECURITY.md +191 -335
- package/dist/src/gateway/broker-check.d.ts +26 -0
- package/dist/src/gateway/broker-check.js +89 -0
- package/dist/src/gateway/broker-check.js.map +1 -0
- package/dist/src/gateway/broker-control.d.ts +72 -0
- package/dist/src/gateway/broker-control.js +164 -0
- package/dist/src/gateway/broker-control.js.map +1 -0
- package/dist/src/gateway/broker.d.ts +117 -0
- package/dist/src/gateway/broker.js +186 -0
- package/dist/src/gateway/broker.js.map +1 -0
- package/dist/src/gateway/claude-peer.d.ts +1 -124
- package/dist/src/gateway/claude-peer.js +62 -1053
- package/dist/src/gateway/claude-peer.js.map +1 -1
- package/dist/src/gateway/codex-stateless-transport.d.ts +0 -12
- package/dist/src/gateway/codex-stateless-transport.js +8 -142
- package/dist/src/gateway/codex-stateless-transport.js.map +1 -1
- package/dist/src/gateway/config.d.ts +2 -6
- package/dist/src/gateway/config.js +17 -27
- package/dist/src/gateway/config.js.map +1 -1
- package/dist/src/gateway/coordinator.d.ts +57 -0
- package/dist/src/gateway/coordinator.js +217 -0
- package/dist/src/gateway/coordinator.js.map +1 -0
- package/dist/src/gateway/core-cli.d.ts +15 -0
- package/dist/src/gateway/core-cli.js +282 -0
- package/dist/src/gateway/core-cli.js.map +1 -0
- package/dist/src/gateway/core-service-command.d.ts +16 -0
- package/dist/src/gateway/core-service-command.js +71 -0
- package/dist/src/gateway/core-service-command.js.map +1 -0
- package/dist/src/gateway/endpoint-directory.d.ts +33 -0
- package/dist/src/gateway/endpoint-directory.js +223 -0
- package/dist/src/gateway/endpoint-directory.js.map +1 -0
- package/dist/src/gateway/federation.d.ts +89 -0
- package/dist/src/gateway/federation.js +632 -0
- package/dist/src/gateway/federation.js.map +1 -0
- package/dist/src/gateway/ledger-codec.d.ts +3 -0
- package/dist/src/gateway/ledger-codec.js +159 -0
- package/dist/src/gateway/ledger-codec.js.map +1 -0
- package/dist/src/gateway/ledger.d.ts +138 -0
- package/dist/src/gateway/ledger.js +319 -0
- package/dist/src/gateway/ledger.js.map +1 -0
- package/dist/src/gateway/local-control.d.ts +23 -0
- package/dist/src/gateway/local-control.js +489 -0
- package/dist/src/gateway/local-control.js.map +1 -0
- package/dist/src/gateway/native-destinations.d.ts +34 -0
- package/dist/src/gateway/native-destinations.js +272 -0
- package/dist/src/gateway/native-destinations.js.map +1 -0
- package/dist/src/gateway/owned-state.d.ts +57 -0
- package/dist/src/gateway/owned-state.js +398 -0
- package/dist/src/gateway/owned-state.js.map +1 -0
- package/dist/src/gateway/provenance-envelope.d.ts +3 -3
- package/dist/src/gateway/provenance-envelope.js +3 -23
- package/dist/src/gateway/provenance-envelope.js.map +1 -1
- package/dist/src/gateway/runtime.d.ts +41 -0
- package/dist/src/gateway/runtime.js +175 -0
- package/dist/src/gateway/runtime.js.map +1 -0
- package/docs/CONFIGURATION.md +178 -266
- package/docs/DELIVERY.md +133 -82
- package/docs/GATEWAY-ARCHITECTURE.md +276 -943
- package/package.json +5 -5
- package/skills/embassy-peer/SKILL.md +38 -194
- package/skills/embassy-peer/agents/openai.yaml +2 -2
- package/dist/src/gateway/claude-helper-client.d.ts +0 -2
- package/dist/src/gateway/claude-helper-client.js +0 -2
- package/dist/src/gateway/claude-helper-client.js.map +0 -1
- package/dist/src/gateway/claude-helper-protocol.d.ts +0 -117
- package/dist/src/gateway/claude-helper-protocol.js +0 -122
- package/dist/src/gateway/claude-helper-protocol.js.map +0 -1
- package/dist/src/gateway/claude-helper-supervisor.d.ts +0 -87
- package/dist/src/gateway/claude-helper-supervisor.js +0 -347
- package/dist/src/gateway/claude-helper-supervisor.js.map +0 -1
- package/dist/src/gateway/claude-helper.d.ts +0 -1
- package/dist/src/gateway/claude-helper.js +0 -252
- package/dist/src/gateway/claude-helper.js.map +0 -1
- package/dist/src/gateway/cli.d.ts +0 -57
- package/dist/src/gateway/cli.js +0 -1503
- package/dist/src/gateway/cli.js.map +0 -1
- package/dist/src/gateway/codex-socket-holder.d.ts +0 -26
- package/dist/src/gateway/codex-socket-holder.js +0 -76
- package/dist/src/gateway/codex-socket-holder.js.map +0 -1
- package/dist/src/gateway/control.d.ts +0 -261
- package/dist/src/gateway/control.js +0 -903
- package/dist/src/gateway/control.js.map +0 -1
- package/dist/src/gateway/peer-client.d.ts +0 -58
- package/dist/src/gateway/peer-client.js +0 -217
- package/dist/src/gateway/peer-client.js.map +0 -1
- package/dist/src/gateway/peer-mailbox.d.ts +0 -78
- package/dist/src/gateway/peer-mailbox.js +0 -168
- package/dist/src/gateway/peer-mailbox.js.map +0 -1
- package/dist/src/gateway/peer-protocol.d.ts +0 -105
- package/dist/src/gateway/peer-protocol.js +0 -70
- package/dist/src/gateway/peer-protocol.js.map +0 -1
- package/dist/src/gateway/peer-stdio.d.ts +0 -23
- package/dist/src/gateway/peer-stdio.js +0 -100
- package/dist/src/gateway/peer-stdio.js.map +0 -1
- package/dist/src/gateway/providers.d.ts +0 -163
- package/dist/src/gateway/providers.js +0 -972
- package/dist/src/gateway/providers.js.map +0 -1
- package/dist/src/gateway/server.d.ts +0 -47
- package/dist/src/gateway/server.js +0 -219
- package/dist/src/gateway/server.js.map +0 -1
- package/dist/src/gateway/service.d.ts +0 -373
- package/dist/src/gateway/service.js +0 -2274
- package/dist/src/gateway/service.js.map +0 -1
- package/dist/src/gateway/status-view.d.ts +0 -167
- package/dist/src/gateway/status-view.js +0 -488
- package/dist/src/gateway/status-view.js.map +0 -1
- package/dist/src/gateway/store.d.ts +0 -185
- package/dist/src/gateway/store.js +0 -2359
- package/dist/src/gateway/store.js.map +0 -1
- package/dist/src/gateway/types.d.ts +0 -528
- package/dist/src/gateway/types.js +0 -341
- package/dist/src/gateway/types.js.map +0 -1
|
@@ -1,2274 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { BridgeError } from "../errors.js";
|
|
3
|
-
import { createGatewayConversationId, startGatewayControlServer, GATEWAY_EPHEMERAL_PEER_TTL_DEFAULT_MS, } from "./control.js";
|
|
4
|
-
import { PeerConnectionLostError, spawnPeerClient } from "./peer-client.js";
|
|
5
|
-
import { peerRouteRef } from "./peer-protocol.js";
|
|
6
|
-
import { GatewayStore } from "./store.js";
|
|
7
|
-
import { CONNECTOR_OBSERVATION_STALE_AFTER_MS, directionId, GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET, gatewayPublicSnapshotLimits, gatewayRegistrationIngressPrefixes, parseDirection, projectGatewayPublicSnapshot, } from "./types.js";
|
|
8
|
-
const SAFE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
9
|
-
const PRIVATE_HANDLE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
|
|
10
|
-
const PUBLIC_ALIAS = /^[a-z][a-z0-9_-]{0,31}@[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?$/;
|
|
11
|
-
const PEER_TOKEN = /^peer_[A-Za-z0-9_-]{32}$/;
|
|
12
|
-
const DISCOVERY_INTERVAL_MS = 30_000;
|
|
13
|
-
const MANAGED_CODEX_SOCKET_CHECK_INTERVAL_MS = 30_000;
|
|
14
|
-
const PEER_REFRESH_INTERVAL_MS = 30_000;
|
|
15
|
-
/** A failed timed retirement of an ephemeral peer is retried this much later… */
|
|
16
|
-
const EPHEMERAL_EXPIRY_RETRY_MS = 5_000;
|
|
17
|
-
/** …this many times in a row before the registration is left to the next restart. */
|
|
18
|
-
const EPHEMERAL_EXPIRY_MAX_ATTEMPTS = 3;
|
|
19
|
-
const PEER_FAILURE_CODES = ["PEER_DIAL_FAILED", "PEER_TUNNEL_UNAVAILABLE", "PEER_PROTOCOL_MISMATCH"];
|
|
20
|
-
const CLEAN_RETRY_DELAY_MS = 500;
|
|
21
|
-
const MAX_CONVERSATIONS = 1_024;
|
|
22
|
-
const MAX_PENDING_CLAUDE_REPLIES = MAX_CONVERSATIONS;
|
|
23
|
-
const CLEAN_RETRY_CODES = new Set([
|
|
24
|
-
"THREAD_NOT_OBSERVED",
|
|
25
|
-
"ROUTE_BUSY",
|
|
26
|
-
"APPROVAL_REQUIRED",
|
|
27
|
-
"MANAGED_CODEX_UNAVAILABLE",
|
|
28
|
-
"LOCAL_APP_SERVER_NOT_RUNNING",
|
|
29
|
-
"ENDPOINT_GENERATION_CHANGED",
|
|
30
|
-
"SPAWN_FAILED",
|
|
31
|
-
"SPAWN_TIMEOUT",
|
|
32
|
-
"TRANSPORT_CONNECT_FAILED",
|
|
33
|
-
"REQUEST_TIMEOUT",
|
|
34
|
-
"PEER_NOT_AWAITING",
|
|
35
|
-
]);
|
|
36
|
-
function registrationId() {
|
|
37
|
-
return `reg_${randomBytes(18).toString("base64url")}`;
|
|
38
|
-
}
|
|
39
|
-
function aliasHost(alias) {
|
|
40
|
-
return alias.slice(alias.lastIndexOf("@") + 1);
|
|
41
|
-
}
|
|
42
|
-
function safeCode(value, fallback) {
|
|
43
|
-
return value !== undefined && SAFE_CODE.test(value) ? value : fallback;
|
|
44
|
-
}
|
|
45
|
-
function decisionFor(error, peerPrincipal = false) {
|
|
46
|
-
if (!(error instanceof BridgeError))
|
|
47
|
-
return { accepted: false, code: "rejected" };
|
|
48
|
-
if (error.code.includes("NOT_FOUND") || error.code.includes("NOT_AVAILABLE")) {
|
|
49
|
-
return { accepted: false, code: "not_found" };
|
|
50
|
-
}
|
|
51
|
-
if (error.code.includes("COLLISION") || error.code.includes("ALREADY") || error.code.includes("OWNER")) {
|
|
52
|
-
return { accepted: false, code: "conflict" };
|
|
53
|
-
}
|
|
54
|
-
if (error.code.includes("BUSY") || error.code.includes("CAPACITY")) {
|
|
55
|
-
return { accepted: false, code: "busy" };
|
|
56
|
-
}
|
|
57
|
-
if (error.code.includes("UNAVAILABLE") || error.code.includes("OFFLINE") || error.code.includes("UNOBSERVED")) {
|
|
58
|
-
return { accepted: false, code: "unavailable" };
|
|
59
|
-
}
|
|
60
|
-
if ((peerPrincipal && error.code === "ROUTE_UNREGISTERED") || error.code.includes("MISMATCH") ||
|
|
61
|
-
error.code.includes("CHANGED") || error.code.includes("ADDRESS")) {
|
|
62
|
-
return { accepted: false, code: "route_mismatch" };
|
|
63
|
-
}
|
|
64
|
-
return { accepted: false, code: "rejected" };
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* The refusal a send returns: the coarse decision clients branch on,
|
|
68
|
-
* plus the safe code behind it so the CLI can name the one remedy that fits.
|
|
69
|
-
* Only a BridgeError's own code is carried; anything else stays anonymous.
|
|
70
|
-
*/
|
|
71
|
-
function sendRefusal(error, peerPrincipal = false) {
|
|
72
|
-
const decision = decisionFor(error, peerPrincipal);
|
|
73
|
-
const reason = error instanceof BridgeError && SAFE_CODE.test(error.code) ? error.code : undefined;
|
|
74
|
-
return { accepted: false, code: decision.code, ...(reason === undefined ? {} : { reason }) };
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* A grammar-valid alias for a session that cannot have the name it discovered
|
|
78
|
-
* under, because a live session already holds it. The suffix is derived from
|
|
79
|
-
* the session's own route handle, so it is stable across sends, and it is a
|
|
80
|
-
* hash rather than the handle itself: a native session UUID must never enter a
|
|
81
|
-
* public alias.
|
|
82
|
-
*/
|
|
83
|
-
function disambiguatedAlias(alias, routeHandle) {
|
|
84
|
-
const at = alias.lastIndexOf("@");
|
|
85
|
-
const name = alias.slice(0, at), host = alias.slice(at + 1);
|
|
86
|
-
const suffix = createHash("sha256").update(routeHandle).digest("hex").slice(0, 8);
|
|
87
|
-
return `${name.slice(0, 32 - suffix.length - 1)}-${suffix}@${host}`;
|
|
88
|
-
}
|
|
89
|
-
/** The decoder already accepted exactly one credential, so the last arm holds. */
|
|
90
|
-
function replyCaller(params) {
|
|
91
|
-
const { fromAlias, threadId, replyAddress, peerToken } = params;
|
|
92
|
-
return threadId !== undefined ? { kind: "codex", alias: fromAlias, threadId }
|
|
93
|
-
: replyAddress !== undefined ? { kind: "claude", alias: fromAlias, replyAddress }
|
|
94
|
-
: { kind: "peer", alias: fromAlias, token: peerToken };
|
|
95
|
-
}
|
|
96
|
-
function sameBinding(left, right) {
|
|
97
|
-
return left.provider === right.provider && left.hostId === right.hostId &&
|
|
98
|
-
left.routeHandle === right.routeHandle && left.registrationId === right.registrationId;
|
|
99
|
-
}
|
|
100
|
-
function connectorKey(identity) {
|
|
101
|
-
return `${identity.provider}\0${identity.hostId}`;
|
|
102
|
-
}
|
|
103
|
-
function publicRegistry(value, previouslyParseable) {
|
|
104
|
-
const counts = new Map();
|
|
105
|
-
for (const row of value.rejected)
|
|
106
|
-
if (SAFE_CODE.test(row.safeErrorCode) && Number.isSafeInteger(row.count) && row.count > 0)
|
|
107
|
-
counts.set(row.safeErrorCode, (counts.get(row.safeErrorCode) ?? 0) + row.count);
|
|
108
|
-
const rejected = [...counts].map(([safeErrorCode, count]) => ({ safeErrorCode, count }))
|
|
109
|
-
.sort((left, right) => left.safeErrorCode < right.safeErrorCode ? -1 :
|
|
110
|
-
left.safeErrorCode > right.safeErrorCode ? 1 : 0);
|
|
111
|
-
const retained = rejected.slice(0, gatewayPublicSnapshotLimits.registryRejectionCodes);
|
|
112
|
-
return {
|
|
113
|
-
entriesScanned: Math.max(0, value.entriesScanned),
|
|
114
|
-
parseableRecords: Math.max(0, Math.min(value.entriesScanned, value.parseableRecords)),
|
|
115
|
-
parseableRecordSeenSinceBoot: previouslyParseable || value.parseableRecords > 0,
|
|
116
|
-
rejected: retained.map((row) => ({ ...row })),
|
|
117
|
-
rejectedCodesOmitted: Math.max(0, rejected.length - retained.length),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
function conversationIdForSuffix(suffix) {
|
|
121
|
-
const fresh = createGatewayConversationId();
|
|
122
|
-
return suffix === undefined ? fresh : `${fresh.slice(0, -8)}${suffix}`;
|
|
123
|
-
}
|
|
124
|
-
function bodyHash(body) {
|
|
125
|
-
return createHash("sha256").update(body, "utf8").digest("hex");
|
|
126
|
-
}
|
|
127
|
-
function peerCatalogAuthorityChanged(prior, current) {
|
|
128
|
-
const authority = (catalog) => JSON.stringify(catalog.routes.map(({ state: _state, queueDepth: _depth, lastSeenAt: _seen, safeErrorCode: _code, ...route }) => route).sort((a, b) => a.ref.localeCompare(b.ref)));
|
|
129
|
-
return prior === undefined || authority(prior) !== authority(current);
|
|
130
|
-
}
|
|
131
|
-
function peerCatalogViewChanged(prior, current) {
|
|
132
|
-
const view = (catalog) => JSON.stringify([
|
|
133
|
-
catalog.health,
|
|
134
|
-
catalog.connectors.map(({ lastSeenAt: _seen, observationAgeMs: _age, ...row }) => row),
|
|
135
|
-
catalog.routes.map(({ lastSeenAt: _seen, ...row }) => row).sort((a, b) => a.ref.localeCompare(b.ref)),
|
|
136
|
-
catalog.alerts.map(({ timestamp: _timestamp, ...row }) => row),
|
|
137
|
-
]);
|
|
138
|
-
return prior === undefined || view(prior) !== view(current);
|
|
139
|
-
}
|
|
140
|
-
function peerHandle(uid, alias, token) {
|
|
141
|
-
return `peer:${createHash("sha256").update(`${uid}\0${alias}\0${token}`).digest("hex")}`;
|
|
142
|
-
}
|
|
143
|
-
export class GatewayService {
|
|
144
|
-
config;
|
|
145
|
-
store;
|
|
146
|
-
adapters;
|
|
147
|
-
now;
|
|
148
|
-
timers;
|
|
149
|
-
nativePeerCwd;
|
|
150
|
-
managedCodexSocketHeld;
|
|
151
|
-
spawnPeer;
|
|
152
|
-
peerClients = new Map();
|
|
153
|
-
peerCatalogs = new Map();
|
|
154
|
-
peerCleanupAliases = new Set();
|
|
155
|
-
/** View-only freshness; never consulted by routing or write authorization. */
|
|
156
|
-
peerRouteViews = new Map();
|
|
157
|
-
connectors = new Map();
|
|
158
|
-
routeObservations = new Map();
|
|
159
|
-
candidates = new Map();
|
|
160
|
-
collidingClaudeAliases = new Set();
|
|
161
|
-
conversations = new Map();
|
|
162
|
-
messageContexts = new Map();
|
|
163
|
-
activeAttempts = new Map();
|
|
164
|
-
reserveOperations = new Set();
|
|
165
|
-
inboundOperations = new Set();
|
|
166
|
-
peerRefreshOperations = new Set();
|
|
167
|
-
pendingClaudeReplies = new Map();
|
|
168
|
-
nativeReceipts = new Map();
|
|
169
|
-
dispatchRunners = new Map();
|
|
170
|
-
steerRunners = new Map();
|
|
171
|
-
startingTargets = new Set();
|
|
172
|
-
runtimeAlerts = [];
|
|
173
|
-
/**
|
|
174
|
-
* Ephemeral peer registrations and the wall-clock instant each one dies.
|
|
175
|
-
* Nothing durable records them, so this map is the whole lifetime: it is
|
|
176
|
-
* swept on the ordinary maintenance tick, and a restart simply never sees
|
|
177
|
-
* the routes at all.
|
|
178
|
-
*/
|
|
179
|
-
ephemeralPeerExpiries = new Map();
|
|
180
|
-
/** Consecutive failed retirements per ephemeral alias; cleared on success or removal. */
|
|
181
|
-
ephemeralExpiryFailures = new Map();
|
|
182
|
-
control;
|
|
183
|
-
wakeTimer;
|
|
184
|
-
nextDiscoveryAt = 0;
|
|
185
|
-
nextManagedCodexCheckAt = 0;
|
|
186
|
-
nextPeerRefreshAt = 0;
|
|
187
|
-
managedCodexSocketHeldOutside = false;
|
|
188
|
-
revision = 0;
|
|
189
|
-
snapshotRevision = 0;
|
|
190
|
-
snapshotFingerprint;
|
|
191
|
-
running = false;
|
|
192
|
-
closing = false;
|
|
193
|
-
closeInFlight;
|
|
194
|
-
constructor(options) {
|
|
195
|
-
this.config = options.config;
|
|
196
|
-
this.adapters = [...(options.adapters ?? [])];
|
|
197
|
-
this.store = options.store ?? new GatewayStore(options.config, options.now === undefined ? {} : { now: options.now });
|
|
198
|
-
this.now = options.now ?? (() => new Date());
|
|
199
|
-
this.nativePeerCwd = options.nativePeerCwd ?? process.cwd();
|
|
200
|
-
this.managedCodexSocketHeld = options.managedCodexSocketHeld;
|
|
201
|
-
this.spawnPeer = options.spawnPeer ?? spawnPeerClient;
|
|
202
|
-
this.timers = options.timers ?? {
|
|
203
|
-
setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
204
|
-
clearTimeout: (timer) => clearTimeout(timer),
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
async start(signal) {
|
|
208
|
-
if (this.running || this.closing)
|
|
209
|
-
return;
|
|
210
|
-
const assertActive = () => {
|
|
211
|
-
if (signal?.aborted === true || this.closing) {
|
|
212
|
-
throw new BridgeError("GATEWAY_START_CANCELLED", "Gateway startup was cancelled.", true);
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
await this.store.initialize({ deferPersistence: true });
|
|
216
|
-
try {
|
|
217
|
-
assertActive();
|
|
218
|
-
const seen = new Set();
|
|
219
|
-
for (const adapter of this.adapters) {
|
|
220
|
-
const key = connectorKey(adapter.identity);
|
|
221
|
-
if (seen.has(key) || !this.config.allowedHosts.includes(adapter.identity.hostId)) {
|
|
222
|
-
throw new BridgeError("GATEWAY_ADAPTER_NOT_ALLOWED", "A provider adapter is duplicated or outside the host allowlist.");
|
|
223
|
-
}
|
|
224
|
-
seen.add(key);
|
|
225
|
-
const started = await adapter.initialize(this.callbacksFor(adapter));
|
|
226
|
-
assertActive();
|
|
227
|
-
const observedAt = this.now().toISOString();
|
|
228
|
-
const runtime = {
|
|
229
|
-
adapter,
|
|
230
|
-
health: started.health,
|
|
231
|
-
observedAt,
|
|
232
|
-
...(started.safeErrorCode === undefined
|
|
233
|
-
? {}
|
|
234
|
-
: { safeErrorCode: safeCode(started.safeErrorCode, "ADAPTER_DEGRADED") }),
|
|
235
|
-
};
|
|
236
|
-
const registry = adapter.latestRegistryObservation?.();
|
|
237
|
-
if (registry !== undefined)
|
|
238
|
-
runtime.registry = publicRegistry(registry, false);
|
|
239
|
-
this.connectors.set(key, runtime);
|
|
240
|
-
if (started.ownedRoute !== undefined) {
|
|
241
|
-
await this.installOwnedRoute(adapter, started.ownedRoute);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
await this.refreshClaudeDiscovery().catch(() => undefined);
|
|
245
|
-
await this.refreshManagedCodexSocket().catch(() => undefined);
|
|
246
|
-
const control = await startGatewayControlServer({
|
|
247
|
-
stateDir: this.store.rootDir,
|
|
248
|
-
socketPath: this.config.controlSocketPath,
|
|
249
|
-
handlers: this.handlers(),
|
|
250
|
-
});
|
|
251
|
-
try {
|
|
252
|
-
assertActive();
|
|
253
|
-
await this.store.commitInitialization();
|
|
254
|
-
}
|
|
255
|
-
catch (error) {
|
|
256
|
-
await control.close();
|
|
257
|
-
throw error;
|
|
258
|
-
}
|
|
259
|
-
this.control = control;
|
|
260
|
-
this.running = true;
|
|
261
|
-
await this.observeLoadedRoutes();
|
|
262
|
-
const now = this.now().getTime();
|
|
263
|
-
this.nextDiscoveryAt = now + DISCOVERY_INTERVAL_MS;
|
|
264
|
-
this.nextManagedCodexCheckAt = now + MANAGED_CODEX_SOCKET_CHECK_INTERVAL_MS;
|
|
265
|
-
this.nextPeerRefreshAt = this.config.peerNodes.length > 0 ? now : 0;
|
|
266
|
-
for (const target of await this.store.inspectDispatchableTargets())
|
|
267
|
-
this.kick(target);
|
|
268
|
-
this.scheduleWake();
|
|
269
|
-
assertActive();
|
|
270
|
-
}
|
|
271
|
-
catch (error) {
|
|
272
|
-
await this.close();
|
|
273
|
-
throw error;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
async close() {
|
|
277
|
-
if (this.closeInFlight !== undefined)
|
|
278
|
-
return await this.closeInFlight;
|
|
279
|
-
this.closing = true;
|
|
280
|
-
this.closeInFlight = this.closeOnce();
|
|
281
|
-
return await this.closeInFlight;
|
|
282
|
-
}
|
|
283
|
-
async closeOnce() {
|
|
284
|
-
this.running = false;
|
|
285
|
-
if (this.wakeTimer !== undefined) {
|
|
286
|
-
this.timers.clearTimeout(this.wakeTimer);
|
|
287
|
-
this.wakeTimer = undefined;
|
|
288
|
-
}
|
|
289
|
-
// The durable phase reducer wins before any provider cleanup I/O. Exact
|
|
290
|
-
// reserved work requeues; armed and accepted work terminalize by phase.
|
|
291
|
-
await Promise.all([...this.inboundOperations]);
|
|
292
|
-
await Promise.all([...this.reserveOperations]);
|
|
293
|
-
for (const attempt of [...this.activeAttempts.values()]) {
|
|
294
|
-
const result = await this.store.settleAttemptForShutdown({
|
|
295
|
-
messageId: attempt.messageId,
|
|
296
|
-
attemptId: attempt.attemptId,
|
|
297
|
-
});
|
|
298
|
-
if (result.status === "settled")
|
|
299
|
-
await this.finishSettlement(result.settlement);
|
|
300
|
-
else if (result.status === "requeued")
|
|
301
|
-
this.activeAttempts.delete(attempt.messageId);
|
|
302
|
-
}
|
|
303
|
-
// Native receipt capabilities are memory-only. Any exact message returned
|
|
304
|
-
// to queued by the shutdown reducer must terminalize before its helper is
|
|
305
|
-
// closed; ordinary queued mail without a live receipt remains restartable.
|
|
306
|
-
for (const messageId of [...this.nativeReceipts.keys()]) {
|
|
307
|
-
const result = await this.store.settleQueuedMessageForShutdown({ messageId });
|
|
308
|
-
if (result.status === "settled")
|
|
309
|
-
await this.finishSettlement(result.settlement);
|
|
310
|
-
}
|
|
311
|
-
const control = this.control;
|
|
312
|
-
this.control = undefined;
|
|
313
|
-
const failures = [];
|
|
314
|
-
if (control !== undefined)
|
|
315
|
-
await control.close().catch((error) => failures.push(error));
|
|
316
|
-
for (const peer of this.peerClients.values())
|
|
317
|
-
peer.close();
|
|
318
|
-
this.peerClients.clear();
|
|
319
|
-
await Promise.all([...this.peerRefreshOperations]).catch((error) => failures.push(error));
|
|
320
|
-
this.peerCatalogs.clear();
|
|
321
|
-
this.peerRouteViews.clear();
|
|
322
|
-
// Provider close aborts exact owned operations. It must never join a turn.
|
|
323
|
-
await Promise.all(this.adapters.map(async (adapter) => {
|
|
324
|
-
await adapter.close().catch((error) => failures.push(error));
|
|
325
|
-
}));
|
|
326
|
-
this.activeAttempts.clear();
|
|
327
|
-
this.dispatchRunners.clear();
|
|
328
|
-
this.steerRunners.clear();
|
|
329
|
-
await this.store.close().catch((error) => failures.push(error));
|
|
330
|
-
if (failures.length > 0) {
|
|
331
|
-
throw new BridgeError("GATEWAY_CLEANUP_FAILED", "Gateway cleanup could not be confirmed.");
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
handlers() {
|
|
335
|
-
const decide = async (operation, peerPrincipal = false) => {
|
|
336
|
-
try {
|
|
337
|
-
this.assertWritable();
|
|
338
|
-
await operation();
|
|
339
|
-
this.revision += 1;
|
|
340
|
-
return { accepted: true, code: "ok" };
|
|
341
|
-
}
|
|
342
|
-
catch (error) {
|
|
343
|
-
return decisionFor(error, peerPrincipal);
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
|
-
return {
|
|
347
|
-
health: () => ({ status: this.health(), revision: this.revision }),
|
|
348
|
-
registerCodex: (params) => decide(async () => this.registerCodex(params)),
|
|
349
|
-
registerPeer: async (params) => {
|
|
350
|
-
try {
|
|
351
|
-
this.assertWritable();
|
|
352
|
-
const token = await this.registerPeer(params);
|
|
353
|
-
this.revision += 1;
|
|
354
|
-
return token === undefined ? { accepted: true, code: "ok" } : { accepted: true, code: "ok", token };
|
|
355
|
-
}
|
|
356
|
-
catch (error) {
|
|
357
|
-
return decisionFor(error, true);
|
|
358
|
-
}
|
|
359
|
-
},
|
|
360
|
-
unregisterPeer: (params) => decide(async () => this.unregisterPeer(params), true),
|
|
361
|
-
awaitPeer: (params) => this.awaitPeer(params),
|
|
362
|
-
peerReceipt: (params) => decide(async () => this.peerReceipt(params), true),
|
|
363
|
-
unregisterCodex: (params) => decide(async () => this.unregisterCodex(params)),
|
|
364
|
-
listSnapshot: () => this.snapshot(),
|
|
365
|
-
observeSnapshot: () => this.observeSnapshot(),
|
|
366
|
-
deliveryStatus: (params) => this.deliveryStatus(params.token),
|
|
367
|
-
send: (params) => this.send(params),
|
|
368
|
-
refreshDiscovery: async () => {
|
|
369
|
-
if (this.closing) {
|
|
370
|
-
return { accepted: false, code: "unavailable", revision: this.revision };
|
|
371
|
-
}
|
|
372
|
-
try {
|
|
373
|
-
// An operator-triggered rescan reports its own failure. It must never
|
|
374
|
-
// journal `discovery_refreshed` for a scan that did not complete, and
|
|
375
|
-
// must never answer `accepted: true` for a provider that is down.
|
|
376
|
-
await this.refreshClaudeDiscovery();
|
|
377
|
-
await this.recordActivity("discovery", "discovery_refreshed", [], true);
|
|
378
|
-
}
|
|
379
|
-
catch (error) {
|
|
380
|
-
return { accepted: false, code: decisionFor(error).code, revision: this.revision };
|
|
381
|
-
}
|
|
382
|
-
return { accepted: true, code: "ok", revision: this.revision };
|
|
383
|
-
},
|
|
384
|
-
peerCatalog: ({ peerHost }) => this.buildPeerCatalog(peerHost),
|
|
385
|
-
peerHandoff: ({ peerHost, handoff }) => this.receivePeerHandoff(peerHost, handoff),
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
async buildPeerCatalog(peerHost) {
|
|
389
|
-
const localHost = this.config.hostId;
|
|
390
|
-
if (!this.config.peerNodes.includes(peerHost))
|
|
391
|
-
throw new BridgeError("PEER_NOT_CONFIGURED", "The requested peer host is not configured.");
|
|
392
|
-
const [snapshot, privateRoutes] = await Promise.all([
|
|
393
|
-
this.snapshot(), this.store.inspectPrivateRoutes(),
|
|
394
|
-
]);
|
|
395
|
-
// An ephemeral registration is never published: a peer node that mirrored
|
|
396
|
-
// one would hold a route this broker will delete on a timer and can never
|
|
397
|
-
// restore, and a self-test must not leave a trace on another machine.
|
|
398
|
-
const localRoutes = privateRoutes.filter((route) => route.registrationMode !== "federated_peer" &&
|
|
399
|
-
route.binding.hostId === localHost && !this.store.isEphemeralRegistration(route.binding.registrationId));
|
|
400
|
-
const publicRoutes = new Map(snapshot.routes.map((route) => [route.alias, route]));
|
|
401
|
-
const routes = localRoutes.map((route) => {
|
|
402
|
-
const row = publicRoutes.get(route.alias);
|
|
403
|
-
return {
|
|
404
|
-
ref: peerRouteRef(localHost, route.binding.registrationId), alias: route.alias, provider: route.binding.provider, host: localHost,
|
|
405
|
-
enabled: route.enabled, state: row?.state ?? (route.enabled ? "stale" : "disabled"),
|
|
406
|
-
queueDepth: row?.queueDepth ?? 0, ...(row?.lastSeenAt === undefined ? {} : { lastSeenAt: row.lastSeenAt }),
|
|
407
|
-
...(row?.safeErrorCode === undefined ? {} : { safeErrorCode: row.safeErrorCode }),
|
|
408
|
-
};
|
|
409
|
-
});
|
|
410
|
-
return { revision: this.revision, complete: true, truncated: false, generatedAt: snapshot.generatedAt,
|
|
411
|
-
health: snapshot.health, connectors: snapshot.connectors.filter((row) => row.host === localHost).map((row) => ({
|
|
412
|
-
provider: row.provider, host: row.host, health: row.health, protocol: row.protocol, protocolVersion: row.protocolVersion,
|
|
413
|
-
...(row.lastSeenAt === undefined ? {} : { lastSeenAt: row.lastSeenAt }),
|
|
414
|
-
...(row.observationAgeMs === undefined ? {} : { observationAgeMs: row.observationAgeMs }),
|
|
415
|
-
...(row.safeErrorCode === undefined ? {} : { safeErrorCode: row.safeErrorCode }),
|
|
416
|
-
})), routes, alerts: snapshot.alerts.filter((alert) => (alert.host === undefined || alert.host === localHost) && (alert.alias === undefined || aliasHost(alert.alias) === localHost)), };
|
|
417
|
-
}
|
|
418
|
-
async receivePeerHandoff(peerHost, handoff) {
|
|
419
|
-
if (!this.config.peerNodes.includes(peerHost))
|
|
420
|
-
throw new BridgeError("PEER_NOT_CONFIGURED", "The sending peer host is not configured.");
|
|
421
|
-
const enqueued = await this.store.enqueuePeerHandoff(peerHost, handoff);
|
|
422
|
-
if (enqueued.messageId !== undefined) {
|
|
423
|
-
const source = await this.store.inspectPrivateRoute(handoff.source.alias);
|
|
424
|
-
const target = await this.store.inspectPrivateRoute(handoff.target.alias);
|
|
425
|
-
if (source === undefined || target === undefined)
|
|
426
|
-
throw new BridgeError("ROUTE_UNREGISTERED", "The committed peer handoff endpoint is no longer current.");
|
|
427
|
-
const conversationId = conversationIdForSuffix(handoff.conversationCorrelation);
|
|
428
|
-
this.rememberConversation({ id: conversationId, sourceAlias: source.alias, targetAlias: target.alias,
|
|
429
|
-
sourceBinding: source.binding, targetBinding: target.binding, expectsReply: handoff.expectsReply,
|
|
430
|
-
nextSequence: 1 });
|
|
431
|
-
this.messageContexts.set(enqueued.messageId, { conversationId, expectsReply: handoff.expectsReply });
|
|
432
|
-
this.kick(target.alias, target.binding.registrationId);
|
|
433
|
-
}
|
|
434
|
-
this.revision += 1;
|
|
435
|
-
return { accepted: true };
|
|
436
|
-
}
|
|
437
|
-
async snapshot() {
|
|
438
|
-
const base = await this.store.publicSnapshot();
|
|
439
|
-
const now = this.now();
|
|
440
|
-
const routes = base.routes.map((route) => {
|
|
441
|
-
const peer = this.peerRouteViews.get(route.alias);
|
|
442
|
-
if (peer !== undefined) {
|
|
443
|
-
const age = now.getTime() - Date.parse(peer.observedAt);
|
|
444
|
-
return { ...route, state: peer.state === "unobserved" || age > CONNECTOR_OBSERVATION_STALE_AFTER_MS ? "stale" : peer.state,
|
|
445
|
-
lastSeenAt: peer.observedAt, ...(peer.safeErrorCode === undefined ? {} : { safeErrorCode: peer.safeErrorCode }) };
|
|
446
|
-
}
|
|
447
|
-
const persisted = this.routeObservations.get(route.alias);
|
|
448
|
-
if (persisted === undefined)
|
|
449
|
-
return route;
|
|
450
|
-
const current = this.routeObservationStillCurrent(persisted);
|
|
451
|
-
if (!current)
|
|
452
|
-
return route;
|
|
453
|
-
const age = now.getTime() - Date.parse(persisted.observedAt);
|
|
454
|
-
const state = persisted.state === "unobserved" ||
|
|
455
|
-
age > CONNECTOR_OBSERVATION_STALE_AFTER_MS ? "stale" : persisted.state;
|
|
456
|
-
return {
|
|
457
|
-
...route,
|
|
458
|
-
state,
|
|
459
|
-
lastSeenAt: persisted.observedAt,
|
|
460
|
-
...(persisted.safeErrorCode === undefined ? {} : { safeErrorCode: persisted.safeErrorCode }),
|
|
461
|
-
};
|
|
462
|
-
});
|
|
463
|
-
const connectors = [...this.connectors.values()].map((runtime) => {
|
|
464
|
-
const observedAt = runtime.observedAt;
|
|
465
|
-
const age = observedAt === undefined ? undefined : Math.max(0, now.getTime() - Date.parse(observedAt));
|
|
466
|
-
const stale = runtime.health === "healthy" &&
|
|
467
|
-
(age === undefined || age > CONNECTOR_OBSERVATION_STALE_AFTER_MS);
|
|
468
|
-
const socketHeld = runtime.adapter.identity.provider === "codex" && this.managedCodexSocketHeldOutside;
|
|
469
|
-
const connectorCode = stale ? "CONNECTOR_OBSERVATION_STALE" : runtime.safeErrorCode ??
|
|
470
|
-
(socketHeld ? "MANAGED_CODEX_UNAVAILABLE" : undefined);
|
|
471
|
-
return {
|
|
472
|
-
provider: runtime.adapter.identity.provider,
|
|
473
|
-
host: runtime.adapter.identity.hostId,
|
|
474
|
-
health: stale || socketHeld ? "degraded" : runtime.health,
|
|
475
|
-
protocol: runtime.adapter.protocol,
|
|
476
|
-
protocolVersion: runtime.adapter.protocolVersion,
|
|
477
|
-
...(observedAt === undefined ? {} : { lastSeenAt: observedAt }),
|
|
478
|
-
...(age === undefined ? {} : { observationAgeMs: age }),
|
|
479
|
-
...(runtime.registry === undefined ? {} : { registry: runtime.registry }),
|
|
480
|
-
...(connectorCode === undefined ? {} : { safeErrorCode: connectorCode }),
|
|
481
|
-
};
|
|
482
|
-
});
|
|
483
|
-
const availablePeers = [...this.candidates.values()]
|
|
484
|
-
.filter((candidate) => !this.collidingClaudeAliases.has(candidate.alias))
|
|
485
|
-
.slice(0, gatewayPublicSnapshotLimits.availablePeers)
|
|
486
|
-
.map((candidate) => ({
|
|
487
|
-
alias: candidate.alias,
|
|
488
|
-
provider: "claude",
|
|
489
|
-
host: candidate.adapter.identity.hostId,
|
|
490
|
-
state: candidate.state,
|
|
491
|
-
validated: true,
|
|
492
|
-
routed: routes.some((route) => route.provider === "claude" && route.alias === candidate.alias),
|
|
493
|
-
lastSeenAt: candidate.observedAt,
|
|
494
|
-
}));
|
|
495
|
-
return projectGatewayPublicSnapshot({
|
|
496
|
-
...base,
|
|
497
|
-
generatedAt: now.toISOString(),
|
|
498
|
-
health: connectors.some((connector) => connector.health === "degraded")
|
|
499
|
-
? "degraded"
|
|
500
|
-
: connectors.length === 0
|
|
501
|
-
? "offline"
|
|
502
|
-
: "healthy",
|
|
503
|
-
connectors,
|
|
504
|
-
availablePeers,
|
|
505
|
-
routes,
|
|
506
|
-
alerts: [...base.alerts, ...this.runtimeAlerts].slice(-gatewayPublicSnapshotLimits.alerts),
|
|
507
|
-
truncation: base.truncation,
|
|
508
|
-
}, GATEWAY_PUBLIC_SNAPSHOT_BYTE_BUDGET);
|
|
509
|
-
}
|
|
510
|
-
async observeSnapshot() {
|
|
511
|
-
const snapshot = await this.snapshot();
|
|
512
|
-
const fingerprint = JSON.stringify({ ...snapshot, generatedAt: "" });
|
|
513
|
-
if (this.snapshotFingerprint !== undefined && fingerprint !== this.snapshotFingerprint) {
|
|
514
|
-
this.snapshotRevision += 1;
|
|
515
|
-
}
|
|
516
|
-
this.snapshotFingerprint = fingerprint;
|
|
517
|
-
return { snapshotRevision: this.snapshotRevision, snapshot };
|
|
518
|
-
}
|
|
519
|
-
health() {
|
|
520
|
-
return this.connectors.size > 0 &&
|
|
521
|
-
[...this.connectors.values()].every((runtime) => runtime.health === "healthy") ? "ok" : "degraded";
|
|
522
|
-
}
|
|
523
|
-
async installOwnedRoute(adapter, owned) {
|
|
524
|
-
const prefix = gatewayRegistrationIngressPrefixes[adapter.identity.provider];
|
|
525
|
-
if (!PUBLIC_ALIAS.test(owned.alias) ||
|
|
526
|
-
!owned.alias.endsWith(`@${adapter.identity.hostId}`) ||
|
|
527
|
-
(prefix !== undefined && !owned.alias.startsWith(prefix)) ||
|
|
528
|
-
!PRIVATE_HANDLE.test(owned.routeHandle)) {
|
|
529
|
-
throw new BridgeError("GATEWAY_OWNED_ROUTE_INVALID", "The configured route identity is invalid.");
|
|
530
|
-
}
|
|
531
|
-
const existing = await this.store.inspectPrivateRoute(owned.alias);
|
|
532
|
-
if (existing !== undefined) {
|
|
533
|
-
if (existing.binding.provider !== adapter.identity.provider ||
|
|
534
|
-
existing.binding.hostId !== adapter.identity.hostId ||
|
|
535
|
-
existing.binding.routeHandle !== owned.routeHandle) {
|
|
536
|
-
throw new BridgeError("ROUTE_ALIAS_ALREADY_REGISTERED", "The configured route conflicts with durable state.");
|
|
537
|
-
}
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
await this.store.registerRoute({
|
|
541
|
-
alias: owned.alias,
|
|
542
|
-
binding: { ...adapter.identity, routeHandle: owned.routeHandle, registrationId: registrationId() },
|
|
543
|
-
registrationMode: "explicit_opt_in",
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
async observeLoadedRoutes() {
|
|
547
|
-
for (const route of await this.store.listLogicalRoutes()) {
|
|
548
|
-
if (route.registrationMode === "federated_peer")
|
|
549
|
-
this.peerRouteViews.set(route.alias, { route: route.binding,
|
|
550
|
-
state: "unobserved", observedAt: this.now().toISOString(), safeErrorCode: "PEER_TUNNEL_UNAVAILABLE" });
|
|
551
|
-
this.observeRoute(route);
|
|
552
|
-
if (route.binding.provider !== "claude") {
|
|
553
|
-
this.reconcileAdvertisement(route);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
observeRoute(route) {
|
|
558
|
-
const adapter = this.adapterFor(route.binding);
|
|
559
|
-
try {
|
|
560
|
-
adapter?.observeLogicalRoute?.({
|
|
561
|
-
alias: route.alias,
|
|
562
|
-
routeHandle: route.binding.routeHandle,
|
|
563
|
-
registrationId: route.binding.registrationId,
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
catch {
|
|
567
|
-
this.alert("ROUTE_OBSERVATION_FAILED", route);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
forgetRoute(route) {
|
|
571
|
-
try {
|
|
572
|
-
this.adapterFor(route.binding)?.forgetLogicalRoute?.(route.binding.registrationId);
|
|
573
|
-
}
|
|
574
|
-
catch {
|
|
575
|
-
this.alert("ROUTE_OBSERVATION_FAILED", route);
|
|
576
|
-
}
|
|
577
|
-
this.routeObservations.delete(route.alias);
|
|
578
|
-
}
|
|
579
|
-
adapterFor(binding) {
|
|
580
|
-
return this.connectors.get(connectorKey(binding))?.adapter ??
|
|
581
|
-
this.adapters.find((adapter) => adapter.identity.provider === binding.provider && adapter.identity.hostId === binding.hostId);
|
|
582
|
-
}
|
|
583
|
-
claudeAdapter(hostId) {
|
|
584
|
-
return this.adapterFor({ provider: "claude", hostId });
|
|
585
|
-
}
|
|
586
|
-
async advertise(route) {
|
|
587
|
-
const hostId = route.registrationMode === "federated_peer"
|
|
588
|
-
? this.config.hostId : route.binding.hostId;
|
|
589
|
-
await this.claudeAdapter(hostId)?.advertiseNativeSourcePeer?.({
|
|
590
|
-
alias: route.alias,
|
|
591
|
-
sourceProvider: route.binding.provider,
|
|
592
|
-
cwd: this.nativePeerCwd,
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
async unadvertise(route) {
|
|
596
|
-
const hostId = route.registrationMode === "federated_peer"
|
|
597
|
-
? this.config.hostId : route.binding.hostId;
|
|
598
|
-
await this.claudeAdapter(hostId)?.unadvertiseNativeSourcePeer?.(route.alias);
|
|
599
|
-
}
|
|
600
|
-
reconcileAdvertisement(route) {
|
|
601
|
-
void this.advertise(route).catch((error) => this.alert("NATIVE_ADVERTISEMENT_FAILED", route, error));
|
|
602
|
-
}
|
|
603
|
-
async reconcileUnadvertisement(route) {
|
|
604
|
-
await this.unadvertise(route).catch((error) => this.alert("NATIVE_UNADVERTISEMENT_FAILED", route, error));
|
|
605
|
-
}
|
|
606
|
-
async removeOwnedRoute(route) {
|
|
607
|
-
this.assertWritable();
|
|
608
|
-
const result = await this.store.removeOwnedRouteAtomic({
|
|
609
|
-
alias: route.alias,
|
|
610
|
-
binding: route.binding,
|
|
611
|
-
activity: { operatorAction: true },
|
|
612
|
-
});
|
|
613
|
-
if (!result.removed)
|
|
614
|
-
throw new BridgeError("CODEX_REGISTRATION_NOT_FOUND", "The exact route is absent.");
|
|
615
|
-
await this.finishSettlements(result.settlements);
|
|
616
|
-
this.forgetRoute(route);
|
|
617
|
-
await this.reconcileUnadvertisement(route);
|
|
618
|
-
}
|
|
619
|
-
async recordActivity(kind, action, aliases, operatorAction) {
|
|
620
|
-
await this.store.recordActivity({
|
|
621
|
-
kind,
|
|
622
|
-
action,
|
|
623
|
-
outcome: "accepted",
|
|
624
|
-
aliases,
|
|
625
|
-
operatorAction,
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
async registerCodex(params) {
|
|
629
|
-
const existing = await this.store.inspectPrivateRoute(params.alias);
|
|
630
|
-
if (existing !== undefined && params.succeedsAlias === undefined) {
|
|
631
|
-
if (existing.binding.provider !== "codex" || existing.binding.routeHandle !== params.threadId) {
|
|
632
|
-
throw new BridgeError("ROUTE_ALIAS_ALREADY_REGISTERED", "The alias belongs to another registration.");
|
|
633
|
-
}
|
|
634
|
-
this.observeRoute(existing);
|
|
635
|
-
this.reconcileAdvertisement(existing);
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
const replacement = {
|
|
639
|
-
alias: params.alias,
|
|
640
|
-
binding: {
|
|
641
|
-
provider: "codex",
|
|
642
|
-
hostId: params.hostId,
|
|
643
|
-
routeHandle: params.threadId,
|
|
644
|
-
registrationId: registrationId(),
|
|
645
|
-
},
|
|
646
|
-
registrationMode: "explicit_opt_in",
|
|
647
|
-
};
|
|
648
|
-
if (params.succeedsAlias === undefined) {
|
|
649
|
-
this.assertWritable();
|
|
650
|
-
await this.store.registerRoute(replacement);
|
|
651
|
-
const installed = (await this.store.inspectPrivateRoute(params.alias));
|
|
652
|
-
await this.recordActivity("registration", "codex_registered", [installed.alias], true);
|
|
653
|
-
this.observeRoute(installed);
|
|
654
|
-
this.reconcileAdvertisement(installed);
|
|
655
|
-
this.kick(params.alias);
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
const previous = await this.store.inspectPrivateRoute(params.succeedsAlias);
|
|
659
|
-
if (previous === undefined) {
|
|
660
|
-
const installed = await this.store.inspectPrivateRoute(params.alias);
|
|
661
|
-
if (installed !== undefined &&
|
|
662
|
-
installed.binding.provider === "codex" &&
|
|
663
|
-
installed.binding.routeHandle === params.threadId) {
|
|
664
|
-
this.observeRoute(installed);
|
|
665
|
-
this.reconcileAdvertisement(installed);
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
throw new BridgeError("CODEX_SUCCESSION_OWNER_MISMATCH", "The succeeded registration is absent.");
|
|
669
|
-
}
|
|
670
|
-
this.assertWritable();
|
|
671
|
-
const result = await this.store.replaceCodexRegistrationAtomic({
|
|
672
|
-
oldAlias: params.succeedsAlias,
|
|
673
|
-
expectedOldRegistrationId: previous.binding.registrationId,
|
|
674
|
-
replacement,
|
|
675
|
-
activity: { operatorAction: true },
|
|
676
|
-
});
|
|
677
|
-
await this.finishSettlements(result.settlements);
|
|
678
|
-
if (!result.idempotent) {
|
|
679
|
-
this.forgetRoute(previous);
|
|
680
|
-
this.reconcileUnadvertisement(previous);
|
|
681
|
-
}
|
|
682
|
-
const installed = (await this.store.inspectPrivateRoute(params.alias));
|
|
683
|
-
this.observeRoute(installed);
|
|
684
|
-
this.reconcileAdvertisement(installed);
|
|
685
|
-
this.kick(params.alias);
|
|
686
|
-
}
|
|
687
|
-
async registerPeer(params) {
|
|
688
|
-
if (this.peerCleanupAliases.has(params.alias)) {
|
|
689
|
-
throw new BridgeError("ROUTE_BUSY", "The route is still completing cleanup.", true);
|
|
690
|
-
}
|
|
691
|
-
const existing = await this.store.inspectPrivateRoute(params.alias);
|
|
692
|
-
if (existing !== undefined) {
|
|
693
|
-
if (params.token === undefined)
|
|
694
|
-
throw new BridgeError("ROUTE_UNREGISTERED", "The route binding does not match.");
|
|
695
|
-
await this.assertPeer({ alias: params.alias, token: params.token });
|
|
696
|
-
this.observeRoute(existing);
|
|
697
|
-
this.reconcileAdvertisement(existing);
|
|
698
|
-
this.kick(existing.alias, existing.binding.registrationId);
|
|
699
|
-
return undefined;
|
|
700
|
-
}
|
|
701
|
-
if (params.token !== undefined || !params.alias.endsWith(`@${this.config.hostId}`)) {
|
|
702
|
-
throw new BridgeError("ROUTE_UNREGISTERED", "The route binding does not match.");
|
|
703
|
-
}
|
|
704
|
-
const token = `peer_${randomBytes(24).toString("base64url")}`;
|
|
705
|
-
await this.store.registerRoute({ alias: params.alias, binding: {
|
|
706
|
-
provider: "peer", hostId: this.config.hostId,
|
|
707
|
-
routeHandle: peerHandle(process.getuid(), params.alias, token), registrationId: registrationId(),
|
|
708
|
-
}, registrationMode: "explicit_opt_in",
|
|
709
|
-
...(params.ephemeral === true ? { ephemeral: true } : {}) });
|
|
710
|
-
const installed = (await this.store.inspectPrivateRoute(params.alias));
|
|
711
|
-
if (params.ephemeral === true) {
|
|
712
|
-
this.ephemeralPeerExpiries.set(installed.alias, this.now().getTime() + (params.ttlMs ?? GATEWAY_EPHEMERAL_PEER_TTL_DEFAULT_MS));
|
|
713
|
-
this.scheduleWake();
|
|
714
|
-
}
|
|
715
|
-
this.observeRoute(installed);
|
|
716
|
-
this.reconcileAdvertisement(installed);
|
|
717
|
-
return token;
|
|
718
|
-
}
|
|
719
|
-
async assertPeer(params) {
|
|
720
|
-
const route = await this.store.inspectPrivateRoute(params.alias);
|
|
721
|
-
const expected = Buffer.from(peerHandle(process.getuid(), params.alias, params.token));
|
|
722
|
-
const actualHandle = route?.binding.provider === "peer" && /^peer:[0-9a-f]{64}$/.test(route.binding.routeHandle)
|
|
723
|
-
? route.binding.routeHandle : `peer:${"0".repeat(64)}`;
|
|
724
|
-
const matches = timingSafeEqual(Buffer.from(actualHandle), expected);
|
|
725
|
-
if (!PEER_TOKEN.test(params.token) || route?.binding.provider !== "peer" || !matches) {
|
|
726
|
-
throw new BridgeError("ROUTE_UNREGISTERED", "The route binding does not match.");
|
|
727
|
-
}
|
|
728
|
-
return route;
|
|
729
|
-
}
|
|
730
|
-
async unregisterPeer(params) {
|
|
731
|
-
const route = await this.assertPeer(params);
|
|
732
|
-
if (this.peerCleanupAliases.has(route.alias))
|
|
733
|
-
throw new BridgeError("ROUTE_BUSY", "The route is still completing cleanup.", true);
|
|
734
|
-
this.peerCleanupAliases.add(route.alias);
|
|
735
|
-
// The expiry leaves the map before the removal, under the same cleanup
|
|
736
|
-
// guard, so a clock that fires mid-removal has nothing to act on; a
|
|
737
|
-
// removal that does not happen puts it back, so a refused unregister
|
|
738
|
-
// still leaves the registration to its own expiry.
|
|
739
|
-
const expiry = this.ephemeralPeerExpiries.get(route.alias);
|
|
740
|
-
this.ephemeralPeerExpiries.delete(route.alias);
|
|
741
|
-
this.ephemeralExpiryFailures.delete(route.alias);
|
|
742
|
-
let removed = false;
|
|
743
|
-
try {
|
|
744
|
-
const result = await this.store.removeOwnedRouteAtomic({ alias: route.alias, binding: route.binding });
|
|
745
|
-
if (!result.removed)
|
|
746
|
-
throw new BridgeError("ROUTE_UNREGISTERED", "The route binding does not match.");
|
|
747
|
-
removed = true;
|
|
748
|
-
await this.finishSettlements(result.settlements);
|
|
749
|
-
this.forgetRoute(route);
|
|
750
|
-
await this.reconcileUnadvertisement(route);
|
|
751
|
-
}
|
|
752
|
-
finally {
|
|
753
|
-
if (!removed && expiry !== undefined)
|
|
754
|
-
this.ephemeralPeerExpiries.set(route.alias, expiry);
|
|
755
|
-
this.peerCleanupAliases.delete(route.alias);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
/**
|
|
759
|
-
* Retires every ephemeral peer registration whose lifetime has run out. It
|
|
760
|
-
* is the same removal an explicit `unregister-peer` performs — queued work
|
|
761
|
-
* is settled, not abandoned — minus the token check, because the authority
|
|
762
|
-
* here is the clock the broker set itself.
|
|
763
|
-
*
|
|
764
|
-
* The clock is keyed by alias, and an alias can be re-registered durably
|
|
765
|
-
* after its ephemeral holder left, so nothing is retired unless the route
|
|
766
|
-
* behind the alias is still an ephemeral one. A retirement that throws
|
|
767
|
-
* keeps its entry and is retried `EPHEMERAL_EXPIRY_RETRY_MS` later, with an
|
|
768
|
-
* alert each time; after `EPHEMERAL_EXPIRY_MAX_ATTEMPTS` consecutive
|
|
769
|
-
* failures the entry is dropped and the registration is left to the next
|
|
770
|
-
* restart, which clears it by construction.
|
|
771
|
-
*/
|
|
772
|
-
async expireEphemeralPeers(now) {
|
|
773
|
-
for (const [alias, expiresAt] of [...this.ephemeralPeerExpiries]) {
|
|
774
|
-
if (expiresAt > now)
|
|
775
|
-
continue;
|
|
776
|
-
// Another owner is mid-removal; the entry is theirs to clear or restore.
|
|
777
|
-
if (this.peerCleanupAliases.has(alias))
|
|
778
|
-
continue;
|
|
779
|
-
const route = await this.store.inspectPrivateRoute(alias);
|
|
780
|
-
if (route === undefined || !this.store.isEphemeralRegistration(route.binding.registrationId)) {
|
|
781
|
-
this.ephemeralPeerExpiries.delete(alias);
|
|
782
|
-
this.ephemeralExpiryFailures.delete(alias);
|
|
783
|
-
continue;
|
|
784
|
-
}
|
|
785
|
-
this.peerCleanupAliases.add(alias);
|
|
786
|
-
try {
|
|
787
|
-
const result = await this.store.removeOwnedRouteAtomic({ alias, binding: route.binding });
|
|
788
|
-
if (result.removed) {
|
|
789
|
-
await this.finishSettlements(result.settlements);
|
|
790
|
-
this.forgetRoute(route);
|
|
791
|
-
await this.reconcileUnadvertisement(route);
|
|
792
|
-
}
|
|
793
|
-
// Not removed means the binding changed underneath: the alias belongs
|
|
794
|
-
// to someone else now, and this clock is spent either way.
|
|
795
|
-
this.ephemeralPeerExpiries.delete(alias);
|
|
796
|
-
this.ephemeralExpiryFailures.delete(alias);
|
|
797
|
-
}
|
|
798
|
-
catch (error) {
|
|
799
|
-
this.alert("EPHEMERAL_ROUTE_EXPIRY_FAILED", route, error);
|
|
800
|
-
const attempts = (this.ephemeralExpiryFailures.get(alias) ?? 0) + 1;
|
|
801
|
-
if (attempts >= EPHEMERAL_EXPIRY_MAX_ATTEMPTS) {
|
|
802
|
-
this.ephemeralPeerExpiries.delete(alias);
|
|
803
|
-
this.ephemeralExpiryFailures.delete(alias);
|
|
804
|
-
}
|
|
805
|
-
else {
|
|
806
|
-
this.ephemeralExpiryFailures.set(alias, attempts);
|
|
807
|
-
this.ephemeralPeerExpiries.set(alias, now + EPHEMERAL_EXPIRY_RETRY_MS);
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
finally {
|
|
811
|
-
this.peerCleanupAliases.delete(alias);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
async awaitPeer(params) {
|
|
816
|
-
this.assertWritable();
|
|
817
|
-
const route = await this.assertPeer(params);
|
|
818
|
-
const adapter = this.adapterFor(route.binding);
|
|
819
|
-
if (adapter?.awaitMessage === undefined)
|
|
820
|
-
throw new BridgeError("PROVIDER_UNAVAILABLE", "The provider is unavailable.", true);
|
|
821
|
-
const pending = adapter.awaitMessage({ alias: route.alias, ...route.binding });
|
|
822
|
-
this.kick(route.alias, route.binding.registrationId);
|
|
823
|
-
return await pending;
|
|
824
|
-
}
|
|
825
|
-
async peerReceipt(params) {
|
|
826
|
-
const route = await this.assertPeer(params);
|
|
827
|
-
const adapter = this.adapterFor(route.binding);
|
|
828
|
-
if (adapter?.acknowledgeReceipt({ alias: route.alias, ...route.binding, receipt: params.receipt }) === "rejected") {
|
|
829
|
-
throw new BridgeError("ROUTE_UNREGISTERED", "The route binding does not match.");
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
async unregisterCodex(params) {
|
|
833
|
-
const route = await this.store.inspectPrivateRoute(params.alias);
|
|
834
|
-
if (route === undefined ||
|
|
835
|
-
route.binding.provider !== "codex" ||
|
|
836
|
-
route.binding.routeHandle !== params.threadId) {
|
|
837
|
-
throw new BridgeError("CODEX_REGISTRATION_NOT_FOUND", "The exact Codex registration is absent.");
|
|
838
|
-
}
|
|
839
|
-
if (route.registrationMode === "federated_peer") {
|
|
840
|
-
throw new BridgeError("FEDERATED_ROUTE_READ_ONLY", "A federated route is owned by its peer node and cannot be unregistered here.");
|
|
841
|
-
}
|
|
842
|
-
await this.removeOwnedRoute(route);
|
|
843
|
-
}
|
|
844
|
-
/**
|
|
845
|
-
* The name this session may take. A name belongs to one route at a time, so
|
|
846
|
-
* when another Claude route already holds it and THAT route's session is
|
|
847
|
-
* still live, this session takes a disambiguated name rather than displacing
|
|
848
|
-
* a working session and cancelling its queued work. Twins that share a
|
|
849
|
-
* display name therefore both keep routes and both stay reachable by UUID;
|
|
850
|
-
* only a route whose session is actually gone is displaced.
|
|
851
|
-
*/
|
|
852
|
-
claimableAlias(candidate, routes) {
|
|
853
|
-
const holder = routes.find((route) => route.alias === candidate.alias &&
|
|
854
|
-
route.binding.provider === "claude" &&
|
|
855
|
-
!(route.binding.hostId === candidate.adapter.identity.hostId &&
|
|
856
|
-
route.binding.routeHandle === candidate.routeHandle));
|
|
857
|
-
if (holder === undefined)
|
|
858
|
-
return candidate.alias;
|
|
859
|
-
const holderIsLive = [...this.candidates.values()].some((row) => row.adapter.identity.hostId === holder.binding.hostId &&
|
|
860
|
-
row.routeHandle === holder.binding.routeHandle);
|
|
861
|
-
return holderIsLive
|
|
862
|
-
? disambiguatedAlias(candidate.alias, candidate.routeHandle)
|
|
863
|
-
: candidate.alias;
|
|
864
|
-
}
|
|
865
|
-
/**
|
|
866
|
-
* Installs (or brings up to date) the route of one discovered Claude
|
|
867
|
-
* session. The adapter pins the session's route handle, the workspace
|
|
868
|
-
* check keeps deliberately broad workspaces out, and the store reuses the
|
|
869
|
-
* registration of a session already bound under the same (host, UUID) so a
|
|
870
|
-
* re-anchored or renamed session keeps its identity and in-flight
|
|
871
|
-
* conversations. Any Claude route the install displaced is forgotten and
|
|
872
|
-
* released at the adapter; its work was settled `ENDPOINT_RETIRED` by the
|
|
873
|
-
* store — and only a route whose own session is gone is ever displaced.
|
|
874
|
-
*/
|
|
875
|
-
async installClaudeRoute(candidate) {
|
|
876
|
-
const adapter = candidate.adapter;
|
|
877
|
-
if (adapter.selectRoute === undefined) {
|
|
878
|
-
throw new BridgeError("CLAUDE_PROVIDER_UNAVAILABLE", "The Claude provider cannot install routes.");
|
|
879
|
-
}
|
|
880
|
-
await adapter.assertWorkspaceDisjoint?.(candidate.routeHandle, this.store.rootDir);
|
|
881
|
-
const chosen = await adapter.selectRoute({ alias: candidate.alias, routeHandle: candidate.routeHandle });
|
|
882
|
-
this.assertWritable();
|
|
883
|
-
const before = (await this.store.listLogicalRoutes()).filter((route) => route.binding.provider === "claude");
|
|
884
|
-
const alias = this.claimableAlias(candidate, before);
|
|
885
|
-
const existing = before.find((route) => route.binding.hostId === adapter.identity.hostId &&
|
|
886
|
-
route.binding.routeHandle === chosen.routeHandle);
|
|
887
|
-
const input = {
|
|
888
|
-
alias,
|
|
889
|
-
binding: {
|
|
890
|
-
provider: "claude",
|
|
891
|
-
hostId: adapter.identity.hostId,
|
|
892
|
-
routeHandle: chosen.routeHandle,
|
|
893
|
-
registrationId: existing?.binding.registrationId ?? registrationId(),
|
|
894
|
-
},
|
|
895
|
-
registrationMode: "selected_live_peer",
|
|
896
|
-
};
|
|
897
|
-
const result = await this.installWithConcurrentFirstSend(input, adapter.identity.hostId, chosen.routeHandle);
|
|
898
|
-
await this.finishSettlements(result.settlements);
|
|
899
|
-
const after = (await this.store.listLogicalRoutes()).filter((route) => route.binding.provider === "claude");
|
|
900
|
-
for (const prior of before) {
|
|
901
|
-
const retained = after.find((route) => route.binding.registrationId === prior.binding.registrationId);
|
|
902
|
-
if (retained === undefined) {
|
|
903
|
-
// Displaced: the alias now names another session.
|
|
904
|
-
this.forgetRoute(prior);
|
|
905
|
-
void this.adapterFor(prior.binding)?.releaseRoute?.(prior.binding.routeHandle)
|
|
906
|
-
.catch((error) => this.alert("ROUTE_RELEASE_FAILED", prior, error));
|
|
907
|
-
}
|
|
908
|
-
else if (retained.alias !== prior.alias) {
|
|
909
|
-
// Renamed: identity kept, the stale display-only observation goes and
|
|
910
|
-
// open conversations follow the session to its new name.
|
|
911
|
-
this.routeObservations.delete(prior.alias);
|
|
912
|
-
this.renameConversationCoordinates(prior.alias, retained.alias, prior.binding.registrationId);
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
const installed = (await this.store.inspectPrivateRoute(alias));
|
|
916
|
-
this.observeRoute(installed);
|
|
917
|
-
this.routeObservations.set(alias, {
|
|
918
|
-
route: installed.binding,
|
|
919
|
-
state: chosen.state,
|
|
920
|
-
observedAt: this.now().toISOString(),
|
|
921
|
-
});
|
|
922
|
-
if (result.installed)
|
|
923
|
-
this.revision += 1;
|
|
924
|
-
this.kick(alias);
|
|
925
|
-
return installed;
|
|
926
|
-
}
|
|
927
|
-
/**
|
|
928
|
-
* Two first sends to the same never-installed session race: both read no
|
|
929
|
-
* route and both mint a registration, and the loser's write is refused
|
|
930
|
-
* because the identity is now bound. That is a race, not a conflict — the
|
|
931
|
-
* session is exactly the one the loser resolved — so re-read once and adopt
|
|
932
|
-
* the registration that won instead of failing an honest send.
|
|
933
|
-
*/
|
|
934
|
-
async installWithConcurrentFirstSend(input, hostId, routeHandle) {
|
|
935
|
-
try {
|
|
936
|
-
return await this.store.installClaudeRoute(input);
|
|
937
|
-
}
|
|
938
|
-
catch (error) {
|
|
939
|
-
if (!(error instanceof BridgeError) || error.code !== "ROUTE_IDENTITY_ALREADY_REGISTERED")
|
|
940
|
-
throw error;
|
|
941
|
-
const bound = (await this.store.listLogicalRoutes()).find((route) => route.binding.provider === "claude" &&
|
|
942
|
-
route.binding.hostId === hostId &&
|
|
943
|
-
route.binding.routeHandle === routeHandle);
|
|
944
|
-
if (bound === undefined)
|
|
945
|
-
throw error;
|
|
946
|
-
return await this.store.installClaudeRoute({
|
|
947
|
-
...input,
|
|
948
|
-
binding: { ...input.binding, registrationId: bound.binding.registrationId },
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
/**
|
|
953
|
-
* Looks a Claude selector up without writing anything. Callers that may
|
|
954
|
-
* still refuse — a `--from` alias that must match the sending session, a
|
|
955
|
-
* native sender whose adapter-reported name must match discovery — resolve
|
|
956
|
-
* here first, refuse on the read, and only then materialize.
|
|
957
|
-
*
|
|
958
|
-
* THE ALIAS-COLLISION FENCE lives here (emb-94, moved into the send path by
|
|
959
|
-
* emb-104). Invariant: discovery is refreshed inside every send that
|
|
960
|
-
* addresses a Claude session, never only on the timer; a NAME currently
|
|
961
|
-
* shared by more than one live session is refused with a hard,
|
|
962
|
-
* non-retryable `PEER_ALIAS_COLLISION` naming that alias, so the broker
|
|
963
|
-
* never picks first and never installs a route under an ambiguous name;
|
|
964
|
-
* collisions stay sticky while a scan is incomplete
|
|
965
|
-
* (`collidingClaudeAliases` retention in refreshClaudeDiscovery); and
|
|
966
|
-
* `availablePeers` keeps filtering colliders so `status` never lists an
|
|
967
|
-
* unaddressable name.
|
|
968
|
-
*
|
|
969
|
-
* The fence is a fence on NAMES, not on sessions. A session UUID is
|
|
970
|
-
* unambiguous by construction, so a UUID selector — an explicit
|
|
971
|
-
* `send --to <uuid>`, an identity-pinned route addressed by its own handle,
|
|
972
|
-
* or a sender resolved from its inherited socket — passes the fence even
|
|
973
|
-
* while its display name collides. That is the emb-94 escape hatch: the
|
|
974
|
-
* operator can always reach an ambiguously named session, and a collision on
|
|
975
|
-
* a *sender's* name never silences that sender, whose identity was attested
|
|
976
|
-
* rather than typed. In-flight conversations are likewise unaffected:
|
|
977
|
-
* `reply` resolves by exact binding.
|
|
978
|
-
*/
|
|
979
|
-
async lookUpClaudeRoute(selector, hostId) {
|
|
980
|
-
await this.refreshClaudeDiscovery().catch(() => undefined);
|
|
981
|
-
if (PUBLIC_ALIAS.test(selector) && this.collidingClaudeAliases.has(selector)) {
|
|
982
|
-
throw new BridgeError("PEER_ALIAS_COLLISION", `The Claude alias ${selector} names more than one live session; rename one, or address the session by UUID.`);
|
|
983
|
-
}
|
|
984
|
-
const routed = (await this.store.listLogicalRoutes()).find((route) => route.binding.provider === "claude" &&
|
|
985
|
-
(hostId === undefined || route.binding.hostId === hostId) &&
|
|
986
|
-
(route.alias === selector || route.binding.routeHandle === selector));
|
|
987
|
-
const candidate = [...this.candidates.values()].find((row) => (hostId === undefined || row.adapter.identity.hostId === hostId) &&
|
|
988
|
-
(row.routeHandle === selector || row.alias === selector));
|
|
989
|
-
return { routed, candidate };
|
|
990
|
-
}
|
|
991
|
-
/**
|
|
992
|
-
* The name discovery currently shows for a looked-up session, which a caller
|
|
993
|
-
* compares against the alias it claimed before anything is written.
|
|
994
|
-
*/
|
|
995
|
-
claimedAlias(resolution) {
|
|
996
|
-
const alias = resolution.candidate?.alias ?? resolution.routed?.alias;
|
|
997
|
-
if (alias === undefined) {
|
|
998
|
-
throw new BridgeError("CLAUDE_ROUTE_NOT_FOUND", "No live Claude session matches that name or UUID; run `embassy refresh` and check `embassy status`.");
|
|
999
|
-
}
|
|
1000
|
-
return alias;
|
|
1001
|
-
}
|
|
1002
|
-
/** Installs the looked-up session's route if it is not already current. */
|
|
1003
|
-
async materializeClaudeRoute(resolution) {
|
|
1004
|
-
const { routed, candidate } = resolution;
|
|
1005
|
-
if (candidate === undefined) {
|
|
1006
|
-
if (routed !== undefined)
|
|
1007
|
-
return routed;
|
|
1008
|
-
throw new BridgeError("CLAUDE_ROUTE_NOT_FOUND", "No live Claude session matches that name or UUID; run `embassy refresh` and check `embassy status`.");
|
|
1009
|
-
}
|
|
1010
|
-
if (routed !== undefined &&
|
|
1011
|
-
routed.binding.hostId === candidate.adapter.identity.hostId &&
|
|
1012
|
-
routed.binding.routeHandle === candidate.routeHandle &&
|
|
1013
|
-
routed.alias === this.claimableAlias(candidate, await this.store.listLogicalRoutes()))
|
|
1014
|
-
return routed;
|
|
1015
|
-
return await this.installClaudeRoute(candidate);
|
|
1016
|
-
}
|
|
1017
|
-
async resolveClaudeRoute(selector, hostId) {
|
|
1018
|
-
return await this.materializeClaudeRoute(await this.lookUpClaudeRoute(selector, hostId));
|
|
1019
|
-
}
|
|
1020
|
-
async assertThread(alias, threadId) {
|
|
1021
|
-
const route = await this.store.inspectPrivateRoute(alias);
|
|
1022
|
-
// No registration at all is its own condition — the state every Codex
|
|
1023
|
-
// task is in after a private state reset — and is never dressed up as a
|
|
1024
|
-
// thread mismatch, which would send the operator hunting a wrong id.
|
|
1025
|
-
if (route === undefined)
|
|
1026
|
-
throw new BridgeError("ROUTE_UNREGISTERED", "The route is not registered.");
|
|
1027
|
-
if (route.binding.provider !== "codex" || route.binding.routeHandle !== threadId) {
|
|
1028
|
-
throw new BridgeError("CODEX_THREAD_MISMATCH", "The task attestation does not match the route.");
|
|
1029
|
-
}
|
|
1030
|
-
return route;
|
|
1031
|
-
}
|
|
1032
|
-
/**
|
|
1033
|
-
* Direction follows the inherited principal, not the route table: a Claude
|
|
1034
|
-
* session or its target may have no route yet, because routes install on
|
|
1035
|
-
* first send. A Codex task sends to Claude sessions and shell peers; a
|
|
1036
|
-
* Claude session sends to Codex tasks and shell peers (Claude-to-Claude is
|
|
1037
|
-
* native); a shell peer sends to either.
|
|
1038
|
-
*/
|
|
1039
|
-
async send(params) {
|
|
1040
|
-
if (params.conversationId !== undefined)
|
|
1041
|
-
return await this.replyToConversation(params);
|
|
1042
|
-
const target = await this.store.inspectPrivateRoute(params.toAlias);
|
|
1043
|
-
if ("replyAddress" in params && params.replyAddress !== undefined) {
|
|
1044
|
-
// A UUID names a Claude session as surely as a name does: recognize it
|
|
1045
|
-
// here, or a Claude-to-Claude send addressed by UUID falls through to the
|
|
1046
|
-
// Codex path and is answered as an unknown target.
|
|
1047
|
-
const claudeTarget = target?.binding.provider === "claude" ||
|
|
1048
|
-
(target === undefined && [...this.candidates.values()].some((row) => row.alias === params.toAlias || row.routeHandle === params.toAlias));
|
|
1049
|
-
return claudeTarget ? { accepted: false, code: "route_mismatch" } : this.sendToCodex(params);
|
|
1050
|
-
}
|
|
1051
|
-
if ("threadId" in params && params.threadId !== undefined) {
|
|
1052
|
-
return target?.binding.provider === "codex" ? { accepted: false, code: "route_mismatch" } : this.sendToClaude(params);
|
|
1053
|
-
}
|
|
1054
|
-
return target === undefined || target.binding.provider === "claude" ? this.sendToClaude(params) : this.sendToCodex(params);
|
|
1055
|
-
}
|
|
1056
|
-
async sendToClaude(params) {
|
|
1057
|
-
try {
|
|
1058
|
-
this.assertWritable();
|
|
1059
|
-
const source = "peerToken" in params && params.peerToken !== undefined
|
|
1060
|
-
? await this.assertPeer({ alias: params.fromAlias, token: params.peerToken })
|
|
1061
|
-
: params.threadId === undefined
|
|
1062
|
-
? undefined
|
|
1063
|
-
: await this.assertThread(params.fromAlias, params.threadId);
|
|
1064
|
-
if (source === undefined)
|
|
1065
|
-
throw new BridgeError("ROUTE_NOT_AVAILABLE", "The source route is absent.");
|
|
1066
|
-
const direct = await this.store.inspectPrivateRoute(params.toAlias);
|
|
1067
|
-
let target;
|
|
1068
|
-
if (direct?.binding.provider === "peer") {
|
|
1069
|
-
target = direct;
|
|
1070
|
-
}
|
|
1071
|
-
else {
|
|
1072
|
-
const seen = await this.lookUpClaudeRoute(params.toAlias);
|
|
1073
|
-
// Admission before materialization: a send that is already going to be
|
|
1074
|
-
// refused for its own reasons — too large, outside the deadline
|
|
1075
|
-
// window, rate limited, or into a full queue — must not install a
|
|
1076
|
-
// route or displace another session on its way to that refusal.
|
|
1077
|
-
await this.store.assertEnqueueAdmissible({
|
|
1078
|
-
sourceAlias: params.fromAlias,
|
|
1079
|
-
targetAlias: this.claimedAlias(seen),
|
|
1080
|
-
direction: directionId(source.binding.provider, "claude"),
|
|
1081
|
-
sourceRegistrationId: source.binding.registrationId,
|
|
1082
|
-
body: params.text,
|
|
1083
|
-
});
|
|
1084
|
-
target = await this.materializeClaudeRoute(seen);
|
|
1085
|
-
}
|
|
1086
|
-
return await this.enqueueConversation({
|
|
1087
|
-
sourceAlias: params.fromAlias,
|
|
1088
|
-
targetAlias: target.alias,
|
|
1089
|
-
text: params.text,
|
|
1090
|
-
expectsReply: params.expectsReply,
|
|
1091
|
-
expectedSourceBinding: source.binding,
|
|
1092
|
-
expectedTargetBinding: target.binding,
|
|
1093
|
-
});
|
|
1094
|
-
}
|
|
1095
|
-
catch (error) {
|
|
1096
|
-
return sendRefusal(error, "peerToken" in params);
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
async sendToCodex(params) {
|
|
1100
|
-
try {
|
|
1101
|
-
this.assertWritable();
|
|
1102
|
-
let source;
|
|
1103
|
-
if ("peerToken" in params && params.peerToken !== undefined) {
|
|
1104
|
-
source = await this.assertPeer({ alias: params.fromAlias, token: params.peerToken });
|
|
1105
|
-
}
|
|
1106
|
-
else if (params.replyAddress === undefined) {
|
|
1107
|
-
throw new BridgeError("CLAUDE_REPLY_ADDRESS_INVALID", "The inherited reply capability is required.");
|
|
1108
|
-
}
|
|
1109
|
-
else {
|
|
1110
|
-
// The sending session's identity comes from its inherited socket, never
|
|
1111
|
-
// from --from. Look the session up read-only first: a send that is
|
|
1112
|
-
// about to be refused must not install, rename, or displace a route,
|
|
1113
|
-
// and must not settle another session's queued work.
|
|
1114
|
-
const resolved = await this.claudeAdapter(aliasHost(params.fromAlias))?.resolveReplyAddress?.(params.replyAddress);
|
|
1115
|
-
if (resolved === undefined)
|
|
1116
|
-
throw new BridgeError("CLAUDE_REPLY_ADDRESS_INVALID", "The reply capability is stale.");
|
|
1117
|
-
const seen = await this.lookUpClaudeRoute(resolved.routeHandle, aliasHost(params.fromAlias));
|
|
1118
|
-
if (this.claimedAlias(seen) !== params.fromAlias) {
|
|
1119
|
-
throw new BridgeError("CLAUDE_ROUTE_MISMATCH", "The --from alias does not name the sending Claude session.");
|
|
1120
|
-
}
|
|
1121
|
-
source = await this.materializeClaudeRoute(seen);
|
|
1122
|
-
}
|
|
1123
|
-
const target = await this.store.inspectPrivateRoute(params.toAlias);
|
|
1124
|
-
if (source === undefined || target === undefined) {
|
|
1125
|
-
throw new BridgeError("ROUTE_NOT_AVAILABLE", "The selected route is absent.");
|
|
1126
|
-
}
|
|
1127
|
-
if (target.binding.provider !== "codex" && target.binding.provider !== "peer") {
|
|
1128
|
-
throw new BridgeError("ROUTE_NOT_AVAILABLE", "The selected route is absent.");
|
|
1129
|
-
}
|
|
1130
|
-
return await this.enqueueConversation({
|
|
1131
|
-
sourceAlias: params.fromAlias,
|
|
1132
|
-
targetAlias: params.toAlias,
|
|
1133
|
-
text: params.text,
|
|
1134
|
-
expectsReply: params.expectsReply,
|
|
1135
|
-
expectedSourceBinding: source.binding,
|
|
1136
|
-
expectedTargetBinding: target.binding,
|
|
1137
|
-
});
|
|
1138
|
-
}
|
|
1139
|
-
catch (error) {
|
|
1140
|
-
return sendRefusal(error, "peerToken" in params);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
/**
|
|
1144
|
-
* A send addressed by conversation token: the caller must own one end, the
|
|
1145
|
-
* other end is the binding the conversation recorded, and the caller's
|
|
1146
|
-
* principal is the same attested one every send carries.
|
|
1147
|
-
*/
|
|
1148
|
-
async replyToConversation(params) {
|
|
1149
|
-
try {
|
|
1150
|
-
this.assertWritable();
|
|
1151
|
-
const conversation = this.conversations.get(params.conversationId);
|
|
1152
|
-
if (conversation === undefined)
|
|
1153
|
-
throw new BridgeError("CONVERSATION_NOT_FOUND", "The conversation is absent.");
|
|
1154
|
-
const caller = replyCaller(params);
|
|
1155
|
-
const callerBinding = await this.assertReplyCaller(conversation, caller);
|
|
1156
|
-
const callerAlias = caller.alias;
|
|
1157
|
-
const targetAlias = callerAlias === conversation.sourceAlias
|
|
1158
|
-
? conversation.targetAlias
|
|
1159
|
-
: conversation.sourceAlias;
|
|
1160
|
-
const targetBinding = callerAlias === conversation.sourceAlias
|
|
1161
|
-
? conversation.targetBinding
|
|
1162
|
-
: conversation.sourceBinding;
|
|
1163
|
-
if (targetBinding === undefined) {
|
|
1164
|
-
throw new BridgeError("CONVERSATION_ROUTE_RETIRED", "The conversation endpoint is no longer available.");
|
|
1165
|
-
}
|
|
1166
|
-
// The target's route was installed when it first sent or was first
|
|
1167
|
-
// sent to; a reply never re-resolves it by alias. A retired binding is
|
|
1168
|
-
// a refusal at enqueue, never a silent retarget.
|
|
1169
|
-
return await this.enqueueConversation({
|
|
1170
|
-
sourceAlias: callerAlias,
|
|
1171
|
-
targetAlias,
|
|
1172
|
-
text: params.text,
|
|
1173
|
-
expectsReply: true,
|
|
1174
|
-
expectedSourceBinding: callerBinding,
|
|
1175
|
-
expectedTargetBinding: targetBinding,
|
|
1176
|
-
existingConversation: conversation,
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
|
-
catch (error) {
|
|
1180
|
-
return sendRefusal(error, params.peerToken !== undefined);
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
async assertReplyCaller(conversation, caller) {
|
|
1184
|
-
if (caller.alias !== conversation.sourceAlias && caller.alias !== conversation.targetAlias) {
|
|
1185
|
-
throw new BridgeError("CONVERSATION_CALLER_MISMATCH", "The caller does not own this conversation.");
|
|
1186
|
-
}
|
|
1187
|
-
const expected = caller.alias === conversation.sourceAlias
|
|
1188
|
-
? conversation.sourceBinding
|
|
1189
|
-
: conversation.targetBinding;
|
|
1190
|
-
const route = await this.store.inspectPrivateRoute(caller.alias);
|
|
1191
|
-
if (expected === undefined || route === undefined || !sameBinding(route.binding, expected)) {
|
|
1192
|
-
throw new BridgeError("CONVERSATION_ROUTE_RETIRED", "The conversation endpoint is no longer available.");
|
|
1193
|
-
}
|
|
1194
|
-
if (caller.kind === "codex" &&
|
|
1195
|
-
(route.binding.provider !== "codex" || route.binding.routeHandle !== caller.threadId)) {
|
|
1196
|
-
throw new BridgeError("CODEX_THREAD_MISMATCH", "The task attestation does not match the conversation.");
|
|
1197
|
-
}
|
|
1198
|
-
if (caller.kind === "claude") {
|
|
1199
|
-
if (route.binding.provider !== "claude") {
|
|
1200
|
-
throw new BridgeError("CLAUDE_REPLY_ADDRESS_INVALID", "The inherited reply capability is required.");
|
|
1201
|
-
}
|
|
1202
|
-
const resolved = await this.claudeAdapter(route.binding.hostId)
|
|
1203
|
-
?.resolveReplyAddress?.(caller.replyAddress);
|
|
1204
|
-
if (resolved?.routeHandle !== route.binding.routeHandle) {
|
|
1205
|
-
throw new BridgeError("CLAUDE_REPLY_ADDRESS_INVALID", "The reply capability is stale.");
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
if (caller.kind === "peer")
|
|
1209
|
-
await this.assertPeer({ alias: caller.alias, token: caller.token });
|
|
1210
|
-
return route.binding;
|
|
1211
|
-
}
|
|
1212
|
-
async enqueueConversation(input) {
|
|
1213
|
-
const target = await this.store.inspectPrivateRoute(input.targetAlias);
|
|
1214
|
-
const source = await this.store.inspectPrivateRoute(input.sourceAlias);
|
|
1215
|
-
const conversationSourceBinding = input.existingConversation === undefined
|
|
1216
|
-
? undefined
|
|
1217
|
-
: input.sourceAlias === input.existingConversation.sourceAlias
|
|
1218
|
-
? input.existingConversation.sourceBinding
|
|
1219
|
-
: input.sourceAlias === input.existingConversation.targetAlias
|
|
1220
|
-
? input.existingConversation.targetBinding
|
|
1221
|
-
: undefined;
|
|
1222
|
-
const conversationTargetBinding = input.existingConversation === undefined
|
|
1223
|
-
? undefined
|
|
1224
|
-
: input.targetAlias === input.existingConversation.sourceAlias
|
|
1225
|
-
? input.existingConversation.sourceBinding
|
|
1226
|
-
: input.targetAlias === input.existingConversation.targetAlias
|
|
1227
|
-
? input.existingConversation.targetBinding
|
|
1228
|
-
: undefined;
|
|
1229
|
-
const sourceBinding = input.expectedSourceBinding ?? conversationSourceBinding ?? source?.binding;
|
|
1230
|
-
const targetBinding = input.expectedTargetBinding ?? conversationTargetBinding ?? target?.binding;
|
|
1231
|
-
const steer = input.text.startsWith("STEER:") && this.config.steeringEnabled &&
|
|
1232
|
-
sourceBinding?.provider === "claude" && targetBinding?.provider === "codex";
|
|
1233
|
-
if (input.existingConversation === undefined &&
|
|
1234
|
-
this.conversations.size >= MAX_CONVERSATIONS &&
|
|
1235
|
-
[...this.conversations.keys()].every((id) => this.conversationIsActive(id))) {
|
|
1236
|
-
throw new BridgeError("CONVERSATION_CAPACITY_REACHED", "Conversation capacity is full.");
|
|
1237
|
-
}
|
|
1238
|
-
const conversation = input.existingConversation ?? {
|
|
1239
|
-
id: createGatewayConversationId(),
|
|
1240
|
-
sourceAlias: input.sourceAlias,
|
|
1241
|
-
targetAlias: input.targetAlias,
|
|
1242
|
-
...(sourceBinding === undefined ? {} : { sourceBinding: { ...sourceBinding } }),
|
|
1243
|
-
...(targetBinding === undefined ? {} : { targetBinding: { ...targetBinding } }),
|
|
1244
|
-
expectsReply: input.expectsReply,
|
|
1245
|
-
nextSequence: 0,
|
|
1246
|
-
};
|
|
1247
|
-
const sequence = conversation.nextSequence++;
|
|
1248
|
-
let enqueued;
|
|
1249
|
-
try {
|
|
1250
|
-
this.assertWritable();
|
|
1251
|
-
enqueued = await this.store.enqueueMessage({
|
|
1252
|
-
sourceAlias: input.sourceAlias,
|
|
1253
|
-
targetAlias: input.targetAlias,
|
|
1254
|
-
body: input.text,
|
|
1255
|
-
dedupeKey: `${conversation.id}:${sequence}`,
|
|
1256
|
-
conversationIdSuffix: conversation.id.slice(-8),
|
|
1257
|
-
...(sourceBinding === undefined || targetBinding === undefined
|
|
1258
|
-
? {}
|
|
1259
|
-
: {
|
|
1260
|
-
expectedSourceRegistrationId: sourceBinding.registrationId,
|
|
1261
|
-
expectedTargetRegistrationId: targetBinding.registrationId,
|
|
1262
|
-
}),
|
|
1263
|
-
...(steer ? { steer: true } : {}),
|
|
1264
|
-
});
|
|
1265
|
-
}
|
|
1266
|
-
catch (error) {
|
|
1267
|
-
conversation.nextSequence -= 1;
|
|
1268
|
-
throw error;
|
|
1269
|
-
}
|
|
1270
|
-
if (!enqueued.accepted || enqueued.messageId === undefined || enqueued.deliveryToken === undefined) {
|
|
1271
|
-
conversation.nextSequence -= 1;
|
|
1272
|
-
throw new BridgeError("MESSAGE_NOT_ACCEPTED", "The message was not accepted.");
|
|
1273
|
-
}
|
|
1274
|
-
this.rememberConversation(conversation);
|
|
1275
|
-
this.messageContexts.set(enqueued.messageId, {
|
|
1276
|
-
conversationId: conversation.id,
|
|
1277
|
-
expectsReply: input.expectsReply,
|
|
1278
|
-
});
|
|
1279
|
-
if (enqueued.supersededSettlement !== undefined) {
|
|
1280
|
-
await this.finishSettlement(enqueued.supersededSettlement);
|
|
1281
|
-
}
|
|
1282
|
-
if (steer)
|
|
1283
|
-
this.kickSteer(input.targetAlias);
|
|
1284
|
-
this.kick(input.targetAlias);
|
|
1285
|
-
this.scheduleWake();
|
|
1286
|
-
return {
|
|
1287
|
-
accepted: true,
|
|
1288
|
-
code: "ok",
|
|
1289
|
-
conversationId: conversation.id,
|
|
1290
|
-
deliveryToken: enqueued.deliveryToken,
|
|
1291
|
-
};
|
|
1292
|
-
}
|
|
1293
|
-
/**
|
|
1294
|
-
* A renamed Claude session is the same principal under a new display name,
|
|
1295
|
-
* so its open conversations follow it: the registration, not the alias, is
|
|
1296
|
-
* the identity, and a reply under the current name must still resolve.
|
|
1297
|
-
*/
|
|
1298
|
-
renameConversationCoordinates(oldAlias, newAlias, registrationId) {
|
|
1299
|
-
for (const conversation of this.conversations.values()) {
|
|
1300
|
-
if (conversation.sourceAlias === oldAlias &&
|
|
1301
|
-
conversation.sourceBinding?.registrationId === registrationId) {
|
|
1302
|
-
conversation.sourceAlias = newAlias;
|
|
1303
|
-
}
|
|
1304
|
-
if (conversation.targetAlias === oldAlias &&
|
|
1305
|
-
conversation.targetBinding?.registrationId === registrationId) {
|
|
1306
|
-
conversation.targetAlias = newAlias;
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
rememberConversation(conversation) {
|
|
1311
|
-
this.conversations.delete(conversation.id);
|
|
1312
|
-
this.conversations.set(conversation.id, conversation);
|
|
1313
|
-
while (this.conversations.size > MAX_CONVERSATIONS) {
|
|
1314
|
-
const oldest = [...this.conversations.keys()].find((id) => id !== conversation.id && !this.conversationIsActive(id));
|
|
1315
|
-
if (oldest === undefined)
|
|
1316
|
-
break;
|
|
1317
|
-
this.conversations.delete(oldest);
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
conversationIsActive(conversationId) {
|
|
1321
|
-
if ([...this.messageContexts.values()].some((row) => row.conversationId === conversationId)) {
|
|
1322
|
-
return true;
|
|
1323
|
-
}
|
|
1324
|
-
return [...this.pendingClaudeReplies.values()].some((rows) => rows.some((row) => row.conversationId === conversationId));
|
|
1325
|
-
}
|
|
1326
|
-
async deliveryStatus(token) {
|
|
1327
|
-
const message = await this.store.deliveryStatus(token);
|
|
1328
|
-
if (message === undefined)
|
|
1329
|
-
return { found: false };
|
|
1330
|
-
const state = message.state.phase === "terminal"
|
|
1331
|
-
? message.state.outcome === "abandoned"
|
|
1332
|
-
? "failed"
|
|
1333
|
-
: message.state.outcome
|
|
1334
|
-
: message.state.phase === "queued"
|
|
1335
|
-
? "queued"
|
|
1336
|
-
: "stalled";
|
|
1337
|
-
return {
|
|
1338
|
-
found: true,
|
|
1339
|
-
state,
|
|
1340
|
-
terminal: message.state.phase === "terminal",
|
|
1341
|
-
updatedAt: this.messageUpdatedAt(message),
|
|
1342
|
-
deadlineAt: message.deadlineAt,
|
|
1343
|
-
...(message.state.phase === "terminal"
|
|
1344
|
-
? message.state.safeErrorCode === undefined
|
|
1345
|
-
? {}
|
|
1346
|
-
: { safeErrorCode: message.state.safeErrorCode }
|
|
1347
|
-
: { pendingForMs: Math.max(0, this.now().getTime() - Date.parse(message.enqueuedAt)) }),
|
|
1348
|
-
};
|
|
1349
|
-
}
|
|
1350
|
-
messageUpdatedAt(message) {
|
|
1351
|
-
switch (message.state.phase) {
|
|
1352
|
-
case "queued": return message.enqueuedAt;
|
|
1353
|
-
case "reserved": return message.state.reservedAt;
|
|
1354
|
-
case "armed": return message.state.armedAt;
|
|
1355
|
-
case "accepted": return message.state.acceptedAt;
|
|
1356
|
-
case "terminal": return message.state.terminalAt;
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
kick(targetAlias, preferredRegistrationId) {
|
|
1360
|
-
const startingKey = `${targetAlias}\0${preferredRegistrationId ?? "current"}`;
|
|
1361
|
-
if (!this.running || this.closing || this.startingTargets.has(startingKey))
|
|
1362
|
-
return;
|
|
1363
|
-
this.startingTargets.add(startingKey);
|
|
1364
|
-
void this.kickExact(targetAlias, preferredRegistrationId)
|
|
1365
|
-
.catch((error) => this.alert("DISPATCH_RUNNER_FAILED", undefined, error))
|
|
1366
|
-
.finally(() => this.startingTargets.delete(startingKey));
|
|
1367
|
-
}
|
|
1368
|
-
async kickExact(targetAlias, preferredRegistrationId) {
|
|
1369
|
-
if (!this.running || this.closing)
|
|
1370
|
-
return;
|
|
1371
|
-
const route = await this.store.inspectPrivateRoute(targetAlias);
|
|
1372
|
-
const registration = preferredRegistrationId ?? route?.binding.registrationId;
|
|
1373
|
-
if (registration === undefined)
|
|
1374
|
-
return;
|
|
1375
|
-
this.startRunner(targetAlias, registration, "any");
|
|
1376
|
-
}
|
|
1377
|
-
kickSteer(targetAlias) {
|
|
1378
|
-
if (!this.running || this.closing)
|
|
1379
|
-
return;
|
|
1380
|
-
void this.store.inspectPrivateRoute(targetAlias).then((route) => {
|
|
1381
|
-
if (route === undefined || route.binding.provider !== "codex")
|
|
1382
|
-
return;
|
|
1383
|
-
this.startRunner(targetAlias, route.binding.registrationId, "steer_only", route);
|
|
1384
|
-
}).catch((error) => this.alert("STEER_RUNNER_FAILED", undefined, error));
|
|
1385
|
-
}
|
|
1386
|
-
startRunner(targetAlias, registrationId, mode, route) {
|
|
1387
|
-
const steer = mode === "steer_only";
|
|
1388
|
-
const runners = steer ? this.steerRunners : this.dispatchRunners;
|
|
1389
|
-
const key = `${targetAlias}\0${registrationId}${steer ? "\0steer" : ""}`;
|
|
1390
|
-
if (runners.has(key))
|
|
1391
|
-
return;
|
|
1392
|
-
let retry = false;
|
|
1393
|
-
const runner = this.runTarget(targetAlias, registrationId, mode)
|
|
1394
|
-
.then((value) => { retry = value; })
|
|
1395
|
-
.finally(() => {
|
|
1396
|
-
if (runners.get(key) !== runner)
|
|
1397
|
-
return;
|
|
1398
|
-
runners.delete(key);
|
|
1399
|
-
if (retry)
|
|
1400
|
-
this.scheduleTargetRetry(targetAlias, registrationId, steer);
|
|
1401
|
-
else if (steer && this.running && !this.closing)
|
|
1402
|
-
this.kickSteer(targetAlias);
|
|
1403
|
-
else if (this.running)
|
|
1404
|
-
void Promise.all([this.store.inspectDispatchableTargets(), this.store.inspectPrivateRoute(targetAlias)]).then(([targets, route]) => {
|
|
1405
|
-
if (targets.includes(targetAlias) &&
|
|
1406
|
-
(route?.registrationMode !== "federated_peer" || this.peerClients.has(route.binding.hostId)))
|
|
1407
|
-
this.kick(targetAlias);
|
|
1408
|
-
}).catch(() => undefined);
|
|
1409
|
-
});
|
|
1410
|
-
runners.set(key, runner);
|
|
1411
|
-
void runner.catch((error) => this.alert(steer ? "STEER_RUNNER_FAILED" : "DISPATCH_RUNNER_FAILED", route, error));
|
|
1412
|
-
}
|
|
1413
|
-
async runTarget(targetAlias, runnerRegistrationId, mode) {
|
|
1414
|
-
while (this.running && !this.closing) {
|
|
1415
|
-
let reserved;
|
|
1416
|
-
const reserveOperation = this.store.reserveMessage(targetAlias, mode).then((result) => {
|
|
1417
|
-
reserved = result;
|
|
1418
|
-
if (result.status === "reserved") {
|
|
1419
|
-
this.activeAttempts.set(result.attempt.messageId, {
|
|
1420
|
-
messageId: result.attempt.messageId,
|
|
1421
|
-
attemptId: result.attempt.attemptId,
|
|
1422
|
-
});
|
|
1423
|
-
}
|
|
1424
|
-
});
|
|
1425
|
-
this.reserveOperations.add(reserveOperation);
|
|
1426
|
-
try {
|
|
1427
|
-
await reserveOperation;
|
|
1428
|
-
}
|
|
1429
|
-
finally {
|
|
1430
|
-
this.reserveOperations.delete(reserveOperation);
|
|
1431
|
-
}
|
|
1432
|
-
if (reserved === undefined)
|
|
1433
|
-
throw new RangeError("RESERVE_RESULT_MISSING");
|
|
1434
|
-
if (reserved.status === "empty")
|
|
1435
|
-
return false;
|
|
1436
|
-
if (reserved.status === "terminal") {
|
|
1437
|
-
await this.finishSettlement(reserved.settlement);
|
|
1438
|
-
return false;
|
|
1439
|
-
}
|
|
1440
|
-
const attempt = reserved.attempt;
|
|
1441
|
-
if (this.closing || !this.running) {
|
|
1442
|
-
await this.settleAttemptForShutdown(attempt.messageId, attempt.attemptId);
|
|
1443
|
-
return false;
|
|
1444
|
-
}
|
|
1445
|
-
if (attempt.targetRegistrationId !== runnerRegistrationId) {
|
|
1446
|
-
await this.resolvePrewrite(attempt.messageId, attempt.attemptId, "requeue", "ENDPOINT_GENERATION_CHANGED");
|
|
1447
|
-
return false;
|
|
1448
|
-
}
|
|
1449
|
-
const messageContext = this.messageContexts.get(attempt.messageId);
|
|
1450
|
-
const conversation = messageContext === undefined
|
|
1451
|
-
? undefined
|
|
1452
|
-
: this.conversations.get(messageContext.conversationId);
|
|
1453
|
-
// Resolve by registration, not by the alias captured at reserve: a
|
|
1454
|
-
// session renamed between reserve and dispatch is the same route and
|
|
1455
|
-
// must still be delivered to, under whatever name it carries now.
|
|
1456
|
-
const installed = await this.store.listLogicalRoutes();
|
|
1457
|
-
const byRegistration = (registration, alias) => installed.find((route) => route.binding.registrationId === registration) ??
|
|
1458
|
-
installed.find((route) => route.alias === alias);
|
|
1459
|
-
const target = byRegistration(attempt.targetRegistrationId, attempt.targetAlias);
|
|
1460
|
-
const source = byRegistration(attempt.sourceRegistrationId, attempt.sourceAlias);
|
|
1461
|
-
if (this.closing || !this.running) {
|
|
1462
|
-
await this.settleAttemptForShutdown(attempt.messageId, attempt.attemptId);
|
|
1463
|
-
return false;
|
|
1464
|
-
}
|
|
1465
|
-
if (target === undefined ||
|
|
1466
|
-
target.binding.registrationId !== attempt.targetRegistrationId ||
|
|
1467
|
-
source?.binding.registrationId !== attempt.sourceRegistrationId) {
|
|
1468
|
-
await this.resolvePrewrite(attempt.messageId, attempt.attemptId, "failed", "ROUTE_UNREGISTERED");
|
|
1469
|
-
return false;
|
|
1470
|
-
}
|
|
1471
|
-
if (target.registrationMode === "federated_peer") {
|
|
1472
|
-
const peer = this.peerClients.get(target.binding.hostId);
|
|
1473
|
-
if (peer === undefined || source.registrationMode === "federated_peer") {
|
|
1474
|
-
await this.resolvePrewrite(attempt.messageId, attempt.attemptId, "requeue", "PEER_TUNNEL_UNAVAILABLE");
|
|
1475
|
-
return false;
|
|
1476
|
-
}
|
|
1477
|
-
const sourceEndpoint = { alias: source.alias, provider: source.binding.provider,
|
|
1478
|
-
host: source.binding.hostId, routeRef: peerRouteRef(source.binding.hostId, source.binding.registrationId) };
|
|
1479
|
-
const targetEndpoint = { alias: target.alias, provider: target.binding.provider,
|
|
1480
|
-
host: target.binding.hostId, routeRef: target.binding.routeHandle };
|
|
1481
|
-
const params = {
|
|
1482
|
-
originAttemptId: attempt.attemptId, originMessageId: attempt.messageId,
|
|
1483
|
-
source: sourceEndpoint, target: targetEndpoint,
|
|
1484
|
-
deadlineAt: attempt.deadlineAt, expectsReply: conversation?.expectsReply ?? true,
|
|
1485
|
-
body: attempt.body,
|
|
1486
|
-
...(attempt.steer === true ? { steer: true } : {}),
|
|
1487
|
-
...(attempt.conversationIdSuffix === undefined ? {} : { conversationCorrelation: attempt.conversationIdSuffix }),
|
|
1488
|
-
};
|
|
1489
|
-
let armed = false, authorizationUncertain = false, acceptanceObserved = false, result;
|
|
1490
|
-
try {
|
|
1491
|
-
const prepared = peer.prepareHandoff(params);
|
|
1492
|
-
let authorized;
|
|
1493
|
-
try {
|
|
1494
|
-
authorized = await this.store.authorizeMessage({ messageId: attempt.messageId,
|
|
1495
|
-
attemptId: attempt.attemptId, sourceRegistrationId: attempt.sourceRegistrationId,
|
|
1496
|
-
targetRegistrationId: attempt.targetRegistrationId,
|
|
1497
|
-
prepared: { kind: "peer_handoff", bodyBytes: prepared.bodyBytes,
|
|
1498
|
-
bodySha256: prepared.bodySha256, frameBytes: prepared.frameBytes, sha256: prepared.sha256 } });
|
|
1499
|
-
}
|
|
1500
|
-
catch (error) {
|
|
1501
|
-
authorizationUncertain = true;
|
|
1502
|
-
throw error;
|
|
1503
|
-
}
|
|
1504
|
-
if (authorized.status !== "authorized") {
|
|
1505
|
-
prepared.cancel();
|
|
1506
|
-
if (authorized.status === "terminal")
|
|
1507
|
-
await this.finishSettlement(authorized.settlement);
|
|
1508
|
-
this.activeAttempts.delete(attempt.messageId);
|
|
1509
|
-
return false;
|
|
1510
|
-
}
|
|
1511
|
-
armed = true;
|
|
1512
|
-
await prepared.perform();
|
|
1513
|
-
acceptanceObserved = true;
|
|
1514
|
-
const accepted = await this.store.acceptMessage({ messageId: attempt.messageId,
|
|
1515
|
-
attemptId: attempt.attemptId, lossOutcome: "unconfirmed" });
|
|
1516
|
-
if (accepted.status !== "accepted")
|
|
1517
|
-
throw new BridgeError("ACCEPTANCE_UNCONFIRMED", "The peer acceptance fence is stale.");
|
|
1518
|
-
result = { state: "delivered", safeErrorCode: "PEER_HANDOFF_CONFIRMED" };
|
|
1519
|
-
}
|
|
1520
|
-
catch {
|
|
1521
|
-
if (!armed && !authorizationUncertain) {
|
|
1522
|
-
peer.close();
|
|
1523
|
-
this.peerClients.delete(target.binding.hostId);
|
|
1524
|
-
await this.resolvePrewrite(attempt.messageId, attempt.attemptId, "requeue", "PEER_TUNNEL_UNAVAILABLE");
|
|
1525
|
-
return false;
|
|
1526
|
-
}
|
|
1527
|
-
result = acceptanceObserved
|
|
1528
|
-
? { state: "unconfirmed", safeErrorCode: "PEER_HANDOFF_ACCEPTANCE_UNCONFIRMED" }
|
|
1529
|
-
: { state: "ambiguous", safeErrorCode: authorizationUncertain ? "WRITE_AUTHORIZATION_UNCERTAIN" : "PEER_HANDOFF_OUTCOME_UNKNOWN" };
|
|
1530
|
-
}
|
|
1531
|
-
const requeued = await this.applyDispatchResult(attempt.messageId, attempt.attemptId, result, armed, conversation, attempt.sourceAlias, attempt.targetAlias);
|
|
1532
|
-
this.activeAttempts.delete(attempt.messageId);
|
|
1533
|
-
return requeued;
|
|
1534
|
-
}
|
|
1535
|
-
const adapter = this.adapterFor(target.binding);
|
|
1536
|
-
if (adapter === undefined) {
|
|
1537
|
-
await this.resolvePrewrite(attempt.messageId, attempt.attemptId, "failed", "PROVIDER_UNAVAILABLE");
|
|
1538
|
-
return false;
|
|
1539
|
-
}
|
|
1540
|
-
const active = this.activeAttempts.get(attempt.messageId);
|
|
1541
|
-
let authorizationUncertain = false;
|
|
1542
|
-
let armed = false;
|
|
1543
|
-
let accepted = false;
|
|
1544
|
-
const conversationId = messageContext?.conversationId ??
|
|
1545
|
-
conversationIdForSuffix(attempt.conversationIdSuffix);
|
|
1546
|
-
const parsed = parseDirection(attempt.direction);
|
|
1547
|
-
if (target.binding.provider === "claude" &&
|
|
1548
|
-
messageContext?.expectsReply === true &&
|
|
1549
|
-
![...this.pendingClaudeReplies.values()].some((rows) => rows.some((row) => row.messageId === attempt.messageId)) &&
|
|
1550
|
-
[...this.pendingClaudeReplies.values()].reduce((count, rows) => count + rows.length, 0) >= MAX_PENDING_CLAUDE_REPLIES) {
|
|
1551
|
-
await this.resolvePrewrite(attempt.messageId, attempt.attemptId, "requeue", "ROUTE_BUSY");
|
|
1552
|
-
return true;
|
|
1553
|
-
}
|
|
1554
|
-
let result;
|
|
1555
|
-
try {
|
|
1556
|
-
result = await adapter.dispatch({
|
|
1557
|
-
attemptId: attempt.attemptId,
|
|
1558
|
-
sourceAlias: attempt.sourceAlias,
|
|
1559
|
-
sourceProvider: parsed.sourceProvider,
|
|
1560
|
-
// The route's CURRENT alias, not the one captured at reserve. The
|
|
1561
|
-
// provider adapters compare this against the alias they hold for the
|
|
1562
|
-
// binding, so a session renamed between reserve and dispatch would
|
|
1563
|
-
// otherwise fail its own delivery over a stale display name.
|
|
1564
|
-
targetAlias: target.alias,
|
|
1565
|
-
conversationId,
|
|
1566
|
-
binding: target.binding,
|
|
1567
|
-
authorization: "selected_route",
|
|
1568
|
-
messageId: attempt.messageId,
|
|
1569
|
-
text: attempt.body,
|
|
1570
|
-
expectsReply: conversation?.expectsReply ?? true,
|
|
1571
|
-
deadlineAt: attempt.deadlineAt,
|
|
1572
|
-
...(attempt.steer === true ? { steer: true, queuedAhead: 0 } : {}),
|
|
1573
|
-
authorizeWrite: async (evidence) => {
|
|
1574
|
-
if (evidence.attemptId !== attempt.attemptId)
|
|
1575
|
-
return false;
|
|
1576
|
-
try {
|
|
1577
|
-
const authorized = await this.store.authorizeMessage({
|
|
1578
|
-
messageId: attempt.messageId,
|
|
1579
|
-
attemptId: attempt.attemptId,
|
|
1580
|
-
sourceRegistrationId: attempt.sourceRegistrationId,
|
|
1581
|
-
targetRegistrationId: attempt.targetRegistrationId,
|
|
1582
|
-
prepared: {
|
|
1583
|
-
kind: evidence.kind,
|
|
1584
|
-
bodyBytes: evidence.bodyBytes,
|
|
1585
|
-
bodySha256: evidence.bodySha256,
|
|
1586
|
-
frameBytes: evidence.frameBytes,
|
|
1587
|
-
sha256: evidence.sha256,
|
|
1588
|
-
},
|
|
1589
|
-
});
|
|
1590
|
-
if (authorized.status === "terminal") {
|
|
1591
|
-
await this.finishSettlement(authorized.settlement);
|
|
1592
|
-
}
|
|
1593
|
-
if (authorized.status === "authorized") {
|
|
1594
|
-
armed = true;
|
|
1595
|
-
if (target.binding.provider === "claude" &&
|
|
1596
|
-
messageContext?.expectsReply === true) {
|
|
1597
|
-
this.installPendingClaudeReply({
|
|
1598
|
-
messageId: attempt.messageId,
|
|
1599
|
-
conversationId,
|
|
1600
|
-
sourceAlias: attempt.sourceAlias,
|
|
1601
|
-
targetAlias: attempt.targetAlias,
|
|
1602
|
-
sourceBinding: source.binding,
|
|
1603
|
-
targetBinding: target.binding,
|
|
1604
|
-
deadlineAt: attempt.deadlineAt,
|
|
1605
|
-
state: "armed",
|
|
1606
|
-
});
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
return authorized.status === "authorized";
|
|
1610
|
-
}
|
|
1611
|
-
catch (error) {
|
|
1612
|
-
authorizationUncertain = true;
|
|
1613
|
-
throw error;
|
|
1614
|
-
}
|
|
1615
|
-
},
|
|
1616
|
-
onAccepted: async (evidence) => {
|
|
1617
|
-
if ((target.binding.provider !== "codex" && target.binding.provider !== "peer") ||
|
|
1618
|
-
evidence.attemptId !== attempt.attemptId) {
|
|
1619
|
-
throw new BridgeError("ACCEPTANCE_UNCONFIRMED", "The provider accepted another attempt.");
|
|
1620
|
-
}
|
|
1621
|
-
const result = await this.store.acceptMessage({
|
|
1622
|
-
messageId: attempt.messageId,
|
|
1623
|
-
attemptId: attempt.attemptId,
|
|
1624
|
-
lossOutcome: attempt.steer === true ? "ambiguous" : "unconfirmed",
|
|
1625
|
-
});
|
|
1626
|
-
if (result.status !== "accepted") {
|
|
1627
|
-
throw new BridgeError("ACCEPTANCE_UNCONFIRMED", "The durable acceptance fence is stale.");
|
|
1628
|
-
}
|
|
1629
|
-
accepted = true;
|
|
1630
|
-
},
|
|
1631
|
-
});
|
|
1632
|
-
}
|
|
1633
|
-
catch {
|
|
1634
|
-
result = {
|
|
1635
|
-
state: accepted
|
|
1636
|
-
? attempt.steer === true
|
|
1637
|
-
? "ambiguous"
|
|
1638
|
-
: "unconfirmed"
|
|
1639
|
-
: armed || authorizationUncertain
|
|
1640
|
-
? "ambiguous"
|
|
1641
|
-
: "failed",
|
|
1642
|
-
safeErrorCode: accepted
|
|
1643
|
-
? "DELIVERY_UNCONFIRMED"
|
|
1644
|
-
: authorizationUncertain || armed
|
|
1645
|
-
? "WRITE_AUTHORIZATION_UNCERTAIN"
|
|
1646
|
-
: "PROVIDER_DISPATCH_FAILED",
|
|
1647
|
-
};
|
|
1648
|
-
}
|
|
1649
|
-
const requeued = await this.applyDispatchResult(attempt.messageId, attempt.attemptId, result, armed, conversation, attempt.sourceAlias, attempt.targetAlias);
|
|
1650
|
-
if (this.activeAttempts.get(attempt.messageId) === active)
|
|
1651
|
-
this.activeAttempts.delete(attempt.messageId);
|
|
1652
|
-
return requeued;
|
|
1653
|
-
}
|
|
1654
|
-
return false;
|
|
1655
|
-
}
|
|
1656
|
-
async settleAttemptForShutdown(messageId, attemptId) {
|
|
1657
|
-
const result = await this.store.settleAttemptForShutdown({ messageId, attemptId });
|
|
1658
|
-
if (result.status === "settled")
|
|
1659
|
-
await this.finishSettlement(result.settlement);
|
|
1660
|
-
}
|
|
1661
|
-
async resolvePrewrite(messageId, attemptId, outcome, safeErrorCode) {
|
|
1662
|
-
const result = await this.store.resolvePrewriteAttempt({
|
|
1663
|
-
messageId, attemptId, outcome, safeErrorCode,
|
|
1664
|
-
});
|
|
1665
|
-
if (result.status === "settled")
|
|
1666
|
-
await this.finishSettlement(result.settlement);
|
|
1667
|
-
else
|
|
1668
|
-
this.activeAttempts.delete(messageId);
|
|
1669
|
-
}
|
|
1670
|
-
async applyDispatchResult(messageId, attemptId, result, armed, conversation, sourceAlias, targetAlias) {
|
|
1671
|
-
if (result.state === "deferred" && armed) {
|
|
1672
|
-
result = {
|
|
1673
|
-
state: "ambiguous",
|
|
1674
|
-
safeErrorCode: "DISPATCH_OUTCOME_AMBIGUOUS",
|
|
1675
|
-
};
|
|
1676
|
-
}
|
|
1677
|
-
if (result.state === "deferred") {
|
|
1678
|
-
const code = safeCode(result.safeErrorCode, "PROVIDER_DEFERRED");
|
|
1679
|
-
const resolution = await this.store.resolvePrewriteAttempt({
|
|
1680
|
-
messageId,
|
|
1681
|
-
attemptId,
|
|
1682
|
-
outcome: CLEAN_RETRY_CODES.has(code) ? "requeue" : "failed",
|
|
1683
|
-
safeErrorCode: code,
|
|
1684
|
-
});
|
|
1685
|
-
if (resolution.status === "settled")
|
|
1686
|
-
await this.finishSettlement(resolution.settlement);
|
|
1687
|
-
return resolution.status === "requeued";
|
|
1688
|
-
}
|
|
1689
|
-
const terminal = result.state;
|
|
1690
|
-
const resultCode = "safeErrorCode" in result
|
|
1691
|
-
? result.safeErrorCode
|
|
1692
|
-
: undefined;
|
|
1693
|
-
const prewrite = await this.store.resolvePrewriteAttempt({
|
|
1694
|
-
messageId,
|
|
1695
|
-
attemptId,
|
|
1696
|
-
outcome: "failed",
|
|
1697
|
-
...(resultCode === undefined ? {} : { safeErrorCode: safeCode(resultCode, "PROVIDER_DISPATCH_FAILED") }),
|
|
1698
|
-
});
|
|
1699
|
-
if (prewrite.status === "settled") {
|
|
1700
|
-
await this.finishSettlement(prewrite.settlement);
|
|
1701
|
-
return false;
|
|
1702
|
-
}
|
|
1703
|
-
if (prewrite.status !== "stale")
|
|
1704
|
-
return false;
|
|
1705
|
-
const settlement = await this.store.settleAttempt({
|
|
1706
|
-
messageId,
|
|
1707
|
-
attemptId,
|
|
1708
|
-
state: terminal,
|
|
1709
|
-
...(resultCode === undefined ? {} : { safeErrorCode: safeCode(resultCode, "PROVIDER_DISPATCH_FAILED") }),
|
|
1710
|
-
});
|
|
1711
|
-
if (settlement.status === "settled") {
|
|
1712
|
-
await this.finishSettlement(settlement.settlement);
|
|
1713
|
-
if (settlement.settlement.state === "delivered") {
|
|
1714
|
-
await this.activatePendingClaudeReply(messageId);
|
|
1715
|
-
}
|
|
1716
|
-
if (conversation !== undefined &&
|
|
1717
|
-
"replyText" in result &&
|
|
1718
|
-
result.replyText !== undefined) {
|
|
1719
|
-
await this.enqueueCorrelatedReply(conversation, sourceAlias, targetAlias, result.replyText);
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
return false;
|
|
1723
|
-
}
|
|
1724
|
-
callbacksFor(adapter) {
|
|
1725
|
-
return {
|
|
1726
|
-
onRouteState: (event) => {
|
|
1727
|
-
if (event.route.provider !== adapter.identity.provider ||
|
|
1728
|
-
event.route.hostId !== adapter.identity.hostId)
|
|
1729
|
-
return;
|
|
1730
|
-
void this.onRouteState(event).catch((error) => this.alert("ROUTE_OBSERVATION_FAILED", undefined, error));
|
|
1731
|
-
},
|
|
1732
|
-
onClaudeReply: (event) => {
|
|
1733
|
-
if (adapter.identity.provider !== "claude" ||
|
|
1734
|
-
event.endpoint.provider !== adapter.identity.provider ||
|
|
1735
|
-
event.endpoint.hostId !== adapter.identity.hostId)
|
|
1736
|
-
return;
|
|
1737
|
-
void this.onClaudeReply(event).catch((error) => this.alert("CLAUDE_REPLY_REJECTED", undefined, error));
|
|
1738
|
-
},
|
|
1739
|
-
onClaudeMessage: (event) => {
|
|
1740
|
-
if (adapter.identity.provider !== "claude" ||
|
|
1741
|
-
event.endpoint.provider !== adapter.identity.provider ||
|
|
1742
|
-
event.endpoint.hostId !== adapter.identity.hostId)
|
|
1743
|
-
return;
|
|
1744
|
-
const operation = this.onClaudeMessage(event).catch((error) => {
|
|
1745
|
-
this.alert("CLAUDE_INGRESS_REJECTED", undefined, error);
|
|
1746
|
-
if (event.receiptHandle !== undefined) {
|
|
1747
|
-
void adapter.updateNativeInboundStatus?.(event.receiptHandle, "denied", safeCode(error instanceof BridgeError ? error.code : undefined, "CLAUDE_INGRESS_REJECTED")).finally(() => adapter.releaseNativeInboundReceipt?.(event.receiptHandle));
|
|
1748
|
-
}
|
|
1749
|
-
}).finally(() => this.inboundOperations.delete(operation));
|
|
1750
|
-
this.inboundOperations.add(operation);
|
|
1751
|
-
void operation;
|
|
1752
|
-
},
|
|
1753
|
-
onProtocolNotice: (event) => this.alert(safeCode(event.code, "PROVIDER_PROTOCOL_NOTICE"), undefined),
|
|
1754
|
-
};
|
|
1755
|
-
}
|
|
1756
|
-
async onRouteState(event) {
|
|
1757
|
-
const route = await this.store.inspectPrivateRoute((await this.store.listLogicalRoutes()).find((candidate) => candidate.binding.registrationId === event.route.registrationId)?.alias ?? "");
|
|
1758
|
-
if (route === undefined || !sameBinding(route.binding, event.route))
|
|
1759
|
-
return;
|
|
1760
|
-
if (!Number.isFinite(Date.parse(event.observedAt)))
|
|
1761
|
-
return;
|
|
1762
|
-
this.routeObservations.set(route.alias, {
|
|
1763
|
-
...event,
|
|
1764
|
-
...(event.safeErrorCode === undefined
|
|
1765
|
-
? {}
|
|
1766
|
-
: { safeErrorCode: safeCode(event.safeErrorCode, "ROUTE_UNOBSERVED") }),
|
|
1767
|
-
});
|
|
1768
|
-
const runtime = this.connectors.get(connectorKey(event.route));
|
|
1769
|
-
if (runtime !== undefined) {
|
|
1770
|
-
runtime.observedAt = event.observedAt;
|
|
1771
|
-
runtime.health = event.state === "unobserved" ? "degraded" : "healthy";
|
|
1772
|
-
if (event.state === "unobserved") {
|
|
1773
|
-
runtime.safeErrorCode = safeCode(event.safeErrorCode, "ROUTE_UNOBSERVED");
|
|
1774
|
-
}
|
|
1775
|
-
else {
|
|
1776
|
-
delete runtime.safeErrorCode;
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
this.revision += 1;
|
|
1780
|
-
}
|
|
1781
|
-
async onClaudeMessage(event) {
|
|
1782
|
-
const target = await this.store.inspectPrivateRoute(event.targetAlias);
|
|
1783
|
-
const localHost = this.config.hostId;
|
|
1784
|
-
if (target === undefined || (target.binding.hostId !== event.endpoint.hostId &&
|
|
1785
|
-
!(target.registrationMode === "federated_peer" && event.endpoint.hostId === localHost))) {
|
|
1786
|
-
throw new BridgeError("ROUTE_NOT_AVAILABLE", "The native target is absent.");
|
|
1787
|
-
}
|
|
1788
|
-
// A native SendMessage is this session's send: its route installs here,
|
|
1789
|
-
// from the exact identity the adapter attested, so the Codex task can
|
|
1790
|
-
// reply through the ordinary path. The adapter's name for the sender must
|
|
1791
|
-
// be the name discovery shows for that UUID; the check runs on a read-only
|
|
1792
|
-
// lookup so a refusal writes nothing.
|
|
1793
|
-
this.assertWritable();
|
|
1794
|
-
const seen = await this.lookUpClaudeRoute(event.endpoint.routeHandle, event.endpoint.hostId);
|
|
1795
|
-
if (this.claimedAlias(seen) !== event.sourceAlias) {
|
|
1796
|
-
throw new BridgeError("CLAUDE_ROUTE_MISMATCH", "The native sender's name does not match its discovered session.");
|
|
1797
|
-
}
|
|
1798
|
-
const source = await this.materializeClaudeRoute(seen);
|
|
1799
|
-
const sourceBinding = source.binding;
|
|
1800
|
-
const conversationId = createGatewayConversationId();
|
|
1801
|
-
const steer = target.binding.provider === "codex" && event.text.startsWith("STEER:") && this.config.steeringEnabled;
|
|
1802
|
-
this.assertWritable();
|
|
1803
|
-
const enqueued = await this.store.enqueueNativeIngress({
|
|
1804
|
-
source: { alias: source.alias, binding: sourceBinding },
|
|
1805
|
-
targetAlias: event.targetAlias,
|
|
1806
|
-
expectedTargetRegistrationId: target.binding.registrationId,
|
|
1807
|
-
body: event.text,
|
|
1808
|
-
dedupeKey: `${conversationId}:0`,
|
|
1809
|
-
conversationIdSuffix: conversationId.slice(-8),
|
|
1810
|
-
...(steer ? { steer: true } : {}),
|
|
1811
|
-
});
|
|
1812
|
-
if (!enqueued.accepted || enqueued.messageId === undefined) {
|
|
1813
|
-
throw new BridgeError("MESSAGE_NOT_ACCEPTED", "The native message was not accepted.");
|
|
1814
|
-
}
|
|
1815
|
-
const conversation = {
|
|
1816
|
-
id: conversationId,
|
|
1817
|
-
sourceAlias: source.alias,
|
|
1818
|
-
targetAlias: event.targetAlias,
|
|
1819
|
-
sourceBinding,
|
|
1820
|
-
targetBinding: target.binding,
|
|
1821
|
-
expectsReply: true,
|
|
1822
|
-
nextSequence: 1,
|
|
1823
|
-
};
|
|
1824
|
-
this.rememberConversation(conversation);
|
|
1825
|
-
this.messageContexts.set(enqueued.messageId, { conversationId, expectsReply: true });
|
|
1826
|
-
if (event.receiptHandle !== undefined) {
|
|
1827
|
-
const adapter = this.claudeAdapter(event.endpoint.hostId);
|
|
1828
|
-
if (adapter !== undefined) {
|
|
1829
|
-
const receipt = {
|
|
1830
|
-
adapter,
|
|
1831
|
-
receiptHandle: event.receiptHandle,
|
|
1832
|
-
targetAlias: event.targetAlias,
|
|
1833
|
-
enqueuedAt: this.now().getTime(),
|
|
1834
|
-
heldWrite: Promise.resolve(),
|
|
1835
|
-
settled: false,
|
|
1836
|
-
};
|
|
1837
|
-
receipt.heldTimer = this.timers.setTimeout(() => {
|
|
1838
|
-
if (receipt.settled)
|
|
1839
|
-
return;
|
|
1840
|
-
delete receipt.heldTimer;
|
|
1841
|
-
receipt.heldWrite = Promise.resolve(adapter.updateNativeInboundStatus?.(receipt.receiptHandle, "held")).then(() => undefined).catch((error) => this.alert("CLAUDE_RECEIPT_HELD_FAILED", undefined, error));
|
|
1842
|
-
}, 1_000);
|
|
1843
|
-
if (this.config.deliveryNotices !== "quiet") {
|
|
1844
|
-
receipt.stallTimer = this.timers.setTimeout(() => {
|
|
1845
|
-
if (receipt.settled)
|
|
1846
|
-
return;
|
|
1847
|
-
delete receipt.stallTimer;
|
|
1848
|
-
void receipt.heldWrite.then(async () => {
|
|
1849
|
-
if (receipt.settled)
|
|
1850
|
-
return;
|
|
1851
|
-
const observation = this.routeObservations.get(receipt.targetAlias);
|
|
1852
|
-
const reason = observation?.state === "awaiting_approval"
|
|
1853
|
-
? "AWAITING_EXTERNAL_APPROVAL"
|
|
1854
|
-
: observation?.state === "busy"
|
|
1855
|
-
? "ROUTE_BUSY"
|
|
1856
|
-
: observation === undefined || observation.state === "unobserved"
|
|
1857
|
-
? "ROUTE_UNAVAILABLE"
|
|
1858
|
-
: "ROUTE_BUSY";
|
|
1859
|
-
await adapter.notifyNativeInboundProgress?.(receipt.receiptHandle, {
|
|
1860
|
-
kind: "stall",
|
|
1861
|
-
reason,
|
|
1862
|
-
queuedForMs: Math.max(0, this.now().getTime() - receipt.enqueuedAt),
|
|
1863
|
-
});
|
|
1864
|
-
}).catch((error) => this.alert("CLAUDE_RECEIPT_PROGRESS_FAILED", undefined, error));
|
|
1865
|
-
}, this.config.stallNoticeMs);
|
|
1866
|
-
}
|
|
1867
|
-
this.nativeReceipts.set(enqueued.messageId, receipt);
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
if (steer)
|
|
1871
|
-
this.kickSteer(event.targetAlias);
|
|
1872
|
-
this.kick(event.targetAlias);
|
|
1873
|
-
this.scheduleWake();
|
|
1874
|
-
}
|
|
1875
|
-
async onClaudeReply(event) {
|
|
1876
|
-
if (event.endpoint.provider !== "claude" || this.closing)
|
|
1877
|
-
return;
|
|
1878
|
-
const key = `${event.endpoint.hostId}\0${event.endpoint.routeHandle}`;
|
|
1879
|
-
const pending = this.pendingClaudeReplies.get(key) ?? [];
|
|
1880
|
-
while (pending.length > 0) {
|
|
1881
|
-
const row = pending[0];
|
|
1882
|
-
if (row.state === "retired") {
|
|
1883
|
-
pending.shift();
|
|
1884
|
-
if (pending.length === 0)
|
|
1885
|
-
this.pendingClaudeReplies.delete(key);
|
|
1886
|
-
else
|
|
1887
|
-
this.pendingClaudeReplies.set(key, pending);
|
|
1888
|
-
return;
|
|
1889
|
-
}
|
|
1890
|
-
const current = await this.store.inspectPrivateRoute(row.targetAlias);
|
|
1891
|
-
const currentSource = await this.store.inspectPrivateRoute(row.sourceAlias);
|
|
1892
|
-
if (current === undefined ||
|
|
1893
|
-
!sameBinding(current.binding, row.targetBinding) ||
|
|
1894
|
-
currentSource === undefined ||
|
|
1895
|
-
!sameBinding(currentSource.binding, row.sourceBinding)) {
|
|
1896
|
-
pending.shift();
|
|
1897
|
-
if (pending.length === 0)
|
|
1898
|
-
this.pendingClaudeReplies.delete(key);
|
|
1899
|
-
else
|
|
1900
|
-
this.pendingClaudeReplies.set(key, pending);
|
|
1901
|
-
return;
|
|
1902
|
-
}
|
|
1903
|
-
if (row.state === "armed") {
|
|
1904
|
-
if (row.bufferedReply === undefined)
|
|
1905
|
-
row.bufferedReply = event.text;
|
|
1906
|
-
this.pendingClaudeReplies.set(key, pending);
|
|
1907
|
-
return;
|
|
1908
|
-
}
|
|
1909
|
-
pending.shift();
|
|
1910
|
-
if (pending.length === 0)
|
|
1911
|
-
this.pendingClaudeReplies.delete(key);
|
|
1912
|
-
else
|
|
1913
|
-
this.pendingClaudeReplies.set(key, pending);
|
|
1914
|
-
await this.deliverPendingClaudeReply(row, event.text);
|
|
1915
|
-
return;
|
|
1916
|
-
}
|
|
1917
|
-
this.pendingClaudeReplies.delete(key);
|
|
1918
|
-
}
|
|
1919
|
-
async enqueueCorrelatedReply(conversation, sourceAlias, targetAlias, text) {
|
|
1920
|
-
const sourceBinding = sourceAlias === conversation.sourceAlias
|
|
1921
|
-
? conversation.sourceBinding
|
|
1922
|
-
: conversation.targetBinding;
|
|
1923
|
-
const targetBinding = targetAlias === conversation.targetAlias
|
|
1924
|
-
? conversation.targetBinding
|
|
1925
|
-
: conversation.sourceBinding;
|
|
1926
|
-
if (sourceBinding === undefined || targetBinding === undefined)
|
|
1927
|
-
return;
|
|
1928
|
-
const currentSource = await this.store.inspectPrivateRoute(sourceAlias);
|
|
1929
|
-
if (currentSource === undefined ||
|
|
1930
|
-
!sameBinding(currentSource.binding, sourceBinding))
|
|
1931
|
-
return;
|
|
1932
|
-
await this.enqueueConversation({
|
|
1933
|
-
sourceAlias: targetAlias,
|
|
1934
|
-
targetAlias: sourceAlias,
|
|
1935
|
-
text,
|
|
1936
|
-
expectsReply: true,
|
|
1937
|
-
expectedSourceBinding: targetBinding,
|
|
1938
|
-
expectedTargetBinding: sourceBinding,
|
|
1939
|
-
existingConversation: conversation,
|
|
1940
|
-
});
|
|
1941
|
-
}
|
|
1942
|
-
installPendingClaudeReply(row) {
|
|
1943
|
-
const key = `${row.targetBinding.hostId}\0${row.targetBinding.routeHandle}`;
|
|
1944
|
-
const pending = this.pendingClaudeReplies.get(key) ?? [];
|
|
1945
|
-
if (!pending.some((candidate) => candidate.messageId === row.messageId)) {
|
|
1946
|
-
pending.push(row);
|
|
1947
|
-
this.pendingClaudeReplies.set(key, pending);
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
pruneExpiredPendingClaudeReplies(now) {
|
|
1951
|
-
for (const [key, pending] of this.pendingClaudeReplies) {
|
|
1952
|
-
const retained = pending.filter((row) => Date.parse(row.deadlineAt) > now);
|
|
1953
|
-
if (retained.length === pending.length)
|
|
1954
|
-
continue;
|
|
1955
|
-
if (retained.length === 0)
|
|
1956
|
-
this.pendingClaudeReplies.delete(key);
|
|
1957
|
-
else
|
|
1958
|
-
this.pendingClaudeReplies.set(key, retained);
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
retirePendingClaudeReply(messageId) {
|
|
1962
|
-
for (const pending of this.pendingClaudeReplies.values()) {
|
|
1963
|
-
const row = pending.find((candidate) => candidate.messageId === messageId);
|
|
1964
|
-
if (row === undefined)
|
|
1965
|
-
continue;
|
|
1966
|
-
row.state = "retired";
|
|
1967
|
-
delete row.bufferedReply;
|
|
1968
|
-
return;
|
|
1969
|
-
}
|
|
1970
|
-
}
|
|
1971
|
-
async activatePendingClaudeReply(messageId) {
|
|
1972
|
-
for (const [key, pending] of this.pendingClaudeReplies) {
|
|
1973
|
-
const row = pending.find((candidate) => candidate.messageId === messageId);
|
|
1974
|
-
if (row === undefined)
|
|
1975
|
-
continue;
|
|
1976
|
-
row.state = "delivered";
|
|
1977
|
-
if (row.bufferedReply === undefined || pending[0] !== row)
|
|
1978
|
-
return;
|
|
1979
|
-
const text = row.bufferedReply;
|
|
1980
|
-
pending.shift();
|
|
1981
|
-
if (pending.length === 0)
|
|
1982
|
-
this.pendingClaudeReplies.delete(key);
|
|
1983
|
-
else
|
|
1984
|
-
this.pendingClaudeReplies.set(key, pending);
|
|
1985
|
-
await this.deliverPendingClaudeReply(row, text);
|
|
1986
|
-
return;
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
async deliverPendingClaudeReply(row, text) {
|
|
1990
|
-
const conversation = this.conversations.get(row.conversationId);
|
|
1991
|
-
if (conversation === undefined)
|
|
1992
|
-
return;
|
|
1993
|
-
const currentSource = await this.store.inspectPrivateRoute(row.sourceAlias);
|
|
1994
|
-
if (currentSource === undefined ||
|
|
1995
|
-
!sameBinding(currentSource.binding, row.sourceBinding))
|
|
1996
|
-
return;
|
|
1997
|
-
await this.enqueueConversation({
|
|
1998
|
-
sourceAlias: row.targetAlias,
|
|
1999
|
-
targetAlias: row.sourceAlias,
|
|
2000
|
-
text,
|
|
2001
|
-
expectsReply: true,
|
|
2002
|
-
expectedSourceBinding: row.targetBinding,
|
|
2003
|
-
expectedTargetBinding: row.sourceBinding,
|
|
2004
|
-
existingConversation: conversation,
|
|
2005
|
-
});
|
|
2006
|
-
}
|
|
2007
|
-
async finishSettlements(settlements) {
|
|
2008
|
-
await Promise.all(settlements.map(async (settlement) => this.finishSettlement(settlement)));
|
|
2009
|
-
}
|
|
2010
|
-
async finishSettlement(settlement) {
|
|
2011
|
-
this.activeAttempts.delete(settlement.messageId);
|
|
2012
|
-
this.messageContexts.delete(settlement.messageId);
|
|
2013
|
-
const receipt = this.nativeReceipts.get(settlement.messageId);
|
|
2014
|
-
if (receipt !== undefined) {
|
|
2015
|
-
this.nativeReceipts.delete(settlement.messageId);
|
|
2016
|
-
receipt.settled = true;
|
|
2017
|
-
if (receipt.heldTimer !== undefined)
|
|
2018
|
-
this.timers.clearTimeout(receipt.heldTimer);
|
|
2019
|
-
if (receipt.stallTimer !== undefined)
|
|
2020
|
-
this.timers.clearTimeout(receipt.stallTimer);
|
|
2021
|
-
const status = settlement.state === "delivered"
|
|
2022
|
-
? "delivered"
|
|
2023
|
-
: "expired";
|
|
2024
|
-
await receipt.heldWrite.then(() => receipt.adapter.updateNativeInboundStatus?.(receipt.receiptHandle, status, settlement.safeErrorCode))
|
|
2025
|
-
.catch((error) => this.alert("CLAUDE_RECEIPT_SETTLEMENT_FAILED", undefined, error))
|
|
2026
|
-
.finally(() => receipt.adapter.releaseNativeInboundReceipt?.(receipt.receiptHandle))
|
|
2027
|
-
.catch((error) => this.alert("CLAUDE_RECEIPT_RELEASE_FAILED", undefined, error));
|
|
2028
|
-
}
|
|
2029
|
-
if (settlement.state !== "delivered") {
|
|
2030
|
-
this.retirePendingClaudeReply(settlement.messageId);
|
|
2031
|
-
}
|
|
2032
|
-
}
|
|
2033
|
-
scheduleTargetRetry(targetAlias, registrationId, steer = false) {
|
|
2034
|
-
this.timers.setTimeout(() => steer
|
|
2035
|
-
? this.kickSteer(targetAlias)
|
|
2036
|
-
: this.kick(targetAlias, registrationId), CLEAN_RETRY_DELAY_MS);
|
|
2037
|
-
}
|
|
2038
|
-
scheduleWake() {
|
|
2039
|
-
if (!this.running || this.closing)
|
|
2040
|
-
return;
|
|
2041
|
-
if (this.wakeTimer !== undefined)
|
|
2042
|
-
this.timers.clearTimeout(this.wakeTimer);
|
|
2043
|
-
void this.store.nextDeadlineAt().then((deadlineAt) => {
|
|
2044
|
-
if (!this.running || this.closing)
|
|
2045
|
-
return;
|
|
2046
|
-
const now = this.now().getTime();
|
|
2047
|
-
const due = [
|
|
2048
|
-
this.nextDiscoveryAt || now + DISCOVERY_INTERVAL_MS,
|
|
2049
|
-
this.nextManagedCodexCheckAt || now + MANAGED_CODEX_SOCKET_CHECK_INTERVAL_MS,
|
|
2050
|
-
...(this.config.peerNodes.length > 0
|
|
2051
|
-
? [this.nextPeerRefreshAt || now + PEER_REFRESH_INTERVAL_MS]
|
|
2052
|
-
: []),
|
|
2053
|
-
...this.ephemeralPeerExpiries.values(),
|
|
2054
|
-
...(deadlineAt === undefined ? [] : [Date.parse(deadlineAt)]),
|
|
2055
|
-
...[...this.pendingClaudeReplies.values()].flatMap((rows) => rows.map((row) => Date.parse(row.deadlineAt))),
|
|
2056
|
-
].filter(Number.isFinite);
|
|
2057
|
-
const delay = Math.max(0, Math.min(...due) - now);
|
|
2058
|
-
this.wakeTimer = this.timers.setTimeout(() => {
|
|
2059
|
-
this.wakeTimer = undefined;
|
|
2060
|
-
void this.onWake().catch((error) => this.alert("GATEWAY_WAKE_FAILED", undefined, error));
|
|
2061
|
-
}, delay);
|
|
2062
|
-
}).catch((error) => this.alert("GATEWAY_WAKE_FAILED", undefined, error));
|
|
2063
|
-
}
|
|
2064
|
-
async onWake() {
|
|
2065
|
-
if (!this.running || this.closing)
|
|
2066
|
-
return;
|
|
2067
|
-
const now = this.now();
|
|
2068
|
-
await this.finishSettlements(await this.store.expireDueMessages(now));
|
|
2069
|
-
this.pruneExpiredPendingClaudeReplies(now.getTime());
|
|
2070
|
-
await this.expireEphemeralPeers(now.getTime());
|
|
2071
|
-
if (now.getTime() >= this.nextDiscoveryAt) {
|
|
2072
|
-
await this.refreshClaudeDiscovery().catch(() => undefined);
|
|
2073
|
-
this.nextDiscoveryAt = now.getTime() + DISCOVERY_INTERVAL_MS;
|
|
2074
|
-
}
|
|
2075
|
-
if (now.getTime() >= this.nextManagedCodexCheckAt) {
|
|
2076
|
-
await this.refreshManagedCodexSocket().catch(() => undefined);
|
|
2077
|
-
this.nextManagedCodexCheckAt = now.getTime() + MANAGED_CODEX_SOCKET_CHECK_INTERVAL_MS;
|
|
2078
|
-
}
|
|
2079
|
-
if (this.nextPeerRefreshAt > 0 && now.getTime() >= this.nextPeerRefreshAt) {
|
|
2080
|
-
const operation = this.refreshPeers();
|
|
2081
|
-
this.peerRefreshOperations.add(operation);
|
|
2082
|
-
try {
|
|
2083
|
-
await operation;
|
|
2084
|
-
}
|
|
2085
|
-
finally {
|
|
2086
|
-
this.peerRefreshOperations.delete(operation);
|
|
2087
|
-
}
|
|
2088
|
-
this.nextPeerRefreshAt = now.getTime() + PEER_REFRESH_INTERVAL_MS;
|
|
2089
|
-
}
|
|
2090
|
-
if (!this.running || this.closing)
|
|
2091
|
-
return;
|
|
2092
|
-
for (const target of await this.store.inspectDispatchableTargets())
|
|
2093
|
-
this.kick(target);
|
|
2094
|
-
this.scheduleWake();
|
|
2095
|
-
}
|
|
2096
|
-
async refreshPeers() {
|
|
2097
|
-
const localHost = this.config.hostId;
|
|
2098
|
-
await Promise.all(this.config.peerNodes.map(async (peerHost) => {
|
|
2099
|
-
let failureCode = "PEER_TUNNEL_UNAVAILABLE";
|
|
2100
|
-
try {
|
|
2101
|
-
let client = this.peerClients.get(peerHost);
|
|
2102
|
-
if (client === undefined) {
|
|
2103
|
-
failureCode = "PEER_DIAL_FAILED";
|
|
2104
|
-
client = await this.spawnPeer({ node: peerHost, localHost });
|
|
2105
|
-
if (!this.running || this.closing) {
|
|
2106
|
-
client.close();
|
|
2107
|
-
return;
|
|
2108
|
-
}
|
|
2109
|
-
this.peerClients.set(peerHost, client);
|
|
2110
|
-
}
|
|
2111
|
-
failureCode = "PEER_TUNNEL_UNAVAILABLE";
|
|
2112
|
-
const catalog = await client.catalog();
|
|
2113
|
-
if (!this.running || this.closing)
|
|
2114
|
-
return;
|
|
2115
|
-
this.recordPeerFailure(peerHost);
|
|
2116
|
-
const prior = this.peerCatalogs.get(peerHost);
|
|
2117
|
-
let routes = await this.store.inspectPrivateRoutes().then((rows) => rows.filter((route) => route.registrationMode === "federated_peer" && route.binding.hostId === peerHost));
|
|
2118
|
-
if (!catalog.complete || catalog.truncated) {
|
|
2119
|
-
const observedAt = this.now().toISOString();
|
|
2120
|
-
for (const route of routes)
|
|
2121
|
-
this.peerRouteViews.set(route.alias, { route: route.binding,
|
|
2122
|
-
state: "unobserved", observedAt, safeErrorCode: "PEER_CATALOG_INCOMPLETE" });
|
|
2123
|
-
return;
|
|
2124
|
-
}
|
|
2125
|
-
const authorityChanged = peerCatalogAuthorityChanged(prior, catalog);
|
|
2126
|
-
const viewChanged = peerCatalogViewChanged(prior, catalog);
|
|
2127
|
-
if (authorityChanged) {
|
|
2128
|
-
const before = routes;
|
|
2129
|
-
const reconciled = await this.store.reconcilePeerCatalog(peerHost, catalog);
|
|
2130
|
-
routes = [...reconciled.routes];
|
|
2131
|
-
await this.finishSettlements(reconciled.settlements);
|
|
2132
|
-
for (const old of before)
|
|
2133
|
-
if (!routes.some((route) => route.alias === old.alias &&
|
|
2134
|
-
route.binding.registrationId === old.binding.registrationId)) {
|
|
2135
|
-
this.peerRouteViews.delete(old.alias);
|
|
2136
|
-
await this.reconcileUnadvertisement(old);
|
|
2137
|
-
}
|
|
2138
|
-
for (const route of routes)
|
|
2139
|
-
if (!before.some((old) => old.alias === route.alias &&
|
|
2140
|
-
old.binding.registrationId === route.binding.registrationId))
|
|
2141
|
-
this.reconcileAdvertisement(route);
|
|
2142
|
-
}
|
|
2143
|
-
this.peerCatalogs.set(peerHost, catalog);
|
|
2144
|
-
const observedAt = this.now().toISOString();
|
|
2145
|
-
for (const route of routes) {
|
|
2146
|
-
const row = catalog.routes.find((candidate) => candidate.ref === route.binding.routeHandle);
|
|
2147
|
-
const state = row?.state === "idle" || row?.state === "busy" || row?.state === "awaiting_approval"
|
|
2148
|
-
? row.state : "unobserved";
|
|
2149
|
-
this.peerRouteViews.set(route.alias, { route: route.binding, state, observedAt,
|
|
2150
|
-
...(state === "unobserved" ? { safeErrorCode: row?.safeErrorCode ?? "PEER_ROUTE_STALE" } : {}) });
|
|
2151
|
-
if (row?.enabled === true)
|
|
2152
|
-
this.kick(route.alias, route.binding.registrationId);
|
|
2153
|
-
}
|
|
2154
|
-
if (viewChanged)
|
|
2155
|
-
this.revision += 1;
|
|
2156
|
-
}
|
|
2157
|
-
catch (error) {
|
|
2158
|
-
this.peerClients.get(peerHost)?.close();
|
|
2159
|
-
this.peerClients.delete(peerHost);
|
|
2160
|
-
if (!this.running || this.closing)
|
|
2161
|
-
return;
|
|
2162
|
-
// A wire-version disagreement is not a tunnel fault: name it so an operator
|
|
2163
|
-
// upgrades the lagging node instead of debugging SSH.
|
|
2164
|
-
if (error instanceof PeerConnectionLostError && error.failureClass === "protocol")
|
|
2165
|
-
failureCode = "PEER_PROTOCOL_MISMATCH";
|
|
2166
|
-
this.recordPeerFailure(peerHost, failureCode);
|
|
2167
|
-
const observedAt = this.now().toISOString();
|
|
2168
|
-
const mirrorCode = failureCode === "PEER_PROTOCOL_MISMATCH" ? failureCode : "PEER_TUNNEL_UNAVAILABLE";
|
|
2169
|
-
for (const route of await this.store.inspectPrivateRoutes())
|
|
2170
|
-
if (route.registrationMode === "federated_peer" &&
|
|
2171
|
-
route.binding.hostId === peerHost)
|
|
2172
|
-
this.peerRouteViews.set(route.alias, { route: route.binding,
|
|
2173
|
-
state: "unobserved", observedAt, safeErrorCode: mirrorCode });
|
|
2174
|
-
}
|
|
2175
|
-
}));
|
|
2176
|
-
}
|
|
2177
|
-
async refreshClaudeDiscovery() {
|
|
2178
|
-
const seen = new Set();
|
|
2179
|
-
const nextCollisions = new Set();
|
|
2180
|
-
const refreshedHosts = new Set();
|
|
2181
|
-
for (const adapter of this.adapters) {
|
|
2182
|
-
if (adapter.identity.provider !== "claude" || adapter.discoverClaudePeers === undefined)
|
|
2183
|
-
continue;
|
|
2184
|
-
const snapshot = await adapter.discoverClaudePeers();
|
|
2185
|
-
const observedAt = this.now().toISOString();
|
|
2186
|
-
const peers = snapshot.peers.filter((peer) => PUBLIC_ALIAS.test(peer.alias) &&
|
|
2187
|
-
peer.alias.endsWith(`@${adapter.identity.hostId}`) && PRIVATE_HANDLE.test(peer.routeHandle));
|
|
2188
|
-
const aliasHandles = new Map();
|
|
2189
|
-
for (const peer of peers) {
|
|
2190
|
-
const handles = aliasHandles.get(peer.alias) ?? new Set();
|
|
2191
|
-
handles.add(peer.routeHandle);
|
|
2192
|
-
aliasHandles.set(peer.alias, handles);
|
|
2193
|
-
}
|
|
2194
|
-
const collidingAliases = new Set([...aliasHandles].filter(([, handles]) => handles.size > 1).map(([alias]) => alias));
|
|
2195
|
-
refreshedHosts.add(adapter.identity.hostId);
|
|
2196
|
-
if (!snapshot.complete)
|
|
2197
|
-
for (const alias of this.collidingClaudeAliases)
|
|
2198
|
-
if (alias.endsWith(`@${adapter.identity.hostId}`))
|
|
2199
|
-
collidingAliases.add(alias);
|
|
2200
|
-
for (const alias of collidingAliases)
|
|
2201
|
-
nextCollisions.add(alias);
|
|
2202
|
-
for (const peer of peers) {
|
|
2203
|
-
const key = `${adapter.identity.hostId}\0${peer.routeHandle}`;
|
|
2204
|
-
seen.add(key);
|
|
2205
|
-
this.candidates.set(key, { ...peer, adapter, observedAt });
|
|
2206
|
-
}
|
|
2207
|
-
const runtime = this.connectors.get(connectorKey(adapter.identity));
|
|
2208
|
-
if (runtime !== undefined) {
|
|
2209
|
-
runtime.observedAt = observedAt;
|
|
2210
|
-
runtime.health = snapshot.complete ? "healthy" : "degraded";
|
|
2211
|
-
const registry = snapshot.registry ?? adapter.latestRegistryObservation?.();
|
|
2212
|
-
if (registry !== undefined) {
|
|
2213
|
-
runtime.registry = publicRegistry({ ...registry, rejected: [
|
|
2214
|
-
...registry.rejected,
|
|
2215
|
-
...(collidingAliases.size === 0 ? [] : [{ safeErrorCode: "PEER_ALIAS_COLLISION",
|
|
2216
|
-
count: collidingAliases.size }]),
|
|
2217
|
-
] }, runtime.registry?.parseableRecordSeenSinceBoot ?? false);
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
for (const [key] of this.candidates)
|
|
2222
|
-
if (!seen.has(key))
|
|
2223
|
-
this.candidates.delete(key);
|
|
2224
|
-
for (const alias of this.collidingClaudeAliases)
|
|
2225
|
-
if (refreshedHosts.has(aliasHost(alias)))
|
|
2226
|
-
this.collidingClaudeAliases.delete(alias);
|
|
2227
|
-
const retainedCollisions = new Set([...nextCollisions].slice(0, gatewayPublicSnapshotLimits.availablePeers));
|
|
2228
|
-
// Discovery currently caps rows such that collisions cannot overflow this independent public bound.
|
|
2229
|
-
// If those constants diverge, discard overflow candidates rather than expose an unfenced alias.
|
|
2230
|
-
for (const [key, candidate] of this.candidates)
|
|
2231
|
-
if (nextCollisions.has(candidate.alias) && !retainedCollisions.has(candidate.alias))
|
|
2232
|
-
this.candidates.delete(key);
|
|
2233
|
-
for (const alias of retainedCollisions)
|
|
2234
|
-
this.collidingClaudeAliases.add(alias);
|
|
2235
|
-
}
|
|
2236
|
-
async refreshManagedCodexSocket() {
|
|
2237
|
-
if (this.managedCodexSocketHeld === undefined)
|
|
2238
|
-
return;
|
|
2239
|
-
this.managedCodexSocketHeldOutside = await this.managedCodexSocketHeld();
|
|
2240
|
-
}
|
|
2241
|
-
assertWritable() {
|
|
2242
|
-
if (this.closing || !this.running) {
|
|
2243
|
-
throw new BridgeError("GATEWAY_CLOSING", "The gateway is not accepting mutations.");
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
routeObservationStillCurrent(event) {
|
|
2247
|
-
return this.connectors.has(connectorKey(event.route));
|
|
2248
|
-
}
|
|
2249
|
-
alert(code, route, error) {
|
|
2250
|
-
const errorCode = error instanceof BridgeError ? error.code : undefined;
|
|
2251
|
-
this.runtimeAlerts.push({
|
|
2252
|
-
code: safeCode(errorCode, safeCode(code, "GATEWAY_RUNTIME_NOTICE")),
|
|
2253
|
-
severity: "warning",
|
|
2254
|
-
timestamp: this.now().toISOString(),
|
|
2255
|
-
...(route === undefined ? {} : {
|
|
2256
|
-
provider: route.binding.provider,
|
|
2257
|
-
host: route.binding.hostId,
|
|
2258
|
-
alias: route.alias,
|
|
2259
|
-
}),
|
|
2260
|
-
});
|
|
2261
|
-
while (this.runtimeAlerts.length > gatewayPublicSnapshotLimits.alerts)
|
|
2262
|
-
this.runtimeAlerts.shift();
|
|
2263
|
-
}
|
|
2264
|
-
recordPeerFailure(host, code) {
|
|
2265
|
-
for (let index = this.runtimeAlerts.length - 1; index >= 0; index -= 1) {
|
|
2266
|
-
const alert = this.runtimeAlerts[index];
|
|
2267
|
-
if (alert?.host === host && PEER_FAILURE_CODES.includes(alert.code))
|
|
2268
|
-
this.runtimeAlerts.splice(index, 1);
|
|
2269
|
-
}
|
|
2270
|
-
if (code !== undefined)
|
|
2271
|
-
this.runtimeAlerts.push({ code, severity: "warning", timestamp: this.now().toISOString(), host });
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
//# sourceMappingURL=service.js.map
|