Repository navigation
refactor(shell): one portable Unix startup dialect instead of shell detection - #14863
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📥 CommitsReviewing files that changed from the base of the PR and between a9102d09222ab378d7fe0dc730bfd3310aed7a1d and d231126. 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan includes up to 10 reviews per rolling hour; 4 remain after this review. 📝 WalkthroughWalkthroughThe change adds worktree-scoped Bash, Zsh, and Fish history isolation across local and remote PTY sessions. It adds safe Fish history deletion, WSL cleanup, tombstone retry limits, orphan sweeping, relay persistence, and remote worktree cleanup. It also centralizes shell overlays and updates Unix quoting and environment clearing for Bash, Zsh, Fish, and other POSIX shells. Live-shell and unit tests cover history isolation, cleanup, quoting, and startup portability. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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: 10
🧹 Nitpick comments (6)
src/relay/pty-shell-overlay-wrappers.ts (1)
191-206: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winConsider degrading gracefully when wrapper writes fail.
This loop has no error handling. On a remote host with a read-only home, no free space, or restrictive permissions,
mkdirSync,writeFileSync, orchmodSyncthrows. The exception propagates out ofgetRelayShellLaunchConfigand fails the PTY spawn.The equivalent local and daemon implementations catch the error, log it, and let the shell start without the wrapper. Consider matching that behavior so a wrapper failure costs the ready marker instead of the session.
src/shared/startup-shell-portability.live-shell.test.ts (1)
1-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCondense the module header.
Keep only the non-obvious test purpose. Move regression-specific detail to the affected test cases if needed. The current header is much longer than necessary.
As per coding guidelines, “Comments must be concise, non-obvious, and brief—prefer one line.”
Source: Coding guidelines
src/main/terminal-history-tombstone-retry.test.ts (1)
127-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
MAX_PENDING_HISTORY_TREE_REMOVALSinstead of the literal64.The test already imports the constant and uses it at Line 173. Lines 127-129 and Line 158 hardcode
64. If the cap changes, these assertions fail without naming the cause.♻️ Proposed change
- 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 replacement at Line 158 and Line 163.
src/main/remote-worktree-history-cleanup.test.ts (1)
15-29: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd a case for a rejection that is not an unknown-method error.
This test pins the behavior for an old relay. It does not show whether
deleteRemoteWorktreeHistoryswallows every rejection or only the unknown-method one. A transient transport failure should be distinguishable from an unsupported relay, because the first one is retryable.Add a case that rejects with, for example,
new Error('connection reset')and assert the intended behavior.src/main/terminal-history-fish-session.node-pty.test.ts (2)
66-67: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSplit the path segments instead of embedding
/.Line 67 passes
'fish/config.fish'as one segment. Use separate segments so no separator is assumed.As per coding guidelines: "Use
path.joinor Electron/Node path utilities for file paths; never assume/or\".♻️ Proposed change
- path.join(home, 'fish/config.fish'), + path.join(home, 'fish', 'config.fish'),Source: Coding guidelines
116-128: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winKill the PTY even when an assertion fails.
If the wait at Line 116 or Line 118 returns
false, theexpectthrows and Lines 122-128 never run. The fish process then survives the test.afterEachremoves only the temp home directory.Register the terminal for teardown, or wrap the interaction in
try/finally.♻️ Proposed change
describe('fish keeps per-worktree history under the session Orca names', () => { let home: string | null = null + let live: { kill: () => void } | null = null afterEach(() => { + try { + live?.kill() + } catch { + // already gone + } + live = null if (home) {Assign
live = termright afternodePty.spawnreturns, and clear it after the expected exit.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 0b49944c-1c19-4354-ae53-aaaafa9d7e7e
📥 Commits
Reviewing files that changed from the base of the PR and between c066a55 and ccb6a5f84976d3a8294a2a93c90b96c5479afdd3.
📒 Files selected for processing (63)
.github/workflows/pr.ymlsrc/main/daemon/daemon-pty-adapter.test.tssrc/main/daemon/daemon-pty-adapter.tssrc/main/daemon/shell-ready.test.tssrc/main/daemon/shell-ready.tssrc/main/fish-history-session.test.tssrc/main/fish-history-session.tssrc/main/ipc/pty.tssrc/main/ipc/worktrees.test.tssrc/main/ipc/worktrees.tssrc/main/providers/local-pty-provider.test.tssrc/main/providers/local-pty-provider.tssrc/main/providers/local-pty-shell-ready.test.tssrc/main/providers/local-pty-shell-ready.tssrc/main/providers/pty-provider-contract.tssrc/main/providers/ssh-pty-provider.test.tssrc/main/providers/ssh-pty-provider.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/wsl-fish-history-cleanup.test.tssrc/main/wsl-fish-history-cleanup.tssrc/main/zsh-scoped-histfile.live-shell.test.tssrc/relay/pty-handler.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.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/hermes-startup-query.tssrc/shared/shell-wrapper-functions.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.tssrc/shared/types.tssrc/shared/windows-terminal-shell.ts
Included review availability: Your plan includes up to 10 reviews per rolling hour; 9 remain after this review.
| itWithZsh('leaves HISTFILE alone when Orca injected nothing', () => { | ||
| withWrapper((home, zdotdir) => { | ||
| const output = runLoginZsh(home, zdotdir, {}) | ||
|
|
||
| expect(output).toMatch(/RESULT=\S/) | ||
| expect(output).not.toContain('ORCA_HISTFILE') | ||
| }) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Limit the host-global HISTFILE assertion to macOS.
Line 82 requires a system zsh startup file to set HISTFILE. Zsh does not set this variable by default, and Linux distributions can omit or configure the global zshrc differently. This test runs on every non-Windows host, so it can fail without a wrapper defect. Gate this assertion to macOS, or create deterministic startup input.
As per coding guidelines, keep platform-dependent behavior behind runtime checks for macOS, Linux, and Windows.
Source: Coding guidelines
ccb6a5f to
c7b010e
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
src/relay/pty-shell-overlay-wrappers.ts (1)
63-76: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRestore
HISTFILEfrom the.zloginwrapper.Relay zsh starts with
-l. The login guard skips the restore at Line 57. User or system startup files can then overwriteHISTFILE, and the worktree-scoped path is not restored.Add
ZSH_HISTFILE_RESTORE_BLOCKafter the user.zloginsource and before the finalZDOTDIRrestore.Proposed fix
${getPosixOmpShellWrapper()} +${ZSH_HISTFILE_RESTORE_BLOCK} ${getZshFinalZdotdirRestoreBlock('"${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"')}
🧹 Nitpick comments (2)
src/shared/tui-agent-startup-shell.ts (1)
172-190: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShorten the new explanatory comments. Keep one concise statement for each non-obvious constraint. Move detailed edge-case inventories to tests or documentation.
src/shared/tui-agent-startup-shell.ts#L172-L190: reduce the quoting rationale to one brief constraint statement.src/shared/tui-agent-startup-shell.ts#L3-L9: shorten the startup-dialect explanation.src/shared/tui-agent-startup-shell.ts#L155-L159: shorten the tokenizer rationale.src/shared/tui-agent-startup-shell.ts#L239-L265: shorten the environment-cleanup rationale.src/relay/pty-shell-overlay-wrappers.ts#L12-L14: shorten the module-extraction comment.src/shared/startup-shell-portability.live-shell.test.ts#L1-L15: shorten the test-suite introduction.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
src/main/fish-history-session.ts (1)
5-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShorten the implementation comments.
Keep the behavioral contract and safety invariant. Remove the detailed implementation narrative. Use brief comments near the affected code.
As per coding guidelines, comments must be concise, non-obvious, and brief—prefer one line.
Also applies to: 30-32, 40-41, 54-56, 68-85, 121-121
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 20073452-76f3-4037-ae06-efc8f0255035
📒 Files selected for processing (57)
.github/workflows/pr.ymlsrc/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/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
🚧 Files skipped from review as they are similar to previous changes (39)
- src/renderer/src/lib/tui-agent-startup.test.ts
- src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts
- src/renderer/src/lib/launch-agent-in-new-tab-windows-quoting.test.ts
- src/main/remote-worktree-history-cleanup.ts
- src/renderer/src/lib/launch-work-item-direct.test.ts
- src/main/providers/ssh-pty-spawn-request.ts
- src/renderer/src/lib/ai-vault-resume-command.test.ts
- src/shared/agent-resume-launch-command.ts
- src/main/runtime/orca-runtime.test.ts
- src/renderer/src/lib/resume-sleeping-agent-session.test.ts
- src/main/ipc/pty.ts
- src/shared/tui-agent-startup.test.ts
- src/shared/tui-agent-startup-shell.test.ts
- src/main/providers/pty-provider-contract.ts
- src/shared/ai-vault-resume-command.ts
- src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx
- src/relay/terminal-history.test.ts
- src/main/remote-worktree-history-cleanup.test.ts
- .github/workflows/pr.yml
- src/renderer/src/lib/launch-agent-background-session.test.ts
- src/main/ipc/worktrees.ts
- src/main/terminal-history-gc.ts
- src/main/terminal-history-tombstone-retry.test.ts
- src/shared/tui-agent-startup-session-options.test.ts
- src/main/terminal-history-id.ts
- src/main/terminal-history-paths.ts
- src/main/runtime/orca-runtime.ts
- src/main/wsl-fish-history-cleanup.test.ts
- src/renderer/src/lib/ai-vault-resume-shell.ts
- src/shared/hermes-startup-query.ts
- src/main/terminal-history-fish-session.node-pty.test.ts
- src/renderer/src/lib/ai-vault-resume-shell.test.ts
- src/relay/terminal-history.ts
- src/main/daemon/daemon-pty-adapter.ts
- src/main/terminal-history.test.ts
- src/main/terminal-history.ts
- src/main/providers/local-pty-provider.ts
- src/relay/pty-handler.ts
- src/main/providers/ssh-pty-provider.test.ts
Included review availability: Your plan includes up to 10 reviews per rolling hour; 7 remain after this review.
| export function clearEnvCommand(name: string | readonly string[], shell: AgentStartupShell): string { | ||
| const names = typeof name === 'string' ? [name] : [...name] | ||
| if (shell === 'powershell') { | ||
| return `Remove-Item Env:${name} -ErrorAction SilentlyContinue` | ||
| return names | ||
| .map((each) => `Remove-Item Env:${each} -ErrorAction SilentlyContinue`) | ||
| .join('; ') | ||
| } | ||
| if (shell === 'cmd') { | ||
| return `set "${name}="` | ||
| } | ||
| // Why: fish has no `unset`; the sh spelling errors out and silently leaves | ||
| // the variable exported (e.g. an account-routed CODEX_HOME survives). | ||
| if (shell === 'fish') { | ||
| return `set -e ${name}` | ||
| return names.map((each) => `set "${each}="`).join(' & ') | ||
| } | ||
| return `unset ${name}` | ||
| const joined = names.join(' ') | ||
| return `test -n "$fish_pid" && set -e ${joined}; test -z "$fish_pid" && unset ${joined}; true` |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
node <<'NODE'
const { spawnSync } = require('node:child_process')
const command =
'test -n "$fish_pid" && set -e ORCA_PI_PREFILL; ' +
'test -z "$fish_pid" && unset ORCA_PI_PREFILL; true'
for (const shell of ['sh', 'bash', 'zsh', 'dash', 'ksh']) {
const env = { ...process.env }
delete env.fish_pid
const result = spawnSync(shell, ['-u', '-c', command], { encoding: 'utf8', env })
if (result.error?.code === 'ENOENT') continue
console.log(`${shell}: status=${result.status}`)
process.stdout.write(result.stderr || '')
}
NODERepository: stablyai/orca
Length of output: 263
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- source symbols and call sites ---'
rg -n -C 4 'clearEnvCommand|fish_pid|startup-shell-portability' src/shared/tui-agent-startup-shell.ts src/shared/startup-shell-portability.live-shell.test.ts src/shared 2>/dev/null | head -240
printf '%s\n' '--- focused source ---'
sed -n '235,290p' src/shared/tui-agent-startup-shell.ts
printf '%s\n' '--- focused portability test ---'
sed -n '1,260p' src/shared/startup-shell-portability.live-shell.test.ts
printf '%s\n' '--- shell availability and behavior ---'
node <<'NODE'
const { spawnSync } = require('node:child_process')
const command =
'test -n "$fish_pid" && set -e ORCA_PI_PREFILL; ' +
'test -z "$fish_pid" && unset ORCA_PI_PREFILL; true'
for (const shell of ['sh', 'bash', 'zsh', 'dash', 'ksh', 'fish']) {
const result = spawnSync(shell, ['-u', '-c', command], {
encoding: 'utf8',
env: { ...process.env, ORCA_PI_PREFILL: 'present' },
})
if (result.error?.code === 'ENOENT') {
console.log(`${shell}: unavailable`)
continue
}
console.log(`${shell}: status=${result.status}, stdout=${JSON.stringify(result.stdout)}, stderr=${JSON.stringify(result.stderr)}`)
}
NODERepository: stablyai/orca
Length of output: 27959
Avoid expanding fish_pid under nounset.
When user startup enables set -u, the POSIX branch expands unset fish_pid before test runs. sh and bash abort the command, so CODEX_HOME and ORCA_CODEX_HOME can remain set.
Use a branch detector that does not expand an undefined POSIX variable. Add nounset coverage to src/shared/startup-shell-portability.live-shell.test.ts.
c7b010e to
d546b26
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/shared/startup-shell-portability.live-shell.test.ts (2)
46-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse the Node path utility.
split('/')assumes a Unix separator. Usepath.basename()and rename the parameter to avoid shadowing the imported module.Proposed fix
-function basename(path: string): string { - return path.split('/').pop() ?? path +function basename(filePath: string): string { + return path.basename(filePath) }As per coding guidelines, “Use
path.joinor Electron/Node path utilities for file paths; never assume/or\.”Source: Coding guidelines
1-15: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueShorten the file header comment.
Keep a one-line suite purpose. Move detailed regression rationale next to the related test when it is necessary.
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: 5b7b6171-48e3-486b-891c-e1289140ac11
📥 Commits
Reviewing files that changed from the base of the PR and between c7b010e and d546b26353c62e86d46afb4a8c64f919511342fc.
📒 Files selected for processing (11)
src/main/terminal-history-deletion.tssrc/main/terminal-history-tombstone-retry.test.tssrc/main/terminal-history.tssrc/renderer/src/lib/ai-vault-resume-command.test.tssrc/renderer/src/lib/ai-vault-resume-shell.test.tssrc/renderer/src/lib/launch-work-item-direct.test.tssrc/renderer/src/lib/tui-agent-startup.test.tssrc/shared/startup-shell-portability.live-shell.test.tssrc/shared/tui-agent-startup-shell.test.tssrc/shared/tui-agent-startup-shell.tssrc/shared/tui-agent-startup.test.ts
🚧 Files skipped from review as they are similar to previous changes (9)
- src/renderer/src/lib/tui-agent-startup.test.ts
- src/renderer/src/lib/launch-work-item-direct.test.ts
- src/shared/tui-agent-startup.test.ts
- src/renderer/src/lib/ai-vault-resume-command.test.ts
- src/renderer/src/lib/ai-vault-resume-shell.test.ts
- src/main/terminal-history-deletion.ts
- src/shared/tui-agent-startup-shell.ts
- src/shared/tui-agent-startup-shell.test.ts
- src/main/terminal-history.ts
Included review availability: Your plan includes up to 10 reviews per rolling hour; 7 remain after this review.
d546b26 to
a9102d0
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/shared/startup-shell-portability.live-shell.test.ts (2)
46-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse the Node path utility.
split('/')assumes a POSIX separator. Rename the parameter and usepath.basename(filePath).Proposed fix
-function basename(path: string): string { - return path.split('/').pop() ?? path +function basename(filePath: string): string { + return path.basename(filePath) }As per coding guidelines, use Node path utilities for file paths and do not assume
/or\.Source: Coding guidelines
1-15: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReduce the long test commentary.
Keep the required invariant and remove implementation narration. The test names and assertions already describe most of this behavior.
As per coding guidelines, comments must be concise, non-obvious, and brief.
Also applies to: 197-211
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e82db3f5-41a5-4ab1-ae87-26688ce86e15
📥 Commits
Reviewing files that changed from the base of the PR and between d546b26353c62e86d46afb4a8c64f919511342fc and a9102d09222ab378d7fe0dc730bfd3310aed7a1d.
📒 Files selected for processing (2)
src/shared/startup-shell-portability.live-shell.test.tssrc/shared/tui-agent-startup-shell.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/shared/tui-agent-startup-shell.ts
Included review availability: Your plan includes up to 10 reviews per rolling hour; 6 remain after this review.
e7e1da4 to
b00b0d8
Compare
Review feedback addressedRebased onto current Fixed now:
Already fixed in earlier commits (these were reviewed against older revisions): the fish-sweep young-directory guard ( Local: 53,834 passing, both typechecks, lint, max-lines ratchet, 85 reliability gates. One unrelated flake ( |
…etection Orca had to guess which shell would parse a queued command line, then emit syntax for it. Guessing is unreliable for a remote or WSL host, and every dialect-dependent function is a place to get it wrong. Replace the guess. Everything emitted for a Unix shell is now built to be correct in sh, bash, zsh, dash, ksh and fish alike, so no detection is needed: - quoteStartupArg emits backslashes as "\\" and apostrophes as "'" between single-quoted runs. Both families read that identically, unlike the sh '\'' idiom, which fish silently halves and which makes a trailing backslash a hard syntax error. - clearEnvCommand emits a self-contained fish/sh 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`/`ksh` login shell launches unwrapped — and the same text is copied to the clipboard and pasted into shells Orca never spawned. In both, a helper would be `command not found`, which is the exact failure this exists to avoid. Two guarded statements rather than `A && B || C`, because fish's `set -e` returns non-zero for an already-unset variable and would fall through to the sh branch; a trailing `true` pins the status, since this is the last statement of a launch line and the prompt renders it. - One tokenizer for Unix. The input is a settings string the shell never parses, so parsing it per-shell only made the same setting mean different things in different workspaces. AgentStartupShell loses its 'fish' and 'unix' members, and the three login-shell resolvers, the fish tokenizer and the agentEnv.SHELL probe go with them. Per-worktree shell history now actually works: - zsh on macOS was a no-op. /etc/zshrc assigns HISTFILE unconditionally before any wrapper Orca controls, so the injected value was already gone — and with ZDOTDIR still pointing at Orca's wrapper dir, history landed inside it. The intended path rides ORCA_HISTFILE and is restored after user config. Fixes #11044. - fish keeps history in its own data dir keyed by session name, since it ignores HISTFILE and has no custom-directory knob. Files are deleted rather than truncated, a symlinked ~/.local/share no longer disables cleanup, and a GC sweep reclaims orphans whose meta.json is gone. The sweep 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 guard against the live-set snapshot race. Verified against real shells rather than asserted as strings: startup-shell-portability.live-shell.test.ts runs 194 assertions across sh/bash/zsh/dash/ksh/fish, and zsh-scoped-histfile.live-shell.test.ts drives a real login zsh through /etc/zshrc. Both are vacuity-checked. The same quoting corpus was replayed byte-exact on Linux, where /bin/sh is dash.
b00b0d8 to
3c702e3
Compare
|
@nwparker we reverted #14863 in #14975 because it introduced a launch-blocking regression: folder-workspace Fish history can be deleted for a still-valid folder workspace after Orca restarts, and generated Unix cleanup/resume commands can abort in shells with Please reland with a fix. Linear: https://linear.app/stably/issue/STA-4566/reland-14863-portable-unix-startup-dialect-after-revert-14975 |
…etection (#14863) Orca had to guess which shell would parse a queued command line, then emit syntax for it. Guessing is unreliable for a remote or WSL host, and every dialect-dependent function is a place to get it wrong. Replace the guess. Everything emitted for a Unix shell is now built to be correct in sh, bash, zsh, dash, ksh and fish alike, so no detection is needed: - quoteStartupArg emits backslashes as "\\" and apostrophes as "'" between single-quoted runs. Both families read that identically, unlike the sh '\'' idiom, which fish silently halves and which makes a trailing backslash a hard syntax error. - clearEnvCommand emits a self-contained fish/sh 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`/`ksh` login shell launches unwrapped — and the same text is copied to the clipboard and pasted into shells Orca never spawned. In both, a helper would be `command not found`, which is the exact failure this exists to avoid. Two guarded statements rather than `A && B || C`, because fish's `set -e` returns non-zero for an already-unset variable and would fall through to the sh branch; a trailing `true` pins the status, since this is the last statement of a launch line and the prompt renders it. - One tokenizer for Unix. The input is a settings string the shell never parses, so parsing it per-shell only made the same setting mean different things in different workspaces. AgentStartupShell loses its 'fish' and 'unix' members, and the three login-shell resolvers, the fish tokenizer and the agentEnv.SHELL probe go with them. Per-worktree shell history now actually works: - zsh on macOS was a no-op. /etc/zshrc assigns HISTFILE unconditionally before any wrapper Orca controls, so the injected value was already gone — and with ZDOTDIR still pointing at Orca's wrapper dir, history landed inside it. The intended path rides ORCA_HISTFILE and is restored after user config. Fixes #11044. - fish keeps history in its own data dir keyed by session name, since it ignores HISTFILE and has no custom-directory knob. Files are deleted rather than truncated, a symlinked ~/.local/share no longer disables cleanup, and a GC sweep reclaims orphans whose meta.json is gone. The sweep 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 guard against the live-set snapshot race. Verified against real shells rather than asserted as strings: startup-shell-portability.live-shell.test.ts runs 194 assertions across sh/bash/zsh/dash/ksh/fish, and zsh-scoped-histfile.live-shell.test.ts drives a real login zsh through /etc/zshrc. Both are vacuity-checked. The same quoting corpus was replayed byte-exact on Linux, where /bin/sh is dash.
…uses fixed (#15018) * reland: portable startup-shell dialect, with the two revert causes fixed 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. * 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.
…etection (stablyai#14863) Orca had to guess which shell would parse a queued command line, then emit syntax for it. Guessing is unreliable for a remote or WSL host, and every dialect-dependent function is a place to get it wrong. Replace the guess. Everything emitted for a Unix shell is now built to be correct in sh, bash, zsh, dash, ksh and fish alike, so no detection is needed: - quoteStartupArg emits backslashes as "\\" and apostrophes as "'" between single-quoted runs. Both families read that identically, unlike the sh '\'' idiom, which fish silently halves and which makes a trailing backslash a hard syntax error. - clearEnvCommand emits a self-contained fish/sh 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`/`ksh` login shell launches unwrapped — and the same text is copied to the clipboard and pasted into shells Orca never spawned. In both, a helper would be `command not found`, which is the exact failure this exists to avoid. Two guarded statements rather than `A && B || C`, because fish's `set -e` returns non-zero for an already-unset variable and would fall through to the sh branch; a trailing `true` pins the status, since this is the last statement of a launch line and the prompt renders it. - One tokenizer for Unix. The input is a settings string the shell never parses, so parsing it per-shell only made the same setting mean different things in different workspaces. AgentStartupShell loses its 'fish' and 'unix' members, and the three login-shell resolvers, the fish tokenizer and the agentEnv.SHELL probe go with them. Per-worktree shell history now actually works: - zsh on macOS was a no-op. /etc/zshrc assigns HISTFILE unconditionally before any wrapper Orca controls, so the injected value was already gone — and with ZDOTDIR still pointing at Orca's wrapper dir, history landed inside it. The intended path rides ORCA_HISTFILE and is restored after user config. Fixes stablyai#11044. - fish keeps history in its own data dir keyed by session name, since it ignores HISTFILE and has no custom-directory knob. Files are deleted rather than truncated, a symlinked ~/.local/share no longer disables cleanup, and a GC sweep reclaims orphans whose meta.json is gone. The sweep 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 guard against the live-set snapshot race. Verified against real shells rather than asserted as strings: startup-shell-portability.live-shell.test.ts runs 194 assertions across sh/bash/zsh/dash/ksh/fish, and zsh-scoped-histfile.live-shell.test.ts drives a real login zsh through /etc/zshrc. Both are vacuity-checked. The same quoting corpus was replayed byte-exact on Linux, where /bin/sh is dash.
…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.
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 🐋