@cortexkit/aft-bridge 0.61.0 → 0.62.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/dist/bash-host-fallback.d.ts +1 -1
- package/dist/bash-host-fallback.d.ts.map +1 -1
- package/dist/bash-host-fallback.js +1 -1
- package/dist/bash-host-fallback.js.map +1 -1
- package/dist/coerce.d.ts +4 -4
- package/dist/coerce.d.ts.map +1 -1
- package/dist/coerce.js +10 -7
- package/dist/coerce.js.map +1 -1
- package/dist/config-watch.d.ts +1 -0
- package/dist/config-watch.d.ts.map +1 -1
- package/dist/config-watch.js +2 -0
- package/dist/config-watch.js.map +1 -1
- package/dist/error-contract.d.ts +15 -6
- package/dist/error-contract.d.ts.map +1 -1
- package/dist/error-contract.js +97 -8
- package/dist/error-contract.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/path-aliases.d.ts +1 -1
- package/dist/path-aliases.d.ts.map +1 -1
- package/dist/path-aliases.js +63 -39
- package/dist/path-aliases.js.map +1 -1
- package/dist/revision-reads.d.ts +25 -0
- package/dist/revision-reads.d.ts.map +1 -0
- package/dist/revision-reads.js +31 -0
- package/dist/revision-reads.js.map +1 -0
- package/dist/subc-transport.d.ts +6 -0
- package/dist/subc-transport.d.ts.map +1 -1
- package/dist/subc-transport.js +194 -33
- package/dist/subc-transport.js.map +1 -1
- package/package.json +1 -1
package/dist/subc-transport.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* published plugin dist; it is never a published runtime dependency.
|
|
18
18
|
*/
|
|
19
19
|
import { statSync } from "node:fs";
|
|
20
|
-
import { AuthError, connectionFileExists, isConsumerReconnectTransient, routeCloseReason, SocketTimeoutError, StaleRouteHandleError, SubcCallError, SubcClient, SubcError, } from "@cortexkit/subc-client";
|
|
20
|
+
import { AuthError, connectionFileExists, isConsumerReconnectTransient, routeCloseReason, SocketClosedError, SocketTimeoutError, StaleRouteHandleError, SubcCallError, SubcClient, SubcError, } from "@cortexkit/subc-client";
|
|
21
21
|
import { log, warn } from "./active-logger.js";
|
|
22
22
|
import { isTerminalStatus } from "./bash-format.js";
|
|
23
23
|
import { isRouteOpenReloadWindowError, isRouteRequestReloadRefusal } from "./error-contract.js";
|
|
@@ -171,6 +171,81 @@ export function isSocketDeathError(error, depth = 0) {
|
|
|
171
171
|
}
|
|
172
172
|
return isConsumerReconnectTransient(error) || isSubcClientClosedError(error);
|
|
173
173
|
}
|
|
174
|
+
/** Typed system codes for a reset or broken connected socket. */
|
|
175
|
+
const REQUEST_CONNECTION_LOSS_CODES = new Set(["ECONNABORTED", "ECONNRESET", "EPIPE"]);
|
|
176
|
+
function hasRequestConnectionLossCode(error) {
|
|
177
|
+
if (typeof error !== "object" || error === null || !("code" in error))
|
|
178
|
+
return false;
|
|
179
|
+
return typeof error.code === "string" && REQUEST_CONNECTION_LOSS_CODES.has(error.code);
|
|
180
|
+
}
|
|
181
|
+
function hasRequestConnectionLossEvidence(error, depth = 0) {
|
|
182
|
+
if (error instanceof SocketClosedError)
|
|
183
|
+
return true;
|
|
184
|
+
if (error instanceof SubcError && error.closeReason === "connection_lost")
|
|
185
|
+
return true;
|
|
186
|
+
if (hasRequestConnectionLossCode(error))
|
|
187
|
+
return true;
|
|
188
|
+
if (!(error instanceof SubcCallError) ||
|
|
189
|
+
error.kind !== "outcome_unknown" ||
|
|
190
|
+
depth >= 4 ||
|
|
191
|
+
error.cause === undefined ||
|
|
192
|
+
error.cause === error) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
return hasRequestConnectionLossEvidence(error.cause, depth + 1);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* A raw request first checks `closedErr`, then records itself pending and starts
|
|
199
|
+
* `writeBorrowed` in the same synchronous send(). A later read-loop failure goes
|
|
200
|
+
* through fail(), which rejects route-pending requests as SubcError with
|
|
201
|
+
* closeReason="connection_lost". That proves the request write was attempted,
|
|
202
|
+
* but the raw request API does not expose whether the socket queued those bytes;
|
|
203
|
+
* the outcome must therefore stay unknown, not be retried.
|
|
204
|
+
*
|
|
205
|
+
* The timer path is different: it rejects with code="request_deadline" while
|
|
206
|
+
* leaving the shared socket potentially live. Read/body timeouts reach fail() and
|
|
207
|
+
* get the connection-lost close reason; a direct SocketTimeoutError from writing
|
|
208
|
+
* can be before or after queueing. The socket's queued/not-queued write wrappers
|
|
209
|
+
* are stripped by the raw write helper, so neither a bare timeout nor a bare
|
|
210
|
+
* SocketClosedError alone identifies the write state. The latter remains
|
|
211
|
+
* supported for older clients that rejected pending calls with that class.
|
|
212
|
+
* Explicit kind="not_sent" is authoritative and always wins over these shapes.
|
|
213
|
+
*/
|
|
214
|
+
function connectionClosedAfterDispatch(error) {
|
|
215
|
+
if (error instanceof SubcCallError && error.kind === "not_sent")
|
|
216
|
+
return error;
|
|
217
|
+
if (typeof error === "object" && error !== null && "kind" in error && error.kind === "not_sent") {
|
|
218
|
+
return error;
|
|
219
|
+
}
|
|
220
|
+
if (!hasRequestConnectionLossEvidence(error))
|
|
221
|
+
return error;
|
|
222
|
+
if (error instanceof SubcCallError)
|
|
223
|
+
return error;
|
|
224
|
+
if (!(error instanceof Error))
|
|
225
|
+
return error;
|
|
226
|
+
return Object.assign(error, { kind: "outcome_unknown" });
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Requests that subc-client's own deadline rejected after the request had been
|
|
230
|
+
* handed to `client.request`. Unlike the same `request_deadline` code on a
|
|
231
|
+
* route.open (which proves nothing was sent), the request may have reached AFT
|
|
232
|
+
* and started running, so its outcome is unknown. Kept outside the error
|
|
233
|
+
* object so the error's class, code, message and retry handling stay as
|
|
234
|
+
* subc-client made them.
|
|
235
|
+
*/
|
|
236
|
+
const dispatchedRequestDeadlines = new WeakSet();
|
|
237
|
+
function noteDispatchedRequestDeadline(error) {
|
|
238
|
+
if (error instanceof SubcError && error.code === REQUEST_DEADLINE_CODE) {
|
|
239
|
+
dispatchedRequestDeadlines.add(error);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* True for a request that ran out of subc-client's deadline after it was sent,
|
|
244
|
+
* so AFT may have started it (see `dispatchedRequestDeadlines`).
|
|
245
|
+
*/
|
|
246
|
+
export function isDispatchedRequestDeadlineError(error) {
|
|
247
|
+
return typeof error === "object" && error !== null && dispatchedRequestDeadlines.has(error);
|
|
248
|
+
}
|
|
174
249
|
/**
|
|
175
250
|
* A call that never got an answer on a socket that may still be alive: the
|
|
176
251
|
* client's own request deadline (raw `request`/`routeOpen` reject with code
|
|
@@ -967,7 +1042,29 @@ class SubcTransport {
|
|
|
967
1042
|
// dropping it here would cap long tool executions at the subc client's
|
|
968
1043
|
// default unary deadline while the command keeps running module-side.
|
|
969
1044
|
const timeoutMs = options.transportTimeoutMs ?? options.timeoutMs;
|
|
970
|
-
|
|
1045
|
+
// subc-client delivers wire bodies; hosts use the same decoded callback
|
|
1046
|
+
// shape as BinaryBridge, regardless of the route carrying the request.
|
|
1047
|
+
const callback = options.onProgress;
|
|
1048
|
+
const onProgress = callback
|
|
1049
|
+
? (body) => {
|
|
1050
|
+
let frame;
|
|
1051
|
+
try {
|
|
1052
|
+
frame = JSON.parse(new TextDecoder().decode(body));
|
|
1053
|
+
}
|
|
1054
|
+
catch {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
if (typeof frame !== "object" || frame === null)
|
|
1058
|
+
return;
|
|
1059
|
+
const progress = frame;
|
|
1060
|
+
if (progress.type !== "progress" || typeof progress.chunk !== "string")
|
|
1061
|
+
return;
|
|
1062
|
+
callback({
|
|
1063
|
+
kind: progress.kind === "stderr" ? "stderr" : "stdout",
|
|
1064
|
+
text: progress.chunk,
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
: undefined;
|
|
971
1068
|
return { preview, timeoutMs, onProgress, abortSignal: options.abortSignal };
|
|
972
1069
|
}
|
|
973
1070
|
}
|
|
@@ -984,7 +1081,7 @@ export class SubcTransportPool {
|
|
|
984
1081
|
hostScopes;
|
|
985
1082
|
/** Scope identity is host-session-wide, even when the session uses multiple project roots. */
|
|
986
1083
|
hostScopeCache = new Map();
|
|
987
|
-
hostScopeRefusals = new
|
|
1084
|
+
hostScopeRefusals = new Map();
|
|
988
1085
|
hostScopeOutcomes = new Map();
|
|
989
1086
|
connectFn;
|
|
990
1087
|
onBgEventsNudge;
|
|
@@ -2038,7 +2135,7 @@ export class SubcTransportPool {
|
|
|
2038
2135
|
return reply;
|
|
2039
2136
|
}
|
|
2040
2137
|
catch (requestError) {
|
|
2041
|
-
const error = frameReadTimeoutError(routeClosedMidCallError(requestError, routeAndEntry.entry, record));
|
|
2138
|
+
const error = frameReadTimeoutError(routeClosedMidCallError(connectionClosedAfterDispatch(requestError), routeAndEntry.entry, record));
|
|
2042
2139
|
if (this.isReapInduced(record))
|
|
2043
2140
|
throw this.annotateReapError(error, record);
|
|
2044
2141
|
const ownsRoute = this.isCurrentSession(key, record) && this.client === client;
|
|
@@ -2085,6 +2182,7 @@ export class SubcTransportPool {
|
|
|
2085
2182
|
continue;
|
|
2086
2183
|
}
|
|
2087
2184
|
handleRequestFailure(error, routeAndEntry.entry);
|
|
2185
|
+
noteDispatchedRequestDeadline(error);
|
|
2088
2186
|
throw error;
|
|
2089
2187
|
}
|
|
2090
2188
|
}
|
|
@@ -2247,21 +2345,45 @@ export class SubcTransportPool {
|
|
|
2247
2345
|
return { route: await opening, entry };
|
|
2248
2346
|
}
|
|
2249
2347
|
hostScope(client, identity) {
|
|
2250
|
-
|
|
2251
|
-
|
|
2348
|
+
let lookupReason = "cache_miss";
|
|
2349
|
+
const refused = this.hostScopeRefusals.get(identity.session);
|
|
2350
|
+
if (refused) {
|
|
2351
|
+
// A refusal applies to one route-binding attempt, not to the session's future scope.
|
|
2352
|
+
// Look up the host scope again on the next bind instead of leaving later routes unscoped.
|
|
2353
|
+
const previous = this.hostScopeCache.get(identity.session);
|
|
2354
|
+
if (previous)
|
|
2355
|
+
this.cancelHostScopeRetry(previous);
|
|
2356
|
+
this.hostScopeCache.delete(identity.session);
|
|
2357
|
+
this.hostScopeRefusals.delete(identity.session);
|
|
2358
|
+
lookupReason = `previous_bind_refusal code=${refused}`;
|
|
2359
|
+
}
|
|
2252
2360
|
let state = this.hostScopeCache.get(identity.session);
|
|
2253
2361
|
if (!state) {
|
|
2254
2362
|
state = { retryAt: 0, retryMs: HOST_SCOPE_RETRY_FLOOR_MS };
|
|
2255
2363
|
this.hostScopeCache.set(identity.session, state);
|
|
2256
2364
|
}
|
|
2257
2365
|
if (state.answer) {
|
|
2258
|
-
|
|
2366
|
+
const reason = state.scopeBindPending
|
|
2367
|
+
? "scope_sync_pending"
|
|
2368
|
+
: `cached_${state.answer.result}`;
|
|
2369
|
+
log(`subc head scope: session=${identity.session} lookup=skipped reason=${reason}`);
|
|
2370
|
+
return Promise.resolve({
|
|
2371
|
+
scope: state.answer.result === "scope" && !state.scopeBindPending
|
|
2372
|
+
? state.answer.scope
|
|
2373
|
+
: undefined,
|
|
2374
|
+
reason,
|
|
2375
|
+
});
|
|
2259
2376
|
}
|
|
2260
|
-
if (state.pending)
|
|
2377
|
+
if (state.pending) {
|
|
2378
|
+
log(`subc head scope: session=${identity.session} lookup=skipped reason=inflight_lookup`);
|
|
2261
2379
|
return state.pending;
|
|
2380
|
+
}
|
|
2262
2381
|
// Backoff skips the lookup, not the bind, so an unavailable core cannot delay every root.
|
|
2263
|
-
if (Date.now() < state.retryAt)
|
|
2264
|
-
|
|
2382
|
+
if (Date.now() < state.retryAt) {
|
|
2383
|
+
log(`subc head scope: session=${identity.session} lookup=skipped reason=backoff retry_in=${state.retryAt - Date.now()}ms`);
|
|
2384
|
+
return Promise.resolve({ reason: state.unscopedReason ?? "lookup_backoff" });
|
|
2385
|
+
}
|
|
2386
|
+
log(`subc head scope: session=${identity.session} lookup=started reason=${lookupReason}`);
|
|
2265
2387
|
const current = state;
|
|
2266
2388
|
clearTimeout(current.retryTimer);
|
|
2267
2389
|
current.retryTimer = undefined;
|
|
@@ -2270,25 +2392,36 @@ export class SubcTransportPool {
|
|
|
2270
2392
|
const lookup = this.readHostScope(client, identity, abort.signal).then((outcome) => {
|
|
2271
2393
|
// A scope-close push may invalidate the session while the management request is in flight.
|
|
2272
2394
|
if (this.hostScopeCache.get(identity.session) !== current)
|
|
2273
|
-
return
|
|
2395
|
+
return { reason: "lookup_invalidated" };
|
|
2274
2396
|
current.pending = undefined;
|
|
2275
2397
|
current.lookupAbort = undefined;
|
|
2276
2398
|
if (abort.signal.aborted)
|
|
2277
|
-
return
|
|
2399
|
+
return { reason: "lookup_cancelled" };
|
|
2278
2400
|
this.logHostScopeOutcome(identity.session, outcome);
|
|
2401
|
+
const reason = outcome.result === "error"
|
|
2402
|
+
? `lookup_error code=${outcome.code}`
|
|
2403
|
+
: `lookup_${outcome.result}`;
|
|
2279
2404
|
if (outcome.result === "scope" || outcome.result === "no_scope") {
|
|
2280
2405
|
current.answer = outcome;
|
|
2281
2406
|
current.retryAt = 0;
|
|
2282
2407
|
current.retryMs = HOST_SCOPE_RETRY_FLOOR_MS;
|
|
2283
|
-
if (outcome.result === "scope")
|
|
2284
|
-
|
|
2408
|
+
if (outcome.result === "scope") {
|
|
2409
|
+
// After a refusal, confirm daemon acceptance before replacing working fallback routes.
|
|
2410
|
+
current.promoteAfterBind = Boolean(refused);
|
|
2411
|
+
if (!current.promoteAfterBind)
|
|
2412
|
+
this.promoteHostScope(identity.session);
|
|
2413
|
+
}
|
|
2285
2414
|
}
|
|
2286
2415
|
else {
|
|
2416
|
+
current.unscopedReason = reason;
|
|
2287
2417
|
current.retryAt = Date.now() + current.retryMs;
|
|
2288
2418
|
current.retryMs = Math.min(current.retryMs * 2, HOST_SCOPE_RETRY_CAP_MS);
|
|
2289
2419
|
this.scheduleHostScopeRetry(identity.session, current);
|
|
2290
2420
|
}
|
|
2291
|
-
return
|
|
2421
|
+
return {
|
|
2422
|
+
scope: outcome.result === "scope" ? outcome.scope : undefined,
|
|
2423
|
+
reason,
|
|
2424
|
+
};
|
|
2292
2425
|
});
|
|
2293
2426
|
current.pending = lookup;
|
|
2294
2427
|
return lookup;
|
|
@@ -2351,7 +2484,7 @@ export class SubcTransportPool {
|
|
|
2351
2484
|
retry = this.hostScope(client, record.identity);
|
|
2352
2485
|
}
|
|
2353
2486
|
void retry.catch((error) => {
|
|
2354
|
-
warn(`subc head scope: background retry failed code=${error instanceof Error ? error.name : "Error"}`);
|
|
2487
|
+
warn(`subc head scope: session=${session} background retry failed code=${error instanceof Error ? error.name : "Error"}`);
|
|
2355
2488
|
});
|
|
2356
2489
|
}, Math.max(0, state.retryAt - Date.now()));
|
|
2357
2490
|
state.retryTimer.unref();
|
|
@@ -2367,8 +2500,8 @@ export class SubcTransportPool {
|
|
|
2367
2500
|
}
|
|
2368
2501
|
refuseHostScopeBind(session, state, code) {
|
|
2369
2502
|
if (!this.hostScopeRefusals.has(session)) {
|
|
2370
|
-
this.hostScopeRefusals.
|
|
2371
|
-
warn(`subc head scope: session=${session} scoped bind refused code=${code};
|
|
2503
|
+
this.hostScopeRefusals.set(session, code);
|
|
2504
|
+
warn(`subc head scope: session=${session} scoped bind refused code=${code}; retry stopped until next bind lookup`);
|
|
2372
2505
|
}
|
|
2373
2506
|
if (state) {
|
|
2374
2507
|
state.scopeBindPending = false;
|
|
@@ -2379,7 +2512,7 @@ export class SubcTransportPool {
|
|
|
2379
2512
|
async retryHostScopeBind(client, identity, state, scope) {
|
|
2380
2513
|
try {
|
|
2381
2514
|
// Validate daemon acceptance before retiring a working fallback route or its held requests.
|
|
2382
|
-
const route = await
|
|
2515
|
+
const route = await this.bindHostToolRoute(client, identity, "scope_probe", "scope_sync_retry", scope);
|
|
2383
2516
|
safeCloseRoute(client, route);
|
|
2384
2517
|
if (this.hostScopeCache.get(identity.session) !== state ||
|
|
2385
2518
|
!state.scopeBindPending ||
|
|
@@ -2388,6 +2521,7 @@ export class SubcTransportPool {
|
|
|
2388
2521
|
!this.unscopedHostRecord(identity.session))
|
|
2389
2522
|
return;
|
|
2390
2523
|
state.scopeBindPending = false;
|
|
2524
|
+
state.promoteAfterBind = false;
|
|
2391
2525
|
state.retryAt = 0;
|
|
2392
2526
|
state.retryMs = HOST_SCOPE_RETRY_FLOOR_MS;
|
|
2393
2527
|
log(`subc head scope: session=${identity.session} scoped bind recovered ref=${scope.ref.slice(0, 8)} epoch=${scope.scopeEpoch}`);
|
|
@@ -2543,27 +2677,49 @@ export class SubcTransportPool {
|
|
|
2543
2677
|
signal.removeEventListener("abort", cancel);
|
|
2544
2678
|
}
|
|
2545
2679
|
}
|
|
2546
|
-
openHostToolRoute(client, identity, record, entry) {
|
|
2680
|
+
openHostToolRoute(client, identity, record, entry, channel = "tool") {
|
|
2547
2681
|
if (!this.hostScopes ||
|
|
2548
2682
|
!["opencode", "pi"].includes(identity.harness) ||
|
|
2549
2683
|
identity.session === DEFAULT_SESSION_ID) {
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2684
|
+
const reason = !this.hostScopes
|
|
2685
|
+
? "host_scopes_disabled"
|
|
2686
|
+
: identity.session === DEFAULT_SESSION_ID
|
|
2687
|
+
? "default_session"
|
|
2688
|
+
: "unsupported_harness";
|
|
2689
|
+
log(`subc head scope: session=${identity.session} lookup=skipped reason=${reason}`);
|
|
2690
|
+
return this.bindHostToolRoute(client, identity, channel, reason);
|
|
2691
|
+
}
|
|
2692
|
+
return this.openScopedHostToolRoute(client, identity, record, entry, channel);
|
|
2693
|
+
}
|
|
2694
|
+
bindHostToolRoute(client, identity, channel, reason, scope) {
|
|
2695
|
+
log(`subc head scope: session=${identity.session} bind=${scope ? "scoped" : "unscoped"} channel=${channel} root=${identity.project_root} reason=${reason}` +
|
|
2696
|
+
(scope ? ` ref=${scope.ref.slice(0, 8)} epoch=${scope.scopeEpoch}` : ""));
|
|
2697
|
+
return client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
|
|
2698
|
+
consumerIdentity: this.consumerIdentity,
|
|
2699
|
+
...(scope ? { scope } : {}),
|
|
2700
|
+
});
|
|
2555
2701
|
}
|
|
2556
|
-
async openScopedHostToolRoute(client, identity, record, entry) {
|
|
2557
|
-
const
|
|
2702
|
+
async openScopedHostToolRoute(client, identity, record, entry, channel = "tool") {
|
|
2703
|
+
const decision = await this.hostScope(client, identity);
|
|
2704
|
+
const scope = decision.scope;
|
|
2705
|
+
let reason = decision.reason;
|
|
2558
2706
|
this.assertRecordLive(record);
|
|
2559
2707
|
if (scope) {
|
|
2560
2708
|
try {
|
|
2561
|
-
const route = await
|
|
2709
|
+
const route = await this.bindHostToolRoute(client, identity, channel, reason, scope);
|
|
2562
2710
|
if (entry)
|
|
2563
2711
|
entry.hostScope = scope;
|
|
2712
|
+
const state = this.hostScopeCache.get(identity.session);
|
|
2713
|
+
if (state?.promoteAfterBind &&
|
|
2714
|
+
state.answer?.result === "scope" &&
|
|
2715
|
+
state.answer.scope === scope) {
|
|
2716
|
+
state.promoteAfterBind = false;
|
|
2717
|
+
this.promoteHostScope(identity.session);
|
|
2718
|
+
}
|
|
2564
2719
|
return route;
|
|
2565
2720
|
}
|
|
2566
2721
|
catch (error) {
|
|
2722
|
+
log(`subc head scope: session=${identity.session} bind=failed channel=${channel} reason=scoped_open_error code=${error instanceof SubcError ? (error.code ?? error.name) : "Error"}`);
|
|
2567
2723
|
if (!(error instanceof SubcError) || !error.code || isSocketDeathError(error))
|
|
2568
2724
|
throw error;
|
|
2569
2725
|
// A restarting provider has not rejected the scope. Keep it for the normal reopen retry.
|
|
@@ -2573,22 +2729,27 @@ export class SubcTransportPool {
|
|
|
2573
2729
|
const state = this.hostScopeCache.get(identity.session);
|
|
2574
2730
|
if (state && isHostScopeSyncRefusal(error.code)) {
|
|
2575
2731
|
this.deferHostScopeBind(identity.session, state, error.code);
|
|
2732
|
+
reason = `scope_sync_pending code=${error.code}`;
|
|
2576
2733
|
}
|
|
2577
2734
|
else {
|
|
2578
2735
|
this.refuseHostScopeBind(identity.session, state, error.code);
|
|
2736
|
+
// Look up the host scope again after a cached scoped route is refused,
|
|
2737
|
+
// before opening an unscoped route.
|
|
2738
|
+
if (decision.reason === "cached_scope") {
|
|
2739
|
+
return this.openScopedHostToolRoute(client, identity, record, entry, channel);
|
|
2740
|
+
}
|
|
2741
|
+
reason = `scoped_bind_refused code=${error.code}`;
|
|
2579
2742
|
}
|
|
2580
2743
|
}
|
|
2581
2744
|
}
|
|
2582
2745
|
this.assertRecordLive(record);
|
|
2583
|
-
const route = await
|
|
2584
|
-
consumerIdentity: this.consumerIdentity,
|
|
2585
|
-
});
|
|
2746
|
+
const route = await this.bindHostToolRoute(client, identity, channel, reason);
|
|
2586
2747
|
// Scope recovery can finish while a fallback bind waits for the provider, including bg_events.
|
|
2587
2748
|
if (this.hostScopeCache.get(identity.session)?.answer?.result === "scope" &&
|
|
2588
2749
|
!this.hostScopeCache.get(identity.session)?.scopeBindPending &&
|
|
2589
2750
|
!this.hostScopeRefusals.has(identity.session)) {
|
|
2590
2751
|
safeCloseRoute(client, route);
|
|
2591
|
-
return this.openScopedHostToolRoute(client, identity, record, entry);
|
|
2752
|
+
return this.openScopedHostToolRoute(client, identity, record, entry, channel);
|
|
2592
2753
|
}
|
|
2593
2754
|
return route;
|
|
2594
2755
|
}
|
|
@@ -2689,7 +2850,7 @@ export class SubcTransportPool {
|
|
|
2689
2850
|
};
|
|
2690
2851
|
sub = new BgSubscription(identity, () => this.ensureClient(), (client, cause) => this.dropClient(client, cause), (client) => {
|
|
2691
2852
|
binding.hostScope = undefined;
|
|
2692
|
-
return this.openHostToolRoute(client, identity, record, binding);
|
|
2853
|
+
return this.openHostToolRoute(client, identity, record, binding, "bg_events");
|
|
2693
2854
|
}, onNudge, this.bgBackoffSleep, () => this.rootCanAttach(record.canonicalRoot), (error) => {
|
|
2694
2855
|
if (!isAbsentRootRouteError(error))
|
|
2695
2856
|
return false;
|