fix(db): agent_sync_state ints are bounded at their own column's ceiling — git_dir_bytes BIGINT (#2800), int4 coercion (#2827) - #2805
Conversation
`git_dir_bytes` was declared INTEGER for the PostgreSQL backend (tables.py Integer + Alembic 0019 `INTEGER`), i.e. int4 with a 2 GiB ceiling. The column exists to observe workspace-repo bloat (#1596), so the values it is there to record are exactly the ones that overflowed: any agent whose `.git` passed 2,147,483,647 bytes made every SyncHealthService upsert raise `psycopg2.errors.NumericValueOutOfRange: integer out of range`, and that agent's sync health went dark at the moment it mattered. SQLite never showed it (its INTEGER is 64-bit), which is how it shipped on 2026-07-14. Dual-track (Invariant #9): - schema.py: `git_dir_bytes BIGINT` — single source of truth for both backends (init_schema_postgres translates the same string), so fresh PG builds get int8 via 0001_baseline. - tables.py: `BigInteger`. - Alembic 0062: `ALTER COLUMN git_dir_bytes TYPE BIGINT` (proven on a real postgres:16 upgraded from 0061 with the column forced back to int4: information_schema reports `bigint` afterwards and a 44 GiB insert lands). - SQLite `agent_sync_state_git_dir_bytes_bigint`: a declared-type rebuild via the #1160 rename-swap, NOT a bare no-op. schema-parity compares a fresh init_schema DB against an upgraded one by declared column type, so a no-op would leave upgraded files reading INTEGER against a fresh BIGINT and turn that guard red forever. One row per agent, all columns copied verbatim, the one index re-created, idempotent. CI regression seam: `TestGitDirBytesRoundTrip` in test_1596_git_sync_observability.py is now `requires_postgres`, so the schema-parity PostgreSQL tier (#2434) runs its [postgres] leg — the leg that had been red for two months while the tier selected only marked tests. A new information_schema assertion names the column type rather than a stack; two SQLite tests pin the rebuild (rows preserved, index back, no-op pre-#1596). Audit of sibling byte-count columns: `agent_shared_files.size_bytes` stays Integer — bounded by MAX_FILE_SIZE_BYTES (50 MB) at the only writer, so it cannot reach int4's ceiling by construction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rename-swap copies exactly the columns its INSERT...SELECT lists and DROPs the old table. Compare the live agent_sync_state column set against that list first and raise — before touching anything — on an unknown column, so a future/unforeseen column is surfaced as a boot failure (`first_pending`, #1160) rather than silently destroyed. No known path produces one today; this is a belt on a migration whose failure mode is data loss. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s are BigInteger, PG tests need the marker Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/review ReportBranch: Non-breaking verification (both tracks, real artifacts — not the test harness)SQLite — upgrade of a legacy file
PostgreSQL —
Critical FindingsNone. Informational Findings[I1] Migration data safety — rename-swap drops columns it doesn't name (Confidence 8/10) — fixed in [I2] Sibling int4 columns behind a 2⁶³ boundary guard (Confidence 5/10 — noted, not changed) Clean Categories
Learnings
Summary
🤖 Generated with Claude Code |
|
|
Resolve by merging |
|
🚧 Alembic head check could not run — this PR conflicts with
Merge Advisory — this check does not block merge. · head_sha: |
merge-train: not on this train — rides the next oneThe diagnosis is right and both migration tracks are genuinely present and tested. Two things to fix first. 1. Live #2068 Alembic fork —
|
…-bigint # Conflicts: # docs/memory/learnings.md
… nothing — the rebuild's justification was false Two merge-train findings on #2805: 1. dev's head moved to `0062_execution_fan_out_task_id` (#2532) after this branched, and this revision declared the same parent — a live #2068 fork, two heads, `upgrade head` applying zero revisions. Re-chained as `0063_agent_sync_state_git_dir_bytes_bigint` off `0062_execution_fan_out_task_id`; `check_alembic_heads.py` on the merged tree: 64 revisions, 1 head. 2. The SQLite rename-swap rebuild was justified by "the schema-parity suite would go red forever" — disproved by a one-line negative control (registration removed, suite still green): both parity fixtures build from empty, so `init_schema` creates the table in both snapshots and the guard cannot see this column's declared type. A boot-time DROP TABLE of a live table for a CI benefit that does not exist is the wrong trade. The rebuild, its column list and its three tests are gone; a note beside `_migrate_agent_sync_state_git_dir_bytes` says why the SQLite track deliberately carries nothing, a test pins that it stays that way for a reason, and the learnings entry now states the honest lesson: run the negative control before writing a guard's behaviour into a durable file. The seven sibling columns with the same int4-vs-`< 2**63` mismatch are filed as #2827. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
|
Both addressed in 1. Alembic fork — re-chained: 2. The SQLite rebuild — dropped, not re-justified. You were right that the parity claim was false, and once it is false there is no reason left for a boot-time Follow-up filed: #2827 — the seven sibling columns of the same upsert ( Body now says |
…ords its mutation (#2829) Three of five ejections on the 2026-09-15 merge train — the third train running — were tests that prove the code was written rather than that it runs: source-text regexes over the module under test (#2811), a bound check at the one value where both bounds coincide (#2817), a docstring claim about CI never negative-controlled (#2805). All green. - docs/testing/STRATEGY.md: a new "Evidence bar for a test" section beside the harness bar — the three spellings, the two greps (the live-consumer grep is the one that decides), guard-vs-source-only with the train's own pair (#2819 kept, #2811 ejected, same shape), mutation as the fix standard, bound tests away from the coincidence — each with what enforces it. - .github/pull_request_template.md: a Testing checkbox for "every new test executes the changed path" and a `Mutation:` line naming the test(s) that go red with the fix reverted ("n/a — not a fix" otherwise). The trailing space after the colon matches the existing `Journey Impact:` line — a fill-in prompt. - docs/memory/learnings.md: the class, with the prior occurrences. The skill half — /review Step 2.5 and /validate-pr §5.4 answered first and in writing, /implement's two done-criteria — is trinity-dev#29. Fixes #2829 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
…iling (#2827) `_coerce_nonneg_int` admitted anything up to INT8_MAX for all eight int columns, but only `git_dir_bytes` is BIGINT on PostgreSQL (#2800). The other seven are int4, so a value the boundary admitted but the column could not hold made the whole upsert raise NumericValueOutOfRange and the agent's sync health went dark. The four ahead/behind counters were not coerced at all. - default ceiling is now INT4_MAX; `git_dir_bytes` and the lock-report ints opt into INT8_MAX explicitly - `_coerce_counter` bounds ahead_main/behind_main/ahead_working/ behind_working (and the legacy ahead/behind keys) the same way Tests drive INT4_MAX+1 through `_sync_agent` per column (rejected → NULL/0, BIGINT admitted), pin that INT4_MAX itself lands, and round-trip each column's admitted maximum through SyncStateOperations on both backends. Mutation (default ceiling back to INT8, counter left raw) → 3 red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
|
Pushed 2d711a4 — folds #2827 into this PR (body + title updated, Widening Mutation evidence: default ceiling back to Also corrected the stale |
/review ReportBranch:
Critical FindingsNone. Informational Findings[I1] Silent rejection at the boundary (Confidence: 7/10) [I2] Body wording (Confidence: 9/10) [I3] The parity guard is blind in this direction (Confidence: 9/10) Clean Categories
Backward-compatibility — tested locally, real boot path both tracksMethod: a worktree at the merge-base (pre-fix code) builds the DB through the real SQLite (
PostgreSQL (postgres:16, throwaway DBs)
Suites with Summary
|
…ct, and says so when it cannot (#2828) (#2832) * fix(ci): the Alembic head watch evaluates through an unrelated conflict, and says so when it cannot (#2828) On 2026-09-15 the watch fired correctly and answered "conflicts with dev — head check not evaluated" for four of nine open PRs, two of which carried a live two-heads fork. All four conflicts were `learnings.md` append collisions. A watcher that stops on ANY conflict is absent on exactly the busy days a fork is likeliest; both forks reached the train green. - The conflict arm now asks WHERE. `git merge-tree --write-tree` writes the merged tree on exit 1 too, and lists the conflicted paths on its own stdout; only a conflict under `src/backend/(enterprise/backend/)?migrations/ versions/` is a `conflict`. Anything else is evaluated on the real three-way merge of the version directories, with the unrelated paths carried onto the verdict as `conflictsElsewhere`. - `alembic-head-verdict.js`: `clean`/`fork` name the unrelated files on the status description and the sticky (capped at 20, fenced); `conflict` — now only a revision file edited on both sides — publishes a visible `error` status plus a sticky that names the file, instead of a comment alone. #2029's rule is against a false `success`; it never argued for silence, and silence is how two forks rode to the train. Proven against the real case, not the YAML: the evaluate step extracted and run locally against origin/dev + #2805's pre-fix head → `fork` with `learnings.md` named (was `unknown`); the fixed head → `clean`; a synthetic both-sides revision edit → `conflict`. Shape pins and executed verdict tests updated in test_2533; the "a conflict publishes no status" pin is re-anchored with the reason. Not here, by scope: per-PR learnings fragments (the collision's own fix) and a merge-time check (direction 3) — both named on the issue. Fixes #2828 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf * merge-train: C-quoted paths hit the version-line anchor; the unknown-arm guard is pinned (#2832) — mechanical, per the merge-train note on the PR - `git -c core.quotePath=off merge-tree …` plus `^"?` in VERSION_LINES: a revision path git C-quotes (non-ASCII, `"`, `\`, control byte) no longer slips past the anchor into the *elsewhere* class, where the guard would run over a marker-bearing file that check_alembic_heads.py omits as unparseable and read PASS. - The `[ -z "$evaluable_conflict" ]` guard on the unknown arm — the one line the #2828 fix turns on — is now asserted; deleting it fails test_an_unrelated_conflict_no_longer_stops_the_evaluation (mutation-checked). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…earnings.md keep-both, mechanical
|
merge-train: merged |
…ords its mutation (#2829) (#2833) Three of five ejections on the 2026-09-15 merge train — the third train running — were tests that prove the code was written rather than that it runs: source-text regexes over the module under test (#2811), a bound check at the one value where both bounds coincide (#2817), a docstring claim about CI never negative-controlled (#2805). All green. - docs/testing/STRATEGY.md: a new "Evidence bar for a test" section beside the harness bar — the three spellings, the two greps (the live-consumer grep is the one that decides), guard-vs-source-only with the train's own pair (#2819 kept, #2811 ejected, same shape), mutation as the fix standard, bound tests away from the coincidence — each with what enforces it. - .github/pull_request_template.md: a Testing checkbox for "every new test executes the changed path" and a `Mutation:` line naming the test(s) that go red with the fix reverted ("n/a — not a fix" otherwise). The trailing space after the colon matches the existing `Journey Impact:` line — a fill-in prompt. - docs/memory/learnings.md: the class, with the prior occurrences. The skill half — /review Step 2.5 and /validate-pr §5.4 answered first and in writing, /implement's two done-criteria — is trinity-dev#29. Fixes #2829 Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Eugene Vyborov <1073874+vybe@users.noreply.github.com>
Fixes #2800
Fixes #2827
Problem
agent_sync_state.git_dir_byteswas declaredINTEGERfor PostgreSQL (tables.pyInteger+ Alembic0019), i.e. int4 with a 2 GiB ceiling. The column exists to observe workspace-repo bloat (#1596), so the values it records are exactly the ones that overflow: any agent whose.gitpasses 2,147,483,647 bytes made everySyncHealthServiceupsert raisepsycopg2.errors.NumericValueOutOfRange: integer out of range. SQLite is 64-bit, so the default backend never showed it.Reproduced pre-fix against a disposable
postgres:16-alpine:Fix (dual-track, Invariant #9)
db/schema.pygit_dir_bytes BIGINT— single source of truth;init_schema_postgrestranslates the same string, so fresh PG gets int8 via0001_baselinedb/tables.pyBigInteger0063_agent_sync_state_git_dir_bytes_bigint(off0062_execution_fan_out_task_id, dev's head)ALTER TABLE agent_sync_state ALTER COLUMN git_dir_bytes TYPE BIGINT; downgrade narrows honestly (fails on a >2 GiB row rather than truncating)_migrate_agent_sync_state_git_dir_bytesindb/migrations.pysays why: INTEGER and BIGINT are one 64-bit affinity there, so an upgraded file declaring INTEGER and a fresh one declaring BIGINT store identical values, and the schema-parity suite cannot observe this column's declared type (both fixtures build from empty). The first version shipped a #1160 rename-swap rebuild on the claim that the guard would otherwise go red; a one-line negative control disproved it (2627928).Regression seam (AC #4)
TestGitDirBytesRoundTripis now@pytest.mark.requires_postgres, so the schema-parity PostgreSQL tier (#2434) runs its[postgres]leg. Added:test_git_dir_bytes_is_64_bit_on_postgres— assertsinformation_schemareportsbigint(names the type, not a stack)TestGitDirBytesNeedsNoSqliteMigration— pins that no SQLite entry is registered for the widening, and proves a pre-bug: agent_sync_state.git_dir_bytes is a 32-bit INTEGER on PostgreSQL — .git over 2 GiB breaks the sync-state upsert #2800 file declaringINTEGERstores a 44 GiB valueByte-column audit
agent_shared_files.size_bytesstaysInteger: bounded byMAX_FILE_SIZE_BYTES(50 MB) at its only writer, cannot reach int4's ceiling by construction. No other byte-count columns intables.py.Boundary ceilings (#2827, folded in)
Widening
git_dir_bytesfixed one column, but the boundary that feeds all eight was still wrong in the other direction:_coerce_nonneg_intadmitted anything up toINT8_MAXfor every column, whilepack_count/loose_objects/maintenance_failures/ahead_main/behind_main/ahead_working/behind_workingare int4 — so a value the boundary admitted but the column could not hold made the whole upsert raiseNumericValueOutOfRangeand the agent's sync health went dark (a raisedpack_countalso drops thegit_dir_bytesreading beside it). The four ahead/behind counters went in uncoerced.INT4_MAX;git_dir_bytesand the lock-report ints opt intoINT8_MAXexplicitly._coerce_counterbounds the four ahead/behind counters (and the legacyahead/behindkeys) the same way — rejected →0, the column default.INT4_MAX + 1through_sync_agentper column (int4 → NULL/0, BIGINT admitted), pin thatINT4_MAXitself lands, and round-trip each column's admitted maximum throughSyncStateOperationson both backends (requires_postgresleg is the proof boundary and column agree).INT8_MAX+ahead_mainleft raw → 3 red (test_every_column_is_bounded_by_its_own_postgres_type,test_the_legacy_ahead_key_is_coerced_too,test_values).Verification
pytest unit/test_1595_sync_health_signals.py unit/test_1596_git_sync_observability.py unit/test_2742_sync_health_leader_lock.py unit/test_sync_health_service.py unit/test_73_sync_health_bulk.py→ 94 passed, 1 skippedpytest tests/unit/test_1596_git_sync_observability.py tests/unit/test_schema_parity.py tests/unit/test_alembic_parity_guard.py tests/unit/test_alembic_revision_id_length.pywithTEST_POSTGRES_URL→ 104 passed, 1 skipped (skip = sqlite leg of the PG-only type assertion)pytest tests/unit -m requires_postgreswithTEST_POSTGRES_URL→ 32 passed, 2 skippedpytest tests/unit -k "migration or schema or sync_state or sync_health or 1596 or 389 or alembic"→ 362 passed, 5 skippedupgrade 0062→ force column toINTEGER→upgrade head→information_schema.data_type = bigint,INSERT … 47244640256lands;alembic current=0063 (head)scripts/ci/check_alembic_heads.py→ 64 revisions, 1 headscripts/ci/check_alembic_parity.py origin/dev HEAD→ PASS🤖 Generated with Claude Code