Summary: Runtime::persist_thread (crates/core/src/lib.rs:836-860) hardcodes approval_mode: None and sandbox_policy: None into ThreadMetadata, and upsert_thread's ON CONFLICT DO UPDATE (crates/state/src/lib.rs:642-643) overwrites those columns with the excluded (None) values. Every resume path (resume_thread_with_history → persist_thread) silently erases any persisted per-thread policy.
When hit: mixing surfaces on one state DB — create/configure a thread with a per-thread approval/sandbox policy, then thread/resume it through the headless runtime. Latent today (no current caller writes non-None values) but a landmine for TUI+API on the same state DB.
Impact: silent policy downgrade to global defaults with no error or log.
Repro: set approval_mode on a thread row; call thread/resume; SELECT approval_mode → NULL.
Found in a read-only v0.9.4 bug hunt (2026-08-02).
Summary:
Runtime::persist_thread(crates/core/src/lib.rs:836-860) hardcodesapproval_mode: Noneandsandbox_policy: NoneintoThreadMetadata, andupsert_thread'sON CONFLICT DO UPDATE(crates/state/src/lib.rs:642-643) overwrites those columns with the excluded (None) values. Every resume path (resume_thread_with_history→persist_thread) silently erases any persisted per-thread policy.When hit: mixing surfaces on one state DB — create/configure a thread with a per-thread approval/sandbox policy, then
thread/resumeit through the headless runtime. Latent today (no current caller writes non-None values) but a landmine for TUI+API on the same state DB.Impact: silent policy downgrade to global defaults with no error or log.
Repro: set approval_mode on a thread row; call thread/resume;
SELECT approval_mode→ NULL.Found in a read-only v0.9.4 bug hunt (2026-08-02).