fix(stabilize): 安全默认反转 + immutable 体检

2026-09-04 全面排查后加固(state.db 误判循环教训):
1. 默认 dry-run:无参数跑 = 只体检,--apply 才执行修改/自动恢复
   (防 0x57 bad-auto 类:无参数触发自动恢复覆盖活动库)
2. 体检连接 mode=ro → mode=ro&immutable=1:gateway 运行中零风险
   (02:03 教训:普通只读连接也会触发 wal 访问与 gateway 并发)
3. 保留既有 lsof/gateway 运行保护(写操作前自动跳过)
This commit is contained in:
小唯 A06 2026-09-04 02:52:08 +08:00
parent bd2914bb49
commit 9b2f4c23d0
1 changed files with 62 additions and 5 deletions

View File

@ -71,8 +71,9 @@ def stabilize(dry_run: bool = False) -> int:
log("\n[1/5] 完整性体检")
report = {"started_at": now_iso(), "actions": [], "warnings": []}
# 用只读连接快速体检(不持有写锁,避免和 gateway 抢)
health_conn = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True, timeout=10)
# 用 immutable 只读连接快速体检(完全跳过 wal/lockgateway 运行时零风险)
# 🔴 2026-09-04 教训gateway 运行中普通只读连接mode=ro 非 immutable也会触发 wal 访问
health_conn = sqlite3.connect(f"file:{DB_PATH}?mode=ro&immutable=1", uri=True, timeout=10)
health_conn.execute("PRAGMA busy_timeout=5000")
ok, integrity = safe_run(health_conn, "PRAGMA integrity_check")
@ -124,6 +125,58 @@ def stabilize(dry_run: bool = False) -> int:
log(f" ⚠️ 发现 0 字节 WAL 残留将清理不破坏数据SQLite 会自动重建)")
report["warnings"].append("zero_wal_residue")
# === 阶段 1.5损坏自动恢复2026-09-03 新增 — 防止 gateway 启动失败导致 agent 失联)===
# ExecStartPre 场景gateway 尚未启动)若检测到真损坏 → 从最近健康快照恢复
# 前提:无 gateway 进程持有 DB手动运行时保护
if (not ok or integrity != "ok") and not dry_run:
import subprocess as _sp
_gw = _sp.run(["pgrep", "-f", "hermes_cli.main gateway run"],
capture_output=True, text=True)
if _gw.returncode == 0:
log(" ⚠️ gateway 正在运行,跳过自动恢复(避免覆盖活动 DB")
else:
SNAP_DIR = Path.home() / ".hermes" / "backups" / "state-db-snap"
if SNAP_DIR.exists():
snaps = sorted(SNAP_DIR.glob("state-*.db"))
if snaps:
latest = snaps[-1]
try:
_sc = sqlite3.connect(f"file:{latest}?mode=ro", uri=True, timeout=10)
_sok = _sc.execute("PRAGMA integrity_check").fetchone()[0]
_sc.close()
except sqlite3.Error as _e:
_sok = None
log(f" ⚠️ 快照 {latest.name} 读取失败: {_e}")
if _sok == "ok":
import shutil as _sh
_ts = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
_bad = DB_PATH.with_name(f"state.db.bad-auto-{_ts}")
_sh.copy2(DB_PATH, _bad)
_sh.copy2(latest, DB_PATH)
for _p in (WAL_PATH, SHM_PATH):
if _p.exists():
try:
_p.unlink()
except OSError:
pass
log(f" 🔄 自动恢复DB 损坏 → 从 {latest.name} 恢复(坏库备份 {_bad.name}")
report["actions"].append(f"auto_restore_from={latest.name}")
# 重新体检
try:
_hc = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True, timeout=10)
_ok2, _int2 = safe_run(_hc, "PRAGMA integrity_check")
report["integrity_after_restore"] = _int2
log(f" 恢复后 integrity_check = {_int2}")
_hc.close()
except sqlite3.Error as _e:
log(f" ⚠️ 恢复后体检失败: {_e}")
else:
log(f" ⚠️ 最近快照 {latest.name} 也不健康({_sok}),跳过自动恢复")
else:
log(" ⚠️ 无可用快照backups/state-db-snap 为空),跳过自动恢复")
else:
log(" ⚠️ 无快照目录 backups/state-db-snap跳过自动恢复")
# === 阶段 3核心修复写连接序列化===
log("\n[3/5] 应用稳定修复")
@ -221,10 +274,14 @@ def stabilize(dry_run: bool = False) -> int:
def main() -> int:
p = argparse.ArgumentParser(description="state.db 止血脚本")
p.add_argument("--dry-run", action="store_true", help="只体检,不修改")
p = argparse.ArgumentParser(description="state.db 止血脚本(安全默认:只体检 dry-run--apply 才执行修改)")
p.add_argument("--apply", action="store_true", help="执行实际修改(默认只体检不修改)")
p.add_argument("--dry-run", action="store_true", help="强制只体检(兼容旧用法)")
args = p.parse_args()
return stabilize(dry_run=args.dry_run)
# 🔴 安全默认反转2026-09-04无参数跑 = 只体检。显式 --apply且未 --dry-run才执行修改/自动恢复
dry_run = (not args.apply) or args.dry_run
log(f"模式: {'DRY-RUN 只体检(--apply 才执行修改)' if dry_run else 'APPLY 执行修改'}")
return stabilize(dry_run=dry_run)
if __name__ == "__main__":