Skip to content

feat(export): export schema v2 manifests from ClickHouse prompt chains - #36

Merged
fergusfinn merged 2 commits into
mainfrom
feat/export-plans
Sep 2, 2026
Merged

fergusfinn merged 2 commits into
mainfrom
feat/export-plans

Conversation

@fergusfinn

Copy link
Copy Markdown
Contributor

Stacked on the schema v2 PR (feat/manifest-schema-v2); merge that first.

What

batchbench export-plans (also python -m batchbench.export_plans and the batchbench-export-plans script) reads a window of clay.prompt_chains records joined with clay.http_analytics and writes a schema version 2 trajectory manifest for batchbench-agent --agent-plans-jsonl.

  • Inputs: --clickhouse-url/--clickhouse-user/--clickhouse-password (or CLICKHOUSE_URL/USER/PASSWORD), --start/--end (RFC 3339), optional --principal-id, --model, --served-by, --sample (deterministic with --seed), --stratify-by-session-length (power-of-two request-count buckets, at least one trajectory per non-empty bucket), --time-scale, --output. --dump-rows-jsonl / --rows-jsonl save and replay fetched rows so an export can be reproduced without the warehouse.
  • Sessions are reconstructed from the chains directly, not from the node view: within a principal, request B continues the latest earlier request (within --link-window-hours, default 24) whose full chain is a strict prefix of B's chain; cumulative hashing makes that a lookup of A's leaf hash inside B's chain. Each root and its descendants form one trajectory ordered by timestamp. The query starts one link window before --start, so a session that began earlier gets reset_before on its first in-window request. Branches (a second child of the same parent) also get reset_before and no live block.
  • Per request: prompt_tokens, output_tokens = completion_tokens, delay_after_ms = next.ts − (this.ts + duration_ms) (clamped at 0), blocks = chain hashes (hex) as seeds zipped with roles and token counts, the block immediately after the previous request's full chain marked live when it is an assistant block, overhead_tokens = max(prompt_tokens − sum(block_tokens), 0), stream and max_tokens when recorded. Requests without block token counts become a single user block of prompt_tokens with zero overhead. Requests without positive prompt or completion counts, or with block arrays that disagree with the chain, are dropped and counted. Token counts for a seed seen earlier in a trajectory are normalized to the first value so the replay's per-seed cache stays consistent.
  • Trajectory id = root leaf hash + counter (identical roots from batch traffic stay separate trajectories). Output is ordered by start_after_ms and validated against the schema v2 rules (a Python-side validator mirroring the Rust checks) before writing; an empty export is an error.
  • The ClickHouse client is confined to fetch_rows behind the new optional extra export = ["clickhouse-connect>=0.7"]; chains are read through clay.prompt_chains_current by default (--chains-table/--chains-final for the base table with FINAL), and both join sides are bounded to the window before joining.
  • batchbench/__init__.py now imports without the compiled extension (the API functions raise a clear error if _core is missing) so the exporter runs from a pure-Python checkout; batchbench export-plans is dispatched in __main__ before the Rust CLI.
  • README: "Exporting plans from production records". CI: the Python job now runs python -m unittest discover -s tests.

Tests

tests/test_export_plans.py (19 tests, fixtures only, no live ClickHouse): strict-prefix linking within a principal, latest-parent choice and link window, pre-window sessions and reset_before, branching, live-block marking, delays/offsets/time scale, single-block fallback, dropped-row accounting, per-seed normalization across resets, deterministic and stratified sampling (including range validation), the validator against each Rust rule, query parameterization, CLI filters and error paths, and a dry run of the exported manifest through batchbench-agent in both admission modes (skipped when the binary is not built).

Codex review findings (default table/FINAL against granted views, unbounded analytics join, empty manifest reported as success, seed definitions lost across resets, sample range) are addressed in the second commit.

I could not run the query against the real warehouse from here; the SQL is parameterized and covered by tests for its shape, but the first real export should be checked against the column names of clay.http_analytics in ClickHouse (--analytics-ts-column defaults to timestamp).

https://claude.ai/code/session_015Rtq2g2DyKJkqHgbi8qK6f

Reopened as a new PR against main: the original #34 was auto-closed when its stacked base branch was deleted on merge of #33.

…kHouse chains

`batchbench export-plans` (also `python -m batchbench.export_plans` and the
`batchbench-export-plans` script) reads clay.prompt_chains joined with
clay.http_analytics for a time window and writes a schema version 2
trajectory manifest. Sessions are reconstructed from the chains directly:
within a principal a request continues the latest earlier request (within
the link window) whose full chain is a strict prefix of its own, and each
root with its descendants becomes one trajectory. Per request the exporter
emits prompt/output tokens, inter-request delay, reset markers for sessions
that began before the window, per-block seeds/roles/tokens with the reply to
the previous request marked live, overhead tokens, and the recorded stream
and max_tokens. Deterministic (optionally length-stratified) sampling and a
time-scale factor shape the output, which is validated against the schema
version 2 rules before it is written.

