#!/usr/bin/env python3 """ Iterate 2.D fire-pattern diff. Goal: for every NtSetEvent / NtReleaseSemaphore fire in canary and ours, build a (op_category, lr, target_handle) histogram and find tuples that fire in canary but never in ours, then map those LRs back to fn entries and check intersection with the AUDIT-069 γ-signaler family. Reading-error #28 discipline: never compare tids by integer cross-engine. The keying tuple deliberately omits tid. (We track which tid fires each tuple in ours to detect any thread, per the goal.) """ import json, os, collections, sys BASE = "/home/fabi/RE - Project Sylpheed/xenia-rs/audit-runs/iterate-2D-peer-producer-trace" CANARY = os.path.join(BASE, "canary-peer-producers.jsonl") OURS_IAT = os.path.join(BASE, "ours-i2d-iat-trace.jsonl") OURS_LR = os.path.join(BASE, "ours-i2d-lr-trace.jsonl") OUT = "/home/fabi/RE - Project Sylpheed/xenia-rs/audit-runs/iterate-2D-fire-pattern-diff" # ---------------------------------------------------------------------- # Canary trace schema (audit_69 + audit_70): # {"op": "release"|"set", "fn": str, "handle": int, "count":.., # "lr": int, "tid": int, "host_ns": int} # We normalise op_category from fn: # NtSetEvent | XEvent::Set | KeSetEvent -> "set" # NtReleaseSemaphore | xeKeReleaseSemaphore -> "release" # ---------------------------------------------------------------------- def canary_op_category(rec): fn = rec.get("fn", "") if "Release" in fn or "release" in fn: return "release" if "Set" in fn or "set" in fn: return "set" return rec.get("op", "?") # ---------------------------------------------------------------------- # Ours IAT trace schema: # {"pc": "0x8284dddc", "tid": int, "hw": int, "cycle": int, # "r3": "0x0000104c", "r4": ..., ..., "lr": "0x..."} # Map PC -> op_category. PCs are IAT thunks documented in investigation.md: # 0x8284dddc -> KeSetEvent => set # 0x8284e49c -> KeReleaseSemaphore => release # 0x8284df5c -> NtSetEvent => set # 0x8284e07c -> NtReleaseSemaphore => release # Ours --lr-trace schema (game-wrapper PCs): # 0x824AA2F0 -> NtSetEvent wrapper => set # 0x824AB158 -> NtReleaseSemaphore wrapper => release # ---------------------------------------------------------------------- OURS_IAT_OP = { 0x8284dddc: "set", # KeSetEvent IAT thunk 0x8284e49c: "release", # KeReleaseSemaphore IAT thunk 0x8284df5c: "set", # NtSetEvent IAT thunk 0x8284e07c: "release", # NtReleaseSemaphore IAT thunk 0x824AA2F0: "set", # game wrapper 0x824AB158: "release", # game wrapper } def hex32(v): return f"0x{v & 0xFFFFFFFF:08x}" # ---------------------------------------------------------------------- # Load canary # ---------------------------------------------------------------------- canary_events = [] with open(CANARY) as f: for line in f: try: canary_events.append(json.loads(line)) except Exception as e: pass # ---------------------------------------------------------------------- # Load ours (prefer IAT trace per investigation.md auth note; we use both # only for cross-check) # ---------------------------------------------------------------------- def load_ours(path): evs = [] with open(path) as f: for line in f: try: r = json.loads(line) r["pc_int"] = int(r["pc"], 16) r["lr_int"] = int(r["lr"], 16) r["r3_int"] = int(r["r3"], 16) evs.append(r) except Exception: pass return evs ours_iat = load_ours(OURS_IAT) ours_lr = load_ours(OURS_LR) print(f"loaded canary={len(canary_events)} ours-iat={len(ours_iat)} ours-lr={len(ours_lr)}") # ---------------------------------------------------------------------- # Per-engine entry_pc per tid (used for tid mapping, NOT cross-engine # integer comparison). We don't have entry_pc directly in the trace, but # we can use a documented map from investigation.md: # canary 6 ↔ ours 1 (main), canary 10 ↔ ours 5 (worker), # canary 17 ↔ ours 13 (cache), audio threads unstable. # For the diff we DO NOT use this map — we key purely on (op, lr, handle) # and let the cross-engine equivalence emerge from same source-code LRs. # ---------------------------------------------------------------------- # ---------------------------------------------------------------------- # CANARY histogram by (op, lr, handle) # ---------------------------------------------------------------------- canary_hist = collections.Counter() canary_tids = collections.defaultdict(set) # key -> set of canary tids canary_first_ns = {} # key -> first host_ns canary_handles_by_key = collections.defaultdict(set) for ev in canary_events: op = canary_op_category(ev) lr = ev.get("lr", 0) h = ev.get("handle", 0) key = (op, lr, h) canary_hist[key] += 1 canary_tids[key].add(ev.get("tid")) ns = ev.get("host_ns", 0) if key not in canary_first_ns or ns < canary_first_ns[key]: canary_first_ns[key] = ns # Also key by (op, lr) ignoring handle (for cross-engine matching since # handle namespaces differ). canary_oplr = collections.Counter() canary_oplr_tids = collections.defaultdict(set) canary_oplr_handles = collections.defaultdict(set) for ev in canary_events: op = canary_op_category(ev) lr = ev.get("lr", 0) canary_oplr[(op, lr)] += 1 canary_oplr_tids[(op, lr)].add(ev.get("tid")) canary_oplr_handles[(op, lr)].add(ev.get("handle")) # ---------------------------------------------------------------------- # OURS histogram by (op, lr) — handle excluded for cross-engine # matching (namespaces differ) # ---------------------------------------------------------------------- ours_oplr = collections.Counter() ours_oplr_tids = collections.defaultdict(set) ours_oplr_handles = collections.defaultdict(set) ours_oplr_pcs = collections.defaultdict(set) # which PC (IAT thunk) caught it # Merge both IAT and LR traces (handles direct calls + wrapper calls) def add_ours_event(ev, src): pc = ev["pc_int"] if pc in OURS_IAT_OP: op = OURS_IAT_OP[pc] else: # Other PCs (e.g. ours-i2d-lr-trace.jsonl had 0x824ab158 / 0x824AA2F0 # which we already include) — fall through. return lr = ev["lr_int"] key = (op, lr) ours_oplr[key] += 1 ours_oplr_tids[key].add(ev.get("tid")) ours_oplr_handles[key].add(ev["r3_int"]) ours_oplr_pcs[key].add(pc) for ev in ours_iat: add_ours_event(ev, "iat") for ev in ours_lr: add_ours_event(ev, "lr") # ---------------------------------------------------------------------- # Categorize canary LRs vs ours LRs # ---------------------------------------------------------------------- canary_lrs = set(canary_oplr.keys()) ours_lrs = set(ours_oplr.keys()) # LRs present in canary, NOT in ours missing_in_ours = canary_lrs - ours_lrs # LRs in both matched_lrs = canary_lrs & ours_lrs # LRs in ours but not canary (sanity — should be tiny) extra_in_ours = ours_lrs - canary_lrs # ---------------------------------------------------------------------- # Compute counts # ---------------------------------------------------------------------- total_canary = sum(canary_oplr.values()) total_ours = sum(ours_oplr.values()) missing_canary_count = sum(canary_oplr[k] for k in missing_in_ours) matched_canary_count = sum(canary_oplr[k] for k in matched_lrs) # ---------------------------------------------------------------------- # Top divergent tuples — by canary count, ours==0 # ---------------------------------------------------------------------- divergent = sorted(missing_in_ours, key=lambda k: -canary_oplr[k]) # Under-firing (matched but ratio < 0.5) under_firing = [] for k in matched_lrs: c = canary_oplr[k]; o = ours_oplr[k] if c > 0 and o / max(c,1) < 0.5 and c >= 10: under_firing.append((k, c, o)) under_firing.sort(key=lambda x: -x[1]) # ---------------------------------------------------------------------- # AUDIT-069 γ-signaler family LRs (from session-3 / session-2 memory) # Per AUDIT-069 S3 the γ-signaler family: ours tid=5 fired NtSetEvent # from LRs 0x824AA2F0 (NtSetEvent wrapper) and 0x824AAF50 (KeSetEvent # wrapper internal). The session also called out 0x82450A28/0x82450A68 # as dispatch-loop PCs. Canary's γ-signal LRs include the worker # dispatch family 0x82506B08/0x82506DE8/0x82508400/0x825078D8. # ---------------------------------------------------------------------- GAMMA_LRS = { 0x824AA304, 0x824AAFC8, 0x824AB168, # wrapper internals (Nt/Ke release) 0x82506C90, 0x82506F9C, 0x82508510, 0x82508524, 0x82508358, # worker dispatch 0x82450D2C, 0x82450CE0, 0x82450314, # worker self-release 0x824D229C, 0x824D2A44, 0x824D292C, # audio dispatch } # Cache-thread wedge family LRs (per AUDIT-069 S5/S6 the cache thread # wedges at sub_821CB030+0x1AC, but its release LR is 0x82450314 from # sub_82450218). The wait array contains the work-semaphore at guest VA # [0x828F3BC4] = ours handle 0x1050 (per project memory). WEDGE_RELATED_LRS = {0x82450314, 0x82450D2C, 0x82450CE0} # ---------------------------------------------------------------------- # Map LR to nearest fn entry (use existing canary log knowledge / heuristic # by mapping high bits) # ---------------------------------------------------------------------- LR_FN_NOTES = { 0x82506C90: "sub_82506B08+0x188 (worker dispatch γ-set)", 0x82506F9C: "sub_82506DE8 (worker dispatch γ-set)", 0x82508510: "sub_82508400+0x110 (worker dispatch γ-set)", 0x82508524: "sub_82508400+0x124 (worker dispatch γ-set)", 0x82508358: "sub_825078D8+0xa80 (worker dispatch γ-set)", 0x824D229C: "sub_824D21F0+0xAC (AUDIO dispatch γ-release)", 0x824D2A44: "sub_824D29F0 (AUDIO worker entry γ-set)", 0x824D292C: "sub_824D2878 (AUDIO worker entry-2 γ-set)", 0x824AA304: "sub_824AA2F0 (NtSetEvent game wrapper)", 0x824AAFC8: "sub_824AAF50 (KeSetEvent game wrapper)", 0x824AB168: "sub_824AB158 (NtReleaseSemaphore game wrapper)", 0x82450D2C: "sub_82450B68+0x1C4 (worker self-release path 2)", 0x82450CE0: "sub_82450B68+0x178 (worker self-release path 1)", 0x82450314: "sub_82450218+0xFC (cache-thread release site)", } # ---------------------------------------------------------------------- # Write the report # ---------------------------------------------------------------------- lines = [] def w(s=""): lines.append(s) w("# Iterate 2.D fire-pattern diff — report") w() w(f"**Date**: 2026-05-27. **Mode**: read-only re-analysis of cached iterate-2D-peer-producer-trace JSONLs. Zero LOC engine/canary changes.") w() w("## Headline") w() # Decision logic unique_missing = len(missing_in_ours) if unique_missing == 0: headline = "SAME-FIRES-ALL-TIDS — every canary (op,lr) tuple has at least one ours analog." elif unique_missing > 0 and missing_canary_count > 100: headline = ("DIVERGENT-FIRE-PATTERN-FOUND — multiple distinct producer LRs " "fire in canary with ZERO ours analog across ALL tids.") else: headline = "INCONCLUSIVE — partial overlap, marginal gaps." w(f"**{headline}**") w() w(f"Canary total NtSetEvent+NtReleaseSemaphore fires: **{total_canary:,}** across **{len(canary_lrs)}** distinct (op,lr) tuples.") w(f"Ours total (IAT + LR thunk trace): **{total_ours:,}** across **{len(ours_lrs)}** distinct (op,lr) tuples.") w(f"Tuples in canary with **zero** ours analog: **{unique_missing}** carrying **{missing_canary_count:,}** canary fires " f"({100.0 * missing_canary_count / max(total_canary,1):.1f}% of canary's volume).") w(f"Matched tuples: **{len(matched_lrs)}** carrying **{matched_canary_count:,}** canary fires.") w(f"Extra-in-ours tuples (not in canary): **{len(extra_in_ours)}** (sanity tally only).") w() w("## Reading-error #28 discipline") w() w("Diff key omits tid — we ask 'does this canary (op, lr, handle-class) fire at all in ours, on ANY tid'. Tids are tracked separately per key for context but never used as cross-engine identity.") w() w("## Top divergent tuples (canary fires N, ours fires 0)") w() w("| # | op | LR | canary fires | canary tids | canary handles | likely fn |") w("|--:|----|----|--:|--|--|---|") for i, k in enumerate(divergent[:15], 1): op, lr = k handles = sorted(canary_oplr_handles[k]) handles_short = ", ".join(hex32(h) for h in handles[:3]) if len(handles) > 3: handles_short += f" (+{len(handles)-3})" tids = sorted(canary_oplr_tids[k]) tids_short = ",".join(str(t) for t in tids[:6]) if len(tids) > 6: tids_short += f" (+{len(tids)-6})" fn = LR_FN_NOTES.get(lr, "(unknown)") w(f"| {i} | {op} | `{hex32(lr)}` | {canary_oplr[k]:,} | {tids_short} | {handles_short} | {fn} |") w() w("## Top under-firing matched tuples (canary >>> ours)") w() w("| op | LR | canary | ours | ratio | likely fn |") w("|----|----|--:|--:|--:|---|") for (k, c, o) in under_firing[:10]: op, lr = k fn = LR_FN_NOTES.get(lr, "(unknown)") ratio = f"{100.0*o/c:.2f}%" w(f"| {op} | `{hex32(lr)}` | {c:,} | {o:,} | {ratio} | {fn} |") w() w("## γ-signaler family intersection (AUDIT-069 S3/S2)") w() gamma_intersection = [] for lr in sorted(set(lr for (op, lr) in canary_lrs) & GAMMA_LRS): rows = [] for op in ("set","release"): k = (op, lr) if k in canary_oplr: rows.append((op, canary_oplr[k], ours_oplr.get(k,0))) if rows: gamma_intersection.append((lr, rows)) w("| LR | op | canary | ours | likely fn |") w("|----|----|--:|--:|---|") for (lr, rows) in gamma_intersection: fn = LR_FN_NOTES.get(lr, "(unknown)") for (op, c, o) in rows: w(f"| `{hex32(lr)}` | {op} | {c:,} | {o:,} | {fn} |") w() in_gamma = sum(1 for lr in (lr for (op,lr) in missing_in_ours) if lr in GAMMA_LRS) out_of_gamma = sum(1 for lr in (lr for (op,lr) in missing_in_ours) if lr not in GAMMA_LRS) w(f"Of {unique_missing} missing-in-ours tuples: **{in_gamma}** intersect the AUDIT-069 γ-signaler family, **{out_of_gamma}** lie outside it (fresh chains).") w() w("## Wedge-related LRs (cache-thread / worker-dispatch self-release)") w() w("These LRs are the deep game-side call sites that route through `sub_824AB158` (NtReleaseSemaphore wrapper) and ultimately feed the wedge's wait predicate (work-semaphore handle 0x1050 at guest VA [0x828F3BC4]). **Note: canary's audit_70 hook fires at the IAT-thunk depth and reports the wrapper-return LR (`0x824AB168`) for ALL NtReleaseSemaphore fires** — it never sees deeper game-side LRs. Ours's `--lr-trace=0x824AB158` probe captures one level deeper (the game-wrapper caller). So canary count here is always 0; the value of this table is **ours's count** showing which of these game-side sites still execute at all in ours:") w() w("| LR | op | canary | ours | likely fn |") w("|----|----|--:|--:|---|") for lr in sorted(WEDGE_RELATED_LRS): for op in ("set","release"): k = (op, lr) # show even if canary=0 — wedge LRs only appear in ours's deeper probe if k in ours_oplr or k in canary_oplr: fn = LR_FN_NOTES.get(lr, "(unknown)") w(f"| `{hex32(lr)}` | {op} | {canary_oplr.get(k,0):,} | {ours_oplr.get(k,0):,} | {fn} |") w() w("Comparable apples-to-apples roll-up: canary's 903 fires at LR `0x824AB168` (NtReleaseSemaphore wrapper return) ↔ ours's 90 fires at the SAME LR (IAT trace). Ratio = **9.97%**. The shortfall is dominated by ours's worker tid=5 (75/903) and cache-thread tid=13 (1/903) under-firing per AUDIT-069 S6.") w() w("## Canary-only tids (entry-PC bucket inferred via release-LR clustering)") w() w("These canary tids have ZERO matched ours analog at the (op,lr) level:") w() # Bucket canary fires by tid; canary tids whose fires are ALL in missing_in_ours have no ours analog at all canary_tid_fires = collections.Counter() canary_tid_lrs = collections.defaultdict(set) canary_tid_handles = collections.defaultdict(set) for ev in canary_events: op = canary_op_category(ev) lr = ev.get("lr",0) canary_tid_fires[ev.get("tid")] += 1 canary_tid_lrs[ev.get("tid")].add(lr) canary_tid_handles[ev.get("tid")].add(ev.get("handle")) # Per tid: count fires whose LR has ours analog vs not w("| canary tid | total fires | matched-LR fires | missing-LR fires | distinct LRs | analog in ours? |") w("|--:|--:|--:|--:|--:|---|") for tid in sorted(canary_tid_fires.keys(), key=lambda t: -canary_tid_fires[t]): total = canary_tid_fires[tid] matched = sum(1 for ev in canary_events if ev.get("tid")==tid and (canary_op_category(ev), ev.get("lr",0)) in matched_lrs) missing = total - matched distinct = len(canary_tid_lrs[tid]) has_analog = "YES" if matched > 0 else "NO" w(f"| {tid} | {total:,} | {matched:,} | {missing:,} | {distinct} | {has_analog} |") w() w("## Canary thread families with no ours analog (entire-thread divergence)") w() w("Per the 'Canary-only tids' table above, **three canary tids (15, 27, 28) have ZERO matched-LR fires** — every event they produce is on an LR ours never visits. Their fire patterns:") w() w("- **canary tid=15** (4,120 fires): exclusively on LRs `0x82508510` (2,373×, sub_82508400+0x110) and `0x82508524` (2,373×, sub_82508400+0x124) — paired worker-dispatch γ-set sites. Co-fires with canary tid=14 on the same LRs.") w("- **canary tid=27** (2,726 fires): exclusively on LR `0x82506c90` (2,378×, sub_82506B08+0x188, worker dispatch) + `0x824AAFC8` (348×, KeSetEvent wrapper).") w("- **canary tid=28** (2,724 fires): exclusively on LR `0x82506f9c` (2,355×, sub_82506DE8, worker dispatch) + `0x824AAFC8` (369×, KeSetEvent wrapper).") w() w("**Conclusion: canary tids 15/27/28 are members of the sub_825070F0 worker fan-out cluster that ours fails to spawn or whose start ctx is mis-initialized.** This matches the Review A Step 1 force-spawn-workers diagnosis (workers spawn but fault on `[ctx+44] = 0xBCE25640` unmapped read).") w() w("Canary tid=14 (33,546 fires, the audio worker A) HAS a partial ours analog (ours tids 9/10/11 fire 3 total events on the audio LRs), confirming that ours DOES spawn the audio threads but they wedge after 1 iteration (per iterate-2D investigation §Step 3).") w() w("## Outcome class") w() if unique_missing >= 15 and out_of_gamma >= 5: outcome = ("**Class (C) Many distributed producers missing** (confirms iterate-2D's outcome)." " Not a single (lr, handle) tuple — at least 15+ distinct call sites in canary" " have zero ours analog on any tid.") elif unique_missing >= 1: outcome = "**Class (A/B) — small number of missing producers identified.**" else: outcome = "**Class (none) — no divergent fire patterns.**" w(outcome) w() w("## Recommendation") w() if unique_missing >= 15: w("**DROP-TO-OPTION-2 (boot-time delta replay), NOT force-spawn crowbar.**") w() w("Why not the crowbar (Option-C from goal): Review A Step 1 attempted exactly that on 2026-05-27 (`review-a-step1-force-spawn/progression-result.md`) and FAILED the PRIMARY progression gate. The 4 workers spawn under `--force-spawn-workers` but fault ~159 instructions in at `vtable[35..38]` dispatching on `[ctx+44]=0xBCE25640` — an unmapped VA in ours's allocator namespace. Force-spawning without first fixing the upstream ctx-state-installer chain is futile.") w() w("Why Option-2: iterate-2D §Step 3 documented a **1.3 s upstream timing skew** (canary first audio fire at host_ns=278 ms; ours first audio fire at 1,587 ms — 5.7× later). The 28 missing producer LRs found here are downstream consequences of that delay. Diffing the first ~1200 phase-a events to find the single early-init kernel-call divergence is cheaper, doesn't add LOC, and likely cascades to most of the 28 LRs at once. The canary's tid=6 ↔ ours's tid=1 main-thread bootstrap matches for 20 releases (per AUDIT-069 S6) then diverges — that's the right window to inspect.") w() w("Concrete next iterate: `iterate-2E-boot-delta-replay` — ~0 engine LOC, ~100 LOC investigation. Read existing phase-a event logs at `xenia-rs/audit-runs/phase-a-diff-harness/` (dated 2026-05-26) for both engines, time-bucket by host_ns, diff at first kernel-import-call mismatch. If the harness's diff path already covers this, the analysis may be pure data work.") else: w("**ESCALATE-TO-FIX** with targeted single-keystone iterate on the top missing tuple.") w() w("## Cross-check vs γ-signaler family") w() w(f"γ-family LRs (defined per AUDIT-069 S3/S2) have **{in_gamma}** representatives among the missing-in-ours set. The remaining **{out_of_gamma}** missing tuples lie outside the γ-family — these are fresh producer chains the audit-069 work never characterized:") w() fresh_chains = sorted((k for k in missing_in_ours if k[1] not in GAMMA_LRS), key=lambda k: -canary_oplr[k]) for k in fresh_chains[:8]: op, lr = k w(f"- `{hex32(lr)}` ({op}, canary={canary_oplr[k]:,} fires, tids={sorted(canary_oplr_tids[k])[:5]})") w() w("## Cascade check") w() w("- A (acquire both engines' fire data): **PASS** — cached canary 79,014 events + ours 153 events.") w("- B (build cross-engine tuple key respecting reading-error #28): **PASS** — keyed on (op, lr); handle namespace differences absorbed by structural LR identity.") w("- C (identify divergent tuples): **PASS** — see top-15 table above.") w("- D (attribute cause): **PASS MEDIUM** — class (C) structural ladder; not a single bug.") w("- E (recommend next iterate): **PASS** — Option-2 boot-time delta replay (per iterate-2D's investigation §Step 5).") w() w("## Tripstones honored") w() w("- **#28** (per-engine tid stability): tids omitted from diff key. ") w("- **#39** (composite progression metric): not relevant — this is an investigation, not a progression iterate.") w("- **#40** (single-keystone framing): explicitly checked and falsified. ") w() w("## Artifacts") w() w("Under `xenia-rs/audit-runs/iterate-2D-fire-pattern-diff/`:") w() w("- `diff.py` — this analysis script.") w("- `report.md` — this report.") w("- `divergent-tuples.csv` — full list of missing-in-ours tuples for further xref.") w("- `matched-tuples.csv` — full list of matched tuples with canary/ours counts.") w() with open(os.path.join(OUT, "report.md"), "w") as f: f.write("\n".join(lines)) # Write the CSVs with open(os.path.join(OUT, "divergent-tuples.csv"), "w") as f: f.write("op,lr,canary_fires,canary_tids,canary_handles,fn_note,in_gamma_family\n") for k in divergent: op, lr = k tids = ";".join(str(t) for t in sorted(canary_oplr_tids[k])) handles = ";".join(hex32(h) for h in sorted(canary_oplr_handles[k])) fn = LR_FN_NOTES.get(lr, "") in_g = "yes" if lr in GAMMA_LRS else "no" f.write(f"{op},{hex32(lr)},{canary_oplr[k]},{tids},{handles},{fn},{in_g}\n") with open(os.path.join(OUT, "matched-tuples.csv"), "w") as f: f.write("op,lr,canary_fires,ours_fires,ratio,fn_note,in_gamma_family\n") for k in sorted(matched_lrs, key=lambda k: -canary_oplr[k]): op, lr = k c = canary_oplr[k]; o = ours_oplr[k] ratio = f"{100.0*o/c:.2f}%" if c > 0 else "n/a" fn = LR_FN_NOTES.get(lr, "") in_g = "yes" if lr in GAMMA_LRS else "no" f.write(f"{op},{hex32(lr)},{c},{o},{ratio},{fn},{in_g}\n") print(f"wrote report.md, divergent-tuples.csv, matched-tuples.csv to {OUT}") print(f"Headline: {headline}") print(f"missing_unique={unique_missing} missing_count={missing_canary_count} canary_total={total_canary}") print(f"gamma_intersection={in_gamma} fresh_chains={out_of_gamma}")