Skip to content

fix(grok): resolve prompt_history across HOME symlink cwd - #1168

Open
TITOCHAN2023 wants to merge 4 commits into
deepcoldy:masterfrom
TITOCHAN2023:fix/grok-cwd-symlink-submit-verify
Open

fix(grok): resolve prompt_history across HOME symlink cwd#1168
TITOCHAN2023 wants to merge 4 commits into
deepcoldy:masterfrom
TITOCHAN2023:fix/grok-cwd-symlink-submit-verify

Conversation

@TITOCHAN2023

Copy link
Copy Markdown

问题

Grok 会话每条飞书消息都会误报 submit_unconfirmed,即使 Enter 已经按下、模型已在跑。

实机:HOME 是软链 /home/user -> /data00/home/user(字节类开发机常见)。

  • botmux session.workingDir / cliCwd = /home/user
  • Grok getcwd() 建桶 = ~/.grok/sessions/%2Fdata00%2Fhome%2Fuser/prompt_history.jsonl
  • resolveGrokCwdBucketDir(cliCwd) 去读 .../%2Fhome%2Fuser/prompt_history.jsonl
  • 这个路径不存在,existsSync 失败,20s 后 submit_unconfirmed

这和 #1052 不是同一事件。#1052 修的是大消息 Enter 被粘贴突发吞掉;#1052 注释里的 busy 期 dequeue 延迟是另一个已知限制。这里是 idle 短消息也每次误报,因为桶路径根本对不上。

改动

src/services/grok-paths.tsresolveGrokCwdBucketDir

  1. 先查 encodeURIComponent(cwd),再查 encodeURIComponent(realpath(cwd))
  2. hashed .cwd 标记也按物理路径比对
  3. 磁盘上还没有桶时,预测 Grok getcwd() 会创建的 encoded 物理路径

writeInput 仍走 grokPromptHistoryPath(cliCwd),不用改适配器。

影响面

  • 只动 Grok 路径解析;Codex / Claude 等不变
  • cwdrealpath(cwd) 相同时行为与现在一致
  • 已经存在的逻辑路径桶(或本地软链 workaround)仍优先命中

测试

新增 test/grok-paths.test.ts

  • HOME 软链:逻辑 cwd 能找到物理路径上的 prompt_history.jsonl
  • 尚未落盘时预测 Grok getcwd 桶
  • hashed .cwd 写物理路径时也能从逻辑 cwd 命中

本机证据:workingDir=/home/... vs /proc//cwd=/data00/home/...;修软链前逻辑 encoded 桶不存在。

Grok names session buckets from getcwd() (physical path). Botmux often holds
the logical cwd when HOME is a symlink, so encodeURIComponent(cliCwd) misses
prompt_history.jsonl and every turn reports submit_unconfirmed.
Grok names session buckets from getcwd() (physical path). Botmux often holds
the logical cwd when HOME is a symlink, so encodeURIComponent(cliCwd) misses
prompt_history.jsonl and every turn reports submit_unconfirmed.

Add unit tests for symlink cwd bucket resolution.
@TITOCHAN2023

TITOCHAN2023 commented Sep 1, 2026

Copy link
Copy Markdown
Author

PR:#1168

结论:能修本机这条假阴性,建议合。 有 1 个测试可移植性问题和 1 个双桶优先级边角,都不挡 idle 短消息这条主路径。

改动在干什么

resolveGrokCwdBucketDir 以前只 encodeURIComponent(cwd)。Grok 用 getcwd() 物理路径建桶,botmux 拿的是 HOME 软链逻辑路径,确认提交对着一个不存在的 prompt_history.jsonl,所以每条都 submit_unconfirmed

现在会依次试逻辑路径、realpath(cwd),hashed .cwd 也按物理路径比对;磁盘上还没有桶时,预测 Grok 会写的物理 encoded 路径。writeInput 不用改,它已经走 grokPromptHistoryPath(cliCwd)

主路径我觉得是对的

问题

  1. 测试:Windows 上 symlinkSync 可能直接抛(suggestion)

    • test/grok-paths.test.ts 三条都建软链,没有 skipIf / try-catch。
    • 仓库里 Grok 现有测试不用软链;若 unit 任务在 windows-latest 上跑 test/,这三条会红。
    • 建议:symlinkSync 失败就 it.skip,或 it.skipIf(process.platform === 'win32')
  2. 双桶都存在时,空的逻辑桶会挡住有数据的物理桶(suggestion)

    • 现在是「哪个 encoded 目录存在就先返回」,逻辑路径优先。
    • 本机 workaround 是逻辑路径 → 物理路径的软链,所以没问题。
    • 若有人 mkdir 出空的 %2Fhome%2F... 目录,仍会 miss。
    • 更稳:在已存在的 candidate 里优先带 prompt_history.jsonl 的那个。
  3. nit:头注释仍写 0.2.93「submit 时即写入」

没测到、但不挡合

  • 没在本 PR 里重跑 grok-transcript.test.ts 的 hashed-bucket 用例(CI 会跑)。
  • 没覆盖「逻辑桶已存在且是软链」——那是本机 workaround,与「物理桶存在、逻辑桶不存在」是两条路。
  • canonicalizeGrokCwd 导出了但测试没直接引,无害。

建议

合入前补上 Windows skip;双桶优先级可以 follow-up。本机先靠软链;上游合了再 botmux upgrade

Empty encoded logical dirs no longer shadow the physical or hashed bucket
that actually has prompt_history.jsonl. canonicalizeGrokCwd stays private,
the stale 0.2.93 "submit-time while busy" header is gone, and symlink tests
skip on Windows via a shared fixture helper.
@deepcoldy

Copy link
Copy Markdown
Owner

感谢这个修复 —— 问题定位很准,我们做了独立复现和验证,核心改动是对的,结论是建议合入。下面是一条建议修改和两条可选 nit,供参考。

以下是自动评审的初步意见,最终以维护者审阅为准


我们验证了什么

不只是读代码。本机装了真实的 grok 1.0.13,直接拿它做了端到端对照:

  1. 造软链 cwd 跑 grok -p "..." → Grok 确实按 getcwd() 建物理路径桶(%2F…%2Fphysical%2Fproj),PR 描述的前提为真
  2. 把 master 与本 PR 的解析结果分别喂给真正的判据函数 matchGrokPromptAppend
    • master → {found: false}正是报障现象本身
    • 本 PR → {found: true, cliSessionId: "01a05cfa-…"}
  3. 长 CJK 路径 + 软链下真的建出了 hashed 桶(workspace-2f155192a57e09ad),其 .cwd 内容是物理路径且无结尾换行,与 PR 的假设一致
  4. 顺带确认 grok --cwd <软链> 同样归一化成物理路径(botmux 其实从不传 --cwd,只是排除掉这条可能)

反变异 4 枪 3 红(新旧 grok 测试合跑 30 条):去掉 realpath 变体 → 红;兜底改回预测逻辑路径 → 红;去掉 prompt_history 优先 → 2 红(说明上轮提到的双桶优先级,这次的修法是有牙的)。

门禁:bun run build ✅、tsc --noEmit ✅、全量单测 20304 通过(少量失败在 master 上是超集,属本机环境问题,与本 PR 无关)。


一、建议合入前修改(1 行)

macOS 上新增的 5 条用例会红 3 条。

上一轮提到 Windows 的 symlinkSync 问题,it.skipIf(win32) 确实解决了那一条。但同一个维度上还有一个更要紧的平台:macOS 的 os.tmpdir() 自己就是软链/var/private/var)。而这组用例断言的恰恰是「软链归一化」—— 脚手架自身的临时根是软链,就成了混淆变量。

我们把 TMPDIR 指向一个软链复现,3 条失败

Expected: ".../sessions/%2Ftmp%2Fmac-sim-link%2F…%2Fdata00-home-main%2Fproj"
Received: ".../sessions/%2Ftmp%2Fmac-sim-real%2F…%2Fdata00-home-main%2Fproj"

仓库里 test/claude-code-cwd.test.ts:39-42 已经因为同样的原因做过这件事,注释写得很清楚。

建议改法:

import { mkdirSync, mkdtempSync, writeFileSync, rmSync, existsSync, symlinkSync, realpathSync } from 'node:fs';

// tmpdir() itself is a symlink on macOS (/var → /private/var); canonicalize so
// the scaffold's own root is not a confounder when asserting symlink
// normalization (see test/claude-code-cwd.test.ts).
const ROOT = realpathSync(mkdtempSync(join(tmpdir(), 'botmux-grok-paths-')));

已验证:普通环境与软链 TMPDIR 下都 5/5 绿,且上面三枪变异在打过补丁的测试上仍然全红(没有把测试打钝)。mkdtemp 顺带比 PID 命名在并行下更稳。

