现象:dsh 的邮件通道全断。老会话读不出来 ⇒ 桥报 SessionQueryError ⇒ 按"不在磁盘"
处理 ⇒ 再 create 撞 `already exists`。修好读路径之后又立刻暴露下一层
`message "undefined" is already pending`。
根因一(历史数据,dsh 侧):v0 会话的 `agent/inbox/spliced.inserted[]` 缺 `id`/`role`,
v0→v1 迁移第一步就拒绝。40 个真 mail-* 会话全部命中。
根因二(**仍在写**,本仓侧):`plugins/dsh-mail-bridge/lib/message.js` 的
`userMessage()` 只产出 `{content, source}`。DSH 0.1.5 的 inbox 按 `message.id` 去重
(`dsh-agent-loop` 的投影 apply() 与 mutate() 各维护一个 Set),id 全是 undefined
⇒ **第二条消息必挂**。日志里最早的同类记录在 2026-09-07,累计 50+ 次。
官方形状在 `@deepseek-ai/dsh-llm` 的 `createMessage()`({id, role, content, source}),
同一份 dsh 里其它插件都用官方的 createUserMessage(),只有这个桥手搓。
以前没炸是因为读路径先坏,根本走不到 followup。
本次改动
- message.js/.d.ts: userMessage() 补 id: randomUUID() 与 role:'user'
- test/message.test.mjs: 钉住「id 非空」「两条消息 id 必须不同」,用官方 inbox
去重逻辑逐字复刻验证(修复前 message "undefined" is already pending,修复后 20 封全唯一)
- scripts/: repair-legacy-spliced-ids.mjs(v0,默认 dry-run)、
repair-v3-usermessage-ids.mjs(v3)、verify-mail-sessions-readable.mjs
(走生产真读路径 JsonlSessionPersistence.open,而非解码器口径)、两个 apply driver
- docs/DSH-0.1.5-MAIL-CHANNEL-ROOTCAUSE.md: 补执行结果与两处新事实
执行与验收(详见文档 §9-§15)
- v0 修 40 个、v3 修 2 个;逐文件解压后与备份 `cmp` **逐字节相等**,事件数 40/40 一致,
零丢失(25.2MB→12.5MB 是单帧改 500 行/帧的重压缩,不是丢数据)
- 真 mail-* 会话最终 **41/41 可读**
- journal 里同一会话从 `already exists` 变为 `resume 续谈`,且持续增长
(22647→22685 事件),最新 user/message 带真实 UUID;修复上线后 already pending 计数为 0
- 已在生产部署(deploy/redeploy-plugin.sh dsh,快照+原子软链+重启+后置验证全绿)
两个必须记住的坑
1. **校验与落盘不能共用同一批对象**:createRestore().decodeRow() 会原地改写入参
(补全 dt 数组),污染后写出去会报 `released Session row N has seq gap`。
这曾让 dry-run 说"40 个可修"、apply 只说"3 个"。
2. **判定磁盘健康只认 open()**:readSession() 走 SessionCorpus.load,命中有 live 会话时
直接返回内存快照、不校验磁盘;open() 才走 validateStoredEvents。两条路径结论相反
是设计使然,不是矛盾。
51 lines
2.1 KiB
Bash
Executable File
51 lines
2.1 KiB
Bash
Executable File
#!/bin/bash
|
||
# 在 dsh.service 离线窗口内执行 spliced-id 修复,并用**生产真读路径**验收。
|
||
#
|
||
# 为什么需要这个 driver:
|
||
# - repair 脚本的 `--apply` 要写 ~/.dsh/sessions,而 pi 会话自身跑在
|
||
# Landlock 沙箱里(只允许写工作区 + /tmp),所以必须由 systemd-run
|
||
# 起的临时单元来做写盘。
|
||
# - 停 dsh.service **不会**杀掉执行者:pi 会话跑在 pi-mail-bridge.service 下
|
||
# (两个独立单元),这一点是实测确认的,不是推断。
|
||
# - 脚本自检走 `sessionFormatCatalog.createRestore`(解码器口径),
|
||
# 与 dsh 桥实际用的 `JsonlSessionPersistence.open` 不是同一条路。
|
||
# 所以验收一律以 verify-mail-sessions-readable.mjs 为准。
|
||
set -uo pipefail
|
||
cd /home/program/agentmail
|
||
|
||
STAMP=$(date +%Y%m%d-%H%M%S)
|
||
LOG=".tmp/repair-apply-$STAMP.log"
|
||
exec > >(tee "$LOG") 2>&1
|
||
|
||
echo "### BEGIN $(date -Is)"
|
||
echo "### 执行者 cgroup: $(cat /proc/self/cgroup)"
|
||
echo "### dsh 处理前: $(systemctl is-active dsh.service)"
|
||
|
||
echo; echo "### [1/5] 停 dsh.service(会话日志单写者,必须离线)"
|
||
systemctl stop dsh.service
|
||
sleep 2
|
||
echo "dsh 处理后: $(systemctl is-active dsh.service)"
|
||
|
||
echo; echo "### [2/5] 修复前:生产真读路径基线"
|
||
node scripts/verify-mail-sessions-readable.mjs --only session.jsonl.zstd | tail -6
|
||
|
||
echo; echo "### [3/5] --apply 写盘"
|
||
node scripts/repair-legacy-spliced-ids.mjs --only session.jsonl.zstd --apply 2>&1 | tail -25
|
||
|
||
echo; echo "### [4/5] 修复后:生产真读路径验收"
|
||
node scripts/verify-mail-sessions-readable.mjs --only session.jsonl.zstd > .tmp/verify-after-$STAMP.log 2>&1
|
||
AFTER_RC=$?
|
||
tail -6 .tmp/verify-after-$STAMP.log
|
||
echo "verify 退出码: $AFTER_RC"
|
||
|
||
echo; echo "### [5/5] 起 dsh.service"
|
||
systemctl start dsh.service
|
||
sleep 6
|
||
echo "dsh: $(systemctl is-active dsh.service) dsh-lan: $(systemctl is-active dsh-lan.service)"
|
||
|
||
echo; echo "### 残留检查(应为 0):"
|
||
echo " .repair-staged: $(find /root/.dsh/sessions -name '*.repair-staged' | wc -l)"
|
||
echo " .bak-*: $(find /root/.dsh/sessions -name '*.bak-*' | wc -l)"
|
||
echo "### END $(date -Is)"
|
||
echo "### LOG: $LOG"
|