From c0e64f48b68f213f2e84c870e71ebac53d6f199c Mon Sep 17 00:00:00 2001 From: Drew T <50529377+Druthulu@users.noreply.github.com> Date: Tue, 11 Aug 2026 21:12:44 -0600 Subject: [PATCH] =?UTF-8?q?feat(phase-30=20S48):=20tools/recover=5Fdrafts.?= =?UTF-8?q?py=20=E2=80=94=20the=20transcript=20IS=20the=20backup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tools/recover_drafts.py | 170 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 tools/recover_drafts.py diff --git a/tools/recover_drafts.py b/tools/recover_drafts.py new file mode 100644 index 0000000000..812df64205 --- /dev/null +++ b/tools/recover_drafts.py @@ -0,0 +1,170 @@ +#!/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 --names func_A,func_B --out .run/recover + tools/recover_drafts.py --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())