Repository navigation
reland(shell): one portable Unix startup dialect, with both revert causes fixed - #15018
Conversation
Relands #14863 (reverted by #14975) with fixes for both regressions the revert cited. 1. History GC deleted folder-workspace shell history. The live set was built from `getAllWorktreeMeta()` alone, but a folder workspace's PTY carries `folder:<id>` as its worktree id, so every live folder workspace looked orphaned. `getKnownWorktreeIdsForHistoryGc` now unions in `getFolderWorkspaces()`. Both consumers — the history-directory prune and the fish-history sweep — read that one set, so the fix covers bash, zsh and fish history alike. The directory prune had this gap since #1524; #14863 only widened its blast radius to fish files. 2. A copied Codex resume command aborted under `set -u`. Its leading clear statement has to test `$fish_pid`, and that unbound expansion takes the whole line — including the agent launch — down with it. Copied text runs in a shell Orca never spawned, so nothing can seed that variable first. The removal now rides on the agent itself as `env -u`, which needs no shell syntax and no expansion. Verified byte-identical under `set -u` in sh, bash, zsh, dash, ksh and fish. `env` cannot run the `cd` builtin, and a child `cd` would not move the agent, so the prefix is placed on the agent rather than on the whole `cd … && agent` chain. cmd and PowerShell have no nounset hazard and keep their clear ahead of the `cd`, which preserves `cd … && agent` — a failed `cd` still cannot launch the agent in the wrong directory.
📝 WalkthroughWalkthroughThe change adds worktree-scoped Bash, Zsh, and Fish history for local, WSL, relay, and SSH PTY sessions. It adds metadata validation, orphan cleanup, tombstone retry limits, and remote deletion flows. Unix startup commands now use shared portable quoting and environment removal. Zsh wrappers restore history after startup files run. Live-shell and unit tests cover history isolation, cleanup, quoting, and shell portability. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (6)
src/relay/pty-shell-overlay-wrappers.ts (1)
191-206: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winWrite the wrapper files atomically.
writeFileSynctruncates the target before it writes. A relay-spawned shell can source.zshrcorrcfileat the same moment that a second concurrent spawn rewrites it, and then reads a truncated file. Theexisting !== contentguard narrows the window to the first spawns after an upgrade, but two concurrent first spawns still race.Write to a temporary file in the same directory, then rename it over the target.
♻️ Proposed refactor
- for (const [path, content] of files) { - mkdirSync(dirname(path), { recursive: true }) - let existing: string | null = null - try { - existing = readFileSync(path, 'utf8') - } catch { - existing = null - } - // Why: relay wrapper files persist under ~/.orca-relay across app - // upgrades. Existence alone is not enough; stale wrappers would miss - // later fixes such as preserving post-.zshenv ZDOTDIR. - if (existing !== content) { - writeFileSync(path, content, 'utf8') - } - chmodSync(path, 0o644) - } + for (const [path, content] of files) { + mkdirSync(dirname(path), { recursive: true }) + let existing: string | null = null + try { + existing = readFileSync(path, 'utf8') + } catch { + existing = null + } + // Why: relay wrapper files persist under ~/.orca-relay across app + // upgrades. Existence alone is not enough; stale wrappers would miss + // later fixes such as preserving post-.zshenv ZDOTDIR. + if (existing !== content) { + // Why: a concurrent spawn must never source a truncated wrapper. + const staging = `${path}.${process.pid}.tmp` + writeFileSync(staging, content, { encoding: 'utf8', mode: 0o644 }) + renameSync(staging, path) + } + chmodSync(path, 0o644) + }Add
renameSyncto thenode:fsimport on line 1.src/main/providers/ssh-pty-provider.test.ts (1)
372-373: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for
SshPtyProvider.deleteWorktreeHistory.The provider sends
pty.deleteWorktreeHistorywith{ worktreeId }, but its test file does not cover this method. Add an assertion for this request.src/main/terminal-history-fish-session.node-pty.test.ts (2)
65-67: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePass each path segment to
path.join.Line 67 embeds a
/inside a singlepath.joinargument. Split the segments instead.♻️ Proposed change
writeFileSync( - path.join(home, 'fish/config.fish'), + path.join(home, 'fish', 'config.fish'),As per coding guidelines: "Use
path.joinor Electron/Node path utilities for file paths; never assume/or\."Source: Coding guidelines
116-128: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winKill the PTY in a
finallyblock.If any assertion between lines 116 and 123 fails,
term.kill()at line 125 never runs and the fish process survives the test. Wrap the interaction intry/finallyso the PTY is always terminated.♻️ Proposed change
- expect(await waitUntil(() => rendered.includes(PROMPT_MARK), 15_000)).toBe(true) - term.write(`${MARKER}\r`) - expect(await waitUntil(() => rendered.includes('orca-worktree-scoped-history'), 5_000)).toBe( - true - ) - // fish flushes history on exit, so the read must wait for the process to go. - term.write('exit\r') - expect(await waitUntil(() => exited, 10_000)).toBe(true) - try { - term.kill() - } catch { - // already gone - } + try { + expect(await waitUntil(() => rendered.includes(PROMPT_MARK), 15_000)).toBe(true) + term.write(`${MARKER}\r`) + expect( + await waitUntil(() => rendered.includes('orca-worktree-scoped-history'), 5_000) + ).toBe(true) + // fish flushes history on exit, so the read must wait for the process to go. + term.write('exit\r') + expect(await waitUntil(() => exited, 10_000)).toBe(true) + } finally { + try { + term.kill() + } catch { + // already gone + } + }src/main/terminal-history-tombstone-retry.test.ts (1)
126-137: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse the imported cap constant instead of the literal
64.The file already imports
MAX_PENDING_HISTORY_TREE_REMOVALS(Line 33) and uses it at Line 175. Lines 127-129 and Lines 160/165 hardcode64. If the cap value changes, these assertions fail for a reason unrelated to the behavior under test.♻️ Proposed change
await vi.advanceTimersByTimeAsync(0) - expect(removeHostTreeMock).toHaveBeenCalledTimes(64) - expect(deleteWslFishHistoryFileMock).toHaveBeenCalledTimes(64) - expect(releases).toHaveLength(64) + expect(removeHostTreeMock).toHaveBeenCalledTimes(MAX_PENDING_HISTORY_TREE_REMOVALS) + expect(deleteWslFishHistoryFileMock).toHaveBeenCalledTimes(MAX_PENDING_HISTORY_TREE_REMOVALS) + expect(releases).toHaveLength(MAX_PENDING_HISTORY_TREE_REMOVALS)Apply the same substitution at Lines 160 and 165.
src/shared/tui-agent-startup-shell.ts (1)
239-300: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCondense this comment block and anchor it to
clearEnvCommand.Two problems:
- The block is about 60 lines of prose for one function. The coding guidelines require concise, brief comments and prefer one line. Keep the decisions that are not recoverable from the code — the fish/sh split,
-gscoping to avoid universal-variable data loss,command testfor alias safety, the trailingtrue, and theset -ulimit — and move the rejected-alternative narrative (mathprobe measurements, the${fish_pid-}exploration) to the PR description or a short ADR.- As written, the block sits above
const ENV_VAR_NAME, so editors and API docs attach it to the regex instead ofclearEnvCommand. Move it below the regex, directly above the function.As per coding guidelines: "Comments must be concise, non-obvious, and brief—prefer one line; do not explain obvious behavior or walk through code."
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 1ac40ee0-8b45-4936-a969-d5e2da9a0977
📒 Files selected for processing (60)
.github/workflows/pr.ymlconfig/scripts/pr-workflow-parallelism.test.mjssrc/main/daemon/daemon-pty-adapter.tssrc/main/daemon/daemon-zsh-shell-ready-rcfile.tssrc/main/daemon/shell-ready.tssrc/main/fish-history-session.test.tssrc/main/fish-history-session.tssrc/main/ipc/pty.tssrc/main/ipc/worktrees.tssrc/main/providers/local-pty-provider.tssrc/main/providers/local-pty-shell-ready-wrapper-generation.tssrc/main/providers/pty-provider-contract.tssrc/main/providers/ssh-pty-provider.test.tssrc/main/providers/ssh-pty-provider.tssrc/main/providers/ssh-pty-session-reattach.tssrc/main/providers/ssh-pty-spawn-request.tssrc/main/remote-worktree-history-cleanup.test.tssrc/main/remote-worktree-history-cleanup.tssrc/main/runtime/orca-runtime.test.tssrc/main/runtime/orca-runtime.tssrc/main/shell-templates.tssrc/main/terminal-history-deletion.tssrc/main/terminal-history-fish-session.node-pty.test.tssrc/main/terminal-history-gc.tssrc/main/terminal-history-id.tssrc/main/terminal-history-paths.tssrc/main/terminal-history-tombstone-retry.test.tssrc/main/terminal-history.test.tssrc/main/terminal-history.tssrc/main/window/history-gc-worktree-ids.test.tssrc/main/window/history-gc-worktree-ids.tssrc/main/wsl-fish-history-cleanup.test.tssrc/main/wsl-fish-history-cleanup.tssrc/main/zsh-scoped-histfile.live-shell.test.tssrc/relay/pty-handler.tssrc/relay/pty-shell-launch.tssrc/relay/pty-shell-overlay-wrappers.tssrc/relay/terminal-history.test.tssrc/relay/terminal-history.tssrc/renderer/src/components/settings/CliSkillRuntimeSetup.tsxsrc/renderer/src/components/sidebar/folder-workspace-composer-submit.test.tssrc/renderer/src/components/terminal-pane/pty-connection-cold-restore-agent-resume.test.tssrc/renderer/src/lib/ai-vault-resume-command.test.tssrc/renderer/src/lib/ai-vault-resume-command.tssrc/renderer/src/lib/ai-vault-resume-shell.test.tssrc/renderer/src/lib/ai-vault-resume-shell.tssrc/renderer/src/lib/launch-agent-background-session.test.tssrc/renderer/src/lib/launch-agent-in-new-tab-windows-quoting.test.tssrc/renderer/src/lib/launch-work-item-direct.test.tssrc/renderer/src/lib/resume-sleeping-agent-session.test.tssrc/renderer/src/lib/tui-agent-startup.test.tssrc/shared/agent-resume-launch-command.tssrc/shared/ai-vault-resume-command.tssrc/shared/global-settings-types.tssrc/shared/hermes-startup-query.tssrc/shared/startup-shell-portability.live-shell.test.tssrc/shared/tui-agent-startup-session-options.test.tssrc/shared/tui-agent-startup-shell.test.tssrc/shared/tui-agent-startup-shell.tssrc/shared/tui-agent-startup.test.ts
Included review availability: Your plan includes up to 10 reviews per rolling hour; 5 remain after this review.
| const preferredShell = wslContext | ||
| ? 'bash' | ||
| : opts.shellOverride || env.SHELL || process.env.SHELL || '/bin/zsh' | ||
| const shellPath = resolveUnixShellPath(preferredShell) | ||
| const historyArgs = [ | ||
| env, | ||
| opts.worktreeId, | ||
| shellPath, | ||
| opts.cwd ?? resolveSafePtyDefaultCwd() | ||
| ] as const | ||
| const result = wslContext | ||
| ? injectHistoryEnv(...historyArgs, { wslDistro: wslContext.distro }) | ||
| : injectHistoryEnv(...historyArgs) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Guard resolveUnixShellPath so history isolation cannot fail the spawn.
resolveUnixShellPath throws when no candidate shell is executable. withHistoryIsolation runs inside spawn, so that throw rejects the whole spawn before doSpawn runs. History scoping is best effort everywhere else in this method (each helper degrades to shared history), and the daemon resolves its own shell anyway. Catch the failure and return the original options.
🛡️ Proposed fix
- const shellPath = resolveUnixShellPath(preferredShell)
+ let shellPath: string
+ try {
+ shellPath = resolveUnixShellPath(preferredShell)
+ } catch {
+ // History scoping is best effort; an unresolvable shell must not fail the spawn.
+ return opts
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const preferredShell = wslContext | |
| ? 'bash' | |
| : opts.shellOverride || env.SHELL || process.env.SHELL || '/bin/zsh' | |
| const shellPath = resolveUnixShellPath(preferredShell) | |
| const historyArgs = [ | |
| env, | |
| opts.worktreeId, | |
| shellPath, | |
| opts.cwd ?? resolveSafePtyDefaultCwd() | |
| ] as const | |
| const result = wslContext | |
| ? injectHistoryEnv(...historyArgs, { wslDistro: wslContext.distro }) | |
| : injectHistoryEnv(...historyArgs) | |
| const preferredShell = wslContext | |
| ? 'bash' | |
| : opts.shellOverride || env.SHELL || process.env.SHELL || '/bin/zsh' | |
| let shellPath: string | |
| try { | |
| shellPath = resolveUnixShellPath(preferredShell) | |
| } catch { | |
| // History scoping is best effort; an unresolvable shell must not fail the spawn. | |
| return opts | |
| } | |
| const historyArgs = [ | |
| env, | |
| opts.worktreeId, | |
| shellPath, | |
| opts.cwd ?? resolveSafePtyDefaultCwd() | |
| ] as const | |
| const result = wslContext | |
| ? injectHistoryEnv(...historyArgs, { wslDistro: wslContext.distro }) | |
| : injectHistoryEnv(...historyArgs) |
| await deleteRemoteWorktreeHistory( | ||
| getSshPtyProvider(remoteConnectionId), | ||
| args.worktreeId | ||
| ) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Delete remote history before push-target cleanup.
cleanupUnusedWorktreePushTargetRemoteSsh() can reject before these lines run. The normal SSH removal path then leaves scoped command history on the remote host. Move deleteRemoteWorktreeHistory() before the push-target cleanup, as done in the orphaned and manually removed paths.
| const requestedEnvShell = | ||
| process.platform !== 'win32' && typeof env?.SHELL === 'string' ? env.SHELL.trim() : '' | ||
| const shell = resolvedShellOverride || requestedEnvShell || resolveDefaultShell() |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Inspect the shell-override resolver and the default-shell fallback.
fd -t f 'pty-shell-utils.ts' src/relay --exec ast-grep outline {} --items all
rg -n -C15 'function resolvePtyShellOverride' src/relayRepository: stablyai/orca
Length of output: 3422
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- pty-shell-utils.ts ---'
cat -n src/relay/pty-shell-utils.ts | sed -n '80,115p'
printf '%s\n' '--- pty-handler.ts resolver and spawn context ---'
cat -n src/relay/pty-handler.ts | sed -n '280,315p;1485,1545p'
printf '%s\n' '--- resolver call sites and existsSync imports ---'
rg -n -C3 'resolvePtyShellOverride|existsSync|pty\.spawn' src/relay/pty-handler.ts src/relay/pty-shell-utils.tsRepository: stablyai/orca
Length of output: 12966
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- change introducing requestedEnvShell ---'
git diff --unified=12 -- src/relay/pty-handler.ts | sed -n '/requestedEnvShell/,+35p'
printf '%s\n' '--- shell selection invariant probe ---'
python3 - <<'PY'
def current_selection(resolved_override, requested_env_shell, default_shell):
return resolved_override or requested_env_shell or default_shell
def fallback_selection(resolved_override, requested_env_shell, default_shell, exists):
return resolved_override or (
requested_env_shell if requested_env_shell and exists(requested_env_shell) else ''
) or default_shell
cases = [
('', '/missing/client-shell', '/bin/bash', lambda path: False),
('', '/bin/zsh', '/bin/bash', lambda path: path == '/bin/zsh'),
]
for case in cases:
print({
'requested': case[1],
'current': current_selection(*case[:3]),
'with_existence_fallback': fallback_selection(*case),
})
PYRepository: stablyai/orca
Length of output: 439
Validate env.SHELL before spawning it.
On POSIX systems, a missing env.SHELL path bypasses resolveDefaultShell() and reaches pty.spawn, which can fail instead of using the fallback shell. Check existsSync(requestedEnvShell) before selecting it.
Do not route env.SHELL through resolvePtyShellOverride(). That resolver returns an empty string on POSIX systems and validates only Windows shell names.
| it('emits the same self-contained teardown under an sh-family login shell', () => { | ||
| // Why identical to the fish case: this text is COPIED, so it may be pasted | ||
| // into any shell — it carries its own fish/sh branch instead of guessing. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Replace the stale shell-branch comment.
The copied command uses shell-independent env -u cleanup. It does not contain a fish/sh branch. Keep this comment to one accurate line.
Proposed fix
- // Why identical to the fish case: this text is COPIED, so it may be pasted
- // into any shell — it carries its own fish/sh branch instead of guessing.
+ // Copied commands use shell-independent `env -u` cleanup.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('emits the same self-contained teardown under an sh-family login shell', () => { | |
| // Why identical to the fish case: this text is COPIED, so it may be pasted | |
| // into any shell — it carries its own fish/sh branch instead of guessing. | |
| it('emits the same self-contained teardown under an sh-family login shell', () => { | |
| // Copied commands use shell-independent `env -u` cleanup. |
Source: Coding guidelines
| * Verified against sh, bash, zsh, dash and fish 4.7.1 over regex escapes, UNC | ||
| * and drive paths, trailing/lone backslashes, mixed quotes, `$`/backtick/`$()` | ||
| * expansions, globs, braces, operators, comments, newlines, tabs and non-BMP | ||
| * unicode — see fish-startup-arg-quoting.live-fish.test.ts. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Locate live-shell/live-fish test files and any references to the cited filename.
fd -H -t f -e ts 'live-fish|live-shell' | sort
rg -n 'fish-startup-arg-quoting|startup-shell-portability' -g '!node_modules'Repository: stablyai/orca
Length of output: 255
Update the live-test reference
The referenced fish-startup-arg-quoting.live-fish.test.ts file does not exist. Use src/shared/startup-shell-portability.live-shell.test.ts instead.
Found by adversarial review of the reland. All three are the same class as the bug that caused the revert: a live set that is missing a category of real workspace, so the GC reads it as orphaned. 1. Profiles. The history root is `userData/terminal-history`, which has no profile segment, but the Store the GC consults is per-profile. So after a profile switch the live set condemned every other profile's history — and fish history, which lands in the user's own fish data dir, is shared by every profile on the machine. The live set now unions in the inactive profiles' worktrees and folder workspaces, read from their data files. A profile whose ids cannot be read reports the empty set rather than one that condemns real history. 2. No empty-set guard on the tree scan. `sweepOrphanedFishHistoryFiles` refuses an empty live set because it cannot be told apart from a store that failed to hydrate; the directory scan, which deletes more, had no such guard. A store that fell back to default state would have taken every worktree's bash and zsh history with it, across all roots including WSL. Four existing tests passed `new Set()` and relied on "empty means everything is orphaned" — exactly the behavior being removed — so they now pass a real live set. 3. Relay fish history. The relay isolates its history tree under its own root but wrote fish history into the shared fish data dir under the desktop naming, keyed by the CLIENT's worktree ids. On a machine running both Orca and a relay host, the desktop sweep deleted remote sessions' history once it went stale. Relay files are now `orca_relay_<hash>`, which the sweep's pattern deliberately does not match; the relay still deletes them by exact name when the worktree goes away.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 043e6146-b8c5-4188-a0db-25b5fccfa305
📒 Files selected for processing (9)
src/main/fish-history-session.test.tssrc/main/fish-history-session.tssrc/main/terminal-history-gc.tssrc/main/terminal-history.test.tssrc/main/window/history-gc-profile-worktree-ids.test.tssrc/main/window/history-gc-profile-worktree-ids.tssrc/main/window/history-gc-worktree-ids.test.tssrc/main/window/history-gc-worktree-ids.tssrc/relay/terminal-history.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/relay/terminal-history.ts
Included review availability: Your plan includes up to 10 reviews per rolling hour; 4 remain after this review.
| if (liveWorktreeIds.size === 0) { | ||
| console.log('[pty:history:gc] Skipped: live worktree set is empty') | ||
| return | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Drain WSL tombstones before this return.
This return occurs before listWslHistoryRoots() and its schedulePendingHistoryTreeRemovals(distroRoot) calls. An empty live set therefore retries main-root tombstones only. WSL .pending-delete entries can remain until a future GC has a nonempty live set.
Enumerate WSL roots and schedule pending removals before this guard. Keep WSL scanning and orphan deletion after the guard.
…g them Both found by adversarial review; both were unreachable from today's callers and silent if reached, which is exactly how they would survive to a caller that does reach them. - A pinned CODEX_HOME and the removal named the same variable, and `env -u` strips what the assignment just set — so the agent would have resumed against the real home and not found the session. The removal list now excludes any name the prefix pins, keeping the assignment authoritative as the old `clear…; CODEX_HOME=x agent` ordering did. Same fix in the git-bash twin. The PowerShell branch already clears before it assigns, so it was never affected. - Placement was keyed on the platform while the grammar it selects is keyed on the shell, so `platform: 'linux'` with `shell: 'powershell'` emitted POSIX `env -u` into a PowerShell line. PowerShell now routes to the PowerShell builder whatever the host, and the POSIX/cmd split below asks the shell rather than the platform.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 132011fc-48ef-4af1-b5b9-62b3b1d08e7b
📒 Files selected for processing (2)
src/shared/ai-vault-resume-command.test.tssrc/shared/ai-vault-resume-command.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/shared/ai-vault-resume-command.ts
Included review availability: Your plan includes up to 10 reviews per rolling hour; 2 remain after this review.
| it('keeps the cmd clear ahead of the cd so a failed cd cannot launch the agent', () => { | ||
| expect( | ||
| buildAiVaultResumeShellCommand({ | ||
| ...base, | ||
| cwd: 'C:\\repo', | ||
| platform: 'win32', | ||
| shell: 'cmd' | ||
| }) | ||
| ).toBe( | ||
| 'set "CODEX_HOME=" & set "ORCA_CODEX_HOME=" & cd /d "C:\\repo" && codex \'resume\' \'sid\'' | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use a cmd-valid resumeCommand fixture.
cmd.exe does not use single quotes for argument quoting. The expected command passes literal 'resume' and 'sid' arguments. This test does not validate the cmd branch with a command that can execute.
Proposed fix
buildAiVaultResumeShellCommand({
...base,
+ resumeCommand: 'codex resume "sid"',
cwd: 'C:\\repo',
platform: 'win32',
shell: 'cmd'
})
).toBe(
- 'set "CODEX_HOME=" & set "ORCA_CODEX_HOME=" & cd /d "C:\\repo" && codex \'resume\' \'sid\''
+ 'set "CODEX_HOME=" & set "ORCA_CODEX_HOME=" & cd /d "C:\\repo" && codex resume "sid"'
)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('keeps the cmd clear ahead of the cd so a failed cd cannot launch the agent', () => { | |
| expect( | |
| buildAiVaultResumeShellCommand({ | |
| ...base, | |
| cwd: 'C:\\repo', | |
| platform: 'win32', | |
| shell: 'cmd' | |
| }) | |
| ).toBe( | |
| 'set "CODEX_HOME=" & set "ORCA_CODEX_HOME=" & cd /d "C:\\repo" && codex \'resume\' \'sid\'' | |
| ) | |
| it('keeps the cmd clear ahead of the cd so a failed cd cannot launch the agent', () => { | |
| expect( | |
| buildAiVaultResumeShellCommand({ | |
| ...base, | |
| resumeCommand: 'codex resume "sid"', | |
| cwd: 'C:\\repo', | |
| platform: 'win32', | |
| shell: 'cmd' | |
| }) | |
| ).toBe( | |
| 'set "CODEX_HOME=" & set "ORCA_CODEX_HOME=" & cd /d "C:\\repo" && codex resume "sid"' | |
| ) |
…uses fixed (stablyai#15018) * reland: portable startup-shell dialect, with the two revert causes fixed Relands stablyai#14863 (reverted by stablyai#14975) with fixes for both regressions the revert cited. 1. History GC deleted folder-workspace shell history. The live set was built from `getAllWorktreeMeta()` alone, but a folder workspace's PTY carries `folder:<id>` as its worktree id, so every live folder workspace looked orphaned. `getKnownWorktreeIdsForHistoryGc` now unions in `getFolderWorkspaces()`. Both consumers — the history-directory prune and the fish-history sweep — read that one set, so the fix covers bash, zsh and fish history alike. The directory prune had this gap since stablyai#1524; stablyai#14863 only widened its blast radius to fish files. 2. A copied Codex resume command aborted under `set -u`. Its leading clear statement has to test `$fish_pid`, and that unbound expansion takes the whole line — including the agent launch — down with it. Copied text runs in a shell Orca never spawned, so nothing can seed that variable first. The removal now rides on the agent itself as `env -u`, which needs no shell syntax and no expansion. Verified byte-identical under `set -u` in sh, bash, zsh, dash, ksh and fish. `env` cannot run the `cd` builtin, and a child `cd` would not move the agent, so the prefix is placed on the agent rather than on the whole `cd … && agent` chain. cmd and PowerShell have no nounset hazard and keep their clear ahead of the `cd`, which preserves `cd … && agent` — a failed `cd` still cannot launch the agent in the wrong directory. * fix(history-gc): stop three more paths from deleting live shell history Found by adversarial review of the reland. All three are the same class as the bug that caused the revert: a live set that is missing a category of real workspace, so the GC reads it as orphaned. 1. Profiles. The history root is `userData/terminal-history`, which has no profile segment, but the Store the GC consults is per-profile. So after a profile switch the live set condemned every other profile's history — and fish history, which lands in the user's own fish data dir, is shared by every profile on the machine. The live set now unions in the inactive profiles' worktrees and folder workspaces, read from their data files. A profile whose ids cannot be read reports the empty set rather than one that condemns real history. 2. No empty-set guard on the tree scan. `sweepOrphanedFishHistoryFiles` refuses an empty live set because it cannot be told apart from a store that failed to hydrate; the directory scan, which deletes more, had no such guard. A store that fell back to default state would have taken every worktree's bash and zsh history with it, across all roots including WSL. Four existing tests passed `new Set()` and relied on "empty means everything is orphaned" — exactly the behavior being removed — so they now pass a real live set. 3. Relay fish history. The relay isolates its history tree under its own root but wrote fish history into the shared fish data dir under the desktop naming, keyed by the CLIENT's worktree ids. On a machine running both Orca and a relay host, the desktop sweep deleted remote sessions' history once it went stale. Relay files are now `orca_relay_<hash>`, which the sweep's pattern deliberately does not match; the relay still deletes them by exact name when the worktree goes away. * fix(resume): enforce the env-removal invariants instead of documenting them Both found by adversarial review; both were unreachable from today's callers and silent if reached, which is exactly how they would survive to a caller that does reach them. - A pinned CODEX_HOME and the removal named the same variable, and `env -u` strips what the assignment just set — so the agent would have resumed against the real home and not found the session. The removal list now excludes any name the prefix pins, keeping the assignment authoritative as the old `clear…; CODEX_HOME=x agent` ordering did. Same fix in the git-bash twin. The PowerShell branch already clears before it assigns, so it was never affected. - Placement was keyed on the platform while the grammar it selects is keyed on the shell, so `platform: 'linux'` with `shell: 'powershell'` emitted POSIX `env -u` into a PowerShell line. PowerShell now routes to the PowerShell builder whatever the host, and the POSIX/cmd split below asks the shell rather than the platform.
…uses fixed (stablyai#15018) * reland: portable startup-shell dialect, with the two revert causes fixed Relands stablyai#14863 (reverted by stablyai#14975) with fixes for both regressions the revert cited. 1. History GC deleted folder-workspace shell history. The live set was built from `getAllWorktreeMeta()` alone, but a folder workspace's PTY carries `folder:<id>` as its worktree id, so every live folder workspace looked orphaned. `getKnownWorktreeIdsForHistoryGc` now unions in `getFolderWorkspaces()`. Both consumers — the history-directory prune and the fish-history sweep — read that one set, so the fix covers bash, zsh and fish history alike. The directory prune had this gap since stablyai#1524; stablyai#14863 only widened its blast radius to fish files. 2. A copied Codex resume command aborted under `set -u`. Its leading clear statement has to test `$fish_pid`, and that unbound expansion takes the whole line — including the agent launch — down with it. Copied text runs in a shell Orca never spawned, so nothing can seed that variable first. The removal now rides on the agent itself as `env -u`, which needs no shell syntax and no expansion. Verified byte-identical under `set -u` in sh, bash, zsh, dash, ksh and fish. `env` cannot run the `cd` builtin, and a child `cd` would not move the agent, so the prefix is placed on the agent rather than on the whole `cd … && agent` chain. cmd and PowerShell have no nounset hazard and keep their clear ahead of the `cd`, which preserves `cd … && agent` — a failed `cd` still cannot launch the agent in the wrong directory. * fix(history-gc): stop three more paths from deleting live shell history Found by adversarial review of the reland. All three are the same class as the bug that caused the revert: a live set that is missing a category of real workspace, so the GC reads it as orphaned. 1. Profiles. The history root is `userData/terminal-history`, which has no profile segment, but the Store the GC consults is per-profile. So after a profile switch the live set condemned every other profile's history — and fish history, which lands in the user's own fish data dir, is shared by every profile on the machine. The live set now unions in the inactive profiles' worktrees and folder workspaces, read from their data files. A profile whose ids cannot be read reports the empty set rather than one that condemns real history. 2. No empty-set guard on the tree scan. `sweepOrphanedFishHistoryFiles` refuses an empty live set because it cannot be told apart from a store that failed to hydrate; the directory scan, which deletes more, had no such guard. A store that fell back to default state would have taken every worktree's bash and zsh history with it, across all roots including WSL. Four existing tests passed `new Set()` and relied on "empty means everything is orphaned" — exactly the behavior being removed — so they now pass a real live set. 3. Relay fish history. The relay isolates its history tree under its own root but wrote fish history into the shared fish data dir under the desktop naming, keyed by the CLIENT's worktree ids. On a machine running both Orca and a relay host, the desktop sweep deleted remote sessions' history once it went stale. Relay files are now `orca_relay_<hash>`, which the sweep's pattern deliberately does not match; the relay still deletes them by exact name when the worktree goes away. * fix(resume): enforce the env-removal invariants instead of documenting them Both found by adversarial review; both were unreachable from today's callers and silent if reached, which is exactly how they would survive to a caller that does reach them. - A pinned CODEX_HOME and the removal named the same variable, and `env -u` strips what the assignment just set — so the agent would have resumed against the real home and not found the session. The removal list now excludes any name the prefix pins, keeping the assignment authoritative as the old `clear…; CODEX_HOME=x agent` ordering did. Same fix in the git-bash twin. The PowerShell branch already clears before it assigns, so it was never affected. - Placement was keyed on the platform while the grammar it selects is keyed on the shell, so `platform: 'linux'` with `shell: 'powershell'` emitted POSIX `env -u` into a PowerShell line. PowerShell now routes to the PowerShell builder whatever the host, and the POSIX/cmd split below asks the shell rather than the platform.
Reland of #14863
#14863 was reverted in #14975 for two regressions. Both are fixed here, each with a test that fails without the fix. The rest of the change is unmodified.
1. History GC deleted folder-workspace shell history (data loss)
The GC's live set came from
getAllWorktreeMeta()alone. A folder workspace's PTY carriesfolder:<id>as its worktree id and lives in a separate store collection, so every live folder workspace looked orphaned and its history was pruned while the user was still writing to it.getKnownWorktreeIdsForHistoryGcnow unions ingetFolderWorkspaces()mapped throughfolderWorkspaceKey. Both consumers — the history-directory prune and the fish-history file sweep — read that one set, so bash, zsh and fish history are all covered.Worth noting: the directory prune has had this gap since #1524, so folder-workspace bash/zsh history was already being deleted on
mainindependently of #14863. #14863 widened the blast radius to fish files, which is how it got caught. This fixes both.2. A copied Codex resume command aborted under
set -uThe clear statement has to test
$fish_pid. Underset -uthat unbound expansion aborts the entire line — and because the clear came first, it took the agent launch down with it. This text is copied, so it runs in a shell Orca never spawned and cannot seed.The removal now rides on the agent itself:
env -uneeds no shell syntax and performs no expansion, so there is nothing forset -uto abort on. Verified byte-identical underset -uinsh,bash,zsh,dash,kshand fish, including a cwd containing a space.Two placement details are load-bearing:
envcannot runcd— it is a shell builtin — and a childcdwould not move the agent anyway. Prefixing the wholecd … && agentchain is broken on both counts.cd. Neither has a nounset hazard, and leading placement preservescd … && agent, so a failedcdstill cannot launch the agent in the wrong directory. Moving their clear inline would have downgraded that to unconditional&sequencing.withoutEnvCommandasserts its names are identifiers, matching the existing guard onclearEnvCommand.Also fixed here: three more live-set gaps found in review
Same class as the revert cause — a live set missing a category of real workspace.
orca_relay_<hash>, which the sweep's pattern deliberately does not match.Residual risk
env -uexecs its argument directly, so on the copy path it bypasses shell alias and function resolution. Two narrow cases behave differently than before:env: codex: No such file or directoryif there is no binary. Note the spawned (non-copied) startup path is unaffected; it removes these throughenvToDeleteat the PTY level, not through the command text.agentCmdOverridesentry (one containing&&,;, or a pipe) — the removal applies to the first segment only.Both were handled by the previous statement form, and both are accepted here deliberately: that form's
$fish_pidtest is what aborted the launch underset -u, and there is no spelling of it that detects fish without either an unbound expansion (aborts under nounset) or naming a command one of the two shell families does not have (fish prints a multi-lineUnknown command: unsetdiagnostic on every paste — verified, and2>/dev/nulldoes not suppress it).command unsetis not a workaround: zsh'scommanddoes not run the builtin, so it silently fails to unset and falls through.Original description follows.
ELI5
If you use fish, three things quietly didn't work: every workspace shared one command history, Orca's draft-prefill cleanup used
unset(which fish doesn't have), and anything Orca quoted for fish was quoted withshrules — so a prompt containing\d+or\\server\sharearrived mangled, and one ending in a backslash didn't launch at all.The fix turned out smaller than the bug list. Orca was trying to detect which shell would read a queued command line and then emit syntax for that shell. It can't detect reliably — a locally-listed workspace can execute on an SSH or WSL host — and every dialect-dependent function is somewhere to get it wrong. So instead of guessing better, this stops guessing: everything Orca emits for a Unix shell is written to be correct in
sh,bash,zsh,dash,kshand fish at the same time.That deleted more than it added.
What Changed
One Unix dialect, no shell detection
AgentStartupShellis now'posix' | 'powershell' | 'cmd'. There is deliberately no'fish'member — not because fish is unsupported, but because nothing needs to know.quoteStartupArgemits backslashes as"\\"and apostrophes as"'"between single-quoted runs. Both families read that identically. Thesh'\''idiom does not survive fish: inside'…'fish treats\\and\'as escapes, so every backslash is halved and a trailing backslash is a hard syntax error that kills the launch.clearEnvCommandemits a self-contained branch:It deliberately does not call a helper defined by Orca's shell wrappers. Orca wraps only zsh, bash and fish, so an
sh/dash/kshlogin shell launches unwrapped — and this same text is copied to the clipboard and pasted into shells Orca never spawned. In both cases a helper would becommand not found, which is precisely the failure this exists to avoid.Every piece of that line is load-bearing, and each was put there by a demonstrated failure:
A && B || C— fish'sset -ereturns non-zero on an already-unset variable, so||fell through to the sh branch and printedUnknown command: unset.true— the guard that doesn't fire leaves a non-zero status, and this is the last statement of a launch line, so the user's prompt would render it as a failure.command test, nottest— interactive shells expand aliases, andalias test=…silently skipped both branches. Only reproducible in an interactive shell, so the test for it drives-iwith the script on stdin.-g, not an unflagged erase — without it, a name that exists only as a fish universal (set -Ux CODEX_HOME …, a normal thing to have) is permanently deleted from every future session. Reachable from the clipboard command, which may run with no injected value at all. Measured: unflagged erase →*** PERMANENTLY DELETED ***; with-g→SURVIVED. A universal shadowed by an injected global is revealed again instead, which is the wanted outcome.set --erase, notset -e—$fish_pidis a heuristic. If it is ever non-empty in an sh-family shell,set -e NAMEenables errexit and replaces the positional parameters, silently changing the semantics of the rest of the session.set --eraseis the same erase in fish, but--ends option parsing in sh, so a misfire costs a usage message and nothing else. (Measured:set -e→flags=ehBc;set --erase→flags=hBc.)Names are asserted, not escaped.
clearEnvCommandrequires every name to match/^[A-Za-z_][A-Za-z0-9_]*$/and throws otherwise. The name is interpolated straight into a shell line, so a non-identifier would be both a command injection and — now that-gerases for real in fish — a way to deletePATHorHOMEout of the user's session. Both callers pass literals from a fixed table, so this cannot fire today; the guard exists so it stays that way.Known limit, documented in the code: under
set -uthe sh side aborts on the unset$fish_pidand the variable survives.${fish_pid-}would be nounset-safe but is a fish parse error, and so isset +u.A fish-builtin probe (
math 1 >/dev/null 2>&1 && … || …) would satisfy both, since it references no variable. Evaluated and rejected: it executes whatever that name resolves to on the user's PATH, twice.mathis a real binary — Wolfram Mathematica ships/usr/local/bin/math. Measured with a slow binary of that name on PATH: 10.4s to clear one variable, and with an exit-0 binary it does not clear at all (unconditional false positive). Trading a rare uncleared prefill for arbitrary program execution on the launch path is the wrong direction.One tokenizer for Unix. The input is a string typed into an Orca settings field that the shell never parses — every token is re-quoted before the line is built. Parsing it per-shell only made the same setting mean different things in different workspaces. (Windows is genuinely different:
cmd/PowerShell re-parse the built line, so that tokenizer stays.)Deleted with the guessing:
fish-command-tokenizer.ts,local-agent-startup-shell.ts,host-agent-startup-shell.ts,client-agent-startup-shell.ts,resolveLoginShellStartupDialect, and theagentEnv.SHELLprobe — which read the user's configured agent env to decide who parses the line, and would have quoted with fish rules on a zsh machine.Per-worktree zsh history was a no-op on macOS — fixes #11044
/etc/zshrcline 16 assignsHISTFILE=${ZDOTDIR:-$HOME}/.zsh_historywith no check-before-set, and it runs before every wrapper file Orca controls. So the injected value was already gone by the first prompt — and becauseZDOTDIRstill pointed at Orca's wrapper dir, history landed inside Orca's own sandbox:The intended path now rides
ORCA_HISTFILE, which/etc/zshrcdoesn't touch, and the wrapper restores it once the user's own config has loaded.The wrapper also repairs the case where Orca injects nothing — history isolation off, or any path that does not inject. There,
/etc/zshrcstill derivesHISTFILEfrom Orca's wrapperZDOTDIR, so history accumulated inside the wrapper directory and the user's real history was invisible: the plain #11044 bug with no per-worktree scoping involved. Only the exact clobbered value ($ZDOTDIR/.zsh_history) is rewritten, so aHISTFILEthe user set deliberately is never touched — verified. bash is unaffected (no system file assignsHISTFILE); verified.fish history isolation
fish ignores
HISTFILEentirely — history lives at<data dir>/<name>_history, keyed only by thefish_historyvariable, and there is no custom-directory knob. So the session name is the only lever, and the file necessarily lands in the user's fish data dir rather than Orca's tree.XDG_DATA_HOMEis deliberately not redirected: that would move every other tool's data.Deletion is a recorded directory plus a self-healing GC sweep that reclaims
orca_<hex>_historyfiles whose worktree is gone. Fixes that fell out:ftruncateSync(fd, 0), leaving a permanent 0-byte file per deleted worktree.injectWslFishHistoryEnvrecords afishSessionfor every WSL terminal, and cleanup shells out towsl.exe --exec fish— which fails outright on a distro with no fish, i.e. most of them. That rejection was chained intoremoveHostTree, so those users' bash/zsh history trees were never reclaimed at all.resolveFishHistoryDir()resolves from the Windows process environment, naming a host directory unrelated to the distro — which the host GC sweep would then scan. WSL cleanup resolves the path inside the distro instead.~/.local/shareno longer disables cleanup. The previous guard walked every path component from/and bailed on the first symlink — which rejects an ordinary dotfile-manager setup, silently leaking every file.meta.jsonholdsfishSession+fishHistoryDir. The inode-attestation file, the 16-entry retained-path ledger and its migration flag are gone; the path fields were write-only.The sweep is guarded twice against deleting live history: it refuses an empty live-worktree set (indistinguishable from a store that failed to hydrate) and skips files younger than
GC_MIN_AGE_MS, mirroring the tree GC's existing guard against the live-set snapshot race.readHistoryMetare-derives the expected session from the directory's own hash, so a tamperedmeta.jsonnaming someone else's session is ignored, and deletion refuses anything that isn't a regular file.Why
All of these are silent failures. Shared history is a daily papercut across worktrees; the
unsetone prints an error into the agent's own terminal on every draft launch and leaves the prefill variable exported, so the next agent start inherits a stale draft; and #11044 means the feature this stack is named after has been doing nothing on the primary platform.The dialect collapse is the load-bearing idea.
isPosixStartupShell('fish') === trueis exactly the shape of a predicate that looks like it branches and doesn't, and threading a new dialect through code shaped like that is how a cosmetic bug becomes corrupted prompts. Removing the dialect removes the whole class.Testing
The shells are the oracle, not hand-written expectations.
src/shared/startup-shell-portability.live-shell.test.ts— 194 assertions acrosssh,bash,zsh,dash,kshand fish 4.7.1. Each value is handed to a real shell, echoed back throughprintf, and compared byte-for-byte. 27 adversarial cases including\\server\share, trailing and lone backslashes,$/backtick/$(…), globs, braces, operators, newlines, tabs and non-BMP unicode; a full argv split on NUL; and the teardown clearing the variable with exit 0 and empty stderr whether the variable was set or already unset.src/main/zsh-scoped-histfile.live-shell.test.ts— drives a real login zsh through/etc/zshrcwith Orca's actual wrapper files, and asserts history never lands in the wrapper dir.Vacuity-checked — each revert fails in exactly the shell it should:
shquoting\\server\share, trailing\, lone\, full argv)command evalformcommandresolves external binaries onlyA && B || CA && B || Cset --erase→set -eset --erase -g→ unflaggedcommand test→testtestaliased| WSL fish-cleanup rethrow restored | the history tree is never removed |
|
ZSH_HISTFILE_RESTORE_BLOCK's un-injected branch → removed | zsh — history lands in Orca's wrapper dir ||
ZSH_HISTFILE_RESTORE_BLOCK→ no-op | 2 zsh cases, incl. "never leaves history inside Orca's own wrapper directory" |Cross-platform: the same 27-case quoting corpus plus the teardown was replayed on real Linux (Ubuntu 24.04) against
sh,bashanddash, compared base64-exact — 27/27 on each. That matters because/bin/shis dash there, unlike macOS. Windows/WSL code paths are unchanged:windows-terminal-shell.tsis byte-identical tomain, and both Windows tokenizer/quoter branches are untouched.Observed locally on macOS 15 (fish 4.7.1, zsh 5.9) — all after rebase onto current
main:npx tsc --noEmit— clean, both projectsnpx oxlint— cleannative-chat/transcript-watch, a file-watcher timing test) passes 28/28 in isolation; this diff touches zero native-chat files.check-max-lines-ratchet.mjs— OK, no new bypassescheck-reliability-gates.mjs— 85 gates passLinked Issue
Fixes #11044
Supersedes #11146 (@innocarpe), which fixes #11044 independently with the same
ORCA_HISTFILEmechanism this converged on. Two things from that PR are adopted here with credit:ORCA_HISTFILEwhen history injection is skipped. An Orca terminal opened from inside another Orca terminal inherits it, and the wrapper would then re-export the parent worktree's history path — the cross-worktree leak this feature exists to prevent. Worse on the early-return path, where it would override a caller-suppliedHISTFILE. This branch now clears it at the top ofinjectHistoryEnv, in the provider when history is off, and on the relay path; three tests pin it, vacuity-checked.[[ -n "${ORCA_HISTFILE:-}" ]]restore placement, which matches what landed here.This PR additionally repairs the case #11146 does not cover: when Orca injects nothing,
/etc/zshrcstill derivesHISTFILEfrom Orca's wrapperZDOTDIR, so history accumulated inside the wrapper directory with no scoping involved.Visual Proof
N/A— no UI surface changes. Every behavior here is shell-side and is covered by real-shell tests instead.Review
orca_<hex>_prefix, so the user's realfish_historyis never a candidate.main.meta.jsongainsfishHistoryDir; a file without it still deletes through this process's own fish dir, and the sweep catches anything both miss. The quoting output changes spelling for values containing'or\('a'\''b'→'a'"'"'b'); both are validsh, only the second is also valid fish.Adversarial review
Four adversarial reviewers were run against this branch with instructions to break it, plus my own passes over the release checklist. Ten real defects were found and fixed, eight of them introduced by this change:
__orca_clear_envhelper missing in unwrappedsh/dash/kshand in clipboard-pasted textset -eon an already-unset variable →Unknown command: unsettrueset -e NAMEsilently enabling errexit on a$fish_pidmisfireset --erasealias test=…silently skipping both branches interactivelycommand test-gConfirmed clean by review and re-verified independently: the
'unix'→'posix'WSL classifier change causes no consumer behavior change (windows-terminal-shell.tsis byte-identical tomain); Windows/WSL path quoting; distro argv with spaces and Unicode; wrapper CRLF generation; andquoteStartupArgagainst history expansion (!,^),~,$'',--, CRLF, RTL/ZWSP/combining marks, 10k-char strings, globs,$(), backticks and operators — none breakable.One more fell out of chasing the last item myself:
ai-vault-resume-command.tshad a second quoting path (quoteShellArg, used when a caller omits the shell) still on the sh'\''idiom — so the same resume command was quoted two different ways depending on the call site. Unified.Follow-up, scoped out deliberately: ten other quoting helpers across the codebase still use the sh
'\''idiom and carry the same fish hazard —pane-helpers.ts,setup-agent-sequencing.ts,setup-runner-command.ts,wsl-login-shell-command.ts,ephemeral-vm-recipe-process.ts,posix-command-path-lookup.ts,cli/shell-command-quote.ts,relay/pty-shell-overlay-wrappers.ts,relay/preflight-handler.ts,main/rate-limits/claude-pty.ts.Each has a different consumer and a different answer to "who parses this" — several are provably
sh -cand safe, others are typed into the user's shell and are not. Converting them blind, with no tests on those paths, is a larger and riskier change than this one; they are listed here so the next person has the inventory rather than having to rediscover it.Accepted, not fixed: if
fish_pidis deleted mid-session the guard misreads (one stderr line, uncleared prefill); zsh'sset --erasesilently clobbers$1on a misfire (no errexit, which is the property that matters);alias command=falsedefeats the guard, as would aliasing anything else; and a NUL byte inside a quoted argument is handled differently by each shell (zsh preserves, bash drops, fish truncates, ksh errors). Unreachable from prompts or settings in practice.Notes for reviewers
mainthis is +428 source / +136 test lines.MAX_PENDING_HISTORY_TREE_REMOVALSconcurrentwsl.execleanups on a machine with many dead WSL fish worktrees. Bounding that belongs with the tombstone scheduler that owns the admission; two attempts to bound it inside the cleanup module produced worse bugs than the fanout, so it is documented at the call site instead.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm testpassAI Disclosure
Claude Opus.
Made with Orca 🐋