@gajae-code/coding-agent 0.12.4 → 0.12.6
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 +36 -0
- package/README.md +4 -1
- package/dist/types/async/job-manager.d.ts +9 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +10 -8
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
- package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -2
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/sdk/host/host.d.ts +23 -2
- package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/artifacts.d.ts +8 -5
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/client-bridge.d.ts +14 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -23
- package/dist/types/session/messages.d.ts +4 -1
- package/dist/types/session/session-manager.d.ts +7 -5
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/session/streaming-output.d.ts +32 -0
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/task/index.d.ts +2 -1
- package/dist/types/task/receipt.d.ts +3 -0
- package/dist/types/task/types.d.ts +194 -0
- package/dist/types/task/worktree.d.ts +13 -6
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +3 -0
- package/dist/types/tools/output-meta.d.ts +37 -5
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/tool-result.d.ts +1 -0
- package/dist/types/utils/artifact-id.d.ts +2 -0
- package/dist/types/utils/git.d.ts +1 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/async/job-manager.ts +13 -5
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/cursor.ts +20 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +425 -151
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
- package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/internal-urls/agent-protocol.ts +98 -38
- package/src/internal-urls/artifact-protocol.ts +28 -17
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/internal-urls/registry-helpers.ts +62 -0
- package/src/modes/acp/acp-agent.ts +51 -13
- package/src/modes/acp/acp-client-bridge.ts +154 -11
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/acp/adapter.ts +200 -9
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +692 -98
- package/src/sdk/bus/chat-daemon-control.ts +33 -5
- package/src/sdk/bus/index.ts +347 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -3
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/host/host.ts +187 -9
- package/src/sdk/host/reverse-leases.ts +249 -42
- package/src/sdk/session.ts +8 -6
- package/src/session/agent-session.ts +930 -354
- package/src/session/artifacts.ts +211 -74
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/client-bridge.ts +15 -0
- package/src/session/internal/managed-session-scope.ts +332 -198
- package/src/session/internal/managed-session-storage.ts +162 -450
- package/src/session/messages.ts +32 -7
- package/src/session/session-manager.ts +31 -28
- package/src/session/session-storage.ts +190 -53
- package/src/session/streaming-output.ts +198 -40
- package/src/task/executor.ts +5 -2
- package/src/task/index.ts +277 -64
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +26 -2
- package/src/task/render.ts +42 -1
- package/src/task/types.ts +52 -0
- package/src/task/worktree.ts +122 -43
- package/src/tools/bash.ts +818 -239
- package/src/tools/eval.ts +99 -4
- package/src/tools/fetch.ts +77 -15
- package/src/tools/find.ts +7 -3
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +706 -116
- package/src/tools/search.ts +9 -4
- package/src/tools/subagent-render.ts +5 -2
- package/src/tools/subagent.ts +8 -0
- package/src/tools/tool-result.ts +1 -0
- package/src/utils/artifact-id.ts +6 -0
- package/src/utils/git.ts +2 -0
|
@@ -4,18 +4,28 @@
|
|
|
4
4
|
* Uses brush-core via native bindings for shell execution.
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from "node:fs/promises";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
executeShell,
|
|
9
|
+
type MinimizerOptions,
|
|
10
|
+
type ShellRunResult as NativeShellRunResult,
|
|
11
|
+
Shell,
|
|
12
|
+
} from "@gajae-code/natives";
|
|
8
13
|
import { postmortem } from "@gajae-code/utils";
|
|
9
14
|
import { Settings, type ShellMinimizerSettings } from "../config/settings";
|
|
10
15
|
import { formatCrashDiagnosticNotice, writeCrashReport } from "../debug/crash-diagnostics";
|
|
11
16
|
import {
|
|
12
|
-
DEFAULT_ARTIFACT_MAX_BYTES,
|
|
13
17
|
DEFAULT_MAX_BYTES,
|
|
14
18
|
OutputSink,
|
|
19
|
+
type OutputSummary,
|
|
15
20
|
type TerminalArtifactPublisher,
|
|
16
21
|
truncateHeadBytes,
|
|
17
22
|
} from "../session/streaming-output";
|
|
18
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
formatArtifactEvidenceNotice,
|
|
25
|
+
isValidArtifactId,
|
|
26
|
+
resolveOutputMaxColumns,
|
|
27
|
+
resolveOutputSinkHeadBytes,
|
|
28
|
+
} from "../tools/output-meta";
|
|
19
29
|
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
|
|
20
30
|
import { NON_INTERACTIVE_ENV } from "./non-interactive-env";
|
|
21
31
|
|
|
@@ -25,7 +35,19 @@ export interface BashArtifactSaveSummary {
|
|
|
25
35
|
omittedBytes?: number;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
|
-
export type BashMinimizedSaveReturn = BashArtifactSaveResult | BashArtifactSaveSummary |
|
|
38
|
+
export type BashMinimizedSaveReturn = BashArtifactSaveResult | BashArtifactSaveSummary | undefined;
|
|
39
|
+
|
|
40
|
+
interface ValidatedNativeMinimized {
|
|
41
|
+
filter: string;
|
|
42
|
+
text: string;
|
|
43
|
+
originalText: string;
|
|
44
|
+
inputBytes: number;
|
|
45
|
+
outputBytes: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const MINIMIZED_SAVE_WAIT_MS = 500;
|
|
49
|
+
const MINIMIZED_SAVE_PENDING_LIMIT = 64;
|
|
50
|
+
const pendingMinimizedSaves = new Set<Promise<unknown>>();
|
|
29
51
|
|
|
30
52
|
export type BashArtifactSaveResult =
|
|
31
53
|
| { status: "saved"; artifactId: string; complete: true; omittedBytes?: undefined }
|
|
@@ -33,18 +55,16 @@ export type BashArtifactSaveResult =
|
|
|
33
55
|
| { status: "unavailable" }
|
|
34
56
|
| { status: "failed"; diagnostic: string };
|
|
35
57
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
omittedBytes: inputBytes - retainedBytes,
|
|
47
|
-
};
|
|
58
|
+
function isValidOmittedBytes(omittedBytes: number): boolean {
|
|
59
|
+
return Number.isSafeInteger(omittedBytes) && omittedBytes >= 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
63
|
+
return typeof value === "object" && value !== null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function invalidArtifactSaveShape(): BashArtifactSaveResult {
|
|
67
|
+
return { status: "failed", diagnostic: "artifact save returned an invalid result" };
|
|
48
68
|
}
|
|
49
69
|
|
|
50
70
|
function normalizeExplicitSavedArtifact(
|
|
@@ -52,6 +72,12 @@ function normalizeExplicitSavedArtifact(
|
|
|
52
72
|
complete: boolean,
|
|
53
73
|
omittedBytes: number | undefined,
|
|
54
74
|
): BashArtifactSaveResult {
|
|
75
|
+
if (!isValidArtifactId(artifactId)) {
|
|
76
|
+
return { status: "failed", diagnostic: "artifact save reported an invalid artifact id" };
|
|
77
|
+
}
|
|
78
|
+
if (omittedBytes !== undefined && !isValidOmittedBytes(omittedBytes)) {
|
|
79
|
+
return { status: "failed", diagnostic: "artifact save reported invalid omitted bytes" };
|
|
80
|
+
}
|
|
55
81
|
if (complete) {
|
|
56
82
|
return (omittedBytes ?? 0) > 0
|
|
57
83
|
? { status: "failed", diagnostic: "artifact save reported complete output with omitted bytes" }
|
|
@@ -62,35 +88,143 @@ function normalizeExplicitSavedArtifact(
|
|
|
62
88
|
: { status: "failed", diagnostic: "artifact save reported incomplete output without omitted bytes" };
|
|
63
89
|
}
|
|
64
90
|
|
|
65
|
-
function normalizeMinimizedSaveResult(value:
|
|
66
|
-
if (
|
|
67
|
-
if (
|
|
68
|
-
if (!(
|
|
69
|
-
|
|
91
|
+
function normalizeMinimizedSaveResult(value: unknown): BashArtifactSaveResult {
|
|
92
|
+
if (value === undefined) return { status: "unavailable" };
|
|
93
|
+
if (typeof value === "string") return invalidArtifactSaveShape();
|
|
94
|
+
if (!isRecord(value)) return invalidArtifactSaveShape();
|
|
95
|
+
|
|
96
|
+
const status = value.status;
|
|
97
|
+
if (status === "unavailable") return { status: "unavailable" };
|
|
98
|
+
if (status === "failed") {
|
|
99
|
+
const diagnostic = value.diagnostic;
|
|
100
|
+
return typeof diagnostic === "string" && diagnostic.trim().length > 0
|
|
101
|
+
? { status: "failed", diagnostic: diagnostic.slice(0, 512) }
|
|
102
|
+
: invalidArtifactSaveShape();
|
|
103
|
+
}
|
|
104
|
+
if (status !== undefined && status !== "saved") return invalidArtifactSaveShape();
|
|
105
|
+
if (typeof value.artifactId !== "string" || typeof value.complete !== "boolean") {
|
|
106
|
+
return invalidArtifactSaveShape();
|
|
107
|
+
}
|
|
108
|
+
if (value.omittedBytes !== undefined && typeof value.omittedBytes !== "number") {
|
|
109
|
+
return invalidArtifactSaveShape();
|
|
70
110
|
}
|
|
71
|
-
if (value.status !== "saved") return value;
|
|
72
111
|
return normalizeExplicitSavedArtifact(value.artifactId, value.complete, value.omittedBytes);
|
|
73
112
|
}
|
|
74
113
|
|
|
75
|
-
export function normalizeMinimizedSaveResultForTests(
|
|
76
|
-
value
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
114
|
+
export function normalizeMinimizedSaveResultForTests(value: unknown, _originalText?: string): BashArtifactSaveResult {
|
|
115
|
+
return normalizeMinimizedSaveResult(value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeNativeMinimized(value: unknown): { minimized?: ValidatedNativeMinimized; malformed: boolean } {
|
|
119
|
+
if (value === undefined) return { malformed: false };
|
|
120
|
+
if (!isRecord(value)) return { malformed: true };
|
|
121
|
+
const { filter, text, originalText, inputBytes, outputBytes } = value;
|
|
122
|
+
if (
|
|
123
|
+
typeof filter !== "string" ||
|
|
124
|
+
typeof text !== "string" ||
|
|
125
|
+
typeof originalText !== "string" ||
|
|
126
|
+
!Number.isSafeInteger(inputBytes) ||
|
|
127
|
+
!Number.isSafeInteger(outputBytes) ||
|
|
128
|
+
(inputBytes as number) < 0 ||
|
|
129
|
+
(outputBytes as number) < 0 ||
|
|
130
|
+
Buffer.byteLength(originalText, "utf-8") !== inputBytes ||
|
|
131
|
+
Buffer.byteLength(text, "utf-8") !== outputBytes
|
|
132
|
+
) {
|
|
133
|
+
return { malformed: true };
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
minimized: { filter, text, originalText, inputBytes: inputBytes as number, outputBytes: outputBytes as number },
|
|
137
|
+
malformed: false,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function nativeCaptureStatusIncomplete(result: NativeShellRunResult | undefined): boolean {
|
|
142
|
+
if (!result) return true;
|
|
143
|
+
const capture = result.outputCaptureIncomplete as unknown;
|
|
144
|
+
const saturated = result.outputLossCountSaturated as unknown;
|
|
145
|
+
return (
|
|
146
|
+
(capture !== undefined && typeof capture !== "boolean") ||
|
|
147
|
+
(saturated !== undefined && typeof saturated !== "boolean") ||
|
|
148
|
+
capture === true ||
|
|
149
|
+
saturated === true
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function saveMinimizedArtifactBounded(
|
|
154
|
+
operation: () => Promise<BashMinimizedSaveReturn>,
|
|
155
|
+
): Promise<BashArtifactSaveResult> {
|
|
156
|
+
if (pendingMinimizedSaves.size >= MINIMIZED_SAVE_PENDING_LIMIT) {
|
|
157
|
+
return { status: "failed", diagnostic: "pending minimized artifact save limit reached" };
|
|
158
|
+
}
|
|
159
|
+
const promise = Promise.resolve().then(operation);
|
|
160
|
+
pendingMinimizedSaves.add(promise);
|
|
161
|
+
void promise.then(
|
|
162
|
+
() => pendingMinimizedSaves.delete(promise),
|
|
163
|
+
() => pendingMinimizedSaves.delete(promise),
|
|
164
|
+
);
|
|
165
|
+
const outcome = await Promise.race([
|
|
166
|
+
promise.then(
|
|
167
|
+
value => ({ status: "ok" as const, value }),
|
|
168
|
+
error => ({ status: "failed" as const, diagnostic: error instanceof Error ? error.message : String(error) }),
|
|
169
|
+
),
|
|
170
|
+
Bun.sleep(MINIMIZED_SAVE_WAIT_MS).then(() => ({
|
|
171
|
+
status: "failed" as const,
|
|
172
|
+
diagnostic: `did not settle within ${MINIMIZED_SAVE_WAIT_MS}ms`,
|
|
173
|
+
})),
|
|
174
|
+
]);
|
|
175
|
+
if (outcome.status === "failed") {
|
|
176
|
+
return {
|
|
177
|
+
status: "failed",
|
|
178
|
+
diagnostic: truncateHeadBytes(outcome.diagnostic.replace(/\s+/gu, " ").trim(), 512).text,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return normalizeMinimizedSaveResult(outcome.value);
|
|
80
182
|
}
|
|
81
183
|
|
|
82
184
|
function completeRawArtifactAvailable(summary: {
|
|
83
185
|
artifactId?: string;
|
|
84
186
|
artifactTruncatedBytes?: number;
|
|
187
|
+
sourceTruncatedBytes?: number;
|
|
188
|
+
sourceCaptureIncomplete?: boolean;
|
|
85
189
|
artifactFailureDiagnostic?: string;
|
|
86
190
|
}): boolean {
|
|
87
191
|
return (
|
|
88
192
|
summary.artifactId !== undefined &&
|
|
89
193
|
(summary.artifactTruncatedBytes ?? 0) <= 0 &&
|
|
194
|
+
(summary.sourceTruncatedBytes ?? 0) <= 0 &&
|
|
195
|
+
!summary.sourceCaptureIncomplete &&
|
|
90
196
|
summary.artifactFailureDiagnostic === undefined
|
|
91
197
|
);
|
|
92
198
|
}
|
|
93
199
|
|
|
200
|
+
function applyShellCallbackLoss(
|
|
201
|
+
summary: OutputSummary,
|
|
202
|
+
droppedOutputBytes: number | undefined,
|
|
203
|
+
droppedOutputChunks: number | undefined,
|
|
204
|
+
sourceCaptureIncomplete = false,
|
|
205
|
+
): OutputSummary {
|
|
206
|
+
const bytesValid =
|
|
207
|
+
droppedOutputBytes === undefined || (Number.isSafeInteger(droppedOutputBytes) && droppedOutputBytes >= 0);
|
|
208
|
+
const chunksValid =
|
|
209
|
+
droppedOutputChunks === undefined || (Number.isSafeInteger(droppedOutputChunks) && droppedOutputChunks >= 0);
|
|
210
|
+
const counterPairValid = (droppedOutputBytes === undefined) === (droppedOutputChunks === undefined);
|
|
211
|
+
const knownDroppedBytes = bytesValid && droppedOutputBytes !== undefined && droppedOutputBytes > 0;
|
|
212
|
+
const knownDroppedChunks = chunksValid && droppedOutputChunks !== undefined && droppedOutputChunks > 0;
|
|
213
|
+
const captureIncomplete =
|
|
214
|
+
sourceCaptureIncomplete ||
|
|
215
|
+
!bytesValid ||
|
|
216
|
+
!chunksValid ||
|
|
217
|
+
!counterPairValid ||
|
|
218
|
+
knownDroppedBytes !== knownDroppedChunks;
|
|
219
|
+
if (!knownDroppedBytes && !captureIncomplete) return summary;
|
|
220
|
+
return {
|
|
221
|
+
...summary,
|
|
222
|
+
truncated: true,
|
|
223
|
+
...(knownDroppedBytes ? { sourceTruncatedBytes: droppedOutputBytes } : {}),
|
|
224
|
+
...(captureIncomplete ? { sourceCaptureIncomplete: true } : {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
94
228
|
function appendModelNotice(output: string, notice: string): string {
|
|
95
229
|
const separator = output.length > 0 && !output.endsWith("\n") ? "\n" : "";
|
|
96
230
|
return `${output}${separator}${notice}\n`;
|
|
@@ -98,7 +232,13 @@ function appendModelNotice(output: string, notice: string): string {
|
|
|
98
232
|
|
|
99
233
|
function minimizedSaveNotice(
|
|
100
234
|
result: BashArtifactSaveResult,
|
|
101
|
-
summary: {
|
|
235
|
+
summary: {
|
|
236
|
+
artifactId?: string;
|
|
237
|
+
artifactTruncatedBytes?: number;
|
|
238
|
+
sourceTruncatedBytes?: number;
|
|
239
|
+
sourceCaptureIncomplete?: boolean;
|
|
240
|
+
artifactFailureDiagnostic?: string;
|
|
241
|
+
},
|
|
102
242
|
): string | undefined {
|
|
103
243
|
if (result.status === "failed") return `Bash output artifact save failed: ${result.diagnostic}`;
|
|
104
244
|
if (result.status === "unavailable" && !completeRawArtifactAvailable(summary)) {
|
|
@@ -107,13 +247,6 @@ function minimizedSaveNotice(
|
|
|
107
247
|
return undefined;
|
|
108
248
|
}
|
|
109
249
|
|
|
110
|
-
function minimizedArtifactFooter(result: Extract<BashArtifactSaveResult, { status: "saved" }>): string {
|
|
111
|
-
const reference = result.complete
|
|
112
|
-
? `artifact://${result.artifactId}`
|
|
113
|
-
: formatArtifactReference(result.artifactId, result.omittedBytes);
|
|
114
|
-
return `[raw output: ${reference}]`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
250
|
export interface BashExecutorOptions {
|
|
118
251
|
/**
|
|
119
252
|
* Invoked when the native minimizer rewrote the command's output, giving
|
|
@@ -170,40 +303,122 @@ export interface BashResult {
|
|
|
170
303
|
outputBytes: number;
|
|
171
304
|
artifactId?: string;
|
|
172
305
|
artifactTruncatedBytes?: number;
|
|
306
|
+
/** Bytes dropped before the Bash executor received the native output stream. */
|
|
307
|
+
sourceTruncatedBytes?: number;
|
|
308
|
+
/** Exact source capture completeness could not be proven. */
|
|
309
|
+
sourceCaptureIncomplete?: boolean;
|
|
173
310
|
artifactFailureDiagnostic?: string;
|
|
174
311
|
}
|
|
175
312
|
|
|
176
313
|
const shellSessions = new Map<string, Shell>();
|
|
177
|
-
const brokenShellSessions = new
|
|
314
|
+
const brokenShellSessions = new Map<string, Shell>();
|
|
178
315
|
const retiringShellSessions = new Set<Shell>();
|
|
179
|
-
|
|
180
|
-
|
|
316
|
+
const shellInFlightRuns = new Map<Shell, number>();
|
|
317
|
+
const shellSessionKeys = new Map<Shell, string>();
|
|
318
|
+
const MAX_OWNED_SHELL_SESSIONS = 64;
|
|
319
|
+
let shellSessionAdmissionTail = Promise.resolve();
|
|
320
|
+
|
|
321
|
+
async function withShellSessionAdmission<T>(operation: () => Promise<T>): Promise<T> {
|
|
322
|
+
const previous = shellSessionAdmissionTail;
|
|
323
|
+
const release = Promise.withResolvers<void>();
|
|
324
|
+
shellSessionAdmissionTail = previous.then(() => release.promise);
|
|
325
|
+
await previous;
|
|
326
|
+
try {
|
|
327
|
+
return await operation();
|
|
328
|
+
} finally {
|
|
329
|
+
release.resolve();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function touchShellSession(sessionKey: string, shellSession: Shell): void {
|
|
334
|
+
if (shellSessions.get(sessionKey) !== shellSession) return;
|
|
335
|
+
shellSessions.delete(sessionKey);
|
|
336
|
+
shellSessions.set(sessionKey, shellSession);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function evictLeastRecentlyUsedIdleShell(): Promise<boolean> {
|
|
340
|
+
for (const [sessionKey, shellSession] of shellSessions) {
|
|
341
|
+
if ((shellInFlightRuns.get(shellSession) ?? 0) > 0) continue;
|
|
342
|
+
quarantineShellSession(sessionKey, shellSession);
|
|
343
|
+
const abortPromise = Promise.resolve()
|
|
344
|
+
.then(() => shellSession.abort())
|
|
345
|
+
.catch(() => undefined);
|
|
346
|
+
void abortPromise.finally(() => releaseQuarantinedShellIfIdle(sessionKey, shellSession));
|
|
347
|
+
return Promise.race([abortPromise.then(() => true), Bun.sleep(CANCEL_CLEANUP_WAIT_MS).then(() => false)]);
|
|
348
|
+
}
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function beginShellRun(shellSession: Shell): void {
|
|
353
|
+
shellInFlightRuns.set(shellSession, (shellInFlightRuns.get(shellSession) ?? 0) + 1);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function releaseQuarantinedShellIfIdle(sessionKey: string, shellSession: Shell): void {
|
|
357
|
+
if ((shellInFlightRuns.get(shellSession) ?? 0) > 0) return;
|
|
358
|
+
clearBrokenShellSession(sessionKey, shellSession);
|
|
359
|
+
retiringShellSessions.delete(shellSession);
|
|
360
|
+
shellSessionKeys.delete(shellSession);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function endShellRun(sessionKey: string, shellSession: Shell): void {
|
|
364
|
+
const remaining = Math.max(0, (shellInFlightRuns.get(shellSession) ?? 1) - 1);
|
|
365
|
+
if (remaining > 0) {
|
|
366
|
+
shellInFlightRuns.set(shellSession, remaining);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const quarantined = brokenShellSessions.get(sessionKey) === shellSession;
|
|
370
|
+
shellInFlightRuns.delete(shellSession);
|
|
371
|
+
if (quarantined) releaseQuarantinedShellIfIdle(sessionKey, shellSession);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function quarantineShellSession(sessionKey: string, shellSession: Shell): void {
|
|
375
|
+
retiringShellSessions.add(shellSession);
|
|
376
|
+
brokenShellSessions.set(sessionKey, shellSession);
|
|
377
|
+
shellSessionKeys.set(shellSession, sessionKey);
|
|
378
|
+
if (shellSessions.get(sessionKey) === shellSession) shellSessions.delete(sessionKey);
|
|
379
|
+
}
|
|
380
|
+
// Give ordinary native cancellation time to settle without turning a stalled
|
|
381
|
+
// cleanup into a multi-second JavaScript tool delay.
|
|
181
382
|
const CANCEL_CLEANUP_WAIT_MS = 400;
|
|
383
|
+
function clearBrokenShellSession(sessionKey: string, shellSession: Shell): void {
|
|
384
|
+
if (brokenShellSessions.get(sessionKey) === shellSession) brokenShellSessions.delete(sessionKey);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
interface AbortCleanupOutcome {
|
|
388
|
+
settled: boolean;
|
|
389
|
+
result?: NativeShellRunResult;
|
|
390
|
+
}
|
|
182
391
|
|
|
183
|
-
/** Number of persistent shell sessions currently retained (owner gauge). */
|
|
392
|
+
/** Number of persistent and retiring shell sessions currently retained (owner gauge). */
|
|
184
393
|
export function getShellSessionCount(): number {
|
|
185
|
-
return shellSessions.size;
|
|
394
|
+
return new Set([...shellSessions.values(), ...retiringShellSessions, ...shellSessionKeys.keys()]).size;
|
|
186
395
|
}
|
|
187
396
|
|
|
188
397
|
/**
|
|
189
|
-
* Dispose all persistent shell sessions
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* is registered as a postmortem cleanup so shutdown/signals release native
|
|
193
|
-
* shell resources, and is also callable directly (e.g. on owner teardown).
|
|
398
|
+
* Dispose all persistent shell sessions without waiting forever on a native
|
|
399
|
+
* abort that failed to settle. Quarantine entries are owner-scoped so a late
|
|
400
|
+
* finalizer cannot erase a successor shell's quarantine.
|
|
194
401
|
*/
|
|
195
402
|
export async function disposeAllShellSessions(): Promise<void> {
|
|
196
|
-
// Snapshot and drop strong references up front so concurrent callers cannot
|
|
197
|
-
// reuse a session that is being torn down, then await every native abort so
|
|
198
|
-
// shutdown/signal cleanup does not return before resources are released.
|
|
199
|
-
// Include retiring shells whose JS call returned after bounded abort cleanup
|
|
200
|
-
// while the native run is still unwinding; they are no longer reusable but
|
|
201
|
-
// remain owned until their run promise settles.
|
|
202
403
|
const sessions = new Set([...shellSessions.values(), ...retiringShellSessions]);
|
|
404
|
+
for (const session of sessions) {
|
|
405
|
+
const sessionKey = shellSessionKeys.get(session);
|
|
406
|
+
if (sessionKey) quarantineShellSession(sessionKey, session);
|
|
407
|
+
else retiringShellSessions.add(session);
|
|
408
|
+
}
|
|
203
409
|
shellSessions.clear();
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
410
|
+
await Promise.all(
|
|
411
|
+
[...sessions].map(async session => {
|
|
412
|
+
const sessionKey = shellSessionKeys.get(session);
|
|
413
|
+
const abortPromise = Promise.resolve()
|
|
414
|
+
.then(() => session.abort())
|
|
415
|
+
.catch(() => undefined);
|
|
416
|
+
if (sessionKey) {
|
|
417
|
+
void abortPromise.finally(() => releaseQuarantinedShellIfIdle(sessionKey, session));
|
|
418
|
+
}
|
|
419
|
+
await Promise.race([abortPromise, Bun.sleep(CANCEL_CLEANUP_WAIT_MS)]);
|
|
420
|
+
}),
|
|
421
|
+
);
|
|
207
422
|
}
|
|
208
423
|
|
|
209
424
|
postmortem.register("bash-executor:shell-sessions", () => disposeAllShellSessions());
|
|
@@ -233,6 +448,9 @@ export function buildMinimizerOptions(group: ShellMinimizerSettings): MinimizerO
|
|
|
233
448
|
}
|
|
234
449
|
|
|
235
450
|
export async function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
|
|
451
|
+
if (options?.artifactId !== undefined && !isValidArtifactId(options.artifactId)) {
|
|
452
|
+
throw new Error("Invalid Bash artifact id");
|
|
453
|
+
}
|
|
236
454
|
const settings = await Settings.init();
|
|
237
455
|
const { shell, env: shellEnv, prefix } = settings.getShellConfig();
|
|
238
456
|
const configuredPrefix = options?.ignoreShellPrefix ? undefined : prefix;
|
|
@@ -279,51 +497,71 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
279
497
|
};
|
|
280
498
|
}
|
|
281
499
|
|
|
282
|
-
const usePersistentShell = options?.oneShot !== true;
|
|
283
|
-
const sessionKey = buildSessionKey(shell, configuredPrefix, snapshotPath, shellEnv, options?.sessionKey, minimizer);
|
|
284
|
-
const persistentSessionBroken = usePersistentShell && brokenShellSessions.has(sessionKey);
|
|
285
|
-
|
|
286
|
-
let shellSession = persistentSessionBroken || !usePersistentShell ? undefined : shellSessions.get(sessionKey);
|
|
287
|
-
if (!shellSession && !persistentSessionBroken && usePersistentShell) {
|
|
288
|
-
shellSession = new Shell({
|
|
289
|
-
sessionEnv: shellEnv,
|
|
290
|
-
snapshotPath: snapshotPath ?? undefined,
|
|
291
|
-
minimizer,
|
|
292
|
-
});
|
|
293
|
-
shellSessions.set(sessionKey, shellSession);
|
|
294
|
-
}
|
|
295
500
|
const userSignal = options?.signal;
|
|
296
501
|
const runAbortController = new AbortController();
|
|
297
502
|
const abortCurrentExecution = () => {
|
|
298
|
-
if (!runAbortController.signal.aborted)
|
|
299
|
-
runAbortController.abort();
|
|
300
|
-
}
|
|
301
|
-
if (shellSession && !abortPromise) {
|
|
302
|
-
abortPromise = shellSession.abort();
|
|
303
|
-
}
|
|
503
|
+
if (!runAbortController.signal.aborted) runAbortController.abort();
|
|
304
504
|
};
|
|
305
505
|
const abortDeferred = Promise.withResolvers<"abort">();
|
|
306
|
-
let abortPromise: Promise<unknown> | undefined;
|
|
307
506
|
const abortHandler = () => {
|
|
308
507
|
abortCurrentExecution();
|
|
309
508
|
abortDeferred.resolve("abort");
|
|
310
509
|
};
|
|
311
|
-
|
|
312
|
-
|
|
510
|
+
userSignal?.addEventListener("abort", abortHandler, { once: true });
|
|
511
|
+
if (userSignal?.aborted) abortHandler();
|
|
512
|
+
|
|
513
|
+
const usePersistentShell = options?.oneShot !== true;
|
|
514
|
+
const sessionKey = buildSessionKey(shell, configuredPrefix, snapshotPath, shellEnv, options?.sessionKey, minimizer);
|
|
515
|
+
let shellSession: Shell | undefined;
|
|
516
|
+
try {
|
|
517
|
+
shellSession = usePersistentShell
|
|
518
|
+
? await withShellSessionAdmission(async () => {
|
|
519
|
+
if (brokenShellSessions.has(sessionKey)) return undefined;
|
|
520
|
+
const existing = shellSessions.get(sessionKey);
|
|
521
|
+
if (existing) {
|
|
522
|
+
touchShellSession(sessionKey, existing);
|
|
523
|
+
beginShellRun(existing);
|
|
524
|
+
return existing;
|
|
525
|
+
}
|
|
526
|
+
if (getShellSessionCount() >= MAX_OWNED_SHELL_SESSIONS) {
|
|
527
|
+
const evicted = await evictLeastRecentlyUsedIdleShell();
|
|
528
|
+
if (!evicted || getShellSessionCount() >= MAX_OWNED_SHELL_SESSIONS) {
|
|
529
|
+
throw new Error(`Bash shell session limit reached (${MAX_OWNED_SHELL_SESSIONS})`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const created = new Shell({
|
|
533
|
+
sessionEnv: shellEnv,
|
|
534
|
+
snapshotPath: snapshotPath ?? undefined,
|
|
535
|
+
minimizer,
|
|
536
|
+
});
|
|
537
|
+
shellSessions.set(sessionKey, created);
|
|
538
|
+
shellSessionKeys.set(created, sessionKey);
|
|
539
|
+
beginShellRun(created);
|
|
540
|
+
return created;
|
|
541
|
+
})
|
|
542
|
+
: undefined;
|
|
543
|
+
} catch (error) {
|
|
544
|
+
userSignal?.removeEventListener("abort", abortHandler);
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
if (userSignal?.aborted) {
|
|
548
|
+
if (shellSession) endShellRun(sessionKey, shellSession);
|
|
549
|
+
userSignal.removeEventListener("abort", abortHandler);
|
|
550
|
+
return {
|
|
551
|
+
exitCode: undefined,
|
|
552
|
+
cancelled: true,
|
|
553
|
+
...(await sink.dump("Command cancelled")),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const awaitAbortCleanup = (runPromise: Promise<NativeShellRunResult>): Promise<AbortCleanupOutcome> =>
|
|
558
|
+
Promise.race([
|
|
313
559
|
runPromise.then(
|
|
314
|
-
|
|
315
|
-
() => true,
|
|
560
|
+
result => ({ settled: true, result }) satisfies AbortCleanupOutcome,
|
|
561
|
+
() => ({ settled: true }) satisfies AbortCleanupOutcome,
|
|
316
562
|
),
|
|
317
|
-
Bun.sleep(CANCEL_CLEANUP_WAIT_MS).then(() => false),
|
|
563
|
+
Bun.sleep(CANCEL_CLEANUP_WAIT_MS).then(() => ({ settled: false }) satisfies AbortCleanupOutcome),
|
|
318
564
|
]);
|
|
319
|
-
if (abortPromise) {
|
|
320
|
-
await Promise.race([abortPromise.catch(() => undefined), Bun.sleep(CANCEL_CLEANUP_WAIT_MS)]);
|
|
321
|
-
}
|
|
322
|
-
return settled;
|
|
323
|
-
};
|
|
324
|
-
if (userSignal) {
|
|
325
|
-
userSignal.addEventListener("abort", abortHandler, { once: true });
|
|
326
|
-
}
|
|
327
565
|
|
|
328
566
|
let timeoutTimer: NodeJS.Timeout | undefined;
|
|
329
567
|
const timeoutDeferred = Promise.withResolvers<"timeout">();
|
|
@@ -337,11 +575,12 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
337
575
|
}
|
|
338
576
|
|
|
339
577
|
let resetSession = false;
|
|
340
|
-
let runSettled = false;
|
|
341
578
|
|
|
342
579
|
try {
|
|
343
|
-
|
|
344
|
-
|
|
580
|
+
let runPromise: Promise<NativeShellRunResult>;
|
|
581
|
+
if (shellSession) {
|
|
582
|
+
try {
|
|
583
|
+
runPromise = shellSession.run(
|
|
345
584
|
{
|
|
346
585
|
command: finalCommand,
|
|
347
586
|
cwd: commandCwd,
|
|
@@ -350,28 +589,31 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
350
589
|
signal: runAbortController.signal,
|
|
351
590
|
},
|
|
352
591
|
(err, chunk) => {
|
|
353
|
-
if (!err)
|
|
354
|
-
enqueueChunk(chunk);
|
|
355
|
-
}
|
|
356
|
-
},
|
|
357
|
-
)
|
|
358
|
-
: executeShell(
|
|
359
|
-
{
|
|
360
|
-
command: finalCommand,
|
|
361
|
-
cwd: commandCwd,
|
|
362
|
-
env: commandEnv,
|
|
363
|
-
sessionEnv: shellEnv,
|
|
364
|
-
snapshotPath: snapshotPath ?? undefined,
|
|
365
|
-
minimizer,
|
|
366
|
-
timeoutMs: executionTimeoutMs,
|
|
367
|
-
signal: runAbortController.signal,
|
|
368
|
-
},
|
|
369
|
-
(err, chunk) => {
|
|
370
|
-
if (!err) {
|
|
371
|
-
enqueueChunk(chunk);
|
|
372
|
-
}
|
|
592
|
+
if (!err) enqueueChunk(chunk);
|
|
373
593
|
},
|
|
374
594
|
);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
endShellRun(sessionKey, shellSession);
|
|
597
|
+
throw error;
|
|
598
|
+
}
|
|
599
|
+
void runPromise.finally(() => endShellRun(sessionKey, shellSession)).catch(() => undefined);
|
|
600
|
+
} else {
|
|
601
|
+
runPromise = executeShell(
|
|
602
|
+
{
|
|
603
|
+
command: finalCommand,
|
|
604
|
+
cwd: commandCwd,
|
|
605
|
+
env: commandEnv,
|
|
606
|
+
sessionEnv: shellEnv,
|
|
607
|
+
snapshotPath: snapshotPath ?? undefined,
|
|
608
|
+
minimizer,
|
|
609
|
+
timeoutMs: executionTimeoutMs,
|
|
610
|
+
signal: runAbortController.signal,
|
|
611
|
+
},
|
|
612
|
+
(err, chunk) => {
|
|
613
|
+
if (!err) enqueueChunk(chunk);
|
|
614
|
+
},
|
|
615
|
+
);
|
|
616
|
+
}
|
|
375
617
|
|
|
376
618
|
const winner = await Promise.race([
|
|
377
619
|
runPromise.then(result => ({ kind: "result" as const, result })),
|
|
@@ -380,44 +622,37 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
380
622
|
]);
|
|
381
623
|
|
|
382
624
|
if (winner.kind === "timeout" || winner.kind === "abort") {
|
|
625
|
+
const abortOutcome = await awaitAbortCleanup(runPromise);
|
|
383
626
|
acceptingChunks = false;
|
|
384
627
|
if (shellSession) {
|
|
385
628
|
resetSession = true;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
runSettled = await awaitAbortCleanup(runPromise);
|
|
390
|
-
if (runSettled) {
|
|
391
|
-
brokenShellSessions.delete(sessionKey);
|
|
392
|
-
retiringShellSessions.delete(shellSession);
|
|
393
|
-
} else {
|
|
394
|
-
void runPromise
|
|
395
|
-
.finally(() => {
|
|
396
|
-
brokenShellSessions.delete(sessionKey);
|
|
397
|
-
retiringShellSessions.delete(shellSession);
|
|
398
|
-
if (shellSessions.get(sessionKey) === shellSession) {
|
|
399
|
-
shellSessions.delete(sessionKey);
|
|
400
|
-
}
|
|
401
|
-
})
|
|
402
|
-
.catch(() => undefined);
|
|
403
|
-
}
|
|
404
|
-
} else {
|
|
629
|
+
quarantineShellSession(sessionKey, shellSession);
|
|
630
|
+
releaseQuarantinedShellIfIdle(sessionKey, shellSession);
|
|
631
|
+
} else if (!abortOutcome.settled) {
|
|
405
632
|
void runPromise.catch(() => undefined);
|
|
406
633
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
cancelled: true,
|
|
410
|
-
...(await sink.dump(
|
|
634
|
+
const summary = applyShellCallbackLoss(
|
|
635
|
+
await sink.dump(
|
|
411
636
|
winner.kind === "timeout" && baseTimeoutMs !== undefined
|
|
412
637
|
? `Command timed out after ${Math.round(baseTimeoutMs / 1000)} seconds`
|
|
413
638
|
: "Command cancelled",
|
|
414
|
-
)
|
|
639
|
+
),
|
|
640
|
+
abortOutcome.result?.droppedOutputBytes,
|
|
641
|
+
abortOutcome.result?.droppedOutputChunks,
|
|
642
|
+
nativeCaptureStatusIncomplete(abortOutcome.result),
|
|
643
|
+
);
|
|
644
|
+
return {
|
|
645
|
+
exitCode: undefined,
|
|
646
|
+
cancelled: true,
|
|
647
|
+
...summary,
|
|
415
648
|
};
|
|
416
649
|
}
|
|
417
650
|
if (timeoutTimer) {
|
|
418
651
|
clearTimeout(timeoutTimer);
|
|
419
652
|
timeoutTimer = undefined;
|
|
420
653
|
}
|
|
654
|
+
const minimizedEvidence = normalizeNativeMinimized(winner.result.minimized);
|
|
655
|
+
const nativeEvidenceIncomplete = nativeCaptureStatusIncomplete(winner.result) || minimizedEvidence.malformed;
|
|
421
656
|
|
|
422
657
|
// Handle timeout
|
|
423
658
|
if (winner.result.timedOut) {
|
|
@@ -428,7 +663,12 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
428
663
|
return {
|
|
429
664
|
exitCode: undefined,
|
|
430
665
|
cancelled: true,
|
|
431
|
-
...(
|
|
666
|
+
...applyShellCallbackLoss(
|
|
667
|
+
await sink.dump(annotation),
|
|
668
|
+
winner.result.droppedOutputBytes,
|
|
669
|
+
winner.result.droppedOutputChunks,
|
|
670
|
+
nativeEvidenceIncomplete,
|
|
671
|
+
),
|
|
432
672
|
};
|
|
433
673
|
}
|
|
434
674
|
|
|
@@ -438,7 +678,12 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
438
678
|
return {
|
|
439
679
|
exitCode: undefined,
|
|
440
680
|
cancelled: true,
|
|
441
|
-
...(
|
|
681
|
+
...applyShellCallbackLoss(
|
|
682
|
+
await sink.dump("Command cancelled"),
|
|
683
|
+
winner.result.droppedOutputBytes,
|
|
684
|
+
winner.result.droppedOutputChunks,
|
|
685
|
+
nativeEvidenceIncomplete,
|
|
686
|
+
),
|
|
442
687
|
};
|
|
443
688
|
}
|
|
444
689
|
|
|
@@ -446,21 +691,24 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
446
691
|
// raw stream for the minimized text, persist the original as a session
|
|
447
692
|
// artifact, and splice an artifact footer into the visible text so the agent
|
|
448
693
|
// can retrieve retained raw bytes without a false completeness claim.
|
|
449
|
-
const minimized =
|
|
694
|
+
const minimized = minimizedEvidence.minimized;
|
|
450
695
|
let minimizedSaveResult: BashArtifactSaveResult | undefined;
|
|
451
696
|
if (minimized && minimized.text !== minimized.originalText) {
|
|
452
697
|
sink.replace(minimized.text);
|
|
453
|
-
|
|
454
|
-
? await
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
698
|
+
minimizedSaveResult = options?.onMinimizedSave
|
|
699
|
+
? await saveMinimizedArtifactBounded(() =>
|
|
700
|
+
options.onMinimizedSave!(minimized.originalText, {
|
|
701
|
+
filter: minimized.filter,
|
|
702
|
+
inputBytes: minimized.inputBytes,
|
|
703
|
+
outputBytes: minimized.outputBytes,
|
|
704
|
+
}),
|
|
705
|
+
)
|
|
706
|
+
: { status: "unavailable" };
|
|
461
707
|
if (minimizedSaveResult.status === "saved") {
|
|
462
|
-
|
|
463
|
-
|
|
708
|
+
minimizedSaveResult = {
|
|
709
|
+
status: "failed",
|
|
710
|
+
diagnostic: "artifact save could not be verified in the current session",
|
|
711
|
+
};
|
|
464
712
|
}
|
|
465
713
|
}
|
|
466
714
|
|
|
@@ -480,7 +728,12 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
480
728
|
}
|
|
481
729
|
|
|
482
730
|
// Normal completion
|
|
483
|
-
const summary =
|
|
731
|
+
const summary = applyShellCallbackLoss(
|
|
732
|
+
await sink.dump(),
|
|
733
|
+
winner.result.droppedOutputBytes,
|
|
734
|
+
winner.result.droppedOutputChunks,
|
|
735
|
+
nativeEvidenceIncomplete,
|
|
736
|
+
);
|
|
484
737
|
const saveNotice = minimizedSaveResult ? minimizedSaveNotice(minimizedSaveResult, summary) : undefined;
|
|
485
738
|
return {
|
|
486
739
|
exitCode: winner.result.exitCode,
|
|
@@ -490,7 +743,23 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
490
743
|
};
|
|
491
744
|
} catch (err) {
|
|
492
745
|
resetSession = true;
|
|
493
|
-
|
|
746
|
+
acceptingChunks = false;
|
|
747
|
+
const captured = await sink.dump();
|
|
748
|
+
const rawErrorMessage = err instanceof Error ? err.message : String(err);
|
|
749
|
+
const boundedErrorMessage = truncateHeadBytes(rawErrorMessage, 1024);
|
|
750
|
+
const errorMessage = `${boundedErrorMessage.text}${
|
|
751
|
+
boundedErrorMessage.bytes < Buffer.byteLength(rawErrorMessage, "utf-8") ? "\n[native error truncated]" : ""
|
|
752
|
+
}`;
|
|
753
|
+
if (captured.totalBytes <= 0) {
|
|
754
|
+
throw new Error(
|
|
755
|
+
`Source capture completeness could not be proven before any output was retained\n\nNative shell execution failed: ${errorMessage}`,
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
const summary = applyShellCallbackLoss(captured, undefined, undefined, true);
|
|
759
|
+
const evidence = formatArtifactEvidenceNotice(summary);
|
|
760
|
+
const suffix = [evidence, `Native shell execution failed: ${errorMessage}`].filter(Boolean).join("\n\n");
|
|
761
|
+
const separator = summary.output.length > 0 ? (summary.output.endsWith("\n") ? "\n" : "\n\n") : "";
|
|
762
|
+
throw new Error(`${summary.output}${separator}${suffix}`);
|
|
494
763
|
} finally {
|
|
495
764
|
if (timeoutTimer) {
|
|
496
765
|
clearTimeout(timeoutTimer);
|
|
@@ -498,8 +767,13 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
498
767
|
if (userSignal) {
|
|
499
768
|
userSignal.removeEventListener("abort", abortHandler);
|
|
500
769
|
}
|
|
501
|
-
if (resetSession &&
|
|
502
|
-
shellSessions.delete(sessionKey);
|
|
770
|
+
if (resetSession && shellSession) {
|
|
771
|
+
if (shellSessions.get(sessionKey) === shellSession) shellSessions.delete(sessionKey);
|
|
772
|
+
if ((shellInFlightRuns.get(shellSession) ?? 0) > 0) {
|
|
773
|
+
quarantineShellSession(sessionKey, shellSession);
|
|
774
|
+
} else {
|
|
775
|
+
releaseQuarantinedShellIfIdle(sessionKey, shellSession);
|
|
776
|
+
}
|
|
503
777
|
}
|
|
504
778
|
}
|
|
505
779
|
}
|