浮之静

Codex 解决了一个抽象 BUG

今天用 Codex 解决了一个十分抽象的 bug,很有意思,就想记录下来。

Image

背景

我正在开发一个名为 Noi 的 AI 浏览器,它基于 Electron 构建。在使用 Electron 时,如果要在本地存储配置信息,一般会使用 electron-store[1] 来做(支持原子写),或通过 node.js fs 模块自己读写文件。

在 Noi 中需大量高频读写本地文件,所以我基于 fs 模块实现了一个类似于 electron-store 的工具函数 NoiStore。自己实现的原因是因为 electron-store:

  • 不支持缓存
  • 不推荐大数据量读写
  • 不支持根数组,必须为对象格式
  • 以及各种自定义...
📌 原子写

原子写(atomic write)就是让一次写入“看起来像一瞬完成”:要么整个新内容落盘、对外可见;要么完全不改动,绝不会出现“写了一半”的撕裂文件。这对配置、索引、账本等关键文件很重要——程序崩溃、断电或多进程并发时,原子写能把风险收敛成两种安全状态:用户要么读到旧版本,要么读到新版本,绝读不到半成品,从而避免数据损坏与复杂的恢复流程。

常见做法(单分区同一文件系统)是“三步走”:

  1. 把完整新内容写到临时文件;
  2. fsync 临时文件(确保数据真正到磁盘,而非仅在内存缓存);
  3. 用同目录的 rename 把临时文件原子替换为目标文件,并再 fsync 目录(确保元数据持久化)。因为同目录下的 rename 在主流系统上是原子的,所以任何用户要么看到旧文件,要么立刻看到新文件,不会读到中间态。数据库会用更高级的变体(如 WAL/日志)实现一系列操作的原子性与可恢复性。

注意:原子写解决的是“写入一致性”,不是“并发协调”。它不等于锁:若多方同时写,还需要版本号/锁/单写入者等策略来避免“最后写入者获胜”的逻辑冲突。原子写的代价是额外 I/O(临时文件 + fsync),但换来的是崩溃一致性与更简单的错误处理。

Bug 描述

在 Noi 的底部有一个工具栏(Toolbar),它支持显示/隐藏。正常表现如下:

Image

Bug 出现后 UI 在状态切换后错位,表现如下:

Image

Bug 修复中

因最近两周新增+修改的代码有点多,导致问题排查难度上升。说白了,就是自己有点懒,想着用 codex 修复一下。因为我明确知道最近修改的代码并未涉及到 Noi View 计算(主进程)以及 UI(渲染进程使用 React),所以初步可以排除布局核心计算和 React 相关代码。状态变更需要通过 NoiStore 来存储,而最近两周更新最多的也是它。所以初步就将问题锁定在 NoiStore 上了。

操作状态会通过 NoiStore 更新到本地,进而影响 UI 状态变化,中间也会涉及 IPC 通信,很多交互穿插起来,让我一时间不知道咋描述(属于嘴跟不上脑子了,知道问题,但不知该如何表述)。于是为了图省事,我直接这样询问 codex(vscode 插件),大意是:

  • 两周前 NoiStore.ts 是正常的,最近某次更新导致一些配置读取错误,请查找潜在修改问题,进行修复。可参考 NoiStore.old.ts(其实 codex 可以读取 git log 追踪代码变更,为了省事,我直接在项目中放了一个两周前的代码副本)对 NoiStore.ts 进行修复,并实现之后的优化(这句话是为了保证它在修复问题的同时,不影响之后的更新,但事实证明我想多了,这个问题有点复杂)。
  • 可能是读取到了缓存,导致数据不是最新(根据实际测试,感觉是状态不同步导致的)?我也不太确定。你对比两份代码,产生了哪些变更?它们是否会产生副作用?
Image
Image

