@gajae-code/coding-agent 0.16.3 → 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.
Files changed (277) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/args.d.ts +2 -2
  4. package/dist/types/cli/customize-doctor.d.ts +22 -0
  5. package/dist/types/cli/root-flags.d.ts +4 -2
  6. package/dist/types/cli.d.ts +1 -1
  7. package/dist/types/commands/launch.d.ts +1 -1
  8. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +31 -3
  10. package/dist/types/config/model-registry.d.ts +4 -0
  11. package/dist/types/config/model-resolver.d.ts +9 -0
  12. package/dist/types/config/settings-schema.d.ts +29 -1
  13. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  15. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  16. package/dist/types/discovery/helpers.d.ts +35 -0
  17. package/dist/types/exec/bash-executor.d.ts +17 -1
  18. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  19. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  20. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  21. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  22. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  23. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  24. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  25. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  26. package/dist/types/exec/isolated-shell.d.ts +19 -0
  27. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  28. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  29. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  30. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  31. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  32. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  33. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  34. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  35. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  36. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  37. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  38. package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
  39. package/dist/types/extensibility/shared-events.d.ts +1 -1
  40. package/dist/types/extensibility/skill-management.d.ts +9 -4
  41. package/dist/types/extensibility/skills.d.ts +2 -0
  42. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  43. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  44. package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
  45. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  46. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  47. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  48. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  49. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  50. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  51. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  52. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  53. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  54. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  55. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  56. package/dist/types/modes/interactive-mode.d.ts +3 -0
  57. package/dist/types/modes/types.d.ts +7 -0
  58. package/dist/types/runtime-mcp/client.d.ts +9 -1
  59. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  60. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  61. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  62. package/dist/types/runtime-mcp/types.d.ts +22 -0
  63. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  64. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  65. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  66. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  68. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  69. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  70. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  71. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  72. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  73. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  74. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  75. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  76. package/dist/types/sdk/client/client.d.ts +6 -0
  77. package/dist/types/sdk/host/host.d.ts +3 -1
  78. package/dist/types/sdk/host/session-runtime.d.ts +26 -1
  79. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  80. package/dist/types/sdk/router/session-router.d.ts +10 -2
  81. package/dist/types/sdk/session-api-key.d.ts +26 -0
  82. package/dist/types/sdk/session.d.ts +5 -1
  83. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  84. package/dist/types/session/agent-session.d.ts +11 -0
  85. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  86. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  87. package/dist/types/session/messages.d.ts +1 -0
  88. package/dist/types/session/session-manager.d.ts +3 -1
  89. package/dist/types/setup/paseo/announce.d.ts +112 -0
  90. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  91. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  92. package/dist/types/system-prompt.d.ts +10 -0
  93. package/dist/types/task/executor.d.ts +5 -1
  94. package/dist/types/tools/bash.d.ts +1 -0
  95. package/dist/types/tools/fetch.d.ts +7 -0
  96. package/dist/types/tools/github-cache.d.ts +2 -1
  97. package/dist/types/tools/index.d.ts +10 -0
  98. package/dist/types/tools/path-utils.d.ts +2 -0
  99. package/dist/types/tools/search.d.ts +6 -0
  100. package/package.json +9 -7
  101. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  102. package/scripts/generate-sdk-operation-inventory.ts +11 -0
  103. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  104. package/src/capability/index.ts +14 -2
  105. package/src/capability/types.ts +4 -0
  106. package/src/cli/args.ts +20 -9
  107. package/src/cli/customize-doctor.ts +155 -12
  108. package/src/cli/root-flags.ts +5 -3
  109. package/src/cli.ts +22 -0
  110. package/src/commands/sdk.ts +74 -31
  111. package/src/config/autorouting-tier-map.ts +1 -0
  112. package/src/config/file-lock-gc.ts +40 -2
  113. package/src/config/file-lock.ts +379 -31
  114. package/src/config/model-preset-registry.ts +120 -19
  115. package/src/config/model-profiles.ts +40 -0
  116. package/src/config/model-registry.ts +73 -23
  117. package/src/config/model-resolver.ts +48 -0
  118. package/src/config/settings-schema.ts +33 -1
  119. package/src/config/settings.ts +1 -0
  120. package/src/coordinator-mcp/question-state.ts +33 -2
  121. package/src/coordinator-mcp/server.ts +477 -101
  122. package/src/crash/sanitize.ts +1 -1
  123. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  124. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  125. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  126. package/src/discovery/builtin.ts +52 -55
  127. package/src/discovery/claude-plugins.ts +12 -4
  128. package/src/discovery/helpers.ts +307 -34
  129. package/src/exec/bash-executor.ts +330 -50
  130. package/src/exec/bash-shell-guardian.ts +388 -0
  131. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  132. package/src/exec/bash-shell-signals.ts +29 -0
  133. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  134. package/src/exec/bash-shell-supervisor.ts +359 -0
  135. package/src/exec/bash-shell-worker-entry.ts +5 -0
  136. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  137. package/src/exec/bash-shell-worker.ts +116 -0
  138. package/src/exec/isolated-shell.ts +393 -0
  139. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  140. package/src/extensibility/extensions/function-hooks.ts +959 -0
  141. package/src/extensibility/extensions/index.ts +22 -0
  142. package/src/extensibility/extensions/loader.ts +53 -2
  143. package/src/extensibility/extensions/runner.ts +895 -264
  144. package/src/extensibility/extensions/types.ts +42 -0
  145. package/src/extensibility/extensions/wrapper.ts +56 -15
  146. package/src/extensibility/gjc-plugins/compiler.ts +228 -60
  147. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  148. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  149. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  150. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  151. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  152. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  153. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  154. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  155. package/src/extensibility/gjc-plugins/types.ts +25 -0
  156. package/src/extensibility/hooks/loader.ts +133 -16
  157. package/src/extensibility/hooks/types.ts +28 -0
  158. package/src/extensibility/runtime-skill-discovery.ts +149 -190
  159. package/src/extensibility/shared-events.ts +1 -1
  160. package/src/extensibility/skill-management.ts +200 -52
  161. package/src/extensibility/skills.ts +73 -25
  162. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  163. package/src/gjc-runtime/gc-runtime.ts +29 -0
  164. package/src/gjc-runtime/linux-proc.ts +14 -0
  165. package/src/gjc-runtime/repository-binding.ts +1 -1
  166. package/src/gjc-runtime/session-state-lock.ts +143 -10
  167. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  168. package/src/gjc-runtime/state-writer.ts +30 -29
  169. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  170. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  171. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  172. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  173. package/src/harness-control-plane/session-lease.ts +2 -2
  174. package/src/internal-urls/docs-index.generated.ts +12 -12
  175. package/src/lsp/config.ts +10 -3
  176. package/src/lsp/index.ts +1 -1
  177. package/src/lsp/path-trust.ts +69 -14
  178. package/src/main.ts +78 -35
  179. package/src/modes/DESIGN.md +54 -0
  180. package/src/modes/acp/acp-agent.ts +16 -3
  181. package/src/modes/components/assistant-message.ts +22 -9
  182. package/src/modes/components/bash-execution.ts +4 -1
  183. package/src/modes/components/execution-shared.ts +4 -1
  184. package/src/modes/components/model-selector.ts +166 -33
  185. package/src/modes/components/session-observer-overlay.ts +1 -1
  186. package/src/modes/components/settings-selector.ts +1075 -235
  187. package/src/modes/components/status-line/command.ts +190 -0
  188. package/src/modes/components/status-line/index.ts +1 -0
  189. package/src/modes/components/status-line/segments.ts +32 -5
  190. package/src/modes/components/status-line/types.ts +5 -0
  191. package/src/modes/components/tool-status-header.ts +396 -22
  192. package/src/modes/controllers/command-controller.ts +3 -0
  193. package/src/modes/controllers/event-controller.ts +151 -7
  194. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  195. package/src/modes/controllers/input-controller.ts +6 -3
  196. package/src/modes/controllers/selector-controller.ts +21 -0
  197. package/src/modes/interactive-mode.ts +53 -6
  198. package/src/modes/print-mode.ts +5 -1
  199. package/src/modes/prompt-action-autocomplete.ts +54 -27
  200. package/src/modes/runtime-init.ts +2 -0
  201. package/src/modes/types.ts +17 -3
  202. package/src/modes/utils/ui-helpers.ts +2 -0
  203. package/src/prompts/system/system-prompt.md +8 -0
  204. package/src/prompts/tools/search.md +2 -0
  205. package/src/prompts/tools/subagent.md +1 -0
  206. package/src/runtime-mcp/client.ts +60 -17
  207. package/src/runtime-mcp/manager.ts +189 -14
  208. package/src/runtime-mcp/pool.test.ts +218 -0
  209. package/src/runtime-mcp/pool.ts +224 -23
  210. package/src/runtime-mcp/transports/stdio.ts +158 -24
  211. package/src/runtime-mcp/types.ts +24 -0
  212. package/src/sdk/broker/broker.ts +124 -32
  213. package/src/sdk/broker/ensure.ts +248 -41
  214. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  215. package/src/sdk/broker/lifecycle.ts +56 -6
  216. package/src/sdk/broker/startup-failure.ts +15 -6
  217. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  218. package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
  219. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  220. package/src/sdk/bus/conversation-store.ts +29 -0
  221. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  222. package/src/sdk/bus/slack-conversation.ts +1 -0
  223. package/src/sdk/bus/slack-daemon.ts +478 -49
  224. package/src/sdk/bus/slack-live-provider.ts +12 -0
  225. package/src/sdk/bus/slack-provider.ts +5 -0
  226. package/src/sdk/bus/telegram-daemon.ts +16 -2
  227. package/src/sdk/bus/topic-registry.ts +19 -0
  228. package/src/sdk/cli/master-cli.ts +1 -1
  229. package/src/sdk/cli/session-cli.ts +11 -8
  230. package/src/sdk/client/client.ts +66 -17
  231. package/src/sdk/host/host.ts +29 -9
  232. package/src/sdk/host/query/revision-store.ts +19 -13
  233. package/src/sdk/host/reverse-leases.ts +7 -4
  234. package/src/sdk/host/session-runtime.test.ts +52 -10
  235. package/src/sdk/host/session-runtime.ts +383 -44
  236. package/src/sdk/lifecycle/service.ts +4 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +77 -0
  238. package/src/sdk/router/session-router.ts +89 -38
  239. package/src/sdk/session-api-key.ts +94 -0
  240. package/src/sdk/session.ts +228 -55
  241. package/src/sdk/transport/relay.ts +35 -13
  242. package/src/sdk/transport/serve-cli.ts +3 -3
  243. package/src/session/agent-session.ts +240 -90
  244. package/src/session/contribution-prep.ts +214 -21
  245. package/src/session/fallback-chain-controller.ts +38 -0
  246. package/src/session/internal/managed-session-storage.ts +19 -5
  247. package/src/session/messages.ts +5 -0
  248. package/src/session/session-manager.ts +29 -5
  249. package/src/session-import/redact.ts +5 -1
  250. package/src/setup/paseo/announce.ts +456 -0
  251. package/src/setup/paseo/setup-deps.ts +15 -3
  252. package/src/skill-state/canonical-skills.ts +12 -0
  253. package/src/skills/skills.test.ts +41 -21
  254. package/src/slash-commands/builtin-registry.ts +1 -0
  255. package/src/system-prompt.ts +29 -3
  256. package/src/task/executor.ts +7 -1
  257. package/src/task/index.ts +21 -4
  258. package/src/tools/bash.ts +63 -19
  259. package/src/tools/cron.ts +1 -1
  260. package/src/tools/fetch.ts +29 -1
  261. package/src/tools/gh.ts +26 -3
  262. package/src/tools/github-cache.ts +68 -16
  263. package/src/tools/index.ts +10 -0
  264. package/src/tools/job.ts +21 -4
  265. package/src/tools/monitor.ts +8 -2
  266. package/src/tools/path-utils.ts +7 -0
  267. package/src/tools/read.ts +14 -2
  268. package/src/tools/render-utils.ts +1 -3
  269. package/src/tools/resource-gc.ts +28 -8
  270. package/src/tools/search.ts +116 -58
  271. package/src/tools/skill-discovery.ts +5 -0
  272. package/src/tools/skill.ts +37 -2
  273. package/src/tools/subagent.ts +6 -3
  274. package/src/tools/tool-catalog.generated.ts +10 -3
  275. package/src/tools/ultragoal-ask-guard.ts +2 -2
  276. package/src/web/insane/bridge.ts +113 -23
  277. package/src/web/search/providers/perplexity.ts +102 -41
@@ -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 { MinimizerOptions, Shell as NativeShell } from "@gajae-code/natives";
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 NativeShellBindings = Pick<typeof import("@gajae-code/natives"), "Shell">;
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 Shell = NativeShell;
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 = 400;
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
- const sessions = new Set([...shellSessions.values(), ...retiringShellSessions]);
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: group.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 = usePersistentShell ? shellSessions.get(sessionKey) : undefined;
300
- if (!shellSession && usePersistentShell) {
301
- shellSession = new Shell({
302
- sessionEnv: shellEnv,
303
- snapshotPath: snapshotPath ?? undefined,
304
- minimizer,
305
- });
306
- shellSessions.set(sessionKey, shellSession);
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
- ? new Shell({
313
- sessionEnv: shellEnv,
314
- snapshotPath: snapshotPath ?? undefined,
315
- minimizer,
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
- const activeShell = shellSession ?? oneShotShell;
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
- if (activeShell && !abortPromise) {
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 runPromise = activeShell!.run(
368
- {
369
- command: finalCommand,
370
- cwd: commandCwd,
371
- env: commandEnv,
372
- timeoutMs: executionTimeoutMs,
373
- signal: runAbortController.signal,
374
- },
375
- (err, chunk) => {
376
- if (!err) {
377
- enqueueChunk(chunk);
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(shellSession);
393
- if (shellSessions.get(sessionKey) === shellSession) {
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(shellSession);
401
- void shellSession.close().catch(() => undefined);
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(shellSession);
406
- if (shellSessions.get(sessionKey) === shellSession) {
407
- shellSessions.delete(sessionKey);
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) void runPromise.catch(() => undefined);
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
- const disposePromise = (abortPromise ?? Promise.resolve()).then(() => oneShotShell.close());
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
  }