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:
parent
bd2914bb49
commit
9b2f4c23d0
|
|
@ -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/lock,gateway 运行时零风险)
|
||||
# 🔴 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__":
|
||||
|
|
|
|||
Loading…
Reference in New Issue