Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions docs/releases/0.7.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# v0.7.0 — PostgreSQL goes to production

**Released:** 2026-06-23 · **Tag:** `v0.7.0` · **Previous:** `v0.6.1`
**Diff:** [v0.6.1...v0.7.0](https://github.com/abilityai/trinity/compare/v0.6.1...v0.7.0)

The headline of this release: **Trinity now runs on PostgreSQL as the recommended
production database.** SQLite stays the zero-config default for local development
and evaluation, but production instances should move to Postgres. This release adds
the configurable backend, Alembic-managed Postgres migrations, a dated SQLite
end-of-support path, and a gated SQLite→Postgres migration flow shipped in the
Trinity Ops Agent.

Alongside Postgres, v0.7.0 lands a pluggable **Codex harness** beside Claude Code,
a body of **execution-correctness** work (fire-and-forget dispatch, status-as-projection),
agent-server inbound-auth hardening, and credential **hot-reload** rotation.

---

## 🐘 PostgreSQL — the theme

- **#300** Configurable database backend — SQLAlchemy Core abstraction over **SQLite + PostgreSQL**. Set a single `DATABASE_URL` and both the backend and the scheduler switch over. Selection is **non-sticky and non-destructive**: comment the variable out and you're back on SQLite on the next restart.
- **#1183** Adopted **Alembic** for PostgreSQL migrations (dual-track alongside the SQLite bespoke runner; a schema change lands in both until SQLite is retired).
- **#1278** **SQLite end-of-support dated 2026-09-01** — PostgreSQL is the forward path for production, with migration notes in the docs.

**Migrating an existing SQLite instance?** Use the **Trinity Ops Agent**'s
`/migrate-to-postgres` skill ([abilityai/trinity-ops-public](https://github.com/abilityai/trinity-ops-public)) —
a gated *validate-then-cutover* flow that stands up a parallel Postgres container,
copies and validates your data, then cuts over in a short downtime window. Your
SQLite file is **never written**, so rollback is always one line.

- Stand up a **new** instance on Postgres → [`docs/POSTGRESQL_SETUP.md`](../POSTGRESQL_SETUP.md)
- **Migrate** an existing SQLite instance → Ops Agent `/migrate-to-postgres`

```bash
# Opt in to the bundled PostgreSQL container
DATABASE_URL=postgresql://trinity:your-password@postgres:5432/trinity
docker compose --profile postgres up -d
```

---

## Features

- **#1187** Codex harness MVP — pluggable agentic execution engine alongside Claude Code
- **#1169** Agent runtime data volumes — declared `data_paths` with snapshot/restore and portable export
- **#1089** Credential rotation via hot-reload, not container recreate
- **#668** Agent deployment compatibility validation — server-side checks with auto-fix offers
- **#1115** Per-schedule performance scorecards on Agent Detail (Overview + Schedules tab)
- **#1116** In-app bug reporting from the floating Help widget (hosted intake → GitHub issues)
- **#1104** Respond to / resolve Operator Queue items over MCP (#1101 follow-up)
- **#1315** WhatsApp outbound media attachments — deliver `ChannelResponse.files` via Twilio MediaUrl
- **#82** Email verification for admin login (email code + password)
- **#679** Plumb cancel signal into the agent task-runner reply (#671 defense-in-depth)
- **#1095** Transactional agent executions — discard workspace changes unless validated as success (research-gated)
- **#941** Enterprise: audit log dashboard — admin viewer for compliance review (v1: list/filter/detail) + entitlement seam
- **trinity-enterprise#5** _(enterprise)_ Two-factor authentication (2FA) via TOTP

## Fixes

- **#1159** 🔒 agent-server HTTP API was unauthenticated on the shared agent network (cross-agent credential theft) — now a per-agent HMAC `X-Trinity-Agent-Token`
- **#1160** Migration runner — close the DROP-rebuild data-loss window + add cross-process serialization
- **#1201** Agent-side timeout (504) no longer drops execution cost/context/tool-call telemetry
- **#858** First-time setup token silently lost — block-buffered `print()` broke fresh installs
- **#1165** First-time setup token was per-worker — prod (`--workers 2`) onboarding still ~50% flaky after #858
- **#1199** `GET /api/ops/auth-report` 500 — SQLAlchemy auto-correlation (v0.6.1 regression)
- **#1200** `GET/PUT /api/agents/{name}/capabilities` 500 — facade delegation (v0.6.1 regression)
- **#1267** Boot-time NameError in lifespan transport startup (misleading Telegram/WhatsApp error on every boot)
- **#1264** Per-agent GitHub PAT never propagated to an existing container — all pushes failed
- **#1265** Dashboard & timeline took 20s+ to load metrics with 10+ agents
- **#1022** Scheduler wrote `status='failed'` with empty error on a 30s dispatch timeout
- **#799** SUB-003 auto-switch had no per-agent lock — concurrent 429s raced the restart
- **#1197** Agent creation crashed with an opaque ValueError on non-integer CPU in template resources
- **#1230** Backend Docker healthcheck flapped to "unhealthy" under batch load (10s timeout too tight for 2 workers)
- **#1231** Agent `/tmp` tmpfs filled and silently broke autonomous git commits — size now configurable
- **#1237** CI: path-filtered required checks bricked unrelated PRs
- **#1260** Fixed test-suite bugs surfaced by the full integration run (fixtures, stale mocks, isolation, flakes)
- **#722** Config validation: 5 critical config issues
- **#767** CB probe executions left open until backend restart inflated failure duration on the timeline
- **#953** Freshly deployed agents reported `M .gitignore` against `origin/main` (startup.sh append)
- **#954** Agent Detail panel width jerked when switching to/from the Chat or Session tab (scrollbar gutter)
- **#957** "Failed to generate avatar" now classifies image-gen failures with actionable detail
- **#958** Build Info dialog displayed "unknown" for every field in local development
- **#960** Visual artifact on the Agents list row hover in light theme

## Refactors

- **#1083** Fire-and-forget dispatch — a hung turn holds zero backend resource
- **#1082** status-as-projection — `schedule_executions.status` is never read as authoritative for "is running"
- **#1025** Harden headless drain/finalize — daemon-thread exception capture + finalize snapshot isolation
- **#1027** Split `db/schedules.py`; replace 15+ param insert signatures with request objects
- **#1088** Unify the failure classifier into one shared package

## Breaking Changes

None.

## Upgrade Notes

- **Database:** PostgreSQL is now the recommended production backend (#300). Opt in with a single `DATABASE_URL`; SQLite remains the default. **SQLite end-of-support is 2026-09-01** (#1278) — migrate with the Ops Agent `/migrate-to-postgres` skill.
- **#1159** introduces `AGENT_AUTH_SECRET` (auto-generated by `start.sh`, like `SECRET_KEY`). Existing agents do one self-reconciling recreate pass to pick up their injected token.
- **#1187** adds the `AGENT_RUNTIME` selector (Codex / Gemini / Claude Code); existing agents default to Claude Code.

## CLI

`trinity-cli` auto-publishes **0.2.7** via the main-push path (license → Apache 2.0
#1192; credential + email-send removed from `trinity init` #1162).

---

**Contributors:** Eugene Vyborov, dolho, andrii.pasternak, vybe, obasilakis,
oleksandr-korin, Pavlo Shulin, Oleksii Dolhov, Alex, dependabot, chrisyangxiaoqi,
webmixgamer.
121 changes: 64 additions & 57 deletions src/backend/db/schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -1934,68 +1934,75 @@ def get_agent_schedules_summary(self, agent_name: str, hours: int) -> Dict:
cap = _PERCENTILE_ROWSET_CAP
FAILED_STATES = (TaskExecutionStatus.FAILED, "error")

with get_db_connection() as conn:
cursor = conn.cursor()

# Schedules (non-deleted)the authoritative row set so a
# zero-run schedule still appears.
cursor.execute(
"""
SELECT id, name, message, cron_expression, enabled
FROM agent_schedules
WHERE agent_name = ? AND deleted_at IS NULL
ORDER BY created_at ASC
""",
(agent_name,),
)
schedule_rows = cursor.fetchall()
# #300/#1115: ported to SQLAlchemy Core so the summary works on both
# SQLite and PostgreSQL (the earlier raw-sqlite path NameError'd on the
# dropped get_db_connection import, and the bare-column-with-MAX last-run
# trick is SQLite-onlyreplaced by a portable ROW_NUMBER window below).
se = schedule_executions.c
sch = agent_schedules.c
base_where = and_(se.agent_name == agent_name, se.started_at > cutoff)

# Schedules (non-deleted) — the authoritative row set so a
# zero-run schedule still appears.
q_sched = (
select(sch.id, sch.name, sch.message, sch.cron_expression, sch.enabled)
.where(and_(sch.agent_name == agent_name, sch.deleted_at.is_(None)))
.order_by(sch.created_at.asc())
)

# One grouped aggregate for every schedule's executions in window.
cursor.execute(
"""
SELECT
schedule_id,
COUNT(*) AS total,
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS success_count,
SUM(CASE WHEN status IN ('failed', 'error') THEN 1 ELSE 0 END) AS failed_count,
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled_count,
SUM(COALESCE(cost, 0)) AS cost_total,
AVG(CASE WHEN duration_ms IS NOT NULL THEN duration_ms END) AS avg_duration_ms,
AVG(CASE WHEN context_used IS NOT NULL THEN context_used END) AS context_avg
FROM schedule_executions
WHERE agent_name = ? AND started_at > ?
GROUP BY schedule_id
""",
(agent_name, cutoff),
# One grouped aggregate for every schedule's executions in window.
# AVG skips NULLs natively, matching the prior CASE-wrapped AVG.
q_agg = (
select(
se.schedule_id,
func.count().label("total"),
func.sum(case((se.status == "success", 1), else_=0)).label("success_count"),
func.sum(case((se.status.in_(("failed", "error")), 1), else_=0)).label("failed_count"),
func.sum(case((se.status == "cancelled", 1), else_=0)).label("cancelled_count"),
func.sum(func.coalesce(se.cost, 0)).label("cost_total"),
func.avg(se.duration_ms).label("avg_duration_ms"),
func.avg(se.context_used).label("context_avg"),
)
agg_by_sched = {r["schedule_id"]: r for r in cursor.fetchall()}
.where(base_where)
.group_by(se.schedule_id)
)

# Last-run outcome per schedule. SQLite's bare-column-with-MAX
# rule returns the row holding the max started_at.
cursor.execute(
"""
SELECT schedule_id, MAX(started_at) AS last_run_at, status AS last_status
FROM schedule_executions
WHERE agent_name = ? AND started_at > ?
GROUP BY schedule_id
""",
(agent_name, cutoff),
# Last-run outcome per schedule — portable ROW_NUMBER window (replaces
# SQLite's bare-column-with-MAX, which errors on PostgreSQL) keeping the
# in-window semantics of the original query.
_last_rn = func.row_number().over(
partition_by=se.schedule_id, order_by=se.started_at.desc()
).label("rn")
_last_subq = (
select(
se.schedule_id,
se.started_at.label("last_run_at"),
se.status.label("last_status"),
_last_rn,
)
last_by_sched = {r["schedule_id"]: r for r in cursor.fetchall()}
.where(base_where)
.subquery()
)
q_last = select(
_last_subq.c.schedule_id,
_last_subq.c.last_run_at,
_last_subq.c.last_status,
).where(_last_subq.c.rn == 1)

# Tool-call totals — bounded JSON parse over newest rows agent-wide
# (cap + 1 to detect sampling), attributed back per schedule.
q_tools = (
select(se.schedule_id, se.tool_calls)
.where(and_(base_where, se.tool_calls.isnot(None)))
.order_by(se.started_at.desc())
.limit(cap + 1)
)

# Tool-call totals — bounded JSON parse over newest rows agent-wide
# (cap + 1 to detect sampling), attributed back per schedule.
cursor.execute(
"""
SELECT schedule_id, tool_calls
FROM schedule_executions
WHERE agent_name = ? AND started_at > ? AND tool_calls IS NOT NULL
ORDER BY started_at DESC
LIMIT ?
""",
(agent_name, cutoff, cap + 1),
)
tool_rows = cursor.fetchall()
with get_engine().connect() as conn:
schedule_rows = conn.execute(q_sched).mappings().all()
agg_by_sched = {r["schedule_id"]: r for r in conn.execute(q_agg).mappings().all()}
last_by_sched = {r["schedule_id"]: r for r in conn.execute(q_last).mappings().all()}
tool_rows = conn.execute(q_tools).mappings().all()

tool_calls_sampled = len(tool_rows) > cap
tool_total_by_sched: Dict[str, int] = defaultdict(int)
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/test_login_rate_limit_split.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ def auth_module(monkeypatch):
config_mod.ACCESS_TOKEN_EXPIRE_MINUTES = 60
config_mod.EMAIL_AUTH_ENABLED = False
config_mod.REDIS_URL = "redis://stub"
# auth.py grew a PUBLIC_ACCESS_REQUESTS_ENABLED dependency (trinity-enterprise#10);
# keep this stub in sync or its import fails under HEAD.
config_mod.PUBLIC_ACCESS_REQUESTS_ENABLED = False
stubs["config"] = config_mod

database_mod = types.ModuleType("database")
Expand Down
Loading