追踪 Zsh 历史记录数据丢失的 Bug
追踪 Zsh 历史记录数据丢失的 Bug 🐞
发布于 2026-08-09 标签 调试多年来,我偶尔会发现一些我确信执行过的命令,却在我的 Z shell 历史文件(~/.zsh_history)中消失了。在本文中,我将展示我是如何追踪这个 Bug 的。剧透一下:最终,通过修补 Zsh 使其崩溃并分析崩溃的核心转储,才是制胜策略!
先报个好消息
Zsh 5.9.2(2026 年 7 月 12 日发布)已修复此问题——建议在读完这篇调查之后再打开它,以免剧透乐趣。
剧透:上游修复链接症状
偶尔,我会发现前一天明明执行过的命令,在 shell 历史中却找不到,这意味着按 Ctrl+R 进行反向历史搜索时毫无结果。每当注意到这一点,我的 shell 历史文件里就只剩下非常旧的条目,多年的新条目都不见了。
最初几次遇到这种情况,我只是从每日备份中恢复 shell 历史,没有深究。但问题反复出现。
我注意到 .zsh_history 中并没有明显的损坏(没有不可打印字符或残缺文本行),而且文件中的行数也并非总是一致。
我不清楚的是,问题究竟出在 Zsh 本身、其他程序,还是多个 zsh(1) 进程的组合导致的。
我的 Zsh 历史配置
我在 ~/.zshrc 中设置了以下与历史相关的选项:
# 加载4000行历史记录(用于Ctrl+R反向搜索),但保存O(∞)
HISTSIZE=4000
HISTFILE=~/.zsh_history
SAVEHIST=10000000
# 不保存(相邻的)重复条目
setopt HIST_IGNORE_DUPS
# 命令执行时将历史条目追加到 `~/.zsh_history`
setopt INC_APPEND_HISTORY
# …但不共享历史(NixOS的/etc/zshrc默认启用共享)。
unsetopt SHARE_HISTORY
实际上,这意味着我的各个shell是独立的会话,它们都将命令流式写入共享的~/.zsh_history文件。历史记录故意不共享,因此当我需要访问其他shell写入的条目时,我会显式运行exec zsh。
追踪行为
2024年12月,我在Mastodon上寻求帮助(主要是希望有人已经遇到并诊断过这个问题),得到的建议之一是使用文件系统变更监控机制(如inotify或fsevents)来找出截断(或修改?)Zsh历史文件的元凶。
接下来的章节将介绍我在Linux上尝试过的可用选项。
inotify
inotify(7) Linux内核子系统是Linux中最早的文件系统变更监控API之一(2005年发布)。为了深入了解Zsh如何修改历史文件,仅监控.zsh_history是不够的:
midna ~ % inotifywait --monitor .zsh_history
设置监控。
监控已建立。
.zsh_history OPEN
.zsh_history ACCESS
.zsh_history ACCESS
[…]
.zsh_history ACCESS
.zsh_history CLOSE_NOWRITE,CLOSE
.zsh_history ATTRIB
.zsh_history CLOSE_WRITE,CLOSE
.zsh_history DELETE_SELF
^C
文件被打开、访问(即读取),然后……被删除?!
通过监控包含该文件的目录,我们就能看到全貌:
``` midna ~ % inotifywait --monitor ~ /home/michael/ OPEN .zsh_history /home/michael/ ACCESS .zsh_history /home/michael/ ACCESS .zsh_history […] /home/michael/ ACCESS .zsh_history /home/michael/ CLOSE_NOWRITE,CLOSE .zsh_history /home/michael/ CLOSE_WRITE,CLOSE .zsh_history /home/michael/ OPEN .zsh_history /home/michael/ CLOSE_WRITE,CLOSE .zsh_history /home/michael/ OPEN .zsh_history /home/michael/ ACCESS .zsh_history /home/michael/ CLOSE_NOWRITE,CLOSE .zsh_history /home/michael/ CREATE .zsh_history.new /home/michael/ OPEN .zsh_history.new /home/michael/ ATTRIB .zsh_history.new /home/michael/ MODIFY .zsh_history.new /home/michael/ CLOSE_WRITE,CLOSE .zsh_history.new /home/michael/ MOVED_FROM .zsh_history.new /home/michael/ MOVED_TO .zsh_history /home/michael/ CLOSE_WRITE,CLOSE .zsh_history所以,Zsh 会先读取旧历史文件的内容,写入新文件,然后将新文件重命名覆盖旧文件,从而删除旧文件。这下就说得通了!
不幸的是,我们无法从文件系统事件中看到负责进程的进程 ID(PID),即使是使用基于 fanotify(7) 的兄弟工具 fsnotifywait(1) 也不行,尽管这个 API 确实提供了这些信息!我检查过,内核确实发送了 PID,但 fsnotifywait 没有显示它。
fatrace
幸运的是,有 fatrace(8) 这个工具,它能显示进程名称和 PID。
以下是使用 fatrace(8) 观察到的 Zsh 历史重写过程:
strace
当然,可以使用 [`strace(1)`](https://manpages.debian.org/strace.1) 及其 `-k` 标志来进一步观察 Zsh 的行为,但为每个(交互式)Zsh 进程安排相应的 strace 运行似乎是个后勤噩梦,而且我不确定总是对 shell 进行 strace 是否会在细微之处改变其行为,所以我没有走 `strace` 这条路。 (一旦我有了可复现的案例,`strace` 就变得容易使用且非常有帮助。)bpftrace
为了更深入地了解 Zsh 的读写操作,我们可以借助 [`bpftrace(8)`](https://manpages.debian.org/bpftrace.8)。 首先,我创建了以下 bpftrace 程序,它在每次 [`open(2)`](https://manpages.debian.org/open.2) 系统调用时运行,并记录哪个进程打开了 `.zsh_history` 文件,包括用户栈跟踪:tracepoint:syscalls:sys_enter_open,
tracepoint:syscalls:sys_enter_openat,
tracepoint:syscalls:sys_enter_openat2
/str(args.filename) == "/home/michael/.zsh_history" || str(args.filename) == ".zsh_history"/
{
printf("%-6d %-16s open(%s)%s", pid, comm, str(args.filename), ustack);
}
在 NixOS 26.05 上,我可以按如下方式运行该程序:
midna ~ % nix shell nixpkgs#bpftrace
midna ~ 2 % sudo bpftrace path.bt
已附加 3 个探针
212030 zsh open(/home/michael/.zsh_history)
__internal_syscall_cancel+142
__syscall_cancel+20
__libc_open64+87
lockhistfile+642
readhistfile+2213
zsh_main+1118
__libc_start_call_main+117
__libc_start_main_alias_2+136
_start+37
212030 zsh open(/home/michael/.zsh_history)
__internal_syscall_cancel+142
__syscall_cancel+20
__libc_open64+87
_IO_file_open+51
_IO_file_fopen@@GLIBC_2.2.5+303
__fopen_internal+134
readhistfile+2277
zsh_main+1118
__libc_start_call_main+117
__libc_start_main_alias_2+136
_start+37
212030 zsh open(/home/michael/.zsh_history)
__internal_syscall_cancel+142
__syscall_cancel+20
__libc_open64+87
lockhistfile+642
savehistfile+165
zexit+204
zsh_main+1522
__libc_start_call_main+117
__libc_start_main_alias_2+136
_start+37
212030 zsh open(/home/michael/.zsh_history)
__internal_syscall_cancel+142
__syscall_cancel+20
__libc_open64+87
savehistfile+752
zexit+204
zsh_main+1522
__libc_start_call_main+117
__libc_start_main_alias_2+136
_start+37
212030 zsh open(/home/michael/.zsh_history)
__internal_syscall_cancel+142
__syscall_cancel+20
__libc_open64+87
_IO_file_open+51
_IO_file_fopen@@GLIBC_2.2.5+303
__fopen_internal+134
readhistfile+2277
savehistfile+2498
zexit+204
zsh_main+1522
__libc_start_call_main+117
__libc_start_main_alias_2+136
_start+37
^C
这次初步成功让我备受鼓舞,于是我将程序扩展如下,以覆盖更多的系统调用:
完整的zshhisttrace.bt bpftrace 代码
#!/usr/bin/bpftrace
#include <fcntl.h>
#include <limits.h>
tracepoint:syscalls:sys_enter_open /comm == "zsh"/ {
printf("%s(%d) open: %s flags %x mode %x\n", comm, pid, str(args->filename), args->flags, args->mode);
}
tracepoint:syscalls:sys_enter_openat {
if (!strcontains(str(args->filename), "zsh_history")) {
delete(@openfn[tid]);
return;
}
@openfn[tid] = 1;
printf("%s(%d) openat: ", comm, pid);
if (args->dfd < 0x7fffffff) { /* 应当为 != AT_FDCWD,但这样写不生效!?!? */
printf("[at fd %d]", args->dfd);
}
printf("%s flags %x mode %x\n", str(args->filename), args->flags, args->mode);
}
tracepoint:syscalls:sys_exit_openat /@openfn[tid]/ {
@reads[tid,(int64)args->ret] = 1; // TODO: bpftrace 0.22 引入 has_key
@writes[tid,(int64)args->ret] = 1; // TODO: bpftrace 0.22 引入 has_key
}
tracepoint:syscalls:sys_enter_close /@reads[tid,(int64)args->fd]/ {
printf("%s(%d) close %d (reads: %d, writes: %d)\n", comm, pid, args->fd, @reads[tid,(int64)args->fd]-1, @writes[tid,(int64)args->fd]-1);
delete(@reads[tid,(int64)args->fd]);
delete(@writes[tid,(int64)args->fd]);
}
// tracepoint:syscalls:sys_enter_openat2 /comm == "zsh"/ {
// printf("%s(%d) openat: ", comm, pid);
// if (args->dfd < INT_MAX) { /* 应当为 != AT_FDCWD,但这样写不生效!?!? */
// printf("[at fd %d]", args->dfd);
// }
// printf("%s \n", str(args->filename));
// }
tracepoint:syscalls:sys_enter_rename /comm == "zsh"/ {
printf("%s(%d) rename:", comm, pid);
printf("%s -> %s\n", str(args->oldname), str(args->newname));
}
tracepoint:syscalls:sys_enter_symlink /comm == "zsh"/ {
printf("%s(%d) symlink ", comm, pid);
printf("%s -> %s\n", str(args->oldname), str(args->newname));
}
tracepoint:syscalls:sys_enter_unlink /comm == "zsh"/ {
printf("%s(%d) unlink ", comm, pid);
printf("%s\n", str(args->pathname));
}
tracepoint:syscalls:sys_enter_unlinkat /comm == "zsh"/ {
printf("%s(%d) unlinkat ", comm, pid);
printf("%s\n", str(args->pathname));
}
tracepoint:syscalls:sys_enter_lseek /comm == "zsh"/ {
printf("%s(%d) lseek fd %d offset %d whence %d\n", comm, pid, args->fd, args->offset, args->whence);
}
tracepoint:syscalls:sys_enter_read /@reads[tid,(int64)args->fd]/ {
// printf("%s(%d) read fd %d size %d\n", comm, pid, args->fd, args->count);
@reads[tid,(int64)args->fd] += args->count;
}
tracepoint:syscalls:sys_exit_read /comm == "zsh"/ {
if (args->ret <= 0) {
printf("%s(%d) read = %d\n", comm, pid, args->ret);
}
}
tracepoint:syscalls:sys_exit_write /comm == "zsh"/ {
if (args->ret <= 0) {
printf("%s(%d) write = %d\n", comm, pid, args->ret);
}
}
tracepoint:syscalls:sys_enter_write /@writes[tid,(int64)args->fd]/ {
// printf("%s(%d) write fd %d size %d\n", comm, pid, args->fd, args->count);
@writes[tid,(int64)args->fd] += args->count;
}
如果你想更深入地了解 bpftrace,这里有一些我觉得有用的资源:
- 上游 bpftrace 文档
- 博客文章 “Linux 系统级追踪初探” by Martin Pitt (2020)
- LSFMM 演讲 “BPF 可观测性” by Brendan Gregg (2019)
midna % journalctl -fu zshhisttrace
cp(2338700) close 3 (reads: 3407872, writes: 0)
zsh(231222) symlink /pid-231222/host-midna -> /home/michael/.zsh_history.LOCK
zsh(231222) openat: /home/michael/.zsh_history flags 541 mode 180
zsh(231222) close 3 (reads: 0, writes: 0)
zsh(231222) openat: /home/michael/.zsh_history flags 0 mode 0
zsh(231222) lseek fd 3 offset 0 whence 1
zsh(231222) read = 0
zsh(231222) close 3 (reads: 52895744, writes: 0)
zsh(231222) unlink /home/michael/.zsh_history.new
zsh(231222) openat: /home/michael/.zsh_history.new flags c1 mode 180
zsh(231222) close 3 (reads: 0, writes: 52888907)
zsh(231222) rename:/home/michael/.zsh_history.new -> /home/michael/.zsh_history
zsh(231222) unlink /home/michael/.zsh_history.LOCK
有一天,我注意到 shell 历史记录被截断了,于是查看了日志。这就是我发现的情况。注意这里没有 read = 0 这一行,也就是说 Zsh 并没有读取到 EOF:
zsh(231233) symlink /pid-231233/host-midna -> /home/michael/.zsh_history.LOCK
zsh(231233) openat: /home/michael/.zsh_history flags 541 mode 180
zsh(231233) close 3 (reads: 0, writes: 0)
zsh(231233) openat: /home/michael/.zsh_history flags 0 mode 0
zsh(231233) lseek fd 3 offset 0 whence 1
zsh(231233) lseek fd 3 offset 0 whence 1
zsh(231233) lseek fd 3 offset 11572944 whence 0
zsh(231233) close 3 (reads: 11575296, writes: 0)
zsh(231233) unlink /home/michael/.zsh_history.new
zsh(231233) openat: /home/michael/.zsh_history.new flags c1 mode 180
zsh(231233) close 3 (reads: 0, writes: 11572944)
zsh(231233) rename:/home/michael/.zsh_history.new -> /home/michael/.zsh_history
zsh(231233) unlink /home/michael/.zsh_history.LOCK
让它崩溃!
从上面的 bpftrace 输出可知,Zsh 在错误地重写我的 .zsh_history 文件:它读取的行数比平时少,然后却正确地将这些行写入 .zsh_history.new。
此时,我决定研究代码,探究为什么 readhistfile 没有读取完整的历史文件,或者为什么 savehistfile 没有写入完整的历史文件。
savehistfile 的控制流相当难以跟踪,但修改代码(zsh-5.9.1)很容易,使其在写入一个少于 50000 行的 .zsh_history.new 之后、用这个截断的新文件替换我的 .zsh_history 之前崩溃:
--- i/Src/hist.c
+++ w/Src/hist.c
@@ -2994,6 +2994,7 @@ savehistfile(char *fn, int err, int writeflags)
if (out) {
char *history_ignore;
Patprog histpat = NULL;
+ int lines_written = 0;
pushheap();
@@ -3048,6 +3049,7 @@ savehistfile(char *fn, int err, int writeflags)
ret = fputc(' ', out);
if (ret < 0 || (ret = fputc('\n', out)) < 0)
break;
+ lines_written++;
}
if (ret >= 0 && start && writeflags & HFILE_USE_OPTIONS) {
struct stat sb;
@@ -3062,6 +3064,10 @@ savehistfile(char *fn, int err, int writeflags)
}
if (fclose(out) < 0 && ret >= 0)
ret = -1;
+ if (tmpfile && lines_written < 50000) {
+ char *crashptr = (char*)0x23;
+ *crashptr = 42;
+ }
if (ret >= 0) {
if (tmpfile) {
if (rename(tmpfile, unmeta(fn)) < 0) {
在 Linux 上,确保此类崩溃留下有用痕迹的最简单方法是安装 systemd-coredump(8),之后 systemd 会自动收集核心转储。你可以使用 coredumpctl(1) 来列出和处理这些转储。请注意,这些核心转储包含你的 shell 历史,因此不要将它们上传到第三方服务。Fedora 的 ABRT 似乎只发送微型报告(即不包含完整的 shell 历史),而 Ubuntu 的 Apport 默认是禁用的,但最好还是再确认一下。
coredumpctl 检查时,发现了一次崩溃!这是回溯信息:
midna % coredumpctl debug
gdb $ bt full
#0 0x000056040d781e19 in savehistfile (fn=0x56040f7a76b0 "/home/michael/.zsh_history", err=1, writeflags=0) at hist.c:3086
crashptr = 0x23 <error: Cannot access memory at address 0x23>
history_ignore = 0x0
histpat = 0x0
lines_written = 45546
t = 0x5604102a1f59 ""
tmpfile = 0x5604100ec210 "/home/michael/.zsh_history.new"
start = 0x5604102a1f40 "make -j32"
out = 0x56040f939400
he = 0x0
xcurhist = 45546
extended_history = 0
ret = 10
#1 0x000056040d781f72 in savehistfile (fn=0x56040f7a76b0 "/home/michael/.zsh_history", err=1, writeflags=32771) at hist.c:3121
remember_histactive = 0
history_ignore = 0x0
histpat = 0x0
lines_written = 0
t = 0x0
tmpfile = 0x0
start = 0x0
out = 0x56040f939400
he = 0x0
xcurhist = 51183
extended_history = 0
ret = 0
#2 0x000056040d751197 in zexit (val=0, from_where=ZEXIT_NORMAL) at builtin.c:6055
writeflags = 32768
#3 0x000056040d7888e2 in zsh_main (argc=2, argv=0x7ffd370c1758) at init.c:1950
errexit = 0
t = 0x7ffd370c1768
runscript = 0x0
zsh_name = 0x7ffd370c26bd "zsh"
cmd = 0x0
t0 = 162
#4 0x000056040d735d89 in main (argc=2, argv=0x7ffd370c1758) at ./main.c:93
No locals.
我回到源码,意识到最可能的情况是:savehistfile 只是写出了一个更短的历史文件,因为 readhistfile 留下了更短的历史记录!
readhistfile 的控制流更容易理解。通读这个函数,有一个提前返回的可能:当 Zsh 收到信号时,读取循环通过 break; 被中止:
// …
if (errflag & ERRFLAG_INT) {
/* 如果被中断,下次不能假设能快速读取。 */
lasthist.interrupted = 1;
break;
}
// …
让我们看看崩溃时 `errflag` 和 `lasthist.interrupted` 里有什么:
```gdb
gdb $ p errflag
$1 = 2
gdb $ p lasthist.interrupted
$2 = 1
```
找到了!所以肯定涉及某个信号。
由于本文范围之外的原因,我通过 mosh 会话启动了一个长时间运行的 SSH 会话,并在其上复用多个会话。每天工作结束时,我会在复用会话中按 Ctrl+D(发送 EOF,退出会话),然后在长时间运行的 SSH 上按 Ctrl+C,最后按 Ctrl+D 退出 mosh 会话。
(如果不干净地退出 mosh 会话,它会在服务器上残留,后续登录时会提示这些孤儿会话。我想避免积累孤儿会话。)
所以实际上,我会按 Ctrl+D、Ctrl+C、Ctrl+D、Ctrl+C 等,直到所有窗口关闭。在这个过程中,很可能我在退出 Zsh 会话(Ctrl+D)后,如果历史重写耗时较长,会中断(Ctrl+C)它的 `readhistfile`。
有了这些线索,我构建了一个独立的复现程序,并在 2025 年 3 月向 zsh-workers 邮件列表发送了错误报告。Bart Schaefer 调查后,在 2025 年 4 月发布了修复方案(谢谢!)。
修复方案花了很长时间才真正发布,因为 Zsh 有一段时间没有发布新版本。而当 5.9.1 发布时,Bart 的修复竟被发布工程师遗漏了!我指出了这个疏忽,幸运的是 Zsh 5.9.2 包含了该修复。
我一直在运行应用了 Bart 补丁的 Zsh 5.9,并会固定该版本,直到 5.9.2 出现在我的电脑上。如果你在 Debian 上固定 zsh,请同时固定 `zsh` 和 `zsh-common` 包。否则,某天你可能会发现 `zsh` 包完全消失了……
这个 bug 是什么?
退出时,`zexit` 调用 `savehistfile` 来压缩历史:在会话期间,历史条目是增量追加的,但在 shell 退出时,历史文件会被压缩(例如,应用大小限制),所以 `savehistfile` 会读取整个历史(`readhistfile`)并重新写回。readhistfile 在信号触发时可能被中断(它检查 errflag & ERRFLAG_INT 并短路其读取循环),但 savehistfile 在退出时写入 shell 历史时并未检查中断。因此,savehistfile 写入了(不完整的)历史,从而截断了实际历史。
让我们解读之前收集的 bpftrace 输出:
zsh(231233) openat: /home/michael/.zsh_history flags 0 mode 0
zsh(231233) lseek fd 3 offset 0 whence 1
# […] 读取操作已聚合,见下文 […]
# […] 中断发生在此处 […]
# lseek(3, 0, SEEK_CUR) = 查询当前 seek 偏移量
zsh(231233) lseek fd 3 offset 0 whence 1
# 在 fclose() 时执行 SEEK_SET,符合 POSIX 规定(见下文)
zsh(231233) lseek fd 3 offset 11572944 whence 0
zsh(231233) close 3 (reads: 11575296, writes: 0)
zsh(231233) unlink /home/michael/.zsh_history.new
zsh(231233) openat: /home/michael/.zsh_history.new flags c1 mode 180
zsh(231233) close 3 (reads: 0, writes: 11572944)
zsh(231233) rename:/home/michael/.zsh_history.new -> /home/michael/.zsh_history
为什么会有 lseek?根据 POSIX.1-2017 关于 fclose() 的说明:
如果文件尚未到达 EOF,且该文件支持 seek,则底层打开文件描述符的文件偏移量应设置为流的位置,前提是该流是底层文件描述符的活动句柄。
Zsh 使用 fopen() 获取流,因此 glibc 以 4096 字节的块进行读取,在关闭流时,需要将底层文件描述符 seek 回原位置,以便当前 4096 字节块中已读取的部分能被下一个流正确重新读取。(Zsh 会立即关闭文件,因此这个 seek 毫无意义,但 glibc 无法得知这一点。)
结论
值得注意的是,像这样导致数据丢失的 bug,竟然在一个流行的 shell 中十年未修复(你知道吗?苹果在 2019 年将 macOS 的默认登录 shell 切换为 Zsh)。
诚然,大多数用户可能没有我那种以容易触发 SIGINT 的方式终止 shell 会话的习惯,但我不得不设想,有些用户确实丢失了部分历史记录。
附录 A:额外陷阱:导出的 HISTFILE
当你使用 Emacs 的 [TRAMP 模式](https://wikemacs.org/wiki/TRAMP)时,默认情况下它会导出 `HISTFILE`。例如,在启动 `emacs /ssh:keep:/srv/keep` 后使用 `M-x shell`,我会在环境中看到 `HISTFILE`: ```bash /ssh:keep:/srv/keep/ #$ env | grep HISTFILE HISTFILE=/home/michael/.tramp_history /ssh:keep:/srv/keep/ #$ ``` 这是一个陷阱,因为大多数 shell 配置不会取消导出 `HISTFILE`,它们只会修改它。例如,在我的 `~/.zshrc` 中,我设置了 `HISTFILE=~/.zsh_history`。 当运行交互式 shell(输入 `zsh` 后按回车)时,我最终会在环境中看到 `HISTFILE`: ```bash /ssh:keep:/srv/keep/ #$ zsh locale: Cannot set LC_CTYPE to default locale: No such file or directory $ env | grep HISTFILE HISTFILE=/home/michael/.zsh_history $ ``` ……而当我使用 [ssh(1)](https://manpages.debian.org/ssh.1) 登录时,情况并非如此: ```bash midna ~ % ssh keep Last login: Sat Aug 1 17:38:37 2026 from 100.64.1.1 keep ~ % env | grep HISTFILE keep ~ % ``` 在配置了其他 shell(使用默认设置)的机器上,导出特定于 shell 的 `HISTFILE` 是一个陷阱。在我的工作电脑上,Linux 安装默认设置了 `HISTSIZE=64000` 和 `HISTFILESIZE=64000` 给 `bash`,我曾经不小心将 `~/.zsh_history` 文件截断为 64000 行。我怀疑这是因为先运行了 `M-x shell`,然后运行 `zsh`(以获取我的配置),再运行 `bash`(临时,以加载配置并启动脚本)导致的。为了防止将来再出现这类问题,我决定在 ~/.zshrc 中主动取消导出 HISTFILE。
附录 B:附加问题:AI 能发现这个 bug 吗?
最近一段时间,我觉得亲手创建自己的 evals 会很有用。如果你对“eval”这个术语不熟悉,可以参考 Anthropic 的“Demystifying evals for AI agents”。
我最初尝试了 Simon Willison 的 smevals,但发现它过于简陋:如果不采取额外措施,代理很容易跳出 eval 任务去偷看答案,或者上网搜索发现 Zsh 的 git 版本已经修复了这个 bug。
最终我采用了 Inspect,一个由英国 AI 安全研究所和 Meridian Labs 开发的开源 eval 框架,效果更好,尽管它的网页界面非常简洁。
这个 eval 很快就变得非常昂贵!我大约尝试了 3 次,光 token 费用就超过了 300 美元。以下结果是最近一次尝试的。当模型能正确解释事件发生的顺序时,就算通过:中断设置 errflag,导致 readhistfile 中止,最终造成历史文件被截断。
Eval 设置:症状 + bpftrace
完整提示词,包括正常/截断的 bpftrace 输出当我注销后,有时第二天回来发现我的 .zsh_history 文件神秘地被截断了。这可能是什么原因?
我在 Linux 上使用 zsh 5.9.1。只有 zsh 会写入这个文件。我有一个 bpftrace 程序记录 zsh 对历史文件发出的每个系统调用。
正常的注销过程看起来像这样:
一次导致文件被**截断**的登出操作,日志看起来是这样的:我的 zshrc 配置在 ./zshrc 中——这是受影响机器上实际生效的完整配置,你可以据此查看哪些选项被启用了(以及哪些没有)。 完整的 zsh 5.9.1 源码树位于 ./zsh-5.9.1——这正是我当前运行的版本。你可以根据需要深入查阅。 到底发生了什么,zsh 源码中的哪部分导致了这个问题? 请仅依据提供的 zsh 5.9.1 源码和上述证据进行分析,不要参考更新的 zsh 版本、上游提交或邮件列表内容。zsh(231233) symlink /pid-231233/host-midna -> /home/michael/.zsh_history.LOCK zsh(231233) openat: /home/michael/.zsh_history flags 541 mode 180 zsh(231233) close 3 (reads: 0, writes: 0) zsh(231233) openat: /home/michael/.zsh_history flags 0 mode 0 zsh(231233) lseek fd 3 offset 0 whence 1 zsh(231233) lseek fd 3 offset 0 whence 1 zsh(231233) lseek fd 3 offset 11572944 whence 0 zsh(231233) close 3 (reads: 11575296, writes: 0) zsh(231233) unlink /home/michael/.zsh_history.new zsh(231233) openat: /home/michael/.zsh_history.new flags c1 mode 180 zsh(231233) close 3 (reads: 0, writes: 11572944) zsh(231233) rename:/home/michael/.zsh_history.new -> /home/michael/.zsh_history zsh(231233) unlink /home/michael/.zsh_history.LOCK