Measure a WEKA corpus without reading a word of it

2a54d8f5aa51 · AtlantisPleb · · parent aa7a61de96df

Measure a WEKA corpus without reading a word of it

The exporter shipped and nothing could replay what it produced, so
#218's acceptance — that a corpus replays with prefix-reuse
characteristics matching the source sessions — was not merely unmet,
it was unevaluable.

bench/replay_weka.py reconstructs the request shape each session
produced: per turn, how many context blocks it carried and how many of
those repeat blocks already seen earlier in that session. That
repetition is the prefix reuse the corpus exists to measure, and the
chained block hashes make it computable — which is the point of the
format. The tool reads block hashes and never content; there is no
path through it that wants the plaintext, because there is no
plaintext to want.

It reports per session and corpus-wide: turns, total blocks, repeated
blocks, reuse ratio, and context growth, with a --json mode for
machines and a readable default. A malformed or empty document is
reported and skipped rather than taking the run down with it.

A synthetic fixture ships beside it with a self-check that the
reported ratio matches a hand-computed expectation, so the measurement
is pinned rather than merely produced.

Built by a Devin child through the openagents coder's delegate tool;
the self-checks and the fixture run were re-run before landing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GoYpb8FEmdxVErsv7ABCYi
Co-Authored-By
Claude Fable 5 <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • added bench/fixtures/weka-trace-v1-sample.json
  • added bench/replay_weka.py
  • added bench/test_replay_weka.py

Diff

3 files changed, +440 -0

bench/fixtures/weka-trace-v1-sample.json added +38

@@ -0,0 +1,38 @@

1
{
2
  "version": "weka-trace-v1",
3
  "session_id": "demo-001",
4
  "salt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
5
  "block_size": 64,
6
  "events": [
7
    {
8
      "t": 0.0,
9
      "modality": "user",
10
      "hash_ids": [
11
        "abc1",
12
        "abc2"
13
      ]
14
    },
15
    {
16
      "t": 1.0,
17
      "modality": "assistant",
18
      "hash_ids": [
19
        "abc1",
20
        "abc2",
21
        "abc3",
22
        "abc4"
23
      ]
24
    },
25
    {
26
      "t": 2.0,
27
      "modality": "user",
28
      "hash_ids": [
29
        "abc1",
30
        "abc2",
31
        "abc3",
32
        "abc4",
33
        "abc5",
34
        "abc6"
35
      ]
36
    }
37
  ]
38
}
bench/replay_weka.py added +332

@@ -0,0 +1,332 @@

1
#!/usr/bin/env python3
2
"""Replay weka-trace-v1 documents and report prefix reuse.
3
4
This tool consumes one or more weka-trace-v1 JSON documents without ever
5
reading plaintext. It walks each session's events in time order, counts the
6
64-token block hashes that appear in each turn, and counts how many of those
7
hashes were already observed earlier in the same session. This is the per-turn
8
prefix-reuse signal.
9
10
Supported schema (robust, accepts any of the listed keys at each level):
11
12
  Top-level trace document:
13
    - "version" (optional): should be "weka-trace-v1".
14
    - One of "id", "session_id", "thread_id", "trace_id": the root session
15
      identifier. Falls back to the file stem.
16
    - One of "events", "requests", "turns": an ordered list of per-turn
17
      records.
18
19
  Each per-turn record supports:
20
    - Time: one of "t", "timestamp", "offset_ms", "offset".
21
    - Modality: one of "modality", "type", "role", "name".
22
    - Context block hashes: one of "hash_ids", "block_hashes", "hashes",
23
      "blocks". The value is a list of hash identifiers (strings or ints).
24
    - A subagent record may also carry one of "agent_id", "id", "name" and a
25
      nested "requests"/"events"/"turns" list. Those nested events are treated
26
      as a separate thread, e.g. "<root>::sa:<agent_id>".
27
28
Per-session and corpus-wide output includes turns, total blocks, repeated
29
blocks, the prefix-reuse ratio (repeated / total), and the context-growth
30
blocks (total - repeated, i.e. newly introduced blocks).
31
"""
32
33
import argparse
34
import datetime as dt
35
import json
36
import sys
37
from pathlib import Path
38
from typing import Any, Dict, Iterable, List, Optional, Tuple
39
40
_VERSION = "weka-trace-v1"
41
_SESSION_KEYS = ("session_id", "thread_id", "trace_id", "id")
42
_EVENT_LIST_KEYS = ("events", "requests", "turns")
43
_HASH_KEYS = ("hash_ids", "block_hashes", "hashes", "blocks")
44
_TIME_KEYS = ("t", "timestamp", "emitted_at", "offset_ms", "offset")
45
_MODALITY_KEYS = ("modality", "type", "role", "name", "event_type")
46
_AGENT_ID_KEYS = ("agent_id", "id", "name")
47
_NESTED_EVENT_KEYS = ("requests", "events", "turns")
48
49
50
def _get(d: Dict[str, Any], keys: Iterable[str], default: Any = None) -> Any:
51
    for k in keys:
