@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/bridge.js
CHANGED
|
@@ -6,6 +6,7 @@ import { binaryContentHash, binaryStampKey, peekBinaryContentHash } from "./bina
|
|
|
6
6
|
import { spawn } from "./child-process.js";
|
|
7
7
|
import { isPassiveCommand, PASSIVE_COMMAND_TIMEOUT_MS } from "./command-timeouts.js";
|
|
8
8
|
import { withPathPrepended } from "./path-env.js";
|
|
9
|
+
import { isTerminalBashStatus } from "./protocol.js";
|
|
9
10
|
import { HOST_TOOL_NAMES_FIELD, WORKER_SESSION_FIELD } from "./transport.js";
|
|
10
11
|
const DEFAULT_BRIDGE_TIMEOUT_MS = 30_000;
|
|
11
12
|
const BRIDGE_HANG_TIMEOUT_THRESHOLD = 2;
|
|
@@ -14,19 +15,6 @@ const MAX_STDOUT_BUFFER = 64 * 1024 * 1024; // 64MB
|
|
|
14
15
|
const MALFORMED_STDOUT_LOG_CHARS = 512;
|
|
15
16
|
const HASHLINE_REGISTRATION_LOG_INTERVAL_MS = 60_000;
|
|
16
17
|
const HASHLINE_REGISTRATION_LOG_STATE_LIMIT = 256;
|
|
17
|
-
const TERMINAL_BASH_STATUSES = new Set([
|
|
18
|
-
"completed",
|
|
19
|
-
"failed",
|
|
20
|
-
"killed",
|
|
21
|
-
"timed_out",
|
|
22
|
-
"fate_unknown",
|
|
23
|
-
// Historical/defensive aliases seen in plugin-side compatibility code.
|
|
24
|
-
"cancelled",
|
|
25
|
-
"timeout",
|
|
26
|
-
]);
|
|
27
|
-
function isTerminalBashStatus(status) {
|
|
28
|
-
return typeof status === "string" && TERMINAL_BASH_STATUSES.has(status);
|
|
29
|
-
}
|
|
30
18
|
function bashTaskIdFrom(response) {
|
|
31
19
|
const snakeCase = response.task_id;
|
|
32
20
|
if (typeof snakeCase === "string" && snakeCase.length > 0)
|
|
@@ -115,47 +103,49 @@ export function shouldSurfaceStderrLine(line) {
|
|
|
115
103
|
normalized === BENIGN_CPUINFO_PROC_CPUINFO_PARSE_FAILURE);
|
|
116
104
|
}
|
|
117
105
|
export function compareSemver(a, b) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
106
|
+
const parse = (version) => {
|
|
107
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version);
|
|
108
|
+
const pre = match?.[4]?.split(".") ?? [];
|
|
109
|
+
if (!match || pre.some((id) => /^\d+$/.test(id) && id.length > 1 && id[0] === "0")) {
|
|
110
|
+
throw new Error(`Invalid semantic version: ${version}`);
|
|
111
|
+
}
|
|
112
|
+
return { core: match.slice(1, 4), pre };
|
|
113
|
+
};
|
|
114
|
+
// Compare numeric identifiers as strings so even large valid identifiers
|
|
115
|
+
// retain their order without Number rounding or overflow.
|
|
116
|
+
const numeric = (a, b) => a.length !== b.length ? a.length - b.length : a === b ? 0 : a < b ? -1 : 1;
|
|
117
|
+
const left = parse(a);
|
|
118
|
+
const right = parse(b);
|
|
122
119
|
for (let i = 0; i < 3; i++) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const bIds = bPre.split(".");
|
|
134
|
-
for (let i = 0; i < Math.max(aIds.length, bIds.length); i++) {
|
|
135
|
-
const ai = aIds[i];
|
|
136
|
-
const bi = bIds[i];
|
|
120
|
+
const diff = numeric(left.core[i], right.core[i]);
|
|
121
|
+
if (diff !== 0)
|
|
122
|
+
return diff;
|
|
123
|
+
}
|
|
124
|
+
if (left.pre.length === 0 || right.pre.length === 0) {
|
|
125
|
+
return left.pre.length === right.pre.length ? 0 : left.pre.length === 0 ? 1 : -1;
|
|
126
|
+
}
|
|
127
|
+
for (let i = 0; i < Math.max(left.pre.length, right.pre.length); i++) {
|
|
128
|
+
const ai = left.pre[i];
|
|
129
|
+
const bi = right.pre[i];
|
|
137
130
|
if (ai === undefined)
|
|
138
131
|
return -1;
|
|
139
132
|
if (bi === undefined)
|
|
140
133
|
return 1;
|
|
141
134
|
const aNum = /^\d+$/.test(ai);
|
|
142
135
|
const bNum = /^\d+$/.test(bi);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (cmp !== 0)
|
|
157
|
-
return cmp;
|
|
158
|
-
}
|
|
136
|
+
const diff = aNum && bNum
|
|
137
|
+
? numeric(ai, bi)
|
|
138
|
+
: aNum !== bNum
|
|
139
|
+
? aNum
|
|
140
|
+
? -1
|
|
141
|
+
: 1
|
|
142
|
+
: ai === bi
|
|
143
|
+
? 0
|
|
144
|
+
: ai < bi
|
|
145
|
+
? -1
|
|
146
|
+
: 1;
|
|
147
|
+
if (diff !== 0)
|
|
148
|
+
return diff;
|
|
159
149
|
}
|
|
160
150
|
return 0;
|
|
161
151
|
}
|
|
@@ -195,13 +185,45 @@ export class BridgeTransportTimeoutError extends Error {
|
|
|
195
185
|
command;
|
|
196
186
|
timeoutMs;
|
|
197
187
|
code = "transport_timeout";
|
|
198
|
-
|
|
188
|
+
/**
|
|
189
|
+
* For a `bash`/`powershell` call, what the bridge learned about the command
|
|
190
|
+
* while it waited a short grace period after the deadline. Undefined for
|
|
191
|
+
* other commands.
|
|
192
|
+
*/
|
|
193
|
+
shellOutcome;
|
|
194
|
+
constructor(command, timeoutMs, message, shellOutcome) {
|
|
199
195
|
super(message);
|
|
200
196
|
this.command = command;
|
|
201
197
|
this.timeoutMs = timeoutMs;
|
|
202
198
|
this.name = "BridgeTransportTimeoutError";
|
|
199
|
+
this.shellOutcome = shellOutcome;
|
|
203
200
|
}
|
|
204
201
|
}
|
|
202
|
+
/** Commands that start a shell task in AFT and answer with its task id. */
|
|
203
|
+
const SHELL_TASK_COMMANDS = new Set(["bash", "powershell"]);
|
|
204
|
+
/**
|
|
205
|
+
* How long a timed-out shell call keeps listening for AFT's late reply. AFT
|
|
206
|
+
* can start the command and answer just after the bridge's deadline (for
|
|
207
|
+
* example while it is busy with other work); that reply carries the task id,
|
|
208
|
+
* which is the only way the agent can find the running command. Kept short
|
|
209
|
+
* because the agent waits this much longer for its error.
|
|
210
|
+
*/
|
|
211
|
+
const DEFAULT_LATE_SHELL_REPLY_GRACE_MS = 10_000;
|
|
212
|
+
function shellOutcomeFromLateReply(response) {
|
|
213
|
+
const taskId = bashTaskIdFrom(response);
|
|
214
|
+
if (taskId)
|
|
215
|
+
return { kind: "task_started", taskId };
|
|
216
|
+
if (response.success === false) {
|
|
217
|
+
return {
|
|
218
|
+
kind: "not_started",
|
|
219
|
+
...(typeof response.code === "string" ? { code: response.code } : {}),
|
|
220
|
+
...(typeof response.message === "string" ? { message: response.message } : {}),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// A successful reply without a task id is not a shape AFT sends for a shell
|
|
224
|
+
// command, so it proves nothing either way.
|
|
225
|
+
return { kind: "unknown" };
|
|
226
|
+
}
|
|
205
227
|
/** A standalone bridge cannot carry a request because its process transport died. */
|
|
206
228
|
export class BridgeTransportUnavailableError extends Error {
|
|
207
229
|
code = "bridge_transport_unavailable";
|
|
@@ -279,6 +301,7 @@ export class BinaryBridge {
|
|
|
279
301
|
_shuttingDown = false;
|
|
280
302
|
timeoutMs;
|
|
281
303
|
hangThreshold;
|
|
304
|
+
lateShellReplyGraceMs;
|
|
282
305
|
maxRestarts;
|
|
283
306
|
configured = false;
|
|
284
307
|
_configurePromise = null;
|
|
@@ -296,6 +319,7 @@ export class BinaryBridge {
|
|
|
296
319
|
statusListeners = new Set();
|
|
297
320
|
/** Notification clients keyed by session_id for async configure warning pushes. */
|
|
298
321
|
configureWarningClients = new Map();
|
|
322
|
+
restartTimer = null;
|
|
299
323
|
restartResetTimer = null;
|
|
300
324
|
/** Updated after every successfully parsed stdout frame from the child. */
|
|
301
325
|
lastChildActivityAt = 0;
|
|
@@ -314,6 +338,8 @@ export class BinaryBridge {
|
|
|
314
338
|
this.cwd = cwd;
|
|
315
339
|
this.timeoutMs = options?.timeoutMs ?? DEFAULT_BRIDGE_TIMEOUT_MS;
|
|
316
340
|
this.hangThreshold = options?.hangThreshold ?? BRIDGE_HANG_TIMEOUT_THRESHOLD;
|
|
341
|
+
this.lateShellReplyGraceMs =
|
|
342
|
+
options?.lateShellReplyGraceMs ?? DEFAULT_LATE_SHELL_REPLY_GRACE_MS;
|
|
317
343
|
this.maxRestarts = options?.maxRestarts ?? 3;
|
|
318
344
|
this.errorPrefix = options?.errorPrefix ?? "[aft-bridge]";
|
|
319
345
|
// P1 config relocation: semantic config now arrives as raw `config` tiers and
|
|
@@ -666,13 +692,6 @@ export class BinaryBridge {
|
|
|
666
692
|
? params.session_id
|
|
667
693
|
: undefined;
|
|
668
694
|
this.ensureSpawned(requestSessionId);
|
|
669
|
-
// Auto-configure can reuse the initiating session's notification client
|
|
670
|
-
// when the deferred configure warning frame arrives later. One project
|
|
671
|
-
// bridge can serve many sessions, so keep this per-session instead of one
|
|
672
|
-
// bridge-wide "last client".
|
|
673
|
-
if (requestSessionId && options?.configureWarningClient !== undefined) {
|
|
674
|
-
this.configureWarningClients.set(requestSessionId, options.configureWarningClient);
|
|
675
|
-
}
|
|
676
695
|
// Per-op timeout override: tool wrappers can pass longer budgets for
|
|
677
696
|
// commands that legitimately need them (callers, trace_to, grep on big
|
|
678
697
|
// repos). Defaults to the bridge-wide timeout otherwise.
|
|
@@ -697,6 +716,13 @@ export class BinaryBridge {
|
|
|
697
716
|
if (!this.configured) {
|
|
698
717
|
if (command !== "configure" && command !== "version") {
|
|
699
718
|
if (!this._configurePromise) {
|
|
719
|
+
// Only the initiating session owns configure's deferred delivery.
|
|
720
|
+
// Later requests must not retain unrelated notification clients.
|
|
721
|
+
if (this.onConfigureWarnings &&
|
|
722
|
+
requestSessionId &&
|
|
723
|
+
options?.configureWarningClient !== undefined) {
|
|
724
|
+
this.configureWarningClients.set(requestSessionId, options.configureWarningClient);
|
|
725
|
+
}
|
|
700
726
|
const configuringChild = this.process;
|
|
701
727
|
const requireConfiguringChild = () => {
|
|
702
728
|
if (this.process !== configuringChild) {
|
|
@@ -737,6 +763,11 @@ export class BinaryBridge {
|
|
|
737
763
|
}
|
|
738
764
|
this.configured = true;
|
|
739
765
|
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
if (sessionIdForConfigure)
|
|
768
|
+
this.configureWarningClients.delete(sessionIdForConfigure);
|
|
769
|
+
throw error;
|
|
770
|
+
}
|
|
740
771
|
finally {
|
|
741
772
|
this._configurePromise = null;
|
|
742
773
|
}
|
|
@@ -792,6 +823,10 @@ export class BinaryBridge {
|
|
|
792
823
|
const entry = this.pending.get(id);
|
|
793
824
|
if (!entry)
|
|
794
825
|
return;
|
|
826
|
+
if (keepBridgeOnTimeout &&
|
|
827
|
+
this.holdForLateShellReply(id, entry, effectiveTimeoutMs, requestSessionId)) {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
795
830
|
this.pending.delete(id);
|
|
796
831
|
clearTimeout(entry.timer);
|
|
797
832
|
entry.onSettled?.();
|
|
@@ -899,6 +934,13 @@ export class BinaryBridge {
|
|
|
899
934
|
}
|
|
900
935
|
}
|
|
901
936
|
async deliverConfigureWarnings(configResult, params, options) {
|
|
937
|
+
const sessionId = typeof params.session_id === "string" ? params.session_id : undefined;
|
|
938
|
+
const client = options?.configureWarningClient ??
|
|
939
|
+
(sessionId ? this.configureWarningClients.get(sessionId) : undefined);
|
|
940
|
+
// Capture ownership before dispatch. A hung host callback must neither hold
|
|
941
|
+
// configure readiness nor delay releasing the terminal delivery's client.
|
|
942
|
+
if (sessionId && configResult.warnings_pending !== true)
|
|
943
|
+
this.configureWarningClients.delete(sessionId);
|
|
902
944
|
if (!this.onConfigureWarnings)
|
|
903
945
|
return;
|
|
904
946
|
const warnings = Array.isArray(configResult.warnings)
|
|
@@ -907,57 +949,40 @@ export class BinaryBridge {
|
|
|
907
949
|
const configDroppedKeys = coerceConfigureDroppedKeys(configResult.config_dropped_keys);
|
|
908
950
|
if (warnings.length === 0 && configDroppedKeys.length === 0)
|
|
909
951
|
return;
|
|
910
|
-
const sessionId = typeof params.session_id === "string" ? params.session_id : undefined;
|
|
911
952
|
const context = {
|
|
912
953
|
projectRoot: this.cwd,
|
|
913
954
|
sessionId,
|
|
914
|
-
client
|
|
915
|
-
(sessionId ? this.configureWarningClients.get(sessionId) : undefined),
|
|
955
|
+
client,
|
|
916
956
|
warnings,
|
|
917
957
|
};
|
|
918
|
-
if (configDroppedKeys.length > 0)
|
|
958
|
+
if (configDroppedKeys.length > 0)
|
|
919
959
|
context.configDroppedKeys = configDroppedKeys;
|
|
920
|
-
|
|
921
|
-
try {
|
|
922
|
-
await this.onConfigureWarnings(context);
|
|
923
|
-
}
|
|
924
|
-
catch (err) {
|
|
925
|
-
this.warnVia(`configure warning delivery failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
926
|
-
}
|
|
927
|
-
finally {
|
|
928
|
-
if (sessionId) {
|
|
929
|
-
this.configureWarningClients.delete(sessionId);
|
|
930
|
-
}
|
|
931
|
-
}
|
|
960
|
+
this.dispatchHostCallback("configure warning", () => this.onConfigureWarnings?.(context));
|
|
932
961
|
}
|
|
933
|
-
/**
|
|
934
|
-
* Handle the `configure_warnings` push frame the Rust binary emits after
|
|
935
|
-
* configure has returned. The frame carries the warnings produced by the
|
|
936
|
-
* deferred file walk + missing-binary detection. Forwards to the same
|
|
937
|
-
* `onConfigureWarnings` handler used for synchronous warnings so plugins
|
|
938
|
-
* don't need to know about the async path.
|
|
939
|
-
*/
|
|
962
|
+
/** Release deferred-warning ownership even when the terminal frame has no warnings. */
|
|
940
963
|
async handleConfigureWarningsFrame(frame) {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
964
|
+
const sessionId = typeof frame.session_id === "string" && frame.session_id.length > 0 ? frame.session_id : null;
|
|
965
|
+
const client = sessionId ? this.configureWarningClients.get(sessionId) : undefined;
|
|
966
|
+
if (sessionId)
|
|
967
|
+
this.configureWarningClients.delete(sessionId);
|
|
968
|
+
if (!this.onConfigureWarnings || !Array.isArray(frame.warnings) || frame.warnings.length === 0)
|
|
945
969
|
return;
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
970
|
+
this.dispatchHostCallback("configure warning", () => this.onConfigureWarnings?.({
|
|
971
|
+
projectRoot: typeof frame.project_root === "string" ? frame.project_root : this.cwd,
|
|
972
|
+
sessionId,
|
|
973
|
+
client,
|
|
974
|
+
warnings: frame.warnings,
|
|
975
|
+
}));
|
|
976
|
+
}
|
|
977
|
+
dispatchHostCallback(label, callback) {
|
|
978
|
+
const failed = (error) => {
|
|
979
|
+
this.warnVia(`${label} delivery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
980
|
+
};
|
|
949
981
|
try {
|
|
950
|
-
|
|
951
|
-
projectRoot,
|
|
952
|
-
sessionId,
|
|
953
|
-
client: sessionId ? this.configureWarningClients.get(sessionId) : undefined,
|
|
954
|
-
warnings: warnings,
|
|
955
|
-
});
|
|
982
|
+
void Promise.resolve(callback()).catch(failed);
|
|
956
983
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
this.configureWarningClients.delete(sessionId);
|
|
960
|
-
}
|
|
984
|
+
catch (error) {
|
|
985
|
+
failed(error);
|
|
961
986
|
}
|
|
962
987
|
}
|
|
963
988
|
handleStatusChangedFrame(frame) {
|
|
@@ -985,6 +1010,7 @@ export class BinaryBridge {
|
|
|
985
1010
|
this._shuttingDown = true;
|
|
986
1011
|
this.spawnedBinaryFingerprint = null;
|
|
987
1012
|
this.clearRestartResetTimer();
|
|
1013
|
+
this.clearRestartTimer();
|
|
988
1014
|
this.configureWarningClients.clear();
|
|
989
1015
|
this.rejectAllPending(new BridgeTransportUnknownOutcomeError(`${this.errorPrefix} Bridge shutting down`));
|
|
990
1016
|
if (this.process) {
|
|
@@ -1073,6 +1099,7 @@ export class BinaryBridge {
|
|
|
1073
1099
|
this.spawnProcess(triggeringSessionId);
|
|
1074
1100
|
}
|
|
1075
1101
|
spawnProcess(triggeringSessionId) {
|
|
1102
|
+
this.clearRestartTimer();
|
|
1076
1103
|
this._retiringDueToBinaryChange = false;
|
|
1077
1104
|
if (triggeringSessionId) {
|
|
1078
1105
|
this.sessionLogVia(triggeringSessionId, `Spawning binary: ${this.binaryPath} (cwd: ${this.cwd})`);
|
|
@@ -1157,6 +1184,13 @@ export class BinaryBridge {
|
|
|
1157
1184
|
env: spawnEnv,
|
|
1158
1185
|
});
|
|
1159
1186
|
const currentChild = child;
|
|
1187
|
+
// Writable streams emit an error in addition to invoking write callbacks.
|
|
1188
|
+
// Keep retired children observed too, but only retire the current transport.
|
|
1189
|
+
child.stdin?.on("error", (cause) => {
|
|
1190
|
+
if (this.process !== currentChild)
|
|
1191
|
+
return;
|
|
1192
|
+
this.invalidateTransportProcess(new BridgeTransportUnknownOutcomeError(`${this.errorPrefix} Failed to write to stdin: ${cause.message}`, { cause }));
|
|
1193
|
+
});
|
|
1160
1194
|
const stdoutDecoder = new StringDecoder("utf8");
|
|
1161
1195
|
child.stdout?.on("data", (chunk) => {
|
|
1162
1196
|
if (this.process !== currentChild)
|
|
@@ -1288,11 +1322,12 @@ export class BinaryBridge {
|
|
|
1288
1322
|
return `\n --- last ${this.stderrTail.length} stderr lines ---\n ${tail}`;
|
|
1289
1323
|
}
|
|
1290
1324
|
onStdoutData(data) {
|
|
1291
|
-
|
|
1292
|
-
|
|
1325
|
+
const exceedsLimit = (fragment) => {
|
|
1326
|
+
if (this.stdoutPendingLength + Buffer.byteLength(fragment, "utf8") <= MAX_STDOUT_BUFFER)
|
|
1327
|
+
return false;
|
|
1293
1328
|
this.handleCrash(new Error(`aft bridge stdout buffer exceeded ${MAX_STDOUT_BUFFER} bytes — killing bridge`));
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1329
|
+
return true;
|
|
1330
|
+
};
|
|
1296
1331
|
// Only the new chunk is searched for newlines: the carried chunks are
|
|
1297
1332
|
// already known to contain none.
|
|
1298
1333
|
this.stdoutScannedChars += data.length;
|
|
@@ -1301,6 +1336,8 @@ export class BinaryBridge {
|
|
|
1301
1336
|
let newlineIdx = data.indexOf("\n");
|
|
1302
1337
|
while (newlineIdx !== -1) {
|
|
1303
1338
|
let line = data.slice(start, newlineIdx);
|
|
1339
|
+
if (exceedsLimit(line))
|
|
1340
|
+
return;
|
|
1304
1341
|
if (this.stdoutPending.length > 0) {
|
|
1305
1342
|
this.stdoutPending.push(line);
|
|
1306
1343
|
line = this.stdoutPending.join("");
|
|
@@ -1320,8 +1357,10 @@ export class BinaryBridge {
|
|
|
1320
1357
|
}
|
|
1321
1358
|
if (start < data.length) {
|
|
1322
1359
|
const rest = start === 0 ? data : data.slice(start);
|
|
1360
|
+
if (exceedsLimit(rest))
|
|
1361
|
+
return;
|
|
1323
1362
|
this.stdoutPending.push(rest);
|
|
1324
|
-
this.stdoutPendingLength += rest
|
|
1363
|
+
this.stdoutPendingLength += Buffer.byteLength(rest, "utf8");
|
|
1325
1364
|
}
|
|
1326
1365
|
}
|
|
1327
1366
|
flushStdoutBuffer() {
|
|
@@ -1365,15 +1404,15 @@ export class BinaryBridge {
|
|
|
1365
1404
|
if (taskId)
|
|
1366
1405
|
this.outstandingBackgroundTaskIds.delete(taskId);
|
|
1367
1406
|
this.syncEventLoopHold();
|
|
1368
|
-
this.onBashCompletion?.(response, this);
|
|
1407
|
+
this.dispatchHostCallback("bash completion", () => this.onBashCompletion?.(response, this));
|
|
1369
1408
|
return;
|
|
1370
1409
|
}
|
|
1371
1410
|
if (response.type === "bash_long_running") {
|
|
1372
|
-
this.onBashLongRunning?.(response, this);
|
|
1411
|
+
this.dispatchHostCallback("bash long-running", () => this.onBashLongRunning?.(response, this));
|
|
1373
1412
|
return;
|
|
1374
1413
|
}
|
|
1375
1414
|
if (response.type === "bash_pattern_match") {
|
|
1376
|
-
this.onBashPatternMatch?.(response, this);
|
|
1415
|
+
this.dispatchHostCallback("bash pattern-match", () => this.onBashPatternMatch?.(response, this));
|
|
1377
1416
|
return;
|
|
1378
1417
|
}
|
|
1379
1418
|
if (response.type === "configure_warnings") {
|
|
@@ -1411,6 +1450,18 @@ export class BinaryBridge {
|
|
|
1411
1450
|
this.accountForBashTaskResponse(entry.command, response);
|
|
1412
1451
|
entry.resolve(response);
|
|
1413
1452
|
}
|
|
1453
|
+
else if (id && typeof response.type !== "string") {
|
|
1454
|
+
// A reply for a request that already settled (its deadline and any
|
|
1455
|
+
// grace period passed). One that names a task is a warning: the task
|
|
1456
|
+
// may still be running, and this log line is the only record of its id.
|
|
1457
|
+
const taskId = bashTaskIdFrom(response);
|
|
1458
|
+
if (taskId) {
|
|
1459
|
+
this.warnVia(`Dropped late reply for settled request id=${id} task_id=${taskId}`);
|
|
1460
|
+
}
|
|
1461
|
+
else {
|
|
1462
|
+
this.logVia(`Dropped late reply for settled request id=${id}`);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1414
1465
|
else if (typeof response.type === "string") {
|
|
1415
1466
|
this.logVia(`Ignoring unknown stdout push frame type: ${response.type}`);
|
|
1416
1467
|
}
|
|
@@ -1423,11 +1474,54 @@ export class BinaryBridge {
|
|
|
1423
1474
|
this.warnVia(`Failed to parse stdout line: ${quoted}`);
|
|
1424
1475
|
}
|
|
1425
1476
|
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Keep a timed-out shell call listening for AFT's reply for a short grace
|
|
1479
|
+
* period instead of rejecting it at once. AFT may already have started the
|
|
1480
|
+
* command; its late reply carries the task id, and dropping it would leave the
|
|
1481
|
+
* agent with a running task it cannot find, which it may then start twice.
|
|
1482
|
+
*
|
|
1483
|
+
* The entry stays in `pending`, so the ordinary reply path still records the
|
|
1484
|
+
* task (see accountForBashTaskResponse) and clears the grace timer. The call
|
|
1485
|
+
* then rejects with a timeout error whose `shellOutcome` says what AFT
|
|
1486
|
+
* answered, or that nothing arrived. Returns false when the call is not a
|
|
1487
|
+
* shell command or the grace period is disabled; the caller then rejects as
|
|
1488
|
+
* before.
|
|
1489
|
+
*/
|
|
1490
|
+
holdForLateShellReply(id, entry, effectiveTimeoutMs, requestSessionId) {
|
|
1491
|
+
const graceMs = this.lateShellReplyGraceMs;
|
|
1492
|
+
if (!SHELL_TASK_COMMANDS.has(entry.command) || graceMs <= 0)
|
|
1493
|
+
return false;
|
|
1494
|
+
const timeoutMsg = `Request "${entry.command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms; waiting up to ${graceMs}ms for a late reply naming its task`;
|
|
1495
|
+
if (requestSessionId) {
|
|
1496
|
+
this.sessionWarnVia(requestSessionId, timeoutMsg);
|
|
1497
|
+
}
|
|
1498
|
+
else {
|
|
1499
|
+
this.warnVia(timeoutMsg);
|
|
1500
|
+
}
|
|
1501
|
+
const message = `${this.errorPrefix} Request "${entry.command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`;
|
|
1502
|
+
const rejectWith = entry.reject;
|
|
1503
|
+
const settle = (outcome) => {
|
|
1504
|
+
rejectWith(new BridgeTransportTimeoutError(entry.command, effectiveTimeoutMs, message, outcome));
|
|
1505
|
+
};
|
|
1506
|
+
// A reply now arrives after the caller's deadline, so it settles the call
|
|
1507
|
+
// as a timeout that reports what AFT answered.
|
|
1508
|
+
entry.resolve = (response) => settle(shellOutcomeFromLateReply(response));
|
|
1509
|
+
clearTimeout(entry.timer);
|
|
1510
|
+
entry.timer = setTimeout(() => {
|
|
1511
|
+
if (this.pending.get(id) !== entry)
|
|
1512
|
+
return;
|
|
1513
|
+
this.pending.delete(id);
|
|
1514
|
+
entry.onSettled?.();
|
|
1515
|
+
this.warnVia(`Request "${entry.command}" (id=${id}) got no reply within ${graceMs}ms after its deadline`);
|
|
1516
|
+
settle({ kind: "unknown" });
|
|
1517
|
+
}, graceMs);
|
|
1518
|
+
return true;
|
|
1519
|
+
}
|
|
1426
1520
|
accountForBashTaskResponse(command, response) {
|
|
1427
1521
|
const taskId = bashTaskIdFrom(response);
|
|
1428
1522
|
if (!taskId)
|
|
1429
1523
|
return;
|
|
1430
|
-
if (isTerminalBashStatus(response.status)) {
|
|
1524
|
+
if (isTerminalBashStatus(response.status, true)) {
|
|
1431
1525
|
this.outstandingBackgroundTaskIds.delete(taskId);
|
|
1432
1526
|
return;
|
|
1433
1527
|
}
|
|
@@ -1486,6 +1580,7 @@ export class BinaryBridge {
|
|
|
1486
1580
|
}
|
|
1487
1581
|
}
|
|
1488
1582
|
handleCrash(cause) {
|
|
1583
|
+
this.clearRestartTimer();
|
|
1489
1584
|
const proc = this.process;
|
|
1490
1585
|
this.process = null;
|
|
1491
1586
|
this.spawnedBinaryFingerprint = null;
|
|
@@ -1517,7 +1612,8 @@ export class BinaryBridge {
|
|
|
1517
1612
|
const delay = 100 * 2 ** this._restartCount; // 100ms, 200ms, 400ms
|
|
1518
1613
|
this._restartCount++;
|
|
1519
1614
|
this.logVia(`Auto-restart #${this._restartCount} in ${delay}ms`);
|
|
1520
|
-
|
|
1615
|
+
this.restartTimer = setTimeout(() => {
|
|
1616
|
+
this.restartTimer = null;
|
|
1521
1617
|
if (!this._shuttingDown && !this.isAlive()) {
|
|
1522
1618
|
try {
|
|
1523
1619
|
this.spawnProcess();
|
|
@@ -1529,7 +1625,7 @@ export class BinaryBridge {
|
|
|
1529
1625
|
}, delay);
|
|
1530
1626
|
// The crash already rejected every pending request, so nothing waits on
|
|
1531
1627
|
// this restart; it must not keep a host that is otherwise done alive.
|
|
1532
|
-
restartTimer.unref?.();
|
|
1628
|
+
this.restartTimer.unref?.();
|
|
1533
1629
|
// Also decay the counter over time so repeated crashes without any
|
|
1534
1630
|
// successful response don't permanently wedge the bridge.
|
|
1535
1631
|
this.scheduleRestartCountReset();
|
|
@@ -1540,6 +1636,7 @@ export class BinaryBridge {
|
|
|
1540
1636
|
}
|
|
1541
1637
|
}
|
|
1542
1638
|
rejectAllPending(error) {
|
|
1639
|
+
this.configureWarningClients.clear();
|
|
1543
1640
|
for (const [_id, entry] of this.pending) {
|
|
1544
1641
|
clearTimeout(entry.timer);
|
|
1545
1642
|
entry.onSettled?.();
|
|
@@ -1548,6 +1645,8 @@ export class BinaryBridge {
|
|
|
1548
1645
|
this.pending.clear();
|
|
1549
1646
|
}
|
|
1550
1647
|
scheduleRestartCountReset() {
|
|
1648
|
+
if (this._restartCount === 0)
|
|
1649
|
+
return;
|
|
1551
1650
|
this.clearRestartResetTimer();
|
|
1552
1651
|
this.restartResetTimer = setTimeout(() => {
|
|
1553
1652
|
this._restartCount = 0;
|
|
@@ -1556,6 +1655,11 @@ export class BinaryBridge {
|
|
|
1556
1655
|
// Bookkeeping only: a five-minute counter reset must not hold the host open.
|
|
1557
1656
|
this.restartResetTimer.unref?.();
|
|
1558
1657
|
}
|
|
1658
|
+
clearRestartTimer() {
|
|
1659
|
+
if (this.restartTimer)
|
|
1660
|
+
clearTimeout(this.restartTimer);
|
|
1661
|
+
this.restartTimer = null;
|
|
1662
|
+
}
|
|
1559
1663
|
clearRestartResetTimer() {
|
|
1560
1664
|
if (this.restartResetTimer) {
|
|
1561
1665
|
clearTimeout(this.restartResetTimer);
|