@gajae-code/coding-agent 0.12.6 → 0.12.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +8 -10
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
- package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +1 -3
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -4
- package/dist/types/sdk/host/host.d.ts +2 -23
- package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
- package/dist/types/session/artifacts.d.ts +5 -8
- package/dist/types/session/client-bridge.d.ts +0 -14
- package/dist/types/session/internal/managed-session-storage.d.ts +2 -5
- package/dist/types/session/messages.d.ts +1 -4
- package/dist/types/session/session-manager.d.ts +4 -5
- package/dist/types/session/streaming-output.d.ts +0 -32
- package/dist/types/task/receipt.d.ts +1 -2
- package/dist/types/task/types.d.ts +43 -178
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +0 -3
- package/dist/types/tools/output-meta.d.ts +5 -37
- package/dist/types/tools/subagent.d.ts +15 -0
- package/dist/types/tools/todo-write.d.ts +3 -0
- package/dist/types/tools/tool-result.d.ts +0 -1
- package/package.json +8 -8
- package/src/async/job-manager.ts +430 -46
- package/src/cursor.ts +13 -20
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +151 -425
- package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
- package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
- package/src/gjc-runtime/workflow-manifest.ts +0 -12
- package/src/internal-urls/agent-protocol.ts +38 -98
- package/src/internal-urls/artifact-protocol.ts +17 -28
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/internal-urls/registry-helpers.ts +0 -62
- package/src/modes/acp/acp-agent.ts +0 -24
- package/src/modes/acp/acp-client-bridge.ts +11 -154
- package/src/prompts/tools/bash.md +2 -2
- package/src/sdk/acp/adapter.ts +9 -200
- package/src/sdk/bus/chat-daemon-control.ts +3 -5
- package/src/sdk/bus/index.ts +8 -324
- package/src/sdk/bus/telegram-daemon-contract.ts +2 -4
- package/src/sdk/host/host.ts +9 -187
- package/src/sdk/host/reverse-leases.ts +42 -249
- package/src/session/agent-session.ts +15 -178
- package/src/session/artifacts.ts +74 -211
- package/src/session/client-bridge.ts +0 -15
- package/src/session/internal/managed-session-storage.ts +5 -104
- package/src/session/messages.ts +7 -32
- package/src/session/session-manager.ts +21 -12
- package/src/session/streaming-output.ts +40 -198
- package/src/task/index.ts +230 -64
- package/src/task/receipt.ts +2 -4
- package/src/task/render.ts +0 -30
- package/src/task/types.ts +10 -34
- package/src/tools/bash.ts +239 -818
- package/src/tools/eval.ts +4 -99
- package/src/tools/fetch.ts +15 -77
- package/src/tools/find.ts +3 -7
- package/src/tools/output-meta.ts +114 -696
- package/src/tools/search.ts +4 -9
- package/src/tools/subagent.ts +97 -75
- package/src/tools/todo-write.ts +47 -0
- package/src/tools/tool-result.ts +0 -1
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
- package/dist/types/utils/artifact-id.d.ts +0 -2
- package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
- package/src/utils/artifact-id.ts +0 -6
package/src/tools/bash.ts
CHANGED
|
@@ -18,12 +18,7 @@ import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
|
18
18
|
import { highlightCode, type Theme } from "../modes/theme/theme";
|
|
19
19
|
import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
|
|
20
20
|
import type { ArtifactManager } from "../session/artifacts";
|
|
21
|
-
import type {
|
|
22
|
-
ClientBridge,
|
|
23
|
-
ClientBridgeTerminalExitStatus,
|
|
24
|
-
ClientBridgeTerminalHandle,
|
|
25
|
-
ClientBridgeTerminalOutput,
|
|
26
|
-
} from "../session/client-bridge";
|
|
21
|
+
import type { ClientBridgeTerminalExitStatus, ClientBridgeTerminalOutput } from "../session/client-bridge";
|
|
27
22
|
import {
|
|
28
23
|
DEFAULT_ARTIFACT_MAX_BYTES,
|
|
29
24
|
OutputSink,
|
|
@@ -48,10 +43,8 @@ import { canUseInteractiveBashPty } from "./bash-pty-selection";
|
|
|
48
43
|
import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
|
|
49
44
|
import { checkComposerBashPolicy } from "./composer-bash-policy";
|
|
50
45
|
import {
|
|
51
|
-
formatArtifactEvidenceNotice,
|
|
52
46
|
formatArtifactReference,
|
|
53
47
|
formatStyledTruncationWarning,
|
|
54
|
-
isValidArtifactId,
|
|
55
48
|
type OutputMeta,
|
|
56
49
|
resolveBashOutputSinkHeadBytes,
|
|
57
50
|
resolveBashOutputSinkTailBytes,
|
|
@@ -69,81 +62,6 @@ const BASH_ERROR_MAX_BYTES = 4096;
|
|
|
69
62
|
const ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES = 256;
|
|
70
63
|
const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
71
64
|
const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
|
|
72
|
-
|
|
73
|
-
function maxBashErrorBytes(session: ToolSession): number {
|
|
74
|
-
const configuredKiB = session.settings.get("tools.maxInlineResultBytes");
|
|
75
|
-
if (configuredKiB <= 0) return BASH_ERROR_MAX_BYTES;
|
|
76
|
-
return Math.min(BASH_ERROR_MAX_BYTES, Math.max(1024, Math.floor(configuredKiB * 1024)));
|
|
77
|
-
}
|
|
78
|
-
const CLIENT_TERMINAL_RPC_WAIT_MS = 500;
|
|
79
|
-
const BASH_PENDING_OPERATION_LIMIT = 64;
|
|
80
|
-
const pendingBashOperations = new Set<Promise<unknown>>();
|
|
81
|
-
interface BashCleanupOwner {
|
|
82
|
-
pending: Set<Promise<unknown>>;
|
|
83
|
-
reservations: number;
|
|
84
|
-
}
|
|
85
|
-
const pendingBashCleanupOwners = new WeakMap<object, BashCleanupOwner>();
|
|
86
|
-
|
|
87
|
-
function getBashCleanupOwner(owner: object): BashCleanupOwner {
|
|
88
|
-
let state = pendingBashCleanupOwners.get(owner);
|
|
89
|
-
if (!state) {
|
|
90
|
-
state = { pending: new Set(), reservations: 0 };
|
|
91
|
-
pendingBashCleanupOwners.set(owner, state);
|
|
92
|
-
}
|
|
93
|
-
return state;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function reserveBashCleanup(owner: object): boolean {
|
|
97
|
-
const state = getBashCleanupOwner(owner);
|
|
98
|
-
if (state.pending.size + state.reservations >= BASH_PENDING_OPERATION_LIMIT) return false;
|
|
99
|
-
state.reservations++;
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function consumeBashCleanupReservation(owner: object): void {
|
|
104
|
-
const state = getBashCleanupOwner(owner);
|
|
105
|
-
state.reservations = Math.max(0, state.reservations - 1);
|
|
106
|
-
}
|
|
107
|
-
interface BashReleaseOwner {
|
|
108
|
-
pending: Set<Promise<unknown>>;
|
|
109
|
-
reservations: number;
|
|
110
|
-
}
|
|
111
|
-
const pendingBashReleaseOwners = new WeakMap<object, BashReleaseOwner>();
|
|
112
|
-
|
|
113
|
-
function getBashReleaseOwner(owner: object): BashReleaseOwner {
|
|
114
|
-
let state = pendingBashReleaseOwners.get(owner);
|
|
115
|
-
if (!state) {
|
|
116
|
-
state = { pending: new Set(), reservations: 0 };
|
|
117
|
-
pendingBashReleaseOwners.set(owner, state);
|
|
118
|
-
}
|
|
119
|
-
return state;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function reserveBashRelease(owner: object): boolean {
|
|
123
|
-
const state = getBashReleaseOwner(owner);
|
|
124
|
-
if (state.pending.size + state.reservations >= BASH_PENDING_OPERATION_LIMIT) return false;
|
|
125
|
-
state.reservations++;
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function consumeBashReleaseReservation(owner: object): void {
|
|
130
|
-
const state = getBashReleaseOwner(owner);
|
|
131
|
-
state.reservations = Math.max(0, state.reservations - 1);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function trackPendingOperation<T>(pending: Set<Promise<unknown>>, limit: number, promise: Promise<T>): boolean {
|
|
135
|
-
if (pending.size >= limit) return false;
|
|
136
|
-
pending.add(promise);
|
|
137
|
-
void promise.then(
|
|
138
|
-
() => pending.delete(promise),
|
|
139
|
-
() => pending.delete(promise),
|
|
140
|
-
);
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function hasBoundedOperationCapacity(): boolean {
|
|
145
|
-
return pendingBashOperations.size < BASH_PENDING_OPERATION_LIMIT;
|
|
146
|
-
}
|
|
147
65
|
const READ_ONLY_BASH_ENV: Record<string, string> = {
|
|
148
66
|
GREP_OPTIONS: "",
|
|
149
67
|
GREP_COLOR: "",
|
|
@@ -159,118 +77,10 @@ function boundArtifactSaveDiagnostic(error: unknown): string {
|
|
|
159
77
|
return truncateHeadBytes(normalized, ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES).text;
|
|
160
78
|
}
|
|
161
79
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
async function awaitBoundedOperationIn<T>(
|
|
168
|
-
pending: Set<Promise<unknown>>,
|
|
169
|
-
operation: () => Promise<T>,
|
|
170
|
-
limitDiagnostic: string,
|
|
171
|
-
reserved = false,
|
|
172
|
-
): Promise<BoundedOperationOutcome<T>> {
|
|
173
|
-
const operationLimit = reserved ? BASH_PENDING_OPERATION_LIMIT + 1 : BASH_PENDING_OPERATION_LIMIT;
|
|
174
|
-
if (pending.size >= operationLimit) {
|
|
175
|
-
return { status: "failed", diagnostic: limitDiagnostic };
|
|
176
|
-
}
|
|
177
|
-
const promise = Promise.resolve().then(operation);
|
|
178
|
-
if (!trackPendingOperation(pending, operationLimit, promise)) {
|
|
179
|
-
return { status: "failed", diagnostic: limitDiagnostic };
|
|
180
|
-
}
|
|
181
|
-
return Promise.race([
|
|
182
|
-
promise.then(
|
|
183
|
-
value => ({ status: "ok", value }) satisfies BoundedOperationOutcome<T>,
|
|
184
|
-
error =>
|
|
185
|
-
({ status: "failed", diagnostic: boundArtifactSaveDiagnostic(error) }) satisfies BoundedOperationOutcome<T>,
|
|
186
|
-
),
|
|
187
|
-
Bun.sleep(CLIENT_TERMINAL_RPC_WAIT_MS).then(
|
|
188
|
-
() => ({ status: "timeout", diagnostic: `did not settle within ${CLIENT_TERMINAL_RPC_WAIT_MS}ms` }) as const,
|
|
189
|
-
),
|
|
190
|
-
]);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function awaitBoundedOperation<T>(operation: () => Promise<T>): Promise<BoundedOperationOutcome<T>> {
|
|
194
|
-
return awaitBoundedOperationIn(pendingBashOperations, operation, "pending operation limit reached");
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function awaitBoundedCleanup<T>(
|
|
198
|
-
owner: object,
|
|
199
|
-
operation: () => Promise<T>,
|
|
200
|
-
reserved = false,
|
|
201
|
-
): Promise<BoundedOperationOutcome<T>> {
|
|
202
|
-
return awaitBoundedOperationIn(
|
|
203
|
-
getBashCleanupOwner(owner).pending,
|
|
204
|
-
operation,
|
|
205
|
-
"pending cleanup limit reached",
|
|
206
|
-
reserved,
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function awaitBoundedRelease<T>(owner: object, operation: () => Promise<T>): Promise<BoundedOperationOutcome<T>> {
|
|
211
|
-
return awaitBoundedOperationIn(getBashReleaseOwner(owner).pending, operation, "pending release limit reached");
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async function quarantineClientTerminalLease(
|
|
215
|
-
bridge: ClientBridge,
|
|
216
|
-
handle: ClientBridgeTerminalHandle,
|
|
217
|
-
reason: string,
|
|
218
|
-
): Promise<BoundedOperationOutcome<void>> {
|
|
219
|
-
if (!bridge.quarantineProviderLease) {
|
|
220
|
-
return { status: "failed", diagnostic: "terminal provider lease quarantine is unavailable" };
|
|
221
|
-
}
|
|
222
|
-
return awaitBoundedCleanup(bridge, () =>
|
|
223
|
-
bridge.quarantineProviderLease!(handle.providerLeaseIdentity ?? handle.providerLeaseId, reason),
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
const quarantinedClientTerminalProviders = new WeakMap<ClientBridge, Set<string>>();
|
|
227
|
-
|
|
228
|
-
function clientTerminalProviderKey(handle: ClientBridgeTerminalHandle): string {
|
|
229
|
-
const identity = handle.providerLeaseIdentity;
|
|
230
|
-
return identity
|
|
231
|
-
? `${identity.leaseId}\0${identity.connectionId}\0${identity.connectionGeneration}\0${identity.fence}`
|
|
232
|
-
: handle.providerLeaseId;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function markClientTerminalProviderQuarantined(bridge: ClientBridge, handle: ClientBridgeTerminalHandle): void {
|
|
236
|
-
let quarantined = quarantinedClientTerminalProviders.get(bridge);
|
|
237
|
-
if (!quarantined) {
|
|
238
|
-
quarantined = new Set();
|
|
239
|
-
quarantinedClientTerminalProviders.set(bridge, quarantined);
|
|
240
|
-
}
|
|
241
|
-
quarantined.add(clientTerminalProviderKey(handle));
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function hasQuarantinedClientTerminalProvider(bridge: ClientBridge): boolean {
|
|
245
|
-
return (quarantinedClientTerminalProviders.get(bridge)?.size ?? 0) > 0;
|
|
246
|
-
}
|
|
247
|
-
function clearClientTerminalProviderQuarantine(bridge: ClientBridge, handle: ClientBridgeTerminalHandle): void {
|
|
248
|
-
const quarantined = quarantinedClientTerminalProviders.get(bridge);
|
|
249
|
-
if (!quarantined) return;
|
|
250
|
-
quarantined.delete(clientTerminalProviderKey(handle));
|
|
251
|
-
if (quarantined.size === 0) quarantinedClientTerminalProviders.delete(bridge);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async function recoverClientTerminalOutput(
|
|
255
|
-
handle: ClientBridgeTerminalHandle,
|
|
256
|
-
phase: string,
|
|
257
|
-
readCurrentOutput: () => Promise<BoundedOperationOutcome<ClientBridgeTerminalOutput>> = () =>
|
|
258
|
-
awaitBoundedOperation(() => handle.currentOutput()),
|
|
259
|
-
): Promise<{ current: ClientBridgeTerminalOutput; diagnostic?: string }> {
|
|
260
|
-
const outcome = await readCurrentOutput();
|
|
261
|
-
if (outcome.status === "ok") return { current: outcome.value };
|
|
262
|
-
logger.warn("ACP terminal output recovery failed", {
|
|
263
|
-
terminalId: handle.terminalId,
|
|
264
|
-
phase,
|
|
265
|
-
diagnostic: outcome.diagnostic,
|
|
266
|
-
});
|
|
267
|
-
return { current: { output: "", truncated: true }, diagnostic: outcome.diagnostic };
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function summarizeOriginalArtifactSave(artifactId: string, originalText: string): BashOriginalArtifactSaveResult {
|
|
271
|
-
if (!isValidArtifactId(artifactId)) {
|
|
272
|
-
return { status: "failed", diagnostic: "storage returned an invalid artifact id" };
|
|
273
|
-
}
|
|
80
|
+
function summarizeOriginalArtifactSave(
|
|
81
|
+
artifactId: string,
|
|
82
|
+
originalText: string,
|
|
83
|
+
): Extract<BashOriginalArtifactSaveResult, { status: "saved" }> {
|
|
274
84
|
const originalBytes = Buffer.byteLength(originalText, "utf-8");
|
|
275
85
|
if (originalBytes <= DEFAULT_ARTIFACT_MAX_BYTES) {
|
|
276
86
|
return { status: "saved", artifactId, complete: true };
|
|
@@ -284,26 +94,6 @@ function summarizeOriginalArtifactSave(artifactId: string, originalText: string)
|
|
|
284
94
|
};
|
|
285
95
|
}
|
|
286
96
|
|
|
287
|
-
async function allocateBashOutputArtifact(
|
|
288
|
-
session: ToolSession,
|
|
289
|
-
toolType: "bash" | "bash-original",
|
|
290
|
-
): Promise<{ id?: string; path?: string }> {
|
|
291
|
-
const allocateOutputArtifact = session.allocateOutputArtifact;
|
|
292
|
-
if (!allocateOutputArtifact) return {};
|
|
293
|
-
const outcome = await awaitBoundedOperation(() => allocateOutputArtifact(toolType));
|
|
294
|
-
if (outcome.status !== "ok") {
|
|
295
|
-
logger.warn("Bash artifact allocation failed", { toolType, error: outcome.diagnostic });
|
|
296
|
-
return {};
|
|
297
|
-
}
|
|
298
|
-
const allocation = outcome.value;
|
|
299
|
-
if (!allocation) return {};
|
|
300
|
-
if (!isValidArtifactId(allocation.id)) {
|
|
301
|
-
logger.warn("Bash artifact allocation returned an invalid id", { toolType });
|
|
302
|
-
return {};
|
|
303
|
-
}
|
|
304
|
-
return allocation;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
97
|
function artifactSaveResultNotice(
|
|
308
98
|
result: BashOriginalArtifactSaveResult,
|
|
309
99
|
hasAlternateCompleteArtifact = false,
|
|
@@ -320,51 +110,36 @@ function artifactTruncatedBytesForResult(result: BashResult | BashInteractiveRes
|
|
|
320
110
|
return typeof bytes === "number" && bytes > 0 ? bytes : undefined;
|
|
321
111
|
}
|
|
322
112
|
|
|
323
|
-
function sourceTruncatedBytesForResult(result: BashResult | BashInteractiveResult): number | undefined {
|
|
324
|
-
const bytes = (result as OutputSummary).sourceTruncatedBytes;
|
|
325
|
-
return typeof bytes === "number" && bytes > 0 ? bytes : undefined;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function sourceCaptureIncompleteForResult(result: BashResult | BashInteractiveResult): boolean {
|
|
329
|
-
return (result as OutputSummary).sourceCaptureIncomplete === true;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
113
|
function artifactReferenceForResult(result: BashResult | BashInteractiveResult): string | undefined {
|
|
333
114
|
return result.artifactId
|
|
334
|
-
? formatArtifactReference(
|
|
335
|
-
result.artifactId,
|
|
336
|
-
artifactTruncatedBytesForResult(result),
|
|
337
|
-
sourceTruncatedBytesForResult(result),
|
|
338
|
-
sourceCaptureIncompleteForResult(result),
|
|
339
|
-
)
|
|
115
|
+
? formatArtifactReference(result.artifactId, artifactTruncatedBytesForResult(result))
|
|
340
116
|
: undefined;
|
|
341
117
|
}
|
|
342
118
|
|
|
119
|
+
function rawArtifactReferenceForSavedResult(
|
|
120
|
+
result: Extract<BashOriginalArtifactSaveResult, { status: "saved" }>,
|
|
121
|
+
): string {
|
|
122
|
+
return result.complete
|
|
123
|
+
? `artifact://${result.artifactId}`
|
|
124
|
+
: formatArtifactReference(result.artifactId, result.omittedBytes);
|
|
125
|
+
}
|
|
126
|
+
|
|
343
127
|
function appendRawArtifactFooter(
|
|
344
128
|
summary: OutputSummary,
|
|
345
129
|
result: Extract<BashOriginalArtifactSaveResult, { status: "saved" }>,
|
|
346
130
|
): void {
|
|
347
|
-
summary.artifactFailureDiagnostic = "artifact save could not be proven at the current delivery boundary";
|
|
348
131
|
summary.artifactId = result.artifactId;
|
|
349
|
-
summary.artifactVerified = false;
|
|
350
132
|
if (!result.complete) summary.artifactTruncatedBytes = result.omittedBytes;
|
|
133
|
+
const separator = summary.output.endsWith("\n") ? "" : "\n";
|
|
134
|
+
summary.output = `${summary.output}${separator}[raw output: ${rawArtifactReferenceForSavedResult(result)}]`;
|
|
351
135
|
}
|
|
352
136
|
|
|
353
137
|
function artifactReferenceIsReachable(text: string, result: BashResult | BashInteractiveResult): boolean {
|
|
354
138
|
if (!result.artifactId || !text.includes(`artifact://${result.artifactId}`)) return false;
|
|
355
139
|
const artifactTruncatedBytes = artifactTruncatedBytesForResult(result);
|
|
356
|
-
const sourceTruncatedBytes = sourceTruncatedBytesForResult(result);
|
|
357
|
-
const sourceCaptureIncomplete = sourceCaptureIncompleteForResult(result);
|
|
358
140
|
return (
|
|
359
|
-
|
|
360
|
-
text.includes(
|
|
361
|
-
formatArtifactReference(
|
|
362
|
-
result.artifactId,
|
|
363
|
-
artifactTruncatedBytes,
|
|
364
|
-
sourceTruncatedBytes,
|
|
365
|
-
sourceCaptureIncomplete,
|
|
366
|
-
),
|
|
367
|
-
)
|
|
141
|
+
artifactTruncatedBytes === undefined ||
|
|
142
|
+
text.includes(formatArtifactReference(result.artifactId, artifactTruncatedBytes))
|
|
368
143
|
);
|
|
369
144
|
}
|
|
370
145
|
|
|
@@ -384,20 +159,11 @@ function artifactWriterFailureNotice(result: BashResult | BashInteractiveResult)
|
|
|
384
159
|
}
|
|
385
160
|
|
|
386
161
|
function completeOutputArtifactAvailable(
|
|
387
|
-
result: Pick<
|
|
388
|
-
OutputSummary,
|
|
389
|
-
| "artifactId"
|
|
390
|
-
| "artifactTruncatedBytes"
|
|
391
|
-
| "sourceTruncatedBytes"
|
|
392
|
-
| "sourceCaptureIncomplete"
|
|
393
|
-
| "artifactFailureDiagnostic"
|
|
394
|
-
>,
|
|
162
|
+
result: Pick<OutputSummary, "artifactId" | "artifactTruncatedBytes" | "artifactFailureDiagnostic">,
|
|
395
163
|
): boolean {
|
|
396
164
|
return (
|
|
397
165
|
result.artifactId !== undefined &&
|
|
398
166
|
(typeof result.artifactTruncatedBytes !== "number" || result.artifactTruncatedBytes <= 0) &&
|
|
399
|
-
(typeof result.sourceTruncatedBytes !== "number" || result.sourceTruncatedBytes <= 0) &&
|
|
400
|
-
!result.sourceCaptureIncomplete &&
|
|
401
167
|
(typeof result.artifactFailureDiagnostic !== "string" || result.artifactFailureDiagnostic.length === 0)
|
|
402
168
|
);
|
|
403
169
|
}
|
|
@@ -430,57 +196,29 @@ function formatBashFailureMessage(
|
|
|
430
196
|
result: BashResult | BashInteractiveResult,
|
|
431
197
|
text: string,
|
|
432
198
|
explicitCause?: string,
|
|
433
|
-
maxBytes = BASH_ERROR_MAX_BYTES,
|
|
434
199
|
): string {
|
|
435
200
|
const statusCause = failureStatusCause(result, text, explicitCause);
|
|
436
201
|
const bodyText = removeTrailingFailureCause(text, statusCause);
|
|
437
|
-
const bodyStatusEvidence = Array.from(
|
|
438
|
-
bodyText.matchAll(/Command (?:timed out(?: after \d+ seconds?)?|cancelled|aborted)/gu),
|
|
439
|
-
)
|
|
440
|
-
.map(match => match[0])
|
|
441
|
-
.findLast(candidate => candidate !== statusCause);
|
|
442
202
|
const suffixParts: string[] = [];
|
|
443
|
-
const
|
|
444
|
-
if (
|
|
203
|
+
const reference = artifactReferenceForResult(result);
|
|
204
|
+
if (reference) suffixParts.push(reference);
|
|
445
205
|
const writerNotice = artifactWriterFailureNotice(result);
|
|
446
206
|
if (writerNotice) suffixParts.push(writerNotice);
|
|
447
|
-
if (bodyStatusEvidence) suffixParts.push(bodyStatusEvidence);
|
|
448
207
|
if (statusCause) suffixParts.push(statusCause);
|
|
449
208
|
const suffix = suffixParts.join("\n\n");
|
|
450
209
|
const separator = bodyText.length > 0 && suffix.length > 0 ? "\n\n" : "";
|
|
451
210
|
const bodyBudget = Math.max(
|
|
452
211
|
0,
|
|
453
|
-
|
|
212
|
+
BASH_ERROR_MAX_BYTES - Buffer.byteLength(suffix, "utf-8") - Buffer.byteLength(separator, "utf-8"),
|
|
454
213
|
);
|
|
455
214
|
const body = truncateTailBytes(bodyText, bodyBudget).text;
|
|
456
215
|
return `${body}${body.length > 0 ? separator : ""}${suffix}`;
|
|
457
216
|
}
|
|
458
217
|
|
|
459
|
-
function normalizeClientTerminalOutput(current: ClientBridgeTerminalOutput): {
|
|
460
|
-
current: ClientBridgeTerminalOutput;
|
|
461
|
-
malformed: boolean;
|
|
462
|
-
} {
|
|
463
|
-
const runtimeCurrent = current as unknown;
|
|
464
|
-
const outputRecord =
|
|
465
|
-
typeof runtimeCurrent === "object" && runtimeCurrent !== null
|
|
466
|
-
? (runtimeCurrent as Record<string, unknown>)
|
|
467
|
-
: undefined;
|
|
468
|
-
const outputValid = typeof outputRecord?.output === "string";
|
|
469
|
-
const truncatedValid = typeof outputRecord?.truncated === "boolean";
|
|
470
|
-
return {
|
|
471
|
-
current: {
|
|
472
|
-
output: outputValid ? (outputRecord.output as string) : "",
|
|
473
|
-
truncated: truncatedValid ? (outputRecord.truncated as boolean) : true,
|
|
474
|
-
},
|
|
475
|
-
malformed: !outputValid || !truncatedValid,
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
218
|
async function boundClientTerminalOutput(
|
|
480
219
|
output: string,
|
|
481
220
|
alreadyTruncated: boolean,
|
|
482
221
|
settings: ToolSession["settings"],
|
|
483
|
-
sourceMalformed = false,
|
|
484
222
|
): Promise<{ summary: OutputSummary; locallyTruncated: boolean }> {
|
|
485
223
|
const tailBytes = resolveBashOutputSinkTailBytes(settings);
|
|
486
224
|
const headBytes = resolveBashOutputSinkHeadBytes(settings);
|
|
@@ -490,8 +228,7 @@ async function boundClientTerminalOutput(
|
|
|
490
228
|
return {
|
|
491
229
|
summary: {
|
|
492
230
|
...bounded,
|
|
493
|
-
truncated: alreadyTruncated ||
|
|
494
|
-
...(alreadyTruncated || sourceMalformed ? { sourceCaptureIncomplete: true } : {}),
|
|
231
|
+
truncated: alreadyTruncated || bounded.truncated,
|
|
495
232
|
},
|
|
496
233
|
locallyTruncated: bounded.truncated,
|
|
497
234
|
};
|
|
@@ -508,52 +245,27 @@ interface PreparedClientTerminalOutput {
|
|
|
508
245
|
async function prepareClientTerminalOutput(
|
|
509
246
|
session: ToolSession,
|
|
510
247
|
current: ClientBridgeTerminalOutput,
|
|
511
|
-
forceSourceIncomplete = false,
|
|
512
248
|
): Promise<PreparedClientTerminalOutput> {
|
|
513
|
-
const normalized = normalizeClientTerminalOutput(current);
|
|
514
|
-
const normalizedCurrent = normalized.current;
|
|
515
|
-
const sourceMalformed = normalized.malformed;
|
|
516
249
|
const { summary, locallyTruncated } = await boundClientTerminalOutput(
|
|
517
|
-
|
|
518
|
-
|
|
250
|
+
current.output,
|
|
251
|
+
current.truncated,
|
|
519
252
|
session.settings,
|
|
520
|
-
sourceMalformed,
|
|
521
253
|
);
|
|
522
|
-
if (forceSourceIncomplete) {
|
|
523
|
-
summary.truncated = true;
|
|
524
|
-
summary.sourceCaptureIncomplete = true;
|
|
525
|
-
}
|
|
526
254
|
let artifactSaveResult: BashOriginalArtifactSaveResult | undefined;
|
|
527
|
-
if (locallyTruncated && !
|
|
528
|
-
artifactSaveResult = await saveBashOriginalArtifact(session,
|
|
255
|
+
if (locallyTruncated && !current.truncated) {
|
|
256
|
+
artifactSaveResult = await saveBashOriginalArtifact(session, current.output);
|
|
529
257
|
if (artifactSaveResult.status === "saved") appendRawArtifactFooter(summary, artifactSaveResult);
|
|
530
258
|
}
|
|
531
259
|
const artifactSaveNotice = artifactSaveResult
|
|
532
260
|
? artifactSaveResultNotice(artifactSaveResult, completeOutputArtifactAvailable(summary))
|
|
533
261
|
: undefined;
|
|
534
|
-
return { current
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function markClientTerminalSourceIncomplete(prepared: PreparedClientTerminalOutput): void {
|
|
538
|
-
prepared.summary.truncated = true;
|
|
539
|
-
prepared.summary.sourceCaptureIncomplete = true;
|
|
540
|
-
if (!prepared.summary.artifactId) return;
|
|
541
|
-
const fullFooter = `[raw output: artifact://${prepared.summary.artifactId}]`;
|
|
542
|
-
if (!prepared.summary.output.endsWith(fullFooter)) return;
|
|
543
|
-
const retainedReference = formatArtifactReference(
|
|
544
|
-
prepared.summary.artifactId,
|
|
545
|
-
prepared.summary.artifactTruncatedBytes,
|
|
546
|
-
prepared.summary.sourceTruncatedBytes,
|
|
547
|
-
true,
|
|
548
|
-
);
|
|
549
|
-
prepared.summary.output = `${prepared.summary.output.slice(0, -fullFooter.length)}${retainedReference}`;
|
|
262
|
+
return { current, summary, locallyTruncated, artifactSaveResult, artifactSaveNotice };
|
|
550
263
|
}
|
|
551
264
|
|
|
552
265
|
function formatClientTerminalAbortFailure(
|
|
553
266
|
prepared: PreparedClientTerminalOutput,
|
|
554
267
|
readDiagnostic?: string,
|
|
555
268
|
pendingNotices: readonly string[] = [],
|
|
556
|
-
maxBytes = BASH_ERROR_MAX_BYTES,
|
|
557
269
|
): string {
|
|
558
270
|
const notices = [
|
|
559
271
|
...pendingNotices,
|
|
@@ -568,27 +280,7 @@ function formatClientTerminalAbortFailure(
|
|
|
568
280
|
exitCode: undefined,
|
|
569
281
|
cancelled: true,
|
|
570
282
|
};
|
|
571
|
-
return formatBashFailureMessage(result, outputText, "Command aborted"
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
function formatClientTerminalOwnershipFailure(
|
|
575
|
-
prepared: PreparedClientTerminalOutput,
|
|
576
|
-
diagnostic: string,
|
|
577
|
-
maxBytes = BASH_ERROR_MAX_BYTES,
|
|
578
|
-
): string {
|
|
579
|
-
markClientTerminalSourceIncomplete(prepared);
|
|
580
|
-
const notices = [
|
|
581
|
-
...(prepared.current.truncated || prepared.locallyTruncated ? ["(output truncated)"] : []),
|
|
582
|
-
...(prepared.artifactSaveNotice ? [prepared.artifactSaveNotice] : []),
|
|
583
|
-
`Terminal release failed: ${diagnostic}`,
|
|
584
|
-
];
|
|
585
|
-
const outputText = [prepared.summary.output || "(no output)", ...notices].filter(Boolean).join("\n");
|
|
586
|
-
const result: BashResult = {
|
|
587
|
-
...prepared.summary,
|
|
588
|
-
exitCode: undefined,
|
|
589
|
-
cancelled: false,
|
|
590
|
-
};
|
|
591
|
-
return formatBashFailureMessage(result, outputText, "Terminal release ownership incomplete", maxBytes);
|
|
283
|
+
return formatBashFailureMessage(result, outputText, "Command aborted");
|
|
592
284
|
}
|
|
593
285
|
|
|
594
286
|
function appendArtifactDetails(text: string, result: BashResult | BashInteractiveResult): string {
|
|
@@ -611,41 +303,41 @@ async function saveBashOriginalArtifact(
|
|
|
611
303
|
return { status: "failed", diagnostic: boundArtifactSaveDiagnostic(error) };
|
|
612
304
|
}
|
|
613
305
|
if (manager) {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
306
|
+
try {
|
|
307
|
+
const artifactId = await manager.save(originalText, "bash-original");
|
|
308
|
+
return artifactId
|
|
309
|
+
? summarizeOriginalArtifactSave(artifactId, originalText)
|
|
310
|
+
: { status: "failed", diagnostic: "storage returned no artifact id" };
|
|
311
|
+
} catch (error) {
|
|
312
|
+
return { status: "failed", diagnostic: boundArtifactSaveDiagnostic(error) };
|
|
313
|
+
}
|
|
619
314
|
}
|
|
620
315
|
|
|
621
316
|
if (!session.allocateOutputArtifact) return { status: "unavailable" };
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
317
|
+
let alloc: { id?: string; path?: string } | undefined;
|
|
318
|
+
try {
|
|
319
|
+
alloc = await session.allocateOutputArtifact("bash-original");
|
|
320
|
+
} catch (error) {
|
|
321
|
+
return { status: "failed", diagnostic: boundArtifactSaveDiagnostic(error) };
|
|
322
|
+
}
|
|
626
323
|
if (!alloc?.path || !alloc.id) return { status: "unavailable" };
|
|
627
|
-
|
|
628
|
-
|
|
324
|
+
try {
|
|
325
|
+
const saveResult = summarizeOriginalArtifactSave(alloc.id, originalText);
|
|
326
|
+
const payload = saveResult.complete
|
|
327
|
+
? originalText
|
|
328
|
+
: (() => {
|
|
329
|
+
const retained = truncateHeadBytes(originalText, DEFAULT_ARTIFACT_MAX_BYTES);
|
|
330
|
+
return `${retained.text}\n[artifact truncated after ${retained.bytes} bytes; omitted at least ${saveResult.omittedBytes} bytes]\n`;
|
|
331
|
+
})();
|
|
332
|
+
await Bun.write(alloc.path, payload);
|
|
333
|
+
return saveResult;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
return { status: "failed", diagnostic: boundArtifactSaveDiagnostic(error) };
|
|
629
336
|
}
|
|
630
|
-
const artifactPath = alloc.path;
|
|
631
|
-
const saveResult = summarizeOriginalArtifactSave(alloc.id, originalText);
|
|
632
|
-
if (saveResult.status !== "saved") return saveResult;
|
|
633
|
-
const payload = saveResult.complete
|
|
634
|
-
? originalText
|
|
635
|
-
: (() => {
|
|
636
|
-
const retained = truncateHeadBytes(originalText, DEFAULT_ARTIFACT_MAX_BYTES);
|
|
637
|
-
return `${retained.text}\n[artifact truncated after ${retained.bytes} bytes; omitted at least ${saveResult.omittedBytes} bytes]\n`;
|
|
638
|
-
})();
|
|
639
|
-
const writeOutcome = await awaitBoundedOperation(() => Bun.write(artifactPath, payload));
|
|
640
|
-
return writeOutcome.status === "ok" ? saveResult : { status: "failed", diagnostic: writeOutcome.diagnostic };
|
|
641
337
|
}
|
|
642
338
|
|
|
643
|
-
const bashArtifactPublishers = new WeakMap<ToolSession, TerminalArtifactPublisher>();
|
|
644
|
-
|
|
645
339
|
function createBashArtifactPublisher(session: ToolSession): TerminalArtifactPublisher {
|
|
646
|
-
|
|
647
|
-
if (existing) return existing;
|
|
648
|
-
const publisher: TerminalArtifactPublisher = async (content, _info): Promise<TerminalArtifactPublishResult> => {
|
|
340
|
+
return async (content, _info): Promise<TerminalArtifactPublishResult> => {
|
|
649
341
|
let manager: ArtifactManager | null | undefined;
|
|
650
342
|
try {
|
|
651
343
|
manager = session.getArtifactManager?.();
|
|
@@ -653,15 +345,15 @@ function createBashArtifactPublisher(session: ToolSession): TerminalArtifactPubl
|
|
|
653
345
|
return { status: "failed", diagnostic: boundArtifactSaveDiagnostic(error) };
|
|
654
346
|
}
|
|
655
347
|
if (!manager) return { status: "unavailable" };
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
348
|
+
try {
|
|
349
|
+
const artifactId = await manager.save(content, "bash");
|
|
350
|
+
return artifactId
|
|
351
|
+
? { status: "published", artifactId }
|
|
352
|
+
: { status: "failed", diagnostic: "storage returned no artifact id" };
|
|
353
|
+
} catch (error) {
|
|
354
|
+
return { status: "failed", diagnostic: boundArtifactSaveDiagnostic(error) };
|
|
355
|
+
}
|
|
661
356
|
};
|
|
662
|
-
Object.defineProperty(publisher, "owner", { value: session });
|
|
663
|
-
bashArtifactPublishers.set(session, publisher);
|
|
664
|
-
return publisher;
|
|
665
357
|
}
|
|
666
358
|
|
|
667
359
|
export async function saveBashOriginalArtifactForTests(
|
|
@@ -1079,7 +771,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1079
771
|
"bash",
|
|
1080
772
|
label,
|
|
1081
773
|
async ({ jobId, signal: runSignal, reportProgress }) => {
|
|
1082
|
-
const { path: artifactPath, id: artifactId } = await
|
|
774
|
+
const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
|
|
1083
775
|
const artifactPublisher = createBashArtifactPublisher(this.session);
|
|
1084
776
|
const spillThreshold = resolveBashOutputSinkTailBytes(this.session.settings);
|
|
1085
777
|
const headBytes = resolveBashOutputSinkHeadBytes(this.session.settings);
|
|
@@ -1437,7 +1129,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1437
1129
|
"bash",
|
|
1438
1130
|
label,
|
|
1439
1131
|
async ({ jobId: id, signal, reportProgress }) => {
|
|
1440
|
-
const { path: artifactPath, id: artifactId } = await
|
|
1132
|
+
const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
|
|
1441
1133
|
const artifactPublisher = createBashArtifactPublisher(this.session);
|
|
1442
1134
|
const spillThreshold = resolveBashOutputSinkTailBytes(this.session.settings);
|
|
1443
1135
|
const headBytes = resolveBashOutputSinkHeadBytes(this.session.settings);
|
|
@@ -1489,7 +1181,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1489
1181
|
}
|
|
1490
1182
|
|
|
1491
1183
|
async execute(
|
|
1492
|
-
|
|
1184
|
+
_toolCallId: string,
|
|
1493
1185
|
{
|
|
1494
1186
|
command: rawCommand,
|
|
1495
1187
|
env: rawEnv,
|
|
@@ -1624,207 +1316,39 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1624
1316
|
}
|
|
1625
1317
|
|
|
1626
1318
|
if (clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty) {
|
|
1627
|
-
const
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
}
|
|
1644
|
-
let killReserved = true;
|
|
1645
|
-
const killHandle = (handle: ClientBridgeTerminalHandle): Promise<BoundedOperationOutcome<void>> => {
|
|
1646
|
-
if (killReserved) {
|
|
1647
|
-
killReserved = false;
|
|
1648
|
-
consumeBashCleanupReservation(clientBridge);
|
|
1649
|
-
}
|
|
1650
|
-
return awaitBoundedCleanup(clientBridge, () => handle.kill());
|
|
1651
|
-
};
|
|
1652
|
-
let releaseReserved = true;
|
|
1653
|
-
const releaseHandle = (handle: ClientBridgeTerminalHandle): Promise<BoundedOperationOutcome<void>> => {
|
|
1654
|
-
if (releaseReserved) {
|
|
1655
|
-
releaseReserved = false;
|
|
1656
|
-
consumeBashReleaseReservation(clientBridge);
|
|
1657
|
-
}
|
|
1658
|
-
return awaitBoundedRelease(clientBridge, () => handle.release());
|
|
1659
|
-
};
|
|
1660
|
-
const terminalCreateAbort = new AbortController();
|
|
1661
|
-
const terminalCreateSignal = signal
|
|
1662
|
-
? AbortSignal.any([signal, terminalCreateAbort.signal])
|
|
1663
|
-
: terminalCreateAbort.signal;
|
|
1664
|
-
const terminalCreatePromise = Promise.resolve().then(() =>
|
|
1665
|
-
createTerminal({
|
|
1666
|
-
toolCallId,
|
|
1667
|
-
command,
|
|
1668
|
-
cwd: commandCwd,
|
|
1669
|
-
env: resolvedEnv
|
|
1670
|
-
? Object.entries(resolvedEnv).map(([name, value]) => ({ name, value: value as string }))
|
|
1671
|
-
: undefined,
|
|
1672
|
-
outputByteLimit: DEFAULT_ARTIFACT_MAX_BYTES,
|
|
1673
|
-
signal: terminalCreateSignal,
|
|
1674
|
-
}),
|
|
1675
|
-
);
|
|
1676
|
-
const terminalCreateOperation = awaitBoundedOperation(() => terminalCreatePromise);
|
|
1677
|
-
const createAborted = Promise.withResolvers<{ status: "aborted" }>();
|
|
1678
|
-
const onCreateAbort = () => createAborted.resolve({ status: "aborted" });
|
|
1679
|
-
signal?.addEventListener("abort", onCreateAbort, { once: true });
|
|
1680
|
-
const terminalCreate = signal
|
|
1681
|
-
? await Promise.race([terminalCreateOperation, createAborted.promise])
|
|
1682
|
-
: await terminalCreateOperation;
|
|
1683
|
-
signal?.removeEventListener("abort", onCreateAbort);
|
|
1684
|
-
terminalCreateAbort.abort();
|
|
1685
|
-
if (terminalCreate.status !== "ok") {
|
|
1686
|
-
void terminalCreatePromise.then(
|
|
1687
|
-
async lateHandle => {
|
|
1688
|
-
const killOutcome = await killHandle(lateHandle);
|
|
1689
|
-
const releaseOutcome = await releaseHandle(lateHandle);
|
|
1690
|
-
const cleanupFailed = killOutcome.status !== "ok" || releaseOutcome.status !== "ok";
|
|
1691
|
-
if (cleanupFailed) markClientTerminalProviderQuarantined(clientBridge, lateHandle);
|
|
1692
|
-
const quarantineOutcome = cleanupFailed
|
|
1693
|
-
? await quarantineClientTerminalLease(
|
|
1694
|
-
clientBridge,
|
|
1695
|
-
lateHandle,
|
|
1696
|
-
"late ACP terminal creation cleanup did not settle",
|
|
1697
|
-
)
|
|
1698
|
-
: undefined;
|
|
1699
|
-
if (cleanupFailed) {
|
|
1700
|
-
logger.warn("Late ACP terminal cleanup failed", {
|
|
1701
|
-
terminalId: lateHandle.terminalId,
|
|
1702
|
-
killDiagnostic: killOutcome.status === "ok" ? undefined : killOutcome.diagnostic,
|
|
1703
|
-
releaseDiagnostic: releaseOutcome.status === "ok" ? undefined : releaseOutcome.diagnostic,
|
|
1704
|
-
quarantineDiagnostic:
|
|
1705
|
-
quarantineOutcome?.status === "ok" ? undefined : quarantineOutcome?.diagnostic,
|
|
1706
|
-
});
|
|
1707
|
-
}
|
|
1708
|
-
},
|
|
1709
|
-
() => {
|
|
1710
|
-
if (releaseReserved) {
|
|
1711
|
-
releaseReserved = false;
|
|
1712
|
-
consumeBashReleaseReservation(clientBridge);
|
|
1713
|
-
}
|
|
1714
|
-
if (killReserved) {
|
|
1715
|
-
killReserved = false;
|
|
1716
|
-
consumeBashCleanupReservation(clientBridge);
|
|
1717
|
-
}
|
|
1718
|
-
},
|
|
1719
|
-
);
|
|
1720
|
-
if (terminalCreate.status === "aborted") {
|
|
1721
|
-
throw new ToolAbortError("Command aborted during ACP terminal creation");
|
|
1722
|
-
}
|
|
1723
|
-
throw new ToolError(`ACP terminal creation failed: ${terminalCreate.diagnostic}`);
|
|
1724
|
-
}
|
|
1725
|
-
const handle = terminalCreate.value;
|
|
1726
|
-
pendingBashOperations.delete(terminalCreatePromise);
|
|
1727
|
-
if (!hasBoundedOperationCapacity()) {
|
|
1728
|
-
const killOutcome = await killHandle(handle);
|
|
1729
|
-
const releaseOutcome = await releaseHandle(handle);
|
|
1730
|
-
if (killOutcome.status !== "ok" || releaseOutcome.status !== "ok") {
|
|
1731
|
-
const quarantineOutcome = await quarantineClientTerminalLease(
|
|
1732
|
-
clientBridge,
|
|
1733
|
-
handle,
|
|
1734
|
-
"terminal exit observation admission failed after cleanup uncertainty",
|
|
1735
|
-
);
|
|
1736
|
-
if (quarantineOutcome.status !== "ok")
|
|
1737
|
-
throw new ToolError(`ACP terminal cleanup ownership incomplete: ${quarantineOutcome.diagnostic}`);
|
|
1738
|
-
}
|
|
1739
|
-
throw new ToolError("ACP terminal exit observation failed: pending operation limit reached");
|
|
1740
|
-
}
|
|
1741
|
-
let clientOutputCaptureIncomplete = false;
|
|
1742
|
-
let currentOutputOperation:
|
|
1743
|
-
| {
|
|
1744
|
-
raw: Promise<ClientBridgeTerminalOutput>;
|
|
1745
|
-
bounded: Promise<BoundedOperationOutcome<ClientBridgeTerminalOutput>>;
|
|
1746
|
-
}
|
|
1747
|
-
| undefined;
|
|
1748
|
-
let latestPreparedOutput: PreparedClientTerminalOutput | undefined;
|
|
1749
|
-
const prepareOutput = async (
|
|
1750
|
-
current: ClientBridgeTerminalOutput,
|
|
1751
|
-
forceSourceIncomplete = false,
|
|
1752
|
-
): Promise<PreparedClientTerminalOutput> => {
|
|
1753
|
-
const prepared = await prepareClientTerminalOutput(this.session, current, forceSourceIncomplete);
|
|
1754
|
-
latestPreparedOutput = prepared;
|
|
1755
|
-
return prepared;
|
|
1756
|
-
};
|
|
1757
|
-
const readCurrentOutput = (): Promise<BoundedOperationOutcome<ClientBridgeTerminalOutput>> => {
|
|
1758
|
-
if (currentOutputOperation) return currentOutputOperation.bounded;
|
|
1759
|
-
if (!hasBoundedOperationCapacity()) {
|
|
1760
|
-
return Promise.resolve({ status: "failed", diagnostic: "pending operation limit reached" });
|
|
1761
|
-
}
|
|
1762
|
-
const raw = Promise.resolve().then(() => handle.currentOutput());
|
|
1763
|
-
const bounded = awaitBoundedOperation(() => raw);
|
|
1764
|
-
currentOutputOperation = { raw, bounded };
|
|
1765
|
-
void raw.then(
|
|
1766
|
-
() => {
|
|
1767
|
-
if (currentOutputOperation?.raw === raw) currentOutputOperation = undefined;
|
|
1768
|
-
},
|
|
1769
|
-
() => {
|
|
1770
|
-
if (currentOutputOperation?.raw === raw) currentOutputOperation = undefined;
|
|
1771
|
-
},
|
|
1772
|
-
);
|
|
1773
|
-
return bounded;
|
|
1774
|
-
};
|
|
1319
|
+
const clientHeadBytes = resolveBashOutputSinkHeadBytes(this.session.settings);
|
|
1320
|
+
const clientTailBytes = resolveBashOutputSinkTailBytes(this.session.settings);
|
|
1321
|
+
const handle = await clientBridge.createTerminal({
|
|
1322
|
+
command,
|
|
1323
|
+
cwd: commandCwd,
|
|
1324
|
+
env: resolvedEnv
|
|
1325
|
+
? Object.entries(resolvedEnv).map(([name, value]) => ({ name, value: value as string }))
|
|
1326
|
+
: undefined,
|
|
1327
|
+
outputByteLimit: clientHeadBytes > 0 ? undefined : clientTailBytes,
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
// Emit partial update so the editor can embed the live terminal card.
|
|
1331
|
+
onUpdate?.({ content: [], details: { terminalId: handle.terminalId } });
|
|
1332
|
+
|
|
1333
|
+
const exitPromise = handle.waitForExit();
|
|
1334
|
+
let exitStatus!: ClientBridgeTerminalExitStatus;
|
|
1775
1335
|
|
|
1776
1336
|
type BridgeRaceResult =
|
|
1777
1337
|
| { kind: "exit"; status: ClientBridgeTerminalExitStatus }
|
|
1778
|
-
| { kind: "exit-error"; diagnostic: string }
|
|
1779
1338
|
| { kind: "poll" }
|
|
1780
1339
|
| { kind: "timeout" }
|
|
1781
1340
|
| { kind: "aborted" };
|
|
1782
|
-
const rawExitPromise = Promise.resolve().then(() => handle.waitForExit());
|
|
1783
|
-
if (!trackPendingOperation(pendingBashOperations, BASH_PENDING_OPERATION_LIMIT, rawExitPromise)) {
|
|
1784
|
-
const killOutcome = await killHandle(handle);
|
|
1785
|
-
const releaseOutcome = await releaseHandle(handle);
|
|
1786
|
-
if (killOutcome.status !== "ok" || releaseOutcome.status !== "ok") {
|
|
1787
|
-
const quarantineOutcome = await quarantineClientTerminalLease(
|
|
1788
|
-
clientBridge,
|
|
1789
|
-
handle,
|
|
1790
|
-
"terminal exit observation registration failed after cleanup uncertainty",
|
|
1791
|
-
);
|
|
1792
|
-
if (quarantineOutcome.status !== "ok")
|
|
1793
|
-
throw new ToolError(`ACP terminal cleanup ownership incomplete: ${quarantineOutcome.diagnostic}`);
|
|
1794
|
-
}
|
|
1795
|
-
throw new ToolError("ACP terminal exit observation failed: pending operation limit reached");
|
|
1796
|
-
}
|
|
1797
|
-
const exitPromise: Promise<BridgeRaceResult> = rawExitPromise.then(
|
|
1798
|
-
status => ({ kind: "exit", status }),
|
|
1799
|
-
error => ({ kind: "exit-error", diagnostic: boundArtifactSaveDiagnostic(error) }),
|
|
1800
|
-
);
|
|
1801
|
-
let exitStatus!: ClientBridgeTerminalExitStatus;
|
|
1802
1341
|
|
|
1803
1342
|
// Set up abort listener before entering the poll loop. The listener
|
|
1804
1343
|
// kicks off `handle.kill()` synchronously so a `session/cancel`
|
|
1805
1344
|
// arriving mid-poll terminates the remote command immediately,
|
|
1806
1345
|
// instead of waiting for the next `currentOutput()` to return.
|
|
1807
1346
|
const { promise: abortedP, resolve: resolveAborted } = Promise.withResolvers<void>();
|
|
1808
|
-
let killPromise: Promise<
|
|
1809
|
-
const fireKill = (): Promise<
|
|
1347
|
+
let killPromise: Promise<void> | undefined;
|
|
1348
|
+
const fireKill = (): Promise<void> => {
|
|
1810
1349
|
if (killPromise) return killPromise;
|
|
1811
|
-
killPromise =
|
|
1812
|
-
|
|
1813
|
-
markClientTerminalProviderQuarantined(clientBridge, handle);
|
|
1814
|
-
const quarantineOutcome = await quarantineClientTerminalLease(
|
|
1815
|
-
clientBridge,
|
|
1816
|
-
handle,
|
|
1817
|
-
"terminal kill did not settle",
|
|
1818
|
-
);
|
|
1819
|
-
if (quarantineOutcome.status === "ok") clearClientTerminalProviderQuarantine(clientBridge, handle);
|
|
1820
|
-
logger.warn("ACP terminal kill failed", {
|
|
1821
|
-
terminalId: handle.terminalId,
|
|
1822
|
-
diagnostic: outcome.diagnostic,
|
|
1823
|
-
quarantineDiagnostic: quarantineOutcome.status === "ok" ? undefined : quarantineOutcome.diagnostic,
|
|
1824
|
-
});
|
|
1825
|
-
return quarantineOutcome.status === "ok"
|
|
1826
|
-
? outcome.diagnostic
|
|
1827
|
-
: `${outcome.diagnostic}; lease quarantine failed: ${quarantineOutcome.diagnostic}`;
|
|
1350
|
+
killPromise = handle.kill().catch((error: unknown) => {
|
|
1351
|
+
logger.warn("ACP terminal kill failed", { terminalId: handle.terminalId, error });
|
|
1828
1352
|
});
|
|
1829
1353
|
return killPromise;
|
|
1830
1354
|
};
|
|
@@ -1834,282 +1358,179 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1834
1358
|
};
|
|
1835
1359
|
signal?.addEventListener("abort", onAbortSignal, { once: true });
|
|
1836
1360
|
|
|
1837
|
-
let operationError: unknown;
|
|
1838
|
-
let operationResult: AgentToolResult<BashToolDetails> | undefined;
|
|
1839
|
-
let releaseFailure: ToolError | undefined;
|
|
1840
1361
|
try {
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
maxBashErrorBytes(this.session),
|
|
1855
|
-
),
|
|
1856
|
-
);
|
|
1362
|
+
try {
|
|
1363
|
+
if (signal?.aborted) {
|
|
1364
|
+
await fireKill();
|
|
1365
|
+
let current: ClientBridgeTerminalOutput = { output: "", truncated: false };
|
|
1366
|
+
let readDiagnostic: string | undefined;
|
|
1367
|
+
try {
|
|
1368
|
+
current = await handle.currentOutput();
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
readDiagnostic = boundArtifactSaveDiagnostic(error);
|
|
1371
|
+
logger.warn("ACP terminal aborted output read failed", {
|
|
1372
|
+
terminalId: handle.terminalId,
|
|
1373
|
+
error,
|
|
1374
|
+
});
|
|
1857
1375
|
}
|
|
1376
|
+
const prepared = await prepareClientTerminalOutput(this.session, current);
|
|
1377
|
+
throw new ToolAbortError(formatClientTerminalAbortFailure(prepared, readDiagnostic, pendingNotices));
|
|
1378
|
+
}
|
|
1858
1379
|
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
...pendingNotices,
|
|
1882
|
-
...(killDiagnostic ? [`Terminal kill failed: ${killDiagnostic}`] : []),
|
|
1883
|
-
],
|
|
1884
|
-
maxBashErrorBytes(this.session),
|
|
1885
|
-
),
|
|
1886
|
-
);
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
if (raced.kind === "timeout") {
|
|
1890
|
-
// Kill before reading final output so a slow `terminal/output`
|
|
1891
|
-
// RPC cannot let a timed-out command keep running past the
|
|
1892
|
-
// enforced timeout. The handle stays valid post-kill so the
|
|
1893
|
-
// buffered output is still readable.
|
|
1894
|
-
const killDiagnostic = await fireKill();
|
|
1895
|
-
const recovered = await recoverClientTerminalOutput(handle, "timeout", readCurrentOutput);
|
|
1896
|
-
const prepared = await prepareOutput(recovered.current, true);
|
|
1897
|
-
const timeoutNotices = [
|
|
1898
|
-
...pendingNotices,
|
|
1899
|
-
...(recovered.current.truncated || prepared.locallyTruncated ? ["(output truncated)"] : []),
|
|
1900
|
-
...(prepared.artifactSaveNotice ? [prepared.artifactSaveNotice] : []),
|
|
1901
|
-
...(killDiagnostic ? [`Terminal kill failed: ${killDiagnostic}`] : []),
|
|
1902
|
-
...(recovered.diagnostic
|
|
1903
|
-
? [`Terminal output recovery failed: ${recovered.diagnostic}`]
|
|
1904
|
-
: []),
|
|
1905
|
-
];
|
|
1906
|
-
const timedOutResult: BashInteractiveResult = {
|
|
1907
|
-
...prepared.summary,
|
|
1908
|
-
exitCode: undefined,
|
|
1909
|
-
cancelled: false,
|
|
1910
|
-
timedOut: true,
|
|
1911
|
-
};
|
|
1912
|
-
return this.#buildCompletedResult(timedOutResult, timeoutSec, {
|
|
1913
|
-
requestedTimeoutSec,
|
|
1914
|
-
notices: timeoutNotices,
|
|
1380
|
+
const timeoutPromise = Bun.sleep(timeoutMs).then(() => ({ kind: "timeout" as const }));
|
|
1381
|
+
// Poll until the process exits, times out, or the caller aborts.
|
|
1382
|
+
for (;;) {
|
|
1383
|
+
const racers: Array<Promise<BridgeRaceResult>> = [
|
|
1384
|
+
exitPromise.then(s => ({ kind: "exit" as const, status: s })),
|
|
1385
|
+
timeoutPromise,
|
|
1386
|
+
Bun.sleep(250).then(() => ({ kind: "poll" as const })),
|
|
1387
|
+
];
|
|
1388
|
+
if (signal) {
|
|
1389
|
+
racers.push(abortedP.then(() => ({ kind: "aborted" as const })));
|
|
1390
|
+
}
|
|
1391
|
+
const raced = await Promise.race(racers);
|
|
1392
|
+
|
|
1393
|
+
if (raced.kind === "aborted" || signal?.aborted) {
|
|
1394
|
+
await fireKill();
|
|
1395
|
+
let current: ClientBridgeTerminalOutput = { output: "", truncated: false };
|
|
1396
|
+
let readDiagnostic: string | undefined;
|
|
1397
|
+
try {
|
|
1398
|
+
current = await handle.currentOutput();
|
|
1399
|
+
} catch (error) {
|
|
1400
|
+
readDiagnostic = boundArtifactSaveDiagnostic(error);
|
|
1401
|
+
logger.warn("ACP terminal aborted output read failed", {
|
|
1915
1402
|
terminalId: handle.terminalId,
|
|
1403
|
+
error,
|
|
1916
1404
|
});
|
|
1917
1405
|
}
|
|
1406
|
+
const prepared = await prepareClientTerminalOutput(this.session, current);
|
|
1407
|
+
throw new ToolAbortError(
|
|
1408
|
+
formatClientTerminalAbortFailure(prepared, readDiagnostic, pendingNotices),
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1918
1411
|
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
failureCause,
|
|
1936
|
-
),
|
|
1937
|
-
);
|
|
1412
|
+
if (raced.kind === "timeout") {
|
|
1413
|
+
// Kill before reading final output so a slow `terminal/output`
|
|
1414
|
+
// RPC cannot let a timed-out command keep running past the
|
|
1415
|
+
// enforced timeout. The handle stays valid post-kill so the
|
|
1416
|
+
// buffered output is still readable.
|
|
1417
|
+
await fireKill();
|
|
1418
|
+
let current: ClientBridgeTerminalOutput = { output: "", truncated: false };
|
|
1419
|
+
let readDiagnostic: string | undefined;
|
|
1420
|
+
try {
|
|
1421
|
+
current = await handle.currentOutput();
|
|
1422
|
+
} catch (error) {
|
|
1423
|
+
readDiagnostic = boundArtifactSaveDiagnostic(error);
|
|
1424
|
+
logger.warn("ACP terminal final output read failed", {
|
|
1425
|
+
terminalId: handle.terminalId,
|
|
1426
|
+
error,
|
|
1427
|
+
});
|
|
1938
1428
|
}
|
|
1429
|
+
const prepared = await prepareClientTerminalOutput(this.session, current);
|
|
1430
|
+
const timeoutNotices = [
|
|
1431
|
+
...pendingNotices,
|
|
1432
|
+
...(current.truncated || prepared.locallyTruncated ? ["(output truncated)"] : []),
|
|
1433
|
+
...(prepared.artifactSaveNotice ? [prepared.artifactSaveNotice] : []),
|
|
1434
|
+
...(readDiagnostic ? [`Terminal output recovery failed: ${readDiagnostic}`] : []),
|
|
1435
|
+
];
|
|
1436
|
+
const timedOutResult: BashInteractiveResult = {
|
|
1437
|
+
...prepared.summary,
|
|
1438
|
+
exitCode: undefined,
|
|
1439
|
+
cancelled: false,
|
|
1440
|
+
timedOut: true,
|
|
1441
|
+
};
|
|
1442
|
+
return this.#buildCompletedResult(timedOutResult, timeoutSec, {
|
|
1443
|
+
requestedTimeoutSec,
|
|
1444
|
+
notices: timeoutNotices,
|
|
1445
|
+
terminalId: handle.terminalId,
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1939
1448
|
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1449
|
+
if (raced.kind === "exit") {
|
|
1450
|
+
exitStatus = raced.status;
|
|
1451
|
+
break;
|
|
1452
|
+
}
|
|
1944
1453
|
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
}
|
|
1957
|
-
const normalizedPoll = normalizeClientTerminalOutput(pollRecovered.current);
|
|
1958
|
-
const pollOutput = normalizedPoll.current;
|
|
1959
|
-
if (pollRecovered.diagnostic || normalizedPoll.malformed || pollOutput.truncated) {
|
|
1960
|
-
clientOutputCaptureIncomplete = true;
|
|
1961
|
-
}
|
|
1962
|
-
const { summary, locallyTruncated } = await boundClientTerminalOutput(
|
|
1963
|
-
pollOutput.output,
|
|
1964
|
-
pollOutput.truncated,
|
|
1965
|
-
this.session.settings,
|
|
1966
|
-
normalizedPoll.malformed,
|
|
1967
|
-
);
|
|
1968
|
-
const pollText =
|
|
1969
|
-
summary.truncated || locallyTruncated
|
|
1970
|
-
? `${summary.output}${summary.output.endsWith("\n") ? "" : "\n"}(output truncated)`
|
|
1971
|
-
: summary.output;
|
|
1972
|
-
onUpdate?.({
|
|
1973
|
-
content: [{ type: "text", text: pollText }],
|
|
1974
|
-
details: { terminalId: handle.terminalId },
|
|
1975
|
-
});
|
|
1454
|
+
// Poll tick: push current output so agent-loop transcript stays consistent.
|
|
1455
|
+
// Race the read against abort so a stuck `terminal/output` RPC does not
|
|
1456
|
+
// delay cancellation.
|
|
1457
|
+
const pollOutput = await Promise.race([
|
|
1458
|
+
handle.currentOutput(),
|
|
1459
|
+
abortedP.then(() => undefined as ClientBridgeTerminalOutput | undefined),
|
|
1460
|
+
]);
|
|
1461
|
+
if (pollOutput === undefined) {
|
|
1462
|
+
// Abort fired during the poll-tick read; let the next loop iteration
|
|
1463
|
+
// observe `signal?.aborted` and exit via the abort branch.
|
|
1464
|
+
continue;
|
|
1976
1465
|
}
|
|
1977
|
-
|
|
1978
|
-
|
|
1466
|
+
const { summary, locallyTruncated } = await boundClientTerminalOutput(
|
|
1467
|
+
pollOutput.output,
|
|
1468
|
+
pollOutput.truncated,
|
|
1469
|
+
this.session.settings,
|
|
1470
|
+
);
|
|
1471
|
+
const pollText =
|
|
1472
|
+
pollOutput.truncated || locallyTruncated
|
|
1473
|
+
? `${summary.output}${summary.output.endsWith("\n") ? "" : "\n"}(output truncated)`
|
|
1474
|
+
: summary.output;
|
|
1475
|
+
onUpdate?.({
|
|
1476
|
+
content: [{ type: "text", text: pollText }],
|
|
1477
|
+
details: { terminalId: handle.terminalId },
|
|
1478
|
+
});
|
|
1979
1479
|
}
|
|
1480
|
+
} finally {
|
|
1481
|
+
signal?.removeEventListener("abort", onAbortSignal);
|
|
1482
|
+
}
|
|
1980
1483
|
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
maxBashErrorBytes(this.session),
|
|
1994
|
-
),
|
|
1995
|
-
);
|
|
1484
|
+
if (signal?.aborted) {
|
|
1485
|
+
await fireKill();
|
|
1486
|
+
let current: ClientBridgeTerminalOutput = { output: "", truncated: false };
|
|
1487
|
+
let readDiagnostic: string | undefined;
|
|
1488
|
+
try {
|
|
1489
|
+
current = await handle.currentOutput();
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
readDiagnostic = boundArtifactSaveDiagnostic(error);
|
|
1492
|
+
logger.warn("ACP terminal aborted output read failed", {
|
|
1493
|
+
terminalId: handle.terminalId,
|
|
1494
|
+
error,
|
|
1495
|
+
});
|
|
1996
1496
|
}
|
|
1497
|
+
const prepared = await prepareClientTerminalOutput(this.session, current);
|
|
1498
|
+
throw new ToolAbortError(formatClientTerminalAbortFailure(prepared, readDiagnostic, pendingNotices));
|
|
1499
|
+
}
|
|
1997
1500
|
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
const finalOutput = finalRecovered.current;
|
|
1501
|
+
// Fetch final output; the terminal is released in the outer finally.
|
|
1502
|
+
const finalOutput = await handle.currentOutput();
|
|
2001
1503
|
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
1504
|
+
// Map exit status: null exitCode with a signal → treat as signal kill (137).
|
|
1505
|
+
const rawExitCode = exitStatus.exitCode;
|
|
1506
|
+
const exitCode: number | undefined =
|
|
1507
|
+
rawExitCode != null ? rawExitCode : exitStatus.signal ? 137 : undefined;
|
|
2006
1508
|
|
|
2007
|
-
|
|
2008
|
-
if (signal?.aborted) {
|
|
2009
|
-
const killDiagnostic = await fireKill();
|
|
2010
|
-
markClientTerminalSourceIncomplete(prepared);
|
|
2011
|
-
throw new ToolAbortError(
|
|
2012
|
-
formatClientTerminalAbortFailure(
|
|
2013
|
-
prepared,
|
|
2014
|
-
finalRecovered.diagnostic,
|
|
2015
|
-
[...pendingNotices, ...(killDiagnostic ? [`Terminal kill failed: ${killDiagnostic}`] : [])],
|
|
2016
|
-
maxBashErrorBytes(this.session),
|
|
2017
|
-
),
|
|
2018
|
-
);
|
|
2019
|
-
}
|
|
1509
|
+
const prepared = await prepareClientTerminalOutput(this.session, finalOutput);
|
|
2020
1510
|
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
const bridgeNotices: string[] = [];
|
|
2028
|
-
if (finalOutput.truncated || prepared.locallyTruncated) bridgeNotices.push("(output truncated)");
|
|
2029
|
-
for (const notice of pendingNotices) bridgeNotices.push(notice);
|
|
2030
|
-
if (prepared.artifactSaveNotice) bridgeNotices.push(prepared.artifactSaveNotice);
|
|
2031
|
-
if (finalRecovered.diagnostic) {
|
|
2032
|
-
bridgeNotices.push(`Terminal output recovery failed: ${finalRecovered.diagnostic}`);
|
|
2033
|
-
}
|
|
1511
|
+
const bridgeResult: BashResult = {
|
|
1512
|
+
...prepared.summary,
|
|
1513
|
+
exitCode,
|
|
1514
|
+
cancelled: false,
|
|
1515
|
+
};
|
|
2034
1516
|
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
1517
|
+
const bridgeNotices: string[] = [];
|
|
1518
|
+
if (finalOutput.truncated || prepared.locallyTruncated) bridgeNotices.push("(output truncated)");
|
|
1519
|
+
for (const notice of pendingNotices) bridgeNotices.push(notice);
|
|
1520
|
+
if (prepared.artifactSaveNotice) bridgeNotices.push(prepared.artifactSaveNotice);
|
|
1521
|
+
|
|
1522
|
+
return this.#buildCompletedResult(bridgeResult, timeoutSec, {
|
|
1523
|
+
requestedTimeoutSec,
|
|
1524
|
+
notices: bridgeNotices,
|
|
1525
|
+
terminalId: handle.terminalId,
|
|
1526
|
+
});
|
|
2043
1527
|
} finally {
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
}
|
|
2049
|
-
const releaseOutcome = await releaseHandle(handle);
|
|
2050
|
-
if (releaseOutcome.status !== "ok") {
|
|
2051
|
-
markClientTerminalProviderQuarantined(clientBridge, handle);
|
|
2052
|
-
const quarantineOutcome = await quarantineClientTerminalLease(
|
|
2053
|
-
clientBridge,
|
|
2054
|
-
handle,
|
|
2055
|
-
"terminal release did not settle",
|
|
2056
|
-
);
|
|
2057
|
-
if (quarantineOutcome.status === "ok") clearClientTerminalProviderQuarantine(clientBridge, handle);
|
|
2058
|
-
const ownershipDiagnostic =
|
|
2059
|
-
quarantineOutcome.status === "ok"
|
|
2060
|
-
? releaseOutcome.diagnostic
|
|
2061
|
-
: `${releaseOutcome.diagnostic}; lease quarantine failed: ${quarantineOutcome.diagnostic}`;
|
|
2062
|
-
logger.warn("ACP terminal release failed", {
|
|
2063
|
-
terminalId: handle.terminalId,
|
|
2064
|
-
diagnostic: releaseOutcome.diagnostic,
|
|
2065
|
-
quarantineDiagnostic: quarantineOutcome.status === "ok" ? undefined : quarantineOutcome.diagnostic,
|
|
2066
|
-
});
|
|
2067
|
-
if (latestPreparedOutput) {
|
|
2068
|
-
releaseFailure = new ToolError(
|
|
2069
|
-
formatClientTerminalOwnershipFailure(
|
|
2070
|
-
latestPreparedOutput,
|
|
2071
|
-
ownershipDiagnostic,
|
|
2072
|
-
maxBashErrorBytes(this.session),
|
|
2073
|
-
),
|
|
2074
|
-
);
|
|
2075
|
-
} else {
|
|
2076
|
-
releaseFailure = new ToolError(
|
|
2077
|
-
truncateHeadBytes(
|
|
2078
|
-
`Terminal release ownership incomplete: ${ownershipDiagnostic}`,
|
|
2079
|
-
maxBashErrorBytes(this.session),
|
|
2080
|
-
).text,
|
|
2081
|
-
);
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
if (operationError) {
|
|
2086
|
-
if (releaseFailure && operationError instanceof Error) {
|
|
2087
|
-
const operationCause = Array.from(
|
|
2088
|
-
operationError.message.matchAll(/Command (?:timed out(?: after \d+ seconds?)?|cancelled|aborted)/gu),
|
|
2089
|
-
).at(-1)?.[0];
|
|
2090
|
-
operationError.message = formatBashFailureMessage(
|
|
2091
|
-
{
|
|
2092
|
-
...(latestPreparedOutput?.summary ?? {
|
|
2093
|
-
totalLines: 0,
|
|
2094
|
-
totalBytes: 0,
|
|
2095
|
-
outputLines: 0,
|
|
2096
|
-
outputBytes: 0,
|
|
2097
|
-
}),
|
|
2098
|
-
output: operationError.message,
|
|
2099
|
-
truncated: true,
|
|
2100
|
-
exitCode: undefined,
|
|
2101
|
-
cancelled: true,
|
|
2102
|
-
},
|
|
2103
|
-
`${removeTrailingFailureCause(operationError.message, operationCause)}\n\n${releaseFailure.message}`,
|
|
2104
|
-
operationCause,
|
|
2105
|
-
maxBashErrorBytes(this.session),
|
|
2106
|
-
);
|
|
1528
|
+
try {
|
|
1529
|
+
await handle.release();
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
logger.warn("ACP terminal release failed", { terminalId: handle.terminalId, error });
|
|
2107
1532
|
}
|
|
2108
|
-
throw operationError;
|
|
2109
1533
|
}
|
|
2110
|
-
if (releaseFailure) throw releaseFailure;
|
|
2111
|
-
if (!operationResult) throw new ToolError("ACP terminal execution did not settle");
|
|
2112
|
-
return operationResult;
|
|
2113
1534
|
}
|
|
2114
1535
|
|
|
2115
1536
|
const spillThreshold = resolveBashOutputSinkTailBytes(this.session.settings);
|
|
@@ -2119,7 +1540,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2119
1540
|
const tailBuffer = new TailBuffer(spillThreshold);
|
|
2120
1541
|
|
|
2121
1542
|
// Allocate artifact for truncated output storage
|
|
2122
|
-
const { path: artifactPath, id: artifactId } = await
|
|
1543
|
+
const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
|
|
2123
1544
|
const artifactPublisher = createBashArtifactPublisher(this.session);
|
|
2124
1545
|
|
|
2125
1546
|
const interactiveUi =
|