52
        if k in d:
53
            return d[k]
54
    return default
55
56
57
def _canon(v: Any) -> Optional[Any]:
58
    if v is None:
59
        return None
60
    if isinstance(v, (str, int, float, bool)):
61
        return (type(v).__name__, v)
62
    return ("json", json.dumps(v, sort_keys=True, ensure_ascii=False, separators=(",", ":")))
63
64
65
def _sortable_time(v: Any) -> Tuple:
66
    if isinstance(v, (int, float)):
67
        return (0, float(v))
68
    if isinstance(v, str):
69
        try:
70
            parsed = dt.datetime.fromisoformat(v.replace("Z", "+00:00"))
71
            return (0, parsed.timestamp())
72
        except ValueError:
73
            return (1, v)
74
    return (2, "")
75
76
77
def _has_blocks(ev: Dict[str, Any]) -> bool:
78
    return any(k in ev and isinstance(ev[k], list) for k in _HASH_KEYS)
79
80
81
def _iter_nested(
82
    events: List[Any],
83
    thread: str,
84
    out: List[Tuple[str, Dict[str, Any]]],
85
) -> None:
86
    for i, ev in enumerate(events):
87
        if not isinstance(ev, dict):
88
            continue
89
        modality = _get(ev, _MODALITY_KEYS)
90
        is_subagent = (
91
            modality == "subagent"
92
            or "agent_id" in ev
93
            or "subagent_type" in ev
94
            or "subagent" in ev
95
        )
96
        if any(k in ev and isinstance(ev[k], list) for k in _HASH_KEYS) and not is_subagent:
97
            # Exported events always carry a blocks list, including empty text
98
            # payloads; retain those as zero-block turns.
99
            out.append((thread, ev))
100
        if is_subagent:
101
            sub_thread = (
102
                f"{thread}::sa:{_get(ev, _AGENT_ID_KEYS) or i}"
103
            )
104
            nested = _get(ev, _NESTED_EVENT_KEYS)
105
            if isinstance(nested, list) and nested:
106
                _iter_nested(nested, sub_thread, out)
107
108
109
def _extract_sessions(doc: Dict[str, Any], fallback_id: str) -> List[Tuple[str, List[Dict[str, Any]]]]:
110
    if not isinstance(doc, dict):
111
        raise ValueError("document is not a JSON object")
112
    version = doc.get("format", doc.get("version"))
113
    if version is not None and version != _VERSION:
114
        raise ValueError(f"unsupported format/version: {version!r}")
115
    root = _get(doc, _SESSION_KEYS) or fallback_id
116
    events = _get(doc, _EVENT_LIST_KEYS)
117
    if not isinstance(events, list):
118
        raise ValueError("missing events/requests/turns list")
