@gajae-code/coding-agent 0.15.6 → 0.16.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 +99 -0
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +14 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +27 -8
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +79 -2
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +21 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +9 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +12 -5
- package/dist/types/sdk/cli/session-cli.d.ts +20 -3
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +70 -17
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +3 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +6 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +52 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +79 -2
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +13 -1
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +140 -23
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +135 -28
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +178 -23
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +291 -42
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +27 -8
- package/src/config/settings.ts +51 -15
- package/src/coordinator-mcp/projection-scan.ts +27 -6
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +115 -21
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +213 -14
- package/src/gjc-runtime/session-state-sidecar.ts +938 -39
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/internal-urls/docs-index.generated.ts +15 -15
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +61 -3
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +610 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +30 -2
- package/src/modes/controllers/selector-controller.ts +200 -98
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +5 -0
- package/src/sdk/broker/broker.ts +1646 -27
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +81 -3
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +440 -75
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +591 -0
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +43 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +84 -7
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +41 -15
- package/src/sdk/cli/session-cli.ts +297 -26
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +101 -40
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/events.ts +1 -0
- package/src/sdk/host/host.ts +29 -2
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +673 -14
- package/src/sdk/host/session-runtime.ts +674 -103
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +17 -4
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +105 -3
- package/src/sdk/protocol/operation-registry.ts +14 -4
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +219 -118
- package/src/sdk/session.ts +416 -195
- package/src/sdk/startup-capability.ts +1 -0
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1628 -318
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +850 -97
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +412 -75
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +10 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +72 -1
package/src/tools/bash.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
2
3
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
|
|
3
4
|
import type { Component } from "@gajae-code/tui";
|
|
4
5
|
import { getKeybindings, ImageProtocol, TERMINAL, Text, visibleWidth } from "@gajae-code/tui";
|
|
5
6
|
import { getProjectDir, isEnoent, logger, prompt } from "@gajae-code/utils";
|
|
6
7
|
import * as z from "zod/v4";
|
|
7
|
-
import { AsyncJobManager } from "../async";
|
|
8
|
+
import { AsyncJobManager, type FoldReason } from "../async";
|
|
8
9
|
import { type BashArtifactSaveResult, type BashResult, executeBash } from "../exec/bash-executor";
|
|
9
10
|
|
|
10
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
@@ -17,12 +18,14 @@ import { InternalUrlRouter } from "../internal-urls";
|
|
|
17
18
|
import { truncateToVisualLines } from "../modes/components/visual-truncate";
|
|
18
19
|
import { highlightCode, type Theme } from "../modes/theme/theme";
|
|
19
20
|
import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
|
|
21
|
+
import { renderSpawnTable, runSdkSpawn, type SdkSpawnArgs } from "../sdk/cli/master-cli";
|
|
20
22
|
import type { ArtifactManager } from "../session/artifacts";
|
|
21
23
|
import type {
|
|
22
24
|
ClientBridgeTerminalExitStatus,
|
|
23
25
|
ClientBridgeTerminalHandle,
|
|
24
26
|
ClientBridgeTerminalOutput,
|
|
25
27
|
} from "../session/client-bridge";
|
|
28
|
+
import type { FoldAdapter } from "../session/fold-coordinator";
|
|
26
29
|
import {
|
|
27
30
|
DEFAULT_ARTIFACT_MAX_BYTES,
|
|
28
31
|
OutputSink,
|
|
@@ -83,10 +86,23 @@ function sliceTextAfterUtf8ByteOffset(text: string, offsetBytes: number): string
|
|
|
83
86
|
import { clampTimeout, TOOL_TIMEOUTS } from "./tool-timeouts";
|
|
84
87
|
|
|
85
88
|
export const BASH_DEFAULT_PREVIEW_LINES = 10;
|
|
89
|
+
/**
|
|
90
|
+
* A user steer folds a running foreground bash call only after it has been
|
|
91
|
+
* running at least this long. Shorter commands finish normally and the steer is
|
|
92
|
+
* consumed at the ordinary tool boundary, so a quick `git status` never turns
|
|
93
|
+
* into a background job plus a wake-up turn.
|
|
94
|
+
*/
|
|
95
|
+
export const STEER_FOLD_GRACE_MS = 2_000;
|
|
96
|
+
|
|
97
|
+
/** Model-facing line appended to a steer-folded background-start result. */
|
|
98
|
+
export function steerFoldReasonLine(jobId: string): string {
|
|
99
|
+
return `Folded into background job ${jobId} because a user steer arrived; the command keeps running with its original timeout and its result will wake a later turn.`;
|
|
100
|
+
}
|
|
86
101
|
|
|
87
102
|
const BASH_ERROR_MAX_BYTES = 4096;
|
|
88
103
|
const ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES = 256;
|
|
89
104
|
const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
105
|
+
const MASTER_CAPABILITY_ENV = "GJC_MASTER_CAPABILITY";
|
|
90
106
|
const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
|
|
91
107
|
const ACP_RELEASE_TIMEOUT_MS = 1_000;
|
|
92
108
|
const READ_ONLY_BASH_ENV: Record<string, string> = {
|
|
@@ -485,6 +501,8 @@ export interface BashToolDetails {
|
|
|
485
501
|
timeoutSeconds?: number;
|
|
486
502
|
requestedTimeoutSeconds?: number;
|
|
487
503
|
terminalId?: string;
|
|
504
|
+
/** Why the foreground wait was folded, when this result is a background-start after a fold. */
|
|
505
|
+
foldReason?: FoldReason;
|
|
488
506
|
async?: {
|
|
489
507
|
state: "running" | "completed" | "failed";
|
|
490
508
|
jobId: string;
|
|
@@ -601,6 +619,107 @@ function normalizeBashEnv(env: Record<string, string> | undefined): Record<strin
|
|
|
601
619
|
return normalized;
|
|
602
620
|
}
|
|
603
621
|
|
|
622
|
+
/**
|
|
623
|
+
* Return true only for a direct, shell-syntax-free `gjc sdk spawn` command.
|
|
624
|
+
*
|
|
625
|
+
* Master authority is process-local and must not become ambient state for an
|
|
626
|
+
* arbitrary shell pipeline. Keep a tiny lexer here instead of a prefix check:
|
|
627
|
+
* shell quoting is allowed for ordinary argument values, while operators,
|
|
628
|
+
* expansions, escapes, globbing, and line breaks all refuse the privileged
|
|
629
|
+
* environment injection path.
|
|
630
|
+
*/
|
|
631
|
+
function strictDirectSdkSpawnTokens(command: string): string[] | undefined {
|
|
632
|
+
if (command.length === 0) return undefined;
|
|
633
|
+
const tokens: string[] = [];
|
|
634
|
+
let token = "";
|
|
635
|
+
let tokenStarted = false;
|
|
636
|
+
let quote: "'" | '"' | undefined;
|
|
637
|
+
for (const character of command) {
|
|
638
|
+
if (quote !== undefined) {
|
|
639
|
+
if (character === quote) {
|
|
640
|
+
quote = undefined;
|
|
641
|
+
tokenStarted = true;
|
|
642
|
+
} else {
|
|
643
|
+
token += character;
|
|
644
|
+
tokenStarted = true;
|
|
645
|
+
}
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (";&|<>()$`\\*?[]{}#!\n\r".includes(character)) return undefined;
|
|
649
|
+
if (character === "'" || character === '"') {
|
|
650
|
+
quote = character;
|
|
651
|
+
tokenStarted = true;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (/\s/u.test(character)) {
|
|
655
|
+
if (tokenStarted) {
|
|
656
|
+
tokens.push(token);
|
|
657
|
+
token = "";
|
|
658
|
+
tokenStarted = false;
|
|
659
|
+
}
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
token += character;
|
|
663
|
+
tokenStarted = true;
|
|
664
|
+
}
|
|
665
|
+
if (quote !== undefined) return undefined;
|
|
666
|
+
if (tokenStarted) tokens.push(token);
|
|
667
|
+
return tokens.length >= 3 && tokens[0] === "gjc" && tokens[1] === "sdk" && tokens[2] === "spawn"
|
|
668
|
+
? tokens
|
|
669
|
+
: undefined;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export function isStrictDirectSdkSpawnCommand(command: string): boolean {
|
|
673
|
+
return strictDirectSdkSpawnTokens(command) !== undefined;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export function parseDirectSdkSpawnArgs(command: string): SdkSpawnArgs {
|
|
677
|
+
const tokens = strictDirectSdkSpawnTokens(command);
|
|
678
|
+
if (tokens === undefined) throw new ToolError("Master spawn command is not a direct shell-safe invocation.");
|
|
679
|
+
const args: SdkSpawnArgs = {};
|
|
680
|
+
for (let index = 3; index < tokens.length; index++) {
|
|
681
|
+
const token = tokens[index]!;
|
|
682
|
+
if (token === "--json") {
|
|
683
|
+
args.json = true;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
const equals = token.indexOf("=");
|
|
687
|
+
const name = equals < 0 ? token : token.slice(0, equals);
|
|
688
|
+
const value = equals < 0 ? tokens[++index] : token.slice(equals + 1);
|
|
689
|
+
if (value === undefined || value.length === 0) throw new ToolError(`Master spawn flag ${name} requires a value.`);
|
|
690
|
+
switch (name) {
|
|
691
|
+
case "--cwd":
|
|
692
|
+
args.cwd = value;
|
|
693
|
+
break;
|
|
694
|
+
case "--prompt":
|
|
695
|
+
args.prompt = value;
|
|
696
|
+
break;
|
|
697
|
+
case "--model":
|
|
698
|
+
args.model = value;
|
|
699
|
+
break;
|
|
700
|
+
case "--profile":
|
|
701
|
+
args.profile = value;
|
|
702
|
+
break;
|
|
703
|
+
case "--idempotency-key":
|
|
704
|
+
args.idempotencyKey = value;
|
|
705
|
+
break;
|
|
706
|
+
case "--agent-dir":
|
|
707
|
+
throw new ToolError("Master spawn cannot override the session agent directory.");
|
|
708
|
+
default:
|
|
709
|
+
throw new ToolError(`Unsupported master spawn flag: ${name}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return args;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export function masterCommandEnvOverrides(
|
|
716
|
+
env: Record<string, string> | undefined,
|
|
717
|
+
directMasterSpawn: boolean,
|
|
718
|
+
): Record<string, string> {
|
|
719
|
+
if (directMasterSpawn) return {};
|
|
720
|
+
return Object.fromEntries(Object.entries(env ?? {}).filter(([key]) => key !== MASTER_CAPABILITY_ENV));
|
|
721
|
+
}
|
|
722
|
+
|
|
604
723
|
function escapeBashEnvValueForDisplay(value: string): string {
|
|
605
724
|
return value
|
|
606
725
|
.replaceAll("\\", "\\\\")
|
|
@@ -830,7 +949,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
830
949
|
label: string,
|
|
831
950
|
previewText: string,
|
|
832
951
|
timeoutSec: number,
|
|
833
|
-
options: {
|
|
952
|
+
options: {
|
|
953
|
+
requestedTimeoutSec?: number;
|
|
954
|
+
notices?: readonly string[];
|
|
955
|
+
terminalId?: string;
|
|
956
|
+
foldReason?: FoldReason;
|
|
957
|
+
} = {},
|
|
834
958
|
): AgentToolResult<BashToolDetails> {
|
|
835
959
|
const details: BashToolDetails = {
|
|
836
960
|
timeoutSeconds: timeoutSec,
|
|
@@ -842,6 +966,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
842
966
|
if (options.requestedTimeoutSec !== undefined && options.requestedTimeoutSec !== timeoutSec) {
|
|
843
967
|
details.requestedTimeoutSeconds = options.requestedTimeoutSec;
|
|
844
968
|
}
|
|
969
|
+
if (options.foldReason !== undefined) details.foldReason = options.foldReason;
|
|
845
970
|
const lines: string[] = [];
|
|
846
971
|
const trimmedPreview = previewText.trimEnd();
|
|
847
972
|
if (trimmedPreview.length > 0) {
|
|
@@ -851,6 +976,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
851
976
|
lines.push(...options.notices, "");
|
|
852
977
|
}
|
|
853
978
|
lines.push(`Background job ${jobId} started: ${label}`);
|
|
979
|
+
if (options.foldReason === "steer") lines.push(steerFoldReasonLine(jobId));
|
|
854
980
|
lines.push("Result will be delivered automatically when complete.");
|
|
855
981
|
lines.push(
|
|
856
982
|
`You can use \`job\` to poll until complete, but prefer to continue with another task in the meanwhile if it's not blocking.`,
|
|
@@ -874,6 +1000,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
874
1000
|
notices?: readonly string[];
|
|
875
1001
|
|
|
876
1002
|
resolvedEnv?: Record<string, string>;
|
|
1003
|
+
directMasterSpawn: boolean;
|
|
877
1004
|
onUpdate?: AgentToolUpdateCallback<BashToolDetails>;
|
|
878
1005
|
startBackgrounded: boolean;
|
|
879
1006
|
onCompletion?: () => void;
|
|
@@ -885,7 +1012,11 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
885
1012
|
throw new ToolError("Background job manager unavailable for this session.");
|
|
886
1013
|
}
|
|
887
1014
|
|
|
888
|
-
const label = options.
|
|
1015
|
+
const label = options.directMasterSpawn
|
|
1016
|
+
? "gjc sdk spawn"
|
|
1017
|
+
: options.command.length > 120
|
|
1018
|
+
? `${options.command.slice(0, 117)}...`
|
|
1019
|
+
: options.command;
|
|
889
1020
|
let latestText = "";
|
|
890
1021
|
let backgrounded = options.startBackgrounded;
|
|
891
1022
|
const runningDetails = (jobId: string): Record<string, unknown> | undefined =>
|
|
@@ -908,37 +1039,37 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
908
1039
|
|
|
909
1040
|
let executionResult: BashResult | BashInteractiveResult | undefined;
|
|
910
1041
|
try {
|
|
911
|
-
const result =
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1042
|
+
const result = options.directMasterSpawn
|
|
1043
|
+
? await this.#runDirectMasterSpawn(
|
|
1044
|
+
options.command,
|
|
1045
|
+
options.commandCwd,
|
|
1046
|
+
options.timeoutMs,
|
|
1047
|
+
runSignal,
|
|
1048
|
+
options.resolvedEnv,
|
|
1049
|
+
)
|
|
1050
|
+
: await executeBash(options.command, {
|
|
1051
|
+
cwd: options.commandCwd,
|
|
1052
|
+
settings: this.session.settings,
|
|
1053
|
+
sessionKey: `${this.session.getSessionId?.() ?? ""}:async:${jobId}`,
|
|
1054
|
+
timeout: options.timeoutMs,
|
|
1055
|
+
signal: runSignal,
|
|
1056
|
+
env: options.resolvedEnv,
|
|
1057
|
+
artifactPath,
|
|
1058
|
+
artifactId,
|
|
1059
|
+
artifactPublisher,
|
|
1060
|
+
spillThreshold,
|
|
1061
|
+
headBytes,
|
|
1062
|
+
oneShot: true,
|
|
1063
|
+
ignoreShellPrefix: this.session.bashRestrictionProfile === "read-only",
|
|
1064
|
+
disableShellSnapshot: this.session.bashRestrictionProfile === "read-only",
|
|
1065
|
+
onChunk: chunk => {
|
|
1066
|
+
tailBuffer.append(chunk);
|
|
1067
|
+
latestText = tailBuffer.text();
|
|
1068
|
+
void reportProgress(latestText, runningDetails(jobId));
|
|
1069
|
+
},
|
|
1070
|
+
onRawChunk: chunk => manager.appendOutput(jobId, chunk),
|
|
1071
|
+
onMinimizedSave: async originalText => saveBashOriginalArtifact(this.session, originalText),
|
|
1072
|
+
});
|
|
942
1073
|
executionResult = result;
|
|
943
1074
|
const finalResult = this.#buildCompletedResult(result, options.timeoutSec, {
|
|
944
1075
|
requestedTimeoutSec: options.requestedTimeoutSec,
|
|
@@ -1006,24 +1137,107 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1006
1137
|
return AsyncJobManager.forEndpoint(endpointId) ?? AsyncJobManager.instance();
|
|
1007
1138
|
}
|
|
1008
1139
|
|
|
1140
|
+
/**
|
|
1141
|
+
* Whether a queued user steer may fold the running foreground wait. Mirrors
|
|
1142
|
+
* the loop's own steer admission: `busyPromptMode=queue` never admits a
|
|
1143
|
+
* steer into the busy run, and `toolInterruptPolicy=finish_tools` explicitly
|
|
1144
|
+
* lets the tool finish, so neither may fold. The
|
|
1145
|
+
* auto-background setting is deliberately not consulted: like the chord,
|
|
1146
|
+
* a steer fold is a user action.
|
|
1147
|
+
*
|
|
1148
|
+
* Fails closed: a session that cannot prove its tool interrupt policy or
|
|
1149
|
+
* report newly admitted steering is not steer-foldable.
|
|
1150
|
+
*/
|
|
1151
|
+
#steerFoldEnabled(): boolean {
|
|
1152
|
+
const { waitForUserSteering, getToolInterruptPolicy, requestForegroundBashBackground } = this.session;
|
|
1153
|
+
if (!waitForUserSteering || !getToolInterruptPolicy || !requestForegroundBashBackground) return false;
|
|
1154
|
+
if (this.session.settings.get("busyPromptMode") !== "steer") return false;
|
|
1155
|
+
return getToolInterruptPolicy() === "abort_tools";
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Fold `adapter` on the first user steer that ARRIVES after the wait has run
|
|
1160
|
+
* for {@link STEER_FOLD_GRACE_MS} since `startedAt`. A steer already queued
|
|
1161
|
+
* when the wait starts, or one that arrives inside the grace window, never
|
|
1162
|
+
* folds: the command finishes normally and that steer is consumed at the
|
|
1163
|
+
* ordinary tool boundary. The watcher keeps observing so a later qualifying
|
|
1164
|
+
* steer still folds, and re-checks the gates at that moment so a
|
|
1165
|
+
* `busyPromptMode`/`toolInterruptPolicy` change during the command is honored.
|
|
1166
|
+
* Returns a stop function; call it when the wait settles. No-op when steer
|
|
1167
|
+
* folding is gated off at start.
|
|
1168
|
+
*/
|
|
1169
|
+
#watchSteerForFold(adapter: FoldAdapter, startedAt: number): () => void {
|
|
1170
|
+
if (!this.#steerFoldEnabled()) return () => {};
|
|
1171
|
+
const waitForSteer = this.session.waitForUserSteering;
|
|
1172
|
+
const requestFold = this.session.requestForegroundBashBackground;
|
|
1173
|
+
if (!waitForSteer || !requestFold) return () => {};
|
|
1174
|
+
const watch = new AbortController();
|
|
1175
|
+
const observe = async (): Promise<void> => {
|
|
1176
|
+
while (!watch.signal.aborted) {
|
|
1177
|
+
await waitForSteer(watch.signal);
|
|
1178
|
+
if (watch.signal.aborted) return;
|
|
1179
|
+
if (Date.now() - startedAt < STEER_FOLD_GRACE_MS) continue;
|
|
1180
|
+
if (!this.#steerFoldEnabled()) continue;
|
|
1181
|
+
await requestFold("steer", adapter);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
observe().catch((error: unknown) => {
|
|
1186
|
+
logger.warn("Steer-triggered fold failed", {
|
|
1187
|
+
jobId: adapter.jobId,
|
|
1188
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
1191
|
+
return () => watch.abort();
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Race a managed job against the auto-background threshold, an explicit fold
|
|
1196
|
+
* (chord / SDK control / steer), and abort. A steer that arrives once the
|
|
1197
|
+
* command has run for {@link STEER_FOLD_GRACE_MS} requests a `steer` fold,
|
|
1198
|
+
* which settles this wait through the same `backgroundRequest` path as the
|
|
1199
|
+
* chord; a steer inside the grace window is ignored here and consumed at the
|
|
1200
|
+
* ordinary tool boundary once the command finishes.
|
|
1201
|
+
*/
|
|
1009
1202
|
async #waitForManagedBashJob(
|
|
1010
1203
|
job: ManagedBashJobHandle,
|
|
1011
1204
|
thresholdMs: number,
|
|
1012
1205
|
signal?: AbortSignal,
|
|
1013
|
-
backgroundRequest?: Promise<
|
|
1014
|
-
|
|
1206
|
+
backgroundRequest?: Promise<FoldReason>,
|
|
1207
|
+
foldAdapter?: FoldAdapter,
|
|
1208
|
+
): Promise<ManagedBashJobCompletion | { kind: "running"; reason: FoldReason } | { kind: "aborted" }> {
|
|
1015
1209
|
if (signal?.aborted) {
|
|
1016
1210
|
return { kind: "aborted" };
|
|
1017
1211
|
}
|
|
1018
1212
|
|
|
1019
|
-
const
|
|
1020
|
-
const
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1213
|
+
const startedAt = Date.now();
|
|
1214
|
+
const threshold = Promise.withResolvers<{ kind: "running"; reason: FoldReason }>();
|
|
1215
|
+
const thresholdTimer = setTimeout(
|
|
1216
|
+
() => {
|
|
1217
|
+
const requestFold = this.session.requestForegroundBashBackground;
|
|
1218
|
+
if (!foldAdapter || !requestFold) {
|
|
1219
|
+
threshold.resolve({ kind: "running", reason: "timer" });
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
void requestFold("timer", foldAdapter)
|
|
1223
|
+
.then(folded => {
|
|
1224
|
+
if (!folded) threshold.resolve({ kind: "running", reason: "timer" });
|
|
1225
|
+
})
|
|
1226
|
+
.catch(error => {
|
|
1227
|
+
logger.warn("Timer-triggered fold failed", {
|
|
1228
|
+
jobId: foldAdapter.jobId,
|
|
1229
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1230
|
+
});
|
|
1231
|
+
threshold.resolve({ kind: "running", reason: "timer" });
|
|
1232
|
+
});
|
|
1233
|
+
},
|
|
1234
|
+
Math.max(0, thresholdMs),
|
|
1235
|
+
);
|
|
1236
|
+
const waiters: Array<
|
|
1237
|
+
Promise<ManagedBashJobCompletion | { kind: "running"; reason: FoldReason } | { kind: "aborted" }>
|
|
1238
|
+
> = [job.completion, threshold.promise];
|
|
1025
1239
|
if (backgroundRequest) {
|
|
1026
|
-
waiters.push(backgroundRequest.then(
|
|
1240
|
+
waiters.push(backgroundRequest.then(reason => ({ kind: "running" as const, reason })));
|
|
1027
1241
|
}
|
|
1028
1242
|
|
|
1029
1243
|
let onAbort: (() => void) | undefined;
|
|
@@ -1033,9 +1247,13 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1033
1247
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1034
1248
|
waiters.push(aborted.promise);
|
|
1035
1249
|
}
|
|
1250
|
+
|
|
1251
|
+
const stopSteerWatch =
|
|
1252
|
+
backgroundRequest && foldAdapter ? this.#watchSteerForFold(foldAdapter, startedAt) : () => {};
|
|
1036
1253
|
try {
|
|
1037
1254
|
return await Promise.race(waiters);
|
|
1038
1255
|
} finally {
|
|
1256
|
+
stopSteerWatch();
|
|
1039
1257
|
clearTimeout(thresholdTimer);
|
|
1040
1258
|
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
1041
1259
|
}
|
|
@@ -1061,6 +1279,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1061
1279
|
command: string;
|
|
1062
1280
|
commandCwd: string;
|
|
1063
1281
|
resolvedEnv: Record<string, string>;
|
|
1282
|
+
directMasterSpawn: boolean;
|
|
1064
1283
|
requestedTimeoutSec: number;
|
|
1065
1284
|
timeoutSec: number;
|
|
1066
1285
|
timeoutMs: number;
|
|
@@ -1206,14 +1425,26 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1206
1425
|
// silently ignore the caller's override.
|
|
1207
1426
|
const explicitAgentDirOverride =
|
|
1208
1427
|
expandedEnv?.GJC_CODING_AGENT_DIR !== undefined || expandedEnv?.PI_CODING_AGENT_DIR !== undefined;
|
|
1428
|
+
// The master capability is a one-shot authority for the direct spawn CLI,
|
|
1429
|
+
// never ambient state for ordinary Bash or a chained/pipelined descendant.
|
|
1430
|
+
// Check both spellings so cwd normalization or URL expansion cannot turn a
|
|
1431
|
+
// command that contained shell syntax into a privileged one.
|
|
1432
|
+
const directMasterSpawn = isStrictDirectSdkSpawnCommand(rawCommand) && isStrictDirectSdkSpawnCommand(command);
|
|
1433
|
+
const masterCapability = directMasterSpawn ? this.session.getMasterBashCapability?.() : undefined;
|
|
1434
|
+
const masterOwnerSessionId = directMasterSpawn ? this.session.getMasterOwnerSessionId?.() : undefined;
|
|
1435
|
+
const commandEnvOverrides = masterCommandEnvOverrides(expandedEnv, directMasterSpawn);
|
|
1209
1436
|
const resolvedEnv = {
|
|
1210
1437
|
...buildGjcRuntimeSessionEnv({
|
|
1211
1438
|
sessionFile: null,
|
|
1212
1439
|
sessionId: this.session.getSessionId?.(),
|
|
1213
1440
|
cwd: this.session.cwd,
|
|
1214
1441
|
}),
|
|
1215
|
-
...(sessionAgentDir && !explicitAgentDirOverride
|
|
1216
|
-
|
|
1442
|
+
...(sessionAgentDir && (!explicitAgentDirOverride || directMasterSpawn)
|
|
1443
|
+
? { GJC_CODING_AGENT_DIR: sessionAgentDir }
|
|
1444
|
+
: {}),
|
|
1445
|
+
...commandEnvOverrides,
|
|
1446
|
+
...(masterCapability ? { [MASTER_CAPABILITY_ENV]: masterCapability } : {}),
|
|
1447
|
+
...(masterOwnerSessionId ? { GJC_MASTER_OWNER_SESSION_ID: masterOwnerSessionId } : {}),
|
|
1217
1448
|
...(this.session.bashRestrictionProfile === "read-only" ? READ_ONLY_BASH_ENV : {}),
|
|
1218
1449
|
...(allowedPrefixes && allowedPrefixes.length > 0 ? { [GJC_RESTRICTED_ROLE_AGENT_BASH_ENV]: "1" } : {}),
|
|
1219
1450
|
};
|
|
@@ -1245,7 +1476,80 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1245
1476
|
const sleepAdvisory = longSleepAdvisory(command, timeoutSec);
|
|
1246
1477
|
if (sleepAdvisory) notices.push(sleepAdvisory);
|
|
1247
1478
|
|
|
1248
|
-
return {
|
|
1479
|
+
return {
|
|
1480
|
+
command,
|
|
1481
|
+
commandCwd,
|
|
1482
|
+
resolvedEnv,
|
|
1483
|
+
directMasterSpawn,
|
|
1484
|
+
requestedTimeoutSec,
|
|
1485
|
+
timeoutSec,
|
|
1486
|
+
timeoutMs,
|
|
1487
|
+
notices,
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
async #runDirectMasterSpawn(
|
|
1492
|
+
command: string,
|
|
1493
|
+
commandCwd: string,
|
|
1494
|
+
timeoutMs: number,
|
|
1495
|
+
signal: AbortSignal | undefined,
|
|
1496
|
+
resolvedEnv: Record<string, string> | undefined,
|
|
1497
|
+
): Promise<BashResult> {
|
|
1498
|
+
const masterCapability = resolvedEnv?.[MASTER_CAPABILITY_ENV];
|
|
1499
|
+
const sessionAgentDir = resolvedEnv?.GJC_CODING_AGENT_DIR;
|
|
1500
|
+
if (masterCapability === undefined || sessionAgentDir === undefined)
|
|
1501
|
+
throw new ToolError("Master spawn authority context is unavailable.");
|
|
1502
|
+
const args = parseDirectSdkSpawnArgs(command);
|
|
1503
|
+
args.agentDir = sessionAgentDir;
|
|
1504
|
+
if (args.cwd !== undefined && !path.isAbsolute(args.cwd)) args.cwd = path.resolve(commandCwd, args.cwd);
|
|
1505
|
+
const request = runSdkSpawn(args, {
|
|
1506
|
+
env: {
|
|
1507
|
+
...process.env,
|
|
1508
|
+
GJC_MASTER_CAPABILITY: masterCapability,
|
|
1509
|
+
GJC_SESSION_ID: resolvedEnv?.GJC_MASTER_OWNER_SESSION_ID ?? this.session.getSessionId?.() ?? undefined,
|
|
1510
|
+
GJC_CODING_AGENT_DIR: sessionAgentDir,
|
|
1511
|
+
},
|
|
1512
|
+
});
|
|
1513
|
+
const timeout = Bun.sleep(timeoutMs).then(() => {
|
|
1514
|
+
throw new ToolError(`Command timed out after ${Math.ceil(timeoutMs / 1000)} seconds`);
|
|
1515
|
+
});
|
|
1516
|
+
const aborted = Promise.withResolvers<never>();
|
|
1517
|
+
const onAbort = (): void => aborted.reject(new ToolAbortError("Command aborted"));
|
|
1518
|
+
if (signal?.aborted) onAbort();
|
|
1519
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
1520
|
+
let result: Awaited<typeof request>;
|
|
1521
|
+
try {
|
|
1522
|
+
result = await Promise.race([request, timeout, aborted.promise]);
|
|
1523
|
+
} finally {
|
|
1524
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1525
|
+
}
|
|
1526
|
+
const output = args.json ? JSON.stringify(result.rendered) : renderSpawnTable(result.rendered);
|
|
1527
|
+
const outputBytes = Buffer.byteLength(output, "utf8");
|
|
1528
|
+
const outputLines = output.split("\n").length;
|
|
1529
|
+
return {
|
|
1530
|
+
output,
|
|
1531
|
+
exitCode: result.exitCode,
|
|
1532
|
+
cancelled: false,
|
|
1533
|
+
truncated: false,
|
|
1534
|
+
totalLines: outputLines,
|
|
1535
|
+
totalBytes: outputBytes,
|
|
1536
|
+
outputLines,
|
|
1537
|
+
outputBytes,
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
async #executeDirectMasterSpawn(
|
|
1542
|
+
command: string,
|
|
1543
|
+
commandCwd: string,
|
|
1544
|
+
timeoutSec: number,
|
|
1545
|
+
timeoutMs: number,
|
|
1546
|
+
requestedTimeoutSec: number,
|
|
1547
|
+
notices: readonly string[],
|
|
1548
|
+
signal: AbortSignal | undefined,
|
|
1549
|
+
resolvedEnv: Record<string, string>,
|
|
1550
|
+
): Promise<AgentToolResult<BashToolDetails>> {
|
|
1551
|
+
const result = await this.#runDirectMasterSpawn(command, commandCwd, timeoutMs, signal, resolvedEnv);
|
|
1552
|
+
return this.#buildCompletedResult(result, timeoutSec, { requestedTimeoutSec, notices });
|
|
1249
1553
|
}
|
|
1250
1554
|
|
|
1251
1555
|
/**
|
|
@@ -1343,8 +1647,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1343
1647
|
spillThreshold,
|
|
1344
1648
|
headBytes,
|
|
1345
1649
|
oneShot: true,
|
|
1346
|
-
ignoreShellPrefix: this.session.bashRestrictionProfile === "read-only",
|
|
1347
|
-
disableShellSnapshot:
|
|
1650
|
+
ignoreShellPrefix: this.session.bashRestrictionProfile === "read-only" || prepared.directMasterSpawn,
|
|
1651
|
+
disableShellSnapshot:
|
|
1652
|
+
this.session.bashRestrictionProfile === "read-only" || prepared.directMasterSpawn,
|
|
1348
1653
|
onChunk: chunk => {
|
|
1349
1654
|
tailBuffer.append(chunk);
|
|
1350
1655
|
void reportProgress(tailBuffer.text(), {
|
|
@@ -1416,11 +1721,25 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1416
1721
|
command,
|
|
1417
1722
|
commandCwd,
|
|
1418
1723
|
resolvedEnv,
|
|
1724
|
+
directMasterSpawn,
|
|
1419
1725
|
requestedTimeoutSec,
|
|
1420
1726
|
timeoutSec,
|
|
1421
1727
|
timeoutMs,
|
|
1422
1728
|
notices: pendingNotices,
|
|
1423
1729
|
} = prepared;
|
|
1730
|
+
const masterCapability = directMasterSpawn ? resolvedEnv[MASTER_CAPABILITY_ENV] : undefined;
|
|
1731
|
+
if (directMasterSpawn && masterCapability !== undefined && !asyncRequested) {
|
|
1732
|
+
return await this.#executeDirectMasterSpawn(
|
|
1733
|
+
command,
|
|
1734
|
+
commandCwd,
|
|
1735
|
+
timeoutSec,
|
|
1736
|
+
timeoutMs,
|
|
1737
|
+
requestedTimeoutSec,
|
|
1738
|
+
pendingNotices,
|
|
1739
|
+
signal,
|
|
1740
|
+
resolvedEnv,
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1424
1743
|
|
|
1425
1744
|
if (asyncRequested) {
|
|
1426
1745
|
// Availability is endpoint-first: a concurrent top-level session
|
|
@@ -1440,13 +1759,14 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1440
1759
|
notices: pendingNotices,
|
|
1441
1760
|
|
|
1442
1761
|
resolvedEnv,
|
|
1762
|
+
directMasterSpawn,
|
|
1443
1763
|
onUpdate,
|
|
1444
1764
|
startBackgrounded: true,
|
|
1445
1765
|
toolCallId,
|
|
1446
1766
|
});
|
|
1447
1767
|
const jobGeneration = asyncManager.getJob(job.jobId)?.generation ?? job.jobId;
|
|
1448
1768
|
job.setBackgrounded(true);
|
|
1449
|
-
asyncManager.
|
|
1769
|
+
asyncManager.markStartedInBackground(job.jobId, jobGeneration);
|
|
1450
1770
|
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec, {
|
|
1451
1771
|
requestedTimeoutSec,
|
|
1452
1772
|
notices: pendingNotices,
|
|
@@ -1457,7 +1777,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1457
1777
|
// Skip when pty=true (PTY needs the local terminal UI).
|
|
1458
1778
|
const clientBridge =
|
|
1459
1779
|
this.session.bashRestrictionProfile === "read-only" ? undefined : this.session.getClientBridge?.();
|
|
1460
|
-
const clientTerminalActive = Boolean(
|
|
1780
|
+
const clientTerminalActive = Boolean(
|
|
1781
|
+
!directMasterSpawn && clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty,
|
|
1782
|
+
);
|
|
1461
1783
|
|
|
1462
1784
|
// Run non-PTY bash through the managed job path so Ctrl+B-twice fold-on-demand works
|
|
1463
1785
|
// even when auto-background is disabled. When a client terminal will handle the
|
|
@@ -1487,6 +1809,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1487
1809
|
notices: pendingNotices,
|
|
1488
1810
|
|
|
1489
1811
|
resolvedEnv,
|
|
1812
|
+
directMasterSpawn,
|
|
1490
1813
|
onUpdate,
|
|
1491
1814
|
startBackgrounded,
|
|
1492
1815
|
onCompletion: () => {
|
|
@@ -1497,13 +1820,13 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1497
1820
|
const jobGeneration = ownedManager.getJob(job.jobId)?.generation ?? job.jobId;
|
|
1498
1821
|
if (startBackgrounded) {
|
|
1499
1822
|
job.setBackgrounded(true);
|
|
1500
|
-
ownedManager.
|
|
1823
|
+
ownedManager.markStartedInBackground(job.jobId, jobGeneration);
|
|
1501
1824
|
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec, {
|
|
1502
1825
|
requestedTimeoutSec,
|
|
1503
1826
|
notices: pendingNotices,
|
|
1504
1827
|
});
|
|
1505
1828
|
}
|
|
1506
|
-
const backgroundRequest = Promise.withResolvers<
|
|
1829
|
+
const backgroundRequest = Promise.withResolvers<FoldReason>();
|
|
1507
1830
|
// Exactly one party may settle the foreground caller. detachObserver (the
|
|
1508
1831
|
// fold won) and resolveForegroundObserver (a completion was handed back
|
|
1509
1832
|
// after a failed fold) share this flag, so a race cannot double-settle.
|
|
@@ -1513,7 +1836,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1513
1836
|
foregroundSettled = true;
|
|
1514
1837
|
return "resolved";
|
|
1515
1838
|
};
|
|
1516
|
-
const
|
|
1839
|
+
const managedFoldAdapter: FoldAdapter = {
|
|
1517
1840
|
kind: "bash-managed",
|
|
1518
1841
|
jobId: job.jobId,
|
|
1519
1842
|
jobGeneration,
|
|
@@ -1532,24 +1855,26 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1532
1855
|
const current = ownedManager.getJob(job.jobId);
|
|
1533
1856
|
return current?.generation === jobGeneration ? current : undefined;
|
|
1534
1857
|
},
|
|
1535
|
-
detachObserver:
|
|
1858
|
+
detachObserver: receipt => {
|
|
1536
1859
|
const outcome = settleForeground();
|
|
1537
1860
|
if (outcome === "resolved") {
|
|
1538
1861
|
job.setBackgrounded(true);
|
|
1539
|
-
ownedManager.markBackgrounded(job.jobId, jobGeneration);
|
|
1540
|
-
backgroundRequest.resolve();
|
|
1862
|
+
ownedManager.markBackgrounded(job.jobId, jobGeneration, receipt.reason);
|
|
1863
|
+
backgroundRequest.resolve(receipt.reason);
|
|
1541
1864
|
}
|
|
1542
1865
|
return outcome;
|
|
1543
1866
|
},
|
|
1544
1867
|
resolveForegroundObserver: () => settleForeground(),
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1868
|
+
};
|
|
1869
|
+
const unregisterBackgroundRequest = this.session.registerForegroundFoldParticipant?.(managedFoldAdapter);
|
|
1870
|
+
let waitResult: ManagedBashJobCompletion | { kind: "running"; reason: FoldReason } | { kind: "aborted" };
|
|
1547
1871
|
try {
|
|
1548
1872
|
waitResult = await this.#waitForManagedBashJob(
|
|
1549
1873
|
job,
|
|
1550
1874
|
autoBackgroundWaitMs,
|
|
1551
1875
|
signal,
|
|
1552
1876
|
backgroundRequest.promise,
|
|
1877
|
+
managedFoldAdapter,
|
|
1553
1878
|
);
|
|
1554
1879
|
} finally {
|
|
1555
1880
|
unregisterBackgroundRequest?.();
|
|
@@ -1577,10 +1902,11 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1577
1902
|
throw new ToolAbortError(formatManagedAbortFailure(undefined, undefined, job.getLatestText()));
|
|
1578
1903
|
}
|
|
1579
1904
|
job.setBackgrounded(true);
|
|
1580
|
-
ownedManager.markBackgrounded(job.jobId, jobGeneration);
|
|
1905
|
+
ownedManager.markBackgrounded(job.jobId, jobGeneration, waitResult.reason);
|
|
1581
1906
|
return this.#buildBackgroundStartResult(job.jobId, job.label, job.getLatestText(), timeoutSec, {
|
|
1582
1907
|
requestedTimeoutSec,
|
|
1583
1908
|
notices: pendingNotices,
|
|
1909
|
+
foldReason: waitResult.reason,
|
|
1584
1910
|
});
|
|
1585
1911
|
}
|
|
1586
1912
|
|
|
@@ -2109,9 +2435,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2109
2435
|
void bridgeHandle.completion.finally(unregisterOwnerCleanup);
|
|
2110
2436
|
registerOwnedIfLineaged(bridgeManager, toolCallId, bridgeJobId, bridgeEndpointId);
|
|
2111
2437
|
|
|
2112
|
-
const bridgeFoldRequest = Promise.withResolvers<
|
|
2438
|
+
const bridgeFoldRequest = Promise.withResolvers<FoldReason>();
|
|
2113
2439
|
const bridgeOriginatingTurn = ctx?.attemptScope !== undefined;
|
|
2114
|
-
const
|
|
2440
|
+
const bridgeFoldAdapter: FoldAdapter = {
|
|
2115
2441
|
kind: "client-terminal",
|
|
2116
2442
|
jobId: bridgeJobId,
|
|
2117
2443
|
jobGeneration: bridgeGeneration,
|
|
@@ -2128,25 +2454,27 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2128
2454
|
const current = bridgeManager.getJob(bridgeJobId);
|
|
2129
2455
|
return current?.generation === bridgeGeneration ? current : undefined;
|
|
2130
2456
|
},
|
|
2131
|
-
detachObserver:
|
|
2457
|
+
detachObserver: receipt => {
|
|
2132
2458
|
const outcome = settleBridgeForeground();
|
|
2133
2459
|
if (outcome === "resolved") {
|
|
2134
2460
|
bridgeHandle.setBackgrounded(true);
|
|
2135
|
-
bridgeManager.markBackgrounded(bridgeJobId, bridgeGeneration);
|
|
2136
|
-
bridgeFoldRequest.resolve();
|
|
2461
|
+
bridgeManager.markBackgrounded(bridgeJobId, bridgeGeneration, receipt.reason);
|
|
2462
|
+
bridgeFoldRequest.resolve(receipt.reason);
|
|
2137
2463
|
}
|
|
2138
2464
|
return outcome;
|
|
2139
2465
|
},
|
|
2140
2466
|
resolveForegroundObserver: () => settleBridgeForeground(),
|
|
2141
|
-
}
|
|
2467
|
+
};
|
|
2468
|
+
const unregisterBridgeFold = this.session.registerForegroundFoldParticipant?.(bridgeFoldAdapter);
|
|
2142
2469
|
|
|
2143
|
-
let bridgeWait: ManagedBashJobCompletion | { kind: "running" } | { kind: "aborted" };
|
|
2470
|
+
let bridgeWait: ManagedBashJobCompletion | { kind: "running"; reason: FoldReason } | { kind: "aborted" };
|
|
2144
2471
|
try {
|
|
2145
2472
|
bridgeWait = await this.#waitForManagedBashJob(
|
|
2146
2473
|
bridgeHandle,
|
|
2147
2474
|
this.#autoBackgroundEnabled ? this.#resolveAutoBackgroundWaitMs(timeoutMs) : timeoutMs + 1_000,
|
|
2148
2475
|
signal,
|
|
2149
2476
|
bridgeFoldRequest.promise,
|
|
2477
|
+
bridgeFoldAdapter,
|
|
2150
2478
|
);
|
|
2151
2479
|
} finally {
|
|
2152
2480
|
unregisterBridgeFold?.();
|
|
@@ -2181,11 +2509,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2181
2509
|
// Folded (or auto-backgrounded): the remote terminal keeps running and its
|
|
2182
2510
|
// result arrives as a background completion.
|
|
2183
2511
|
bridgeHandle.setBackgrounded(true);
|
|
2184
|
-
bridgeManager.markBackgrounded(bridgeJobId, bridgeGeneration);
|
|
2512
|
+
bridgeManager.markBackgrounded(bridgeJobId, bridgeGeneration, bridgeWait.reason);
|
|
2185
2513
|
return this.#buildBackgroundStartResult(bridgeJobId, bridgeLabel, bridgeHandle.getLatestText(), timeoutSec, {
|
|
2186
2514
|
requestedTimeoutSec,
|
|
2187
2515
|
notices: pendingNotices,
|
|
2188
2516
|
terminalId: handle.terminalId,
|
|
2517
|
+
foldReason: bridgeWait.reason,
|
|
2189
2518
|
});
|
|
2190
2519
|
}
|
|
2191
2520
|
|
|
@@ -2200,7 +2529,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2200
2529
|
const artifactPublisher = createBashArtifactPublisher(this.session);
|
|
2201
2530
|
|
|
2202
2531
|
const interactiveUi =
|
|
2203
|
-
this.session.bashRestrictionProfile === "read-only"
|
|
2532
|
+
this.session.bashRestrictionProfile === "read-only" || directMasterSpawn
|
|
2204
2533
|
? undefined
|
|
2205
2534
|
: canUseInteractiveBashPty(pty, ctx)
|
|
2206
2535
|
? ctx?.ui
|
|
@@ -2307,7 +2636,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2307
2636
|
},
|
|
2308
2637
|
);
|
|
2309
2638
|
void controls.terminalCompletion.finally(unregisterPtyOwnerCleanup);
|
|
2310
|
-
|
|
2639
|
+
const ptyStartedAt = Date.now();
|
|
2640
|
+
const ptyFoldAdapter: FoldAdapter = {
|
|
2311
2641
|
kind: "bash-pty",
|
|
2312
2642
|
jobId: ptyJobId,
|
|
2313
2643
|
jobGeneration: ptyGeneration,
|
|
@@ -2324,12 +2654,13 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2324
2654
|
const current = ptyManager.getJob(ptyJobId);
|
|
2325
2655
|
return current?.generation === ptyGeneration ? current : undefined;
|
|
2326
2656
|
},
|
|
2327
|
-
detachObserver:
|
|
2657
|
+
detachObserver: receipt => {
|
|
2328
2658
|
// Output-only continuation: stdin forwarding ends here and the
|
|
2329
2659
|
// process is never killed or restarted by folding.
|
|
2330
2660
|
const started = this.#buildBackgroundStartResult(ptyJobId, ptyLabel, "", timeoutSec, {
|
|
2331
2661
|
requestedTimeoutSec,
|
|
2332
2662
|
notices: pendingNotices,
|
|
2663
|
+
foldReason: receipt.reason,
|
|
2333
2664
|
});
|
|
2334
2665
|
const outcome = controls.detachObserver(
|
|
2335
2666
|
interactiveResultFromText(this.#extractTextResult(started)),
|
|
@@ -2337,13 +2668,19 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2337
2668
|
if (outcome === "resolved") {
|
|
2338
2669
|
controls.detachForegroundCancellation();
|
|
2339
2670
|
ptyBackgrounded = true;
|
|
2340
|
-
ptyManager.markBackgrounded(ptyJobId, ptyGeneration);
|
|
2671
|
+
ptyManager.markBackgrounded(ptyJobId, ptyGeneration, receipt.reason);
|
|
2341
2672
|
ptyFoldResult = started;
|
|
2342
2673
|
}
|
|
2343
2674
|
return outcome;
|
|
2344
2675
|
},
|
|
2345
2676
|
resolveForegroundObserver: () => "already-settled",
|
|
2346
|
-
}
|
|
2677
|
+
};
|
|
2678
|
+
const unregisterPtyFold = this.session.registerForegroundFoldParticipant?.(ptyFoldAdapter);
|
|
2679
|
+
const stopPtySteerWatch = this.#watchSteerForFold(ptyFoldAdapter, ptyStartedAt);
|
|
2680
|
+
ptyFoldUnregister = () => {
|
|
2681
|
+
stopPtySteerWatch();
|
|
2682
|
+
unregisterPtyFold?.();
|
|
2683
|
+
};
|
|
2347
2684
|
},
|
|
2348
2685
|
onFoldKey: () => {
|
|
2349
2686
|
const now = Date.now();
|
|
@@ -2368,7 +2705,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2368
2705
|
cwd: commandCwd,
|
|
2369
2706
|
settings: this.session.settings,
|
|
2370
2707
|
sessionKey: this.session.getSessionId?.() ?? undefined,
|
|
2371
|
-
oneShot: this.session.bashRestrictionProfile === "read-only",
|
|
2708
|
+
oneShot: this.session.bashRestrictionProfile === "read-only" || directMasterSpawn,
|
|
2372
2709
|
timeout: timeoutMs,
|
|
2373
2710
|
signal,
|
|
2374
2711
|
env: resolvedEnv,
|
|
@@ -2379,8 +2716,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2379
2716
|
headBytes,
|
|
2380
2717
|
onChunk: streamTailUpdates(tailBuffer, onUpdate),
|
|
2381
2718
|
onMinimizedSave: async originalText => saveBashOriginalArtifact(this.session, originalText),
|
|
2382
|
-
ignoreShellPrefix: this.session.bashRestrictionProfile === "read-only",
|
|
2383
|
-
disableShellSnapshot: this.session.bashRestrictionProfile === "read-only",
|
|
2719
|
+
ignoreShellPrefix: this.session.bashRestrictionProfile === "read-only" || directMasterSpawn,
|
|
2720
|
+
disableShellSnapshot: this.session.bashRestrictionProfile === "read-only" || directMasterSpawn,
|
|
2384
2721
|
});
|
|
2385
2722
|
ptyFoldUnregister?.();
|
|
2386
2723
|
// Folded: the foreground was settled by the fold, so return the background
|