#1329

Shell snapshot drops `_`-prefixed functions, so wrappers that call them fail

Opentomdaleopened 27d ago

The shell snapshot added in #1137 (v0.0.13) leaves out every function whose name starts with _. Public functions that call those helpers survive the replay, then fail with command not found (exit 127). /shell reload doesn't help because it takes a new snapshot with the same filter.

  • Regression: v0.0.12 ran the startup files for every command, so helpers were always defined. The snapshot and the filter both came in with commit a13c6e5.
  • Impact: common shell integrations break. zoxide's z() calls __zoxide_z (verified below). conda's conda() calls __conda_activate and __conda_exe (from source, not tested).

Repro

This uses a throwaway ZDOTDIR, so your own startup files aren't involved:

d=$(mktemp -d)
cat > $d/.zshrc <<'EOF'
_greet_helper() { print "hello from helper"; }
greet() { _greet_helper; }
EOF
cd $d && ZDOTDIR=$d fx ask --auto --no-save -- \
  'Run exactly this shell command once with the shell tool: whence -v greet _greet_helper; greet; echo exit=$?'

Expected: hello from helper, exit=0.

Actual output on fx 0.0.13 (macOS, zsh 5.9):

greet is a shell function from /dev/fd/0
_greet_helper not found
greet:1: command not found: _greet_helper
exit=127

The same setup with only eval "$(zoxide init zsh)" in .zshrc prints z:1: command not found: __zoxide_z.

Cause

The name filter in the capture script, shell_resolver.zig (still present in v0.0.14 and on main at 9656a2c):

case $__fx_snapshot_name in (_*|prompt_*|__fx_snapshot_*) continue;; esac   # zsh
case $__fx_snapshot_name in _*|__fx_snapshot_*) continue;; esac             # bash

This probably exists to skip completion functions. After compinit, zsh has about 1,000 of them, but nearly all are unloaded autoload stubs. On one real config:

_* functionsCount
Autoload stubs (body is builtin autoload -X…)991
Loaded definitions21

Suggested fix

Filter on whether a function is loaded, not on its name:

  • zsh: skip a function whose $functions[$name] contains builtin autoload -X, or emit it as an autoload -Uz line. Dump every other function, including _* names. On the config above, that adds about 21 functions to the snapshot.
  • bash: drop the name filter. declare -F lists only defined functions.

Why it can look intermittent

Commands run without a snapshot (full startup) when the capture fails, times out, exceeds 8 MiB, or can't be replayed. They switch to snapshot replay after a later capture succeeds. In full-startup mode the helpers exist and wrappers work. Under replay they fail. So the same wrapper can work early in a session and fail later. Two things make this harder to pin down:

  • Each fx process has its own snapshot, shared with its subagents, so two fx processes can be in different modes at once.
  • Only the top-level startup files are watched for changes. Editing a file they source doesn't trigger a new capture.

To tell which mode a command ran in: hooks such as chpwd_functions aren't replayed. A chpwd hook that writes to /dev/tty therefore prints device not configured: /dev/tty only in full-startup mode.

Workaround

Run command <name> …, or call the binary by its absolute path.

Activity

0

No activity yet.