Skip to content

fix(ci): cap test SQLAlchemy below 2.1 — pg-migrations broke on the new default driver - #3015

Merged
vybe merged 1 commit into
devfrom
fix/ci-pin-sqlalchemy-2.0
Sep 25, 2026
Merged

vybe merged 1 commit into
devfrom
fix/ci-pin-sqlalchemy-2.0

Conversation

@dolho

@dolho dolho commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Verification

sqlalchemy 2.0.54 -> make_url('postgresql://…').get_dialect().driver == 'psycopg2'
sqlalchemy 2.1.0  -> 'psycopg'

Test Plan

  • pg-migrations green on this PR (it triggers on tests/requirements-test.txt)
  • schema-parity green

🤖 Generated with Claude Code

…ew default driver

SQLAlchemy 2.1.0 (released 2026-09-24) resolves a bare postgresql:// URL
to the psycopg (v3) driver. tests/requirements-test.txt carried an
uncapped sqlalchemy>=2.0.0, so every run since resolved 2.1.0 and the
pg-migrations job's alembic upgrade died with
ModuleNotFoundError: No module named 'psycopg'. The backend image pins
2.0.36 + psycopg2-binary, so production was never affected; CI was
running a stack prod never does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@dolho

dolho commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

/review Report

Branch: fix/ci-pin-sqlalchemy-2.0 → dev (merge-base cec2a64be)
Files changed: 1 (+4/−1), tests/requirements-test.txt
Scope: CLEAN. The PR sets out to cap the test env's SQLAlchemy below 2.1 so pg-migrations stops resolving the psycopg v3 driver, and that is exactly what the diff does.
Plan completion: no issue plan (CI hotfix). The PR test plan has 2 items: schema-parity is ✅ green; pg-migrations is ⚠️ not run by CI on this PR (see I1), so I verified it locally below.

Execution coverage

changed symbol executed by live consumer verdict
sqlalchemy>=2.0.0,<2.1 every CI job that runs pip install -r tests/requirements-test.txt (pytest head/base, schema-parity, pg-migrations) .github/workflows/pg-migrations.yml:74 and the pytest/schema-parity install steps ✅ executed

Fix mutation: I reproduced the pg-migrations job locally, step for step: postgres:16-alpine, the narrow VARCHAR(32) alembic_version seed, the same pip install lines, and alembic upgrade head from src/backend.

tree resolved SQLAlchemy alembic upgrade head result
dev (no cap) 2.1.0 exit 1, ModuleNotFoundError: No module named 'psycopg' 🔴
PR head (capped) 2.0.54 exit 0 ✅ head 0074_role_readiness_rollout_seed, version_num width 255

Critical findings

None.

Informational findings

[I1] Test gap: pg-migrations doesn't trigger on this file, so CI never proved this PR (Confidence: 9/10)
File: .github/workflows/pg-migrations.yml:18-28

  pull_request:
    paths:
      - 'src/backend/migrations/**'
      - 'src/backend/db/**'
      - 'src/backend/alembic.ini'
      - '.github/workflows/pg-migrations.yml'

[I2] The production path still relies on SQLAlchemy's default driver for a bare postgresql:// URL (Confidence: 7/10)
File: src/backend/db/engine.py (URL taken as-is from DATABASE_URL; no drivername normalisation)

  • The image is safe today: docker/backend/Dockerfile:79 pins sqlalchemy==2.0.36 and ships psycopg2-binary.
  • But a future bump of that pin to 2.x ≥ 2.1 would reproduce this exact failure at production boot on every PostgreSQL install. The CI cap here would then hide the problem rather than catch it.
  • Suggestion (follow-up, not this PR): name the driver explicitly. For example, normalise postgresql:// → postgresql+psycopg2:// in db/engine.py and migrations/env.py. That makes the driver a code decision instead of a library default.

Clean categories

  • SQL / data safety, concurrency, auth, credentials: the diff is a single dependency constraint plus a comment. No code paths.
  • Enterprise disclosure: no docs/ changes.
  • Version consistency: the test env (2.0.54) and the image (2.0.36) are both 2.0.x on psycopg2. The comment's claim that the image pins 2.0.36 is accurate (docker/backend/Dockerfile:79).
  • Blast radius: the other jobs that install this file (pytest head/base on SQLite) are green on this PR at 2.0.54.

Summary

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge-train: validated (lane A, one-line test-env cap; green incl. regression diff; #3020's green pg-migrations run carries this cap). Merged alone ahead of the train because every migration-bearing PR's pg-migrations/schema-parity is red on SQLAlchemy 2.1 until it lands.

@vybe
vybe merged commit 4766851 into dev Sep 25, 2026
21 checks passed
vybe pushed a commit to harimaruthachalam/trinity that referenced this pull request Sep 29, 2026
…nity-enterprise#705)

Create-time kind: agent (default) | deployment, on POST /api/agents and
MCP create_agent. An explicit source_mode always wins.

An agent gets a working branch + auto_sync_enabled + freeze-on-sync-
failure, but only when the Abilityai#2107 push probe says its token can push to
that repo. A deployment, an ephemeral ghost, a tokenless create, a
refused or unverifiable probe all stay pull-only, with the reason on the
create response's git_mode (never on the /ws broadcast). A template
someone else owns therefore never receives an agent's branches (the
ent#162 class); Cornelius, built from a shared public upstream, is
pinned pull-only. Fork-to-own gets the trio.

Existing agents are not flipped. Operator runbook for migrating a live
agent: docs/migrations/AGENT_WORKING_BRANCH_DEFAULT_2026-09.md.

Stacked on Abilityai#3016, Abilityai#3017, Abilityai#3018 (+Abilityai#3015): merges only after them.

Related to Abilityai/trinity-enterprise#705

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
vybe pushed a commit that referenced this pull request Oct 2, 2026
…rise#703) (#3021)

* feat(git-sync): the container pulls origin on its own (trinity-enterprise#703)

Invariant G3: human and fleet work reaches the agent within a bound.
Nothing inside the container ever pulled; the 2026-09-24 audit found
agents up to 31 commits behind.

- agent server: a pull loop beside the push loop, gated per cycle on
  the owner's pull_sync_enabled (read live, GIT_SYNC_PULL fallback),
  interval GIT_SYNC_PULL_INTERVAL_SECONDS (defaults to the push one).
  Under _REPO_LOCK, never while an execution runs (checked again right
  before the tree is touched). Fast-forward, or rebase aborted on
  conflict. Uncommitted edits are stashed explicitly and a pull that
  collides with them is undone - not --autostash, which strands them
  in the stash while reporting success.
- pull_sync_enabled + last_pull_at/_status/behind_after_pull on both
  migration tracks; backfill on only where auto-sync is on; new github
  agents get it at creation (source mode included), GIT_SYNC_PULL env
  derived from the DB flag alone at recreate.
- GET/PUT /api/agents/{name}/git/pull-sync; sync-health persists the
  pull outcome (bounded); Settings -> Git sync gains the toggle.

Stacked on #3020 (+#3015-#3018): merges after it.

Related to Abilityai/trinity-enterprise#703

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(schema): keep the FK strip working on agent_git_config for PostgreSQL (trinity-enterprise#703)

The inline comment on `pull_sync_enabled` sat between the comma and the
FOREIGN KEY clause. `_PG_TABLE_SUBS` strips that clause only when it directly
follows the comma, so the clause survived without its REFERENCES and
PostgreSQL rejected the table ("syntax error at or near ')'"), failing 18
requires_postgres tests in schema-parity. The comment moves above the column.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(git-sync): pull cycle never strands local work, counts queued turns, brings main to working branches (trinity-enterprise#703)

PR #3021 review:
- _with_stash wraps every post-stash step: a git child that times out
  (run_registered raises TimeoutExpired) goes back to the pre-pull HEAD
  and re-applies the stash; when that is impossible the error says the
  edits are kept in `git stash`.
- reset --hard's return code is checked. A failed undo stops with an
  error naming it instead of leaving UU files behind silently.
- Both cycles refuse to run over unmerged paths. The push cycle checks
  before `git add -A`, which would stage conflict markers and push them
  to origin as a successful sync.
- The execution gate counts register_pending entries (#2433), so a turn
  queued on the chat lock or the headless pool is busy, not idle.
- The pull reaps stale lock litter first, as the push cycle does; a
  pull-only agent has no push cycle to do it.
- sync-state gains last_pull_error streak fields: consecutive pull
  failures / skips and last_successful_pull_at.

Ruling on the intent question: a trinity/* working branch only ever
pulled itself, so human pushes to main never arrived (G3). The cycle
now also merges origin/main (via _get_pull_branch) into the working
branch — a merge, not a rebase, since the branch is already pushed; a
conflict is aborted and recorded.

Nine real-repo tests, all red before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(git-sync): persist pull health; renumber to 0080_pull_sync; docs + UI copy (trinity-enterprise#703)

- Alembic 0076_pull_sync -> 0080_pull_sync <- 0079_telegram_group_context
  (one head, 81 revisions); SQLite entry re-appended after dev's.
- The same migration pair adds agent_sync_state.last_pull_error,
  last_successful_pull_at, consecutive_pull_failures and
  consecutive_pull_skips, persisted by the sync-health poller (bounded,
  coerced; a success clears the error). ent#706/#707 read these, so they
  land here rather than as a second migration on the same table.
- agent-runtime.md says three loops; the flow and requirement describe the
  main merge, the queued-turn gate, the unmerged-path refusal, what
  behind_after_pull measures, and that auto-sync agents already rebase in
  the push cycle.
- Settings copy no longer hard-codes 15 minutes
  (GIT_SYNC_PULL_INTERVAL_SECONDS) and no longer claims the push cycle
  never touches the tree while the agent works.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(git-sync): pull records merge conflicts as conflicts; rebases keep merges (trinity-enterprise#703)

PR #3021 re-review:
- A conflict merging main was recorded as "Auto-merging <file>": git
  prints it on stdout with an empty stderr. Unmerged paths are read
  before the abort; the error is "diverged: merge conflict with main
  (<files>)", and a failed merge --abort is named.
- `git rebase` flattened the merge the pull made into copies of main's
  commits; both cycles now rebase with --rebase-merges.
- A failed strict ahead/behind count fails the pull instead of reading
  as up to date.
- A timed-out step is reset to the pre-pull HEAD even with nothing
  stashed; a reset blocked by the killed child's index.lock says so.
- "Never runs while the agent is working" is check-then-act: the copy
  and docs now say "never starts" and document the admission window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(git-sync): pull-sync write is person-only; classify the agent's read

#2996's route census (merged to dev) requires every new route to be
classified. The agent's pull loop reads GET .../git/pull-sync with its
own key each cycle (AGENT_CALLABLE); the PUT is a setting write, so it
takes Depends(require_person) like the other #2996 settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(git-sync): the push and pull cycles share the repo lock without starving each other (#3021 review)

The two background loops started together on one interval and took
_REPO_LOCK non-blocking, so the loser skipped silently every tick and
the effective pull bound could quietly double. Background cycles now
wait up to GIT_SYNC_LOCK_WAIT_SECONDS (default 120 s) for the lock, and
the pull loop's first tick is half an interval after the push loop's.
Operator endpoints keep their immediate 409.

The pull's reset --hard undo re-checks that no execution started
meanwhile; if one did, the tree is left as it is (conflict markers block
the next push and pull) instead of discarding the turn's writes.
merge --abort runs only when MERGE_HEAD exists, so a merge that refused
to start no longer records "There is no merge to abort".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* merge-train: narrow the "never discards local work" claims to what _safe_to_reset guarantees (#3021) — mechanical, per the merge-train note on the PR

The undo paths no longer reset over a registered execution, but writes outside
the process registry (Files API, docker exec, web terminal) made during the
integrate window are not protected, as the author's e6cb8b5 note concedes.
Wording only: two git.py docstrings, the auto_sync.py comment,
requirements/github.md and the GitSyncSettingsPanel.vue copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Andrii Pasternak <44377756+AndriiPasternak31@users.noreply.github.com>
Co-authored-by: trinity-ability <309458136+trinity-ability@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants