@gajae-code/coding-agent 0.16.4 → 0.16.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- package/dist/types/config/file-lock.d.ts +31 -3
- package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +7 -0
- package/dist/types/discovery/helpers.d.ts +33 -0
- package/dist/types/exec/bash-executor.d.ts +17 -1
- package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
- package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-signals.d.ts +2 -0
- package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
- package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
- package/dist/types/exec/bash-shell-worker.d.ts +1 -0
- package/dist/types/exec/isolated-shell.d.ts +19 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
- package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +46 -6
- package/dist/types/extensibility/extensions/types.d.ts +16 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -1
- package/dist/types/extensibility/skill-management.d.ts +9 -4
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +12 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
- package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
- package/dist/types/modes/components/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/broker/ensure.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
- package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
- package/dist/types/sdk/client/client.d.ts +6 -0
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/session-runtime.d.ts +4 -2
- package/dist/types/sdk/lifecycle/service.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +10 -2
- package/dist/types/sdk/session.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/setup/paseo/announce.d.ts +4 -3
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/package.json +9 -7
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +198 -14
- package/src/config/model-preset-registry.ts +115 -16
- package/src/config/model-registry.ts +16 -12
- package/src/coordinator-mcp/question-state.ts +33 -2
- package/src/coordinator-mcp/server.ts +477 -101
- package/src/crash/sanitize.ts +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +50 -62
- package/src/discovery/helpers.ts +297 -32
- package/src/exec/bash-executor.ts +330 -50
- package/src/exec/bash-shell-guardian.ts +388 -0
- package/src/exec/bash-shell-runtime-entry.ts +5 -0
- package/src/exec/bash-shell-signals.ts +29 -0
- package/src/exec/bash-shell-supervisor-entry.ts +3 -0
- package/src/exec/bash-shell-supervisor.ts +359 -0
- package/src/exec/bash-shell-worker-entry.ts +5 -0
- package/src/exec/bash-shell-worker-protocol.ts +36 -0
- package/src/exec/bash-shell-worker.ts +116 -0
- package/src/exec/isolated-shell.ts +393 -0
- package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
- package/src/extensibility/extensions/function-hooks.ts +959 -0
- package/src/extensibility/extensions/index.ts +22 -0
- package/src/extensibility/extensions/loader.ts +53 -2
- package/src/extensibility/extensions/runner.ts +895 -264
- package/src/extensibility/extensions/types.ts +42 -0
- package/src/extensibility/extensions/wrapper.ts +56 -15
- package/src/extensibility/gjc-plugins/compiler.ts +20 -2
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- package/src/extensibility/gjc-plugins/registry.ts +113 -1
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
- package/src/extensibility/gjc-plugins/schema.ts +121 -4
- package/src/extensibility/gjc-plugins/types.ts +25 -0
- package/src/extensibility/hooks/loader.ts +133 -16
- package/src/extensibility/hooks/types.ts +28 -0
- package/src/extensibility/runtime-skill-discovery.ts +80 -47
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +29 -9
- package/src/gjc-runtime/empty-delete-gc.ts +94 -4
- package/src/gjc-runtime/gc-runtime.ts +29 -0
- package/src/gjc-runtime/linux-proc.ts +14 -0
- package/src/gjc-runtime/session-state-lock.ts +51 -8
- package/src/gjc-runtime/session-state-sidecar.ts +27 -11
- package/src/gjc-runtime/state-writer.ts +30 -29
- package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
- package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -0
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/lsp/config.ts +10 -3
- package/src/lsp/index.ts +1 -1
- package/src/lsp/path-trust.ts +69 -14
- package/src/main.ts +6 -1
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/tool-status-header.ts +3 -1
- package/src/modes/controllers/command-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/interactive-mode.ts +6 -5
- package/src/modes/print-mode.ts +5 -1
- package/src/modes/prompt-action-autocomplete.ts +54 -27
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/manager.ts +20 -2
- package/src/runtime-mcp/pool.test.ts +20 -11
- package/src/sdk/broker/broker.ts +119 -28
- package/src/sdk/broker/ensure.ts +248 -41
- package/src/sdk/broker/lifecycle-ledger.ts +7 -17
- package/src/sdk/broker/lifecycle.ts +56 -6
- package/src/sdk/broker/startup-failure.ts +15 -6
- package/src/sdk/bus/chat-daemon-control.ts +3 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +35 -5
- package/src/sdk/bus/chat-effect-journal.ts +132 -7
- package/src/sdk/bus/conversation-store.ts +29 -0
- package/src/sdk/bus/lifecycle-commands.ts +17 -11
- package/src/sdk/bus/slack-conversation.ts +1 -0
- package/src/sdk/bus/slack-daemon.ts +387 -48
- package/src/sdk/bus/telegram-daemon.ts +16 -2
- package/src/sdk/bus/topic-registry.ts +19 -0
- package/src/sdk/cli/master-cli.ts +1 -1
- package/src/sdk/cli/session-cli.ts +2 -2
- package/src/sdk/client/client.ts +66 -17
- package/src/sdk/host/host.ts +29 -9
- package/src/sdk/host/query/revision-store.ts +19 -13
- package/src/sdk/host/reverse-leases.ts +7 -4
- package/src/sdk/host/session-runtime.test.ts +50 -8
- package/src/sdk/host/session-runtime.ts +158 -51
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session.ts +155 -26
- package/src/sdk/transport/relay.ts +35 -13
- package/src/session/agent-session.ts +117 -52
- package/src/session/fallback-chain-controller.ts +38 -0
- package/src/session/internal/managed-session-storage.ts +19 -5
- package/src/session/messages.ts +5 -0
- package/src/session/session-manager.ts +29 -5
- package/src/session-import/redact.ts +5 -1
- package/src/setup/paseo/announce.ts +49 -43
- package/src/setup/paseo/setup-deps.ts +1 -1
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/system-prompt.ts +25 -3
- package/src/task/executor.ts +5 -0
- package/src/task/index.ts +4 -0
- package/src/tools/bash.ts +28 -11
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +13 -2
- package/src/tools/read.ts +14 -2
- package/src/tools/render-utils.ts +1 -3
- package/src/tools/resource-gc.ts +28 -8
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +15 -0
- package/src/tools/subagent.ts +3 -2
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/tools/ultragoal-ask-guard.ts +2 -2
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
* Uses brush-core via native bindings for shell execution.
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from "node:fs/promises";
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
MinimizerOptions,
|
|
9
|
+
Shell as NativeShell,
|
|
10
|
+
ShellOptions,
|
|
11
|
+
ShellRunOptions,
|
|
12
|
+
ShellRunResult,
|
|
13
|
+
} from "@gajae-code/natives";
|
|
8
14
|
import { postmortem } from "@gajae-code/utils";
|
|
9
15
|
import { Settings, type ShellMinimizerSettings } from "../config/settings";
|
|
10
16
|
import { formatCrashDiagnosticNotice, writeCrashReport } from "../debug/crash-diagnostics";
|
|
@@ -17,9 +23,11 @@ import {
|
|
|
17
23
|
} from "../session/streaming-output";
|
|
18
24
|
import { formatArtifactReference, resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
|
|
19
25
|
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
|
|
26
|
+
import { IsolatedShell } from "./isolated-shell";
|
|
20
27
|
import { NON_INTERACTIVE_ENV } from "./non-interactive-env";
|
|
21
28
|
|
|
22
|
-
type
|
|
29
|
+
type NativeShellConstructor = new (options?: ShellOptions | null) => NativeShell;
|
|
30
|
+
type NativeShellBindings = { Shell: NativeShellConstructor };
|
|
23
31
|
let nativeShellBindingsLoad: Promise<NativeShellBindings> | undefined;
|
|
24
32
|
|
|
25
33
|
async function shellNatives(): Promise<NativeShellBindings> {
|
|
@@ -27,7 +35,164 @@ async function shellNatives(): Promise<NativeShellBindings> {
|
|
|
27
35
|
return await nativeShellBindingsLoad;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
type
|
|
38
|
+
type BashShellRunResult = ShellRunResult & { signal?: string };
|
|
39
|
+
type Shell = {
|
|
40
|
+
run(options: ShellRunOptions, onChunk?: (error: Error | null, chunk: string) => void): Promise<BashShellRunResult>;
|
|
41
|
+
abort(): Promise<void>;
|
|
42
|
+
close(): Promise<void>;
|
|
43
|
+
isTerminal?(): boolean;
|
|
44
|
+
ready?(): Promise<void>;
|
|
45
|
+
isRunSignalActive?(signal: AbortSignal): boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
class AdmissionAwareShell implements Shell {
|
|
49
|
+
readonly #inner: Shell;
|
|
50
|
+
#runTail: Promise<void> = Promise.resolve();
|
|
51
|
+
#activeSignals = new WeakSet<AbortSignal>();
|
|
52
|
+
|
|
53
|
+
constructor(inner: Shell) {
|
|
54
|
+
this.#inner = inner;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async run(
|
|
58
|
+
options: ShellRunOptions,
|
|
59
|
+
onChunk?: (error: Error | null, chunk: string) => void,
|
|
60
|
+
): Promise<BashShellRunResult> {
|
|
61
|
+
const predecessor = this.#runTail;
|
|
62
|
+
const admission = Promise.withResolvers<void>();
|
|
63
|
+
this.#runTail = admission.promise;
|
|
64
|
+
try {
|
|
65
|
+
await predecessor;
|
|
66
|
+
if (options.signal instanceof AbortSignal && options.signal.aborted) {
|
|
67
|
+
return { exitCode: undefined, cancelled: true, timedOut: false };
|
|
68
|
+
}
|
|
69
|
+
if (options.signal instanceof AbortSignal) this.#activeSignals.add(options.signal);
|
|
70
|
+
return await this.#inner.run(options, onChunk);
|
|
71
|
+
} finally {
|
|
72
|
+
if (options.signal instanceof AbortSignal) this.#activeSignals.delete(options.signal);
|
|
73
|
+
admission.resolve();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
abort(): Promise<void> {
|
|
78
|
+
return this.#inner.abort();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
close(): Promise<void> {
|
|
82
|
+
return this.#inner.close();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
isRunSignalActive(signal: AbortSignal): boolean {
|
|
86
|
+
return this.#activeSignals.has(signal);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
type ShellFactory = (options?: ShellOptions) => Shell | Promise<Shell>;
|
|
90
|
+
let shellFactoryForTests: ShellFactory | undefined;
|
|
91
|
+
|
|
92
|
+
export function setShellFactoryForTests(factory: ShellFactory | undefined): void {
|
|
93
|
+
shellFactoryForTests = factory;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function createShell(
|
|
97
|
+
options?: ShellOptions,
|
|
98
|
+
onTerminal?: (shell: Shell) => void,
|
|
99
|
+
signal?: AbortSignal,
|
|
100
|
+
onCreated?: (shell: Shell) => void,
|
|
101
|
+
): Promise<Shell> {
|
|
102
|
+
let shell: Shell;
|
|
103
|
+
if (shellFactoryForTests) {
|
|
104
|
+
shell = await shellFactoryForTests(options);
|
|
105
|
+
const { Shell } = await shellNatives();
|
|
106
|
+
if (shell instanceof Shell && !shell.isRunSignalActive) shell = new AdmissionAwareShell(shell);
|
|
107
|
+
} else if (process.platform !== "win32") {
|
|
108
|
+
// Windows brush exposes only its stub signal set, so the POSIX self-signal
|
|
109
|
+
// hazard and session/process-group containment contract do not apply there.
|
|
110
|
+
shell = new IsolatedShell(options, { onTerminal });
|
|
111
|
+
} else {
|
|
112
|
+
const { Shell } = await shellNatives();
|
|
113
|
+
shell = new AdmissionAwareShell(new Shell(options));
|
|
114
|
+
}
|
|
115
|
+
onCreated?.(shell);
|
|
116
|
+
const readyPromise = shell.ready?.();
|
|
117
|
+
if (readyPromise) {
|
|
118
|
+
const startupAbort = Promise.withResolvers<"abort">();
|
|
119
|
+
const startupTimeout = Promise.withResolvers<"timeout">();
|
|
120
|
+
const startupTimeoutTimer = setTimeout(() => startupTimeout.resolve("timeout"), SHELL_STARTUP_TIMEOUT_MS);
|
|
121
|
+
const onAbort = () => startupAbort.resolve("abort");
|
|
122
|
+
if (signal?.aborted) startupAbort.resolve("abort");
|
|
123
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
124
|
+
let winner: "ready" | "abort" | "timeout";
|
|
125
|
+
try {
|
|
126
|
+
winner = await Promise.race([
|
|
127
|
+
readyPromise.then(() => "ready" as const),
|
|
128
|
+
startupAbort.promise,
|
|
129
|
+
startupTimeout.promise,
|
|
130
|
+
]);
|
|
131
|
+
} finally {
|
|
132
|
+
clearTimeout(startupTimeoutTimer);
|
|
133
|
+
signal?.removeEventListener("abort", onAbort);
|
|
134
|
+
}
|
|
135
|
+
if (winner !== "ready") {
|
|
136
|
+
if (winner === "timeout") {
|
|
137
|
+
await shell.close().catch(() => undefined);
|
|
138
|
+
throw new Error(`Isolated shell worker did not become ready within ${SHELL_STARTUP_TIMEOUT_MS}ms.`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return shell;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function getOrCreatePersistentShell(
|
|
146
|
+
sessionKey: string,
|
|
147
|
+
options: ShellOptions,
|
|
148
|
+
signal: AbortSignal | undefined,
|
|
149
|
+
disposalGeneration: number,
|
|
150
|
+
): Promise<Shell> {
|
|
151
|
+
let shellSession = shellSessions.get(sessionKey);
|
|
152
|
+
while (!shellSession && shellStartupLocks.has(sessionKey)) {
|
|
153
|
+
const startupLock = shellStartupLocks.get(sessionKey);
|
|
154
|
+
if (!startupLock) break;
|
|
155
|
+
const aborted = Promise.withResolvers<boolean>();
|
|
156
|
+
const onAbort = () => aborted.resolve(true);
|
|
157
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
158
|
+
const wasAborted = await Promise.race([startupLock.promise.then(() => false), aborted.promise]);
|
|
159
|
+
signal?.removeEventListener("abort", onAbort);
|
|
160
|
+
if (wasAborted) throw new DOMException("Command cancelled", "AbortError");
|
|
161
|
+
shellSession = shellSessions.get(sessionKey);
|
|
162
|
+
}
|
|
163
|
+
if (disposalGeneration !== shellDisposalGeneration) {
|
|
164
|
+
throw new Error("Shell session was disposed during startup.");
|
|
165
|
+
}
|
|
166
|
+
if (shellSession) return shellSession;
|
|
167
|
+
|
|
168
|
+
const startupLock = Promise.withResolvers<void>();
|
|
169
|
+
shellStartupLocks.set(sessionKey, startupLock);
|
|
170
|
+
let startingShell: Shell | undefined;
|
|
171
|
+
try {
|
|
172
|
+
shellSession = await createShell(
|
|
173
|
+
options,
|
|
174
|
+
terminalShell => {
|
|
175
|
+
startingShellSessions.delete(terminalShell);
|
|
176
|
+
if (shellSessions.get(sessionKey) === terminalShell) shellSessions.delete(sessionKey);
|
|
177
|
+
},
|
|
178
|
+
signal,
|
|
179
|
+
createdShell => {
|
|
180
|
+
startingShell = createdShell;
|
|
181
|
+
startingShellSessions.add(createdShell);
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
} finally {
|
|
185
|
+
if (startingShell) startingShellSessions.delete(startingShell);
|
|
186
|
+
if (shellStartupLocks.get(sessionKey) === startupLock) shellStartupLocks.delete(sessionKey);
|
|
187
|
+
startupLock.resolve();
|
|
188
|
+
}
|
|
189
|
+
if (disposalGeneration !== shellDisposalGeneration) {
|
|
190
|
+
await shellSession.close().catch(() => undefined);
|
|
191
|
+
throw new Error("Shell session was disposed during startup.");
|
|
192
|
+
}
|
|
193
|
+
if (!shellSession.isTerminal?.()) shellSessions.set(sessionKey, shellSession);
|
|
194
|
+
return shellSession;
|
|
195
|
+
}
|
|
31
196
|
|
|
32
197
|
export interface BashArtifactSaveSummary {
|
|
33
198
|
artifactId: string;
|
|
@@ -174,6 +339,8 @@ export interface BashExecutorOptions {
|
|
|
174
339
|
export interface BashResult {
|
|
175
340
|
output: string;
|
|
176
341
|
exitCode: number | undefined;
|
|
342
|
+
/** POSIX signal that terminated the isolated shell execution boundary. */
|
|
343
|
+
signal?: string;
|
|
177
344
|
cancelled: boolean;
|
|
178
345
|
truncated: boolean;
|
|
179
346
|
totalLines: number;
|
|
@@ -186,14 +353,22 @@ export interface BashResult {
|
|
|
186
353
|
}
|
|
187
354
|
|
|
188
355
|
const shellSessions = new Map<string, Shell>();
|
|
356
|
+
const startingShellSessions = new Set<Shell>();
|
|
357
|
+
interface ShellStartupLock {
|
|
358
|
+
promise: Promise<void>;
|
|
359
|
+
resolve: () => void;
|
|
360
|
+
}
|
|
361
|
+
const shellStartupLocks = new Map<string, ShellStartupLock>();
|
|
189
362
|
const retiringShellSessions = new Set<Shell>();
|
|
363
|
+
let shellDisposalGeneration = 0;
|
|
190
364
|
// Cover pi-shell's normal cancellation kill waves without turning a stalled
|
|
191
365
|
// native cleanup into a multi-second JavaScript tool stall.
|
|
192
|
-
const CANCEL_CLEANUP_WAIT_MS =
|
|
366
|
+
const CANCEL_CLEANUP_WAIT_MS = 250;
|
|
367
|
+
const SHELL_STARTUP_TIMEOUT_MS = 10_000;
|
|
193
368
|
|
|
194
369
|
/** Number of persistent shell sessions currently retained (owner gauge). */
|
|
195
370
|
export function getShellSessionCount(): number {
|
|
196
|
-
return shellSessions.size;
|
|
371
|
+
return shellSessions.size + startingShellSessions.size;
|
|
197
372
|
}
|
|
198
373
|
|
|
199
374
|
/**
|
|
@@ -213,9 +388,13 @@ export async function disposeAllShellSessions(): Promise<void> {
|
|
|
213
388
|
// `close` rather than `abort`: aborting only cancels in-flight commands and
|
|
214
389
|
// leaves a completed session retained for reuse, which keeps the native shell
|
|
215
390
|
// alive for the rest of the process lifetime.
|
|
216
|
-
|
|
391
|
+
shellDisposalGeneration++;
|
|
392
|
+
const sessions = new Set([...shellSessions.values(), ...startingShellSessions, ...retiringShellSessions]);
|
|
217
393
|
shellSessions.clear();
|
|
394
|
+
startingShellSessions.clear();
|
|
218
395
|
retiringShellSessions.clear();
|
|
396
|
+
for (const lock of shellStartupLocks.values()) lock.resolve();
|
|
397
|
+
shellStartupLocks.clear();
|
|
219
398
|
await Promise.allSettled([...sessions].map(session => session.close()));
|
|
220
399
|
}
|
|
221
400
|
|
|
@@ -236,12 +415,13 @@ async function resolveShellCwd(cwd: string | undefined): Promise<string | undefi
|
|
|
236
415
|
/** Translate `ShellMinimizerSettings` into native `MinimizerOptions`, or `undefined` when disabled. */
|
|
237
416
|
export function buildMinimizerOptions(group: ShellMinimizerSettings): MinimizerOptions | undefined {
|
|
238
417
|
if (!group.enabled) return undefined;
|
|
418
|
+
const maxCaptureBytes = Math.min(4 * 1024 * 1024, Math.max(1024, Math.trunc(group.maxCaptureBytes)));
|
|
239
419
|
return {
|
|
240
420
|
enabled: true,
|
|
241
421
|
settingsPath: group.settingsPath || undefined,
|
|
242
422
|
only: group.only.length > 0 ? group.only : undefined,
|
|
243
423
|
except: group.except.length > 0 ? group.except : undefined,
|
|
244
|
-
maxCaptureBytes
|
|
424
|
+
maxCaptureBytes,
|
|
245
425
|
};
|
|
246
426
|
}
|
|
247
427
|
|
|
@@ -291,38 +471,67 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
291
471
|
...(await sink.dump("Command cancelled")),
|
|
292
472
|
};
|
|
293
473
|
}
|
|
294
|
-
const { Shell } = await shellNatives();
|
|
295
|
-
|
|
296
474
|
const usePersistentShell = options?.oneShot !== true;
|
|
297
475
|
const sessionKey = buildSessionKey(shell, configuredPrefix, snapshotPath, shellEnv, options?.sessionKey, minimizer);
|
|
476
|
+
const invocationDisposalGeneration = shellDisposalGeneration;
|
|
298
477
|
|
|
299
|
-
let shellSession
|
|
300
|
-
|
|
301
|
-
shellSession =
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
478
|
+
let shellSession: Shell | undefined;
|
|
479
|
+
try {
|
|
480
|
+
shellSession = usePersistentShell
|
|
481
|
+
? await getOrCreatePersistentShell(
|
|
482
|
+
sessionKey,
|
|
483
|
+
{ sessionEnv: shellEnv, snapshotPath: snapshotPath ?? undefined, minimizer },
|
|
484
|
+
options?.signal,
|
|
485
|
+
invocationDisposalGeneration,
|
|
486
|
+
)
|
|
487
|
+
: undefined;
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
490
|
+
return {
|
|
491
|
+
exitCode: undefined,
|
|
492
|
+
cancelled: true,
|
|
493
|
+
...(await sink.dump("Command cancelled")),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
throw error;
|
|
307
497
|
}
|
|
308
498
|
// Non-persistent invocations still need an owned native Shell so its lifetime
|
|
309
499
|
// can be ended explicitly. executeShell creates a native shell outside the
|
|
310
500
|
// persistent registry, leaving its cleanup untrackable by the host process.
|
|
311
501
|
const oneShotShell = !usePersistentShell
|
|
312
|
-
?
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
502
|
+
? await createShell(
|
|
503
|
+
{
|
|
504
|
+
sessionEnv: shellEnv,
|
|
505
|
+
snapshotPath: snapshotPath ?? undefined,
|
|
506
|
+
minimizer,
|
|
507
|
+
},
|
|
508
|
+
undefined,
|
|
509
|
+
options?.signal,
|
|
510
|
+
)
|
|
317
511
|
: undefined;
|
|
318
|
-
|
|
512
|
+
let activeShell = shellSession ?? oneShotShell;
|
|
513
|
+
if (options?.signal?.aborted) {
|
|
514
|
+
if (shellSession && shellSessions.get(sessionKey) === shellSession) shellSessions.delete(sessionKey);
|
|
515
|
+
await activeShell?.close().catch(() => undefined);
|
|
516
|
+
return {
|
|
517
|
+
exitCode: undefined,
|
|
518
|
+
cancelled: true,
|
|
519
|
+
...(await sink.dump("Command cancelled")),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
319
522
|
const userSignal = options?.signal;
|
|
320
523
|
const runAbortController = new AbortController();
|
|
321
524
|
const abortCurrentExecution = () => {
|
|
322
525
|
if (!runAbortController.signal.aborted) {
|
|
323
526
|
runAbortController.abort();
|
|
324
527
|
}
|
|
325
|
-
|
|
528
|
+
// IsolatedShell owns run admission and observes this exact run signal. Calling
|
|
529
|
+
// its shared abort method here would let a queued request cancel the active run.
|
|
530
|
+
if (
|
|
531
|
+
activeShell &&
|
|
532
|
+
(!activeShell.isRunSignalActive || activeShell.isRunSignalActive(runAbortController.signal)) &&
|
|
533
|
+
!abortPromise
|
|
534
|
+
) {
|
|
326
535
|
abortPromise = activeShell.abort();
|
|
327
536
|
}
|
|
328
537
|
};
|
|
@@ -364,20 +573,37 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
364
573
|
let runSettled = false;
|
|
365
574
|
|
|
366
575
|
try {
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
(err
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
576
|
+
const runOptions: ShellRunOptions = {
|
|
577
|
+
command: finalCommand,
|
|
578
|
+
cwd: commandCwd,
|
|
579
|
+
env: commandEnv,
|
|
580
|
+
timeoutMs: executionTimeoutMs,
|
|
581
|
+
signal: runAbortController.signal,
|
|
582
|
+
};
|
|
583
|
+
const onRunChunk = (err: Error | null, chunk: string) => {
|
|
584
|
+
if (!err) enqueueChunk(chunk);
|
|
585
|
+
};
|
|
586
|
+
const admittedShell = activeShell!;
|
|
587
|
+
const runPromise = admittedShell.run(runOptions, onRunChunk).catch(async error => {
|
|
588
|
+
if (
|
|
589
|
+
usePersistentShell &&
|
|
590
|
+
admittedShell instanceof IsolatedShell &&
|
|
591
|
+
!admittedShell.wasRunSignalDispatched(runAbortController.signal) &&
|
|
592
|
+
admittedShell.isTerminal() &&
|
|
593
|
+
!runAbortController.signal.aborted &&
|
|
594
|
+
shellSessions.get(sessionKey) !== admittedShell
|
|
595
|
+
) {
|
|
596
|
+
shellSession = await getOrCreatePersistentShell(
|
|
597
|
+
sessionKey,
|
|
598
|
+
{ sessionEnv: shellEnv, snapshotPath: snapshotPath ?? undefined, minimizer },
|
|
599
|
+
runAbortController.signal,
|
|
600
|
+
invocationDisposalGeneration,
|
|
601
|
+
);
|
|
602
|
+
activeShell = shellSession;
|
|
603
|
+
return await shellSession.run(runOptions, onRunChunk);
|
|
604
|
+
}
|
|
605
|
+
throw error;
|
|
606
|
+
});
|
|
381
607
|
|
|
382
608
|
const winner = await Promise.race([
|
|
383
609
|
runPromise.then(result => ({ kind: "result" as const, result })),
|
|
@@ -387,32 +613,60 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
387
613
|
|
|
388
614
|
if (winner.kind === "timeout" || winner.kind === "abort") {
|
|
389
615
|
acceptingChunks = false;
|
|
616
|
+
if (activeShell?.isRunSignalActive && !activeShell.isRunSignalActive(runAbortController.signal)) {
|
|
617
|
+
// The queued run observes the aborted signal when it later reaches
|
|
618
|
+
// admission, so it cannot execute. Do not make this caller wait for an
|
|
619
|
+
// unrelated active predecessor to finish.
|
|
620
|
+
void runPromise.catch(() => undefined);
|
|
621
|
+
return {
|
|
622
|
+
exitCode: undefined,
|
|
623
|
+
cancelled: true,
|
|
624
|
+
...(await sink.dump(
|
|
625
|
+
winner.kind === "timeout" && baseTimeoutMs !== undefined
|
|
626
|
+
? `Command timed out after ${Math.round(baseTimeoutMs / 1000)} seconds`
|
|
627
|
+
: "Command cancelled",
|
|
628
|
+
)),
|
|
629
|
+
};
|
|
630
|
+
}
|
|
390
631
|
if (shellSession) {
|
|
632
|
+
const retiringShell = shellSession;
|
|
391
633
|
resetSession = true;
|
|
392
|
-
retiringShellSessions.add(
|
|
393
|
-
if (shellSessions.get(sessionKey) ===
|
|
634
|
+
retiringShellSessions.add(retiringShell);
|
|
635
|
+
if (shellSessions.get(sessionKey) === retiringShell) {
|
|
394
636
|
shellSessions.delete(sessionKey);
|
|
395
637
|
}
|
|
396
638
|
runSettled = await awaitAbortCleanup(runPromise);
|
|
397
639
|
// A retired session is never reused, so release the native shell instead
|
|
398
640
|
// of leaving it retained for the rest of the process lifetime.
|
|
399
641
|
if (runSettled) {
|
|
400
|
-
retiringShellSessions.delete(
|
|
401
|
-
void
|
|
642
|
+
retiringShellSessions.delete(retiringShell);
|
|
643
|
+
void retiringShell.close().catch(() => undefined);
|
|
644
|
+
} else if (retiringShell instanceof IsolatedShell) {
|
|
645
|
+
// A stopped or otherwise wedged isolated worker cannot settle its protocol
|
|
646
|
+
// run. Close the supervisor-owned boundary now so it force-reaps the group.
|
|
647
|
+
await retiringShell.close().catch(() => undefined);
|
|
648
|
+
retiringShellSessions.delete(retiringShell);
|
|
649
|
+
void runPromise.catch(() => undefined);
|
|
402
650
|
} else {
|
|
651
|
+
// NativeShell.close() waits on the run's session mutex and can remain
|
|
652
|
+
// unresolved on Windows. Keep the shell quarantined until its run settles
|
|
653
|
+
// instead of blocking cancellation on that close.
|
|
403
654
|
void runPromise
|
|
404
655
|
.finally(() => {
|
|
405
|
-
retiringShellSessions.delete(
|
|
406
|
-
if (shellSessions.get(sessionKey) ===
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
void shellSession.close().catch(() => undefined);
|
|
656
|
+
retiringShellSessions.delete(retiringShell);
|
|
657
|
+
if (shellSessions.get(sessionKey) === retiringShell) shellSessions.delete(sessionKey);
|
|
658
|
+
void retiringShell.close().catch(() => undefined);
|
|
410
659
|
})
|
|
411
660
|
.catch(() => undefined);
|
|
412
661
|
}
|
|
413
662
|
} else {
|
|
414
663
|
runSettled = await awaitAbortCleanup(runPromise);
|
|
415
|
-
if (!runSettled)
|
|
664
|
+
if (!runSettled) {
|
|
665
|
+
if (oneShotShell instanceof IsolatedShell) {
|
|
666
|
+
await oneShotShell.close().catch(() => undefined);
|
|
667
|
+
}
|
|
668
|
+
void runPromise.catch(() => undefined);
|
|
669
|
+
}
|
|
416
670
|
}
|
|
417
671
|
return {
|
|
418
672
|
exitCode: undefined,
|
|
@@ -435,6 +689,11 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
435
689
|
? `Command timed out after ${Math.round(options.timeout / 1000)} seconds`
|
|
436
690
|
: "Command timed out";
|
|
437
691
|
resetSession = true;
|
|
692
|
+
runSettled = true;
|
|
693
|
+
if (shellSession && shellSessions.get(sessionKey) === shellSession) {
|
|
694
|
+
shellSessions.delete(sessionKey);
|
|
695
|
+
}
|
|
696
|
+
void activeShell?.close().catch(() => undefined);
|
|
438
697
|
return {
|
|
439
698
|
exitCode: undefined,
|
|
440
699
|
cancelled: true,
|
|
@@ -445,6 +704,11 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
445
704
|
// Handle cancellation
|
|
446
705
|
if (winner.result.cancelled) {
|
|
447
706
|
resetSession = true;
|
|
707
|
+
runSettled = true;
|
|
708
|
+
if (shellSession && shellSessions.get(sessionKey) === shellSession) {
|
|
709
|
+
shellSessions.delete(sessionKey);
|
|
710
|
+
}
|
|
711
|
+
void activeShell?.close().catch(() => undefined);
|
|
448
712
|
return {
|
|
449
713
|
exitCode: undefined,
|
|
450
714
|
cancelled: true,
|
|
@@ -452,6 +716,18 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
452
716
|
};
|
|
453
717
|
}
|
|
454
718
|
|
|
719
|
+
// A self-signal terminates the isolated worker rather than the GJC host.
|
|
720
|
+
// Retire that worker-backed shell immediately; the next command with the
|
|
721
|
+
// same session key creates a fresh persistent shell.
|
|
722
|
+
if (winner.result.signal) {
|
|
723
|
+
resetSession = true;
|
|
724
|
+
runSettled = true;
|
|
725
|
+
if (shellSession && shellSessions.get(sessionKey) === shellSession) {
|
|
726
|
+
shellSessions.delete(sessionKey);
|
|
727
|
+
}
|
|
728
|
+
void activeShell?.close().catch(() => undefined);
|
|
729
|
+
}
|
|
730
|
+
|
|
455
731
|
// When the native minimizer rewrote the output, swap the sink's accumulated
|
|
456
732
|
// raw stream for the minimized text, persist the original as a session
|
|
457
733
|
// artifact, and splice an artifact footer into the visible text so the agent
|
|
@@ -494,12 +770,15 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
494
770
|
const saveNotice = minimizedSaveResult ? minimizedSaveNotice(minimizedSaveResult, summary) : undefined;
|
|
495
771
|
return {
|
|
496
772
|
exitCode: winner.result.exitCode,
|
|
773
|
+
...(winner.result.signal ? { signal: winner.result.signal } : {}),
|
|
497
774
|
cancelled: false,
|
|
498
775
|
...summary,
|
|
499
776
|
...(saveNotice ? { output: appendModelNotice(summary.output, saveNotice) } : {}),
|
|
500
777
|
};
|
|
501
778
|
} catch (err) {
|
|
502
779
|
resetSession = true;
|
|
780
|
+
if (shellSession && shellSessions.get(sessionKey) === shellSession) shellSessions.delete(sessionKey);
|
|
781
|
+
void activeShell?.close().catch(() => undefined);
|
|
503
782
|
throw err;
|
|
504
783
|
} finally {
|
|
505
784
|
if (timeoutTimer) {
|
|
@@ -513,8 +792,9 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
513
792
|
}
|
|
514
793
|
if (oneShotShell) {
|
|
515
794
|
// Always close: a successful run keeps its session retained, so aborting
|
|
516
|
-
// alone would leak the native shell and hold the event loop open.
|
|
517
|
-
|
|
795
|
+
// alone would leak the native shell and hold the event loop open. Closing
|
|
796
|
+
// must not wait behind an abort acknowledgement from a stopped runtime.
|
|
797
|
+
const disposePromise = oneShotShell.close();
|
|
518
798
|
await Promise.race([disposePromise.catch(() => undefined), Bun.sleep(CANCEL_CLEANUP_WAIT_MS)]);
|
|
519
799
|
}
|
|
520
800
|
}
|