- test_column_notnull_probe_sees_gap3_outcome now branches on the
post-ensure probe: when outcome is relaxed (PG; SQLite 3.53+)
the resume write must land and read back NULL; on older SQLite
the ALTER is swallowed and INSERT OR IGNORE drops the row
(version-dependent boundary, not a migration failure).
- attempt_ledger.py comments updated: ALTER COLUMN DROP NOT NULL
works on Postgres and SQLite 3.53.0+, swallowed before that.
- PG proof summary section 3.9/8 corrected to the same boundary.
- 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.
- cloud/attempt_hook.py (new): canonical policy -- one counted
dispatch = one row, is_defect always False from the worker;
human/first rounds and human breakpoint resumes count;
platform auto-resume reruns, coordinator fan-out, and
pre-execution gate failures do not; transient retries inside
one dispatch are not new attempts. Platform origin survives
via the task_attempt_origin_markers table (same-transaction
write on requeue, queued_at generation guard on consume).
Defects are explicit-recorder only; corrections are
append-only (record_correction, last-wins).
- cloud/worker.py: Worker(attempt_ledger_hook=False) + CLI
--attempt-ledger-hook (default OFF = gap-3 behaviour exactly);
marker write in auto_resume_interrupted, marker consume +
classify in run_one, one ledger write at each terminal point
(cancel branch, normal terminal block); all hook writes
best-effort, never fail the task or block a requeue.
- cloud/attempt_ledger.py: corrects_attempt_no column (new DDL
supersedes gap 3's pre-merge DDL) + record_correction +
correction-aware aggregation; read path uses SELECT * so
gap-3-era tables still read; cost code untouched.
- cloud/tests/test_attempt_hook.py: 32 tests (policy, markers,
corrections, worker integration, mixed endpoint).
- docs/arch-conformance-metrics.md: policy, switch, PG manual
steps (new PG_DDL + PG_MARKER_DDL), remaining user decisions.
- cloud/attempt_ledger.py: separate task_attempts table (not tasks
columns) with per-attempt rows, MAX+1 attempt_no, append-only
intent; interrupted rows kept but never counted; is_defect is
explicit, never inferred from failure; first defect category
wins; no-ledger tasks stay unknown (None), never backfilled.
SQLite DDL is CREATE TABLE IF NOT EXISTS (idempotent); PG_DDL
is the verbatim manual step for production Postgres, kept out
of POSTGRES_SCHEMA on purpose (migrations coverage untouched).
- cloud/server.py GET /metrics-report: aggregates the ledger
into compute_metrics records; missing ledger table degrades to
unknown rates, not an error. Cost dual-column code untouched.
- cloud/metrics.py: docstring only (ledger is now the source);
compute_metrics logic and all #238 semantics unchanged.
- Tests: 16 ledger tests (store/aggregate/metrics/PG adapter
evidence) + 3 endpoint tests (real rates, old task beside
ledger task, interrupted status non-terminal).