关于严重度,我们想说明白:CI 里两条跑单测的腿(buildbun-test都只在 ubuntu-latest,没有 macOS/Windows 单测腿 —— 所以这不会让门禁变红。它扎的是 macOS 开发者本地跑 bun run test 时看到 3 条与自己改动无关的红。真实,但不阻断,且只要 1 行。

二、nit(可选):trim() 顺带改了合法目录名的语义

canonicalizeGrokCwdcwd.trim() 之后返回的是 trim 过的字符串。POSIX 下目录名结尾允许有空格,这时 master 编出真实桶名、本 PR 编出被削掉的名字。我们真建了一个结尾带空格的目录实测:

  • master → …%201121564%20
  • 本 PR → …%201121564

建议 trim() 只作为「是否空串」的判据,realpathSync 传原始 cwd

if (!cwd.trim()) return cwd;
try { return realpathSync(cwd); } catch { return cwd; }

已验证改完与 master 行为一致,30 条全绿。极端场景,但确实是一处无意的行为回归。

三、nit(可选):解析成本随桶数线性增长

master 在编码目录存在时直接早返回;现在每次都会跑一遍 collectHashedBucketDirs(sessions 根全量 readdir + 每桶一次 existsSync)。实测:

桶数 master 本 PR
25(我们本机的实际形态) 1.6 µs 44.6 µs(~28×)
200 2.0 µs 388 µs(~190×)

这不在热路径上 —— writeInput 每次提交只解析一次,bridge tick 在未挂载时最多 1 次/秒。所以今天不构成实际问题,只是白付的、随桶数增长的成本。如果愿意收,建议加一条快路径:

// 编码目录存在且已有 prompt_history —— 绝大多数稳态。扫描不可能给出更优
// 候选,直接返回。(空目录仍然落到完整扫描,shadow 保护不受影响。)
for (const candidate of variants) {
  const dir = join(root, encodeGrokCwd(candidate));
  if (existsSync(dir) && grokBucketHasPromptHistory(dir)) return dir;
}

实测降回 3.9 µs,30 条全绿。(我们先试过更简单的 variants.length === 1 && 目录存在 → 返回,但那个版本会重新打开一个 shadow 缺口 —— 已构造用例证实,所以推荐上面这版。)

四、关于 .cwd 的 realpath 比对(仅作说明,不必改)

grokCwdMatchesMarkercanonicalizeGrokCwd(marker) === canonicalizeGrokCwd(cwd) 这一行,把它改成 return false 时测试仍然全绿。我们查清了原因:variants[] 里已经包含 realpath(cwd),而真实 Grok 的 .cwd 写的就是 getcwd() 物理路径,两者字符串相等,在到达 realpath 分支前就已命中

也就是说这一行只在 .cwd 内容本身非规范时才可达 —— 即不是当前 Grok 用 getcwd() 写的(更老/未来版本、手写 marker、其它工具)。作为纵深防御留着挺合理(错路径上多一次 realpath,很便宜),只是想说明:当前 Grok 版本下它不可达,所以别指望现有用例能覆盖它。真要补测的话,marker 必须写软链拼写才盖得到;写物理路径的用例(如现有第 3 条)永远盖不到。


再次感谢 —— 定位准确,prompt_history 优先那一版修法也确实把上轮的双桶优先级问题闭合了。建议先收下第 1 条(1 行),nit 两条随意。

本条为自动评审的初步意见,最终以维护者审阅为准。

- realpath mkdtemp so macOS /var → /private/var is not a test confounder
- trim() only tests emptiness; realpath/encode keep the original cwd
- return early when an encoded bucket already has prompt_history.jsonl
@TITOCHAN2023

Copy link
Copy Markdown
Author

感谢复现和评审。三条都收下了,在 7cd89e7

  1. 测试 ROOT 改成 realpathSync(mkdtempSync(...)),对齐 test/claude-code-cwd.test.ts,避免 macOS /var/private/var 把脚手架自己变成混淆变量。
  2. canonicalizeGrokCwd / grokCwdVariantstrim() 只判断空串;realpathSync 和 encode 走原始 cwd,结尾空格的目录名不再被削掉。加了一条预测桶用例锁住 %20
  3. encoded 目录已经有 prompt_history.jsonl 时早返回,空目录仍走完整扫描,shadow 保护不变。

.cwd 的 realpath 比对按你的说明留着当纵深防御,没改。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants