@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/README.md +1 -1
- package/dist/cli.js +4685 -4614
- package/dist/types/async/job-manager.d.ts +37 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +41 -0
- package/dist/types/commands/bench.d.ts +15 -3
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/models-config-schema.d.ts +18 -6
- package/dist/types/config/models-config.d.ts +14 -5
- package/dist/types/config/provider-globals.d.ts +2 -2
- package/dist/types/config/service-tier.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +374 -47
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/dap/client.d.ts +20 -0
- package/dist/types/exec/bash-executor.d.ts +38 -0
- package/dist/types/exec/direnv.d.ts +33 -0
- package/dist/types/export/html/args.d.ts +15 -0
- package/dist/types/export/html/index.d.ts +11 -26
- package/dist/types/export/html/web-palette.d.ts +55 -116
- package/dist/types/extensibility/extensions/loader.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
- package/dist/types/live/attestation.d.ts +2 -0
- package/dist/types/live/controller.d.ts +48 -0
- package/dist/types/live/protocol.d.ts +85 -0
- package/dist/types/live/protocol.test.d.ts +1 -0
- package/dist/types/live/transport.d.ts +35 -0
- package/dist/types/live/visualizer.d.ts +30 -0
- package/dist/types/lsp/client.d.ts +16 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/memory-backend/tool-names.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/custom-message.d.ts +1 -1
- package/dist/types/modes/components/login-dialog.d.ts +2 -2
- package/dist/types/modes/components/message-frame.d.ts +8 -4
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/session-account-selector.d.ts +11 -0
- package/dist/types/modes/components/settings-defs.d.ts +7 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +6 -1
- package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -2
- package/dist/types/plan-mode/plan-files.d.ts +10 -0
- package/dist/types/registry/agent-lifecycle.d.ts +21 -9
- package/dist/types/registry/agent-registry.d.ts +11 -4
- package/dist/types/sdk.d.ts +15 -1
- package/dist/types/secrets/index.d.ts +15 -2
- package/dist/types/secrets/obfuscator.d.ts +113 -15
- package/dist/types/session/acp-permission-gate.d.ts +19 -0
- package/dist/types/session/agent-session-events.d.ts +84 -0
- package/dist/types/session/agent-session-types.d.ts +321 -0
- package/dist/types/session/agent-session.d.ts +135 -753
- package/dist/types/session/async-job-delivery.d.ts +28 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/bash-runner.d.ts +75 -0
- package/dist/types/session/blob-store.d.ts +11 -1
- package/dist/types/session/blob-store.test.d.ts +1 -0
- package/dist/types/session/checkpoint-entries.d.ts +28 -0
- package/dist/types/session/eval-runner.d.ts +51 -0
- package/dist/types/session/irc-bridge.d.ts +46 -0
- package/dist/types/session/messages.d.ts +24 -0
- package/dist/types/session/model-controls.d.ts +180 -0
- package/dist/types/session/prewalk.d.ts +46 -0
- package/dist/types/session/queued-messages.d.ts +22 -0
- package/dist/types/session/redis-session-storage.d.ts +1 -0
- package/dist/types/session/retry-fallback-chains.d.ts +74 -0
- package/dist/types/session/role-models.d.ts +15 -0
- package/dist/types/session/session-advisors.d.ts +228 -0
- package/dist/types/session/session-entries.d.ts +8 -0
- package/dist/types/session/session-handoff.d.ts +70 -0
- package/dist/types/session/session-history-format.d.ts +6 -1
- package/dist/types/session/session-maintenance.d.ts +246 -0
- package/dist/types/session/session-manager.d.ts +41 -0
- package/dist/types/session/session-memory.d.ts +56 -0
- package/dist/types/session/session-provider-boundary.d.ts +62 -0
- package/dist/types/session/session-stats.d.ts +48 -0
- package/dist/types/session/session-tools.d.ts +158 -0
- package/dist/types/session/session-workspace.d.ts +32 -0
- package/dist/types/session/stream-guards.d.ts +44 -0
- package/dist/types/session/streaming-output.d.ts +8 -0
- package/dist/types/session/todo-tracker.d.ts +56 -0
- package/dist/types/session/ttsr-coordinator.d.ts +51 -0
- package/dist/types/session/turn-recovery.d.ts +225 -0
- package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
- package/dist/types/stt/index.d.ts +0 -2
- package/dist/types/stt/stt-controller.d.ts +7 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +4 -1
- package/dist/types/task/output-manager.d.ts +3 -1
- package/dist/types/tiny/title-client.d.ts +10 -0
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/computer/protocol.d.ts +43 -0
- package/dist/types/tools/computer/supervisor.d.ts +32 -0
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +15 -0
- package/dist/types/tools/computer-renderer.d.ts +22 -0
- package/dist/types/tools/computer.d.ts +71 -0
- package/dist/types/tools/context.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +21 -0
- package/dist/types/tools/image-gen.d.ts +4 -4
- package/dist/types/tools/image-providers.d.ts +38 -0
- package/dist/types/tools/index.d.ts +7 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/todo.d.ts +10 -4
- package/dist/types/tools/xdev.d.ts +5 -1
- package/dist/types/tts/streaming-player.d.ts +10 -29
- package/dist/types/tts/vocalizer.d.ts +5 -0
- package/dist/types/utils/jj.d.ts +29 -0
- package/dist/types/utils/lang-from-path.d.ts +1 -0
- package/dist/types/utils/title-generator.d.ts +30 -0
- package/dist/types/utils/tools-manager.d.ts +1 -2
- package/dist/types/vibe/runtime.d.ts +36 -8
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +15 -5
- package/dist/types/web/search/types.d.ts +94 -0
- package/package.json +12 -12
- package/scripts/generate-share-viewer.ts +4 -6
- package/src/advisor/__tests__/advisor.test.ts +643 -0
- package/src/advisor/runtime.ts +144 -43
- package/src/async/job-manager.ts +90 -2
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-gateway-cli.ts +18 -3
- package/src/cli/bench-cli.ts +395 -29
- package/src/cli/flag-tables.ts +3 -0
- package/src/cli/gc-cli.ts +2 -2
- package/src/cli/setup-cli.ts +2 -14
- package/src/cli/usage-cli.ts +10 -1
- package/src/cli.ts +8 -0
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/model-registry.ts +16 -2
- package/src/config/models-config-schema.ts +15 -4
- package/src/config/provider-globals.ts +9 -9
- package/src/config/service-tier.ts +26 -1
- package/src/config/settings-schema.ts +270 -51
- package/src/config/settings.ts +81 -2
- package/src/dap/client.ts +37 -4
- package/src/edit/index.ts +49 -12
- package/src/eval/executor-base.ts +1 -0
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/py/executor.ts +103 -33
- package/src/exec/bash-executor.ts +90 -6
- package/src/exec/direnv.ts +145 -0
- package/src/export/html/args.ts +20 -0
- package/src/export/html/index.ts +45 -47
- package/src/export/html/template.css +19 -1
- package/src/export/html/template.html +6 -0
- package/src/export/html/template.js +21 -0
- package/src/export/html/tool-views.generated.js +31 -31
- package/src/export/html/web-palette.ts +116 -132
- package/src/export/share.ts +249 -49
- package/src/extensibility/extensions/loader.ts +29 -1
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +33 -2
- package/src/extensibility/extensions/wrapper.ts +68 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
- package/src/hindsight/state.ts +64 -6
- package/src/internal-urls/local-protocol.ts +2 -1
- package/src/internal-urls/memory-protocol.ts +2 -2
- package/src/internal-urls/skill-protocol.ts +2 -2
- package/src/internal-urls/ssh-protocol.ts +2 -1
- package/src/internal-urls/vault-protocol.ts +2 -1
- package/src/live/attestation.ts +91 -0
- package/src/live/controller.ts +517 -0
- package/src/live/prompts/agent-final-message.md +3 -0
- package/src/live/prompts/live-instructions.md +23 -0
- package/src/live/protocol.test.ts +140 -0
- package/src/live/protocol.ts +233 -0
- package/src/live/transport.ts +422 -0
- package/src/live/visualizer.ts +214 -0
- package/src/lsp/client.ts +29 -9
- package/src/lsp/index.ts +19 -4
- package/src/main.ts +8 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- package/src/memories/index.ts +40 -20
- package/src/memory-backend/tool-names.ts +2 -0
- package/src/mnemopi/backend.ts +24 -7
- package/src/modes/acp/acp-agent.ts +56 -10
- package/src/modes/acp/acp-event-mapper.ts +8 -1
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/assistant-message.ts +7 -2
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/message-frame.ts +14 -8
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/session-account-selector.ts +62 -0
- package/src/modes/components/settings-defs.ts +23 -1
- package/src/modes/components/settings-selector.ts +169 -1
- package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
- package/src/modes/components/status-line/component.ts +123 -2
- package/src/modes/components/tool-execution.ts +28 -112
- package/src/modes/controllers/command-controller.ts +56 -15
- package/src/modes/controllers/event-controller.ts +111 -16
- package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -4
- package/src/modes/controllers/input-controller.ts +47 -39
- package/src/modes/controllers/live-command-controller.ts +255 -0
- package/src/modes/controllers/mcp-command-controller.ts +10 -1
- package/src/modes/controllers/omfg-controller.ts +44 -40
- package/src/modes/controllers/selector-controller.ts +80 -10
- package/src/modes/controllers/session-focus-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +176 -38
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/context-usage.ts +23 -7
- package/src/modes/utils/ui-helpers.ts +10 -5
- package/src/plan-mode/plan-files.ts +40 -0
- package/src/prompts/bench/cache-prefix-chunk.md +1 -0
- package/src/prompts/bench/cache-prefix.md +3 -0
- package/src/prompts/bench/cache-suffix.md +1 -0
- package/src/prompts/system/computer-safety.md +14 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/project-prompt.md +8 -1
- package/src/prompts/system/subagent-async-pending.md +6 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/workflow-notice.md +48 -40
- package/src/prompts/system/xdev-mount-notice.md +4 -0
- package/src/prompts/tools/computer.md +26 -0
- package/src/prompts/tools/todo.md +3 -1
- package/src/registry/agent-lifecycle.ts +116 -46
- package/src/registry/agent-registry.ts +35 -8
- package/src/registry/persisted-agents.ts +26 -2
- package/src/sdk.ts +215 -136
- package/src/secrets/index.ts +127 -4
- package/src/secrets/obfuscator.ts +2328 -105
- package/src/session/acp-permission-gate.ts +165 -0
- package/src/session/agent-session-error-log.test.ts +1 -1
- package/src/session/agent-session-events.ts +66 -0
- package/src/session/agent-session-types.ts +343 -0
- package/src/session/agent-session.ts +1940 -12218
- package/src/session/artifacts.ts +6 -5
- package/src/session/async-job-delivery.ts +74 -0
- package/src/session/auth-storage.ts +1 -0
- package/src/session/bash-runner.ts +326 -0
- package/src/session/blob-store.test.ts +56 -0
- package/src/session/blob-store.ts +18 -2
- package/src/session/checkpoint-entries.ts +81 -0
- package/src/session/eval-runner.ts +217 -0
- package/src/session/indexed-session-storage.ts +9 -2
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/messages.ts +266 -1
- package/src/session/model-controls.ts +714 -0
- package/src/session/prewalk.ts +252 -0
- package/src/session/queued-messages.ts +93 -0
- package/src/session/redis-session-storage.ts +51 -11
- package/src/session/retry-fallback-chains.ts +455 -0
- package/src/session/role-models.ts +85 -0
- package/src/session/session-advisors.ts +1679 -0
- package/src/session/session-context.ts +6 -3
- package/src/session/session-entries.ts +8 -0
- package/src/session/session-handoff.ts +308 -0
- package/src/session/session-history-format.ts +20 -9
- package/src/session/session-loader.ts +0 -46
- package/src/session/session-maintenance.ts +2983 -0
- package/src/session/session-manager.ts +390 -30
- package/src/session/session-memory.ts +222 -0
- package/src/session/session-provider-boundary.ts +307 -0
- package/src/session/session-stats.ts +293 -0
- package/src/session/session-tools.ts +980 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/streaming-output.ts +52 -5
- package/src/session/todo-tracker.ts +380 -0
- package/src/session/ttsr-coordinator.ts +496 -0
- package/src/session/turn-recovery.ts +1629 -0
- package/src/slash-commands/builtin-registry.ts +257 -20
- package/src/slash-commands/helpers/session-pin.ts +44 -0
- package/src/slash-commands/helpers/usage-report.ts +22 -3
- package/src/stt/downloader.ts +0 -2
- package/src/stt/index.ts +0 -2
- package/src/stt/stt-controller.ts +57 -146
- package/src/system-prompt.ts +45 -17
- package/src/task/executor.ts +285 -70
- package/src/task/index.ts +98 -105
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +1 -0
- package/src/tiny/title-client.ts +22 -0
- package/src/tools/approval.ts +55 -10
- package/src/tools/bash-interactive.ts +116 -86
- package/src/tools/bash-skill-urls.ts +28 -2
- package/src/tools/bash.ts +339 -123
- package/src/tools/browser/launch.ts +9 -1
- package/src/tools/browser/tab-supervisor.ts +8 -5
- package/src/tools/browser.ts +94 -47
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/computer/protocol.ts +28 -0
- package/src/tools/computer/supervisor.ts +258 -0
- package/src/tools/computer/worker-entry.ts +25 -0
- package/src/tools/computer/worker.ts +135 -0
- package/src/tools/computer-renderer.ts +108 -0
- package/src/tools/computer.ts +433 -0
- package/src/tools/context.ts +2 -0
- package/src/tools/default-renderer.ts +139 -0
- package/src/tools/essential-tools.ts +1 -0
- package/src/tools/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +14 -0
- package/src/tools/path-utils.ts +49 -2
- package/src/tools/read.ts +156 -88
- package/src/tools/renderers.ts +9 -1
- package/src/tools/todo.ts +101 -11
- package/src/tools/vibe.ts +1 -2
- package/src/tools/write.ts +51 -1
- package/src/tools/xdev.ts +121 -29
- package/src/tts/streaming-player.ts +82 -283
- package/src/tts/vocalizer.ts +18 -1
- package/src/utils/clipboard.ts +1 -30
- package/src/utils/jj.ts +125 -4
- package/src/utils/lang-from-path.ts +7 -0
- package/src/utils/mac-file-urls.applescript +37 -0
- package/src/utils/title-generator.ts +137 -1
- package/src/utils/tool-choice.ts +14 -0
- package/src/utils/tools-manager.ts +1 -19
- package/src/vibe/runtime.ts +852 -72
- package/src/web/search/index.ts +3 -6
- package/src/web/search/provider.ts +31 -16
- package/src/web/search/providers/xai.ts +7 -1
- package/src/web/search/types.ts +3 -0
- package/dist/types/stt/recorder.d.ts +0 -30
- package/dist/types/stt/transcriber.d.ts +0 -14
- package/dist/types/stt/wav.d.ts +0 -29
- package/dist/types/tts/player.d.ts +0 -32
- package/src/stt/recorder.ts +0 -551
- package/src/stt/transcriber.ts +0 -60
- package/src/stt/wav.ts +0 -173
- package/src/tts/player.ts +0 -137
package/src/dap/client.ts
CHANGED
|
@@ -260,7 +260,7 @@ export class DapClient {
|
|
|
260
260
|
// socket connect fails, we must not leak the detached adapter process.
|
|
261
261
|
try {
|
|
262
262
|
await waitForCondition(() => isUnixSocketReady(socketPath), timeoutMs, proc);
|
|
263
|
-
const { readable, writeSink, socket } = await connectSocket({ unix: socketPath });
|
|
263
|
+
const { readable, writeSink, socket } = await connectSocket({ unix: socketPath }, timeoutMs);
|
|
264
264
|
const client = new DapClient(adapter, cwd, proc, { readable, writeSink, socket });
|
|
265
265
|
proc.exited.then(() => client.#handleProcessExit());
|
|
266
266
|
void client.#startMessageReader();
|
|
@@ -924,10 +924,20 @@ function socketToSink(socket: Bun.Socket<undefined>): DapWriteSink {
|
|
|
924
924
|
};
|
|
925
925
|
}
|
|
926
926
|
|
|
927
|
-
/**
|
|
928
|
-
|
|
929
|
-
|
|
927
|
+
/**
|
|
928
|
+
* Connect to a unix domain socket and return DAP transport streams.
|
|
929
|
+
*
|
|
930
|
+
* Rejects (rather than hanging) when the connect fails — a stat-ready but dead
|
|
931
|
+
* socket returns ECONNREFUSED, a socket removed between the readiness stat and
|
|
932
|
+
* the connect returns ENOENT, a permission mismatch returns EACCES — and when
|
|
933
|
+
* neither `open` nor an error arrives within `timeoutMs` (e.g. a TOCTOU stall).
|
|
934
|
+
* `#spawnSocketUnix`'s catch then kills the detached adapter instead of leaking
|
|
935
|
+
* it. Exported so tests can drive the reject path deterministically.
|
|
936
|
+
*/
|
|
937
|
+
export async function connectSocket(options: { unix: string }, timeoutMs: number): Promise<SocketTransport> {
|
|
938
|
+
const { promise, resolve, reject } = Promise.withResolvers<SocketTransport>();
|
|
930
939
|
let streamController: ReadableStreamDefaultController<Uint8Array>;
|
|
940
|
+
let opened = false;
|
|
931
941
|
|
|
932
942
|
const readable = new ReadableStream<Uint8Array>({
|
|
933
943
|
start(controller) {
|
|
@@ -935,10 +945,21 @@ async function connectSocket(options: { unix: string }): Promise<SocketTransport
|
|
|
935
945
|
},
|
|
936
946
|
});
|
|
937
947
|
|
|
948
|
+
const timer = setTimeout(() => {
|
|
949
|
+
reject(new Error(`Timed out connecting to unix socket ${options.unix} after ${timeoutMs}ms`));
|
|
950
|
+
}, timeoutMs);
|
|
951
|
+
// A late socket callback after settle is a no-op; clearing the timer just
|
|
952
|
+
// stops it from keeping the event loop alive past the connect.
|
|
953
|
+
void promise.then(
|
|
954
|
+
() => clearTimeout(timer),
|
|
955
|
+
() => clearTimeout(timer),
|
|
956
|
+
);
|
|
957
|
+
|
|
938
958
|
Bun.connect({
|
|
939
959
|
unix: options.unix,
|
|
940
960
|
socket: {
|
|
941
961
|
open(socket) {
|
|
962
|
+
opened = true;
|
|
942
963
|
resolve({
|
|
943
964
|
readable,
|
|
944
965
|
writeSink: socketToSink(socket),
|
|
@@ -949,6 +970,9 @@ async function connectSocket(options: { unix: string }): Promise<SocketTransport
|
|
|
949
970
|
streamController.enqueue(new Uint8Array(data));
|
|
950
971
|
},
|
|
951
972
|
close() {
|
|
973
|
+
if (!opened) {
|
|
974
|
+
reject(new Error(`Unix socket ${options.unix} closed before opening`));
|
|
975
|
+
}
|
|
952
976
|
try {
|
|
953
977
|
streamController.close();
|
|
954
978
|
} catch {
|
|
@@ -956,6 +980,9 @@ async function connectSocket(options: { unix: string }): Promise<SocketTransport
|
|
|
956
980
|
}
|
|
957
981
|
},
|
|
958
982
|
error(_socket, err) {
|
|
983
|
+
if (!opened) {
|
|
984
|
+
reject(err);
|
|
985
|
+
}
|
|
959
986
|
try {
|
|
960
987
|
streamController.error(err);
|
|
961
988
|
} catch {
|
|
@@ -963,6 +990,12 @@ async function connectSocket(options: { unix: string }): Promise<SocketTransport
|
|
|
963
990
|
}
|
|
964
991
|
},
|
|
965
992
|
},
|
|
993
|
+
}).catch(err => {
|
|
994
|
+
// Bun.connect rejects the returned promise on synchronous connect
|
|
995
|
+
// failures (e.g. ENOENT) without always firing the `error` handler.
|
|
996
|
+
if (!opened) {
|
|
997
|
+
reject(err);
|
|
998
|
+
}
|
|
966
999
|
});
|
|
967
1000
|
|
|
968
1001
|
return promise;
|
package/src/edit/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import hashlineGrammar from "@oh-my-pi/hashline/grammar.lark" with { type: "text
|
|
|
3
3
|
import hashlineDescription from "@oh-my-pi/hashline/prompt.md" with { type: "text" };
|
|
4
4
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import type { ToolExample } from "@oh-my-pi/pi-ai";
|
|
6
|
-
import { prompt } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { isEnoent, isEnotdir, prompt } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { createLspWritethrough, flushLspWritethroughBatch, type WritethroughCallback, writethroughNoop } from "../lsp";
|
|
8
8
|
import { DeferredDiagnostics } from "../lsp/deferred-diagnostics";
|
|
9
9
|
import { getDiagnosticsLedger } from "../lsp/diagnostics-ledger";
|
|
@@ -12,7 +12,8 @@ import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
|
|
|
12
12
|
import replaceDescription from "../prompts/tools/replace.md" with { type: "text" };
|
|
13
13
|
import type { ToolSession } from "../tools";
|
|
14
14
|
import { truncateForPrompt } from "../tools/approval";
|
|
15
|
-
import { isInternalUrlPath } from "../tools/path-utils";
|
|
15
|
+
import { findUniqueWorkspaceSuffix, isInternalUrlPath } from "../tools/path-utils";
|
|
16
|
+
import { resolvePlanPath } from "../tools/plan-mode-guard";
|
|
16
17
|
import { type EditMode, normalizeEditMode, resolveEditMode } from "../utils/edit-mode";
|
|
17
18
|
import { executeHashlineSingle, hashlineEditParamsSchema } from "./hashline";
|
|
18
19
|
import { type ApplyPatchParams, applyPatchSchema, expandApplyPatchToEntries } from "./modes/apply-patch";
|
|
@@ -73,6 +74,24 @@ function resolveConfiguredEditMode(rawEditMode: string): EditMode | undefined {
|
|
|
73
74
|
return editMode;
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
async function resolveEditPath(
|
|
78
|
+
session: ToolSession,
|
|
79
|
+
authoredPath: string,
|
|
80
|
+
options: { mustExist: boolean; signal?: AbortSignal },
|
|
81
|
+
): Promise<string> {
|
|
82
|
+
if (!options.mustExist || isInternalUrlPath(authoredPath)) return authoredPath;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await Bun.file(resolvePlanPath(session, authoredPath)).stat();
|
|
86
|
+
return authoredPath;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!isEnoent(error) && !isEnotdir(error)) throw error;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const match = await findUniqueWorkspaceSuffix(authoredPath, session.cwd, options.signal);
|
|
92
|
+
return match?.displayPath ?? authoredPath;
|
|
93
|
+
}
|
|
94
|
+
|
|
76
95
|
function resolveAllowFuzzy(session: ToolSession, rawValue: string): boolean {
|
|
77
96
|
switch (rawValue) {
|
|
78
97
|
case "true":
|
|
@@ -517,7 +536,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
517
536
|
note: "All entries in one call apply to the top-level `path`; use separate calls for different files.",
|
|
518
537
|
},
|
|
519
538
|
] satisfies readonly ToolExample<PatchParams>[],
|
|
520
|
-
execute: (
|
|
539
|
+
execute: async (
|
|
521
540
|
tool: EditTool,
|
|
522
541
|
params: EditParams,
|
|
523
542
|
signal: AbortSignal | undefined,
|
|
@@ -525,11 +544,15 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
525
544
|
onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
|
|
526
545
|
) => {
|
|
527
546
|
const { edits, path } = params as PatchParams;
|
|
547
|
+
const targetPath = await resolveEditPath(tool.session, path, {
|
|
548
|
+
mustExist: (edits[0]?.op ?? "update") !== "create",
|
|
549
|
+
signal,
|
|
550
|
+
});
|
|
528
551
|
const runs = (edits as PatchEditEntry[]).map(
|
|
529
552
|
entry => (br: LspBatchRequest | undefined) =>
|
|
530
553
|
executePatchSingle({
|
|
531
554
|
session: tool.session,
|
|
532
|
-
path,
|
|
555
|
+
path: targetPath,
|
|
533
556
|
params: entry,
|
|
534
557
|
signal,
|
|
535
558
|
batchRequest: br,
|
|
@@ -542,7 +565,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
542
565
|
beginDeferredDiagnosticsForPath: p => tool.#deferredDiagnostics.begin(p),
|
|
543
566
|
}),
|
|
544
567
|
);
|
|
545
|
-
return executeSinglePathEntries(
|
|
568
|
+
return executeSinglePathEntries(targetPath, runs, batchRequest, onUpdate, tool.session.cwd, signal);
|
|
546
569
|
},
|
|
547
570
|
},
|
|
548
571
|
apply_patch: {
|
|
@@ -564,14 +587,26 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
564
587
|
onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
|
|
565
588
|
) => {
|
|
566
589
|
const entries = expandApplyPatchToEntries(params as ApplyPatchParams);
|
|
590
|
+
// Resolve each authored path once per patch so paired hunks (e.g. delete
|
|
591
|
+
// then re-add of the same file) share the same workspace target.
|
|
592
|
+
const resolvedTargets = new Map<string, Promise<string>>();
|
|
593
|
+
const resolveOnce = (path: string, mustExist: boolean): Promise<string> => {
|
|
594
|
+
let pending = resolvedTargets.get(path);
|
|
595
|
+
if (!pending) {
|
|
596
|
+
pending = resolveEditPath(tool.session, path, { mustExist, signal });
|
|
597
|
+
resolvedTargets.set(path, pending);
|
|
598
|
+
}
|
|
599
|
+
return pending;
|
|
600
|
+
};
|
|
567
601
|
const perFile = entries.map(entry => {
|
|
568
602
|
const { path, ...patchParams } = entry;
|
|
569
603
|
return {
|
|
570
604
|
path,
|
|
571
|
-
run: (br: LspBatchRequest | undefined) =>
|
|
572
|
-
|
|
605
|
+
run: async (br: LspBatchRequest | undefined) => {
|
|
606
|
+
const targetPath = await resolveOnce(path, patchParams.op !== "create");
|
|
607
|
+
return executePatchSingle({
|
|
573
608
|
session: tool.session,
|
|
574
|
-
path,
|
|
609
|
+
path: targetPath,
|
|
575
610
|
params: patchParams,
|
|
576
611
|
signal,
|
|
577
612
|
batchRequest: br,
|
|
@@ -579,7 +614,8 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
579
614
|
fuzzyThreshold: tool.#fuzzyThreshold,
|
|
580
615
|
writethrough: tool.#writethrough,
|
|
581
616
|
beginDeferredDiagnosticsForPath: p => tool.#deferredDiagnostics.begin(p),
|
|
582
|
-
})
|
|
617
|
+
});
|
|
618
|
+
},
|
|
583
619
|
};
|
|
584
620
|
});
|
|
585
621
|
return executeApplyPatchPerFile(perFile, batchRequest, tool.session.cwd, signal, onUpdate);
|
|
@@ -609,7 +645,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
609
645
|
replace: {
|
|
610
646
|
description: () => prompt.render(replaceDescription),
|
|
611
647
|
parameters: replaceEditSchema,
|
|
612
|
-
execute: (
|
|
648
|
+
execute: async (
|
|
613
649
|
tool: EditTool,
|
|
614
650
|
params: EditParams,
|
|
615
651
|
signal: AbortSignal | undefined,
|
|
@@ -617,11 +653,12 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
617
653
|
onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
|
|
618
654
|
) => {
|
|
619
655
|
const { edits, path } = params as ReplaceParams;
|
|
656
|
+
const targetPath = await resolveEditPath(tool.session, path, { mustExist: true, signal });
|
|
620
657
|
const runs = (edits as ReplaceEditEntry[]).map(
|
|
621
658
|
entry => (br: LspBatchRequest | undefined) =>
|
|
622
659
|
executeReplaceSingle({
|
|
623
660
|
session: tool.session,
|
|
624
|
-
path,
|
|
661
|
+
path: targetPath,
|
|
625
662
|
params: entry,
|
|
626
663
|
signal,
|
|
627
664
|
batchRequest: br,
|
|
@@ -631,7 +668,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
631
668
|
beginDeferredDiagnosticsForPath: p => tool.#deferredDiagnostics.begin(p),
|
|
632
669
|
}),
|
|
633
670
|
);
|
|
634
|
-
return executeSinglePathEntries(
|
|
671
|
+
return executeSinglePathEntries(targetPath, runs, batchRequest, onUpdate, tool.session.cwd, signal);
|
|
635
672
|
},
|
|
636
673
|
},
|
|
637
674
|
}[this.mode];
|
package/src/eval/js/executor.ts
CHANGED
package/src/eval/py/executor.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type KernelDisplayOutput,
|
|
22
22
|
type KernelExecuteOptions,
|
|
23
23
|
type KernelExecuteResult,
|
|
24
|
+
type KernelShutdownResult,
|
|
24
25
|
PythonKernel,
|
|
25
26
|
} from "./kernel";
|
|
26
27
|
import { resolveExplicitPythonRuntime } from "./runtime";
|
|
@@ -136,11 +137,19 @@ export interface PythonResult {
|
|
|
136
137
|
// the same tuple; the kernel stays alive until the last owner detaches.
|
|
137
138
|
// ---------------------------------------------------------------------------
|
|
138
139
|
|
|
140
|
+
interface SessionKernelReplacement {
|
|
141
|
+
generation: number;
|
|
142
|
+
deadlineMs?: number;
|
|
143
|
+
promise: Promise<PythonKernel>;
|
|
144
|
+
}
|
|
145
|
+
|
|
139
146
|
interface PythonSession {
|
|
140
147
|
sessionKey: string;
|
|
141
148
|
sessionId: string;
|
|
142
149
|
cwd: string;
|
|
143
150
|
kernel: PythonKernel;
|
|
151
|
+
generation: number;
|
|
152
|
+
replacement?: SessionKernelReplacement;
|
|
144
153
|
ownerIds: Set<string>;
|
|
145
154
|
hasFallbackOwner: boolean;
|
|
146
155
|
}
|
|
@@ -254,6 +263,7 @@ async function acquireSession(
|
|
|
254
263
|
sessionId,
|
|
255
264
|
cwd,
|
|
256
265
|
kernel,
|
|
266
|
+
generation: 0,
|
|
257
267
|
ownerIds: new Set(),
|
|
258
268
|
hasFallbackOwner: false,
|
|
259
269
|
};
|
|
@@ -272,31 +282,99 @@ async function acquireSession(
|
|
|
272
282
|
|
|
273
283
|
async function replaceSessionKernel(
|
|
274
284
|
session: PythonSession,
|
|
285
|
+
kernel: PythonKernel,
|
|
286
|
+
generation: number,
|
|
275
287
|
cwd: string,
|
|
276
288
|
options: PythonExecutorOptions,
|
|
277
|
-
): Promise<
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
289
|
+
): Promise<PythonKernel> {
|
|
290
|
+
const inFlight = session.replacement;
|
|
291
|
+
if (inFlight?.generation === generation) {
|
|
292
|
+
if (
|
|
293
|
+
inFlight.deadlineMs !== undefined &&
|
|
294
|
+
(options.deadlineMs === undefined || options.deadlineMs > inFlight.deadlineMs)
|
|
295
|
+
) {
|
|
296
|
+
inFlight.deadlineMs = options.deadlineMs;
|
|
297
|
+
}
|
|
298
|
+
return await waitForPromiseWithCancellation(inFlight.promise, options, PythonExecutionCancelledError);
|
|
285
299
|
}
|
|
286
|
-
|
|
287
|
-
const next = await startKernel(cwd, options);
|
|
288
|
-
if (sessions.get(session.sessionKey) !== session) {
|
|
289
|
-
await next.shutdown().catch(() => undefined);
|
|
300
|
+
if (sessions.get(session.sessionKey) !== session || session.generation !== generation || session.kernel !== kernel) {
|
|
290
301
|
throw new PythonExecutionCancelledError(false);
|
|
291
302
|
}
|
|
292
|
-
|
|
303
|
+
|
|
304
|
+
const deferred = Promise.withResolvers<PythonKernel>();
|
|
305
|
+
const replacement: SessionKernelReplacement = {
|
|
306
|
+
generation,
|
|
307
|
+
deadlineMs: options.deadlineMs,
|
|
308
|
+
promise: deferred.promise,
|
|
309
|
+
};
|
|
310
|
+
session.replacement = replacement;
|
|
311
|
+
void (async () => {
|
|
312
|
+
try {
|
|
313
|
+
const remaining = getRemainingTimeoutMs(options.deadlineMs);
|
|
314
|
+
await kernel
|
|
315
|
+
.shutdown(remaining !== undefined ? { timeoutMs: Math.max(0, remaining) } : undefined)
|
|
316
|
+
.catch(() => undefined);
|
|
317
|
+
if (replacement.deadlineMs !== undefined && replacement.deadlineMs <= Date.now()) {
|
|
318
|
+
throw new PythonExecutionCancelledError(true);
|
|
319
|
+
}
|
|
320
|
+
if (
|
|
321
|
+
sessions.get(session.sessionKey) !== session ||
|
|
322
|
+
session.generation !== generation ||
|
|
323
|
+
session.kernel !== kernel
|
|
324
|
+
) {
|
|
325
|
+
throw new PythonExecutionCancelledError(false);
|
|
326
|
+
}
|
|
327
|
+
const next = await startKernel(cwd, {
|
|
328
|
+
...options,
|
|
329
|
+
signal: undefined,
|
|
330
|
+
deadlineMs: undefined,
|
|
331
|
+
});
|
|
332
|
+
if (
|
|
333
|
+
sessions.get(session.sessionKey) !== session ||
|
|
334
|
+
session.generation !== generation ||
|
|
335
|
+
session.kernel !== kernel
|
|
336
|
+
) {
|
|
337
|
+
await next.shutdown().catch(() => undefined);
|
|
338
|
+
throw new PythonExecutionCancelledError(false);
|
|
339
|
+
}
|
|
340
|
+
session.kernel = next;
|
|
341
|
+
session.generation += 1;
|
|
342
|
+
deferred.resolve(next);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
deferred.reject(err);
|
|
345
|
+
} finally {
|
|
346
|
+
if (session.replacement === replacement) session.replacement = undefined;
|
|
347
|
+
}
|
|
348
|
+
})();
|
|
349
|
+
return await waitForPromiseWithCancellation(deferred.promise, options, PythonExecutionCancelledError);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function shutdownInvalidatedSession(session: PythonSession): Promise<KernelShutdownResult> {
|
|
353
|
+
const replacement = session.replacement;
|
|
354
|
+
if (replacement) await replacement.promise.catch(() => undefined);
|
|
355
|
+
return await session.kernel.shutdown();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function acquireLiveSessionKernel(
|
|
359
|
+
session: PythonSession,
|
|
360
|
+
cwd: string,
|
|
361
|
+
options: PythonExecutorOptions,
|
|
362
|
+
): Promise<PythonKernel> {
|
|
363
|
+
while (sessions.get(session.sessionKey) === session) {
|
|
364
|
+
const kernel = session.kernel;
|
|
365
|
+
const generation = session.generation;
|
|
366
|
+
if (kernel.isAlive()) return kernel;
|
|
367
|
+
await replaceSessionKernel(session, kernel, generation, cwd, options);
|
|
368
|
+
}
|
|
369
|
+
throw new PythonExecutionCancelledError(false);
|
|
293
370
|
}
|
|
294
371
|
|
|
295
372
|
async function resetSession(sessionKey: string): Promise<void> {
|
|
296
373
|
const existing = sessions.get(sessionKey) ?? (await startingSessions.get(sessionKey)?.catch(() => undefined));
|
|
297
374
|
if (!existing) return;
|
|
375
|
+
existing.generation += 1;
|
|
298
376
|
sessions.delete(sessionKey);
|
|
299
|
-
await existing
|
|
377
|
+
await shutdownInvalidatedSession(existing).catch(() => undefined);
|
|
300
378
|
}
|
|
301
379
|
|
|
302
380
|
// ---------------------------------------------------------------------------
|
|
@@ -315,9 +393,10 @@ export async function disposeAllKernelSessions(): Promise<void> {
|
|
|
315
393
|
}
|
|
316
394
|
}
|
|
317
395
|
for (const [id, session] of all) {
|
|
396
|
+
session.generation += 1;
|
|
318
397
|
if (sessions.get(id) === session) sessions.delete(id);
|
|
319
398
|
}
|
|
320
|
-
const results = await Promise.allSettled(all.map(([, session]) => session
|
|
399
|
+
const results = await Promise.allSettled(all.map(([, session]) => shutdownInvalidatedSession(session)));
|
|
321
400
|
for (let i = 0; i < all.length; i += 1) {
|
|
322
401
|
const [id, session] = all[i];
|
|
323
402
|
const result = results[i];
|
|
@@ -344,9 +423,10 @@ export async function disposeKernelSessionsByOwner(ownerId: string): Promise<voi
|
|
|
344
423
|
session.ownerIds.delete(ownerId);
|
|
345
424
|
}
|
|
346
425
|
for (const session of toShutdown) {
|
|
426
|
+
session.generation += 1;
|
|
347
427
|
if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
|
|
348
428
|
}
|
|
349
|
-
const results = await Promise.allSettled(toShutdown.map(session => session
|
|
429
|
+
const results = await Promise.allSettled(toShutdown.map(session => shutdownInvalidatedSession(session)));
|
|
350
430
|
for (let i = 0; i < toShutdown.length; i += 1) {
|
|
351
431
|
const session = toShutdown[i];
|
|
352
432
|
const result = results[i];
|
|
@@ -458,31 +538,21 @@ async function executeOnSession(code: string, cwd: string, options: PythonExecut
|
|
|
458
538
|
isTimedOutCancellation(options.signal.reason, PythonExecutionCancelledError, options.signal),
|
|
459
539
|
);
|
|
460
540
|
}
|
|
461
|
-
|
|
541
|
+
const kernel = await acquireLiveSessionKernel(session, cwd, options);
|
|
542
|
+
if (sessions.get(session.sessionKey) !== session || session.kernel !== kernel) {
|
|
462
543
|
throw new PythonExecutionCancelledError(false);
|
|
463
544
|
}
|
|
464
|
-
if (!session.kernel.isAlive()) {
|
|
465
|
-
await replaceSessionKernel(session, cwd, options);
|
|
466
|
-
if (sessions.get(session.sessionKey) !== session) {
|
|
467
|
-
throw new PythonExecutionCancelledError(false);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
545
|
const runOptions = { ...options, cwd };
|
|
471
546
|
try {
|
|
472
|
-
return await executeWithKernel(
|
|
547
|
+
return await executeWithKernel(kernel, code, runOptions);
|
|
473
548
|
} catch (err) {
|
|
474
549
|
if (isCancellationError(err, PythonExecutionCancelledError) || options.signal?.aborted) throw err;
|
|
475
|
-
if (
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
// Shared kernels are keyed by cwd, so a dead kernel can be recreated in place
|
|
480
|
-
// without risking cross-directory state bleed.
|
|
481
|
-
await replaceSessionKernel(session, cwd, options);
|
|
482
|
-
if (sessions.get(session.sessionKey) !== session) {
|
|
550
|
+
if (kernel.isAlive()) throw err;
|
|
551
|
+
const retryKernel = await acquireLiveSessionKernel(session, cwd, options);
|
|
552
|
+
if (sessions.get(session.sessionKey) !== session || session.kernel !== retryKernel) {
|
|
483
553
|
throw new PythonExecutionCancelledError(false);
|
|
484
554
|
}
|
|
485
|
-
return await executeWithKernel(
|
|
555
|
+
return await executeWithKernel(retryKernel, code, runOptions);
|
|
486
556
|
}
|
|
487
557
|
}
|
|
488
558
|
|
|
@@ -10,6 +10,7 @@ import { Settings, type ShellMinimizerSettings } from "../config/settings";
|
|
|
10
10
|
import { OutputSink } from "../session/streaming-output";
|
|
11
11
|
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
|
|
12
12
|
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
|
|
13
|
+
import { loadDirenvEnv } from "./direnv";
|
|
13
14
|
import { buildNonInteractiveEnv } from "./non-interactive-env";
|
|
14
15
|
|
|
15
16
|
export interface BashExecutorOptions {
|
|
@@ -56,6 +57,78 @@ export interface BashResult {
|
|
|
56
57
|
workingDir?: string;
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
/** POSIX-safe variable name — gates which direnv unsets we inject into the
|
|
61
|
+
* command line, so a hostile `.envrc` can't smuggle shell syntax through
|
|
62
|
+
* `unset`. `.envrc` never produces non-identifier names in practice. */
|
|
63
|
+
const SAFE_ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
64
|
+
|
|
65
|
+
export interface DirenvPreflightOptions {
|
|
66
|
+
/** Caller-supplied env overlay; these values win over direnv-provided ones. */
|
|
67
|
+
callerEnv?: Record<string, string>;
|
|
68
|
+
signal?: AbortSignal;
|
|
69
|
+
/** Full direnv-load budget (`bash.direnvLoadTimeoutMs`). A positive
|
|
70
|
+
* `callerTimeoutMs` clamps the effective load below this; `0`/undefined
|
|
71
|
+
* leaves the full budget. */
|
|
72
|
+
timeoutMs?: number;
|
|
73
|
+
/** The caller's command deadline (ms). A positive value clamps the direnv
|
|
74
|
+
* load so a cold `.envrc` can't outlast a short-timeout command; `0` or
|
|
75
|
+
* undefined means "no caller clamp" — the load keeps its full `timeoutMs`
|
|
76
|
+
* budget (a disabled command deadline is NOT a 0 ms load). Centralizing the
|
|
77
|
+
* clamp here keeps every backend (executeBash, ACP terminal, PTY) on one
|
|
78
|
+
* contract instead of each re-deriving it. */
|
|
79
|
+
callerTimeoutMs?: number;
|
|
80
|
+
/** `bash.direnv` setting — `"off"` skips the load entirely. */
|
|
81
|
+
direnvSetting: "auto" | "off";
|
|
82
|
+
/** Shell wrapper prefix (profiler/strace) to place *after* the unset prefix,
|
|
83
|
+
* matching `executeBash`'s ordering. Backends that apply their own shell
|
|
84
|
+
* wrapping (ACP `wrapShellLineForClientTerminal`) omit this. */
|
|
85
|
+
commandPrefix?: string | undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Load the repo's direnv/devenv env and fold it into a `(command, env)` pair so
|
|
90
|
+
* every bash backend (one-shot `executeBash`, ACP client terminal, PTY) exposes
|
|
91
|
+
* the same devenv tools. Encapsulates: load the diff, merge `set` under the
|
|
92
|
+
* caller's overlay (caller wins), and prepend a regex-gated `unset -v` for
|
|
93
|
+
* variables the `.envrc` removes (skipping any the caller re-supplied).
|
|
94
|
+
*
|
|
95
|
+
* Returns the possibly-prefixed command plus the merged env, or the inputs
|
|
96
|
+
* unchanged (`env` = `callerEnv`) when direnv is off, absent, or has no `.envrc`.
|
|
97
|
+
* Pure transform: does NOT layer non-interactive env defaults — that stays the
|
|
98
|
+
* caller's job (so interactive PTY/ACP paths keep their own env shape).
|
|
99
|
+
*/
|
|
100
|
+
export async function applyDirenvPreflight(
|
|
101
|
+
command: string,
|
|
102
|
+
cwd: string,
|
|
103
|
+
opts: DirenvPreflightOptions,
|
|
104
|
+
): Promise<{ command: string; env: Record<string, string> | undefined }> {
|
|
105
|
+
const withPrefix = (line: string): string => (opts.commandPrefix ? `${opts.commandPrefix} ${line}` : line);
|
|
106
|
+
// A positive caller deadline clamps the direnv load below its full budget so
|
|
107
|
+
// a cold `.envrc` can't outlast a short-timeout command; `0`/undefined means
|
|
108
|
+
// "no caller clamp" (a disabled command deadline is not a 0 ms load). Every
|
|
109
|
+
// backend routes through here, so the clamp lives in one place.
|
|
110
|
+
const loadTimeoutMs =
|
|
111
|
+
opts.callerTimeoutMs !== undefined && opts.callerTimeoutMs > 0
|
|
112
|
+
? Math.min(opts.timeoutMs ?? opts.callerTimeoutMs, opts.callerTimeoutMs)
|
|
113
|
+
: opts.timeoutMs;
|
|
114
|
+
const direnvDiff =
|
|
115
|
+
opts.direnvSetting === "off" ? null : await loadDirenvEnv(cwd, { timeoutMs: loadTimeoutMs, signal: opts.signal });
|
|
116
|
+
if (!direnvDiff) {
|
|
117
|
+
return { command: withPrefix(command), env: opts.callerEnv };
|
|
118
|
+
}
|
|
119
|
+
// The caller's explicit env still wins over direnv-provided values.
|
|
120
|
+
const mergedEnv = { ...direnvDiff.set, ...opts.callerEnv };
|
|
121
|
+
// direnv can also *remove* inherited variables (a `.envrc` doing
|
|
122
|
+
// `unset AWS_PROFILE`). An env overlay can only add/override, so prepend a
|
|
123
|
+
// real `unset` for those — unless the caller re-supplied the same var
|
|
124
|
+
// explicitly, in which case the caller wins.
|
|
125
|
+
const direnvUnsets = direnvDiff.unset.filter(
|
|
126
|
+
name => !(opts.callerEnv && name in opts.callerEnv) && SAFE_ENV_NAME.test(name),
|
|
127
|
+
);
|
|
128
|
+
const unsetPrefix = direnvUnsets.length > 0 ? `unset -v ${direnvUnsets.join(" ")}; ` : "";
|
|
129
|
+
return { command: `${unsetPrefix}${withPrefix(command)}`, env: mergedEnv };
|
|
130
|
+
}
|
|
131
|
+
|
|
59
132
|
const shellSessions = new Map<string, Shell>();
|
|
60
133
|
const brokenShellSessions = new Set<string>();
|
|
61
134
|
const shellSessionQuarantines = new Map<string, Promise<unknown>>();
|
|
@@ -279,15 +352,25 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
279
352
|
const minimizer = buildMinimizerOptions(settings.getGroup("shellMinimizer"));
|
|
280
353
|
|
|
281
354
|
const commandCwd = resolveShellCwd(options?.cwd);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
|
|
355
|
+
// Fold the repo's direnv/devenv env into the command + env so devenv tools
|
|
356
|
+
// land on PATH; the caller's explicit `env` still wins. Thread the caller's
|
|
357
|
+
// signal + timeout so an aborted / short-timeout call can't hang on a cold
|
|
358
|
+
// `.envrc` load before the abort listener is installed. The helper applies
|
|
359
|
+
// the configured shell `prefix` after any `unset -v` it prepends.
|
|
360
|
+
const preflight = await applyDirenvPreflight(command, commandCwd ?? process.cwd(), {
|
|
361
|
+
callerEnv: options?.env,
|
|
362
|
+
signal: options?.signal,
|
|
363
|
+
timeoutMs: settings.get("bash.direnvLoadTimeoutMs"),
|
|
364
|
+
callerTimeoutMs: options?.timeout,
|
|
365
|
+
direnvSetting: settings.get("bash.direnv"),
|
|
366
|
+
commandPrefix: prefix,
|
|
367
|
+
});
|
|
368
|
+
const commandEnv = buildNonInteractiveEnv(preflight.env);
|
|
286
369
|
const runCdInPersistentShell = options?.useUserShell === true && !prefix && isPersistentShellCdCommand(command);
|
|
287
370
|
const finalCommand =
|
|
288
371
|
options?.useUserShell === true && !bashShell && !runCdInPersistentShell
|
|
289
|
-
? buildUserShellCommand(shell, args,
|
|
290
|
-
:
|
|
372
|
+
? buildUserShellCommand(shell, args, preflight.command)
|
|
373
|
+
: preflight.command;
|
|
291
374
|
|
|
292
375
|
// Create output sink for truncation and artifact handling
|
|
293
376
|
const sink = new OutputSink({
|
|
@@ -498,6 +581,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
498
581
|
resetSession = true;
|
|
499
582
|
throw err;
|
|
500
583
|
} finally {
|
|
584
|
+
await sink.dispose();
|
|
501
585
|
if (timeoutTimer) {
|
|
502
586
|
clearTimeout(timeoutTimer);
|
|
503
587
|
}
|