@gajae-code/coding-agent 0.12.0 → 0.12.1
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 +82 -1
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/config/settings-schema.d.ts +54 -0
- package/dist/types/exa/mcp-client.d.ts +10 -1
- package/dist/types/exec/bash-executor.d.ts +45 -12
- package/dist/types/extensibility/extensions/types.d.ts +16 -2
- package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
- package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +18 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
- package/dist/types/modes/acp/acp-agent.d.ts +2 -19
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
- package/dist/types/modes/components/settings-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line/gh.d.ts +5 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
- package/dist/types/reminders/star-reminder.d.ts +3 -13
- package/dist/types/runtime-mcp/manager.d.ts +1 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
- package/dist/types/sdk/bus/config.d.ts +6 -2
- package/dist/types/sdk/bus/index.d.ts +29 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
- package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
- package/dist/types/sdk/bus/rich-render.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
- package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
- package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
- package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/prompt-status.d.ts +23 -3
- package/dist/types/sdk/session.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +38 -2
- package/dist/types/session/artifacts.d.ts +8 -0
- package/dist/types/session/blob-store.d.ts +57 -4
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
- package/dist/types/session/session-manager.d.ts +23 -6
- package/dist/types/session/session-storage.d.ts +4 -0
- package/dist/types/session/streaming-output.d.ts +36 -7
- package/dist/types/setup/credential-import.d.ts +12 -0
- package/dist/types/skill-state/active-state.d.ts +12 -0
- package/dist/types/skill-state/workflow-hud.d.ts +6 -0
- package/dist/types/task/executor.d.ts +3 -2
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +7 -1
- package/dist/types/task/types.d.ts +23 -0
- package/dist/types/tools/ask-answer-registry.d.ts +2 -2
- package/dist/types/tools/bash-interactive.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/output-meta.d.ts +23 -2
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read.d.ts +0 -6
- package/dist/types/tools/review.d.ts +5 -7
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/utils/gh.d.ts +13 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/searxng.d.ts +7 -0
- package/dist/types/web/search/providers/xai.d.ts +2 -0
- package/package.json +10 -8
- package/scripts/acp-conformance-agent.ts +218 -0
- package/scripts/capture-gjc-bundle-settings.ts +306 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +285 -0
- package/src/async/job-manager.ts +78 -20
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/plugin-cli.ts +176 -18
- package/src/commands/team.ts +19 -1
- package/src/config/file-lock.ts +11 -2
- package/src/config/settings-schema.ts +51 -0
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +19 -16
- package/src/defaults/gjc/skills/team/SKILL.md +2 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -24
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
- package/src/defaults/gjc-defaults.ts +4 -4
- package/src/exa/mcp-client.ts +12 -3
- package/src/exec/bash-executor.ts +144 -27
- package/src/extensibility/extensions/runner.ts +15 -0
- package/src/extensibility/extensions/types.ts +18 -1
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
- package/src/extensibility/gjc-plugins/index.ts +19 -2
- package/src/extensibility/gjc-plugins/installer.ts +189 -50
- package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
- package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
- package/src/extensibility/gjc-plugins/observability.ts +3 -3
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
- package/src/extensibility/gjc-plugins/schema.ts +84 -12
- package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
- package/src/extensibility/gjc-plugins/types.ts +141 -0
- package/src/extensibility/gjc-plugins/validation.ts +9 -0
- package/src/gjc-runtime/launch-tmux.ts +25 -6
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/ralplan-runtime.ts +411 -17
- package/src/gjc-runtime/team-launch.ts +8 -0
- package/src/gjc-runtime/team-runtime.ts +140 -32
- package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
- package/src/gjc-runtime/tmux-sessions.ts +58 -13
- package/src/gjc-runtime/ultragoal-runtime.ts +401 -611
- package/src/gjc-runtime/workflow-manifest.generated.json +8 -99
- package/src/gjc-runtime/workflow-manifest.ts +4 -68
- package/src/internal-urls/docs-index.generated.ts +15 -15
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/acp-agent.ts +298 -186
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- package/src/modes/components/notifications-settings-editor.ts +12 -1
- package/src/modes/components/oauth-selector.ts +68 -30
- package/src/modes/components/session-observer-overlay.ts +305 -86
- package/src/modes/components/settings-selector.ts +56 -5
- package/src/modes/components/status-line/gh.ts +16 -0
- package/src/modes/components/tool-status-header.ts +3 -14
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/extension-ui-controller.ts +7 -0
- package/src/modes/controllers/selector-controller.ts +45 -0
- package/src/modes/runtime-init.ts +3 -0
- package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
- package/src/prompts/system/eager-todo.md +8 -4
- package/src/prompts/system/system-prompt.md +11 -4
- package/src/prompts/tools/bash.md +5 -5
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/reminders/star-reminder.ts +4 -42
- package/src/runtime-mcp/manager.ts +19 -4
- package/src/sdk/acp/adapter.ts +22 -6
- package/src/sdk/broker/transport.ts +35 -3
- package/src/sdk/bus/attachment-registry.ts +5 -1
- package/src/sdk/bus/chat-daemon-cli.ts +4 -39
- package/src/sdk/bus/chat-daemon-control.ts +6 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/index.ts +661 -126
- package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
- package/src/sdk/bus/lifecycle-commands.ts +4 -1
- package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
- package/src/sdk/bus/reconciliation-store.ts +95 -16
- package/src/sdk/bus/rich-draft.ts +1 -1
- package/src/sdk/bus/rich-render.ts +2 -2
- package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
- package/src/sdk/bus/telegram-cli.ts +33 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
- package/src/sdk/bus/telegram-daemon.ts +916 -23
- package/src/sdk/bus/telegram-reference.ts +40 -3
- package/src/sdk/bus/threaded-render.ts +48 -3
- package/src/sdk/bus/topic-registry.ts +20 -0
- package/src/sdk/host/query/revision-store.ts +36 -2
- package/src/sdk/host/reverse-leases.ts +44 -7
- package/src/sdk/prompt-status.ts +16 -3
- package/src/sdk/protocol/operation-inventory.generated.json +22 -0
- package/src/sdk/session.ts +88 -9
- package/src/session/agent-session.ts +822 -172
- package/src/session/artifacts.ts +22 -0
- package/src/session/auth-broker-config.ts +10 -3
- package/src/session/blob-store.ts +915 -24
- package/src/session/internal/managed-session-scope.ts +44 -6
- package/src/session/internal/managed-session-storage.ts +48 -6
- package/src/session/session-manager.ts +1614 -435
- package/src/session/session-storage.ts +9 -1
- package/src/session/streaming-output.ts +238 -68
- package/src/setup/credential-import.ts +75 -13
- package/src/skill-state/active-state.ts +32 -0
- package/src/skill-state/workflow-hud.ts +17 -0
- package/src/skill-state/workflow-mutation-guard.ts +496 -2
- package/src/task/executor.ts +122 -39
- package/src/task/index.ts +23 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +23 -7
- package/src/task/render.ts +6 -2
- package/src/task/types.ts +108 -0
- package/src/tools/ask-answer-registry.ts +12 -6
- package/src/tools/ask.ts +86 -55
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash-interactive.ts +8 -3
- package/src/tools/bash.ts +503 -60
- package/src/tools/find.ts +1 -0
- package/src/tools/image-gen.ts +29 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/output-meta.ts +108 -14
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +18 -0
- package/src/tools/review.ts +5 -7
- package/src/tools/search.ts +1 -0
- package/src/tools/subagent-render.ts +6 -2
- package/src/tools/subagent.ts +6 -0
- package/src/tools/telegram-send.ts +6 -3
- package/src/utils/gh.ts +39 -0
- package/src/utils/image-resize.ts +3 -0
- package/src/web/search/providers/anthropic.ts +4 -4
- package/src/web/search/providers/kimi.ts +15 -5
- package/src/web/search/providers/searxng.ts +47 -4
- package/src/web/search/providers/xai.ts +7 -2
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
- package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
package/src/tools/bash.ts
CHANGED
|
@@ -5,7 +5,8 @@ import { ImageProtocol, TERMINAL, Text } from "@gajae-code/tui";
|
|
|
5
5
|
import { getProjectDir, isEnoent, logger, prompt } from "@gajae-code/utils";
|
|
6
6
|
import * as z from "zod/v4";
|
|
7
7
|
import { AsyncJobManager } from "../async";
|
|
8
|
-
import { type BashResult, executeBash } from "../exec/bash-executor";
|
|
8
|
+
import { type BashArtifactSaveResult, type BashResult, executeBash } from "../exec/bash-executor";
|
|
9
|
+
|
|
9
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
11
|
import { buildGjcRuntimeSessionEnv } from "../gjc-runtime/goal-mode-request";
|
|
11
12
|
import {
|
|
@@ -16,8 +17,20 @@ import { InternalUrlRouter } from "../internal-urls";
|
|
|
16
17
|
import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
17
18
|
import { highlightCode, type Theme } from "../modes/theme/theme";
|
|
18
19
|
import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
|
|
20
|
+
import type { ArtifactManager } from "../session/artifacts";
|
|
19
21
|
import type { ClientBridgeTerminalExitStatus, ClientBridgeTerminalOutput } from "../session/client-bridge";
|
|
20
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_ARTIFACT_MAX_BYTES,
|
|
24
|
+
OutputSink,
|
|
25
|
+
type OutputSummary,
|
|
26
|
+
streamTailUpdates,
|
|
27
|
+
TailBuffer,
|
|
28
|
+
type TerminalArtifactPublisher,
|
|
29
|
+
type TerminalArtifactPublishResult,
|
|
30
|
+
truncateHeadBytes,
|
|
31
|
+
truncateTailBytes,
|
|
32
|
+
} from "../session/streaming-output";
|
|
33
|
+
|
|
21
34
|
import { renderStatusLine } from "../tui";
|
|
22
35
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
23
36
|
import { getSixelLineMask } from "../utils/sixel";
|
|
@@ -29,7 +42,14 @@ import { checkBashInterception } from "./bash-interceptor";
|
|
|
29
42
|
import { canUseInteractiveBashPty } from "./bash-pty-selection";
|
|
30
43
|
import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
|
|
31
44
|
import { checkComposerBashPolicy } from "./composer-bash-policy";
|
|
32
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
formatArtifactReference,
|
|
47
|
+
formatStyledTruncationWarning,
|
|
48
|
+
type OutputMeta,
|
|
49
|
+
resolveBashOutputSinkHeadBytes,
|
|
50
|
+
resolveBashOutputSinkTailBytes,
|
|
51
|
+
stripOutputNotice,
|
|
52
|
+
} from "./output-meta";
|
|
33
53
|
import { resolveToCwd } from "./path-utils";
|
|
34
54
|
import { formatToolWorkingDirectory, replaceTabs } from "./render-utils";
|
|
35
55
|
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
@@ -38,6 +58,8 @@ import { clampTimeout, TOOL_TIMEOUTS } from "./tool-timeouts";
|
|
|
38
58
|
|
|
39
59
|
export const BASH_DEFAULT_PREVIEW_LINES = 10;
|
|
40
60
|
|
|
61
|
+
const BASH_ERROR_MAX_BYTES = 4096;
|
|
62
|
+
const ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES = 256;
|
|
41
63
|
const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
42
64
|
const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
|
|
43
65
|
const READ_ONLY_BASH_ENV: Record<string, string> = {
|
|
@@ -47,20 +69,301 @@ const READ_ONLY_BASH_ENV: Record<string, string> = {
|
|
|
47
69
|
RIPGREP_CONFIG_PATH: "",
|
|
48
70
|
};
|
|
49
71
|
|
|
50
|
-
export
|
|
72
|
+
export type BashOriginalArtifactSaveResult = BashArtifactSaveResult;
|
|
73
|
+
|
|
74
|
+
function boundArtifactSaveDiagnostic(error: unknown): string {
|
|
75
|
+
const message = (error instanceof Error ? error.message : String(error)).replace(/\s+/gu, " ").trim();
|
|
76
|
+
const normalized = message || "unknown storage error";
|
|
77
|
+
return truncateHeadBytes(normalized, ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES).text;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function summarizeOriginalArtifactSave(
|
|
81
|
+
artifactId: string,
|
|
82
|
+
originalText: string,
|
|
83
|
+
): Extract<BashOriginalArtifactSaveResult, { status: "saved" }> {
|
|
84
|
+
const originalBytes = Buffer.byteLength(originalText, "utf-8");
|
|
85
|
+
if (originalBytes <= DEFAULT_ARTIFACT_MAX_BYTES) {
|
|
86
|
+
return { status: "saved", artifactId, complete: true };
|
|
87
|
+
}
|
|
88
|
+
const retainedBytes = truncateHeadBytes(originalText, DEFAULT_ARTIFACT_MAX_BYTES).bytes;
|
|
89
|
+
return {
|
|
90
|
+
status: "saved",
|
|
91
|
+
artifactId,
|
|
92
|
+
complete: false,
|
|
93
|
+
omittedBytes: originalBytes - retainedBytes,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function artifactSaveResultNotice(
|
|
98
|
+
result: BashOriginalArtifactSaveResult,
|
|
99
|
+
hasAlternateCompleteArtifact = false,
|
|
100
|
+
): string | undefined {
|
|
101
|
+
if (result.status === "failed") return `Bash output artifact save failed: ${result.diagnostic}`;
|
|
102
|
+
if (result.status === "unavailable" && !hasAlternateCompleteArtifact) {
|
|
103
|
+
return "Bash output artifact unavailable: full original output could not be stored because artifact storage is unavailable.";
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function artifactTruncatedBytesForResult(result: BashResult | BashInteractiveResult): number | undefined {
|
|
109
|
+
const bytes = (result as OutputSummary).artifactTruncatedBytes;
|
|
110
|
+
return typeof bytes === "number" && bytes > 0 ? bytes : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function artifactReferenceForResult(result: BashResult | BashInteractiveResult): string | undefined {
|
|
114
|
+
return result.artifactId
|
|
115
|
+
? formatArtifactReference(result.artifactId, artifactTruncatedBytesForResult(result))
|
|
116
|
+
: undefined;
|
|
117
|
+
}
|
|
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
|
+
|
|
127
|
+
function appendRawArtifactFooter(
|
|
128
|
+
summary: OutputSummary,
|
|
129
|
+
result: Extract<BashOriginalArtifactSaveResult, { status: "saved" }>,
|
|
130
|
+
): void {
|
|
131
|
+
summary.artifactId = result.artifactId;
|
|
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)}]`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function artifactReferenceIsReachable(text: string, result: BashResult | BashInteractiveResult): boolean {
|
|
138
|
+
if (!result.artifactId || !text.includes(`artifact://${result.artifactId}`)) return false;
|
|
139
|
+
const artifactTruncatedBytes = artifactTruncatedBytesForResult(result);
|
|
140
|
+
return (
|
|
141
|
+
artifactTruncatedBytes === undefined ||
|
|
142
|
+
text.includes(formatArtifactReference(result.artifactId, artifactTruncatedBytes))
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function artifactFailureDiagnosticForResult(result: BashResult | BashInteractiveResult): string | undefined {
|
|
147
|
+
const diagnostic = (result as OutputSummary).artifactFailureDiagnostic;
|
|
148
|
+
return typeof diagnostic === "string" && diagnostic.length > 0 ? diagnostic : undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function artifactWriterFailureNotice(result: BashResult | BashInteractiveResult): string | undefined {
|
|
152
|
+
const diagnostic = artifactFailureDiagnosticForResult(result);
|
|
153
|
+
if (!diagnostic) return undefined;
|
|
154
|
+
if (diagnostic.startsWith("unavailable:")) {
|
|
155
|
+
return `Bash output artifact unavailable: ${diagnostic.slice("unavailable:".length).trim()}`;
|
|
156
|
+
}
|
|
157
|
+
const normalized = diagnostic.startsWith("failed:") ? diagnostic.slice("failed:".length).trim() : diagnostic;
|
|
158
|
+
return `Bash output artifact writer failed: ${normalized}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function completeOutputArtifactAvailable(
|
|
162
|
+
result: Pick<OutputSummary, "artifactId" | "artifactTruncatedBytes" | "artifactFailureDiagnostic">,
|
|
163
|
+
): boolean {
|
|
164
|
+
return (
|
|
165
|
+
result.artifactId !== undefined &&
|
|
166
|
+
(typeof result.artifactTruncatedBytes !== "number" || result.artifactTruncatedBytes <= 0) &&
|
|
167
|
+
(typeof result.artifactFailureDiagnostic !== "string" || result.artifactFailureDiagnostic.length === 0)
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function failureStatusCause(
|
|
172
|
+
result: BashResult | BashInteractiveResult,
|
|
173
|
+
text: string,
|
|
174
|
+
explicitCause?: string,
|
|
175
|
+
): string | undefined {
|
|
176
|
+
if (explicitCause) return explicitCause;
|
|
177
|
+
const leadingStatus = /^\[(Command (?:timed out(?: after \d+ seconds?)?|cancelled))\]/u.exec(text)?.[1];
|
|
178
|
+
const statusMatches = Array.from(text.matchAll(/Command (?:timed out(?: after \d+ seconds?)?|cancelled|aborted)/gu));
|
|
179
|
+
const lastStatus = statusMatches[statusMatches.length - 1]?.[0];
|
|
180
|
+
if (result.cancelled) return leadingStatus ?? lastStatus ?? "Command cancelled";
|
|
181
|
+
if (isInteractiveResult(result) && result.timedOut) return lastStatus ?? "Command timed out";
|
|
182
|
+
if (result.exitCode === undefined) return "Command failed: missing exit status";
|
|
183
|
+
if (result.exitCode !== 0) return `Command exited with code ${result.exitCode}`;
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function removeTrailingFailureCause(text: string, cause: string | undefined): string {
|
|
188
|
+
if (!cause) return text;
|
|
189
|
+
for (const suffix of [`\n\n${cause}`, `\n${cause}`, cause]) {
|
|
190
|
+
if (text.endsWith(suffix)) return text.slice(0, -suffix.length);
|
|
191
|
+
}
|
|
192
|
+
return text;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function formatBashFailureMessage(
|
|
196
|
+
result: BashResult | BashInteractiveResult,
|
|
197
|
+
text: string,
|
|
198
|
+
explicitCause?: string,
|
|
199
|
+
): string {
|
|
200
|
+
const statusCause = failureStatusCause(result, text, explicitCause);
|
|
201
|
+
const bodyText = removeTrailingFailureCause(text, statusCause);
|
|
202
|
+
const suffixParts: string[] = [];
|
|
203
|
+
const reference = artifactReferenceForResult(result);
|
|
204
|
+
if (reference) suffixParts.push(reference);
|
|
205
|
+
const writerNotice = artifactWriterFailureNotice(result);
|
|
206
|
+
if (writerNotice) suffixParts.push(writerNotice);
|
|
207
|
+
if (statusCause) suffixParts.push(statusCause);
|
|
208
|
+
const suffix = suffixParts.join("\n\n");
|
|
209
|
+
const separator = bodyText.length > 0 && suffix.length > 0 ? "\n\n" : "";
|
|
210
|
+
const bodyBudget = Math.max(
|
|
211
|
+
0,
|
|
212
|
+
BASH_ERROR_MAX_BYTES - Buffer.byteLength(suffix, "utf-8") - Buffer.byteLength(separator, "utf-8"),
|
|
213
|
+
);
|
|
214
|
+
const body = truncateTailBytes(bodyText, bodyBudget).text;
|
|
215
|
+
return `${body}${body.length > 0 ? separator : ""}${suffix}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function boundClientTerminalOutput(
|
|
219
|
+
output: string,
|
|
220
|
+
alreadyTruncated: boolean,
|
|
221
|
+
settings: ToolSession["settings"],
|
|
222
|
+
): Promise<{ summary: OutputSummary; locallyTruncated: boolean }> {
|
|
223
|
+
const tailBytes = resolveBashOutputSinkTailBytes(settings);
|
|
224
|
+
const headBytes = resolveBashOutputSinkHeadBytes(settings);
|
|
225
|
+
const sink = new OutputSink({ spillThreshold: tailBytes, headBytes });
|
|
226
|
+
sink.push(output);
|
|
227
|
+
const bounded = await sink.dump();
|
|
228
|
+
return {
|
|
229
|
+
summary: {
|
|
230
|
+
...bounded,
|
|
231
|
+
truncated: alreadyTruncated || bounded.truncated,
|
|
232
|
+
},
|
|
233
|
+
locallyTruncated: bounded.truncated,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
interface PreparedClientTerminalOutput {
|
|
238
|
+
current: ClientBridgeTerminalOutput;
|
|
239
|
+
summary: OutputSummary;
|
|
240
|
+
locallyTruncated: boolean;
|
|
241
|
+
artifactSaveResult?: BashOriginalArtifactSaveResult;
|
|
242
|
+
artifactSaveNotice?: string;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function prepareClientTerminalOutput(
|
|
246
|
+
session: ToolSession,
|
|
247
|
+
current: ClientBridgeTerminalOutput,
|
|
248
|
+
): Promise<PreparedClientTerminalOutput> {
|
|
249
|
+
const { summary, locallyTruncated } = await boundClientTerminalOutput(
|
|
250
|
+
current.output,
|
|
251
|
+
current.truncated,
|
|
252
|
+
session.settings,
|
|
253
|
+
);
|
|
254
|
+
let artifactSaveResult: BashOriginalArtifactSaveResult | undefined;
|
|
255
|
+
if (locallyTruncated && !current.truncated) {
|
|
256
|
+
artifactSaveResult = await saveBashOriginalArtifact(session, current.output);
|
|
257
|
+
if (artifactSaveResult.status === "saved") appendRawArtifactFooter(summary, artifactSaveResult);
|
|
258
|
+
}
|
|
259
|
+
const artifactSaveNotice = artifactSaveResult
|
|
260
|
+
? artifactSaveResultNotice(artifactSaveResult, completeOutputArtifactAvailable(summary))
|
|
261
|
+
: undefined;
|
|
262
|
+
return { current, summary, locallyTruncated, artifactSaveResult, artifactSaveNotice };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function formatClientTerminalAbortFailure(
|
|
266
|
+
prepared: PreparedClientTerminalOutput,
|
|
267
|
+
readDiagnostic?: string,
|
|
268
|
+
pendingNotices: readonly string[] = [],
|
|
269
|
+
): string {
|
|
270
|
+
const notices = [
|
|
271
|
+
...pendingNotices,
|
|
272
|
+
...(prepared.current.truncated || prepared.locallyTruncated ? ["(output truncated)"] : []),
|
|
273
|
+
...(prepared.artifactSaveNotice ? [prepared.artifactSaveNotice] : []),
|
|
274
|
+
...(readDiagnostic ? [`Terminal output recovery failed: ${readDiagnostic}`] : []),
|
|
275
|
+
];
|
|
276
|
+
const outputLines = [prepared.summary.output || "(no output)", ...notices].filter(Boolean);
|
|
277
|
+
const outputText = outputLines.join("\n");
|
|
278
|
+
const result: BashResult = {
|
|
279
|
+
...prepared.summary,
|
|
280
|
+
exitCode: undefined,
|
|
281
|
+
cancelled: true,
|
|
282
|
+
};
|
|
283
|
+
return formatBashFailureMessage(result, outputText, "Command aborted");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function appendArtifactDetails(text: string, result: BashResult | BashInteractiveResult): string {
|
|
287
|
+
const suffixParts: string[] = [];
|
|
288
|
+
const reference = artifactReferenceForResult(result);
|
|
289
|
+
if (reference && !artifactReferenceIsReachable(text, result)) suffixParts.push(reference);
|
|
290
|
+
const writerNotice = artifactWriterFailureNotice(result);
|
|
291
|
+
if (writerNotice && !text.includes(writerNotice)) suffixParts.push(writerNotice);
|
|
292
|
+
return suffixParts.length > 0 ? `${text}${text.endsWith("\n") ? "" : "\n"}${suffixParts.join("\n\n")}` : text;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function saveBashOriginalArtifact(
|
|
51
296
|
session: ToolSession,
|
|
52
297
|
originalText: string,
|
|
53
|
-
): Promise<
|
|
298
|
+
): Promise<BashOriginalArtifactSaveResult> {
|
|
299
|
+
let manager: ArtifactManager | null | undefined;
|
|
300
|
+
try {
|
|
301
|
+
manager = session.getArtifactManager?.();
|
|
302
|
+
} catch (error) {
|
|
303
|
+
return { status: "failed", diagnostic: boundArtifactSaveDiagnostic(error) };
|
|
304
|
+
}
|
|
305
|
+
if (manager) {
|
|
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
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!session.allocateOutputArtifact) return { status: "unavailable" };
|
|
317
|
+
let alloc: { id?: string; path?: string } | undefined;
|
|
54
318
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (!alloc?.path || !alloc.id) return undefined;
|
|
59
|
-
await Bun.write(alloc.path, originalText);
|
|
60
|
-
return alloc.id;
|
|
61
|
-
} catch {
|
|
62
|
-
return undefined;
|
|
319
|
+
alloc = await session.allocateOutputArtifact("bash-original");
|
|
320
|
+
} catch (error) {
|
|
321
|
+
return { status: "failed", diagnostic: boundArtifactSaveDiagnostic(error) };
|
|
63
322
|
}
|
|
323
|
+
if (!alloc?.path || !alloc.id) return { status: "unavailable" };
|
|
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) };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function createBashArtifactPublisher(session: ToolSession): TerminalArtifactPublisher {
|
|
340
|
+
return async (content, _info): Promise<TerminalArtifactPublishResult> => {
|
|
341
|
+
let manager: ArtifactManager | null | undefined;
|
|
342
|
+
try {
|
|
343
|
+
manager = session.getArtifactManager?.();
|
|
344
|
+
} catch (error) {
|
|
345
|
+
return { status: "failed", diagnostic: boundArtifactSaveDiagnostic(error) };
|
|
346
|
+
}
|
|
347
|
+
if (!manager) return { status: "unavailable" };
|
|
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
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export async function saveBashOriginalArtifactForTests(
|
|
360
|
+
session: ToolSession,
|
|
361
|
+
originalText: string,
|
|
362
|
+
onResult?: (result: BashOriginalArtifactSaveResult) => void,
|
|
363
|
+
): Promise<string | undefined> {
|
|
364
|
+
const result = await saveBashOriginalArtifact(session, originalText);
|
|
365
|
+
onResult?.(result);
|
|
366
|
+
return result.status === "saved" ? result.artifactId : undefined;
|
|
64
367
|
}
|
|
65
368
|
|
|
66
369
|
const bashSchemaBase = z.object({
|
|
@@ -146,6 +449,7 @@ type ManagedBashJobCompletion =
|
|
|
146
449
|
| {
|
|
147
450
|
kind: "failed";
|
|
148
451
|
error: unknown;
|
|
452
|
+
result?: BashResult | BashInteractiveResult;
|
|
149
453
|
};
|
|
150
454
|
|
|
151
455
|
interface ManagedBashJobHandle {
|
|
@@ -164,6 +468,22 @@ function isInteractiveResult(result: BashResult | BashInteractiveResult): result
|
|
|
164
468
|
return "timedOut" in result;
|
|
165
469
|
}
|
|
166
470
|
|
|
471
|
+
function formatManagedAbortFailure(
|
|
472
|
+
error: unknown,
|
|
473
|
+
result: BashResult | BashInteractiveResult | undefined,
|
|
474
|
+
latestText: string,
|
|
475
|
+
): string {
|
|
476
|
+
if (result) {
|
|
477
|
+
const output = normalizeResultOutput(result).replace(/\[Command (?:cancelled|aborted)\]\n?/gu, "");
|
|
478
|
+
return formatBashFailureMessage(result, output || "Command aborted", "Command aborted");
|
|
479
|
+
}
|
|
480
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
481
|
+
const bodyText = raw.replace(/Command cancelled(?: after \d+ seconds?)?/gu, "").trim() || latestText.trim();
|
|
482
|
+
const bodyBudget = Math.max(0, BASH_ERROR_MAX_BYTES - Buffer.byteLength("Command aborted", "utf-8") - 2);
|
|
483
|
+
const body = truncateTailBytes(bodyText, bodyBudget).text;
|
|
484
|
+
return body.length > 0 ? `${body}\n\nCommand aborted` : "Command aborted";
|
|
485
|
+
}
|
|
486
|
+
|
|
167
487
|
function normalizeBashEnv(env: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
168
488
|
if (!env || Object.keys(env).length === 0) return undefined;
|
|
169
489
|
const normalized: Record<string, string> = {};
|
|
@@ -319,17 +639,24 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
319
639
|
}
|
|
320
640
|
|
|
321
641
|
#buildResultText(result: BashResult | BashInteractiveResult, timeoutSec: number, outputText: string): string {
|
|
642
|
+
const trimmedOutput = outputText.trim();
|
|
643
|
+
const renderedOutput =
|
|
644
|
+
trimmedOutput && trimmedOutput !== "(no output)" ? outputText : normalizeResultOutput(result);
|
|
322
645
|
if (result.cancelled) {
|
|
323
|
-
throw new ToolError(
|
|
646
|
+
throw new ToolError(formatBashFailureMessage(result, renderedOutput || "Command aborted"));
|
|
324
647
|
}
|
|
325
648
|
if (isInteractiveResult(result) && result.timedOut) {
|
|
326
|
-
|
|
649
|
+
const timeoutMessage = `Command timed out after ${timeoutSec} seconds`;
|
|
650
|
+
const message = renderedOutput ? `${renderedOutput}\n\n${timeoutMessage}` : timeoutMessage;
|
|
651
|
+
throw new ToolError(formatBashFailureMessage(result, message));
|
|
327
652
|
}
|
|
328
653
|
if (result.exitCode === undefined) {
|
|
329
|
-
throw new ToolError(`${outputText}\n\nCommand failed: missing exit status`);
|
|
654
|
+
throw new ToolError(formatBashFailureMessage(result, `${outputText}\n\nCommand failed: missing exit status`));
|
|
330
655
|
}
|
|
331
656
|
if (result.exitCode !== 0) {
|
|
332
|
-
throw new ToolError(
|
|
657
|
+
throw new ToolError(
|
|
658
|
+
formatBashFailureMessage(result, `${outputText}\n\nCommand exited with code ${result.exitCode}`),
|
|
659
|
+
);
|
|
333
660
|
}
|
|
334
661
|
return outputText;
|
|
335
662
|
}
|
|
@@ -337,9 +664,24 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
337
664
|
#buildCompletedResult(
|
|
338
665
|
result: BashResult | BashInteractiveResult,
|
|
339
666
|
timeoutSec: number,
|
|
340
|
-
options: {
|
|
667
|
+
options: {
|
|
668
|
+
requestedTimeoutSec?: number;
|
|
669
|
+
notices?: readonly string[];
|
|
670
|
+
terminalId?: string;
|
|
671
|
+
artifactReferenceInBody?: boolean;
|
|
672
|
+
} = {},
|
|
341
673
|
): AgentToolResult<BashToolDetails> {
|
|
342
|
-
const
|
|
674
|
+
const shortArtifactInBody = !result.truncated && result.artifactId !== undefined;
|
|
675
|
+
const baseBody =
|
|
676
|
+
options.artifactReferenceInBody || shortArtifactInBody
|
|
677
|
+
? appendArtifactDetails(this.#formatResultOutput(result), result)
|
|
678
|
+
: this.#formatResultOutput(result);
|
|
679
|
+
const writerNotice = artifactWriterFailureNotice(result);
|
|
680
|
+
const body =
|
|
681
|
+
writerNotice && !baseBody.includes(writerNotice)
|
|
682
|
+
? `${baseBody}${baseBody.endsWith("\n") ? "" : "\n"}${writerNotice}`
|
|
683
|
+
: baseBody;
|
|
684
|
+
const outputLines = [body];
|
|
343
685
|
const notices = options.notices?.filter(Boolean) ?? [];
|
|
344
686
|
if (notices.length > 0) outputLines.push("", ...notices);
|
|
345
687
|
const outputText = outputLines.join("\n");
|
|
@@ -350,7 +692,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
350
692
|
if (options.terminalId !== undefined) {
|
|
351
693
|
details.terminalId = options.terminalId;
|
|
352
694
|
}
|
|
353
|
-
const resultBuilder = toolResult(details)
|
|
695
|
+
const resultBuilder = toolResult(details)
|
|
696
|
+
.text(outputText)
|
|
697
|
+
.truncationFromSummary(result, {
|
|
698
|
+
direction: "tail",
|
|
699
|
+
...(shortArtifactInBody ? { noticeOwner: "body" } : {}),
|
|
700
|
+
});
|
|
354
701
|
this.#buildResultText(result, timeoutSec, outputText);
|
|
355
702
|
return resultBuilder.done();
|
|
356
703
|
}
|
|
@@ -425,7 +772,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
425
772
|
label,
|
|
426
773
|
async ({ jobId, signal: runSignal, reportProgress }) => {
|
|
427
774
|
const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
|
|
428
|
-
const
|
|
775
|
+
const artifactPublisher = createBashArtifactPublisher(this.session);
|
|
776
|
+
const spillThreshold = resolveBashOutputSinkTailBytes(this.session.settings);
|
|
777
|
+
const headBytes = resolveBashOutputSinkHeadBytes(this.session.settings);
|
|
778
|
+
const tailBuffer = new TailBuffer(spillThreshold);
|
|
779
|
+
|
|
780
|
+
let executionResult: BashResult | BashInteractiveResult | undefined;
|
|
429
781
|
try {
|
|
430
782
|
const result = await executeBash(options.command, {
|
|
431
783
|
cwd: options.commandCwd,
|
|
@@ -435,6 +787,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
435
787
|
env: options.resolvedEnv,
|
|
436
788
|
artifactPath,
|
|
437
789
|
artifactId,
|
|
790
|
+
artifactPublisher,
|
|
791
|
+
spillThreshold,
|
|
792
|
+
headBytes,
|
|
438
793
|
oneShot: true,
|
|
439
794
|
ignoreShellPrefix: this.session.bashRestrictionProfile === "read-only",
|
|
440
795
|
disableShellSnapshot: this.session.bashRestrictionProfile === "read-only",
|
|
@@ -450,11 +805,15 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
450
805
|
// path above.
|
|
451
806
|
manager.appendOutput(jobId, chunk);
|
|
452
807
|
},
|
|
453
|
-
onMinimizedSave: originalText =>
|
|
808
|
+
onMinimizedSave: async originalText => {
|
|
809
|
+
return saveBashOriginalArtifact(this.session, originalText);
|
|
810
|
+
},
|
|
454
811
|
});
|
|
812
|
+
executionResult = result;
|
|
455
813
|
const finalResult = this.#buildCompletedResult(result, options.timeoutSec, {
|
|
456
814
|
requestedTimeoutSec: options.requestedTimeoutSec,
|
|
457
|
-
notices: options.notices
|
|
815
|
+
notices: options.notices,
|
|
816
|
+
artifactReferenceInBody: true,
|
|
458
817
|
});
|
|
459
818
|
const finalText = this.#extractTextResult(finalResult);
|
|
460
819
|
latestText = finalText;
|
|
@@ -464,7 +823,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
464
823
|
} catch (error) {
|
|
465
824
|
const message = error instanceof Error ? error.message : String(error);
|
|
466
825
|
latestText = message;
|
|
467
|
-
completion.resolve({ kind: "failed", error });
|
|
826
|
+
completion.resolve({ kind: "failed", error, result: executionResult });
|
|
468
827
|
await reportProgress(message, failedDetails(jobId));
|
|
469
828
|
throw error;
|
|
470
829
|
}
|
|
@@ -771,7 +1130,11 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
771
1130
|
label,
|
|
772
1131
|
async ({ jobId: id, signal, reportProgress }) => {
|
|
773
1132
|
const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
|
|
774
|
-
const
|
|
1133
|
+
const artifactPublisher = createBashArtifactPublisher(this.session);
|
|
1134
|
+
const spillThreshold = resolveBashOutputSinkTailBytes(this.session.settings);
|
|
1135
|
+
const headBytes = resolveBashOutputSinkHeadBytes(this.session.settings);
|
|
1136
|
+
const tailBuffer = new TailBuffer(spillThreshold);
|
|
1137
|
+
|
|
775
1138
|
try {
|
|
776
1139
|
const result = await executeBash(prepared.command, {
|
|
777
1140
|
cwd: prepared.commandCwd,
|
|
@@ -781,6 +1144,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
781
1144
|
env: prepared.resolvedEnv,
|
|
782
1145
|
artifactPath,
|
|
783
1146
|
artifactId,
|
|
1147
|
+
artifactPublisher,
|
|
1148
|
+
spillThreshold,
|
|
1149
|
+
headBytes,
|
|
784
1150
|
oneShot: true,
|
|
785
1151
|
ignoreShellPrefix: this.session.bashRestrictionProfile === "read-only",
|
|
786
1152
|
disableShellSnapshot: this.session.bashRestrictionProfile === "read-only",
|
|
@@ -797,11 +1163,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
797
1163
|
cursorOffset = slice.nextOffset;
|
|
798
1164
|
dispatchLines(slice.text);
|
|
799
1165
|
},
|
|
800
|
-
onMinimizedSave: originalText =>
|
|
1166
|
+
onMinimizedSave: async originalText => saveBashOriginalArtifact(this.session, originalText),
|
|
801
1167
|
});
|
|
802
1168
|
flushTrailingLine();
|
|
803
|
-
|
|
804
|
-
|
|
1169
|
+
const resultText = appendArtifactDetails(result.output || "(no output)", result);
|
|
1170
|
+
this.#buildResultText(result, prepared.timeoutSec, resultText);
|
|
1171
|
+
return resultText;
|
|
805
1172
|
} catch (error) {
|
|
806
1173
|
flushTrailingLine();
|
|
807
1174
|
throw error instanceof Error ? error : new Error(String(error));
|
|
@@ -932,8 +1299,14 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
932
1299
|
}
|
|
933
1300
|
if (waitResult.kind === "aborted") {
|
|
934
1301
|
autoBgManager.cancel(job.jobId);
|
|
1302
|
+
const terminal = await job.completion;
|
|
935
1303
|
autoBgManager.acknowledgeDeliveries([job.jobId]);
|
|
936
|
-
|
|
1304
|
+
if (terminal.kind === "failed") {
|
|
1305
|
+
throw new ToolAbortError(
|
|
1306
|
+
formatManagedAbortFailure(terminal.error, terminal.result, job.getLatestText()),
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
throw new ToolAbortError(formatManagedAbortFailure(undefined, undefined, job.getLatestText()));
|
|
937
1310
|
}
|
|
938
1311
|
job.setBackgrounded(true);
|
|
939
1312
|
return this.#buildBackgroundStartResult(job.jobId, job.label, job.getLatestText(), timeoutSec, {
|
|
@@ -943,13 +1316,15 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
943
1316
|
}
|
|
944
1317
|
|
|
945
1318
|
if (clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty) {
|
|
1319
|
+
const clientHeadBytes = resolveBashOutputSinkHeadBytes(this.session.settings);
|
|
1320
|
+
const clientTailBytes = resolveBashOutputSinkTailBytes(this.session.settings);
|
|
946
1321
|
const handle = await clientBridge.createTerminal({
|
|
947
1322
|
command,
|
|
948
1323
|
cwd: commandCwd,
|
|
949
1324
|
env: resolvedEnv
|
|
950
1325
|
? Object.entries(resolvedEnv).map(([name, value]) => ({ name, value: value as string }))
|
|
951
1326
|
: undefined,
|
|
952
|
-
outputByteLimit:
|
|
1327
|
+
outputByteLimit: clientHeadBytes > 0 ? undefined : clientTailBytes,
|
|
953
1328
|
});
|
|
954
1329
|
|
|
955
1330
|
// Emit partial update so the editor can embed the live terminal card.
|
|
@@ -969,13 +1344,13 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
969
1344
|
// arriving mid-poll terminates the remote command immediately,
|
|
970
1345
|
// instead of waiting for the next `currentOutput()` to return.
|
|
971
1346
|
const { promise: abortedP, resolve: resolveAborted } = Promise.withResolvers<void>();
|
|
972
|
-
let
|
|
1347
|
+
let killPromise: Promise<void> | undefined;
|
|
973
1348
|
const fireKill = (): Promise<void> => {
|
|
974
|
-
if (
|
|
975
|
-
|
|
976
|
-
return handle.kill().catch((error: unknown) => {
|
|
1349
|
+
if (killPromise) return killPromise;
|
|
1350
|
+
killPromise = handle.kill().catch((error: unknown) => {
|
|
977
1351
|
logger.warn("ACP terminal kill failed", { terminalId: handle.terminalId, error });
|
|
978
1352
|
});
|
|
1353
|
+
return killPromise;
|
|
979
1354
|
};
|
|
980
1355
|
const onAbortSignal = () => {
|
|
981
1356
|
resolveAborted();
|
|
@@ -987,7 +1362,19 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
987
1362
|
try {
|
|
988
1363
|
if (signal?.aborted) {
|
|
989
1364
|
await fireKill();
|
|
990
|
-
|
|
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
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
const prepared = await prepareClientTerminalOutput(this.session, current);
|
|
1377
|
+
throw new ToolAbortError(formatClientTerminalAbortFailure(prepared, readDiagnostic, pendingNotices));
|
|
991
1378
|
}
|
|
992
1379
|
|
|
993
1380
|
const timeoutPromise = Bun.sleep(timeoutMs).then(() => ({ kind: "timeout" as const }));
|
|
@@ -1005,7 +1392,21 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1005
1392
|
|
|
1006
1393
|
if (raced.kind === "aborted" || signal?.aborted) {
|
|
1007
1394
|
await fireKill();
|
|
1008
|
-
|
|
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", {
|
|
1402
|
+
terminalId: handle.terminalId,
|
|
1403
|
+
error,
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
const prepared = await prepareClientTerminalOutput(this.session, current);
|
|
1407
|
+
throw new ToolAbortError(
|
|
1408
|
+
formatClientTerminalAbortFailure(prepared, readDiagnostic, pendingNotices),
|
|
1409
|
+
);
|
|
1009
1410
|
}
|
|
1010
1411
|
|
|
1011
1412
|
if (raced.kind === "timeout") {
|
|
@@ -1014,29 +1415,33 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1014
1415
|
// enforced timeout. The handle stays valid post-kill so the
|
|
1015
1416
|
// buffered output is still readable.
|
|
1016
1417
|
await fireKill();
|
|
1017
|
-
let current = { output: "", truncated: false };
|
|
1418
|
+
let current: ClientBridgeTerminalOutput = { output: "", truncated: false };
|
|
1419
|
+
let readDiagnostic: string | undefined;
|
|
1018
1420
|
try {
|
|
1019
1421
|
current = await handle.currentOutput();
|
|
1020
1422
|
} catch (error) {
|
|
1423
|
+
readDiagnostic = boundArtifactSaveDiagnostic(error);
|
|
1021
1424
|
logger.warn("ACP terminal final output read failed", {
|
|
1022
1425
|
terminalId: handle.terminalId,
|
|
1023
1426
|
error,
|
|
1024
1427
|
});
|
|
1025
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
|
+
];
|
|
1026
1436
|
const timedOutResult: BashInteractiveResult = {
|
|
1027
|
-
|
|
1437
|
+
...prepared.summary,
|
|
1028
1438
|
exitCode: undefined,
|
|
1029
1439
|
cancelled: false,
|
|
1030
1440
|
timedOut: true,
|
|
1031
|
-
truncated: current.truncated,
|
|
1032
|
-
totalLines: current.output.length > 0 ? current.output.split("\n").length : 0,
|
|
1033
|
-
totalBytes: current.output.length,
|
|
1034
|
-
outputLines: current.output.length > 0 ? current.output.split("\n").length : 0,
|
|
1035
|
-
outputBytes: current.output.length,
|
|
1036
1441
|
};
|
|
1037
1442
|
return this.#buildCompletedResult(timedOutResult, timeoutSec, {
|
|
1038
1443
|
requestedTimeoutSec,
|
|
1039
|
-
notices:
|
|
1444
|
+
notices: timeoutNotices,
|
|
1040
1445
|
terminalId: handle.terminalId,
|
|
1041
1446
|
});
|
|
1042
1447
|
}
|
|
@@ -1058,8 +1463,17 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1058
1463
|
// observe `signal?.aborted` and exit via the abort branch.
|
|
1059
1464
|
continue;
|
|
1060
1465
|
}
|
|
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;
|
|
1061
1475
|
onUpdate?.({
|
|
1062
|
-
content: [{ type: "text", text:
|
|
1476
|
+
content: [{ type: "text", text: pollText }],
|
|
1063
1477
|
details: { terminalId: handle.terminalId },
|
|
1064
1478
|
});
|
|
1065
1479
|
}
|
|
@@ -1067,6 +1481,23 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1067
1481
|
signal?.removeEventListener("abort", onAbortSignal);
|
|
1068
1482
|
}
|
|
1069
1483
|
|
|
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
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
const prepared = await prepareClientTerminalOutput(this.session, current);
|
|
1498
|
+
throw new ToolAbortError(formatClientTerminalAbortFailure(prepared, readDiagnostic, pendingNotices));
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1070
1501
|
// Fetch final output; the terminal is released in the outer finally.
|
|
1071
1502
|
const finalOutput = await handle.currentOutput();
|
|
1072
1503
|
|
|
@@ -1075,24 +1506,18 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1075
1506
|
const exitCode: number | undefined =
|
|
1076
1507
|
rawExitCode != null ? rawExitCode : exitStatus.signal ? 137 : undefined;
|
|
1077
1508
|
|
|
1078
|
-
const
|
|
1079
|
-
const outputByteLen = outputText.length;
|
|
1080
|
-
const outputLineCount = outputText.length > 0 ? outputText.split("\n").length : 0;
|
|
1509
|
+
const prepared = await prepareClientTerminalOutput(this.session, finalOutput);
|
|
1081
1510
|
|
|
1082
1511
|
const bridgeResult: BashResult = {
|
|
1083
|
-
|
|
1512
|
+
...prepared.summary,
|
|
1084
1513
|
exitCode,
|
|
1085
1514
|
cancelled: false,
|
|
1086
|
-
truncated: finalOutput.truncated,
|
|
1087
|
-
totalLines: outputLineCount,
|
|
1088
|
-
totalBytes: outputByteLen,
|
|
1089
|
-
outputLines: outputLineCount,
|
|
1090
|
-
outputBytes: outputByteLen,
|
|
1091
1515
|
};
|
|
1092
1516
|
|
|
1093
1517
|
const bridgeNotices: string[] = [];
|
|
1094
|
-
if (finalOutput.truncated) bridgeNotices.push("(output truncated)");
|
|
1518
|
+
if (finalOutput.truncated || prepared.locallyTruncated) bridgeNotices.push("(output truncated)");
|
|
1095
1519
|
for (const notice of pendingNotices) bridgeNotices.push(notice);
|
|
1520
|
+
if (prepared.artifactSaveNotice) bridgeNotices.push(prepared.artifactSaveNotice);
|
|
1096
1521
|
|
|
1097
1522
|
return this.#buildCompletedResult(bridgeResult, timeoutSec, {
|
|
1098
1523
|
requestedTimeoutSec,
|
|
@@ -1108,11 +1533,15 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1108
1533
|
}
|
|
1109
1534
|
}
|
|
1110
1535
|
|
|
1536
|
+
const spillThreshold = resolveBashOutputSinkTailBytes(this.session.settings);
|
|
1537
|
+
const headBytes = resolveBashOutputSinkHeadBytes(this.session.settings);
|
|
1538
|
+
|
|
1111
1539
|
// Track output for streaming updates (tail only)
|
|
1112
|
-
const tailBuffer = new TailBuffer(
|
|
1540
|
+
const tailBuffer = new TailBuffer(spillThreshold);
|
|
1113
1541
|
|
|
1114
1542
|
// Allocate artifact for truncated output storage
|
|
1115
1543
|
const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
|
|
1544
|
+
const artifactPublisher = createBashArtifactPublisher(this.session);
|
|
1116
1545
|
|
|
1117
1546
|
const interactiveUi =
|
|
1118
1547
|
this.session.bashRestrictionProfile === "read-only"
|
|
@@ -1129,6 +1558,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1129
1558
|
env: resolvedEnv,
|
|
1130
1559
|
artifactPath,
|
|
1131
1560
|
artifactId,
|
|
1561
|
+
artifactPublisher,
|
|
1562
|
+
spillThreshold,
|
|
1563
|
+
headBytes,
|
|
1132
1564
|
})
|
|
1133
1565
|
: await executeBash(command, {
|
|
1134
1566
|
cwd: commandCwd,
|
|
@@ -1139,19 +1571,30 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1139
1571
|
env: resolvedEnv,
|
|
1140
1572
|
artifactPath,
|
|
1141
1573
|
artifactId,
|
|
1574
|
+
artifactPublisher,
|
|
1575
|
+
spillThreshold,
|
|
1576
|
+
headBytes,
|
|
1142
1577
|
onChunk: streamTailUpdates(tailBuffer, onUpdate),
|
|
1143
|
-
onMinimizedSave: originalText =>
|
|
1578
|
+
onMinimizedSave: async originalText => saveBashOriginalArtifact(this.session, originalText),
|
|
1144
1579
|
ignoreShellPrefix: this.session.bashRestrictionProfile === "read-only",
|
|
1145
1580
|
disableShellSnapshot: this.session.bashRestrictionProfile === "read-only",
|
|
1146
1581
|
});
|
|
1147
1582
|
if (result.cancelled) {
|
|
1583
|
+
const failureText = formatBashFailureMessage(
|
|
1584
|
+
result,
|
|
1585
|
+
normalizeResultOutput(result) || "Command aborted",
|
|
1586
|
+
signal?.aborted ? "Command aborted" : undefined,
|
|
1587
|
+
);
|
|
1148
1588
|
if (signal?.aborted) {
|
|
1149
|
-
throw new ToolAbortError(
|
|
1589
|
+
throw new ToolAbortError(failureText);
|
|
1150
1590
|
}
|
|
1151
|
-
throw new ToolError(
|
|
1591
|
+
throw new ToolError(failureText);
|
|
1152
1592
|
}
|
|
1153
1593
|
if (isInteractiveResult(result) && result.timedOut) {
|
|
1154
|
-
|
|
1594
|
+
const timeoutMessage = `Command timed out after ${timeoutSec} seconds`;
|
|
1595
|
+
const output = normalizeResultOutput(result);
|
|
1596
|
+
const failureText = output ? `${output}\n\n${timeoutMessage}` : timeoutMessage;
|
|
1597
|
+
throw new ToolError(formatBashFailureMessage(result, failureText, timeoutMessage));
|
|
1155
1598
|
}
|
|
1156
1599
|
return this.#buildCompletedResult(result, timeoutSec, {
|
|
1157
1600
|
requestedTimeoutSec,
|