Control-flow offset-path diff: Frida engine op-tracer (recon-gated tick/0x41b940 hook) + VM ITraceSink offsets + diff_optrace.py first- divergence report. Deterministic opening (SC0000 --boot). Ready to execute in a fresh context; prereq = game running at the opening. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
Differential Oracle (control-flow) — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Pinpoint mis-modeled opcodes by diffing the real engine's offset-execution path against our VM's, on the deterministic opening — output "agreed N steps, diverge at offset X = op 0xYY".
Architecture: A Frida engine op-tracer emits per-op (codebase, offset) to build/engine-optrace.jsonl (recon-gated capture). Our VM emits the SC0000 offset sequence to build/vm-optrace.json. diff_optrace.py identifies SC0000's codebase in the engine trace, aligns the two offset sequences, and reports the first divergence. The diff/parse logic is pure (unit-tested); Frida + VM emitter are exercised live.
Tech Stack: Python 3.11 (Frida 17.x for the tracer, tomllib/json for the diff), Age.Cli (.NET 8) for the VM trace, ghidra-mcp only if we need to re-confirm a handler addr.
Spec: docs/superpowers/specs/2026-07-09-differential-oracle-design.md.
Global Constraints
- Frida scripts:
py -3.11 -u -X utf8 tools/frida/<name>.py; read-only (Interceptor + memory reads; no patching). Model ondump_engine.py/probe_frame_cadence.py(attach +send()/on(message); frida-subdir tools computeREPO = parents[2], nopaths.py). - Known-safe vs risky hooks:
vm_operand_fetch@0x41b940is proven-safe (plain-JS,probe_frame_cadence.py).adv_interpreter_tick@0x410fb0crashed with a CModule hook — only ever plain-JS here, and Task 1 gates on it. - Engine ctx offsets (now typed):
cur_ctx_index@0x53d14,frame_pc@0x53d2c,frame_codebase@0x53d28; per-context stride0x78.offset = (pc − codebase)/4. - The opening is
SC0000 --boot(deterministic).build/is disposable/gitignored. - Control-flow only (offset path); effects-diff is out of scope (phase 2).
Task 1: Engine op-tracer + recon gate
Files:
-
Create:
tools/frida/trace_engine_ops.py -
Reference:
tools/frida/probe_frame_cadence.py(the proven 0x41b940 hook + attach boilerplate) -
Step 1: Write the tracer with a selectable hook + batched flush
trace_engine_ops.py: attach to the running game; a --hook tick|operand flag (default tick). JS Interceptor.attach:
-
tickmode →adv_interpreter_tick0x410fb0;operandmode →vm_operand_fetch0x41b940. -
onEnter:
ctx=this.context.ecx(thiscallecx=ctx);curCtx = ctx.add(0x53d14).readU32();pc = ctx.add(0x53d2c + curCtx*0x78).readU32()(read via a computed pointer — recompute the address each call);codebase = ctx.add(0x53d28 + curCtx*0x78).readU32(); push{cb: codebase, off: (pc-codebase)>>2}to a JS buffer. Inoperandmode, skip ifpc === lastPc(dedupe consecutive same-pc). -
Flush the buffer to Python via
send(batch)every ~2000 entries; Python appends each{codebase, offset}tobuild/engine-optrace.jsonl. Model attach/on(message)exactly onprobe_frame_cadence.py. -
Step 2: RECON — capture the opening safely (THE GATE)
Launch the game to the opening (or attach at the title and let the opening replay). Run:
py -3.11 -u -X utf8 tools/frida/trace_engine_ops.py --hook tick
Let the opening auto-advance to the first wait-for-input, then stop. Inspect build/engine-optrace.jsonl.
-
GATE — tick hook stable + trace captured: proceed. Record entry count + number of distinct codebases.
-
GATE — tick hook destabilizes the game (crash/hang): switch to
--hook operand(proven-safe) and re-capture. Record which hook worked. -
GATE — neither yields a usable trace: STOP, document in
engine-re.md, end the slice. Expected: a jsonl with thousands of(codebase, offset)entries across a handful of codebases (boot scripts + SC0000). -
Step 3: Sanity-identify the SC0000 codebase
Quick check (inline Python): group entries by codebase; for each, does its offset-0/low-offset region's static opcode (from build/disasm/SC0000.asm via sys4load) match SC0000's opening? Report the codebase whose executed offsets are consistent with SC0000 (largest plausible run). This de-risks Task 3's codebase identification.
Expected: one codebase identifiable as SC0000.
- Step 4: Commit the tracer
git add tools/frida/trace_engine_ops.py
git commit -m "re(frida): engine op-path tracer (tick/operand hook, per-script offsets)"
Task 2: VM offset-trace emitter
Files:
-
Modify (if needed):
Age.Clitrace command (a--trace-json <path>that dumps executed SC0000 offsets), or reuse an existing offset-trace artifact. -
Test:
engine/Age.Engine.Tests/(a small test for the JSON emitter if one is added). -
Step 1: Check for an existing full offset trace
Inspect how Age.Cli trace --trace / the vm0-trace.json selftest artifact are produced (grep Age.Cli, ITraceSink, vm0-trace). If a full ordered-offset JSON for a booted scene already exists or is trivially emittable, use it and skip to Step 3.
Expected: a decision — reuse vs add a small emitter.
- Step 2: Add a minimal
--trace-jsonemitter (if needed)
Add a JsonOffsetTraceSink : ITraceSink (or a post-run collector on the existing sink) that records each Step's script-relative offset in order, and a --trace-json <path> option on the trace command writing {"scene":"SC0000","offsets":[...]}. Keep it observe-only (parity preserved — non-JSON runs unaffected). Add one xUnit test: running a tiny synthetic scene yields the expected offset list. dotnet test green.
- Step 3: Produce the VM trace for SC0000 (booted, to match the engine)
Run: dotnet run --project engine/Age.Cli -- trace SC0000.BIN --boot --trace-json build/vm-optrace.json (exact invocation per the CLI). Confirm the offset count is on the order of the engine trace's SC0000 run.
Expected: build/vm-optrace.json with the ordered SC0000 offsets.
- Step 4: Commit (if a C# change was made)
git add engine/
git commit -m "feat(engine): --trace-json emitter for the differential oracle"
Task 3: diff_optrace.py — align + first-divergence
Files:
- Create:
tools/diff_optrace.py - Create:
tools/test_diff_optrace.py
Interfaces:
-
first_divergence(a: list[int], b: list[int]) -> dict→{"agreed": n, "index": i, "a": a[i]|None, "b": b[i]|None}or{"agreed": len, "index": None}if one is a prefix of the other / equal. -
pick_scene_codebase(entries: list[dict], vm_offsets: list[int]) -> int|None→ the codebase whose offset sequence shares the longest common prefix withvm_offsets(identifies SC0000's instance). -
Step 1: Write failing tests
# tools/test_diff_optrace.py (plain runner)
import sys
from diff_optrace import first_divergence, pick_scene_codebase
FAILS=[]
def check(c,m): (FAILS.append(m) or print("FAIL:",m)) if not c else print("ok:",m)
def test_equal_no_divergence():
r = first_divergence([0,1,2,3],[0,1,2,3])
check(r["index"] is None and r["agreed"]==4, "equal traces -> no divergence")
def test_first_divergence_point():
r = first_divergence([0,1,2,9],[0,1,2,3])
check(r["index"]==3 and r["a"]==9 and r["b"]==3, "divergence at first differing offset")
def test_prefix_shorter_vm():
r = first_divergence([0,1,2,3],[0,1]) # vm ends early
check(r["index"]==2 and r["b"] is None and r["agreed"]==2, "shorter VM trace flagged at end")
def test_pick_codebase_by_longest_common_prefix():
entries=[{"codebase":100,"offset":0},{"codebase":100,"offset":5}, # cb100: [0,5,...]
{"codebase":200,"offset":0},{"codebase":200,"offset":1},{"codebase":200,"offset":2}]
check(pick_scene_codebase(entries,[0,1,2])==200, "codebase matching VM prefix chosen")
def main():
test_equal_no_divergence(); test_first_divergence_point(); test_prefix_shorter_vm()
test_pick_codebase_by_longest_common_prefix()
print("FAILURES:",len(FAILS)); return 1 if FAILS else 0
if __name__=="__main__": sys.exit(main())
Run → FAIL (ModuleNotFoundError).
- Step 2: Implement
diff_optrace.py
Pure functions first_divergence + pick_scene_codebase (per the tests), plus a CLI: load build/engine-optrace.jsonl (list of {codebase, offset} in order) and build/vm-optrace.json ({"offsets":[...]}); cb = pick_scene_codebase(entries, vm_offsets); extract the engine offset sequence for cb; d = first_divergence(engine_seq, vm_offsets); if diverged, print the offset, the opcode at that offset (decode build/disasm/SC0000.asm or call sys4load), and ±3 ops of context on each side. Run tests → PASS.
- Step 3: Run the oracle end-to-end
Run: py -3.11 -X utf8 tools/diff_optrace.py SC0000
Expected: either "no divergence over N steps" or "agreed N, diverge at offset X = op 0xYY" with context. Interpret the result: if it lands on the known coroutine yield (0x140/0x7b, ~0x450–0x50f), that confirms the tool pinpoints a real known gap (the self-test). Record the finding.
- Step 4: Commit
git add tools/diff_optrace.py tools/test_diff_optrace.py
git commit -m "re: diff_optrace.py — engine-vs-VM offset-path divergence oracle"
Task 4: Docs + memory + close
Files:
-
Modify:
docs/engine-re.md(the oracle: mechanism, capture method that worked, the first divergence found) -
Modify:
docs/tools-reference.md(trace_engine_ops.py,diff_optrace.py,--trace-json) -
Modify:
~/.claude/…/memory/himegari-port-status.md(milestone; lever #3 done; phase-2 effects-diff next) -
Step 1: Update engine-re.md
Record: the differential-oracle mechanism (offset-path diff), which capture hook worked (tick vs 0x41b940), the SC0000 codebase identification, and the first divergence the oracle reported (offset + op + interpretation). Note it as the repeatable way to localize a mis-modeled op.
- Step 2: Update tools-reference.md
Rows for trace_engine_ops.py (Runtime capture / Frida) and diff_optrace.py (Native engine RE or a new "Validation" group); note the --trace-json VM emitter.
- Step 3: Update the status memory
Record: differential oracle DONE — capture method, first divergence found, tool names; lever #3 of 3 complete; phase-2 (effects-diff: global/gfx writes) is the next extension.
- Step 4: Commit docs
git add docs/engine-re.md docs/tools-reference.md
git commit -m "re: record differential oracle outcome + first divergence"
Self-Review
Spec coverage: engine tracer + recon gate → Task 1; per-script codebase tagging → Task 1 Step 3 + Task 3 pick_scene_codebase; VM offset trace → Task 2; diff + first-divergence + opcode context → Task 3; coroutine-gap self-test → Task 3 Step 3; docs/memory → Task 4. Covered. (The spec's loader-hook codebase-tagging is simplified to post-hoc pick_scene_codebase matching — noted; a loader hook can be added later if identification is ambiguous.)
Placeholder scan: Task 1/Task 2 describe the Frida/C# bodies to the key calls (hook target, ctx reads, --trace-json) rather than full source, and flag "model on probe_frame_cadence.py" / "reuse if exists" — intentional (live-API bring-up like the earlier Frida/Ghidra tasks). Task 3 (the pure, deterministic core) is complete code + tests. No hidden TODOs.
Type consistency: engine trace = list of {codebase:int, offset:int}; VM trace = {"offsets":[int]}; pick_scene_codebase(entries, vm_offsets)->int; first_divergence(list[int],list[int])->{agreed,index,a,b} — consumed consistently by the Task 3 CLI. Matches the tests.