feat(capture): ingest codex session rollouts into review-gated summaries - #395
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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.pyrollout parser + ingest implementation (incl. dedup +--latestresolution 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. |
| 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 |
| 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() |
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
8fc003e to
0eb7917
Compare
|
addressed the copilot review: |
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.
session auto-capture was claude-code-only:
.claude/settings.jsonhooks drivevouch capture observe(PostToolUse) andvouch 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, everythingbuild_summary_bodyneeds, 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_commandcalls become the same Bash-shaped observations live capture writes, with failures detected from the recorded exit code;apply_patchheredocs 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_body→propose_page— so the result is the same kind of PENDING session-summary proposal a claude session yields, gated by the samecapture: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_metaraise aCodexRolloutErrorthat the cli renders as a one-lineError: ...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).--latestresolves the newest rollout whose recordedcwdmatches the current project, with--codex-homefor test isolation and non-default homes. proposals are attributed to the codex actor —VOUCH_AGENTwins when set,codexotherwise — consistent with the adapter'senvblock.review gate and scope are identical to existing capture: one page proposal via
propose_page, neverapprove(); 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),
--latestresolution, 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 reportedalready-ingestedwith the same proposal id.independent of the adapter-manifest prs; branches straight off
test.closes #387