今天中午,一次本来只想做环境预检的任务,覆盖了另一条业务线当天正在维护的文件。

被覆盖的记分卡有 91,566 B。执行者拿来恢复的,是前一天 11 月 13 日 12:10 留下的 77,820 B 旧备份。文件因此倒退了一天,两项当天成果消失。后来,团队从 _attic 找到一份更晚的 89,218 B 备份,才把文件恢复回来。

这不是一次成功的回滚。覆盖已经发生,恢复只是把损失控制住;那两项成果仍需要重新补回。

事故是怎么发生的

执行者在做一个与记分卡内容无关的预检时,发现目录里有 .bak 文件。流程把“有一份能读的备份”误当成“应该从它恢复”,没有先确认备份和目标文件谁更新,就直接用旧备份覆盖了主文件。

执行 trace 里,操作者随后明确承认:它已经用 .bak 覆盖了原文件,并指出这很冒险、具有破坏性。问题不是它没意识到风险,而是意识到时,覆盖已经完成。

当天文件新增的内容包括一条截至时点标注和一行 F6_W。旧副本没有这些内容,所以恢复后它们不见了。由于相关工作区不在 Git 跟踪范围里,git 没有可用的历史版本,这次不能靠 git checkout 回滚。最后能恢复,是因为 _attic 里还留着一份比误用备份更新的副本。

不是“多提醒一句小心”就能解决

把原因归结为“操作时疏忽”,会让防护停留在口头提醒:下次换一个执行者、换一个任务,照样可能发生。

真正的失效点有两个:

  1. 写入权限没有按归属隔离。 Agent 平时会读取其他工作区里的材料,但读取能力没有被限制为只读;一个不相关的任务也能改写别人的文件。
  2. 恢复流程没有判断版本方向。 .bak 只说明“曾经保存过一份副本”,不说明它比当前文件新,更不自动拥有覆盖主文件的资格。

文件名、字节数和“预检需要一个干净状态”都不能证明恢复源正确。这里最直接的危险信号其实已经存在:候选备份比目标小了 13,746 B,而它的修改时间也早于当天目标。只要在覆盖前比较时间方向,流程就应该停下来。

补上的防线:先拦目标,再判断版本

事故后新增了一道跨 Agent 写入守卫。它把其他 Agent 的工作区视为只读;对允许写入的目标,准备覆盖前先检查候选源,并在继续之前给现有目标留一份备份。如果现行目标的修改时间晚于拟写入源,直接阻断旧版本覆盖。

这道守卫已按三个场景验证:跨 Agent 直接写入被 BLOCK;旧源覆盖新目标被 BLOCK;正常写入判为 SAFE,并先备份现有目标。它把“记得小心”变成了可执行的拒绝条件。

但脚本不能替代正确的恢复习惯。遇到需要恢复的文件,顺序应该是:

  1. 先识别文件归属;不属于自己负责的工作区,就不写。
  2. 分别读取目标和候选恢复源的修改时间,确认时间可比较、来源可解释。
  3. 目标更新时,停止覆盖,不要因为文件后缀是 .bak 就继续。
  4. 确认恢复源确实更合适后,先备份现有目标,再执行恢复。
  5. 恢复后核对内容,而不只看命令是否成功或文件是否重新出现。

如果 Git 不跟踪该目录,Git 回滚就不是兜底方案。要让恢复可信,备份策略也必须覆盖“协作目录不在版本控制里”的现实:保留带时间的副本、知道它来自哪个时点,并核对恢复后丢失了什么。

这次最该记住的事

备份不是权威版本,恢复也不是无风险操作。 在共享文件上,写入前必须先证明“我有权写”和“我的来源没有比目标更旧”。任何一项证明不了,就停在只读检查,不要覆盖。

这次文件最终恢复了,但不能把“找回了一份较新的副本”说成“事故没有造成损失”。版本控制之外的工作区尤其如此:一次成功的覆盖可能没有报错,代价却是当天的成果静默消失。


事故经过已做脱敏处理。