mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
8edb918480
THE CARD USED TO HAND AGENTS A WRONG TWIN ABOUT ONCE IN FIVE. `⭐ func X IS BANKED AT THIS ADDRESS`
never checked that the two functions were the same SIZE, and overlays share addresses between
unrelated functions as readily as they share code. Measured over this session's ~60 cards: about a
dozen agents reported discarding the lead themselves, and one card advertised a 72-instruction
namesake — with journal history claiming "already MATCH closeness 0" — to a 241-instruction target.
A confidently wrong lead costs more than no lead, because the agent believes it.
corpus.sig already carries `nins` and `h_seq`, so the fix is free: `_same_addr_banked` now returns
(binary, nins, h_seq); the card keeps a lead only at a MATCHING instruction count, marks it strong
when the mnemonic skeleton matches too, and prints an explicit `⚠ IGNORE` naming the binaries where
that address holds something else, with both sizes.
VERIFIED IN BOTH DIRECTIONS against known-true cases before being believed (never trust a filter you
have not tried to fool):
* the trap: ov_SC03_105:func_801806F8 (241) vs ov_SC03_013 (72) -> `⚠ IGNORE`.
* the positive: ov_SC02_003:func_80187B40 (158) -> strong lead to ov_SC02_000 (158, same h_seq,
banked this session) AND, in the same card, warned off ov_SC04_011's 138-ins homonym at that
same address. That is precisely the pair a wave agent sorted out by hand hours earlier.
Cookbook §438 (the law: a lead is fuel only if it carries the cheapest fact that can refute it —
size refutes a homonym for free and nobody had asked) and §439, the S74 lever set: MEM_IN_STRUCT_P
as a two-way alias-oracle dial (four agents converged on it independently); `goto`-into-a-shared-tail
vs longhand as a REGALLOC dial because gcc-2.7.2 cross-jumps after allocation; `for` -> do/while as a
length-changing scheduling dial; allocno PRIORITY via a non-volatile asm at a loop head, with the
measurement that register pins are actively harmful for that class; the -O0 global-RMW rule
(`x++` emits the copy-back quartet, `x = x+1` does not); why `sll 16; srl 16` survives only across a
CALL; `sltiu N` without `addiu -1` proving an empty `case 0` is mandatory; block-scoped temps in
duplicated bodies; two `register asm` vars cannot share a hard reg; and `x*32` vs `x<<5` emitting
lh vs lhu — which match_one's %lo mask HIDES, so it must be checked with objdump.
1128 lines
71 KiB
Python
1128 lines
71 KiB
Python
#!/usr/bin/env python3
|
|
"""api_agent.py — give an OpenAI-compatible API model the SAME HARNESS a Claude agent gets.
|
|
|
|
WHY THIS EXISTS (P31 S56, measured):
|
|
`api_draft.py` hands a model one enormous single-shot prompt: the whole cookbook inlined,
|
|
the .s, the Ghidra-C, and "reply with one ```c block". A Claude agent doing the same function
|
|
used SIXTEEN TOOL CALLS -- read the .s, locate and read the 0.99-similarity banked seed in its
|
|
own destination TU, GREP the cookbook for the one §31 tell that mattered, three match_one
|
|
iterations, then an objdump relocation audit it ran unprompted to satisfy law 1c -- and matched.
|
|
|
|
Those are different experiments. Inlining 22,009 cookbook lines costs ~568k tokens per call
|
|
(~$1.02) and still leaves the model hunting for the relevant section inside a wall of text;
|
|
the agent greps for what it needs and spends ~60k tokens TOTAL. Comparing a single-shot model
|
|
against a tool-using agent measures the HARNESS, not the model.
|
|
|
|
This module closes that gap: the same four capabilities the Claude agent actually used, exposed
|
|
as OpenAI-style function tools, driven in a loop, with match_one as the iteration oracle. Then
|
|
"GLM vs Opus" is a question about models.
|
|
|
|
WHAT IT DELIBERATELY DOES NOT DO:
|
|
* It does not bank. The draft is the deliverable; the whole-binary byte-gate stays the sole
|
|
arbiter (G3/P9), exactly as for an agent-drafted wave card.
|
|
* It does not widen file access beyond the repo's read-only source/asm/docs surface.
|
|
* match_one MASKS jal/HI16/LO16 relocations -- it verifies instruction SHAPE, not symbol
|
|
IDENTITY (§174 law 1c). A MATCH here is a CANDIDATE. The prompt says so, and the caller must
|
|
still run reloc_identity before crediting one.
|
|
|
|
USAGE
|
|
API_BASE=https://openrouter.ai/api/v1 API_KEY=sk-or-... MODEL=z-ai/glm-5.3 \
|
|
REASON_CAP=6000 .venv/bin/python tools/api_agent.py \
|
|
--targets .run/bakeoff/rung0.json --out .run/bakeoff/glm-agent --max-turns 24
|
|
"""
|
|
import argparse, glob, http.client, json, os, re, socket, subprocess, sys, time, urllib.request, urllib.error
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import api_draft as AD # reuse the validated endpoint/match_one/extract plumbing
|
|
|
|
REPO = AD.REPO
|
|
|
|
# --- request telemetry (P31 S58) --------------------------------------------------------------
|
|
# Every POST appends one line to .run/api_rate.jsonl: when, which model, which pid, and the event.
|
|
# WHY: the overnight ox campaign needs a MEASURED requests-per-minute and a MEASURED 429 rate with
|
|
# platform-vs-provider attribution (R40/R41). Before this, the only 429 evidence was a flush=True
|
|
# print into a block-buffered log file — so "no 429s" and "the buffer has not filled yet" were the
|
|
# same observation. A shared append-only JSONL is readable from outside the process at any moment.
|
|
_RATE_PATH = os.path.join(REPO, '.run/api_rate.jsonl')
|
|
|
|
|
|
def _rate_log(event, **kw):
|
|
"""Append one telemetry record. Never raises: telemetry must not be able to kill a run."""
|
|
try:
|
|
rec = {'t': time.time(), 'pid': os.getpid(), 'model': AD.MODEL, 'ev': event}
|
|
rec.update(kw)
|
|
with open(_RATE_PATH, 'a') as fh:
|
|
fh.write(json.dumps(rec) + chr(10))
|
|
except Exception:
|
|
pass
|
|
|
|
# Read-only surface. EXTRA_READABLE widens it for a task that genuinely needs more — a
|
|
# TOOLING-DESIGN brief must read config/ and tools/, which a decompilation brief must not.
|
|
READABLE = tuple(x for x in (('src/', 'asm/', 'docs/', 'include/', '.run/bakeoff/')
|
|
+ tuple(y for d in os.environ.get('EXTRA_READABLE', '').split(',')
|
|
if d.strip()
|
|
# BOTH forms: 'config/' matches a directory tree, bare
|
|
# 'Makefile' matches a single granted FILE (which never
|
|
# matches a trailing-slash prefix).
|
|
for y in (f'{d.strip().rstrip("/")}/', d.strip().rstrip("/")))))
|
|
MAX_BYTES = 60000 # per read_file / grep response, so one call cannot flood context
|
|
NUDGE_MAX = int(os.environ.get('NUDGE_MAX', '6')) # continuations offered when a turn yields no tool call
|
|
REPEAT_MAX = int(os.environ.get('REPEAT_MAX', '4')) # identical consecutive tool calls before the loop is broken
|
|
COOKBOOK_FULL = os.environ.get('COOKBOOK_FULL', '0') != '0' # inline the whole cookbook (big-context models only)
|
|
|
|
TOOLS = [
|
|
{'type': 'function', 'function': {
|
|
'name': 'grep',
|
|
'description': ("Search the repository with ripgrep-style regex. THIS IS HOW YOU USE THE "
|
|
"COOKBOOK: docs/matching-cookbook.md is ~22,000 lines and ~450 numbered "
|
|
"sections -- never ask to read it whole, grep it for the idiom you are "
|
|
"actually looking at, then read_file the section you hit. Each cookbook "
|
|
"hit comes back prefixed with the SECTION it lives in (e.g. '§164 | "
|
|
"docs/...:13446: ...'): cite that §, never the line number after the "
|
|
"colon -- '§13446' is not a section and nobody can grep it."),
|
|
'parameters': {'type': 'object', 'required': ['pattern'], 'properties': {
|
|
'pattern': {'type': 'string', 'description': 'regex'},
|
|
'path': {'type': 'string', 'description': 'file or dir, default docs/matching-cookbook.md'},
|
|
'max_hits': {'type': 'integer', 'description': 'default 40'}}}}},
|
|
{'type': 'function', 'function': {
|
|
'name': 'read_file',
|
|
'description': 'Read a repo file, optionally a line range. Use after grep to read a section.',
|
|
'parameters': {'type': 'object', 'required': ['path'], 'properties': {
|
|
'path': {'type': 'string'},
|
|
'start_line': {'type': 'integer'},
|
|
'end_line': {'type': 'integer'}}}}},
|
|
{'type': 'function', 'function': {
|
|
'name': 'match_one',
|
|
'description': ("THE ORACLE. Compile your C and diff it against the target instruction by "
|
|
"instruction. Returns MATCH, or 'near N' with a per-instruction diff "
|
|
"(idx | MINE | TARGET). Call this as often as you like -- it is free and "
|
|
"local. NOTE: it masks jal/HI16/LO16 relocations, so it checks instruction "
|
|
"SHAPE, not symbol IDENTITY: after it says MATCH, re-read the .s and verify "
|
|
"every symbol you wrote against its own relocation lines."),
|
|
'parameters': {'type': 'object', 'required': ['code'], 'properties': {
|
|
'code': {'type': 'string', 'description': 'the complete C function, no ``` fences'}}}}},
|
|
{'type': 'function', 'function': {
|
|
'name': 'submit',
|
|
'description': 'Submit your final C. Call this when match_one says MATCH, or when you have exhausted your levers.',
|
|
'parameters': {'type': 'object', 'required': ['code'], 'properties': {
|
|
'code': {'type': 'string'},
|
|
'status': {'type': 'string', 'description': 'MATCH | NEAR | IMMOVABLE'},
|
|
'notes': {'type': 'string', 'description': 'residual class, levers tried, what the cookbook did NOT tell you'}}}}},
|
|
]
|
|
|
|
|
|
def _safe(path):
|
|
p = os.path.normpath(path).lstrip('/')
|
|
# Compare with a trailing slash appended so a BARE DIRECTORY matches its own allowed root.
|
|
# Every entry in READABLE ends in '/', so `p = '.run/bakeoff'` failed startswith and the agent
|
|
# was refused access to the very directory that was granted to it — measured P31 S58: a tooling
|
|
# brief could not grep the tool sources staged for it under .run/bakeoff/toolwork, and burned
|
|
# its turns on nudges. A refusal that names the granted path as "outside the readable surface"
|
|
# is a harness bug wearing a permissions message (R40).
|
|
probe = p if p.endswith('/') else p + '/'
|
|
if not (probe.startswith(READABLE) or p.startswith(READABLE)) or '..' in p.split(os.sep):
|
|
return None
|
|
full = os.path.join(REPO, p)
|
|
return full if os.path.exists(full) else None
|
|
|
|
|
|
_SEC_CACHE = {}
|
|
_SEC_HEAD = re.compile(r'^#+\s*(\u00a7[0-9][\w.\-]*)')
|
|
|
|
|
|
def _section_index(full):
|
|
"""[(first_line, label)] for every `## §N` heading in a cookbook-shaped file (cached by mtime)."""
|
|
key = (full, os.path.getmtime(full))
|
|
if key not in _SEC_CACHE:
|
|
# BOTH LEVELS. The corpus nests: `## §164` holds `#### §16Xy`, and the sub-heading is the
|
|
# precise citation while the top-level one is the stable anchor an agent can always find.
|
|
# Emitting only the parent throws away precision; only the child sends the next agent
|
|
# hunting for a heading it cannot place. Emit `§164 > §16Xy` and both are greppable.
|
|
idx, top = [], ''
|
|
with open(full, errors='replace') as fh:
|
|
for i, line in enumerate(fh, 1):
|
|
m = _SEC_HEAD.match(line)
|
|
if not m:
|
|
continue
|
|
lab = m.group(1)
|
|
if line.startswith('## ') and not line.startswith('### '):
|
|
top = lab
|
|
idx.append((i, lab))
|
|
else:
|
|
idx.append((i, f'{top} > {lab}' if top and top != lab else lab))
|
|
_SEC_CACHE.clear()
|
|
_SEC_CACHE[key] = idx
|
|
return _SEC_CACHE[key]
|
|
|
|
|
|
def _label_sections(full, out):
|
|
"""Prefix each cookbook grep hit with the § section that CONTAINS it.
|
|
|
|
AGENTS CITE THE GREP LINE NUMBER AS A SECTION NUMBER (P31 S60, measured). The harvested
|
|
notes carry 13 citations to sections that do not exist — `§2329` (x5) is line 2329, inside
|
|
§28; `§13446/13474` are lines inside §164; `§1914` is a line in §20; the corpus stops at
|
|
§273. A citation nobody can grep is worse than none: the next agent reads "covered by
|
|
§2329", finds nothing, and re-derives a lever we already own — and a distill reviewer
|
|
scores the note as covered by a section that was never written.
|
|
|
|
The fix is not a warning, it is the right answer in the output (R33): grep hands back the
|
|
containing §, so the number in front of the agent is the one it should cite.
|
|
"""
|
|
idx = _section_index(full)
|
|
if not idx:
|
|
return out
|
|
import bisect
|
|
starts = [i for i, _lab in idx]
|
|
lines = []
|
|
for ln in out.splitlines():
|
|
parts = ln.split(':', 2)
|
|
lab = ''
|
|
if len(parts) == 3 and parts[1].isdigit():
|
|
k = bisect.bisect_right(starts, int(parts[1])) - 1
|
|
if k >= 0:
|
|
lab = idx[k][1] + ' | '
|
|
lines.append(lab + ln)
|
|
return '\n'.join(lines)
|
|
|
|
|
|
def run_tool(name, args, target, outdir, state):
|
|
if name == 'grep':
|
|
path = args.get('path') or 'docs/matching-cookbook.md'
|
|
full = _safe(path)
|
|
if not full:
|
|
return f'REFUSED: {path} is outside the readable surface {READABLE}'
|
|
n = int(args.get('max_hits') or 40)
|
|
r = subprocess.run(['grep', '-rn', '-m', str(n), '-E', args['pattern'], full],
|
|
capture_output=True, text=True, timeout=120)
|
|
out = r.stdout or '(no hits)'
|
|
if full.endswith('matching-cookbook.md') and r.stdout:
|
|
out = _label_sections(full, out)
|
|
return out[:MAX_BYTES]
|
|
if name == 'read_file':
|
|
full = _safe(args['path'])
|
|
if not full:
|
|
return f"REFUSED or missing: {args['path']}"
|
|
if os.path.isdir(full):
|
|
# A DIRECTORY IS A REASONABLE THING TO ASK FOR (P31 S56). The brief itself said
|
|
# "grep src/ov_SC03_099/ for jtbl_", so the model read_file'd the directory -- and the
|
|
# bare open() raised IsADirectoryError, which was unhandled and killed the run at turn 2
|
|
# of 50. Answer the question instead of dying: list it, so the next turn can pick a file.
|
|
try:
|
|
names = sorted(os.listdir(full))[:200]
|
|
except OSError as e: # noqa: BLE001 - report, never crash
|
|
return f'cannot list {args["path"]}: {e}'
|
|
return (f'{args["path"]} is a DIRECTORY with {len(names)} entries — read one of these, '
|
|
f'or grep the directory instead:\n' + '\n'.join(' ' + n for n in names))
|
|
lines = open(full, errors='replace').read().splitlines()
|
|
a = max(1, int(args.get('start_line') or 1))
|
|
b = min(len(lines), int(args.get('end_line') or min(len(lines), a + 400)))
|
|
body = '\n'.join(f'{i}: {lines[i-1]}' for i in range(a, b + 1))
|
|
return body[:MAX_BYTES] + ('' if len(body) <= MAX_BYTES else '\n... (truncated, narrow the range)')
|
|
if name in ('match_one', 'submit'):
|
|
code = args.get('code') or ''
|
|
cfile = os.path.join(outdir, target['name'] + '.c')
|
|
open(cfile, 'w').write(code if code.endswith('\n') else code + '\n')
|
|
status, close, out = AD.match_one(target['name'], cfile, os.path.dirname(target['asm']))
|
|
score = 0 if status == 'match' else (close if status == 'near' else 10 ** 8)
|
|
if score < state['best'][1]:
|
|
state['best'] = (code, score)
|
|
state['calls'] += 1
|
|
if name == 'submit':
|
|
state['submitted'] = {'status': args.get('status'), 'notes': args.get('notes')}
|
|
tag = 'MATCH' if status == 'match' else (f'near {close}' if status == 'near' else 'compile-fail')
|
|
return f'{tag}\n' + '\n'.join(out.splitlines()[:60])
|
|
return f'unknown tool {name}'
|
|
|
|
|
|
def call(messages, tools):
|
|
payload = {'model': AD.MODEL, 'messages': messages, 'tools': tools,
|
|
'max_tokens': AD.MAXTOK, 'temperature': AD.TEMP}
|
|
if AD.REASON_CAP:
|
|
payload['reasoning'] = {'max_tokens': AD.REASON_CAP}
|
|
elif AD.REASON_EFFORT:
|
|
payload['reasoning'] = {'effort': AD.REASON_EFFORT}
|
|
req = urllib.request.Request(AD.API_BASE + '/chat/completions', data=json.dumps(payload).encode(),
|
|
headers={'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer ' + (AD.API_KEY or 'none')})
|
|
# 429 IS A PACE SIGNAL, NOT A VERDICT (P31 S56). OpenRouter's free tier allows 20 requests per
|
|
# MINUTE (1000/day at >=$10 lifetime credit). A tool loop bursts one request per turn, so two
|
|
# free arms running concurrently trip the per-minute ceiling within a few turns -- and the first
|
|
# version of this function let that kill the run: dots-3-note died at turn 21 and nemotron at
|
|
# turn 3, both at $0.00 with ZERO oracle calls, and both got written down as model failures.
|
|
# The daily budget was barely touched. Back off and continue; only give up after MAX_429 tries.
|
|
# MAX_429 is env-tunable because a PROVIDER burst is not a verdict and not our ceiling.
|
|
# The upstream shared pool saturates and then resumes; the only wrong response is to give
|
|
# up inside the burst. Default 6 tolerates ~6 min of backoff (20+40+80+120+120); the
|
|
# overnight campaign raises it so a long burst costs latency, never a lost function.
|
|
MAX_429, delay = int(os.environ.get('MAX_429', '6')), 20
|
|
for attempt in range(MAX_429):
|
|
_rate_log('POST')
|
|
try:
|
|
# 1800s meant one hung generation parked an agent for THIRTY MINUTES, and from the
|
|
# outside that is indistinguishable from a stall (measured: an agent 1,690s silent
|
|
# while the 429 backoff chain caps at ~14 min, so it was not backoff). At 2,000 agents
|
|
# a 30-minute park is 30 minutes of a worker doing nothing. Env-tunable; the retry loop
|
|
# above already treats a timeout as transient and tries again.
|
|
with urllib.request.urlopen(req, timeout=int(os.environ.get('HTTP_TIMEOUT', '420'))) as r:
|
|
d = json.loads(r.read())
|
|
# A SOFT 429: HTTP 200 WITH A RATE-LIMIT ERROR IN THE BODY (P31 S59, measured).
|
|
# OpenRouter returns the provider's throttle as a 200 whose body carries no `choices`
|
|
# and an `error` of {"message": "Provider returned error", "code": 429}. That never
|
|
# reached the 429 handler below — which keys on HTTPError — so it fell through to the
|
|
# `no choices` raise and KILLED THE AGENT at turn 1 with no draft, no submit, $0.00.
|
|
# Measured on wave cd: 260 of 260 shards "finished cleanly" and 187 produced NOTHING,
|
|
# 186 of them on this exact error. It reads as a completion collapse (28-60% where the
|
|
# fleet used to do 84-89%) and it is really an unretried rate limit. Same treatment as
|
|
# every other transient: back off and carry on.
|
|
_err = (d or {}).get('error') or {}
|
|
if 'choices' not in (d or {}) and (
|
|
str(_err.get('code')) == '429' or 'rate' in str(_err.get('message', '')).lower()
|
|
or 'Provider returned error' in str(_err.get('message', ''))):
|
|
if attempt == MAX_429 - 1:
|
|
raise RuntimeError('soft 429 exhausted: ' + json.dumps(_err)[:300])
|
|
_rate_log('429', src='SOFT-BODY', wait=delay, attempt=attempt + 1)
|
|
print(f' (soft 429 in a 200 body — waiting {delay}s, '
|
|
f'retry {attempt + 1}/{MAX_429 - 1})', flush=True)
|
|
time.sleep(delay)
|
|
delay = min(delay * 2, 120)
|
|
continue
|
|
break
|
|
except (http.client.IncompleteRead, http.client.RemoteDisconnected,
|
|
ConnectionError, TimeoutError, socket.timeout) as e:
|
|
# TRANSIENT TRANSPORT FAILURES ARE NOT VERDICTS EITHER (P31 S56). The 429 path below
|
|
# was added first and everything else stayed fatal, so an IncompleteRead at turn 30 --
|
|
# a truncated HTTP body, nothing to do with the model -- ended deepseek's BEST rung-1
|
|
# run (near 17 of 121, 10 turns still unspent) and it was nearly written down as the
|
|
# model's ceiling. Same treatment as a rate limit: back off and carry on.
|
|
if attempt == MAX_429 - 1:
|
|
raise
|
|
print(f' (transport {type(e).__name__} — waiting {delay}s, '
|
|
f'retry {attempt + 1}/{MAX_429 - 1})', flush=True)
|
|
time.sleep(delay)
|
|
delay = min(delay * 2, 120)
|
|
continue
|
|
except urllib.error.HTTPError as e:
|
|
# 5xx IS TRANSIENT TRANSPORT, NOT A VERDICT — retry it exactly like a 429.
|
|
# Only 429 was retried, so a single 502/503/504 raised, the caller logged "API error
|
|
# HTTPError", and the function was ABANDONED mid-solve. Measured P31 S58: one shard
|
|
# lost func_801833E8 to a 502 at turn 11 while sitting at "near 19" — two oracle calls
|
|
# of real progress discarded and written down as a model outcome ("near 19, no submit").
|
|
# A gateway hiccup must cost latency, never a function. (R40: exonerate the instrument.)
|
|
if (e.code != 429 and not (500 <= e.code < 600)) or attempt == MAX_429 - 1:
|
|
raise
|
|
if e.code != 429:
|
|
wait = min(delay, 60)
|
|
_rate_log(str(e.code), wait=wait, attempt=attempt + 1)
|
|
print(f' (HTTP {e.code} — transient, waiting {wait}s, '
|
|
f'retry {attempt + 1}/{MAX_429 - 1})', flush=True)
|
|
time.sleep(wait)
|
|
delay = min(delay * 2, 120)
|
|
continue
|
|
# A 429 has TWO possible sources and they need different responses:
|
|
# * PLATFORM -- OpenRouter's own free-variant caps (20/min, 1000/day at >=$10
|
|
# lifetime credit) or DDoS protection. Carries X-RateLimit-{Limit,Remaining,Reset}.
|
|
# * PROVIDER -- the upstream serving this model is throttling or at capacity.
|
|
# Carries error.metadata.provider_code; OpenRouter already retried other providers.
|
|
# Report which one fired instead of guessing: the first version of this comment asserted
|
|
# "per-minute ceiling" without reading either signal (R14 -- a cause you did not measure
|
|
# is not a finding).
|
|
hdrs = getattr(e, 'headers', None) or {}
|
|
lim = hdrs.get('X-RateLimit-Limit')
|
|
rem = hdrs.get('X-RateLimit-Remaining')
|
|
rst = hdrs.get('X-RateLimit-Reset')
|
|
ra = hdrs.get('Retry-After')
|
|
pcode = ''
|
|
try:
|
|
body = json.loads(e.read().decode() or '{}')
|
|
md = (body.get('error') or {}).get('metadata') or {}
|
|
# The field is provider_error_code, NOT provider_code -- the first version guessed
|
|
# the name and so every 429 logged UNATTRIBUTED while the body said exactly what
|
|
# was wrong (measured: glm-5.2:free -> provider_name=Decart,
|
|
# limit_source=upstream_provider_shared_pool). limit_source distinguishes an
|
|
# account/platform cap from a provider's shared free pool, which need different
|
|
# remedies: waiting helps the former, only provider routing or a BYOK key helps
|
|
# the latter.
|
|
pcode = md.get('provider_error_code') or md.get('provider_code') or ''
|
|
if md.get('provider_name'):
|
|
pcode += f" via {md['provider_name']}"
|
|
if md.get('limit_source'):
|
|
pcode += f" [{md['limit_source']}]"
|
|
except Exception: # noqa: BLE001 - diagnostics only
|
|
pass
|
|
src = f'PROVIDER (provider_code={pcode})' if pcode else (
|
|
f'PLATFORM (limit={lim} remaining={rem} reset={rst})' if lim else 'UNATTRIBUTED')
|
|
wait = delay
|
|
if ra: # honor the server's own hint (docs)
|
|
try:
|
|
wait = max(1, min(int(float(ra)), 300))
|
|
except ValueError:
|
|
pass
|
|
_rate_log('429', src=src, wait=wait, attempt=attempt + 1)
|
|
print(f' (429 {src} — waiting {wait}s, retry {attempt + 1}/{MAX_429 - 1})', flush=True)
|
|
time.sleep(wait)
|
|
delay = min(delay * 2, 120)
|
|
AD._COST[0] += (d.get('usage') or {}).get('cost', 0) or 0
|
|
# A 200 RESPONSE CAN STILL CARRY AN ERROR INSTEAD OF choices (P31 S56). Reading d['choices']
|
|
# blind turned two distinct causes into the same bare `KeyError 'choices'`: nemotron-ultra's
|
|
# provider 502 on reasoning.max_tokens+tools, and a later context-exhaustion failure on the
|
|
# 566k-token cookbook preload. Surface what the body actually said, so the next one is
|
|
# diagnosable from the log instead of needing a reproduction.
|
|
if 'choices' not in d:
|
|
raise RuntimeError('no choices in response: ' + json.dumps(d.get('error') or d)[:400])
|
|
return d['choices'][0]['message'], d['choices'][0].get('finish_reason')
|
|
|
|
|
|
SYS = """You are byte-matching ONE PlayStation function (MIPS, gcc-2.7.2 -O2, PsyQ SDK) in a
|
|
decompilation project. Your output must be C that compiles to instructions IDENTICAL to the target.
|
|
|
|
YOU HAVE TOOLS. Use them the way a careful engineer would:
|
|
* read_file the target .s IN FULL first. The assembly is the ONLY ground truth.
|
|
* START AT docs/cookbook-index.md -- it is symptom-keyed and it is the entry point; grep THAT
|
|
for what you see in the diff, then read the section it names in docs/matching-cookbook.md
|
|
(760+ numbered sections of hard-won gcc-2.7.2 behaviour, far too large to read whole). Search
|
|
for the pass name, the instruction pattern, the C construct. A drafter who does not grep
|
|
re-derives laws the project has held for months.
|
|
* If you cite or read a number like `12479` and grepping `§12479` finds nothing, it is a cookbook
|
|
LINE number, not a section id. Resolve it:
|
|
`.venv/bin/python tools/cookbook_index.py --resolve 12479`.
|
|
* read_file the destination TU: if it already declares a symbol, copy that declaration EXACTLY
|
|
(same type, arity, volatile, struct name). Only if absent, type by access width
|
|
(lb/lbu->s8/u8, lh/lhu->s16/u16, lw->s32, sw->s32).
|
|
* match_one as often as you like. It is free, local, and gives you a per-instruction diff.
|
|
* submit when you are done.
|
|
|
|
THE LAWS THAT MATTER MOST:
|
|
1. SPELL EVERY SYMBOL FROM YOUR TARGET. Every D_########, func_########, jtbl_######## you write
|
|
must appear in the TARGET .s's own relocation lines. Never carry a seed's or a neighbour's
|
|
per-location symbols. Curated/PsyQ names too: if the .s says `jal RotMatrixY`, call it that.
|
|
1c. match_one MASKS jal/HI16/LO16 relocations -- it verifies instruction SHAPE, NOT SYMBOL IDENTITY.
|
|
A draft that calls the WRONG function reports a clean MATCH. After MATCH, walk the .s once more
|
|
and check every symbol you wrote against its own relocation lines.
|
|
2. A seed or twin gives you the SHAPE, never the symbols, and never the literals. Re-read both.
|
|
3. IF YOUR CARD NAMES A LEVER, grep the cookbook for the § its crib cites BEFORE you draft. And a
|
|
plateaued diff whose class says [permuter] is DONE: submit NEAR with the class rather than
|
|
spending ten more compiles on a 2-instruction register swap the search cannot reach from C.
|
|
4. AN ARGUMENT YOU INVENTED CHANGES SCHEDULING (§263). If your only residual is one stolen delay
|
|
slot holding a `move $aN,$sN`, you gave a no-argument callee a parameter — check its arity
|
|
against decl_prior before touching a fence or the permuter, which cannot change call arity.
|
|
|
|
Work until match_one says MATCH, or until you have genuinely exhausted your levers -- then submit
|
|
with an honest status and say what the cookbook did NOT already tell you."""
|
|
|
|
|
|
# THE LEVER CRIB (P31 S59). The card used to state its lever as a bare label and nothing else, and
|
|
# the label is a word this project's 750-section cookbook DOES NOT CONTAIN: measured over the tells
|
|
# waves, 108 failure transcripts grepped `extend-tell` and 28 grepped `swaprepeat` for zero hits
|
|
# while the knowledge sat at §172a/§172b/§264 under different words. A label the drafter cannot look
|
|
# up is not fuel; it is a dead end that costs a grep and buys nothing. Each entry below states what
|
|
# the tell MEANS, names the § to grep, and gives the C spellings — all byte-proven (§264).
|
|
LEVER_CRIB = {
|
|
'extend-tell': (
|
|
"extend-tell = §172b-1 + §264 (grep 'sll/sra-16 pair tell'). The target promotes\n"
|
|
"REGISTER-held s16 values. Four placements, four C spellings:\n"
|
|
" * naked sll/sra pair = a MULTI-DEF s16/u16 variable merged across branches — write it\n"
|
|
" on BOTH paths mirroring the mem slot (`x[i] = w = ...`), then compare (s16)w;\n"
|
|
" * plain lh = one promoted use — read the s16 memory DIRECTLY, no temp;\n"
|
|
" * sll before a jal, sra IN its delay slot = the (s16) cast written INLINE in the call\n"
|
|
" argument (a temp kills the delay-slot fill);\n"
|
|
" * lhu + separate sll/sra = the value is ALSO used as u16 — keep the u16 variable.\n"
|
|
"Sign test (s16)t < 0 emits sll+bgez only (the sra is elided).\n"
|
|
"NO register pins first — §17 is the last resort; pins block the parm-copy and arg-copy\n"
|
|
"shapes. If a parameter is copied to $aN, type the PARAMETER and use it everywhere rather\n"
|
|
"than inventing `self = arg0`.\n"),
|
|
'swaprepeat-tell': (
|
|
"swaprepeat-tell = §172a + §172b-2 (grep 'swapped-arm select tell'). Textual REPEATS are\n"
|
|
"load-bearing: a repeated compare/select re-emits its own slt+branch, so write the repeat\n"
|
|
"TEXTUALLY — nested macros repeat their operands, inline functions collapse them.\n"
|
|
"A range check is plain `if (-lim < x && x < lim)`; the second slt rides the first beqz's\n"
|
|
"delay slot for free.\n"),
|
|
's16-div-tell': (
|
|
"s16-div-tell = §172b-4 (grep 'Division-by-constant sign-correction'). For s16/K magic\n"
|
|
"multiplies keep the promotion as an EXPRESSION, never a temp, so the sign-sra can reuse\n"
|
|
"the <<16 intermediate; the magic pairs for K are in §167-25 and the odd-part table.\n"),
|
|
}
|
|
|
|
|
|
def _fuel(t, card):
|
|
"""THE CARD FUEL, in the wave's own proven wording (P31 S56).
|
|
|
|
Until now this module handed a model only the .s path and the TU path -- i.e. it drafted BLIND to
|
|
everything the wave harness spent three sessions learning to put on a card. Wave Z's first result
|
|
was "the card is the cheapest place in the pipeline to put a fact": `seed_ref` banked 51/51 on
|
|
wave U's draw where `exemplar` banked 0/73, `tu_ref` reaches 48-64 cards a wave, and `decl_prior`
|
|
answers the arity question §195-A proved the asm cannot answer. A cheap model without that fuel
|
|
is not being compared to a wave agent -- it is being compared to a wave agent's worst day.
|
|
"""
|
|
if not card:
|
|
return ''
|
|
out = []
|
|
sr = card.get('seed_ref')
|
|
if sr:
|
|
out.append(f"\n⭐ YOUR CARD'S BANKED TWIN: {sr['binary']}:{sr['fn']} ({sr['nins']} ins"
|
|
+ (f", skeleton similarity {card['seed_sim']}" if card.get('seed_sim') else '') + ").")
|
|
out.append(f" This is MATCHED C sitting in the tree RIGHT NOW. Find it with")
|
|
out.append(f" grep(pattern='{sr['fn']}', path='src/{sr['binary']}')")
|
|
out.append( " then read_file the body in full. At similarity >=0.9 it is frequently a")
|
|
out.append( " line-for-line relative of your target: same guards, same block copies, same")
|
|
out.append( " tail. It is the highest-value 60 seconds in this task (§193-A).")
|
|
else:
|
|
# NEVER ASSERT "no twin" WITHOUT THE CHEAP ADDRESS CHECK FIRST (P31 S70).
|
|
# `seed_ref` joins on signature hashes and is blind to indexed-global relocs (§389), so a
|
|
# RELOC-ONLY twin of an already-banked body hashes differently and reads as a singleton.
|
|
# Overlays share code at the SAME VRAM, so "is this address banked in another binary?" is a
|
|
# one-line question the card never asked. MEASURED over the 130 S70 targets: 110 cards said
|
|
# "NO banked twin" and **75 of them (68%) had one at the same address in a sibling overlay** —
|
|
# one agent found its answer banked at src/ov_SC02_000/... and reported the card was simply
|
|
# wrong. That is the wave-playbook's "cross-overlay same-address grep as STEP 0", now supplied
|
|
# instead of hoped for.
|
|
same = _same_addr_banked(t.get('binary'), t.get('name'))
|
|
# SIZE-FILTER THE ADDRESS LEAD (P31 S74, §238). A same-address function in a sibling overlay
|
|
# is very often YOUR function — and about a dozen times in one session it was a DIFFERENT
|
|
# function that merely lives at the same VRAM, which the agent then had to disprove itself.
|
|
# Instruction count settles it for free: a different length is a different function, full
|
|
# stop. Same length + same mnemonic skeleton (h_seq) is the strong form.
|
|
mine_ins = t.get('nins')
|
|
sized = [x for x in same if x[1] and mine_ins and x[1] == mine_ins]
|
|
strong = [x for x in sized if x[2] and x[2] == (t.get('h_seq') or _h_seq_of(t))]
|
|
wrong = [x for x in same if x not in sized]
|
|
if sized:
|
|
best = strong or sized
|
|
out.append(f"\n⭐ NO hash-twin — BUT func {t['name']} IS BANKED AT THIS ADDRESS, at the "
|
|
f"SAME instruction count ({mine_ins}), in: "
|
|
+ ", ".join(f"{b} ({n} ins)" for b, n, _ in best[:6]) + ".")
|
|
if strong:
|
|
out.append( " Same length AND same mnemonic skeleton — this is as close to a twin")
|
|
out.append( " as a non-hash match gets.")
|
|
out.append( " Overlays share code at the same VRAM, so this is very often YOUR function")
|
|
out.append( " already matched. seed_ref is blind to it (§389 reloc-only twins hash apart).")
|
|
out.append(f" grep(pattern='{t['name']}', path='src/{best[0][0]}')")
|
|
out.append( " READ IT FIRST. Verify the symbols against your own .s (law 1c).")
|
|
if wrong:
|
|
out.append(f"\n⚠ IGNORE the same-address lead for {', '.join(b for b, _, _ in wrong[:6])}"
|
|
f" — that address holds a DIFFERENT function there "
|
|
+ ", ".join(f"({n} ins)" for _, n, _ in wrong[:6])
|
|
+ f" against your {mine_ins}. This is the §238 homonym trap; a wrong twin is")
|
|
out.append( " worse than no twin, because you would believe it. Do not read it.")
|
|
if not same:
|
|
out.append("\nThis card has NO banked twin, and no same-address banked function in any "
|
|
"other binary either — derive the structure from the .s.")
|
|
tr = card.get('tu_ref') or []
|
|
if tr:
|
|
out.append("\n⭐ AND READ THE NEIGHBOUR IN YOUR OWN TU — the highest-yield source measured")
|
|
out.append("(§194-E: 62% of targets have one). Already-banked C in the very file your draft")
|
|
out.append("lands in, sharing symbols with YOUR target's relocations:")
|
|
for r in tr:
|
|
out.append(f" {r['fn']} ({r.get('shared')} shared: {', '.join(r.get('symbols') or [])})")
|
|
out.append(" It shows this TU's house style: how it declares these exact globals and")
|
|
out.append(" callees, which struct spellings it uses. Adopting that spelling IS law 2,")
|
|
out.append(" and law 2 is the single biggest cause of bank failures.")
|
|
dp = card.get('decl_prior') or []
|
|
if dp:
|
|
out.append("\n⭐ THE DECLARATIONS ARE ALREADY DECIDED — do not guess a callee's arity or return")
|
|
out.append("type, and do not spend a compile on a two-arity A/B (§195-A: the asm has NO tell).")
|
|
out.append("Signatures print as (return, (params...)). '?' means an UNSPECIFIED parameter list,")
|
|
out.append("which is compatible with any prototype in C89 and is NEVER a conflict — do not")
|
|
out.append("'fix' it. A row with a `tu` field is AUTHORITATIVE for you: copy it verbatim.")
|
|
for r in dp[:24]:
|
|
bits = [f"sym={r['sym']}"]
|
|
for k in ('tu', 'def', 'fleet'):
|
|
if r.get(k):
|
|
bits.append(f"{k}={r[k]}")
|
|
if r.get('n'):
|
|
bits.append(f"n={r['n']}")
|
|
if r.get('rivals'):
|
|
bits.append(f"rivals={'; '.join(r['rivals'][:2])}")
|
|
if r.get('fleet_note'):
|
|
bits.append("⚠ FUNCTION POINTER — this rendering is NOT valid C; write the spelling "
|
|
"yourself from the .s, do not paste it")
|
|
if r.get('def_absent'):
|
|
bits.append(f"(no def: {r['def_absent']})")
|
|
out.append(' ' + ' '.join(bits))
|
|
sb = card.get('sibs') or []
|
|
if sb:
|
|
worth = t['nins'] + sum(x['n'] for x in sb)
|
|
out.append(f"\n⭐ THIS CARD IS WORTH {worth} INSTRUCTIONS, NOT {t['nins']}: {len(sb)} other still-open")
|
|
out.append(f"function(s) share your skeleton ({', '.join(x['b'] + ':' + x['f'] for x in sb)}) and are")
|
|
out.append( "remapped from your draft MECHANICALLY once it banks. So law 1 matters twice as much:")
|
|
out.append( "spell YOUR target's symbols exactly — no seed leftovers.")
|
|
out.append( "⚠ Do NOT hunt in `sibs` for a body to read: they are drawn from the OPEN set by")
|
|
out.append( "construction, so they are stubs 100% of the time. They tell you what your work is")
|
|
out.append( "WORTH, not where an answer is.")
|
|
# 6.3 TELL COUNTS (P31 S60). Counts derived from the TARGET's own bytes by atlas_features,
|
|
# joined onto the card by build_wave_atlas. They are a CHECKLIST, not a hint: the target
|
|
# provably contains this many sites, so a draft that produces fewer has missed some.
|
|
_t = card.get('tells') or {}
|
|
_bits = []
|
|
if _t.get('extpair'):
|
|
_bits.append(f"{_t['extpair']} sign-extend pair site(s) (sll/sra, §172b EXTPAIR)")
|
|
if _t.get('dupselect'):
|
|
_bits.append(f"{_t['dupselect']} repeated two-arm select(s) (§172b SELECT)")
|
|
if _t.get('magic_div'):
|
|
_bits.append(f"{_t['magic_div']} magic-division site(s)")
|
|
if _t.get('sign_lh'):
|
|
_bits.append("signed halfword load(s) (lh, not lhu)")
|
|
if _t.get('sign_lb'):
|
|
_bits.append("signed byte load(s) (lb, not lbu)")
|
|
if _bits:
|
|
out.append("\nTELLS COUNTED IN THE TARGET (from its bytes, not a guess) — expect: "
|
|
+ "; ".join(_bits) + ".")
|
|
out.append("If your draft emits fewer of these than the count says, you have missed "
|
|
"sites — find them before spending a turn on anything else.")
|
|
if card.get('lever'):
|
|
out.append(f"\nThe atlas labels this card's lever: {card['lever']}.")
|
|
crib = LEVER_CRIB.get(card['lever'])
|
|
if crib:
|
|
out.append(crib)
|
|
out.append("Universal fold-blockers when a shape refuses: name the inner term of a "
|
|
"nested +/- as a temp (keeps association); hold a non-slti constant bound "
|
|
"in a LOCAL assigned right before its `if` (defeats the 0xFFFF "
|
|
"canonicalization and lets the arm's store reuse its register); hold a "
|
|
"global base in a pointer local when it must live in an $sN across calls.")
|
|
return '\n'.join(out)
|
|
|
|
|
|
def prior_transcript(t, budget=240000):
|
|
"""The most recent prior CONVERSATION for this function, trimmed to a character budget.
|
|
|
|
Replayed as real assistant/tool turns rather than summarized: the tool results carry the .s
|
|
excerpts, cookbook sections and match_one diffs the last attempt paid to fetch, and a summary
|
|
of "it read the cookbook" is worth far less than the section it actually read. The opening
|
|
user turn is dropped (the new run builds its own, with fresher cards) and the oldest middle
|
|
turns are trimmed first, keeping the most recent exchanges where the reasoning converged.
|
|
"""
|
|
newest, path = None, None
|
|
for p in sorted(glob.glob(f".run/wave_*/shard*/{t['name']}.messages.json"),
|
|
key=lambda x: -os.path.getmtime(x)):
|
|
try:
|
|
newest = json.load(open(p)); path = p; break
|
|
except (OSError, json.JSONDecodeError):
|
|
continue
|
|
if not newest:
|
|
return None, None, None
|
|
msgs = [m for m in newest.get('messages', []) if m.get('role') != 'system'][1:]
|
|
while msgs and sum(len(json.dumps(m)) for m in msgs) > budget:
|
|
del msgs[0]
|
|
while msgs and msgs[0].get('role') == 'tool': # never orphan a tool result
|
|
del msgs[0]
|
|
return msgs, path, newest
|
|
|
|
|
|
def gate_feedback(t):
|
|
"""WHY the last attempt did not bank, from the gate's own ledgers — added S61 once the verdicts
|
|
became class-accurate (before that the dominant reasons were misattributed harness faults, and
|
|
feeding them to a retry would have taught it wrong). Two sources, both keyed by (binary, fn) --
|
|
never bare name (overlays share names):
|
|
* the latest backlog row -> verdict class + closeness + where_stuck;
|
|
* the latest reloc_rejects row with shape MATCH -> the EXACT symbol mismatches (mechanically
|
|
fixable: the instruction stream already matched; only names were wrong).
|
|
Harness-fault classes (BASELINE-RED / TU-BROKEN) are stated as NOT the draft's fault so the
|
|
retry resubmits with minimal change instead of mangling a possibly-correct body."""
|
|
b, fn = t.get('binary'), t['name']
|
|
out = []
|
|
try:
|
|
rows = []
|
|
for src in ('.run/backlog.jsonl', '.run/auto/bulk/%s.backlog.jsonl' % b):
|
|
if not os.path.exists(src):
|
|
continue
|
|
for line in open(src, errors='replace'):
|
|
try:
|
|
r = json.loads(line)
|
|
except Exception:
|
|
continue
|
|
if r.get('name') == fn and r.get('binary') == b:
|
|
rows.append(r)
|
|
# NEWEST BY TIME, NOT BY FILE ORDER. The loop used to keep the last matching line of the
|
|
# last file, so the per-binary bulk ledger always won regardless of age -- 42 pairs were
|
|
# served a verdict older than one sitting in .run/backlog.jsonl.
|
|
last = max(rows, key=lambda r: str(r.get('ts') or ''), default=None)
|
|
# ...BUT RECENCY IS NOT AUTHORITY, AND A SORT MUST NOT DESTROY A MEASUREMENT (P31 S71).
|
|
# A pair's rows are not a progression: one draft accumulates rows from DIFFERENT PROBES
|
|
# (sweep-parallel, S67-cc1, resolver, t6-recover, S70-standalone) that alternate between
|
|
# `closeness 4` and `won't compile standalone`, minute by minute. Sorting by ts alone hands
|
|
# the agent whichever probe happened to run last, which is often the least informative one --
|
|
# the first draft of this fix did exactly that and its own adversarial review refused it
|
|
# (it moved 159 BASELINE-RED->REAL pairs while silently discarding real residuals). So keep
|
|
# the newest verdict AND carry the best measurement anyone ever took, side by side.
|
|
measured = [r for r in rows if isinstance(r.get('closeness'), (int, float))]
|
|
best_m = min(measured, key=lambda r: r['closeness']) if measured else None
|
|
# A BASELINE-RED VERDICT IS A FACT ABOUT THE BINARY AT A MOMENT, NEVER ABOUT THE DRAFT (R51).
|
|
# It was frozen into an append-only ledger and replayed as present tense forever: 2,676 rows,
|
|
# every one stamped 2026-08-26, served to 174 (binary, fn) pairs -- 4 of them inside the S70
|
|
# packs -- for binaries that are byte-identical today. Read the SAME live red union gate_stage
|
|
# consults (gate_stage.py:274-278), so a pack and the next gate run cannot disagree.
|
|
red = set()
|
|
for rf in ('.run/baseline_red.txt', '.run/fleet_red.txt'):
|
|
try:
|
|
red |= {ln.strip() for ln in open(rf) if ln.strip()}
|
|
except OSError:
|
|
pass
|
|
if last:
|
|
ws = (last.get('where_stuck') or '')[:260]
|
|
cl = last.get('closeness')
|
|
harness = 'BASELINE-RED' in ws or 'TU-BROKEN' in ws
|
|
if harness and b in red:
|
|
out.append("LAST GATE VERDICT: the rejection was the HARNESS's fault (%s) -- the binary or its "
|
|
"TU was broken when your predecessor was judged. The draft may be fully correct: "
|
|
"verify against the .s and resubmit with MINIMAL changes." % ws)
|
|
elif harness:
|
|
out.append("LAST GATE VERDICT: the stored verdict (%s, %s) is EXPIRED -- %s is not on the "
|
|
"current red list, so that claim is about a binary state that no longer exists. "
|
|
"Treat it as no verdict at all."
|
|
% (ws[:120], last.get('ts') or 'undated', b))
|
|
else:
|
|
out.append('LAST GATE VERDICT: %s%s' % (('closeness %s (instructions still wrong) -- ' % cl)
|
|
if cl is not None else '', ws))
|
|
# The best measurement ever taken on this pair, whenever it was taken. Emitted only when
|
|
# the newest row is not itself that measurement, so nothing is repeated.
|
|
if best_m is not None and best_m is not last:
|
|
out.append('BEST MEASURED RESIDUAL (%s, %s): closeness %s -- %s'
|
|
% (best_m.get('source') or 'unknown probe', best_m.get('ts') or 'undated',
|
|
best_m['closeness'], (best_m.get('where_stuck') or '')[:180]))
|
|
except Exception:
|
|
pass
|
|
try:
|
|
best = None
|
|
if os.path.exists('.run/reloc_rejects.jsonl'):
|
|
for line in open('.run/reloc_rejects.jsonl', errors='replace'):
|
|
try:
|
|
r = json.loads(line)
|
|
except Exception:
|
|
continue
|
|
if r.get('fn') == fn and r.get('binary') == b and r.get('shape') == 'MATCH':
|
|
best = r
|
|
if best and best.get('mismatches'):
|
|
ms = best['mismatches'][:6]
|
|
# THE BLOCK MUST BE PROVABLY ABOUT *THIS* (binary, fn) OR IT IS NOT SERVED (P31 S70).
|
|
# This read side keys the row by (binary, fn) -- but ox_campaign.reloc_filter STAMPS that
|
|
# binary from `binof = {c["fn"]: c["binary"]}`, a BARE-NAME dict (R48 violation, still
|
|
# unfixed upstream): wave `el` carried 44 names in >=2 binaries, so func_8017D918's row
|
|
# was stamped ov_SC06_020 while the draft it checked was ov_SC01_074's. A read-side key
|
|
# is only as good as the write-side stamp, so validate the row against the TARGET'S OWN
|
|
# bytes before quoting it. MEASURED over .run/reloc_rejects.jsonl (14,071 rows -> 182
|
|
# servable (binary, fn)):
|
|
# * 149 of 182 carry aligned=False / status "MISMATCH?" -- reloc_identity's own caveat
|
|
# on those rows reads "streams are not index-aligned -- verdicts are ADVISORY; fix
|
|
# the shape first", yet the text below asserted the exact opposite ("your instruction
|
|
# stream already matched; ONLY these names were wrong"). 140 of those 149 print at
|
|
# least one "the target references 0x..." that is not an address at all (0x000F2840,
|
|
# 0x287F8019) -- arithmetic proof that index i was read off the wrong stream.
|
|
# * ALL 15 of the 15 S70 packs carrying this block were aligned=False, and 55 of their
|
|
# 66 printed lines (83%) named a non-address. For the 4 whose .s is on disk, 4/4
|
|
# named symbols the target .s never relocates -- precisely what the
|
|
# ov_SC02_035:func_8017D3F4 and ov_SC06_020:func_8017D918 agents reported as
|
|
# "cross-overlay contamination ... ignore it".
|
|
# R39 NEGATIVE CONTROL over every block that can still reach an agent (the 14 whose
|
|
# function is still an open stub): 12 withheld, every one aligned=False; the 2 aligned
|
|
# survivors (ov_SC03_029:func_80186A34, ov_SC07_011:func_8016AB6C) pass every check --
|
|
# all 6 of their target addresses ARE relocated by their own .s, and both are still
|
|
# served verbatim. Zero false positives. Whole index: 145 withheld not-aligned, 1
|
|
# withheld non-address, 30 served hedged (aligned but the .s is not extracted right
|
|
# now, so the block never CLAIMS validation), 2 served validated, 4 unchanged.
|
|
# R33: `aligned` is reloc_identity's own invariant, not a new scanner. R32: withheld
|
|
# LOUDLY, in the pack and on stdout, never silently dropped.
|
|
_asm = t.get('asm')
|
|
try:
|
|
_s = open(os.path.join(REPO, _asm), errors='replace').read() if _asm else None
|
|
except Exception:
|
|
_s = None
|
|
_why = None
|
|
if not best.get('aligned'):
|
|
_why = ('reloc_identity marked it ADVISORY (aligned=false, status %s): the draft and '
|
|
'target relocation streams are not index-aligned, so every per-index claim '
|
|
'in it is void' % best.get('status'))
|
|
else:
|
|
for m in ms:
|
|
try:
|
|
_a = int(str(m.get('target_addr')), 16)
|
|
except Exception:
|
|
_why = 'it prints an unparsable target address %r' % (m.get('target_addr'),)
|
|
break
|
|
if not 0x80000000 <= _a < 0x80800000:
|
|
_why = ('it claims the target references %s, which is not an address -- its '
|
|
'index alignment is wrong' % m.get('target_addr'))
|
|
break
|
|
if _s is not None and ('func_%08X' % _a) not in _s and ('D_%08X' % _a) not in _s \
|
|
and (m.get('target_name') or '\0') not in _s:
|
|
_why = ('it claims the target references %s, which %s does NOT relocate -- '
|
|
'the row was computed against a different overlay' % (m.get('target_addr'), _asm))
|
|
break
|
|
if _why:
|
|
out.append('SYMBOL MISMATCH FEEDBACK WITHHELD for %s:%s -- %s. Treat the previous '
|
|
'attempt as wrong in SHAPE, not merely in naming, and derive every symbol '
|
|
'from %s yourself.' % (b, fn, _why, _asm or 'the target .s'))
|
|
print(' %s:%s: SYMBOL MISMATCHES block withheld -- %s' % (b, fn, _why), flush=True)
|
|
else:
|
|
_prov = ('validated against %s: every address below IS relocated by this target'
|
|
% _asm) if _s is not None else \
|
|
('NOT validated -- %s is not on disk, so confirm each address against the asm '
|
|
'before acting on it' % (_asm or 'the target .s'))
|
|
out.append('SYMBOL MISMATCHES from the last shape-MATCH attempt (%s). The instruction '
|
|
'stream already matched; ONLY these names were wrong -- fix the names, '
|
|
'change nothing else:\n' % _prov +
|
|
'\n'.join(' i=%s %s: draft named %s but the target references %s%s'
|
|
% (m.get('i'), m.get('kind'), m.get('draft_symbol'), m.get('target_addr'),
|
|
(' = ' + m['target_name']) if m.get('target_name') else '')
|
|
for m in ms))
|
|
except Exception:
|
|
pass
|
|
return ('\n\n'.join(out)) if out else None
|
|
|
|
|
|
_SAME_ADDR = None
|
|
|
|
|
|
def _h_seq_of(t):
|
|
"""The target's own mnemonic-skeleton hash, or None. Used only to STRENGTHEN a size-matched
|
|
address lead — never to withhold one."""
|
|
try:
|
|
import corpus
|
|
row = corpus.sig(t.get('binary')).get(int(t['name'][len('func_'):], 16))
|
|
return (row or {}).get('h_seq')
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _same_addr_banked(binary, fn):
|
|
"""[(binary, nins, h_seq)] where `fn`'s ADDRESS is already BANKED (real C), excluding `binary`.
|
|
|
|
THE SIZE AND SKELETON TRAVEL WITH THE LEAD (P31 S74). Overlays share code at the same VRAM, but
|
|
they also share ADDRESSES between functions that have nothing to do with each other — the §238
|
|
homonym. Measured over this session's ~60 cards: about a dozen agents were handed a
|
|
same-address "twin" that was a different function and had to discover it themselves, and one
|
|
card carried a journal history claiming "already MATCH, 72 ins" for a 241-instruction target,
|
|
which is worse than no lead at all. The caller now filters on instruction count, so the trap is
|
|
sprung by the tool instead of by the agent.
|
|
|
|
Derived from the corpus invariant (R33): a function is banked iff it is in the binary's sig and
|
|
NOT an INCLUDE_ASM stub. Built once per process (~30 s over 213 binaries) and memoized.
|
|
Returns [] on any failure — this only ever ADDS fuel, so a miss degrades to today's behaviour
|
|
rather than withholding a card."""
|
|
global _SAME_ADDR
|
|
m = re.match(r'(?:func|jr)_([0-9A-Fa-f]{8})$', fn or '')
|
|
if not m:
|
|
return []
|
|
if _SAME_ADDR is None:
|
|
_SAME_ADDR = {}
|
|
try:
|
|
import corpus
|
|
import subprocess as _sp
|
|
names = _sp.run(['make', '-f', '.run/S70_printbins.mk', '-s', 'print-binaries'],
|
|
capture_output=True, text=True, cwd=REPO).stdout.split()
|
|
if not names:
|
|
names = sorted(os.path.basename(p) for p in glob.glob(os.path.join(REPO, 'src', '*'))
|
|
if os.path.isdir(p))
|
|
for b in names:
|
|
try:
|
|
st = set(corpus.stubs(b))
|
|
for a, row in corpus.sig(b).items():
|
|
if int(a) not in st:
|
|
_SAME_ADDR.setdefault(int(a), []).append(
|
|
(b, row.get("nins"), row.get("h_seq")))
|
|
except Exception:
|
|
continue
|
|
except Exception:
|
|
_SAME_ADDR = {}
|
|
return [t for t in _SAME_ADDR.get(int(m.group(1), 16), []) if t[0] != binary]
|
|
|
|
|
|
def prior_draft(t):
|
|
"""The best body a PREVIOUS attempt on this function reached, if one is on disk.
|
|
|
|
A retry currently starts cold and re-derives everything the last attempt already worked out.
|
|
That is pure waste at any time and acute now: --retry-unbanked returned 1,763 drawn-but-unbanked
|
|
cards to the pool (P31 S58), every one of which has a saved draft. It is also the only recovery
|
|
path for work lost to transport — one shard reached "near 19" on func_801833E8 and a 502 ended
|
|
the run; the conversation is gone (nothing persists it) but the body survived.
|
|
|
|
Deliberately NOT presented as correct: a saved draft is by definition one the byte-gate has not
|
|
accepted, so it is offered as a starting point to improve or discard, never as a seed to trust.
|
|
"""
|
|
best, where = None, None
|
|
# LAW 1c FOR WARM STARTS (P31 S62 T4): drafts are stored by function NAME, and the same name at
|
|
# the same address in a DIFFERENT overlay is usually a different function. 22 of 60 probe agents
|
|
# reported "the warm-start body was a different function entirely" — a strong model discards
|
|
# it, a weak one follows it, so the warm start was confounding the arms. Accept a candidate
|
|
# only when the symbols it references overlap the target .s's own relocations.
|
|
try:
|
|
tgt = set(re.findall(r'\b(?:func_|D_|jtbl_|a[A-Z0-9]+_)[0-9A-Fa-f]{8}\b', open(os.path.join(REPO, t['asm'])).read()))
|
|
except Exception:
|
|
tgt = None
|
|
skipped = 0
|
|
for pat in (f".run/wave_*/shard*/{t['name']}.c", f".run/*/{t['name']}.c"):
|
|
for p in sorted(glob.glob(pat), key=lambda x: -os.path.getmtime(x)):
|
|
try:
|
|
body = open(p).read()
|
|
except OSError:
|
|
continue
|
|
if not body.strip():
|
|
continue
|
|
if tgt is not None:
|
|
syms = set(re.findall(r'\b(?:func_|D_|jtbl_|a[A-Z0-9]+_)[0-9A-Fa-f]{8}\b', body)) - {t['name']}
|
|
# TIGHTENED (P31 S70). The old test was `len(syms) >= 2 and overlap*2 < len(syms)`
|
|
# and had TWO holes, both measured live in the S70 wave:
|
|
# * `len(syms) >= 2` exempted every body referencing 0 or 1 symbols — exactly the
|
|
# small-function case. func_80182438 (21 ins, ONE symbol) sailed through carrying
|
|
# ov_SC02_028's body for the same address, and its agent reported that as the
|
|
# reason its PRIOR attempt failed.
|
|
# * needing a strict majority foreign meant a body sharing half its symbols passed.
|
|
# A correct draft can only reference what the target's .s actually relocates, so ANY
|
|
# foreign symbol disqualifies. Negative-controlled over all 50 S70 targets: admits 42,
|
|
# rejects 4 — and the 4 are precisely the bodies four independent agents reported as
|
|
# "a different function entirely" (func_80182438, func_800D0664, func_801831D0,
|
|
# func_80185F4C). No collateral rejections. R48: never key by bare function name.
|
|
if syms - tgt:
|
|
skipped += 1
|
|
continue # another overlay's same-named function — not this body
|
|
best, where = body, p
|
|
break
|
|
if best:
|
|
break
|
|
if skipped:
|
|
print(f" {t['name']}: skipped {skipped} same-named draft(s) whose symbols do not match this .s (law 1c)", flush=True)
|
|
return best, where
|
|
|
|
|
|
def user_msg(t, card=None):
|
|
base = f"""TARGET: {t['name']} · {t['nins']} instructions · binary {t.get('binary','?')}
|
|
target asm : {t['asm']}
|
|
destination TU : {t.get('tu','(unknown)')}
|
|
{_fuel(t, card)}
|
|
|
|
Start by reading the asm in full. Then work. Call submit when done."""
|
|
if not COOKBOOK_FULL:
|
|
return base
|
|
# PRELOAD THE WHOLE COOKBOOK (Drew, P31 S56). Only worth doing on a model whose context can
|
|
# hold it -- ~568k tokens of the ~22,000-line knowledge base -- e.g. nemotron-3-ultra's 1M
|
|
# window. The question this asks is a real one: the Claude agent's edge came from GREPPING the
|
|
# index selectively (16 tool calls, ~60k tokens total). Does having everything resident from
|
|
# turn 0 beat searching on demand, or does it bury the two sections that matter in 22,000 lines
|
|
# of everything else? Costs ~568k input tokens on EVERY turn, so use it only where input is free.
|
|
book = open(os.path.join(REPO, 'docs', 'matching-cookbook.md'), errors='replace').read()
|
|
return (base + "\n\nThe ENTIRE matching cookbook follows — every idiom this project has "
|
|
"learned. You still have the grep tool, but you should not need it for the cookbook: "
|
|
"read what is relevant below. The .s is still the only ground truth.\n\n"
|
|
"===== docs/matching-cookbook.md =====\n" + book)
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument('--targets', required=True)
|
|
ap.add_argument('--out', required=True)
|
|
ap.add_argument('--max-turns', type=int, default=24)
|
|
ap.add_argument('--brief', help='path to a task brief prepended to the first user message. '
|
|
'Lets api_agent run ANALYSIS tasks (e.g. an autopsy: "classify why this does '
|
|
'not remap") rather than only "byte-match this function", with the same tools '
|
|
'and the same oracle available as evidence.')
|
|
ap.add_argument('--max-cost', type=float, default=0.0,
|
|
help='TOTAL USD ceiling for this run (OpenRouter usage.cost, summed across every '
|
|
'call). 0 = no ceiling. Borrowed from OpenRouter Agent SDK\'s maxCost stop '
|
|
'condition, which is a better bound than a turn cap: a turn is not a unit '
|
|
'of money, and the runs that hurt are the ones that grind many cheap turns.')
|
|
ap.add_argument('--max-cost-per-fn', type=float, default=0.0,
|
|
help='USD ceiling for ONE function before moving on. 0 = no ceiling. This is the '
|
|
'per-item bound the batch economics actually need: GLM burned $1.10 on a '
|
|
'single rung-3 failure -- 5x its successful match -- and a turn cap could '
|
|
'not have stopped it.')
|
|
ap.add_argument('--cards', help='wave card file (e.g. .run/wave_aa_cards.json). Injects the '
|
|
'card fuel a wave agent gets: seed_ref / tu_ref / decl_prior / sibs / lever. '
|
|
'Without it the model drafts BLIND to everything the wave harness knows.')
|
|
a = ap.parse_args()
|
|
targets = json.load(open(a.targets))
|
|
cards = {}
|
|
if a.cards:
|
|
raw = json.load(open(a.cards))
|
|
_rows = raw if isinstance(raw, list) else raw.get('cards', [])
|
|
# A CARD FILE OF THE WRONG SHAPE MUST REFUSE, NOT CRASH (R43, P31 S60). This was
|
|
# `{c['fn']: c for c in _rows}` and died with KeyError: 'fn' when idiom_serial passed
|
|
# .run/aprop_cards.json — a FAMILY-card file (family/members/seed, no per-function key).
|
|
# The traceback killed the agent at turn 0, so every serial-lane target logged "no-draft"
|
|
# and the lane read as a model failure. The R32 warning two lines below already existed to
|
|
# catch exactly this ("ZERO matched — wrong card file?") and was unreachable behind the
|
|
# crash. Accept 'fn' or 'name', skip rows with neither, and let that warning do its job.
|
|
cards = {}
|
|
_skipped = 0
|
|
for c in _rows:
|
|
k = c.get('fn') or c.get('name') if isinstance(c, dict) else None
|
|
if k:
|
|
cards[k] = c
|
|
else:
|
|
_skipped += 1
|
|
if _skipped:
|
|
print(f'card fuel: {_skipped} row(s) in {a.cards} carry no fn/name key — '
|
|
f'is this a per-function card file?')
|
|
hit = sum(1 for t in targets if t['name'] in cards)
|
|
# R32: say what fraction of the batch actually got fuel. A card file that silently matches
|
|
# nothing would look exactly like a model that ignored it.
|
|
print(f'card fuel: {hit}/{len(targets)} targets matched in {a.cards}'
|
|
+ ('' if hit else ' ⚠ ZERO matched — wrong card file?'))
|
|
outdir = os.path.join(REPO, a.out)
|
|
os.makedirs(outdir, exist_ok=True)
|
|
print(f'api_agent: {AD.MODEL} @ {AD.API_BASE} -> {len(targets)} target(s), max {a.max_turns} turns')
|
|
|
|
stop_all = False
|
|
for t in targets:
|
|
if stop_all:
|
|
print(f' {t["name"]}: SKIPPED — run cost ceiling already reached')
|
|
continue
|
|
state = {'best': (None, 10 ** 9), 'calls': 0, 'submitted': None, 'nudges': 0,
|
|
'recent': []}
|
|
um = user_msg(t, cards.get(t['name']))
|
|
pd, pdwhere = prior_draft(t)
|
|
if pd:
|
|
um += ("\n\nA PREVIOUS ATTEMPT on this exact function left this body behind "
|
|
f"({pdwhere}). The whole-binary byte-gate did NOT accept it, so it is wrong "
|
|
"somewhere — but it is usually wrong in ONE place, and re-deriving the other "
|
|
"90% from scratch wastes the turns you need for the real difference. Read it "
|
|
"against the .s, keep what matches, and fix what does not. Discard it entirely "
|
|
"if it is a different function's body.\n\n```c\n" + pd.strip() + "\n```\n")
|
|
print(f' {t["name"]}: warm start from {pdwhere}', flush=True)
|
|
gf = gate_feedback(t)
|
|
if gf:
|
|
um += '\n\n' + gf + '\n'
|
|
print(f' {t["name"]}: gate feedback attached', flush=True)
|
|
if a.brief:
|
|
um = open(a.brief).read().rstrip() + '\n\n' + um
|
|
messages = [{'role': 'system', 'content': SYS}, {'role': 'user', 'content': um}]
|
|
# RESUME rather than restart: splice the previous attempt's turns in after the new opening
|
|
# message, then tell the model plainly what it is looking at. Its own prior tool results —
|
|
# the .s excerpts, the cookbook sections, the match_one diffs — come back with it, so the
|
|
# retry begins where the last one stopped instead of re-buying the same context.
|
|
prev, prevpath, prevmeta = prior_transcript(t)
|
|
if prev:
|
|
messages += prev
|
|
messages.append({'role': 'user', 'content':
|
|
f"RESUMING. Everything above is YOUR OWN previous attempt on {t['name']}, replayed "
|
|
f"from {os.path.basename(prevpath)}. It ended at "
|
|
f"{prevmeta.get('verdict','an unknown verdict')} after "
|
|
f"{prevmeta.get('oracle_calls','?')} oracle call(s) — so it did NOT bank, and "
|
|
"somewhere in there is a wrong assumption. Do not repeat the searches and reads you "
|
|
"already did; their results are above. Start from the best body you reached, decide "
|
|
"what is still unexplained in the .s, and go at THAT. If the previous line of attack "
|
|
"was a dead end, say so and take a different one."})
|
|
print(f' {t["name"]}: RESUMING from {prevpath} '
|
|
f'({len(prev)} prior turns, ended {prevmeta.get("verdict")})', flush=True)
|
|
spent_at_start = AD._COST[0]
|
|
for turn in range(a.max_turns):
|
|
spent_fn = AD._COST[0] - spent_at_start
|
|
if a.max_cost_per_fn and spent_fn >= a.max_cost_per_fn:
|
|
print(f' {t["name"]} turn {turn}: PER-FN COST CEILING '
|
|
f'${spent_fn:.4f} >= ${a.max_cost_per_fn:.4f} — moving on', flush=True)
|
|
break
|
|
if a.max_cost and AD._COST[0] >= a.max_cost:
|
|
print(f' {t["name"]} turn {turn}: RUN COST CEILING '
|
|
f'${AD._COST[0]:.4f} >= ${a.max_cost:.4f} — stopping the whole run', flush=True)
|
|
stop_all = True
|
|
break
|
|
try:
|
|
msg, finish = call(messages, TOOLS)
|
|
except Exception as e: # noqa: BLE001 - report, never crash the run
|
|
print(f' {t["name"]} turn {turn}: API error {type(e).__name__} {str(e)[:120]}')
|
|
break
|
|
tcs = msg.get('tool_calls') or []
|
|
messages.append({k: v for k, v in msg.items()
|
|
if k in ('role', 'content', 'tool_calls')} or {'role': 'assistant', 'content': ''})
|
|
if not tcs:
|
|
# A TURN WITH NO TOOL CALL IS NOT NECESSARILY THE END (P31 S56). The first version
|
|
# broke here, and that silently ended two runs for opposite reasons:
|
|
# * deepseek-v4-flash rung 1 came back finish='length' -- TRUNCATED at the output
|
|
# cap mid-reply -- on the turn AFTER it had just read the two cookbook sections
|
|
# that bear on its residual (§164-20, §205), with 27 of 40 turns unspent and a
|
|
# score still improving (74 -> 73 -> 70). Reported as "gave up". It did not.
|
|
# * kimi-k3 came back finish='stop' with a plateaued diff -- a real surrender.
|
|
# Both deserve a nudge with their own best draft back in hand; only a model that
|
|
# keeps declining after NUDGE_MAX of them has actually finished.
|
|
code = AD.extract_code(msg.get('content') or '')
|
|
if code.strip():
|
|
print(f' {t["name"]} turn {turn}: replied with code and no tool call — scoring it')
|
|
print(' ', run_tool('submit', {'code': code, 'status': 'UNKNOWN'}, t, outdir, state).splitlines()[0])
|
|
break
|
|
state['nudges'] += 1
|
|
if state['nudges'] > NUDGE_MAX:
|
|
print(f' {t["name"]} turn {turn}: no tool call (finish={finish}); '
|
|
f'{NUDGE_MAX} nudges spent — stopping')
|
|
break
|
|
best_code, best_score = state['best']
|
|
where = ('MATCH' if best_score == 0 else
|
|
f'near {best_score}' if best_score < 10 ** 7 else 'no compiling draft yet')
|
|
why = ('Your last reply was CUT OFF at the output limit before you finished — that is '
|
|
'a budget accident, not a dead end. Reply again, SHORTER: think less, call one '
|
|
'tool.' if finish == 'length' else
|
|
'You stopped without calling a tool. You are not done unless match_one says '
|
|
'MATCH.')
|
|
back = (f'{why}\n\nYour best draft so far scores {where}. It is below — continue '
|
|
f'from it. You have {NUDGE_MAX - state["nudges"] + 1} continuation(s) left and '
|
|
f'{a.max_turns - turn - 1} turn(s). Call match_one with an improved version, '
|
|
f'grep the cookbook for the residual you are stuck on, or call submit if you '
|
|
f'genuinely have no lever left.\n\n```c\n{(best_code or "(none)")}\n```')
|
|
print(f' {t["name"]} turn {turn}: no tool call (finish={finish}) — '
|
|
f'NUDGE {state["nudges"]}/{NUDGE_MAX}, best={where}')
|
|
messages.append({'role': 'user', 'content': back})
|
|
continue
|
|
# REPEATED-CALL GUARD (P31 S56). A model can loop forever on VALID tool calls and no
|
|
# existing bound catches it: NUDGE_MAX only fires on a turn with NO tool call, and
|
|
# --max-cost is meaningless for a $0 model. Measured: stealth/ox-alpha spent 56 turns on
|
|
# func_8018568C -- 46 greps, 10 reads, ZERO compiles -- repeating the same cookbook grep
|
|
# six turns running, and produced no draft at all. Unattended, that burns a whole run.
|
|
sig = tuple(sorted((tc['function']['name'], tc['function'].get('arguments') or '')
|
|
for tc in tcs))
|
|
state['recent'].append(sig)
|
|
del state['recent'][:-REPEAT_MAX]
|
|
if len(state['recent']) == REPEAT_MAX and len(set(state['recent'])) == 1:
|
|
nm = tcs[0]['function']['name']
|
|
print(f' {t["name"]} turn {turn}: REPEATED the identical {nm} call '
|
|
f'{REPEAT_MAX}x — breaking the loop', flush=True)
|
|
messages.append({'role': 'user', 'content':
|
|
f'STOP. You have made the identical {nm} call {REPEAT_MAX} times in a row and '
|
|
f'it is not advancing you. Do something DIFFERENT now: call match_one with your '
|
|
f'best attempt at the C (even an incomplete one — the diff is information), or '
|
|
f'read the target .s again, or call submit with an honest NEAR/IMMOVABLE status '
|
|
f'and say what blocked you. Do not repeat that search.'})
|
|
state['recent'].clear()
|
|
continue
|
|
for tc in tcs:
|
|
fn = tc['function']['name']
|
|
try:
|
|
args = json.loads(tc['function'].get('arguments') or '{}')
|
|
except json.JSONDecodeError:
|
|
args = {}
|
|
try:
|
|
res = run_tool(fn, args, t, outdir, state)
|
|
except Exception as e: # noqa: BLE001 - a tool fault is DATA
|
|
# NO TOOL ERROR IS FATAL. Four runs died tonight to unhandled conditions the
|
|
# model could have worked around if it had been told: a directory read, a 502
|
|
# with no `choices`, a truncated HTTP body, a turn with no tool call. Hand the
|
|
# exception back as a tool result and let the model choose again.
|
|
res = (f'TOOL ERROR {type(e).__name__}: {str(e)[:300]}\n'
|
|
f'That call failed. Fix the arguments and try again, or use a different '
|
|
f'tool — this is not fatal and you have turns remaining.')
|
|
print(f' {t["name"]} turn {turn}: {fn} raised {type(e).__name__} '
|
|
f'— returned to the model as feedback', flush=True)
|
|
first = res.splitlines()[0] if res else ''
|
|
print(f' {t["name"]} turn {turn}: {fn}({str(args)[:70]}) -> {first[:90]}')
|
|
messages.append({'role': 'tool', 'tool_call_id': tc.get('id'), 'name': fn, 'content': res})
|
|
if state['submitted']:
|
|
break
|
|
best, score = state['best']
|
|
if best is not None:
|
|
open(os.path.join(outdir, t['name'] + '.c'), 'w').write(best if best.endswith('\n') else best + '\n')
|
|
# PERSIST THE TRANSCRIPT. Until now only the final body survived a run, so a retry restarted
|
|
# cold and re-derived every read, every grep and every rejected hypothesis. The body says
|
|
# WHAT the last attempt concluded; the transcript says WHY, and which approaches were already
|
|
# ruled out. Acute with --retry-unbanked returning 1,763 drawn-but-unbanked cards to the pool.
|
|
try:
|
|
json.dump({'fn': t['name'], 'binary': t.get('binary'), 'score': score,
|
|
'verdict': 'MATCH' if score == 0 else
|
|
(f'near {score}' if score < 10 ** 7 else 'no compiling draft'),
|
|
'oracle_calls': state['calls'], 'model': AD.MODEL, 'messages': messages},
|
|
open(os.path.join(outdir, t['name'] + '.messages.json'), 'w'))
|
|
except Exception:
|
|
pass
|
|
verdict = 'MATCH' if score == 0 else (f'near {score}' if score < 10 ** 7 else 'no compiling draft')
|
|
print(f' {t["name"]}: {verdict} after {state["calls"]} oracle call(s), '
|
|
f'${AD._COST[0] - spent_at_start:.4f}, {state["submitted"] or "no submit"}')
|
|
print(f'api_agent done. cost ${AD._COST[0]:.4f}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|