Skip to content

feat(capture): ingest codex session rollouts into review-gated summaries - #395

Merged
plind-junior merged 2 commits into
vouchdev:testfrom
dripsmvcp:feat/codex-ingest
Jul 6, 2026
Merged

feat(capture): ingest codex session rollouts into review-gated summaries#395
plind-junior merged 2 commits into
vouchdev:testfrom
dripsmvcp:feat/codex-ingest

Conversation

@dripsmvcp

Copy link
Copy Markdown
Contributor

session auto-capture was claude-code-only: .claude/settings.json hooks drive vouch capture observe (PostToolUse) and vouch capture finalize (SessionEnd). codex has no equivalent live hook stream, so codex sessions left no trace in the kb unless the agent proposed explicitly. codex does persist every session as a rollout jsonl under $CODEX_HOME/sessions/YYYY/MM/DD/ — user messages, tool calls, and outputs, everything build_summary_body needs, just after the fact instead of live.

this adds vouch capture ingest-codex [<rollout> | --latest]. the rollout parsing lives in a small dedicated module (src/vouch/codex_rollout.py): exec_command calls become the same Bash-shaped observations live capture writes, with failures detected from the recorded exit code; apply_patch heredocs surface as file edits with the touched paths; mcp/custom tools keep their own names; session mechanics (update_plan, write_stdin) are skipped. the ingest side then reuses the existing rollup — build_summary_bodypropose_page — so the result is the same kind of PENDING session-summary proposal a claude session yields, gated by the same capture: config (enabled, min_observations). one code path from observation to proposal, two front doors.

the rollout format is not a stable public contract, so drift degrades to a clear error, not a stack trace: unknown record types are tolerated, while unreadable files, zstd-compressed rollouts, and files with no session_meta raise a CodexRolloutError that the cli renders as a one-line Error: ... with a non-zero exit and nothing written. re-ingesting the same session is a no-op keyed on the rollout's session id (any prior proposal for that session, whatever its status, blocks a duplicate). --latest resolves the newest rollout whose recorded cwd matches the current project, with --codex-home for test isolation and non-default homes. proposals are attributed to the codex actor — VOUCH_AGENT wins when set, codex otherwise — consistent with the adapter's env block.

review gate and scope are identical to existing capture: one page proposal via propose_page, never approve(); reads one local file the user names (or resolves); no network.

23 new tests cover parsing (meta extraction, observation mapping, failure marking, patch-file extraction, unknown-type tolerance), the error paths (no meta, missing file, zstd), ingest (pending proposal shape, actor attribution, dedup no-op, below-min, disabled, gate intact), --latest resolution, and the cli surface including the exactly-one-source check. fixtures use placeholder data only (alice-example), enforced by a test. also verified end-to-end against a real codex 0.142.0 rollout: 89 observations rolled into one PENDING proposal, and the re-run reported already-ingested with the same proposal id.

independent of the adapter-manifest prs; branches straight off test.

closes #387

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a0d904ab-522a-4563-98fa-57a96621e617

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

Pull request overview

Adds a Codex-specific “front door” into the existing capture → summary → proposal pipeline by ingesting Codex rollout JSONL files and converting their recorded tool calls/outputs into the same observation shape used by live capture, producing the usual review-gated PENDING session-summary page proposal (deduped by session id).

Changes:

  • Introduce vouch capture ingest-codex [<rollout> | --latest] to ingest a Codex rollout into a review-gated session-summary proposal.
  • Add src/vouch/codex_rollout.py rollout parser + ingest implementation (incl. dedup + --latest resolution by project cwd).
  • Add fixtures and a comprehensive test suite covering parsing, error paths, ingest behavior, dedup, --latest, and CLI surface.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/vouch/codex_rollout.py New rollout parsing + ingest module mapping Codex rollout records into capture observations and filing a PENDING page proposal.
src/vouch/cli.py Wires new capture ingest-codex command and routes CodexRolloutError through the CLI error wrapper.
tests/test_codex_rollout.py End-to-end tests for parsing, ingest semantics (gate intact, dedup, config gating), --latest, and CLI behavior.
tests/fixtures/codex/rollout-basic.jsonl Placeholder Codex rollout fixture used for parser/ingest tests.
tests/fixtures/codex/rollout-no-meta.jsonl Fixture validating actionable errors when required rollout metadata is missing.