The ClickHouse client is confined to one function behind the optional
`export` extra; the pure logic works on row dictionaries and is covered by
fixture tests, including a dry run of the exported manifest through
batchbench-agent when the binary is available. The package now imports
without the compiled extension so the exporter can run from a pure-Python
checkout.

Claude-Session: https://claude.ai/code/session_015Rtq2g2DyKJkqHgbi8qK6f
…review

- Read chains through clay.prompt_chains_current by default (the view that
  warehouse readers are granted); FINAL on the base table is now opt-in.
- Bound the analytics side of the join to the export window before joining
  instead of scanning the whole http_analytics table.
- Fail instead of writing an empty manifest that batchbench-agent rejects.
- Keep per-seed token definitions across resets so a re-tokenized shared
  block is normalized rather than redefined.
- Reject sample fractions outside [0, 1] or non-finite.

Claude-Session: https://claude.ai/code/session_015Rtq2g2DyKJkqHgbi8qK6f
Copilot AI lite review requested due to automatic review settings September 2, 2026 12:50
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-02T12:56:03.586549Z f4776e0 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The current trajectory root resolution and per-root counter logic can produce unstable/incorrect trajectory_id values (especially for pre-window sessions and empty-chain roots).

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a pure-Python exporter (batchbench export-plans) that converts ClickHouse prompt-chain + analytics rows into schema v2 trajectory manifests consumable by batchbench-agent --agent-plans-jsonl, along with docs, tests, packaging, and CI coverage.

Changes:

  • Implement src/batchbench/export_plans.py (ClickHouse query builder + row parsing + session reconstruction + v2 manifest generation + sampling + validator + CLI).
  • Make batchbench dispatch export-plans before loading the Rust extension, and allow importing batchbench without _core for pure-Python exports.
  • Add comprehensive unit tests for the exporter, document the workflow in README, add an optional export dependency, and run Python unit tests in CI.
File summaries
File Description
src/batchbench/export_plans.py New ClickHouse-backed exporter producing schema v2 manifests with linking, sampling, validation, and a CLI.
tests/test_export_plans.py Fixture-based unit tests covering linking, shaping, validation, query shape, and CLI paths.
src/batchbench/__main__.py Routes batchbench export-plans to the pure-Python exporter before importing Rust _core.
src/batchbench/__init__.py Makes batchbench importable without _core; API functions now error lazily when _core is missing.
README.md Adds documentation for exporting schema v2 plans from production ClickHouse records.
pyproject.toml Adds optional extra export (clickhouse-connect) and a batchbench-export-plans console script.
.github/workflows/ci.yaml Runs Python unit tests via unittest discover.
Review details

Suppressed comments (1)

src/batchbench/export_plans.py:471

  • build_plans() uses stem = trajectory.root.leaf or "" for the per-root counter map. If a root has an empty chain (leaf is None), all such trajectories share the same stem and get counters incremented across unrelated sessions, making the trajectory_id suffix depend on other empty-chain trajectories. Use the same stem logic as trajectory_id_for() instead.
    for trajectory in trajectories:
        stem = trajectory.root.leaf or ""
        counter = counters.get(stem, 0)
        counters[stem] = counter + 1
  • Files reviewed: 7/7 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +4 to +7
try:
from . import _core
except ImportError: # pragma: no cover - pure-Python checkout without the compiled extension
_core = None
Comment on lines +299 to +314
def resolve_root(record: ChainRecord) -> int:
path = []
current = record
while current.index not in root_of:
parent_index = parents.get(current.index)
parent = by_index.get(parent_index) if parent_index is not None else None
if parent is None or not in_window(parent):
root_of[current.index] = current.index
began_before[current.index] = parent is not None
break
path.append(current.index)
current = parent
root = root_of[current.index]
for index in path:
root_of[index] = root
return root
Comment thread README.md
Comment on lines +383 to +384
the next request's start, `blocks` zips the chain hashes (as seeds) with the block
roles and token counts, `overhead_tokens` is `prompt_tokens - sum(block_tokens)`, and
Comment on lines +500 to +503
Plans are ordered by a seeded hash of their trajectory id and the first ``round(fraction *
n)`` are kept. With stratification the same rule is applied per session-length bucket with
``ceil`` instead of ``round``, so every non-empty bucket keeps at least one trajectory.
"""

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f4776e0afd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +382 to +387
known = seed_tokens.get(seed)
if known is None:
seed_tokens[seed] = tokens
elif known != tokens:
stats.normalized_block_tokens += 1
tokens = known

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject normalization that exceeds the prompt target

When a repeated seed's recorded token count decreases—for example, from 100 tokens in the first request to 50 in a later 60-token prompt—this replaces the later count with 100, while the overhead calculation merely clamps the resulting negative remainder to zero. The manifest still passes validate_plans, but build_block_replay_plan replays the oversized blocks as written, so the request can substantially exceed its recorded prompt_tokens and corrupt the workload shape. Conflicting records should be split/dropped or normalized in a way that preserves every request's prompt target.

Useful? React with 👍 / 👎.

@fergusfinn
fergusfinn merged commit 89cc7b0 into main Sep 2, 2026
3 checks passed
@fergusfinn
fergusfinn deleted the feat/export-plans branch September 2, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants