Persist Guardian review failures for reports across restarts (#51651)

## Why

Failed Guardian review evidence was process-local, so restarting Codex lost the context needed for later diagnostic reports.

## What changed

- Persist failed-review records in SQLite before reviewer cleanup, retaining at most 64 records globally, eight per thread, and 8 MiB of evidence. Delete persisted evidence when its reviewed thread is deleted.
- Recover and deduplicate persisted and in-memory records for the reported task tree when uploading `auto-review-failures.jsonl`.
- Bound persistence attempts to 250 ms and retain an in-memory fallback when storage is unavailable. Keep capture gated by user opt-in and non-ephemeral sessions, and export evidence only when logs are included.

## Testing

Add coverage for persistence across reopen and process restart, retention limits, cascading deletion, busy-storage fallback, and report uploads with and without logs, including SQLite log failures.

GitOrigin-RevId: ea2da1818d4a5da3da29f211042ca45e834342bc
This commit is contained in:
jif
2026-10-07 11:18:05 +00:00
committed by copyberry
parent 37eaae6eeb
commit 30bdfec59d
17 changed files with 442 additions and 74 deletions
+24 -3
View File
@@ -2207,10 +2207,31 @@ async fn guardian_denial_rejects_tool_call_with_rationale(
assert!(guardian_request.body_contains_text(&command));
assert_eq!(guardian_request.body_json()["model"], "gpt-5.6-luna");
let feedback = codex_feedback::guardian_review_failures(&[test.session_configured.thread_id])
.attachment
.expect("failed Guardian review");
test.codex.shutdown_and_wait().await?;
let state_db = codex_state::StateRuntime::init(
test.config.sqlite.clone(),
test.config.model_provider_id.clone(),
)
.await?;
let durable = state_db.list_guardian_review_records().await?;
assert_eq!(
durable.len(),
1,
"the Guardian capture must persist, not just fall back to memory"
);
let feedback = codex_feedback::guardian_review_failures(
Some(&state_db),
&[test.session_configured.thread_id],
)
.await
.attachment
.expect("failed Guardian review");
let record: serde_json::Value = serde_json::from_slice(&feedback.buffer)?;
assert_eq!(
record,
serde_json::from_slice::<serde_json::Value>(&durable[0].record)?
);
assert_eq!(record["instructions"], guardian_request.instructions_text());
assert!(
guardian_request.body_contains_text(record["action"].as_str().expect("reviewed action"))
);
@@ -1389,7 +1389,11 @@ async fn run_guardian_subagent_review(
.thread_manager
.list_agent_subtree_thread_ids(root_thread_id)
.await?;
let failures = codex_feedback::guardian_review_failures(&feedback_thread_ids);
let failures = codex_feedback::guardian_review_failures(
test.codex.state_db().as_deref(),
&feedback_thread_ids,
)
.await;
assert_eq!(failures.thread_ids, vec![worker_thread_id]);
let feedback = failures.attachment.expect("failed worker review");
let record: Value = serde_json::from_slice(&feedback.buffer)?;