119
    flat: List[Tuple[str, Dict[str, Any]]] = []
120
    _iter_nested(events, root, flat)
121
    grouped: Dict[str, List[Dict[str, Any]]] = {}
122
    for thread, ev in flat:
123
        eid = _get(ev, _SESSION_KEYS)
124
        key = eid if isinstance(eid, str) else thread
125
        grouped.setdefault(key, []).append(ev)
126
    if not grouped:
127
        raise ValueError("no turns with block hashes found")
128
    return list(grouped.items())
129
130
131
def _blocks_for_event(ev: Dict[str, Any]) -> List[Any]:
132
    return _get(ev, _HASH_KEYS) or []
133
134
135
def _analyze_session(events: List[Dict[str, Any]]) -> Dict[str, Any]:
136
    indexed = sorted(enumerate(events), key=lambda x: (_sortable_time(_get(x[1], _TIME_KEYS)), x[0]))
137
    seen: set = set()
138
    turns: List[Dict[str, Any]] = []
139
    total, repeated, new = 0, 0, 0
140
    first_total, last_total = 0, 0
141
    for orig_idx, ev in indexed:
142
        raw = _blocks_for_event(ev)
143
        if not isinstance(raw, list):
144
            raise ValueError("turn block list is not an array")
145
        keys = [_canon(b) for b in raw if _canon(b) is not None]
146
        n_total = len(keys)
147
        n_repeated = sum(1 for k in keys if k in seen)
148
        n_new = n_total - n_repeated
149
        seen.update(keys)
150
        total += n_total
151
        repeated += n_repeated
152
        new += n_new
153
        if not turns:
154
            first_total = n_total
155
        last_total = n_total
156
        turns.append(
157
            {
158
                "index": len(turns) + 1,
159
                "timestamp": _get(ev, _TIME_KEYS),
160
                "modality": _get(ev, _MODALITY_KEYS) or "?",
161
                "total_blocks": n_total,
162
                "repeated_blocks": n_repeated,
163
                "new_blocks": n_new,
164
                "prefix_reuse_ratio": n_repeated / n_total if n_total else 0.0,
165
            }
166
        )
167
    if not turns:
168
        raise ValueError("session has no turns with block arrays")
169
    return {
170
        "turns": turns,
171
        "summary": {
172
            "turns": len(turns),
173
            "total_blocks": total,
174
            "repeated_blocks": repeated,
175
            "new_blocks": new,
176
            "prefix_reuse_ratio": repeated / total,
177
            "context_growth": new,
178
            "first_turn_blocks": first_total,
179
            "last_turn_blocks": last_total,
180
        },
181
    }
182
183
184
def _load_documents(path: Path) -> List[Dict[str, Any]]:
185
    raw = path.read_text(encoding="utf-8")
186
    if not raw.strip():
187
        raise ValueError("empty document")
188
    try:
189
        payload = json.loads(raw)
190
    except json.JSONDecodeError as exc:
191
        docs: List[Dict[str, Any]] = []
192
        for i, line in enumerate(raw.splitlines(), 1):
193
            line = line.strip()
194
            if not line or line.startswith("#"):
195
                continue
196
            try:
197
                docs.append(json.loads(line))
198
            except json.JSONDecodeError as line_exc:
199
                raise ValueError(f"line {i}: invalid JSON ({line_exc})") from exc
200
        if not docs:
201
            raise ValueError("no JSON objects") from exc
202
        return docs
203
    if isinstance(payload, dict):
204
        return [payload]
205
    if isinstance(payload, list):
206
        if not payload:
207
            raise ValueError("empty JSON array")
208
        if all(isinstance(x, dict) and _has_blocks(x) for x in payload):
209
            return [{"events": payload}]
210
        docs = [x for x in payload if isinstance(x, dict)]
211
        if not docs:
212
            raise ValueError("JSON array contains no documents")
213
        return docs