结果也可想而知,因为我以上两个描述比较抽象宽泛,codex 只给了这类回复,并未得出有效结论:

  • 我已经把新版和两周前的 NoiStore 实现对比完了,不过现在的症状还抓不牢——需要知道“读取错误”的具体表现才能对症下药。能再补充一下吗?
    • 思考:出现抓不牢,具体表现等描述,意思是缺少关键上下文(问题过于宽泛,让 ai 都茫然了)。
  • 缓存刷新逻辑本身与旧版一致...
    • 思考:与第一次差不多的结论,还是因为上下文不足。

其实我挺喜欢看 thinking 的,比如下面这张截图,它调用了 git 命令、以及通过 shell 在本地写入测试数据验证函数功能等。

Image
📌 git 重要性

在 ai coding 中,版本管理尤为重要。它不但是你代码的最后防线(ai 搞乱你的代码可以回滚),还可以让 ai 追踪代码的变更,去解决一些特殊问题。codex 在多次编辑代码时,一般会将之前的代码进行暂存(git stash)。

既然上下文不足,那就继续补充呗。于是我进行了第三次提问:

Image

这次的 thinking 就靠谱了很多,我摘取了一些关键截图。在看 thinking 时,当它分析到跨进程状态同步读取比较棘手时,我就感觉这次解决 bug 有戏。

Image
Image
Image
Image

如果以上截图没看明白,我简单解释一下,codex 通过代码检索分析以及 shell 授权写入测试用例准确定位到了问题:

  • UI 错位根因:store 实例缓存 + 异步磁盘刷新延迟;NoiWindowStyle 获取配置链路与布局 class/padding 条件需复核。
  • 克隆策略风险:cloneShallow 改动可能导致嵌套对象替换/保留异常,影响 watchers 与更新触发。
  • 复现实验:脚本比对“缓存读 vs 磁盘新读”,定位陈旧值来源与同步时序问题。
  • 跨进程同步:无立即写盘/无 watcher 时渲染进程短暂读旧值;可禁用配置写入去抖或改为传参绕开竞争。
  • 立即一致性:配置类 store 将 writeDebounceMs=0;可新增 flushOnWrite 选项细粒度控制。
  • 读取陈旧值原因:stat 节流 + watcher 刷新机制耦合;调低 statThrottleMs、使用同步写或强制 flush。
  • 配置一致性治理:应用中配置项多由 config store 下发,旧 store 类型文件是否统一更新可权衡;用 ensureConfStore 跑测以防回归。

补充截图:shell 授权执行测试用例(系统权限,需用户手动确认)。

Image

最终在修改 17 行代码后,bug 被修复。

Image

Bug 复盘

截止到 bug 修复,我都还没反应过来是修改了哪些代码才导致此 bug,于是就让 codex 进行复盘。

Image

一语惊醒梦中人,当看到 @electron/remote 时,我恍然大悟。这部分代码正是后来新加的,为了兼容在渲染进程中使用 NoiStore。但它在渲染进程调用时会初始化一个新的 NoiStore 实例(和主进程不同步)。比较巧的是之前为了做缓存性能优化,引入了 writeDebounceMs(默认 32ms)延迟写入,几个因素共同作用,导致状态更新不即时。

结语

之所以想记录这次 bug,是因为自己的前两次提问都很抽象,几乎没有给出有效上下文,但 codex 经过自己的一系列测试分析,仅用三次对话就解决了让我一脸茫然的 bug(NoiStore 的核心代码都是 ai 实现的,我也没有逐行 review)。这可能也是一次小小的警告,完全放手交给 ai,自己也不去理解 ai 实现的代码,出了问题,大概率是一脸懵逼...

codex 分析解决问题能力确实 👍,但 AI 想要高效解决问题,还是需要人的编程经验去辅助(如果我第三次没有缩减范围提问,codex 可能还需要花些功夫才能定位问题)!有点相互成就的意思,你强,ai 则强!

References

[1]

electron-store:https://github.com/sindresorhus/electron-store