Files
MailUI4Agents/scripts/apply-spliced-id-repair.sh
JianFeeeee b4a8f74ae5 修复: dsh 邮件通道全断的**两侧**根因(桥侧不产 message id 是真正在写的那一处)
现象: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。两条路径结论相反
   是设计使然,不是矛盾。
2026-09-19 12:03:34 +08:00

51 lines
2.1 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"