mixdog 0.9.147 → 0.9.149
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/package.json +23 -3
- package/scripts/build-voice-runtime-unix.sh +101 -0
- package/scripts/embedding-quality-probe.mjs +96 -0
- package/scripts/generate-voice-runtime-manifest.mjs +300 -0
- package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
- package/scripts/native-binary-arch.mjs +30 -13
- package/scripts/native-binary-arch.test.mjs +40 -4
- package/scripts/plan-voice-runtime-release.mjs +73 -0
- package/scripts/prepare-native-assets.mjs +50 -9
- package/scripts/prune-embedding-runtime.mjs +3 -3
- package/scripts/sync-voice-runtime-manifest.mjs +46 -0
- package/scripts/verify-release-assets.mjs +106 -6
- package/scripts/verify-voice-runtime-release.mjs +89 -0
- package/scripts/voice-runtime-config.json +52 -0
- package/src/app.mjs +42 -5
- package/src/headless-exec.mjs +58 -32
- package/src/headless-exec.test.mjs +74 -13
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/10-tool-workflow.md +7 -4
- package/src/rules/shared/30-exploration.md +5 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
- package/src/runtime/agent/orchestrator/config.mjs +53 -18
- package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +20 -11
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
- package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +69 -3
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
- package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
- package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -9
- package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
- package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
- package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
- package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/attachments/store.mjs +11 -3
- package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
- package/src/runtime/channels/lib/event-queue.mjs +51 -41
- package/src/runtime/channels/lib/scheduler.mjs +23 -28
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
- package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
- package/src/runtime/channels/lib/webhook.mjs +39 -20
- package/src/runtime/channels/lib/whisper-server.mjs +46 -1
- package/src/runtime/channels/lib/worker-main.mjs +3 -1
- package/src/runtime/media/adapters/gemini-video.mjs +20 -4
- package/src/runtime/media/jobs.mjs +13 -0
- package/src/runtime/media/renditions.mjs +7 -1
- package/src/runtime/media/store.mjs +42 -22
- package/src/runtime/memory/index.mjs +4 -9
- package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
- package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
- package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
- package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
- package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
- package/src/runtime/memory/lib/ko-morph.mjs +46 -15
- package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
- package/src/runtime/memory/lib/memory.mjs +50 -12
- package/src/runtime/memory/lib/pg/process.mjs +1 -10
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
- package/src/runtime/memory/lib/query-handlers.mjs +2 -4
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
- package/src/runtime/shared/background-tasks.mjs +15 -0
- package/src/runtime/shared/background-tasks.test.mjs +78 -0
- package/src/runtime/shared/child-guardian.mjs +2 -10
- package/src/runtime/shared/pid-liveness.mjs +27 -0
- package/src/runtime/shared/pristine-execution-contract.json +0 -1
- package/src/runtime/shared/runtime-root.mjs +1 -11
- package/src/runtime/shared/service-discovery.mjs +1 -17
- package/src/runtime/shared/singleton-owner.mjs +1 -11
- package/src/runtime/shared/staged-update.mjs +2 -12
- package/src/runtime/shared/tool-status.mjs +5 -1
- package/src/runtime/shared/tool-surface.mjs +15 -7
- package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
- package/src/runtime/shared/turn-snapshot.mjs +137 -13
- package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
- package/src/runtime/shared/webhook-session-run.mjs +16 -2
- package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
- package/src/runtime/web-search/lib/state.mjs +15 -15
- package/src/runtime/web-search/lib/web-tools.mjs +46 -5
- package/src/session-runtime/context-status.mjs +4 -12
- package/src/session-runtime/runtime-core.mjs +20 -1
- package/src/session-runtime/session-lifecycle.mjs +9 -0
- package/src/standalone/agent-dispatch-broker.mjs +33 -2
- package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
- package/src/standalone/agent-tool/notify.mjs +3 -1
- package/src/standalone/agent-tool/spawn-flow.mjs +52 -70
- package/src/standalone/agent-tool/tag-registry.mjs +116 -33
- package/src/standalone/agent-tool/tool-def.mjs +2 -2
- package/src/standalone/agent-tool/worker-index.mjs +8 -11
- package/src/standalone/agent-tool/worker-rows.mjs +4 -1
- package/src/standalone/agent-tool.mjs +20 -55
- package/src/standalone/agent-watchdog-registry.mjs +28 -4
- package/src/standalone/channel-client.mjs +47 -15
- package/src/standalone/channel-transport.mjs +44 -25
- package/src/standalone/daemon.mjs +31 -2
- package/src/standalone/hook-bus/config.mjs +25 -9
- package/src/standalone/hook-bus/handlers.mjs +16 -52
- package/src/standalone/hook-bus.mjs +29 -23
- package/src/standalone/memory-runtime-proxy.mjs +1 -16
- package/src/standalone/session-client.mjs +86 -23
- package/src/standalone/session-protocol.mjs +0 -2
- package/src/standalone/session-runtime-host-health.test.mjs +5 -4
- package/src/standalone/session-runtime-host.mjs +101 -6
- package/src/standalone/session-runtime-record.mjs +1 -5
- package/src/standalone/session-runtime-worker.mjs +52 -57
- package/src/standalone/session-service.mjs +4 -1
- package/src/standalone/session-transport.mjs +56 -25
- package/src/tui/App.jsx +40 -49
- package/src/tui/app/app-view.jsx +14 -21
- package/src/tui/app/core-memory-picker.mjs +29 -16
- package/src/tui/app/create-app-pickers.mjs +15 -36
- package/src/tui/app/extension-pickers.mjs +60 -49
- package/src/tui/app/maintenance-pickers.mjs +100 -57
- package/src/tui/app/message-selector.mjs +6 -5
- package/src/tui/app/model-picker.mjs +30 -13
- package/src/tui/app/onboarding-steps.mjs +21 -12
- package/src/tui/app/panel-epoch.mjs +56 -0
- package/src/tui/app/panel-epoch.test.mjs +1009 -0
- package/src/tui/app/panel-surface.mjs +98 -0
- package/src/tui/app/project-picker.mjs +30 -29
- package/src/tui/app/prompt-submit.mjs +192 -97
- package/src/tui/app/prompt-submit.test.mjs +206 -0
- package/src/tui/app/provider-setup-picker.mjs +74 -43
- package/src/tui/app/resume-picker.mjs +11 -8
- package/src/tui/app/route-pickers.mjs +45 -28
- package/src/tui/app/settings-picker.mjs +69 -18
- package/src/tui/app/shell-layout.mjs +1 -3
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +4 -2
- package/src/tui/app/text-entry-policy.mjs +25 -0
- package/src/tui/app/theme-effort-pickers.mjs +11 -16
- package/src/tui/app/usage-context-panels.mjs +21 -25
- package/src/tui/app/use-global-key-input.mjs +3 -2
- package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
- package/src/tui/app/use-prompt-handlers.mjs +3 -2
- package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
- package/src/tui/components/Picker.jsx +1 -1
- package/src/tui/components/TextEntryPanel.jsx +7 -2
- package/src/tui/dist/index.mjs +652 -561
- package/src/tui/lib/voice-setup.mjs +1 -2
- package/src/tui/session/agent-job-feed.mjs +73 -11
- package/src/tui/session/session-api-ext.mjs +5 -6
- package/src/tui/session/session-api.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +15 -5
- package/src/tui/session/tui-steering-persist.mjs +33 -19
- package/src/tui/session/tui-steering-persist.test.mjs +104 -0
- package/src/tui/session-local.mjs +0 -22
- package/scripts/.tmp-coverage-report.mjs +0 -96
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// improvements over spawnSync are:
|
|
6
6
|
// - native process-tree termination on timeout / abort.
|
|
7
7
|
// - automatic spill to $PLUGIN_DATA/shell-output/<taskId>.* once the
|
|
8
|
-
// in-memory buffers exceed SHELL_OUTPUT_INLINE_CAP
|
|
8
|
+
// in-memory buffers exceed SHELL_OUTPUT_INLINE_CAP bytes. The caller
|
|
9
9
|
// receives an outputFilePath marker the model can FileRead later
|
|
10
10
|
// instead of losing the tail past the inline cap.
|
|
11
11
|
// - external AbortSignal hookup so a session-scoped abort (ESC, new
|
|
@@ -19,18 +19,24 @@ import { acquire as acquireChildSpawnSlot } from '../../../shared/child-spawn-ga
|
|
|
19
19
|
// a static cycle — safe because neither binding is touched at module-eval
|
|
20
20
|
// time, only when the respective functions actually run.
|
|
21
21
|
import {
|
|
22
|
-
adoptForegroundShellJob,
|
|
23
22
|
attachShellJobResourceLease,
|
|
24
23
|
killShellJob,
|
|
24
|
+
promoteForegroundShellJob,
|
|
25
25
|
publishForegroundShellRecord,
|
|
26
26
|
retireForegroundShellRecord,
|
|
27
|
+
trackForegroundShellJob,
|
|
27
28
|
} from './builtin/shell-jobs.mjs';
|
|
29
|
+
import { nativeSpawnFileCaptureReady, setNativeTaskStartedAt } from './lib/native-spawn-client.mjs';
|
|
28
30
|
import {
|
|
29
31
|
_maybeEncodePowerShellCommand,
|
|
30
32
|
extractPowerShellCommandInner,
|
|
31
33
|
} from './shell-powershell.mjs';
|
|
32
34
|
import { spawnShellWithRetry as _spawnShellWithRetry } from './lib/shell-spawn-retry.mjs';
|
|
33
35
|
import { takeWarmShellStandby } from './lib/shell-warm-standby.mjs';
|
|
36
|
+
import {
|
|
37
|
+
probeShellDescendants,
|
|
38
|
+
STDIO_HELD_AFTER_EXIT_MS,
|
|
39
|
+
} from './lib/shell-descendants.mjs';
|
|
34
40
|
|
|
35
41
|
export {
|
|
36
42
|
_maybeEncodePowerShellCommand,
|
|
@@ -152,6 +158,29 @@ export function _abortReasonIsInterrupt(abortSignal) {
|
|
|
152
158
|
return false;
|
|
153
159
|
}
|
|
154
160
|
|
|
161
|
+
/** Host family of the shell that will ACTUALLY execute the command, resolved
|
|
162
|
+
* from the spawn target itself: the binary being launched and its arguments.
|
|
163
|
+
* Caller-supplied metadata never overrides the target — a spec claiming `cmd`
|
|
164
|
+
* while launching pwsh.exe is answered `powershell`, because pwsh is what
|
|
165
|
+
* parses the text. Returns null when the target cannot be identified; callers
|
|
166
|
+
* must then leave the command exactly as written rather than rewrite it on a
|
|
167
|
+
* guess. (A union of "plausible" families was worse than the ambiguity: it
|
|
168
|
+
* rewrote a valid CMD `echo literal ^&` into `echo literal ^`.) */
|
|
169
|
+
// Arguments are accepted for call-site convenience but never classify.
|
|
170
|
+
export function _shellFamilyForSpawn({ shell = '', shellArg: _shellArg = '', shellArgs: _shellArgs = null } = {}) {
|
|
171
|
+
const name = String(shell || '').toLowerCase().replace(/\.exe$/, '').split(/[\\/]/).pop();
|
|
172
|
+
if (name === 'pwsh' || name === 'powershell') return 'powershell';
|
|
173
|
+
if (name === 'cmd') return 'cmd';
|
|
174
|
+
// bash-family shells add `$'…'` / `$"…"`; sh/dash/ash/busybox do not, and
|
|
175
|
+
// their delimiter words keep `$` as an ordinary character.
|
|
176
|
+
if (/^(?:bash|zsh|ksh|ksh93|mksh)$/.test(name)) return 'bash';
|
|
177
|
+
if (/^(?:sh|dash|ash|busybox)$/.test(name)) return 'posix';
|
|
178
|
+
// Arguments never classify: `/c` looks like cmd.exe, but `{shell:'/usr/bin/env',
|
|
179
|
+
// shellArg:'/c'}` is not cmd — only the executable receiving the command text
|
|
180
|
+
// decides, and an unrecognized one means detect nothing and rewrite nothing.
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
155
184
|
export async function acquireShellLeaseBounded(admission, {
|
|
156
185
|
abortSignal, label, dependency = 'scoped', ownerKey = null,
|
|
157
186
|
} = {}) {
|
|
@@ -226,6 +255,12 @@ export function execShellCommand({
|
|
|
226
255
|
backgroundDeadlineMs = 0,
|
|
227
256
|
admission = resourceAdmission,
|
|
228
257
|
directArgv = null,
|
|
258
|
+
// What the shell PARSES, when that must differ from what the caller shows.
|
|
259
|
+
// `command` stays the display/record text (job labels, telemetry, shell-job
|
|
260
|
+
// rows); `execScript` is the argv payload. They diverge only when a caller
|
|
261
|
+
// hands the script to the shell out-of-band — see the POSIX env+eval path in
|
|
262
|
+
// bash-tool.mjs, which keeps the command body out of the child's argv.
|
|
263
|
+
execScript = null,
|
|
229
264
|
}) {
|
|
230
265
|
return new Promise(async (resolve) => {
|
|
231
266
|
let resultResolved = false;
|
|
@@ -237,6 +272,11 @@ export function execShellCommand({
|
|
|
237
272
|
};
|
|
238
273
|
const taskId = `shell_${randomUUID().slice(0, 8)}`;
|
|
239
274
|
const taskOutput = new TaskOutput(taskId);
|
|
275
|
+
// The command text is NEVER inspected or rewritten here: whatever the
|
|
276
|
+
// caller wrote reaches the shell byte for byte, trailing `&` included.
|
|
277
|
+
// A command that detaches work is dealt with AFTER it runs, by observing
|
|
278
|
+
// whether the shell's process group / tree still holds live processes
|
|
279
|
+
// (see the descendant probe in settle()).
|
|
240
280
|
const stdoutDecoder = new ShellTextDecoder();
|
|
241
281
|
const stderrDecoder = new ShellTextDecoder();
|
|
242
282
|
let timedOut = false;
|
|
@@ -251,6 +291,10 @@ export function execShellCommand({
|
|
|
251
291
|
let timer = null;
|
|
252
292
|
let abortHandler = null;
|
|
253
293
|
let partialOutput = false;
|
|
294
|
+
// Moment the shell PROCESS itself exited. The gap between it and the
|
|
295
|
+
// stdio close is the win32 evidence that descendants inherited (and still
|
|
296
|
+
// hold) this command's stdout/stderr — see lib/shell-descendants.mjs.
|
|
297
|
+
let rootExitAtMs = 0;
|
|
254
298
|
let resourceLease = null;
|
|
255
299
|
const releaseResourceLease = async () => {
|
|
256
300
|
if (!resourceLease) return null;
|
|
@@ -285,7 +329,7 @@ export function execShellCommand({
|
|
|
285
329
|
if (outputTailTimer) { clearInterval(outputTailTimer); outputTailTimer = null; }
|
|
286
330
|
};
|
|
287
331
|
// Auto-background transition flag. Set the moment the autoBackgroundMs
|
|
288
|
-
// timer fires and
|
|
332
|
+
// timer fires and promotes the still-running child. Once
|
|
289
333
|
// true the normal settle()/close/exit/treeKill paths are inert for this
|
|
290
334
|
// run — the call has already resolved with a 'backgrounded' result and
|
|
291
335
|
// the child's lifecycle is owned by the shell-jobs registry. Mutually
|
|
@@ -293,31 +337,47 @@ export function execShellCommand({
|
|
|
293
337
|
let autoBackgrounded = false;
|
|
294
338
|
let autoBackgroundJobId = null;
|
|
295
339
|
let autoBgTimer = null;
|
|
296
|
-
// Treekill +
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
// 5 s
|
|
340
|
+
// Treekill + exit confirmation. treeKill alone leaves settle() pending on
|
|
341
|
+
// 'close'/'exit'; on Windows a taskkill miss or a grandchild holding stdio
|
|
342
|
+
// fds keeps the dispatch stalled until the upstream ceiling. Covers every
|
|
343
|
+
// kill path (timeout / pre-aborted / abort / capture-error /
|
|
344
|
+
// size-watchdog) so the hang risk does not live on outside the timeout
|
|
345
|
+
// branch. Function declaration so callers placed above settle()'s const
|
|
346
|
+
// definition still resolve via hoisting.
|
|
347
|
+
// A single 5 s force-settle was NOT enough: settling on that timer while
|
|
348
|
+
// the tree was still alive released the admission lease and the capture
|
|
349
|
+
// files under a running process. Each deadline now RE-issues the kill and
|
|
350
|
+
// re-arms; only a confirmed exit settles normally, and the bounded last
|
|
351
|
+
// resort settles with killUnconfirmed so cleanup follows the real exit.
|
|
352
|
+
const KILL_CONFIRM_INTERVAL_MS = 5000;
|
|
353
|
+
const KILL_CONFIRM_ATTEMPTS = 3;
|
|
354
|
+
let killUnconfirmed = false;
|
|
304
355
|
function _treeKillForceSettle(cause) {
|
|
305
356
|
killed = true;
|
|
306
357
|
killCause = killCause || cause || 'runtime-guard';
|
|
358
|
+
let attempts = 0;
|
|
359
|
+
const armConfirmation = () => {
|
|
360
|
+
const deadline = setTimeout(() => {
|
|
361
|
+
if (settled || autoBackgrounded) return;
|
|
362
|
+
// A confirmed exit is settled by the child's own close/exit
|
|
363
|
+
// handlers with its real status; nothing to force here.
|
|
364
|
+
if (child?.exitCode != null || child?.signalCode != null) return;
|
|
365
|
+
attempts += 1;
|
|
366
|
+
if (attempts < KILL_CONFIRM_ATTEMPTS) {
|
|
367
|
+
treeKill(child);
|
|
368
|
+
armConfirmation();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
partialOutput = true;
|
|
372
|
+
killUnconfirmed = true;
|
|
373
|
+
failureReason = failureReason || 'kill unconfirmed';
|
|
374
|
+
settle(1, 'SIGKILL');
|
|
375
|
+
}, KILL_CONFIRM_INTERVAL_MS);
|
|
376
|
+
if (deadline.unref) deadline.unref();
|
|
377
|
+
};
|
|
307
378
|
treeKill(child);
|
|
308
|
-
|
|
309
|
-
if (settled) return;
|
|
310
|
-
partialOutput = true;
|
|
311
|
-
settle(1, 'SIGKILL');
|
|
312
|
-
}, 5000);
|
|
313
|
-
if (_killDeadline.unref) _killDeadline.unref();
|
|
379
|
+
armConfirmation();
|
|
314
380
|
}
|
|
315
|
-
// Background commands (trailing `&`) intentionally detach stdio
|
|
316
|
-
// from the parent shell, so 'close' may never fire while the
|
|
317
|
-
// backgrounded grandchild is still alive. For those we settle
|
|
318
|
-
// immediately on direct-child exit instead of waiting for close.
|
|
319
|
-
const _trimmed = String(command || '').replace(/\s+$/, '');
|
|
320
|
-
const _isBackground = /(^|[^&|])&$/.test(_trimmed);
|
|
321
381
|
|
|
322
382
|
let child;
|
|
323
383
|
let _onChildErrorRef = null;
|
|
@@ -366,7 +426,12 @@ export function execShellCommand({
|
|
|
366
426
|
return;
|
|
367
427
|
}
|
|
368
428
|
const _useDirectArgv = Array.isArray(directArgv);
|
|
369
|
-
|
|
429
|
+
// Direct-exe spawns already bypass the shell, so they keep using the
|
|
430
|
+
// command verbatim; only the shell-parsed form honours execScript.
|
|
431
|
+
const _shellScript = (!_useDirectArgv && typeof execScript === 'string' && execScript)
|
|
432
|
+
? execScript
|
|
433
|
+
: command;
|
|
434
|
+
const _spawnCommand = _useDirectArgv ? String(command ?? '') : _maybeEncodePowerShellCommand(_shellScript);
|
|
370
435
|
const argv = _useDirectArgv
|
|
371
436
|
? [...directArgv]
|
|
372
437
|
: (Array.isArray(shellArgs) && shellArgs.length > 0
|
|
@@ -395,44 +460,76 @@ export function execShellCommand({
|
|
|
395
460
|
// full-lifetime gating concern in the note below stays true.
|
|
396
461
|
let spawned = null;
|
|
397
462
|
if (_standby) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
463
|
+
let tracked = null;
|
|
464
|
+
try {
|
|
465
|
+
tracked = await trackForegroundShellJob({
|
|
466
|
+
command,
|
|
467
|
+
cwd,
|
|
468
|
+
child: _standby.spawned.child,
|
|
469
|
+
jobId: _foregroundRecordId,
|
|
470
|
+
ownerSessionId,
|
|
471
|
+
clientHostPid,
|
|
472
|
+
});
|
|
473
|
+
} catch {}
|
|
474
|
+
if (tracked) {
|
|
475
|
+
spawned = _standby.spawned;
|
|
476
|
+
} else {
|
|
477
|
+
// The parked process settled or lost its native request identity.
|
|
478
|
+
// Do not feed it; replace it with a normally tracked spawn.
|
|
479
|
+
try { _standby.spawned.child.kill(); } catch {}
|
|
480
|
+
_standby = null;
|
|
481
|
+
}
|
|
409
482
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
argv,
|
|
414
|
-
shellArg,
|
|
415
|
-
cwd,
|
|
416
|
-
spawnOptions: {
|
|
417
|
-
env,
|
|
418
|
-
cwd,
|
|
419
|
-
outputLimit: SHELL_OUTPUT_DISK_CAP,
|
|
420
|
-
rawOutput: true,
|
|
421
|
-
// NOTE (child-spawn-gate): the full command lifetime is intentionally
|
|
422
|
-
// NOT gated — bash/pwsh commands can run for minutes and would starve
|
|
423
|
-
// rg/code_graph. Only the spawn window above holds a slot.
|
|
424
|
-
// POSIX: detached gives the child its own process group so treeKill can
|
|
425
|
-
// signal the whole group. The child is still CLI-owned because we do
|
|
426
|
-
// not unref it after adoption. Windows detached has different console
|
|
427
|
-
// semantics, so it stays off there.
|
|
428
|
-
},
|
|
483
|
+
if (!spawned) {
|
|
484
|
+
const _releaseSpawnSlot = await acquireChildSpawnSlot(abortSignal || null, 'process-spawn', {
|
|
485
|
+
ownerKey: ownerSessionId,
|
|
429
486
|
});
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
487
|
+
// Gate drain can grant several waiters in one tick. Yield so CreateProcess
|
|
488
|
+
// + AV does not freeze graph/patch/search callbacks in that same turn.
|
|
489
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
490
|
+
if (abortSignal?.aborted) {
|
|
491
|
+
try { _releaseSpawnSlot(); } catch { /* idempotent */ }
|
|
492
|
+
throw abortSignal.reason || new Error('aborted');
|
|
493
|
+
}
|
|
494
|
+
// CC parity (ShellCommand.ts): give the child its OWN fds on the
|
|
495
|
+
// capture files so its output never depends on a reader living in this
|
|
496
|
+
// process. A job the caller deliberately leaves running — a task's
|
|
497
|
+
// server — then keeps writing after we exit; on a pipe it dies on its
|
|
498
|
+
// next write the moment our reader is gone.
|
|
499
|
+
const _capture = nativeSpawnFileCaptureReady() ? taskOutput.openDirectCapture() : null;
|
|
500
|
+
try {
|
|
501
|
+
spawned = await _spawnShellWithRetry({
|
|
502
|
+
shell,
|
|
503
|
+
argv,
|
|
504
|
+
shellArg,
|
|
505
|
+
cwd,
|
|
506
|
+
spawnOptions: {
|
|
507
|
+
env,
|
|
508
|
+
cwd,
|
|
509
|
+
outputLimit: SHELL_OUTPUT_DISK_CAP,
|
|
510
|
+
rawOutput: true,
|
|
511
|
+
jobId: _foregroundRecordId,
|
|
512
|
+
command,
|
|
513
|
+
ownerSessionId,
|
|
514
|
+
clientHostPid,
|
|
515
|
+
...(_capture ? {
|
|
516
|
+
stdoutPath: taskOutput.stdoutPath,
|
|
517
|
+
stderrPath: taskOutput.stderrPath,
|
|
518
|
+
} : {}),
|
|
519
|
+
// NOTE (child-spawn-gate): the full command lifetime is intentionally
|
|
520
|
+
// NOT gated — bash/pwsh commands can run for minutes and would starve
|
|
521
|
+
// rg/code_graph. Only the spawn window above holds a slot.
|
|
522
|
+
// POSIX: detached gives the child its own process group so treeKill can
|
|
523
|
+
// signal the whole group. Windows detached has different console
|
|
524
|
+
// semantics, so it stays off there.
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
} finally {
|
|
528
|
+
try { _releaseSpawnSlot(); } catch { /* idempotent */ }
|
|
529
|
+
}
|
|
433
530
|
}
|
|
434
531
|
child = spawned.child;
|
|
435
|
-
spawned.
|
|
532
|
+
spawned.attachErrorHandler(_onChildError);
|
|
436
533
|
// Feed the standby only after the error handler is attached; server
|
|
437
534
|
// messages cannot be processed before this synchronous block yields.
|
|
438
535
|
if (_standby) _standby.feed(_spawnCommand, cwd);
|
|
@@ -440,6 +537,7 @@ export function execShellCommand({
|
|
|
440
537
|
// readout measures from, and the point from which a still-running
|
|
441
538
|
// command deserves to be visible in the shell readouts.
|
|
442
539
|
_commandStartedAtMs = Date.now();
|
|
540
|
+
setNativeTaskStartedAt(_foregroundRecordId, _commandStartedAtMs);
|
|
443
541
|
_foregroundRecordTimer = setTimeout(() => {
|
|
444
542
|
_foregroundRecordTimer = null;
|
|
445
543
|
if (settled || autoBackgrounded || !child?.pid) return;
|
|
@@ -486,16 +584,20 @@ export function execShellCommand({
|
|
|
486
584
|
_treeKillForceSettle('cancellation');
|
|
487
585
|
}
|
|
488
586
|
|
|
587
|
+
// Binary bytes are sanitized by the capture layer and the run CONTINUES.
|
|
588
|
+
// Killing the whole process tree on the first non-text chunk also killed
|
|
589
|
+
// the servers and pipelines that legitimately emit binary (git http
|
|
590
|
+
// protocol, VM stdout, PDF/ISO dumps) and returned zero output for work
|
|
591
|
+
// that had already succeeded. Runaway volume stays bounded by the inline
|
|
592
|
+
// cap and the SHELL_OUTPUT_DISK_CAP watchdog.
|
|
489
593
|
const _stdoutData = (chunk) => {
|
|
490
594
|
const text = stdoutDecoder.write(chunk);
|
|
491
595
|
if (text) taskOutput.writeStdout(text);
|
|
492
|
-
if (taskOutput.binaryOutput && !settled && !autoBackgrounded) _treeKillForceSettle('binary-output');
|
|
493
596
|
if (taskOutput.writeError && !settled && !autoBackgrounded) _treeKillForceSettle('output-capture-error');
|
|
494
597
|
};
|
|
495
598
|
const _stderrData = (chunk) => {
|
|
496
599
|
const text = stderrDecoder.write(chunk);
|
|
497
600
|
if (text) taskOutput.writeStderr(text);
|
|
498
|
-
if (taskOutput.binaryOutput && !settled && !autoBackgrounded) _treeKillForceSettle('binary-output');
|
|
499
601
|
if (taskOutput.writeError && !settled && !autoBackgrounded) _treeKillForceSettle('output-capture-error');
|
|
500
602
|
};
|
|
501
603
|
if (child.stdout) {
|
|
@@ -505,8 +607,49 @@ export function execShellCommand({
|
|
|
505
607
|
child.stderr.on('data', _stderrData);
|
|
506
608
|
}
|
|
507
609
|
|
|
610
|
+
// Promotion resolves the call and hands the child's lifecycle to the
|
|
611
|
+
// shell-jobs registry, but the spill FDs stay owned by THIS runner and
|
|
612
|
+
// nothing else would ever close them — one leaked descriptor pair per
|
|
613
|
+
// promoted command. Released on the promoted child's own terminal event.
|
|
614
|
+
let _promotedCaptureReleased = false;
|
|
615
|
+
const _releasePromotedCapture = () => {
|
|
616
|
+
if (_promotedCaptureReleased) return;
|
|
617
|
+
if (child?.exitCode == null && child?.signalCode == null) return;
|
|
618
|
+
_promotedCaptureReleased = true;
|
|
619
|
+
try {
|
|
620
|
+
// An empty spill pair is garbage nothing can reference; captured
|
|
621
|
+
// bytes stay, because the promoted task record points at these files.
|
|
622
|
+
if (taskOutput.spilled && taskOutput.totalDiskBytes() === 0) taskOutput.deleteFiles();
|
|
623
|
+
else taskOutput.closeFds();
|
|
624
|
+
} catch { /* best-effort */ }
|
|
625
|
+
};
|
|
626
|
+
// Unconfirmed-kill cleanup. The result is already reported, but the tree
|
|
627
|
+
// may still be alive: defer closing the capture and returning the
|
|
628
|
+
// admission lease to the child's real exit, with a ceiling so an
|
|
629
|
+
// unkillable tree cannot pin the shell lane forever.
|
|
630
|
+
const UNCONFIRMED_KILL_CLEANUP_CEILING_MS = 60_000;
|
|
631
|
+
let _deferredCleanupDone = false;
|
|
632
|
+
const _deferCleanupToChildExit = () => {
|
|
633
|
+
const run = () => {
|
|
634
|
+
if (_deferredCleanupDone) return;
|
|
635
|
+
_deferredCleanupDone = true;
|
|
636
|
+
try { taskOutput.closeFds(); } catch { /* best-effort */ }
|
|
637
|
+
void releaseResourceLease();
|
|
638
|
+
};
|
|
639
|
+
try {
|
|
640
|
+
child.once('close', run);
|
|
641
|
+
child.once('exit', run);
|
|
642
|
+
} catch { /* child may already be gone */ }
|
|
643
|
+
const ceiling = setTimeout(run, UNCONFIRMED_KILL_CLEANUP_CEILING_MS);
|
|
644
|
+
if (ceiling.unref) ceiling.unref();
|
|
645
|
+
};
|
|
646
|
+
|
|
508
647
|
settle = async (exitCode, signal) => {
|
|
509
|
-
if (settled
|
|
648
|
+
if (settled) return;
|
|
649
|
+
if (autoBackgrounded) {
|
|
650
|
+
_releasePromotedCapture();
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
510
653
|
settled = true;
|
|
511
654
|
// Off the readouts the instant the command is over, before any awaited
|
|
512
655
|
// output capture — a finished command must never linger as "running".
|
|
@@ -538,18 +681,42 @@ export function execShellCommand({
|
|
|
538
681
|
catch (err) { taskOutput.writeError = taskOutput.writeError || err; }
|
|
539
682
|
if (spawnError && !stderr) stderr = String(spawnError.message || spawnError);
|
|
540
683
|
// Inline-only path: nothing spilled. Nothing to clean up.
|
|
541
|
-
// Spilled but
|
|
542
|
-
// the
|
|
543
|
-
// the
|
|
544
|
-
|
|
684
|
+
// Spilled but within the inline cap: getStdout/getStderr already
|
|
685
|
+
// returned the whole file, so the files would only duplicate the
|
|
686
|
+
// inline body — drop them. Past the cap the rendered body is head+tail
|
|
687
|
+
// carrying a "full output at <path>" marker, so the files MUST survive.
|
|
688
|
+
// The test compares captured BYTES, not rendered UTF-16 length: a CJK
|
|
689
|
+
// head+tail holds ~1/3 the char count of its byte size and under a
|
|
690
|
+
// length test would delete the very file its own marker names.
|
|
691
|
+
if (killUnconfirmed) {
|
|
692
|
+
// Never confirmed dead: deleting the capture files or handing back the
|
|
693
|
+
// lease now would release resources a live process still owns. The
|
|
694
|
+
// spilled paths therefore survive and travel with the result below.
|
|
695
|
+
_deferCleanupToChildExit();
|
|
696
|
+
} else if (
|
|
545
697
|
taskOutput.spilled &&
|
|
546
|
-
|
|
698
|
+
taskOutput.totalDiskBytes() <= SHELL_OUTPUT_INLINE_CAP
|
|
547
699
|
) {
|
|
548
700
|
taskOutput.deleteFiles();
|
|
701
|
+
void releaseResourceLease();
|
|
549
702
|
} else {
|
|
550
703
|
taskOutput.closeFds();
|
|
704
|
+
void releaseResourceLease();
|
|
705
|
+
}
|
|
706
|
+
// The shell is gone — did it leave anything RUNNING? Answered from the
|
|
707
|
+
// process group / process tree the spawn layer already owns, never from
|
|
708
|
+
// the command text. A handle here means the caller reports a tracked
|
|
709
|
+
// task instead of a clean finish; null means nothing survived.
|
|
710
|
+
let descendants = null;
|
|
711
|
+
if (!killed && !timedOut && child?.pid) {
|
|
712
|
+
try {
|
|
713
|
+
descendants = await probeShellDescendants({
|
|
714
|
+
pid: child.pid,
|
|
715
|
+
stdioHeld: rootExitAtMs > 0 && (Date.now() - rootExitAtMs) >= STDIO_HELD_AFTER_EXIT_MS,
|
|
716
|
+
});
|
|
717
|
+
if (descendants) descendants.taskId = _foregroundRecordId;
|
|
718
|
+
} catch { descendants = null; }
|
|
551
719
|
}
|
|
552
|
-
void releaseResourceLease();
|
|
553
720
|
resolveResult(
|
|
554
721
|
new ExecResult({
|
|
555
722
|
stdout,
|
|
@@ -568,6 +735,7 @@ export function execShellCommand({
|
|
|
568
735
|
outputCaptureError: taskOutput.writeError,
|
|
569
736
|
failurePhase,
|
|
570
737
|
failureReason,
|
|
738
|
+
descendants,
|
|
571
739
|
}),
|
|
572
740
|
);
|
|
573
741
|
};
|
|
@@ -583,31 +751,36 @@ export function execShellCommand({
|
|
|
583
751
|
// 'close' only fires after stdio drains; a forked grandchild that
|
|
584
752
|
// inherited stdout/stderr fds can hold them open past direct-child
|
|
585
753
|
// exit and stall settle() until timeoutMs. 'exit' fires on direct
|
|
586
|
-
// child termination regardless — give 'close' a
|
|
587
|
-
// settle anyway.
|
|
754
|
+
// child termination regardless — give 'close' a 3 s grace then
|
|
755
|
+
// settle anyway. The grace outlives the spawn server's own 2 s drain
|
|
756
|
+
// deadline, so the real exit status normally still arrives first.
|
|
588
757
|
const _onChildExit = (code, signal) => {
|
|
589
|
-
if (
|
|
590
|
-
setImmediate(() => settle(code == null ? 1 : code, signal));
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
758
|
+
if (!rootExitAtMs) rootExitAtMs = Date.now();
|
|
593
759
|
const grace = setTimeout(() => {
|
|
594
|
-
if (settled
|
|
760
|
+
if (settled) return;
|
|
761
|
+
if (autoBackgrounded) {
|
|
762
|
+
// A promoted child whose 'close' never arrives (grandchild holding
|
|
763
|
+
// the stdio) would otherwise keep its spill descriptors forever:
|
|
764
|
+
// 'exit' is the only terminal event this path gets.
|
|
765
|
+
_releasePromotedCapture();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
595
768
|
partialOutput = true;
|
|
596
769
|
settle(code == null ? 1 : code, signal);
|
|
597
|
-
},
|
|
770
|
+
}, 3000);
|
|
598
771
|
if (grace.unref) grace.unref();
|
|
599
772
|
};
|
|
600
773
|
child.once('exit', _onChildExit);
|
|
601
774
|
// Auto-background transition. Two triggers
|
|
602
775
|
// resolve the call immediately with a 'backgrounded' result while the
|
|
603
|
-
// child keeps running,
|
|
776
|
+
// child keeps running, promoted in the shell-jobs registry but still
|
|
604
777
|
// owned by this CLI process:
|
|
605
778
|
// 1. the autoBackgroundMs soft foreground threshold — an EARLIER
|
|
606
779
|
// promotion before the timeout, and
|
|
607
780
|
// 2. the foreground timeout deadline (backgroundOnTimeout) — the default
|
|
608
781
|
// promote-on-timeout that replaces the old tree-kill.
|
|
609
782
|
// A capped explicit foreground timeout supplies its remaining deadline to
|
|
610
|
-
// the
|
|
783
|
+
// the promoted job; otherwise background execution remains unlimited.
|
|
611
784
|
// Mutually exclusive with settle() via the autoBackgrounded flag set
|
|
612
785
|
// synchronously at the top before any await.
|
|
613
786
|
const _autoBackground = async ({ reason = 'threshold' } = {}) => {
|
|
@@ -618,49 +791,50 @@ export function execShellCommand({
|
|
|
618
791
|
if (child.exitCode != null || child.signalCode != null) return;
|
|
619
792
|
autoBackgrounded = true;
|
|
620
793
|
// The foreground capture is over; stop the local watchdogs/timers so
|
|
621
|
-
// they cannot treeKill the now-
|
|
794
|
+
// they cannot treeKill the now-promoted child.
|
|
622
795
|
if (timer) { clearTimeout(timer); timer = null; }
|
|
623
796
|
_clearProgressTimer();
|
|
624
797
|
if (autoBgTimer) { clearTimeout(autoBgTimer); autoBgTimer = null; }
|
|
625
798
|
// Keep the abort handler ATTACHED through the promotion window. A user
|
|
626
|
-
// cancel racing in after promotion starts must still bring the
|
|
799
|
+
// cancel racing in after promotion starts must still bring the promoted
|
|
627
800
|
// child down — the handler's treeKill(child) does exactly that (settle()
|
|
628
801
|
// is inert once autoBackgrounded, but the kill itself still lands, and
|
|
629
802
|
// refreshShellJob then flags the job failed). We only detach on a real
|
|
630
|
-
// settle() or on the
|
|
803
|
+
// settle() or on the promotion-failure fallback below.
|
|
631
804
|
// Every subsequent stdout/stderr chunk must hit disk — the call is
|
|
632
805
|
// about to resolve and nobody will drain the in-memory buffers again.
|
|
633
806
|
try { taskOutput.forceSpill(); } catch {}
|
|
634
807
|
// The foreground sizeWatchdog was cleared above; the output cap now
|
|
635
|
-
// travels with the
|
|
808
|
+
// travels with the promoted job — the shell-job watcher arms a periodic
|
|
636
809
|
// refreshShellJob tick that enforces SHELL_JOB_OUTPUT_DISK_CAP against the
|
|
637
810
|
// same spill files (stdoutPath/stderrPath below), killing + flagging a
|
|
638
811
|
// runaway background producer even with no active task waiter.
|
|
639
812
|
const stdoutPath = taskOutput.spilled ? taskOutput.stdoutPath : null;
|
|
640
813
|
const stderrPath = taskOutput.spilled ? taskOutput.stderrPath : null;
|
|
641
814
|
let job = null;
|
|
642
|
-
// The
|
|
815
|
+
// The promoted job publishes its own record under the real task id; drop
|
|
643
816
|
// the foreground marker first so the command is never counted twice.
|
|
644
817
|
_clearForegroundRecord();
|
|
645
818
|
const elapsedMs = _elapsedSinceStart();
|
|
646
|
-
const
|
|
819
|
+
const remainingBackgroundTimeoutMs = reason === 'timeout'
|
|
647
820
|
? promotedTimeoutMs
|
|
648
821
|
: (backgroundDeadlineMs > 0
|
|
649
822
|
? Math.max(1, backgroundDeadlineMs - elapsedMs)
|
|
650
823
|
: 0);
|
|
651
824
|
try {
|
|
652
|
-
job = await
|
|
825
|
+
job = await promoteForegroundShellJob({
|
|
653
826
|
command,
|
|
654
827
|
cwd,
|
|
655
828
|
pid: child.pid,
|
|
656
|
-
|
|
657
|
-
|
|
829
|
+
jobId: _foregroundRecordId,
|
|
830
|
+
timeoutMs: remainingBackgroundTimeoutMs,
|
|
831
|
+
// Carry the command's own start moment into the job: promotion time
|
|
658
832
|
// and a standby's process-creation time are both wrong.
|
|
659
833
|
startedAtMs: _commandStartedAtMs,
|
|
660
834
|
mergeStderr: false,
|
|
661
835
|
stdoutPath,
|
|
662
836
|
stderrPath,
|
|
663
|
-
// Stamp the
|
|
837
|
+
// Stamp the promoted job with the dispatching terminal's claude.exe
|
|
664
838
|
// pid so the statusline scopes it to the owning session.
|
|
665
839
|
clientHostPid,
|
|
666
840
|
// …and with the dispatching SESSION, so a pooled host (desktop) can
|
|
@@ -682,7 +856,7 @@ export function execShellCommand({
|
|
|
682
856
|
timedOut = true;
|
|
683
857
|
_treeKillForceSettle('timeout');
|
|
684
858
|
} else {
|
|
685
|
-
_treeKillForceSettle('background-
|
|
859
|
+
_treeKillForceSettle('background-promotion-failed');
|
|
686
860
|
}
|
|
687
861
|
return;
|
|
688
862
|
}
|
|
@@ -707,12 +881,12 @@ export function execShellCommand({
|
|
|
707
881
|
try { stderr = await taskOutput.getStderr(); }
|
|
708
882
|
catch (err) { taskOutput.writeError = taskOutput.writeError || err; }
|
|
709
883
|
// Re-check after the awaited capture reads: cancellation can race after
|
|
710
|
-
//
|
|
884
|
+
// promotion commits. Never report that cancelled process as a successful
|
|
711
885
|
// still-running background task.
|
|
712
886
|
// EXCEPTION: an interrupt-driven promotion starts FROM an aborted signal
|
|
713
887
|
// by design (the user typed a new message), so this guard must not undo
|
|
714
888
|
// the very transition it was asked to perform. A plain cancellation still
|
|
715
|
-
// reverts
|
|
889
|
+
// reverts promotion and kills.
|
|
716
890
|
if (abortSignal && abortSignal.aborted
|
|
717
891
|
&& !(reason === 'interrupt' && _abortReasonIsInterrupt(abortSignal))) {
|
|
718
892
|
killed = true;
|
|
@@ -739,11 +913,11 @@ export function execShellCommand({
|
|
|
739
913
|
return;
|
|
740
914
|
}
|
|
741
915
|
// Completed-during-promotion race: the child
|
|
742
|
-
// finished while
|
|
916
|
+
// finished while promotion was committing. Report a clean COMPLETED
|
|
743
917
|
// result instead of backgrounded — the caller then never arms the
|
|
744
918
|
// completion watcher, so no redundant task notification fires. Write
|
|
745
919
|
// the exit/done files here as well: when 'close' fired before the
|
|
746
|
-
// once('close') wiring above, nothing else would ever flip the
|
|
920
|
+
// once('close') wiring above, nothing else would ever flip the promoted
|
|
747
921
|
// job detail off 'running'.
|
|
748
922
|
if (child.exitCode != null || child.signalCode != null) {
|
|
749
923
|
detachAbortHandler();
|
|
@@ -765,7 +939,7 @@ export function execShellCommand({
|
|
|
765
939
|
}));
|
|
766
940
|
return;
|
|
767
941
|
}
|
|
768
|
-
// The
|
|
942
|
+
// The promoted job now owns cancellation through task control. Retaining
|
|
769
943
|
// the foreground caller's signal listener would keep the completed tool
|
|
770
944
|
// frame alive and could later kill an unrelated, already-returned job.
|
|
771
945
|
detachAbortHandler();
|
|
@@ -790,9 +964,9 @@ export function execShellCommand({
|
|
|
790
964
|
outputCaptureError: taskOutput.writeError,
|
|
791
965
|
backgrounded: true,
|
|
792
966
|
jobId,
|
|
793
|
-
backgroundTimeoutMs:
|
|
967
|
+
backgroundTimeoutMs: remainingBackgroundTimeoutMs,
|
|
794
968
|
backgroundMessage: jobId
|
|
795
|
-
? `${_verb}; still running. Completion is automatic;
|
|
969
|
+
? `${_verb}; still running. Completion is automatic; unless periodic task reports were requested, continue independent work or end the turn. When the next step needs the result or the next report interval, call task wait instead of polling task read: it returns the moment the task settles, or hands back the current output at its ceiling so you can re-decide.`
|
|
796
970
|
: `${_verb}; still running — judge from the partial output whether waiting can finish in budget, or diagnose and pursue an alternative.`,
|
|
797
971
|
}),
|
|
798
972
|
);
|
|
@@ -810,6 +984,12 @@ export function execShellCommand({
|
|
|
810
984
|
detachAbortHandler();
|
|
811
985
|
try { if (autoBackgroundJobId) killShellJob(autoBackgroundJobId); } catch {}
|
|
812
986
|
try { treeKill(child); } catch {}
|
|
987
|
+
// settle() is inert from here (settled), so the cleanup it owns has to
|
|
988
|
+
// run in this branch: the reported result carries no spill paths, so
|
|
989
|
+
// the capture files are unreferenced garbage — drop them, close their
|
|
990
|
+
// descriptors and hand back any lease the failed handoff still holds.
|
|
991
|
+
try { taskOutput.deleteFiles(); } catch { /* best-effort */ }
|
|
992
|
+
void releaseResourceLease();
|
|
813
993
|
resolveResult(new ExecResult({
|
|
814
994
|
stdout: '',
|
|
815
995
|
stderr: `resource cleanup failed during background promotion: ${error?.message || error}`,
|
|
@@ -831,13 +1011,12 @@ export function execShellCommand({
|
|
|
831
1011
|
if (timeoutMs > 0) {
|
|
832
1012
|
timer = setTimeout(() => {
|
|
833
1013
|
// Promote-on-timeout: if the caller allows backgrounding and the child
|
|
834
|
-
// is still running
|
|
835
|
-
//
|
|
836
|
-
//
|
|
837
|
-
//
|
|
1014
|
+
// is still running, promote it as a tracked background job instead of
|
|
1015
|
+
// tree-killing it. Falls through to the old kill path for
|
|
1016
|
+
// disallowed/opted-out commands (backgroundOnTimeout false) or when a
|
|
1017
|
+
// terminal transition already won the race.
|
|
838
1018
|
if (
|
|
839
1019
|
backgroundOnTimeout &&
|
|
840
|
-
!_isBackground &&
|
|
841
1020
|
!settled &&
|
|
842
1021
|
!autoBackgrounded &&
|
|
843
1022
|
!killed &&
|
|
@@ -856,9 +1035,8 @@ export function execShellCommand({
|
|
|
856
1035
|
// Live-progress heartbeat: every 2 s while the foreground command runs,
|
|
857
1036
|
// emit "running Ns" so the MCP client renders live progress
|
|
858
1037
|
// instead of an opaque hang. Only armed for a genuine foreground run with
|
|
859
|
-
// a subscribed client
|
|
860
|
-
|
|
861
|
-
if (_hasProgress && !_isBackground) {
|
|
1038
|
+
// a subscribed client. Cleared on settle / auto-background (see above).
|
|
1039
|
+
if (_hasProgress) {
|
|
862
1040
|
progressTimer = setInterval(() => {
|
|
863
1041
|
if (settled || autoBackgrounded) return;
|
|
864
1042
|
const secs = Math.round(_elapsedSinceStart() / 1000);
|
|
@@ -869,7 +1047,7 @@ export function execShellCommand({
|
|
|
869
1047
|
|
|
870
1048
|
// Live output tail pump (see declaration above). Emits only on change so
|
|
871
1049
|
// idle commands cost one getLiveTail per second and zero downstream work.
|
|
872
|
-
if (_hasOutputTail
|
|
1050
|
+
if (_hasOutputTail) {
|
|
873
1051
|
outputTailTimer = setInterval(() => {
|
|
874
1052
|
if (settled || autoBackgrounded) return;
|
|
875
1053
|
try {
|
|
@@ -884,12 +1062,10 @@ export function execShellCommand({
|
|
|
884
1062
|
}
|
|
885
1063
|
|
|
886
1064
|
// Arm the auto-background timer only for the genuine foreground one-shot
|
|
887
|
-
// path: a positive threshold strictly below the hard timeout
|
|
888
|
-
// trailing-`&` background command (those already detach + settle on exit).
|
|
1065
|
+
// path: a positive threshold strictly below the hard timeout.
|
|
889
1066
|
if (
|
|
890
1067
|
typeof autoBackgroundMs === 'number' &&
|
|
891
1068
|
autoBackgroundMs > 0 &&
|
|
892
|
-
!_isBackground &&
|
|
893
1069
|
(timeoutMs <= 0 || autoBackgroundMs < timeoutMs)
|
|
894
1070
|
) {
|
|
895
1071
|
autoBgTimer = setTimeout(() => { fireAutoBackground(); }, autoBackgroundMs);
|
|
@@ -904,11 +1080,10 @@ export function execShellCommand({
|
|
|
904
1080
|
// to stop is the worse outcome. Explicit cancellation (ESC) keeps the
|
|
905
1081
|
// kill. The promotion reuses the timeout path's guards verbatim so a
|
|
906
1082
|
// detached / already-settled / already-exited child can never be
|
|
907
|
-
//
|
|
1083
|
+
// promoted as a job.
|
|
908
1084
|
if (
|
|
909
1085
|
_abortReasonIsInterrupt(abortSignal) &&
|
|
910
1086
|
backgroundOnTimeout &&
|
|
911
|
-
!_isBackground &&
|
|
912
1087
|
!settled &&
|
|
913
1088
|
!autoBackgrounded &&
|
|
914
1089
|
!killed &&
|