mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
c0e64f48b6
Today a 28-agent wave lost 21 adversarially-verified drafts to a shared output directory, and I wrote them off before Drew asked whether the workflow results could just be analysed. They were all recoverable, for zero agent tokens. Encodes the method that worked 21/21, including the two shortcuts that do NOT: - taking each Write's content recovers only single-write drafts (8/21 — agents refine); - taking an Edit's new_string as a file yields a FRAGMENT, not a file. So it replays the mutation history per (agent, file_path), snapshots after every mutation, emits newest-first, and also scans Bash heredocs (the 21st draft never used Write/Edit at all). --gate runs match_one newest-first and keeps the first MATCH. Self-test on wf_d804f25a-f6f: 3/3 including the heredoc case.
171 lines
7.3 KiB
Python
171 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Recover agent-written draft files from a workflow run's TRANSCRIPTS. (P30 S48)
|
|
|
|
WHY THIS EXISTS. A 28-agent wave wrote its drafts into one shared directory, and later agents
|
|
deleted earlier agents' files while tidying. 21 adversarially-VERIFIED cracks looked destroyed and
|
|
were written off; a 5M-token re-crack wave was launched before Drew asked the right question —
|
|
"can you just analyse the workflow results to get those back?"
|
|
|
|
They were all recoverable. Every agent tool call is recorded in the run transcript WITH ITS CONTENT,
|
|
so the transcript is the backup for anything an agent produced. Recovery is deterministic and costs
|
|
zero agent tokens.
|
|
|
|
THE METHOD, and why the two obvious shortcuts fail:
|
|
* Taking each `Write` record's content recovers only the drafts that were written once — agents
|
|
REFINE. (Measured: 8 of 21.)
|
|
* Taking each `Edit`'s `new_string` as a file is worse: it is a FRAGMENT, not a file.
|
|
So: replay the mutation history per (agent, file_path) — `Write` sets the content, `Edit` applies
|
|
old->new to the current content — and snapshot after EVERY mutation. The wanted file state is one
|
|
of those snapshots, usually but not always the last (an agent may end on a failed experiment).
|
|
(Measured: 20 of 21.)
|
|
* The last file never used Write/Edit at all — it was written by a shell heredoc. So also scan
|
|
`Bash` commands for `<<'EOF' … EOF` bodies. (Measured: 21 of 21.)
|
|
|
|
Snapshots are emitted newest-first so a caller can gate them in order and stop at the first MATCH;
|
|
`--gate` does exactly that when given the per-function asm subdir map.
|
|
|
|
tools/recover_drafts.py <run_dir> --names func_A,func_B --out .run/recover
|
|
tools/recover_drafts.py <run_dir> --names-json lost.json --out .run/recover \
|
|
--gate targets.json # [{"name":..., "sub":...}] -> runs match_one, writes recovered.json
|
|
"""
|
|
import argparse
|
|
import glob
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
HEREDOC_RE = re.compile(r"<<\s*'?(\w+)'?\s*\n(.*?)\n\1", re.S)
|
|
|
|
|
|
def _tool_blocks(obj, out):
|
|
"""Every {name, input} tool-use block anywhere in a transcript record."""
|
|
if isinstance(obj, dict):
|
|
if isinstance(obj.get("input"), dict) and obj.get("name"):
|
|
out.append((obj["name"], obj["input"]))
|
|
for v in obj.values():
|
|
_tool_blocks(v, out)
|
|
elif isinstance(obj, list):
|
|
for v in obj:
|
|
_tool_blocks(v, out)
|
|
|
|
|
|
def snapshots(run_dir, names):
|
|
"""{fn: [content, ...]} newest-first, deduped, from every agent transcript in `run_dir`."""
|
|
want = set(names)
|
|
per_fn = {n: [] for n in want}
|
|
for path in sorted(glob.glob(os.path.join(run_dir, "agent-*.jsonl"))):
|
|
state = {} # file_path -> current content, PER AGENT
|
|
for line in open(path, errors="ignore"):
|
|
if not any(t in line for t in ('"Write"', '"Edit"', '"Bash"')):
|
|
continue
|
|
try:
|
|
rec = json.loads(line)
|
|
except Exception:
|
|
continue
|
|
ops = []
|
|
_tool_blocks(rec, ops)
|
|
for name, inp in ops:
|
|
if name == "Bash": # heredoc-written files
|
|
cmd = inp.get("command", "") or ""
|
|
for _tag, body in HEREDOC_RE.findall(cmd):
|
|
for fn in want:
|
|
if fn in body and "{" in body:
|
|
per_fn[fn].append(body)
|
|
continue
|
|
fp = inp.get("file_path", "") or ""
|
|
if not fp.endswith(".c"):
|
|
continue
|
|
fn = os.path.basename(fp)[:-2]
|
|
if fn not in want:
|
|
continue
|
|
if name == "Write":
|
|
state[fp] = inp.get("content", "")
|
|
else:
|
|
cur = state.get(fp)
|
|
if cur is None:
|
|
continue # an Edit to a file this agent never wrote
|
|
old, new = inp.get("old_string", ""), inp.get("new_string", "")
|
|
if not old or old not in cur:
|
|
continue # stale/failed edit — keep the prior state
|
|
state[fp] = cur.replace(old, new) if inp.get("replace_all") else cur.replace(old, new, 1)
|
|
per_fn[fn].append(state[fp])
|
|
out = {}
|
|
for fn, versions in per_fn.items():
|
|
seen, uniq = set(), []
|
|
for c in reversed(versions): # newest first
|
|
h = hash(c)
|
|
if h in seen:
|
|
continue
|
|
seen.add(h)
|
|
uniq.append(c)
|
|
if uniq:
|
|
out[fn] = uniq
|
|
return out
|
|
|
|
|
|
def gate(fn, path, sub):
|
|
r = subprocess.run(f"python3 tools/match_one.py {fn} --c {path} --asm-subdir {sub}",
|
|
shell=True, cwd=REPO, capture_output=True, text=True, timeout=600)
|
|
first = (r.stdout or "").strip().split("\n")[0]
|
|
return first.startswith("MATCH"), first[:80]
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("run_dir", help="the workflow run's transcript dir (…/subagents/workflows/wf_…)")
|
|
ap.add_argument("--names", help="comma-separated func_XXXXXXXX to recover")
|
|
ap.add_argument("--names-json", help="JSON list of {'fn':…} or bare names")
|
|
ap.add_argument("--out", default=".run/recover")
|
|
ap.add_argument("--gate", metavar="TARGETS_JSON",
|
|
help="[{name, sub}] — gate snapshots newest-first, keep the first MATCH")
|
|
a = ap.parse_args()
|
|
|
|
names = set()
|
|
if a.names:
|
|
names |= {x.strip() for x in a.names.split(",") if x.strip()}
|
|
if a.names_json:
|
|
for r in json.load(open(a.names_json)):
|
|
names.add(r["fn"] if isinstance(r, dict) else r)
|
|
if not names:
|
|
ap.error("give --names or --names-json")
|
|
|
|
snaps = snapshots(a.run_dir, names)
|
|
outdir = os.path.join(REPO, a.out)
|
|
for fn, versions in snaps.items():
|
|
d = os.path.join(outdir, fn)
|
|
os.makedirs(d, exist_ok=True)
|
|
for i, c in enumerate(versions):
|
|
open(os.path.join(d, f"v{i:02d}.c"), "w").write(c)
|
|
missing = sorted(names - set(snaps))
|
|
print(f"recover_drafts: {len(snaps)}/{len(names)} functions have snapshots "
|
|
f"({sum(len(v) for v in snaps.values())} versions) -> {a.out}"
|
|
+ (f"; NO RECORD: {missing}" if missing else ""))
|
|
|
|
if not a.gate:
|
|
return 0
|
|
subs = {t["name"]: t["sub"] for t in json.load(open(a.gate))}
|
|
hits = {}
|
|
for fn, versions in sorted(snaps.items()):
|
|
sub = subs.get(fn)
|
|
if not sub:
|
|
print(f" {fn}: no asm subdir given — skipped"); continue
|
|
for i, _c in enumerate(versions):
|
|
p = os.path.join(a.out, fn, f"v{i:02d}.c")
|
|
ok, line = gate(fn, p, sub)
|
|
if ok:
|
|
hits[fn] = p
|
|
print(f" MATCH {fn} <- v{i:02d}.c")
|
|
break
|
|
else:
|
|
print(f" no version matches: {fn} ({len(versions)} tried)")
|
|
json.dump(hits, open(os.path.join(REPO, a.out, "recovered.json"), "w"), indent=1)
|
|
print(f"recover_drafts: {len(hits)}/{len(snaps)} gated MATCH -> {a.out}/recovered.json")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|