@oh-my-pi/pi-coding-agent 17.3.0 → 17.3.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 +12 -0
- package/dist/{CHANGELOG-66nakf5b.md → CHANGELOG-2swgsx9f.md} +12 -0
- package/dist/cli.js +3437 -3437
- package/dist/docs-index.generated.txt +1 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/extension-flags.d.ts +3 -3
- package/dist/types/cli/flag-tables.d.ts +0 -1
- package/dist/types/cli/setup-cli.d.ts +10 -0
- package/dist/types/cli/update-cli.d.ts +13 -8
- package/dist/types/commands/completions.d.ts +3 -0
- package/dist/types/config/claude-paths.d.ts +7 -0
- package/dist/types/discovery/agents.d.ts +6 -6
- package/dist/types/discovery/helpers.d.ts +3 -4
- package/dist/types/extensibility/extensions/runner.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +4 -0
- package/dist/types/launch/broker.d.ts +5 -1
- package/dist/types/main.d.ts +1 -1
- package/dist/types/mcp/transports/stdio.d.ts +6 -3
- package/dist/types/modes/components/footer.d.ts +3 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
- package/dist/types/modes/runtime-init.d.ts +3 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/utils/git.d.ts +19 -0
- package/dist/types/utils/shell-snapshot.d.ts +4 -1
- package/package.json +13 -13
- package/src/async/job-manager.ts +33 -4
- package/src/cli/args.ts +14 -3
- package/src/cli/extension-flags.ts +6 -10
- package/src/cli/flag-tables.ts +2 -10
- package/src/cli/gc-cli.ts +13 -3
- package/src/cli/setup-cli.ts +2 -2
- package/src/cli/update-cli.ts +125 -82
- package/src/commands/completions.ts +16 -14
- package/src/config/claude-paths.ts +18 -0
- package/src/config/model-registry.ts +2 -2
- package/src/config.ts +4 -3
- package/src/discovery/agents.ts +7 -7
- package/src/discovery/claude.ts +5 -6
- package/src/discovery/helpers.ts +12 -11
- package/src/extensibility/extensions/runner.ts +5 -0
- package/src/extensibility/extensions/types.ts +5 -0
- package/src/extensibility/legacy-typebox.ts +45 -4
- package/src/launch/broker.ts +26 -4
- package/src/lsp/mux/server.ts +7 -1
- package/src/main.ts +30 -3
- package/src/mcp/transports/stdio.ts +7 -3
- package/src/modes/acp/acp-agent.ts +1 -0
- package/src/modes/components/footer.ts +17 -35
- package/src/modes/components/status-line/component.ts +14 -27
- package/src/modes/controllers/extension-ui-controller.ts +2 -2
- package/src/modes/interactive-mode.ts +16 -9
- package/src/modes/print-mode.ts +1 -0
- package/src/modes/rpc/rpc-client.ts +4 -2
- package/src/modes/rpc/rpc-input.ts +27 -0
- package/src/modes/rpc/rpc-mode.ts +11 -19
- package/src/modes/runtime-init.ts +5 -1
- package/src/modes/utils/ui-helpers.ts +74 -53
- package/src/session/agent-session.ts +1 -0
- package/src/session/claude-session-store.ts +4 -3
- package/src/task/executor.ts +6 -3
- package/src/tools/browser/launch.ts +9 -0
- package/src/utils/git.ts +27 -0
- package/src/utils/shell-snapshot.ts +5 -1
|
@@ -33,8 +33,8 @@ import {
|
|
|
33
33
|
import { SkillMessageComponent } from "../../modes/components/skill-message";
|
|
34
34
|
import { StrippedToolCallsPlaceholder } from "../../modes/components/stripped-tool-calls-placeholder";
|
|
35
35
|
import { ToolActivityContainer } from "../../modes/components/tool-activity";
|
|
36
|
-
import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
37
|
-
import { TranscriptBlock } from "../../modes/components/transcript-container";
|
|
36
|
+
import { ToolExecutionComponent, type ToolExecutionHandle } from "../../modes/components/tool-execution";
|
|
37
|
+
import { TranscriptBlock, TranscriptContainer } from "../../modes/components/transcript-container";
|
|
38
38
|
import { createUsageRowBlock } from "../../modes/components/usage-row";
|
|
39
39
|
import { UserMessageComponent } from "../../modes/components/user-message";
|
|
40
40
|
import { decodeStreamedToolArgs, streamingStringKeysForTool } from "../../modes/controllers/tool-args-reveal";
|
|
@@ -325,7 +325,7 @@ export class UiHelpers {
|
|
|
325
325
|
while (!steps.next().done) {}
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
-
/**
|
|
328
|
+
/** Build a session context in bounded chunks so terminal input runs between event-loop turns. */
|
|
329
329
|
async renderSessionContextIncrementally(
|
|
330
330
|
sessionContext: SessionContext,
|
|
331
331
|
options: RenderSessionContextOptions,
|
|
@@ -729,18 +729,23 @@ export class UiHelpers {
|
|
|
729
729
|
}
|
|
730
730
|
|
|
731
731
|
async renderInitialMessages(options: RenderInitialMessagesOptions = {}): Promise<void> {
|
|
732
|
-
//
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
732
|
+
// Build against a detached container. Incremental construction still yields
|
|
733
|
+
// to terminal input, while paints keep using the complete visible transcript
|
|
734
|
+
// until the replacement is ready to swap in.
|
|
735
|
+
const visibleChatContainer = this.ctx.chatContainer;
|
|
736
|
+
const stagedChatContainer = new TranscriptContainer();
|
|
737
|
+
stagedChatContainer.setToolActivityVisible(!this.ctx.hideToolActivity);
|
|
738
|
+
const preservedChatChildren = options.preserveExistingChat ? [...visibleChatContainer.children] : undefined;
|
|
739
|
+
const previousTranscriptMessageComponents = this.ctx.transcriptMessageComponents;
|
|
740
|
+
const previousPendingTools = this.ctx.pendingTools;
|
|
741
|
+
const previousPendingBashComponents = this.ctx.pendingBashComponents;
|
|
742
|
+
const previousPendingPythonComponents = this.ctx.pendingPythonComponents;
|
|
743
|
+
const previousLastAssistantUsage = this.ctx.lastAssistantUsage;
|
|
738
744
|
const chatWasAlreadyRendered = this.ctx.initialChatRendered;
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
}
|
|
745
|
+
|
|
746
|
+
this.ctx.chatContainer = stagedChatContainer;
|
|
747
|
+
this.ctx.transcriptMessageComponents = new WeakMap<AgentMessage, Component>();
|
|
748
|
+
this.ctx.pendingTools = new Map<string, ToolExecutionHandle>();
|
|
744
749
|
this.ctx.pendingMessagesContainer.disposeChildren();
|
|
745
750
|
this.ctx.pendingBashComponents = [];
|
|
746
751
|
this.ctx.pendingPythonComponents = [];
|
|
@@ -751,13 +756,6 @@ export class UiHelpers {
|
|
|
751
756
|
// (focus attach/unfocus while a tool executes) keep dangling toolCalls so
|
|
752
757
|
// the in-flight call re-renders as pending instead of vanishing;
|
|
753
758
|
// renderSessionContext then keeps it in `pendingTools` for live routing.
|
|
754
|
-
let terminalHistoryCleared = false;
|
|
755
|
-
const renderChunk = options.clearTerminalHistory
|
|
756
|
-
? () => {
|
|
757
|
-
this.ctx.ui.requestRender(true, { clearScrollback: !terminalHistoryCleared });
|
|
758
|
-
terminalHistoryCleared = true;
|
|
759
|
-
}
|
|
760
|
-
: undefined;
|
|
761
759
|
let context = this.ctx.viewSession.buildTranscriptSessionContext({
|
|
762
760
|
collapseCompactedHistory: settings.get("display.collapseCompacted"),
|
|
763
761
|
keepDanglingToolCalls: this.ctx.viewSession.isStreaming,
|
|
@@ -770,6 +768,7 @@ export class UiHelpers {
|
|
|
770
768
|
// every attempt.
|
|
771
769
|
populateHistory: false,
|
|
772
770
|
};
|
|
771
|
+
let committed = false;
|
|
773
772
|
this.ctx.initialChatRendered = false;
|
|
774
773
|
try {
|
|
775
774
|
while (true) {
|
|
@@ -777,8 +776,6 @@ export class UiHelpers {
|
|
|
777
776
|
// Live events mutate the same component maps; keep their replay atomic so
|
|
778
777
|
// a delta cannot land halfway through rebuilding its pending tool block.
|
|
779
778
|
this.ctx.renderSessionContext(context, renderOptions);
|
|
780
|
-
} else if (renderChunk) {
|
|
781
|
-
await this.ctx.renderSessionContextIncrementally(context, renderOptions, renderChunk);
|
|
782
779
|
} else {
|
|
783
780
|
await this.ctx.renderSessionContextIncrementally(context, renderOptions);
|
|
784
781
|
}
|
|
@@ -790,49 +787,73 @@ export class UiHelpers {
|
|
|
790
787
|
// yielded. The display callback stayed gated by initialChatRendered;
|
|
791
788
|
// discard the stale partial tree and replay the current session once
|
|
792
789
|
// more instead of letting a reentrant synchronous rebuild interleave.
|
|
793
|
-
|
|
790
|
+
stagedChatContainer.disposeChildren();
|
|
791
|
+
this.ctx.transcriptMessageComponents = new WeakMap<AgentMessage, Component>();
|
|
792
|
+
this.ctx.pendingTools.clear();
|
|
794
793
|
this.ctx.pendingBashComponents = [];
|
|
795
794
|
this.ctx.pendingPythonComponents = [];
|
|
796
|
-
terminalHistoryCleared = false;
|
|
797
795
|
context = this.ctx.viewSession.buildTranscriptSessionContext({
|
|
798
796
|
collapseCompactedHistory: settings.get("display.collapseCompacted"),
|
|
799
797
|
keepDanglingToolCalls: this.ctx.viewSession.isStreaming,
|
|
800
798
|
});
|
|
801
799
|
replayEntryCount = this.ctx.viewSession.sessionManager.getEntries().length;
|
|
802
800
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
801
|
+
|
|
802
|
+
const replayedChatChildren = [...stagedChatContainer.children];
|
|
803
|
+
stagedChatContainer.clear();
|
|
804
|
+
this.ctx.chatContainer = visibleChatContainer;
|
|
805
|
+
if (preservedChatChildren) {
|
|
806
|
+
visibleChatContainer.clear();
|
|
807
|
+
} else {
|
|
808
|
+
visibleChatContainer.disposeChildren();
|
|
811
809
|
}
|
|
812
|
-
|
|
810
|
+
for (const child of replayedChatChildren) {
|
|
811
|
+
visibleChatContainer.addChild(child);
|
|
812
|
+
}
|
|
813
|
+
if (preservedChatChildren) {
|
|
814
|
+
for (const child of preservedChatChildren) {
|
|
815
|
+
visibleChatContainer.addChild(child);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
committed = true;
|
|
813
819
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
+
if (!this.ctx.focusedAgentId) {
|
|
821
|
+
for (const message of context.messages) {
|
|
822
|
+
if (message.role !== "user" || message.synthetic) continue;
|
|
823
|
+
const text = this.getUserMessageText(message);
|
|
824
|
+
if (text) this.ctx.editor.addToHistory(text);
|
|
825
|
+
}
|
|
820
826
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
if (preservedChatChildren && preservedChatChildren.length > 0) {
|
|
830
|
-
for (const child of preservedChatChildren) {
|
|
831
|
-
this.ctx.chatContainer.addChild(child);
|
|
827
|
+
|
|
828
|
+
// Show compaction info if session was compacted.
|
|
829
|
+
const allEntries = this.ctx.viewSession.sessionManager.getEntries();
|
|
830
|
+
let compactionCount = 0;
|
|
831
|
+
for (const entry of allEntries) {
|
|
832
|
+
if (entry.type === "compaction") {
|
|
833
|
+
compactionCount++;
|
|
834
|
+
}
|
|
832
835
|
}
|
|
833
|
-
|
|
836
|
+
if (compactionCount > 0) {
|
|
837
|
+
const times = compactionCount === 1 ? "1 time" : `${compactionCount} times`;
|
|
838
|
+
this.ctx.showStatus(`Session compacted ${times}`);
|
|
839
|
+
}
|
|
840
|
+
if (options.clearTerminalHistory) {
|
|
841
|
+
this.ctx.ui.requestRender(true, { clearScrollback: true });
|
|
842
|
+
} else {
|
|
843
|
+
this.ctx.ui.requestRender();
|
|
844
|
+
}
|
|
845
|
+
} finally {
|
|
846
|
+
if (!committed) {
|
|
847
|
+
this.ctx.chatContainer = visibleChatContainer;
|
|
848
|
+
this.ctx.transcriptMessageComponents = previousTranscriptMessageComponents;
|
|
849
|
+
this.ctx.pendingTools = previousPendingTools;
|
|
850
|
+
this.ctx.pendingBashComponents = previousPendingBashComponents;
|
|
851
|
+
this.ctx.pendingPythonComponents = previousPendingPythonComponents;
|
|
852
|
+
this.ctx.lastAssistantUsage = previousLastAssistantUsage;
|
|
853
|
+
stagedChatContainer.disposeChildren();
|
|
854
|
+
}
|
|
855
|
+
this.ctx.initialChatRendered = committed ? true : chatWasAlreadyRendered;
|
|
834
856
|
}
|
|
835
|
-
this.ctx.initialChatRendered = true;
|
|
836
857
|
}
|
|
837
858
|
|
|
838
859
|
clearEditor(): void {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type * as fsTypes from "node:fs";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
|
-
import * as os from "node:os";
|
|
4
3
|
import * as path from "node:path";
|
|
5
4
|
import type {
|
|
6
5
|
AssistantMessage,
|
|
@@ -13,6 +12,7 @@ import type {
|
|
|
13
12
|
UserMessage,
|
|
14
13
|
} from "@oh-my-pi/pi-ai";
|
|
15
14
|
import { isRecord } from "@oh-my-pi/pi-utils";
|
|
15
|
+
import { resolveClaudePaths } from "../config/claude-paths";
|
|
16
16
|
import { collectForeignJsonRecords, type ForeignJsonRecord, readForeignJsonRecords } from "./foreign-session-jsonl";
|
|
17
17
|
import type { ForeignSessionInfo, ForeignSessionStore } from "./foreign-session-store";
|
|
18
18
|
import type { ModelChangeEntry, SessionMessageEntry } from "./session-entries";
|
|
@@ -99,7 +99,8 @@ async function readHistoryIndex(file: string): Promise<Map<string, ClaudeHistory
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
async function readRegisteredProjects(root: string): Promise<string[]> {
|
|
102
|
-
const
|
|
102
|
+
const { configDir, configFile } = resolveClaudePaths();
|
|
103
|
+
const config = root === configDir ? configFile : path.join(path.dirname(root), ".claude.json");
|
|
103
104
|
try {
|
|
104
105
|
const parsed: unknown = await Bun.file(config).json();
|
|
105
106
|
if (!isRecord(parsed) || !isRecord(parsed.projects)) return [];
|
|
@@ -306,7 +307,7 @@ export class ClaudeSessionStore implements ForeignSessionStore {
|
|
|
306
307
|
readonly #root: string;
|
|
307
308
|
|
|
308
309
|
/** Creates a store rooted at Claude's data directory, or at a fixture root when supplied. */
|
|
309
|
-
constructor(root: string =
|
|
310
|
+
constructor(root: string = resolveClaudePaths().configDir) {
|
|
310
311
|
this.#root = path.resolve(root);
|
|
311
312
|
}
|
|
312
313
|
|
package/src/task/executor.ts
CHANGED
|
@@ -493,6 +493,8 @@ export interface ExecutorOptions {
|
|
|
493
493
|
keepAlive?: boolean;
|
|
494
494
|
/** Internal ownership handoff for cleanup that outlives the visible Task result. */
|
|
495
495
|
onCleanupDeferred?: (completion: Promise<void>) => void;
|
|
496
|
+
/** Internal cleanup grace override for deterministic lifecycle tests. */
|
|
497
|
+
cleanupGraceMs?: number;
|
|
496
498
|
}
|
|
497
499
|
|
|
498
500
|
function parseStringifiedJson(value: unknown): unknown {
|
|
@@ -2657,6 +2659,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2657
2659
|
signal,
|
|
2658
2660
|
onProgress,
|
|
2659
2661
|
} = options;
|
|
2662
|
+
const cleanupGraceMs = options.cleanupGraceMs ?? TASK_ABORT_CLEANUP_GRACE_MS;
|
|
2660
2663
|
const startTime = Date.now();
|
|
2661
2664
|
// Set by the session's onFirstChatDispatch hook the first time the agent
|
|
2662
2665
|
// loop dispatches a chat request to the provider — the launch-complete boundary.
|
|
@@ -3328,7 +3331,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
3328
3331
|
error = err instanceof Error ? err.stack || err.message : String(err);
|
|
3329
3332
|
}
|
|
3330
3333
|
} finally {
|
|
3331
|
-
const cleanupDeadlineAt = Date.now() +
|
|
3334
|
+
const cleanupDeadlineAt = Date.now() + cleanupGraceMs;
|
|
3332
3335
|
const cleanupChangeStatus =
|
|
3333
3336
|
worktree === undefined
|
|
3334
3337
|
? "This task was not isolated, so its changes may remain in the working directory."
|
|
@@ -3339,8 +3342,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
3339
3342
|
lateCleanups.push(completion);
|
|
3340
3343
|
exitCode = 1;
|
|
3341
3344
|
aborted = true;
|
|
3342
|
-
abortReasonText = `cleanup exceeded ${
|
|
3343
|
-
error ??= `Task aborted. Cleanup did not finish within ${
|
|
3345
|
+
abortReasonText = `cleanup exceeded ${cleanupGraceMs} ms`;
|
|
3346
|
+
error ??= `Task aborted. Cleanup did not finish within ${cleanupGraceMs} ms. ${cleanupChangeStatus}`;
|
|
3344
3347
|
};
|
|
3345
3348
|
if (abortSignal.aborted) {
|
|
3346
3349
|
aborted = monitor.isAbortedRun();
|
|
@@ -204,6 +204,15 @@ function isExecutableFile(p: string): boolean {
|
|
|
204
204
|
|
|
205
205
|
async function isChromiumExecutable(p: string): Promise<boolean> {
|
|
206
206
|
if (!isExecutableFile(p)) return false;
|
|
207
|
+
// The version probe below launches the candidate. It exists to reject
|
|
208
|
+
// non-Chromium `chrome`/`chromium` wrapper scripts that appear on a Linux
|
|
209
|
+
// PATH (ecb22957, "validate Linux browser executables"). On Windows and
|
|
210
|
+
// macOS the candidates are fixed GUI application paths, not PATH wrappers,
|
|
211
|
+
// and executing them is harmful: a GUI `chrome.exe --version` does not print
|
|
212
|
+
// to a detached stdout and can hand off to the user's running instance,
|
|
213
|
+
// opening/activating a normal browser window (#8445). Confine the probe to
|
|
214
|
+
// Linux and trust the executable-file check elsewhere.
|
|
215
|
+
if (process.platform !== "linux") return true;
|
|
207
216
|
try {
|
|
208
217
|
const probeTimeoutMs = 3000;
|
|
209
218
|
const proc = Bun.spawn([p, "--version"], {
|
package/src/utils/git.ts
CHANGED
|
@@ -226,6 +226,11 @@ export const GIT_COMMAND_OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024;
|
|
|
226
226
|
* degrades instead of freezing the UI indefinitely.
|
|
227
227
|
*/
|
|
228
228
|
export const GIT_SPAWN_SYNC_TIMEOUT_MS = 5_000;
|
|
229
|
+
/**
|
|
230
|
+
* Stat-poll interval for {@link head.watch}. One `stat` per interval keeps an
|
|
231
|
+
* always-on status line cheap while surfacing a branch switch within a second.
|
|
232
|
+
*/
|
|
233
|
+
export const HEAD_WATCH_INTERVAL_MS = 1000;
|
|
229
234
|
|
|
230
235
|
const GIT_COMMAND_TIMEOUT_EXIT_CODE = 124;
|
|
231
236
|
// Exit code returned when the `git` binary cannot be launched at all (spawn
|
|
@@ -2296,6 +2301,28 @@ export const head = {
|
|
|
2296
2301
|
if (result.exitCode !== 0) return null;
|
|
2297
2302
|
return result.stdout.trim() || null;
|
|
2298
2303
|
},
|
|
2304
|
+
|
|
2305
|
+
/**
|
|
2306
|
+
* Watch the repository's HEAD for branch moves. Returns a disposer.
|
|
2307
|
+
*
|
|
2308
|
+
* Deliberately stat-polls via `fs.watchFile` instead of `fs.watch`: git
|
|
2309
|
+
* swaps HEAD with `HEAD.lock` + atomic rename, which unlinks the HEAD inode
|
|
2310
|
+
* — and Bun's inotify-backed `fs.watch` permanently stops delivering events
|
|
2311
|
+
* after observing a rename in the watched directory (oven-sh/bun#24875), so
|
|
2312
|
+
* an event watcher fires once and then freezes on Linux (issue #8412 was
|
|
2313
|
+
* the same freeze for file-inode watches on every platform). A path-based
|
|
2314
|
+
* stat poll re-resolves the path each interval and survives inode swaps
|
|
2315
|
+
* everywhere. Reftable repos keep ref state in `<gitDir>/reftable` (their
|
|
2316
|
+
* HEAD file is a static stub), so the poll targets that directory instead.
|
|
2317
|
+
*/
|
|
2318
|
+
watch(repository: GitRepository, onChange: () => void): () => void {
|
|
2319
|
+
const target = isReftableRepoSync(repository) ? path.join(repository.gitDir, "reftable") : repository.headPath;
|
|
2320
|
+
const listener = (curr: fs.Stats, prev: fs.Stats) => {
|
|
2321
|
+
if (curr.mtimeMs !== prev.mtimeMs || curr.ino !== prev.ino || curr.size !== prev.size) onChange();
|
|
2322
|
+
};
|
|
2323
|
+
fs.watchFile(target, { interval: HEAD_WATCH_INTERVAL_MS }, listener).unref();
|
|
2324
|
+
return () => fs.unwatchFile(target, listener);
|
|
2325
|
+
},
|
|
2299
2326
|
};
|
|
2300
2327
|
|
|
2301
2328
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -208,10 +208,14 @@ fi
|
|
|
208
208
|
/**
|
|
209
209
|
* Create a shell snapshot, caching the result.
|
|
210
210
|
* Returns the path to the snapshot file, or null if creation failed.
|
|
211
|
+
*
|
|
212
|
+
* `timeoutMs` is configurable so callers exercising failure handling do not
|
|
213
|
+
* have to wait out the production startup budget.
|
|
211
214
|
*/
|
|
212
215
|
export async function getOrCreateSnapshot(
|
|
213
216
|
shell: string,
|
|
214
217
|
env: Record<string, string | undefined>,
|
|
218
|
+
timeoutMs = SNAPSHOT_TIMEOUT_MS,
|
|
215
219
|
): Promise<string | null> {
|
|
216
220
|
const cacheKey = shell;
|
|
217
221
|
// Return cached snapshot if valid
|
|
@@ -284,7 +288,7 @@ export async function getOrCreateSnapshot(
|
|
|
284
288
|
stdin: "ignore",
|
|
285
289
|
stdout: "ignore",
|
|
286
290
|
stderr: "ignore",
|
|
287
|
-
timeout:
|
|
291
|
+
timeout: timeoutMs,
|
|
288
292
|
killSignal: "SIGKILL",
|
|
289
293
|
});
|
|
290
294
|
|