Comment thread src/vouch/codex_rollout.py Outdated
Comment on lines +257 to +268
for path in sorted(sessions.rglob("rollout-*.jsonl"), reverse=True):
try:
with path.open(encoding="utf-8") as fh:
first = json.loads(fh.readline())
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
continue
if not isinstance(first, dict) or first.get("type") != "session_meta":
continue
payload = first.get("payload")
if isinstance(payload, dict) and payload.get("cwd") == target:
return path
return None
Comment thread src/vouch/codex_rollout.py Outdated
Comment on lines +151 to +172
try:
raw = path.read_bytes()
except OSError as e:
raise CodexRolloutError(f"cannot read rollout file {path}: {e}") from e
if raw[:4] == _ZSTD_MAGIC:
raise CodexRolloutError(
f"{path.name} is zstd-compressed; decompress it first "
f"(`zstd -d {path.name}`) and ingest the .jsonl"
)
text = raw.decode("utf-8", errors="replace")

session_id: str | None = None
cwd: str | None = None
started_at: str | None = None
first_prompt: str | None = None
observations: list[dict[str, Any]] = []
# call_id -> index into observations, so a later function_call_output
# can mark the command as failed the way summarize_tool does live.
open_calls: dict[str, int] = {}

for line in text.splitlines():
line = line.strip()
dripsmvcp added 2 commits July 6, 2026 17:32
vouchdev#361 inserted `_log = logging.getLogger(...)` between two import
groups in jsonl_server.py. that statement-among-imports trips ruff's
E402 on every import that follows it, so `ruff check src tests` — the
lint gate in ci — now fails on the whole repo. move the logger
definition below the imports (where storage.py already keeps its own
module logger); no behaviour change.
session auto-capture was claude-code-only: hooks drive capture observe
and capture finalize live. codex has no hook stream, but it persists
every session as a rollout jsonl under $CODEX_HOME/sessions containing
user messages, tool calls, and outputs — everything the existing
rollup needs, just after the fact.

new cli command `vouch capture ingest-codex [<rollout> | --latest]`
parses one rollout through a small dedicated parser (codex_rollout.py)
that maps function_call records into the same observation shape
capture.observe produces — shell commands with failure detection from
exit codes, apply_patch heredocs surfaced as file edits, mcp tools
under their own names, session mechanics skipped — then reuses the
existing build_summary_body -> propose_page rollup. one code path from
observation to proposal, two front doors.

the rollout format is not a stable public contract: unknown record
types are tolerated, and unreadable, compressed, or meta-less files
degrade to a CodexRolloutError with an actionable message and a
non-zero exit, never a stack trace. re-ingesting a session is a no-op
keyed on the rollout's session id; --latest resolves the newest
rollout whose recorded cwd matches the current project. proposals are
attributed to the codex actor (VOUCH_AGENT wins when set), respect
capture's enabled/min_observations config, and never touch approve().

fixture rollouts use placeholder data only, enforced by a test.

closes vouchdev#387
@dripsmvcp
dripsmvcp force-pushed the feat/codex-ingest branch from 8fc003e to 0eb7917 Compare July 6, 2026 08:42
@github-actions github-actions Bot added the mcp mcp, jsonl, and http surfaces label Jul 6, 2026
@dripsmvcp

Copy link
Copy Markdown
Contributor Author

addressed the copilot review: parse_rollout now streams the file line-by-line (peeking the zstd magic up front) instead of reading it whole, so a session with large tool outputs no longer loads entirely into memory; find_latest_rollout keeps the newest match and skips opening any candidate that can't beat it rather than sorting the whole sessions/ tree; _patch_observation reports Delete-only patches as "Deleted"; and the no-session_meta error no longer claims the record must be on the "first line" (the parser scans the whole file). tests added.

@plind-junior
plind-junior merged commit 87b4a5e into vouchdev:test Jul 6, 2026
8 checks passed
minion1227 added a commit to minion1227/vouch that referenced this pull request Jul 6, 2026
resolves the vouchdev#297 conflicts against its actual base branch (test, not
main) after test advanced to vouchdev#395.

- context.py / proposals.py: keep the typed `store.config` readers, drop
  the ad-hoc yaml parsing test still carried at those call sites.
- proposals.py `_approval_block_reason`: keep test's protected-page-kind
  self-approval guard, source `approver_role` from the typed config.
- health.py: import both `ConfigError` (vouchdev#243) and `Source` (test).
- test_storage.py: keep both the typed-config tests and test's
  corrupt-file resilience tests.
- changelog: move the vouchdev#243 entry into `[Unreleased]`; the auto-labeling
  dup test already carries in [1.1.0].
- models.py: register `capture` / `recall` / `compile` / `triage` as
  loose known config sections so test's starter-config and own-reader
  sections don't trip the vouchdev#243 unknown-key check.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli command line interface mcp mcp, jsonl, and http surfaces size: L 500-999 changed non-doc lines tests tests and fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants