@cortexkit/aft-bridge 0.60.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-format.d.ts +5 -0
- package/dist/bash-format.d.ts.map +1 -1
- package/dist/bash-format.js +12 -5
- package/dist/bash-format.js.map +1 -1
- package/dist/bash-hints.d.ts.map +1 -1
- package/dist/bash-hints.js +3 -9
- package/dist/bash-hints.js.map +1 -1
- 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/bridge.d.ts +54 -9
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +210 -106
- package/dist/bridge.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 +8 -0
- package/dist/config-watch.js.map +1 -1
- package/dist/downloader.js +4 -3
- package/dist/downloader.js.map +1 -1
- package/dist/error-contract.d.ts +26 -4
- package/dist/error-contract.d.ts.map +1 -1
- package/dist/error-contract.js +135 -10
- package/dist/error-contract.js.map +1 -1
- package/dist/feature-config.d.ts +3 -0
- package/dist/feature-config.d.ts.map +1 -1
- package/dist/feature-config.js +49 -0
- package/dist/feature-config.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/lsp-github-install.d.ts +104 -0
- package/dist/lsp-github-install.d.ts.map +1 -0
- package/dist/lsp-github-install.js +1455 -0
- package/dist/lsp-github-install.js.map +1 -0
- package/dist/lsp-github-probe.d.ts +81 -0
- package/dist/lsp-github-probe.d.ts.map +1 -0
- package/dist/lsp-github-probe.js +131 -0
- package/dist/lsp-github-probe.js.map +1 -0
- package/dist/lsp-github-table.d.ts +64 -0
- package/dist/lsp-github-table.d.ts.map +1 -0
- package/dist/lsp-github-table.js +174 -0
- package/dist/lsp-github-table.js.map +1 -0
- package/dist/migration.d.ts +10 -3
- package/dist/migration.d.ts.map +1 -1
- package/dist/migration.js +19 -14
- package/dist/migration.js.map +1 -1
- package/dist/onnx-runtime.d.ts +2 -0
- package/dist/onnx-runtime.d.ts.map +1 -1
- package/dist/onnx-runtime.js +12 -7
- package/dist/onnx-runtime.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/pool.d.ts +6 -0
- package/dist/pool.d.ts.map +1 -1
- package/dist/pool.js +27 -7
- package/dist/pool.js.map +1 -1
- package/dist/project-identity.d.ts +2 -0
- package/dist/project-identity.d.ts.map +1 -1
- package/dist/project-identity.js +8 -2
- package/dist/project-identity.js.map +1 -1
- package/dist/protocol.d.ts +3 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +14 -1
- package/dist/protocol.js.map +1 -1
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +5 -2
- package/dist/resolver.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/revivable-transport.d.ts +12 -1
- package/dist/revivable-transport.d.ts.map +1 -1
- package/dist/revivable-transport.js +68 -7
- package/dist/revivable-transport.js.map +1 -1
- package/dist/subc-transport.d.ts +8 -0
- package/dist/subc-transport.d.ts.map +1 -1
- package/dist/subc-transport.js +275 -58
- package/dist/subc-transport.js.map +1 -1
- package/dist/tar-executable.d.ts +3 -1
- package/dist/tar-executable.d.ts.map +1 -1
- package/dist/tar-executable.js +35 -15
- package/dist/tar-executable.js.map +1 -1
- package/dist/transport-factory.d.ts.map +1 -1
- package/dist/transport-factory.js +8 -33
- package/dist/transport-factory.js.map +1 -1
- package/dist/transport.d.ts +2 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js.map +1 -1
- package/package.json +1 -1
package/dist/subc-transport.js
CHANGED
|
@@ -17,8 +17,9 @@
|
|
|
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
|
+
import { isTerminalStatus } from "./bash-format.js";
|
|
22
23
|
import { isRouteOpenReloadWindowError, isRouteRequestReloadRefusal } from "./error-contract.js";
|
|
23
24
|
import { asCanonicalRootPath, asRootGeneration, LifecycleRegistry, } from "./lifecycle-registry.js";
|
|
24
25
|
import { canonicalizeProjectRoot } from "./project-identity.js";
|
|
@@ -170,6 +171,81 @@ export function isSocketDeathError(error, depth = 0) {
|
|
|
170
171
|
}
|
|
171
172
|
return isConsumerReconnectTransient(error) || isSubcClientClosedError(error);
|
|
172
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
|
+
}
|
|
173
249
|
/**
|
|
174
250
|
* A call that never got an answer on a socket that may still be alive: the
|
|
175
251
|
* client's own request deadline (raw `request`/`routeOpen` reject with code
|
|
@@ -420,8 +496,10 @@ class BgSubscription {
|
|
|
420
496
|
get attachedRoute() {
|
|
421
497
|
return this.route;
|
|
422
498
|
}
|
|
499
|
+
cancelBackoff = null;
|
|
423
500
|
async stop() {
|
|
424
501
|
this.stopped = true;
|
|
502
|
+
this.cancelBackoff?.();
|
|
425
503
|
// Wake a live `await sub.closed`; the loop's `finally` remains the sole
|
|
426
504
|
// owner of closeRouteChannel, avoiding a double close from stop() + run().
|
|
427
505
|
const sub = this.current;
|
|
@@ -668,7 +746,39 @@ class BgSubscription {
|
|
|
668
746
|
giveUp("stopped");
|
|
669
747
|
}
|
|
670
748
|
async backoff(attempt) {
|
|
671
|
-
|
|
749
|
+
if (this.stopped)
|
|
750
|
+
return;
|
|
751
|
+
await new Promise((resolve, reject) => {
|
|
752
|
+
let timer;
|
|
753
|
+
let finished = false;
|
|
754
|
+
const settle = (error) => {
|
|
755
|
+
if (finished)
|
|
756
|
+
return;
|
|
757
|
+
finished = true;
|
|
758
|
+
if (timer)
|
|
759
|
+
clearTimeout(timer);
|
|
760
|
+
this.cancelBackoff = null;
|
|
761
|
+
if (error !== undefined)
|
|
762
|
+
reject(error);
|
|
763
|
+
else
|
|
764
|
+
resolve();
|
|
765
|
+
};
|
|
766
|
+
this.cancelBackoff = () => settle();
|
|
767
|
+
const delay = reconnectBackoffMs(attempt);
|
|
768
|
+
if (this.sleep) {
|
|
769
|
+
// An injected sleeper may not be cancellable. Observe its eventual
|
|
770
|
+
// rejection, but let stop release the subscription without awaiting it.
|
|
771
|
+
try {
|
|
772
|
+
void this.sleep(delay).then(() => settle(), settle);
|
|
773
|
+
}
|
|
774
|
+
catch (error) {
|
|
775
|
+
settle(error);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
timer = setTimeout(() => settle(), delay);
|
|
780
|
+
}
|
|
781
|
+
});
|
|
672
782
|
}
|
|
673
783
|
}
|
|
674
784
|
function isRecord(value) {
|
|
@@ -711,17 +821,25 @@ function callDeadlinePassedError(lastError, callDeadlineMs) {
|
|
|
711
821
|
return withDeadlineSuffix(lastError, suffix);
|
|
712
822
|
return new SubcError(`route to module '${AFT_MODULE_ID}' was not ready before the call ran out of time.${suffix}`, REQUEST_DEADLINE_CODE);
|
|
713
823
|
}
|
|
824
|
+
export const __withDeadlineSuffixForTests = withDeadlineSuffix;
|
|
714
825
|
function withDeadlineSuffix(error, suffix) {
|
|
715
|
-
if (error instanceof Error)
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
}
|
|
724
|
-
|
|
826
|
+
if (!(error instanceof Error))
|
|
827
|
+
return new Error(`${String(error)}${suffix}`, { cause: error });
|
|
828
|
+
const message = `${error.message}${suffix}`;
|
|
829
|
+
if (error instanceof SubcCallError) {
|
|
830
|
+
const wrapped = new SubcCallError(error.kind, message, error.code, error);
|
|
831
|
+
// SubcCallError's detail getter only unwraps a direct SubcError cause.
|
|
832
|
+
Object.defineProperty(wrapped, "detail", { value: error.detail });
|
|
833
|
+
return wrapped;
|
|
834
|
+
}
|
|
835
|
+
if (error instanceof SubcError) {
|
|
836
|
+
const wrapped = new SubcError(message, error.code, error.detail, error.closeReason);
|
|
837
|
+
Object.defineProperty(wrapped, "cause", { value: error });
|
|
838
|
+
return wrapped;
|
|
839
|
+
}
|
|
840
|
+
const wrapped = new Error(message, { cause: error });
|
|
841
|
+
wrapped.name = error.name;
|
|
842
|
+
return wrapped;
|
|
725
843
|
}
|
|
726
844
|
/**
|
|
727
845
|
* Fire-and-forget route close that can never throw — neither synchronously (a
|
|
@@ -924,7 +1042,29 @@ class SubcTransport {
|
|
|
924
1042
|
// dropping it here would cap long tool executions at the subc client's
|
|
925
1043
|
// default unary deadline while the command keeps running module-side.
|
|
926
1044
|
const timeoutMs = options.transportTimeoutMs ?? options.timeoutMs;
|
|
927
|
-
|
|
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;
|
|
928
1068
|
return { preview, timeoutMs, onProgress, abortSignal: options.abortSignal };
|
|
929
1069
|
}
|
|
930
1070
|
}
|
|
@@ -941,7 +1081,7 @@ export class SubcTransportPool {
|
|
|
941
1081
|
hostScopes;
|
|
942
1082
|
/** Scope identity is host-session-wide, even when the session uses multiple project roots. */
|
|
943
1083
|
hostScopeCache = new Map();
|
|
944
|
-
hostScopeRefusals = new
|
|
1084
|
+
hostScopeRefusals = new Map();
|
|
945
1085
|
hostScopeOutcomes = new Map();
|
|
946
1086
|
connectFn;
|
|
947
1087
|
onBgEventsNudge;
|
|
@@ -1008,8 +1148,7 @@ export class SubcTransportPool {
|
|
|
1008
1148
|
this.connectFn = options.connect ?? ((opts) => SubcClient.connect(opts));
|
|
1009
1149
|
this.onBgEventsNudge = options.onBgEventsNudge;
|
|
1010
1150
|
this.onBgEventsNudgeRef = options.onBgEventsNudgeRef;
|
|
1011
|
-
this.bgBackoffSleep =
|
|
1012
|
-
options.bgBackoffSleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1151
|
+
this.bgBackoffSleep = options.bgBackoffSleep;
|
|
1013
1152
|
this.routeRetrySleep =
|
|
1014
1153
|
options.routeRetrySleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1015
1154
|
this.bgDispatchProbeIntervalMs =
|
|
@@ -1395,8 +1534,12 @@ export class SubcTransportPool {
|
|
|
1395
1534
|
* always nudges a drain, replaying completions queued while no carrier existed.
|
|
1396
1535
|
*/
|
|
1397
1536
|
async reapIdleSessions(now = Date.now()) {
|
|
1398
|
-
for (const [key,
|
|
1399
|
-
|
|
1537
|
+
for (const [key, bucket] of this.nudgeDeliveryLogState) {
|
|
1538
|
+
for (const [kind, state] of bucket) {
|
|
1539
|
+
if (now - state.lastEmittedAt >= 15 * 60_000)
|
|
1540
|
+
bucket.delete(kind);
|
|
1541
|
+
}
|
|
1542
|
+
if (bucket.size === 0)
|
|
1400
1543
|
this.nudgeDeliveryLogState.delete(key);
|
|
1401
1544
|
}
|
|
1402
1545
|
const detached = [];
|
|
@@ -1484,7 +1627,7 @@ export class SubcTransportPool {
|
|
|
1484
1627
|
}
|
|
1485
1628
|
if (typeof data.task_id === "string") {
|
|
1486
1629
|
// Terminal tasks remain pinned until the daemon explicitly ACKs them.
|
|
1487
|
-
const terminal =
|
|
1630
|
+
const terminal = isTerminalStatus(data.status);
|
|
1488
1631
|
const running = ["starting", "running", "killing"].includes(String(data.status));
|
|
1489
1632
|
record.backgroundTasks.set(data.task_id, terminal ? "terminal" : running ? "running" : "unknown");
|
|
1490
1633
|
if (name === "bash" || name === "powershell")
|
|
@@ -1569,15 +1712,19 @@ export class SubcTransportPool {
|
|
|
1569
1712
|
};
|
|
1570
1713
|
}
|
|
1571
1714
|
logNudgeDelivery(kind, record, message) {
|
|
1572
|
-
|
|
1715
|
+
let bucket = this.nudgeDeliveryLogState.get(record.identityKey);
|
|
1716
|
+
if (!bucket) {
|
|
1717
|
+
bucket = new Map();
|
|
1718
|
+
this.nudgeDeliveryLogState.set(record.identityKey, bucket);
|
|
1719
|
+
}
|
|
1573
1720
|
const now = Date.now();
|
|
1574
|
-
const state =
|
|
1721
|
+
const state = bucket.get(kind);
|
|
1575
1722
|
if (state && now - state.lastEmittedAt < BG_LIFECYCLE_LOG_INTERVAL_MS) {
|
|
1576
1723
|
state.suppressed += 1;
|
|
1577
1724
|
return;
|
|
1578
1725
|
}
|
|
1579
1726
|
const suppressed = state?.suppressed ?? 0;
|
|
1580
|
-
|
|
1727
|
+
bucket.set(kind, { lastEmittedAt: now, suppressed: 0 });
|
|
1581
1728
|
const suffix = suppressed > 0 ? ` suppressed=${suppressed}` : "";
|
|
1582
1729
|
log(`subc bg_events: ${message}${suffix}`, { sessionId: record.identity.session });
|
|
1583
1730
|
}
|
|
@@ -1622,10 +1769,7 @@ export class SubcTransportPool {
|
|
|
1622
1769
|
record.teardownReason = reason;
|
|
1623
1770
|
this.sessions.delete(key);
|
|
1624
1771
|
this.removeIndexMembership(record);
|
|
1625
|
-
|
|
1626
|
-
if (logKey.endsWith(`\u0000${key}`))
|
|
1627
|
-
this.nudgeDeliveryLogState.delete(logKey);
|
|
1628
|
-
}
|
|
1772
|
+
this.nudgeDeliveryLogState.delete(key);
|
|
1629
1773
|
const bgSub = record.bgSub;
|
|
1630
1774
|
record.bgSub = null;
|
|
1631
1775
|
const routeEntry = record.routeEntry;
|
|
@@ -1991,7 +2135,7 @@ export class SubcTransportPool {
|
|
|
1991
2135
|
return reply;
|
|
1992
2136
|
}
|
|
1993
2137
|
catch (requestError) {
|
|
1994
|
-
const error = frameReadTimeoutError(routeClosedMidCallError(requestError, routeAndEntry.entry, record));
|
|
2138
|
+
const error = frameReadTimeoutError(routeClosedMidCallError(connectionClosedAfterDispatch(requestError), routeAndEntry.entry, record));
|
|
1995
2139
|
if (this.isReapInduced(record))
|
|
1996
2140
|
throw this.annotateReapError(error, record);
|
|
1997
2141
|
const ownsRoute = this.isCurrentSession(key, record) && this.client === client;
|
|
@@ -2038,6 +2182,7 @@ export class SubcTransportPool {
|
|
|
2038
2182
|
continue;
|
|
2039
2183
|
}
|
|
2040
2184
|
handleRequestFailure(error, routeAndEntry.entry);
|
|
2185
|
+
noteDispatchedRequestDeadline(error);
|
|
2041
2186
|
throw error;
|
|
2042
2187
|
}
|
|
2043
2188
|
}
|
|
@@ -2200,21 +2345,45 @@ export class SubcTransportPool {
|
|
|
2200
2345
|
return { route: await opening, entry };
|
|
2201
2346
|
}
|
|
2202
2347
|
hostScope(client, identity) {
|
|
2203
|
-
|
|
2204
|
-
|
|
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
|
+
}
|
|
2205
2360
|
let state = this.hostScopeCache.get(identity.session);
|
|
2206
2361
|
if (!state) {
|
|
2207
2362
|
state = { retryAt: 0, retryMs: HOST_SCOPE_RETRY_FLOOR_MS };
|
|
2208
2363
|
this.hostScopeCache.set(identity.session, state);
|
|
2209
2364
|
}
|
|
2210
2365
|
if (state.answer) {
|
|
2211
|
-
|
|
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
|
+
});
|
|
2212
2376
|
}
|
|
2213
|
-
if (state.pending)
|
|
2377
|
+
if (state.pending) {
|
|
2378
|
+
log(`subc head scope: session=${identity.session} lookup=skipped reason=inflight_lookup`);
|
|
2214
2379
|
return state.pending;
|
|
2380
|
+
}
|
|
2215
2381
|
// Backoff skips the lookup, not the bind, so an unavailable core cannot delay every root.
|
|
2216
|
-
if (Date.now() < state.retryAt)
|
|
2217
|
-
|
|
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}`);
|
|
2218
2387
|
const current = state;
|
|
2219
2388
|
clearTimeout(current.retryTimer);
|
|
2220
2389
|
current.retryTimer = undefined;
|
|
@@ -2223,25 +2392,36 @@ export class SubcTransportPool {
|
|
|
2223
2392
|
const lookup = this.readHostScope(client, identity, abort.signal).then((outcome) => {
|
|
2224
2393
|
// A scope-close push may invalidate the session while the management request is in flight.
|
|
2225
2394
|
if (this.hostScopeCache.get(identity.session) !== current)
|
|
2226
|
-
return
|
|
2395
|
+
return { reason: "lookup_invalidated" };
|
|
2227
2396
|
current.pending = undefined;
|
|
2228
2397
|
current.lookupAbort = undefined;
|
|
2229
2398
|
if (abort.signal.aborted)
|
|
2230
|
-
return
|
|
2399
|
+
return { reason: "lookup_cancelled" };
|
|
2231
2400
|
this.logHostScopeOutcome(identity.session, outcome);
|
|
2401
|
+
const reason = outcome.result === "error"
|
|
2402
|
+
? `lookup_error code=${outcome.code}`
|
|
2403
|
+
: `lookup_${outcome.result}`;
|
|
2232
2404
|
if (outcome.result === "scope" || outcome.result === "no_scope") {
|
|
2233
2405
|
current.answer = outcome;
|
|
2234
2406
|
current.retryAt = 0;
|
|
2235
2407
|
current.retryMs = HOST_SCOPE_RETRY_FLOOR_MS;
|
|
2236
|
-
if (outcome.result === "scope")
|
|
2237
|
-
|
|
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
|
+
}
|
|
2238
2414
|
}
|
|
2239
2415
|
else {
|
|
2416
|
+
current.unscopedReason = reason;
|
|
2240
2417
|
current.retryAt = Date.now() + current.retryMs;
|
|
2241
2418
|
current.retryMs = Math.min(current.retryMs * 2, HOST_SCOPE_RETRY_CAP_MS);
|
|
2242
2419
|
this.scheduleHostScopeRetry(identity.session, current);
|
|
2243
2420
|
}
|
|
2244
|
-
return
|
|
2421
|
+
return {
|
|
2422
|
+
scope: outcome.result === "scope" ? outcome.scope : undefined,
|
|
2423
|
+
reason,
|
|
2424
|
+
};
|
|
2245
2425
|
});
|
|
2246
2426
|
current.pending = lookup;
|
|
2247
2427
|
return lookup;
|
|
@@ -2304,7 +2484,7 @@ export class SubcTransportPool {
|
|
|
2304
2484
|
retry = this.hostScope(client, record.identity);
|
|
2305
2485
|
}
|
|
2306
2486
|
void retry.catch((error) => {
|
|
2307
|
-
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"}`);
|
|
2308
2488
|
});
|
|
2309
2489
|
}, Math.max(0, state.retryAt - Date.now()));
|
|
2310
2490
|
state.retryTimer.unref();
|
|
@@ -2320,8 +2500,8 @@ export class SubcTransportPool {
|
|
|
2320
2500
|
}
|
|
2321
2501
|
refuseHostScopeBind(session, state, code) {
|
|
2322
2502
|
if (!this.hostScopeRefusals.has(session)) {
|
|
2323
|
-
this.hostScopeRefusals.
|
|
2324
|
-
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`);
|
|
2325
2505
|
}
|
|
2326
2506
|
if (state) {
|
|
2327
2507
|
state.scopeBindPending = false;
|
|
@@ -2332,7 +2512,7 @@ export class SubcTransportPool {
|
|
|
2332
2512
|
async retryHostScopeBind(client, identity, state, scope) {
|
|
2333
2513
|
try {
|
|
2334
2514
|
// Validate daemon acceptance before retiring a working fallback route or its held requests.
|
|
2335
|
-
const route = await
|
|
2515
|
+
const route = await this.bindHostToolRoute(client, identity, "scope_probe", "scope_sync_retry", scope);
|
|
2336
2516
|
safeCloseRoute(client, route);
|
|
2337
2517
|
if (this.hostScopeCache.get(identity.session) !== state ||
|
|
2338
2518
|
!state.scopeBindPending ||
|
|
@@ -2341,6 +2521,7 @@ export class SubcTransportPool {
|
|
|
2341
2521
|
!this.unscopedHostRecord(identity.session))
|
|
2342
2522
|
return;
|
|
2343
2523
|
state.scopeBindPending = false;
|
|
2524
|
+
state.promoteAfterBind = false;
|
|
2344
2525
|
state.retryAt = 0;
|
|
2345
2526
|
state.retryMs = HOST_SCOPE_RETRY_FLOOR_MS;
|
|
2346
2527
|
log(`subc head scope: session=${identity.session} scoped bind recovered ref=${scope.ref.slice(0, 8)} epoch=${scope.scopeEpoch}`);
|
|
@@ -2496,27 +2677,49 @@ export class SubcTransportPool {
|
|
|
2496
2677
|
signal.removeEventListener("abort", cancel);
|
|
2497
2678
|
}
|
|
2498
2679
|
}
|
|
2499
|
-
openHostToolRoute(client, identity, record, entry) {
|
|
2680
|
+
openHostToolRoute(client, identity, record, entry, channel = "tool") {
|
|
2500
2681
|
if (!this.hostScopes ||
|
|
2501
2682
|
!["opencode", "pi"].includes(identity.harness) ||
|
|
2502
2683
|
identity.session === DEFAULT_SESSION_ID) {
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
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
|
+
});
|
|
2508
2701
|
}
|
|
2509
|
-
async openScopedHostToolRoute(client, identity, record, entry) {
|
|
2510
|
-
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;
|
|
2511
2706
|
this.assertRecordLive(record);
|
|
2512
2707
|
if (scope) {
|
|
2513
2708
|
try {
|
|
2514
|
-
const route = await
|
|
2709
|
+
const route = await this.bindHostToolRoute(client, identity, channel, reason, scope);
|
|
2515
2710
|
if (entry)
|
|
2516
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
|
+
}
|
|
2517
2719
|
return route;
|
|
2518
2720
|
}
|
|
2519
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"}`);
|
|
2520
2723
|
if (!(error instanceof SubcError) || !error.code || isSocketDeathError(error))
|
|
2521
2724
|
throw error;
|
|
2522
2725
|
// A restarting provider has not rejected the scope. Keep it for the normal reopen retry.
|
|
@@ -2526,22 +2729,27 @@ export class SubcTransportPool {
|
|
|
2526
2729
|
const state = this.hostScopeCache.get(identity.session);
|
|
2527
2730
|
if (state && isHostScopeSyncRefusal(error.code)) {
|
|
2528
2731
|
this.deferHostScopeBind(identity.session, state, error.code);
|
|
2732
|
+
reason = `scope_sync_pending code=${error.code}`;
|
|
2529
2733
|
}
|
|
2530
2734
|
else {
|
|
2531
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}`;
|
|
2532
2742
|
}
|
|
2533
2743
|
}
|
|
2534
2744
|
}
|
|
2535
2745
|
this.assertRecordLive(record);
|
|
2536
|
-
const route = await
|
|
2537
|
-
consumerIdentity: this.consumerIdentity,
|
|
2538
|
-
});
|
|
2746
|
+
const route = await this.bindHostToolRoute(client, identity, channel, reason);
|
|
2539
2747
|
// Scope recovery can finish while a fallback bind waits for the provider, including bg_events.
|
|
2540
2748
|
if (this.hostScopeCache.get(identity.session)?.answer?.result === "scope" &&
|
|
2541
2749
|
!this.hostScopeCache.get(identity.session)?.scopeBindPending &&
|
|
2542
2750
|
!this.hostScopeRefusals.has(identity.session)) {
|
|
2543
2751
|
safeCloseRoute(client, route);
|
|
2544
|
-
return this.openScopedHostToolRoute(client, identity, record, entry);
|
|
2752
|
+
return this.openScopedHostToolRoute(client, identity, record, entry, channel);
|
|
2545
2753
|
}
|
|
2546
2754
|
return route;
|
|
2547
2755
|
}
|
|
@@ -2596,6 +2804,15 @@ export class SubcTransportPool {
|
|
|
2596
2804
|
if (!this.rootCanAttach(record.canonicalRoot))
|
|
2597
2805
|
return;
|
|
2598
2806
|
const nudgeRef = this.nudgeRefFor(record);
|
|
2807
|
+
const dispatch = (callback) => {
|
|
2808
|
+
const failed = (error) => warn(`subc bg nudge delivery failed: ${describeFailure(error)}`);
|
|
2809
|
+
try {
|
|
2810
|
+
void Promise.resolve(callback()).catch(failed);
|
|
2811
|
+
}
|
|
2812
|
+
catch (error) {
|
|
2813
|
+
failed(error);
|
|
2814
|
+
}
|
|
2815
|
+
};
|
|
2599
2816
|
const onNudge = (observedEvent) => {
|
|
2600
2817
|
const currentRecord = this.currentSessionForNudge(identity);
|
|
2601
2818
|
if (!currentRecord) {
|
|
@@ -2611,14 +2828,14 @@ export class SubcTransportPool {
|
|
|
2611
2828
|
const currentRef = this.nudgeRefFor(currentRecord);
|
|
2612
2829
|
let delivered = false;
|
|
2613
2830
|
if (currentRef && this.onBgEventsNudgeRef) {
|
|
2614
|
-
this.onBgEventsNudgeRef(currentRef);
|
|
2831
|
+
dispatch(() => this.onBgEventsNudgeRef?.(currentRef));
|
|
2615
2832
|
delivered = true;
|
|
2616
2833
|
}
|
|
2617
2834
|
if (this.onBgEventsNudge) {
|
|
2618
2835
|
if (!currentRef && this.onBgEventsNudgeRef) {
|
|
2619
2836
|
this.logNudgeDelivery("fallback-missing-generation", currentRecord, `nudge dispatch fallback=root-session-handler cause=generation-provenance-unavailable root=${currentRecord.canonicalRoot}`);
|
|
2620
2837
|
}
|
|
2621
|
-
this.onBgEventsNudge(currentRecord.identity.project_root, currentRecord.identity.session);
|
|
2838
|
+
dispatch(() => this.onBgEventsNudge?.(currentRecord.identity.project_root, currentRecord.identity.session));
|
|
2622
2839
|
delivered = true;
|
|
2623
2840
|
}
|
|
2624
2841
|
if (delivered)
|
|
@@ -2633,7 +2850,7 @@ export class SubcTransportPool {
|
|
|
2633
2850
|
};
|
|
2634
2851
|
sub = new BgSubscription(identity, () => this.ensureClient(), (client, cause) => this.dropClient(client, cause), (client) => {
|
|
2635
2852
|
binding.hostScope = undefined;
|
|
2636
|
-
return this.openHostToolRoute(client, identity, record, binding);
|
|
2853
|
+
return this.openHostToolRoute(client, identity, record, binding, "bg_events");
|
|
2637
2854
|
}, onNudge, this.bgBackoffSleep, () => this.rootCanAttach(record.canonicalRoot), (error) => {
|
|
2638
2855
|
if (!isAbsentRootRouteError(error))
|
|
2639
2856
|
return false;
|