214
    raise ValueError(f"unsupported top-level JSON type: {type(payload).__name__}")
215
216
217
def _summarize_corpus(sessions: List[Dict[str, Any]]) -> Dict[str, Any]:
218
    total = sum(s["summary"]["total_blocks"] for s in sessions)
219
    repeated = sum(s["summary"]["repeated_blocks"] for s in sessions)
220
    turns = sum(s["summary"]["turns"] for s in sessions)
221
    new = total - repeated
222
    return {
223
        "turns": turns,
224
        "total_blocks": total,
225
        "repeated_blocks": repeated,
226
        "new_blocks": new,
227
        "prefix_reuse_ratio": repeated / total if total else 0.0,
228
        "context_growth": new,
229
    }
230
231
232
def _fmt_time(v: Any) -> str:
233
    if v is None:
234
        return "-"
235
    return str(v)
236
237
238
def _print_human(results: List[Dict[str, Any]], corpus: Dict[str, Any]) -> None:
239
    for r in results:
240
        path = r["path"]
241
        print(f"\n{path}")
242
        if "error" in r:
243
            print(f"  ERROR: {r['error']}")
244
            continue
245
        for d in r["documents"]:
246
            for s in d["sessions"]:
247
                sid = s["session_id"]
248
                print(f"  session {sid}: {len(s['turns'])} turns")
249
                print(f"    {'idx':<4} {'time':<8} {'modality':<10} {'total':<6} {'repeat':<7} {'reuse':<6} {'new':<5}")
250
                for t in s["turns"]:
251
                    print(
252
                        f"    {t['index']:<4} {_fmt_time(t['timestamp']):<8} "
253
                        f"{t['modality']:<10} {t['total_blocks']:<6} "
254
                        f"{t['repeated_blocks']:<7} {t['prefix_reuse_ratio']:<6.4f} "
255
                        f"{t['new_blocks']:<5}"
256
                    )
257
                sm = s["summary"]
258
                print(
259
                    f"  summary: turns={sm['turns']} total={sm['total_blocks']} "
260
                    f"repeated={sm['repeated_blocks']} "
261
                    f"reuse={sm['prefix_reuse_ratio']:.4f} growth={sm['context_growth']}"
262
                )
263
    print("\nCorpus summary")
264
    print(
265
        f"  turns={corpus['turns']} total={corpus['total_blocks']} "
266
        f"repeated={corpus['repeated_blocks']} "
267
        f"reuse={corpus['prefix_reuse_ratio']:.4f} growth={corpus['context_growth']}"
268
    )
269
270
271
def _build_output(results: List[Dict[str, Any]], corpus: Dict[str, Any]) -> Dict[str, Any]:
272
    return {"files": results, "corpus": corpus}
273
274
275
def process_paths(paths: List[Path]) -> Tuple[List[Dict[str, Any]], List[str]]:
276
    results: List[Dict[str, Any]] = []
277
    errors: List[str] = []
278
    for p in paths:
279
        try:
280
            if not p.exists():
281
                raise FileNotFoundError(f"file not found: {p}")
282
            docs = _load_documents(p)
283
            file_docs: List[Dict[str, Any]] = []
284
            for i, doc in enumerate(docs):
285
                sessions = _extract_sessions(doc, f"{p.stem}#{i}")
286
                analyzed = [
287
                    {
288
                        "session_id": sid,
289
                        "turns": a["turns"],
290
                        "summary": a["summary"],
291
                    }
292
                    for sid, a in ((sid, _analyze_session(ev)) for sid, ev in sessions)
293
                ]
294
                file_docs.append({"document_index": i, "sessions": analyzed})
295
            results.append({"path": str(p), "documents": file_docs})
296
        except Exception as exc:
297
            msg = f"{p}: {exc}"
298
            errors.append(msg)
299
            results.append({"path": str(p), "error": msg})
300
    return results, errors
