@cortexkit/aft-bridge 0.58.2 → 0.59.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.map +1 -1
- package/dist/bash-format.js +5 -0
- package/dist/bash-format.js.map +1 -1
- package/dist/bash-hints.d.ts +76 -30
- package/dist/bash-hints.d.ts.map +1 -1
- package/dist/bash-hints.js +218 -47
- package/dist/bash-hints.js.map +1 -1
- package/dist/bash-host-fallback.d.ts +27 -0
- package/dist/bash-host-fallback.d.ts.map +1 -1
- package/dist/bash-host-fallback.js +55 -16
- package/dist/bash-host-fallback.js.map +1 -1
- package/dist/binary-identity.d.ts +13 -0
- package/dist/binary-identity.d.ts.map +1 -1
- package/dist/binary-identity.js +91 -2
- package/dist/binary-identity.js.map +1 -1
- package/dist/bridge.d.ts +12 -3
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +80 -38
- package/dist/bridge.js.map +1 -1
- package/dist/callgraph-format.d.ts.map +1 -1
- package/dist/callgraph-format.js +7 -7
- package/dist/callgraph-format.js.map +1 -1
- package/dist/child-process.d.ts.map +1 -1
- package/dist/child-process.js +10 -1
- package/dist/child-process.js.map +1 -1
- package/dist/config-error.d.ts +2 -2
- package/dist/config-error.js +2 -2
- package/dist/config-watch.d.ts +2 -0
- package/dist/config-watch.d.ts.map +1 -1
- package/dist/config-watch.js +35 -0
- package/dist/config-watch.js.map +1 -1
- package/dist/durable-log.d.ts +11 -0
- package/dist/durable-log.d.ts.map +1 -1
- package/dist/durable-log.js +67 -24
- package/dist/durable-log.js.map +1 -1
- package/dist/error-contract.d.ts +14 -5
- package/dist/error-contract.d.ts.map +1 -1
- package/dist/error-contract.js +42 -9
- package/dist/error-contract.js.map +1 -1
- package/dist/feature-config.d.ts +35 -18
- package/dist/feature-config.d.ts.map +1 -1
- package/dist/feature-config.js +161 -94
- package/dist/feature-config.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/location-lifecycle.d.ts.map +1 -1
- package/dist/location-lifecycle.js +4 -2
- package/dist/location-lifecycle.js.map +1 -1
- package/dist/message-detach.d.ts +8 -0
- package/dist/message-detach.d.ts.map +1 -1
- package/dist/message-detach.js +32 -0
- package/dist/message-detach.js.map +1 -1
- package/dist/migration-notices.d.ts.map +1 -1
- package/dist/migration-notices.js +12 -5
- package/dist/migration-notices.js.map +1 -1
- package/dist/migration.d.ts +1 -0
- package/dist/migration.d.ts.map +1 -1
- package/dist/migration.js +39 -12
- package/dist/migration.js.map +1 -1
- package/dist/onnx-runtime.d.ts.map +1 -1
- package/dist/onnx-runtime.js +37 -32
- package/dist/onnx-runtime.js.map +1 -1
- package/dist/path-display.d.ts +5 -0
- package/dist/path-display.d.ts.map +1 -0
- package/dist/path-display.js +21 -0
- package/dist/path-display.js.map +1 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +7 -6
- package/dist/paths.js.map +1 -1
- package/dist/private-storage.d.ts +9 -0
- package/dist/private-storage.d.ts.map +1 -0
- package/dist/private-storage.js +75 -0
- package/dist/private-storage.js.map +1 -0
- package/dist/protocol.d.ts +4 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/revivable-transport.d.ts +4 -2
- package/dist/revivable-transport.d.ts.map +1 -1
- package/dist/revivable-transport.js +48 -19
- package/dist/revivable-transport.js.map +1 -1
- package/dist/storage-paths.d.ts +3 -4
- package/dist/storage-paths.d.ts.map +1 -1
- package/dist/storage-paths.js +4 -5
- package/dist/storage-paths.js.map +1 -1
- package/dist/subc-transport.d.ts +61 -3
- package/dist/subc-transport.d.ts.map +1 -1
- package/dist/subc-transport.js +383 -43
- package/dist/subc-transport.js.map +1 -1
- package/dist/tar-executable.d.ts +1 -1
- package/dist/tar-executable.d.ts.map +1 -1
- package/dist/tar-executable.js +16 -2
- package/dist/tar-executable.js.map +1 -1
- package/dist/test-child-environment.d.ts +8 -0
- package/dist/test-child-environment.d.ts.map +1 -0
- package/dist/test-child-environment.js +105 -0
- package/dist/test-child-environment.js.map +1 -0
- package/dist/transport.d.ts +14 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +17 -0
- package/dist/transport.js.map +1 -1
- package/dist/zoom-format.d.ts +1 -0
- package/dist/zoom-format.d.ts.map +1 -1
- package/dist/zoom-format.js +5 -1
- package/dist/zoom-format.js.map +1 -1
- package/package.json +2 -2
package/dist/subc-transport.js
CHANGED
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
* top of it. subc-client is a build-time path dependency bundled into the
|
|
17
17
|
* published plugin dist; it is never a published runtime dependency.
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
19
|
+
import { statSync } from "node:fs";
|
|
20
20
|
import { AuthError, connectionFileExists, isConsumerReconnectTransient, StaleRouteHandleError, SubcCallError, SubcClient, SubcError, } from "@cortexkit/subc-client";
|
|
21
21
|
import { log, warn } from "./active-logger.js";
|
|
22
22
|
import { isRouteOpenReloadWindowError, isRouteRequestReloadRefusal } from "./error-contract.js";
|
|
23
23
|
import { asCanonicalRootPath, asRootGeneration, LifecycleRegistry, } from "./lifecycle-registry.js";
|
|
24
24
|
import { canonicalizeProjectRoot } from "./project-identity.js";
|
|
25
|
-
import { WORKER_SESSION_FIELD } from "./transport.js";
|
|
25
|
+
import { callPresetFor, PRESET_FIELD, WORKER_SESSION_FIELD } from "./transport.js";
|
|
26
26
|
/** The subc pool is closing and cannot carry another request. */
|
|
27
27
|
export class SubcTransportShuttingDownError extends SubcCallError {
|
|
28
28
|
constructor() {
|
|
@@ -30,6 +30,51 @@ export class SubcTransportShuttingDownError extends SubcCallError {
|
|
|
30
30
|
this.name = "SubcTransportShuttingDownError";
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The plugin closed one of its own routes (pool shutdown, session close,
|
|
35
|
+
* project-root close, or a sibling call on the same session discarding the
|
|
36
|
+
* shared route) while this call's request was already in flight on it.
|
|
37
|
+
*
|
|
38
|
+
* subc-client records a request as pending and starts writing its frame in the
|
|
39
|
+
* same synchronous step, and its `closeRoute` rejects exactly those pending
|
|
40
|
+
* requests. So a request rejected that way was written, or queued to be
|
|
41
|
+
* written, and the daemon may have run it: the outcome is unknown. The call
|
|
42
|
+
* must not be blind-retried, and bash must not re-run it on the host.
|
|
43
|
+
*/
|
|
44
|
+
export class SubcRouteClosedMidCallError extends SubcCallError {
|
|
45
|
+
reason;
|
|
46
|
+
constructor(reason, cause) {
|
|
47
|
+
super("outcome_unknown", `the command may have run; the route closed mid-call: ${reason}`, "route_closed", cause);
|
|
48
|
+
this.name = "SubcRouteClosedMidCallError";
|
|
49
|
+
this.reason = reason;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* subc-client's rejection of a request that was pending when the client itself
|
|
54
|
+
* closed the route: `closeRoute` tearing that route down, or the client closing
|
|
55
|
+
* its connection. The client types both with `closeReason: "closed_by_caller"`;
|
|
56
|
+
* a daemon GOODBYE carries the daemon's reason and a lost connection carries
|
|
57
|
+
* `"connection_lost"`. A route closed with no request pending produces no error
|
|
58
|
+
* at all, and a request started on an already-closed route fails with
|
|
59
|
+
* `StaleRouteHandleError`. A route closed while still opening is reported as a
|
|
60
|
+
* not-sent `SubcCallError`, which is not a `SubcError` and so never matches.
|
|
61
|
+
*/
|
|
62
|
+
export function isLocalRouteCloseRejection(error) {
|
|
63
|
+
return error instanceof SubcError && error.closeReason === "closed_by_caller";
|
|
64
|
+
}
|
|
65
|
+
/** The route-close reason that matches a session's teardown, if it was torn down. */
|
|
66
|
+
function teardownCloseReason(teardown) {
|
|
67
|
+
switch (teardown) {
|
|
68
|
+
case "shutdown":
|
|
69
|
+
return "transport shutting down";
|
|
70
|
+
case "session_closed":
|
|
71
|
+
return "session closed";
|
|
72
|
+
case "root_reaped":
|
|
73
|
+
return "project root closed";
|
|
74
|
+
default:
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
33
78
|
class ReconnectKeyChangedError extends Error {
|
|
34
79
|
constructor() {
|
|
35
80
|
super("the subc daemon restarted and its connection key changed; the reconnect did not complete within the deadline, retry");
|
|
@@ -56,6 +101,10 @@ export function isSubcClientClosedError(error) {
|
|
|
56
101
|
* the connection has already been torn down locally.
|
|
57
102
|
*/
|
|
58
103
|
export function isSocketDeathError(error, depth = 0) {
|
|
104
|
+
// Raw data-plane requests wrap socket death in a SubcError; its typed
|
|
105
|
+
// connection-loss reason survives even when the underlying errno does not.
|
|
106
|
+
if (error instanceof SubcError && error.closeReason === "connection_lost")
|
|
107
|
+
return true;
|
|
59
108
|
if (error instanceof SubcCallError) {
|
|
60
109
|
const cause = error.cause;
|
|
61
110
|
return depth < 4 && cause !== undefined && cause !== error
|
|
@@ -350,6 +399,11 @@ class BgSubscription {
|
|
|
350
399
|
const initial = client.droppedIngressFrames;
|
|
351
400
|
if (typeof initial !== "number")
|
|
352
401
|
return () => undefined;
|
|
402
|
+
const existing = dispatchProbes.get(client);
|
|
403
|
+
if (existing) {
|
|
404
|
+
existing.users += 1;
|
|
405
|
+
return () => releaseDispatchProbe(client);
|
|
406
|
+
}
|
|
353
407
|
let previous = initial;
|
|
354
408
|
const timer = setInterval(() => {
|
|
355
409
|
const total = client.droppedIngressFrames;
|
|
@@ -359,8 +413,10 @@ class BgSubscription {
|
|
|
359
413
|
previous = total;
|
|
360
414
|
this.info("dispatch-epoch-drop", `client ingress epoch drops scope=client observed_while_channel=${routeId} delta=${delta} total=${total}`);
|
|
361
415
|
}, this.dispatchProbeIntervalMs);
|
|
416
|
+
dispatchProbeTimers += 1;
|
|
362
417
|
timer.unref?.();
|
|
363
|
-
|
|
418
|
+
dispatchProbes.set(client, { users: 1, timer });
|
|
419
|
+
return () => releaseDispatchProbe(client);
|
|
364
420
|
}
|
|
365
421
|
async run() {
|
|
366
422
|
let backoffAttempt = 0;
|
|
@@ -476,7 +532,7 @@ class BgSubscription {
|
|
|
476
532
|
if (!this.isCurrent()) {
|
|
477
533
|
this.info("nudge-stale-carrier", `nudge carried by stale subscription; checking current session channel=${routeId}`);
|
|
478
534
|
}
|
|
479
|
-
this.onNudge();
|
|
535
|
+
this.onNudge(true);
|
|
480
536
|
});
|
|
481
537
|
this.current = sub;
|
|
482
538
|
this.attached = client;
|
|
@@ -494,7 +550,7 @@ class BgSubscription {
|
|
|
494
550
|
// A reconnect may have missed completions, so force a drain after every
|
|
495
551
|
// successful subscribe to replay anything queued while the stream was down.
|
|
496
552
|
if (!this.stopped && this.isCurrent())
|
|
497
|
-
this.onNudge();
|
|
553
|
+
this.onNudge(false);
|
|
498
554
|
await sub.closed;
|
|
499
555
|
this.info("stream-end", `stream ended channel=${routeId}`);
|
|
500
556
|
if (this.stopped) {
|
|
@@ -610,6 +666,21 @@ function abortedRequestError() {
|
|
|
610
666
|
error.name = "AbortError";
|
|
611
667
|
return error;
|
|
612
668
|
}
|
|
669
|
+
// The metric is connection-wide, so observing it needs one timer, not one per
|
|
670
|
+
// session. Last unsubscribe releases the timer, including on a dead socket.
|
|
671
|
+
const dispatchProbes = new Map();
|
|
672
|
+
let dispatchProbeTimers = 0;
|
|
673
|
+
function releaseDispatchProbe(client) {
|
|
674
|
+
const probe = dispatchProbes.get(client);
|
|
675
|
+
if (!probe || --probe.users > 0)
|
|
676
|
+
return;
|
|
677
|
+
clearInterval(probe.timer);
|
|
678
|
+
dispatchProbeTimers -= 1;
|
|
679
|
+
dispatchProbes.delete(client);
|
|
680
|
+
}
|
|
681
|
+
export function __dispatchProbeCountForTests() {
|
|
682
|
+
return dispatchProbeTimers;
|
|
683
|
+
}
|
|
613
684
|
/**
|
|
614
685
|
* A route open that resolved AFTER its session was torn down (closeSession /
|
|
615
686
|
* shutdown / client swap). The caller's request can't proceed, but this is an
|
|
@@ -618,6 +689,25 @@ function abortedRequestError() {
|
|
|
618
689
|
*/
|
|
619
690
|
class RouteTornDownError extends Error {
|
|
620
691
|
}
|
|
692
|
+
/**
|
|
693
|
+
* True when a call stopped at a teardown before its request was handed to the
|
|
694
|
+
* client (a closed session, or a route that opened after the teardown), so the
|
|
695
|
+
* request provably never reached AFT.
|
|
696
|
+
*/
|
|
697
|
+
export function isSubcRouteTornDownError(error) {
|
|
698
|
+
return error instanceof RouteTornDownError;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Turn subc-client's bare rejection of a request that was in flight on a route
|
|
702
|
+
* the plugin closed into an outcome-unknown error that names the close reason.
|
|
703
|
+
* Any other error passes through unchanged.
|
|
704
|
+
*/
|
|
705
|
+
function routeClosedMidCallError(error, entry, record) {
|
|
706
|
+
if (!isLocalRouteCloseRejection(error))
|
|
707
|
+
return error;
|
|
708
|
+
const reason = teardownCloseReason(record.teardownReason) ?? entry.closedBy ?? "route closed by the plugin";
|
|
709
|
+
return new SubcRouteClosedMidCallError(reason, error);
|
|
710
|
+
}
|
|
621
711
|
/**
|
|
622
712
|
* Re-lift the route reply into the flat {@link ToolCallResult} shape the standalone
|
|
623
713
|
* `BinaryBridge.toolCall` returns. The Rust module wraps the full flat response
|
|
@@ -709,6 +799,7 @@ class SubcTransport {
|
|
|
709
799
|
body.preview = true;
|
|
710
800
|
if (options?.workerSession === true)
|
|
711
801
|
body[WORKER_SESSION_FIELD] = true;
|
|
802
|
+
body[PRESET_FIELD] = callPresetFor(options?.workerSession === true);
|
|
712
803
|
const reply = await this.pool.routeRequest(this.identityFor(sessionId), body, timeoutMs, onProgress, this.generation, abortSignal);
|
|
713
804
|
return reliftReply(reply);
|
|
714
805
|
}
|
|
@@ -736,6 +827,7 @@ class SubcTransport {
|
|
|
736
827
|
const body = { name: command, arguments: args };
|
|
737
828
|
if (workerSession === true)
|
|
738
829
|
body[WORKER_SESSION_FIELD] = true;
|
|
830
|
+
body[PRESET_FIELD] = callPresetFor(workerSession === true);
|
|
739
831
|
const editSlotSurvives = this.pool.getEditSlotSurvives();
|
|
740
832
|
if (editSlotSurvives !== undefined)
|
|
741
833
|
body.edit_slot_survives = editSlotSurvives;
|
|
@@ -791,6 +883,10 @@ export class SubcTransportPool {
|
|
|
791
883
|
routeReopenRetryMs = null;
|
|
792
884
|
/** Per-session records keyed by the opaque identity key. */
|
|
793
885
|
sessions = new Map();
|
|
886
|
+
// Pending host creation events are bounded. Forgetting one only makes that
|
|
887
|
+
// session unknown (retained), never falsely eligible for eviction.
|
|
888
|
+
observedStarts = new Set();
|
|
889
|
+
sessionSweep = null;
|
|
794
890
|
/**
|
|
795
891
|
* The sole root-scoped session enumeration authority. Keys are opaque and are
|
|
796
892
|
* removed by the same detacher that removes the corresponding session record.
|
|
@@ -912,9 +1008,10 @@ export class SubcTransportPool {
|
|
|
912
1008
|
return asCanonicalRootPath(canonicalizeProjectRoot(projectRoot));
|
|
913
1009
|
}
|
|
914
1010
|
/**
|
|
915
|
-
* Check a root immediately before opening a route.
|
|
916
|
-
*
|
|
917
|
-
*
|
|
1011
|
+
* Check a root immediately before opening a route. Only the directory's
|
|
1012
|
+
* existence decides: a reclaim marker beside it (`<root>.reclaimed`) needs
|
|
1013
|
+
* no check, since an absent directory suspends the bind either way and a
|
|
1014
|
+
* directory that has returned wins over a stale marker.
|
|
918
1015
|
*/
|
|
919
1016
|
rootCanAttach(root) {
|
|
920
1017
|
let directoryExists = false;
|
|
@@ -928,13 +1025,6 @@ export class SubcTransportPool {
|
|
|
928
1025
|
this.dormantRoots.delete(root);
|
|
929
1026
|
return true;
|
|
930
1027
|
}
|
|
931
|
-
// Read the marker only as a latency hint. Directory absence is sufficient
|
|
932
|
-
// to suspend the bind, and the marker's JSON is intentionally irrelevant.
|
|
933
|
-
const reclaimedMarkerPresent = existsSync(`${root}.reclaimed`);
|
|
934
|
-
if (reclaimedMarkerPresent) {
|
|
935
|
-
this.markRootDormant(root);
|
|
936
|
-
return false;
|
|
937
|
-
}
|
|
938
1028
|
this.markRootDormant(root);
|
|
939
1029
|
return false;
|
|
940
1030
|
}
|
|
@@ -1166,6 +1256,7 @@ export class SubcTransportPool {
|
|
|
1166
1256
|
if (this.lifecycleEnabled() && record.generation !== generation) {
|
|
1167
1257
|
throw this.generationExpiredError(root, generation ?? asRootGeneration(1));
|
|
1168
1258
|
}
|
|
1259
|
+
record.lastUsedAt = Date.now();
|
|
1169
1260
|
return record;
|
|
1170
1261
|
}
|
|
1171
1262
|
record = {
|
|
@@ -1174,10 +1265,18 @@ export class SubcTransportPool {
|
|
|
1174
1265
|
canonicalRoot: root,
|
|
1175
1266
|
generation,
|
|
1176
1267
|
routeEntry: null,
|
|
1268
|
+
retiredRoutes: new Set(),
|
|
1177
1269
|
bgSub: null,
|
|
1178
1270
|
closed: false,
|
|
1179
1271
|
teardownReason: null,
|
|
1180
1272
|
inflight: 0,
|
|
1273
|
+
lastUsedAt: Date.now(),
|
|
1274
|
+
observedFromStart: this.observedStarts.delete(key),
|
|
1275
|
+
backgroundTasks: new Map(),
|
|
1276
|
+
backgroundWatches: new Map(),
|
|
1277
|
+
unknownBackgroundWork: false,
|
|
1278
|
+
pendingBgNudge: false,
|
|
1279
|
+
pendingDetach: false,
|
|
1181
1280
|
};
|
|
1182
1281
|
this.sessions.set(key, record);
|
|
1183
1282
|
let keys = this.rootIndex.get(root);
|
|
@@ -1186,8 +1285,173 @@ export class SubcTransportPool {
|
|
|
1186
1285
|
this.rootIndex.set(root, keys);
|
|
1187
1286
|
}
|
|
1188
1287
|
keys.add(key);
|
|
1288
|
+
if (!this.sessionSweep) {
|
|
1289
|
+
this.sessionSweep = setInterval(() => {
|
|
1290
|
+
void this.reapIdleSessions();
|
|
1291
|
+
}, 60_000);
|
|
1292
|
+
this.sessionSweep.unref?.();
|
|
1293
|
+
}
|
|
1189
1294
|
return record;
|
|
1190
1295
|
}
|
|
1296
|
+
/** Release only idle carriers, not the daemon's durable session or completion queue.
|
|
1297
|
+
* The next request opens a new route and subscribes again; subscription startup
|
|
1298
|
+
* always nudges a drain, replaying completions queued while no carrier existed.
|
|
1299
|
+
*/
|
|
1300
|
+
async reapIdleSessions(now = Date.now()) {
|
|
1301
|
+
for (const [key, state] of this.nudgeDeliveryLogState) {
|
|
1302
|
+
if (now - state.lastEmittedAt >= 15 * 60_000)
|
|
1303
|
+
this.nudgeDeliveryLogState.delete(key);
|
|
1304
|
+
}
|
|
1305
|
+
const detached = [];
|
|
1306
|
+
for (const [key, record] of this.sessions) {
|
|
1307
|
+
if (
|
|
1308
|
+
// The wire has no session-wide background census. A resumed/rebound
|
|
1309
|
+
// session may have tasks or watches this pool never saw, so only a
|
|
1310
|
+
// host-observed creation can establish a complete lifetime history.
|
|
1311
|
+
!record.observedFromStart ||
|
|
1312
|
+
record.backgroundTasks.size > 0 ||
|
|
1313
|
+
record.backgroundWatches.size > 0 ||
|
|
1314
|
+
record.unknownBackgroundWork ||
|
|
1315
|
+
record.pendingBgNudge ||
|
|
1316
|
+
record.pendingDetach ||
|
|
1317
|
+
record.inflight > 0 ||
|
|
1318
|
+
record.retiredRoutes.size > 0 ||
|
|
1319
|
+
record.routeEntry?.opening ||
|
|
1320
|
+
now - record.lastUsedAt < 15 * 60_000)
|
|
1321
|
+
continue;
|
|
1322
|
+
const owner = this.detachSession(key, "session_closed");
|
|
1323
|
+
if (owner)
|
|
1324
|
+
detached.push(owner);
|
|
1325
|
+
}
|
|
1326
|
+
if (this.sessions.size === 0 && this.sessionSweep) {
|
|
1327
|
+
clearInterval(this.sessionSweep);
|
|
1328
|
+
this.sessionSweep = null;
|
|
1329
|
+
}
|
|
1330
|
+
const cleanup = Promise.allSettled(detached.map((owner) => this.cleanupDetached(owner)));
|
|
1331
|
+
this.pendingRootCleanups.add(cleanup);
|
|
1332
|
+
try {
|
|
1333
|
+
await cleanup;
|
|
1334
|
+
}
|
|
1335
|
+
finally {
|
|
1336
|
+
this.pendingRootCleanups.delete(cleanup);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
observeSessionStart(projectRoot, session) {
|
|
1340
|
+
if (this.shuttingDown || !session)
|
|
1341
|
+
return;
|
|
1342
|
+
const key = identityKey({
|
|
1343
|
+
project_root: canonicalizeProjectRoot(projectRoot),
|
|
1344
|
+
harness: this.harness,
|
|
1345
|
+
session,
|
|
1346
|
+
});
|
|
1347
|
+
const record = this.sessions.get(key);
|
|
1348
|
+
if (record) {
|
|
1349
|
+
record.observedFromStart = true;
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
if (this.observedStarts.size >= 256)
|
|
1353
|
+
this.observedStarts.delete(this.observedStarts.values().next().value);
|
|
1354
|
+
this.observedStarts.add(key);
|
|
1355
|
+
}
|
|
1356
|
+
__pendingSessionStartsForTests() {
|
|
1357
|
+
return this.observedStarts.size;
|
|
1358
|
+
}
|
|
1359
|
+
observeBackgroundReply(record, body, reply) {
|
|
1360
|
+
const name = String(body.name);
|
|
1361
|
+
const args = isRecord(body.arguments) ? body.arguments : {};
|
|
1362
|
+
const data = isRecord(reply) && isRecord(reply.structuredContent) ? reply.structuredContent : null;
|
|
1363
|
+
const affectsBackground = name === "bash" ||
|
|
1364
|
+
name === "powershell" ||
|
|
1365
|
+
[
|
|
1366
|
+
"bash_status",
|
|
1367
|
+
"bash_kill",
|
|
1368
|
+
"bash_write",
|
|
1369
|
+
"bash_promote",
|
|
1370
|
+
"bash_watch",
|
|
1371
|
+
"bash_notify",
|
|
1372
|
+
"bash_unnotify",
|
|
1373
|
+
"bash_wait_detach",
|
|
1374
|
+
"bash_drain_completions",
|
|
1375
|
+
"bash_ack_completions",
|
|
1376
|
+
].includes(name);
|
|
1377
|
+
if (!data || data.success !== true) {
|
|
1378
|
+
if (affectsBackground) {
|
|
1379
|
+
if (typeof args.task_id === "string") {
|
|
1380
|
+
if (!record.backgroundTasks.has(args.task_id))
|
|
1381
|
+
record.backgroundTasks.set(args.task_id, "unknown");
|
|
1382
|
+
}
|
|
1383
|
+
else
|
|
1384
|
+
record.unknownBackgroundWork = true;
|
|
1385
|
+
}
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
if (typeof data.task_id === "string") {
|
|
1389
|
+
// Terminal tasks remain pinned until the daemon explicitly ACKs them.
|
|
1390
|
+
const terminal = ["completed", "failed", "killed", "timed_out"].includes(String(data.status));
|
|
1391
|
+
const running = ["starting", "running", "killing"].includes(String(data.status));
|
|
1392
|
+
record.backgroundTasks.set(data.task_id, terminal ? "terminal" : running ? "running" : "unknown");
|
|
1393
|
+
if (name === "bash" || name === "powershell")
|
|
1394
|
+
record.pendingDetach = false;
|
|
1395
|
+
}
|
|
1396
|
+
if (Array.isArray(data.bg_completions)) {
|
|
1397
|
+
for (const completion of data.bg_completions) {
|
|
1398
|
+
if (isRecord(completion) && typeof completion.task_id === "string")
|
|
1399
|
+
record.backgroundTasks.set(completion.task_id, "terminal");
|
|
1400
|
+
else
|
|
1401
|
+
record.unknownBackgroundWork = true;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
if (Array.isArray(data.pending_matches)) {
|
|
1405
|
+
for (const match of data.pending_matches) {
|
|
1406
|
+
if (isRecord(match) &&
|
|
1407
|
+
typeof match.watch_id === "string" &&
|
|
1408
|
+
typeof match.task_id === "string")
|
|
1409
|
+
record.backgroundWatches.set(match.watch_id, match.task_id);
|
|
1410
|
+
else
|
|
1411
|
+
record.unknownBackgroundWork = true;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
if (name === "bash_drain_completions" &&
|
|
1415
|
+
Array.isArray(data.bg_completions) &&
|
|
1416
|
+
Array.isArray(data.pending_matches))
|
|
1417
|
+
record.pendingBgNudge = false;
|
|
1418
|
+
if (name === "bash_notify") {
|
|
1419
|
+
if (typeof data.watch_id === "string" && typeof args.task_id === "string")
|
|
1420
|
+
record.backgroundWatches.set(data.watch_id, args.task_id);
|
|
1421
|
+
else
|
|
1422
|
+
record.unknownBackgroundWork = true;
|
|
1423
|
+
}
|
|
1424
|
+
if (name === "bash_unnotify" && data.unregistered === true && typeof args.watch_id === "string")
|
|
1425
|
+
record.backgroundWatches.delete(args.watch_id);
|
|
1426
|
+
if (name === "bash_ack_completions" && Array.isArray(data.acked_task_ids)) {
|
|
1427
|
+
// ACK also acknowledges mid-run pattern matches. It does not prove a
|
|
1428
|
+
// running/unknown task is terminal, so those must keep their carrier.
|
|
1429
|
+
for (const task of data.acked_task_ids)
|
|
1430
|
+
if (typeof task === "string" && record.backgroundTasks.get(task) === "terminal") {
|
|
1431
|
+
record.backgroundTasks.delete(task);
|
|
1432
|
+
// The daemon's terminal ACK also removes every watch on that task.
|
|
1433
|
+
for (const [watch, owner] of record.backgroundWatches) {
|
|
1434
|
+
if (owner === task)
|
|
1435
|
+
record.backgroundWatches.delete(watch);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
if (name === "bash_wait_detach") {
|
|
1440
|
+
if (typeof data.detached === "boolean")
|
|
1441
|
+
record.pendingDetach = data.detached;
|
|
1442
|
+
else
|
|
1443
|
+
record.unknownBackgroundWork = true;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
__retainedSessionCountsForTests() {
|
|
1447
|
+
return {
|
|
1448
|
+
sessions: this.sessions.size,
|
|
1449
|
+
roots: this.rootIndex.size,
|
|
1450
|
+
routes: [...this.sessions.values()].filter((r) => r.routeEntry !== null).length,
|
|
1451
|
+
subscriptions: [...this.sessions.values()].filter((r) => r.bgSub !== null).length,
|
|
1452
|
+
sweepTimers: this.sessionSweep ? 1 : 0,
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1191
1455
|
isCurrentSession(key, record) {
|
|
1192
1456
|
return this.sessions.get(key) === record && !record.closed;
|
|
1193
1457
|
}
|
|
@@ -1232,6 +1496,12 @@ export class SubcTransportPool {
|
|
|
1232
1496
|
if (this.sessions.get(record.identityKey) === record &&
|
|
1233
1497
|
!record.closed &&
|
|
1234
1498
|
record.inflight === 0 &&
|
|
1499
|
+
record.retiredRoutes.size === 0 &&
|
|
1500
|
+
record.backgroundTasks.size === 0 &&
|
|
1501
|
+
record.backgroundWatches.size === 0 &&
|
|
1502
|
+
!record.unknownBackgroundWork &&
|
|
1503
|
+
!record.pendingBgNudge &&
|
|
1504
|
+
!record.pendingDetach &&
|
|
1235
1505
|
record.routeEntry === null &&
|
|
1236
1506
|
record.bgSub === null) {
|
|
1237
1507
|
this.sessions.delete(record.identityKey);
|
|
@@ -1250,6 +1520,10 @@ export class SubcTransportPool {
|
|
|
1250
1520
|
record.teardownReason = reason;
|
|
1251
1521
|
this.sessions.delete(key);
|
|
1252
1522
|
this.removeIndexMembership(record);
|
|
1523
|
+
for (const logKey of this.nudgeDeliveryLogState.keys()) {
|
|
1524
|
+
if (logKey.endsWith(`\u0000${key}`))
|
|
1525
|
+
this.nudgeDeliveryLogState.delete(logKey);
|
|
1526
|
+
}
|
|
1253
1527
|
const bgSub = record.bgSub;
|
|
1254
1528
|
record.bgSub = null;
|
|
1255
1529
|
const routeEntry = record.routeEntry;
|
|
@@ -1262,14 +1536,19 @@ export class SubcTransportPool {
|
|
|
1262
1536
|
const cleanup = [];
|
|
1263
1537
|
if (detached.bgSub)
|
|
1264
1538
|
cleanup.push(detached.bgSub.stop());
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1267
|
-
if (routeEntry
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1539
|
+
const entries = new Set(detached.record.retiredRoutes);
|
|
1540
|
+
detached.record.retiredRoutes.clear();
|
|
1541
|
+
if (detached.routeEntry)
|
|
1542
|
+
entries.add(detached.routeEntry);
|
|
1543
|
+
for (const entry of entries) {
|
|
1544
|
+
entry.closed = true;
|
|
1545
|
+
if (entry.handle != null) {
|
|
1546
|
+
try {
|
|
1547
|
+
cleanup.push(Promise.resolve(entry.client.closeRouteChannel(entry.handle)).catch(() => undefined));
|
|
1548
|
+
}
|
|
1549
|
+
catch {
|
|
1550
|
+
// A client may throw synchronously when its socket is already closed.
|
|
1551
|
+
}
|
|
1273
1552
|
}
|
|
1274
1553
|
}
|
|
1275
1554
|
await Promise.allSettled(cleanup);
|
|
@@ -1299,6 +1578,17 @@ export class SubcTransportPool {
|
|
|
1299
1578
|
}
|
|
1300
1579
|
return this.rootReapedError(record);
|
|
1301
1580
|
}
|
|
1581
|
+
closeRetiredRoute(record, entry) {
|
|
1582
|
+
if (entry.inflight > 0 || !record.retiredRoutes.delete(entry))
|
|
1583
|
+
return;
|
|
1584
|
+
if (entry.handle != null)
|
|
1585
|
+
safeCloseRoute(entry.client, entry.handle);
|
|
1586
|
+
this.deleteSessionIfEmpty(record.identityKey, record);
|
|
1587
|
+
}
|
|
1588
|
+
releaseRouteRequest(record, entry) {
|
|
1589
|
+
entry.inflight -= 1;
|
|
1590
|
+
this.closeRetiredRoute(record, entry);
|
|
1591
|
+
}
|
|
1302
1592
|
/** Open or reuse a route while guarding every lifecycle boundary. */
|
|
1303
1593
|
async routeRequest(identity, body, timeoutMs, onProgress, expectedGeneration, abortSignal) {
|
|
1304
1594
|
const callStartedAt = performance.now();
|
|
@@ -1471,9 +1761,10 @@ export class SubcTransportPool {
|
|
|
1471
1761
|
if (record.routeEntry !== entry)
|
|
1472
1762
|
return;
|
|
1473
1763
|
entry.closed = true;
|
|
1764
|
+
entry.closedBy ??= "route replaced";
|
|
1474
1765
|
record.routeEntry = null;
|
|
1475
|
-
|
|
1476
|
-
|
|
1766
|
+
record.retiredRoutes.add(entry);
|
|
1767
|
+
this.closeRetiredRoute(record, entry);
|
|
1477
1768
|
};
|
|
1478
1769
|
const handleRequestFailure = (error, entry) => {
|
|
1479
1770
|
const aborted = error instanceof Error && error.name === "AbortError";
|
|
@@ -1489,20 +1780,30 @@ export class SubcTransportPool {
|
|
|
1489
1780
|
route: routeLabel(entry.handle),
|
|
1490
1781
|
});
|
|
1491
1782
|
}
|
|
1492
|
-
|
|
1783
|
+
// A call's timeout or refusal says nothing about the route's health.
|
|
1784
|
+
// Retire only a cancelled carrier or one known to be gone; a slow live
|
|
1785
|
+
// route remains reusable, and no replacement may cut its other calls.
|
|
1786
|
+
if (aborted ||
|
|
1787
|
+
isRouteProvenAbsentError(error) ||
|
|
1788
|
+
(error instanceof SubcError && error.closeReason !== undefined)) {
|
|
1789
|
+
clearRouteEntry(entry);
|
|
1790
|
+
}
|
|
1493
1791
|
if (aborted || !ownsClient || this.client !== client)
|
|
1494
1792
|
return;
|
|
1495
1793
|
if (isRouteProvenAbsentError(error))
|
|
1496
1794
|
return;
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1795
|
+
if (isDaemonAnswerError(error)) {
|
|
1796
|
+
this.transportFailures = 0;
|
|
1797
|
+
this.noteClientAlive(client);
|
|
1798
|
+
}
|
|
1799
|
+
else if (isUnansweredCallError(error)) {
|
|
1800
|
+
this.noteUnansweredCall(client, {
|
|
1801
|
+
error,
|
|
1802
|
+
trigger: "request",
|
|
1803
|
+
session: identity.session,
|
|
1804
|
+
route: routeLabel(entry.handle),
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1506
1807
|
};
|
|
1507
1808
|
const requestOnRoute = async (route, entry, requestTimeoutMs) => {
|
|
1508
1809
|
this.assertRecordLive(record);
|
|
@@ -1520,10 +1821,22 @@ export class SubcTransportPool {
|
|
|
1520
1821
|
};
|
|
1521
1822
|
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
1522
1823
|
try {
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1824
|
+
// Track the actual client promise, not the abort race. A cancelled
|
|
1825
|
+
// caller has no terminal from the daemon yet, so closing its carrier
|
|
1826
|
+
// here would still cut sibling calls and invalidate request credits.
|
|
1827
|
+
entry.inflight += 1;
|
|
1828
|
+
let request;
|
|
1829
|
+
try {
|
|
1830
|
+
request = client.request(route, body, {
|
|
1831
|
+
timeoutMs: Math.ceil(requestTimeoutMs),
|
|
1832
|
+
onProgress,
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
catch (error) {
|
|
1836
|
+
this.releaseRouteRequest(record, entry);
|
|
1837
|
+
throw error;
|
|
1838
|
+
}
|
|
1839
|
+
request = request.finally(() => this.releaseRouteRequest(record, entry));
|
|
1527
1840
|
const reply = abortSignal ? await Promise.race([request, aborted]) : await request;
|
|
1528
1841
|
// A legacy closeSession may intentionally let an already-delivered reply
|
|
1529
1842
|
// settle. It must not mutate shared state or recreate a subscription.
|
|
@@ -1536,6 +1849,7 @@ export class SubcTransportPool {
|
|
|
1536
1849
|
if (this.client === client)
|
|
1537
1850
|
this.transportFailures = 0;
|
|
1538
1851
|
this.noteClientAlive(client);
|
|
1852
|
+
this.observeBackgroundReply(record, body, reply);
|
|
1539
1853
|
this.ensureBgSubscription(identity, record);
|
|
1540
1854
|
return reply;
|
|
1541
1855
|
}
|
|
@@ -1547,6 +1861,10 @@ export class SubcTransportPool {
|
|
|
1547
1861
|
let reopened = false;
|
|
1548
1862
|
let retriedAbsentRoute = false;
|
|
1549
1863
|
while (true) {
|
|
1864
|
+
// Another call can retire a just-acquired carrier before this caller
|
|
1865
|
+
// dispatches. Only already-pending requests belong on retired routes.
|
|
1866
|
+
if (routeAndEntry.entry.closed)
|
|
1867
|
+
routeAndEntry = await openRouteAfterReloadWindow();
|
|
1550
1868
|
// Checked outside the try below so the retry branches never mistake
|
|
1551
1869
|
// this call's own expiry for a daemon refusal worth retrying.
|
|
1552
1870
|
const requestTimeoutMs = remainingMs();
|
|
@@ -1558,7 +1876,8 @@ export class SubcTransportPool {
|
|
|
1558
1876
|
this.resetRouteReopenBackoff();
|
|
1559
1877
|
return reply;
|
|
1560
1878
|
}
|
|
1561
|
-
catch (
|
|
1879
|
+
catch (requestError) {
|
|
1880
|
+
const error = routeClosedMidCallError(requestError, routeAndEntry.entry, record);
|
|
1562
1881
|
if (this.isReapInduced(record))
|
|
1563
1882
|
throw this.annotateReapError(error, record);
|
|
1564
1883
|
const ownsRoute = this.isCurrentSession(key, record) && this.client === client;
|
|
@@ -1607,10 +1926,14 @@ export class SubcTransportPool {
|
|
|
1607
1926
|
catch (error) {
|
|
1608
1927
|
if (this.isReapInduced(record))
|
|
1609
1928
|
throw this.annotateReapError(error, record);
|
|
1929
|
+
this.observeBackgroundReply(record, body, null);
|
|
1930
|
+
if (record.backgroundTasks.size > 0 || record.unknownBackgroundWork)
|
|
1931
|
+
this.ensureBgSubscription(identity, record);
|
|
1610
1932
|
throw error;
|
|
1611
1933
|
}
|
|
1612
1934
|
finally {
|
|
1613
1935
|
record.inflight -= 1;
|
|
1936
|
+
record.lastUsedAt = Date.now();
|
|
1614
1937
|
this.deleteSessionIfEmpty(key, record);
|
|
1615
1938
|
}
|
|
1616
1939
|
}
|
|
@@ -1700,6 +2023,8 @@ export class SubcTransportPool {
|
|
|
1700
2023
|
opening: null,
|
|
1701
2024
|
handle: null,
|
|
1702
2025
|
closed: false,
|
|
2026
|
+
inflight: 0,
|
|
2027
|
+
closedBy: null,
|
|
1703
2028
|
};
|
|
1704
2029
|
this.assertRootCanAttach(record.canonicalRoot);
|
|
1705
2030
|
const opening = client
|
|
@@ -1747,12 +2072,12 @@ export class SubcTransportPool {
|
|
|
1747
2072
|
return;
|
|
1748
2073
|
if (!this.isCurrentSession(record.identityKey, record))
|
|
1749
2074
|
return;
|
|
1750
|
-
if (!this.rootCanAttach(record.canonicalRoot))
|
|
1751
|
-
return;
|
|
1752
2075
|
if (record.bgSub)
|
|
1753
2076
|
return;
|
|
2077
|
+
if (!this.rootCanAttach(record.canonicalRoot))
|
|
2078
|
+
return;
|
|
1754
2079
|
const nudgeRef = this.nudgeRefFor(record);
|
|
1755
|
-
const onNudge = () => {
|
|
2080
|
+
const onNudge = (observedEvent) => {
|
|
1756
2081
|
const currentRecord = this.currentSessionForNudge(identity);
|
|
1757
2082
|
if (!currentRecord) {
|
|
1758
2083
|
this.logNudgeDelivery("drop-no-current-session", record, `nudge dropped cause=no-current-session root=${record.canonicalRoot}`);
|
|
@@ -1761,6 +2086,9 @@ export class SubcTransportPool {
|
|
|
1761
2086
|
if (currentRecord !== record) {
|
|
1762
2087
|
this.logNudgeDelivery("forward-superseded-carrier", currentRecord, `nudge forwarding cause=superseded-carrying-record root=${currentRecord.canonicalRoot}`);
|
|
1763
2088
|
}
|
|
2089
|
+
currentRecord.lastUsedAt = Date.now();
|
|
2090
|
+
if (observedEvent)
|
|
2091
|
+
currentRecord.pendingBgNudge = true;
|
|
1764
2092
|
const currentRef = this.nudgeRefFor(currentRecord);
|
|
1765
2093
|
let delivered = false;
|
|
1766
2094
|
if (currentRef && this.onBgEventsNudgeRef) {
|
|
@@ -1825,6 +2153,12 @@ export class SubcTransportPool {
|
|
|
1825
2153
|
` message=${JSON.stringify(message)}`);
|
|
1826
2154
|
this.client = null;
|
|
1827
2155
|
for (const record of this.sessions.values()) {
|
|
2156
|
+
for (const retired of record.retiredRoutes) {
|
|
2157
|
+
if (retired.client !== client)
|
|
2158
|
+
continue;
|
|
2159
|
+
retired.closed = true;
|
|
2160
|
+
record.retiredRoutes.delete(retired);
|
|
2161
|
+
}
|
|
1828
2162
|
const entry = record.routeEntry;
|
|
1829
2163
|
if (entry?.client !== client)
|
|
1830
2164
|
continue;
|
|
@@ -1996,6 +2330,12 @@ export class SubcTransportPool {
|
|
|
1996
2330
|
if (this.shuttingDown)
|
|
1997
2331
|
return;
|
|
1998
2332
|
this.shuttingDown = true;
|
|
2333
|
+
if (this.sessionSweep)
|
|
2334
|
+
clearInterval(this.sessionSweep);
|
|
2335
|
+
this.sessionSweep = null;
|
|
2336
|
+
this.nudgeDeliveryLogState.clear();
|
|
2337
|
+
this.generationRejections.clear();
|
|
2338
|
+
this.observedStarts.clear();
|
|
1999
2339
|
this.dormantRoots.clear();
|
|
2000
2340
|
// Deregistration synchronously tombstones all registered roots and invokes
|
|
2001
2341
|
// closeProjectRoot before this method reaches its first await.
|