- attempt_ledger.ensure: relax gap-3 outcome NOT NULL on Postgres
(ALTER COLUMN outcome DROP NOT NULL, probe-gated); without it
every resume write on a gap-3-migrated PG table fails with
NotNullViolation. SQLite keeps NOT NULL (no such ALTER).
- db.PgCursor: PRAGMA table_info fake rows had the notnull flag
inverted (NO -> 0); now matches sqlite semantics (1 = NOT NULL).
No caller read row[3] before, so this was latent.
- ledger_onboarding: stale 'PG path unverified' notes updated to
point at the real-PG proof.
- tests: pragma_info notnull pin (fake driver) + _column_notnull
probe pins (fresh + gap-3 SQLite).
Proof: 28/28 checks on real Postgres 16.15 (see verification/
codex-task-attempts-pg-proof-20261106.md).
User decision 2026-11-05: unified schema, rejecting the rehearsal
runtime column-union workaround and a ledger rename. One definition
(cloud.attempt_ledger.TASK_ATTEMPTS_DDL, 12 columns) shared by
cloud.attempts and cloud.db.POSTGRES_SCHEMA; ensure builds the
unified table fresh and migrates old resume, gap-3 ledger, and
rehearsal shapes in place with rows intact. aggregate counts
ledger rows only (outcome NULL = resume row, never counted).
SQLite verified by 8 new tests; Postgres real-DB upgrade path
explicitly NOT verified (see batch summary). Local commit only:
no merge, no push, no deploy.