301
302
303
def main(argv: Optional[List[str]] = None) -> int:
304
    parser = argparse.ArgumentParser(
305
        description="Replay weka-trace-v1 documents and report prefix reuse."
306
    )
307
    parser.add_argument("traces", nargs="+", type=Path, help="weka-trace-v1 JSON files")
308
    parser.add_argument("--json", action="store_true", help="emit machine-readable JSON output")
309
    arguments = parser.parse_args(argv)
310
311
    results, errors = process_paths(arguments.traces)
312
    all_sessions = [
313
        s for r in results for d in r.get("documents", []) for s in d["sessions"]
314
    ]
315
    corpus = _summarize_corpus(all_sessions)
316
317
    if arguments.json:
318
        out = _build_output(results, corpus)
319
        json.dump(out, sys.stdout, indent=None)
320
        print()
321
    else:
322
        _print_human(results, corpus)
323
324
    if errors:
325
        for e in errors:
326
            print(e, file=sys.stderr)
327
        return 1
328
    return 0
329
330
331
if __name__ == "__main__":
332
    raise SystemExit(main())
bench/test_replay_weka.py added +70

@@ -0,0 +1,70 @@

1
#!/usr/bin/env python3
2
"""Self-check for bench/replay_weka.py.
3
4
Run with the Python that ships the monorepo bench scripts:
5
6
    python3 bench/test_replay_weka.py
7
"""
8
9
import json
10
import subprocess
11
import sys
12
import tempfile
13
from pathlib import Path
14
from typing import List
15
16
_BENCH = Path(__file__).resolve().parent
17
_TOOL = _BENCH / "replay_weka.py"
18
_FIXTURE = _BENCH / "fixtures" / "weka-trace-v1-sample.json"
19
20
21
def _run(args: List[str]) -> subprocess.CompletedProcess:
22
    return subprocess.run(
23
        [sys.executable, str(_TOOL), "--json", *args],
24
        capture_output=True,
25
        text=True,
26
        cwd=str(_BENCH.parent),
27
    )
28
29
30
def test_sample_reuse_ratio() -> None:
31
    proc = _run([str(_FIXTURE)])
32
    if proc.returncode != 0:
33
        raise AssertionError(f"tool failed: {proc.stderr}")
34
    data = json.loads(proc.stdout)
35
    corpus = data["corpus"]
36
    assert corpus["turns"] == 3
37
    assert corpus["total_blocks"] == 12
38
    assert corpus["repeated_blocks"] == 6
39
    assert corpus["prefix_reuse_ratio"] == 0.5
40
    assert corpus["context_growth"] == 6
41
42
43
def test_malformed_and_empty_do_not_block_valid() -> None:
44
    with tempfile.TemporaryDirectory() as d:
45
        empty = Path(d) / "empty.json"
46
        empty.write_text("")
47
        bad = Path(d) / "bad.json"
48
        bad.write_text("not json")
49
        proc = _run([str(empty), str(bad), str(_FIXTURE)])
50
        assert proc.returncode == 1, f"expected exit 1, got {proc.returncode}"
51
        data = json.loads(proc.stdout)
52
        errors = [f for f in data["files"] if "error" in f]
53
        assert len(errors) == 2
54
        corpus = data["corpus"]
55
        assert corpus["turns"] == 3
56
        assert corpus["total_blocks"] == 12
57
        assert corpus["prefix_reuse_ratio"] == 0.5
58
59
60
if __name__ == "__main__":
61
    failures = 0
62
    for test in (test_sample_reuse_ratio, test_malformed_and_empty_do_not_block_valid):
63
        name = test.__name__
64
        try:
65
            test()
66
            print(f"{name}: PASS")
67
        except Exception as exc:
68
            print(f"{name}: FAIL {exc}")
69
            failures += 1
70
    raise SystemExit(1 if failures else 0)

This page updates live while a promote is in flight · changelog