Skip to content

feat(metrics): a corrected value restates its metric point (abilityai/trinity-enterprise#729) - #3169

Merged
vybe merged 2 commits into
devfrom
feature/729-metric-restatement
Oct 1, 2026
Merged

vybe merged 2 commits into
devfrom
feature/729-metric-restatement

Conversation

@webmixgamer

@webmixgamer webmixgamer commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Ruling R45 ("Accept corrections"). When record_metrics gets a point whose (metric, ts, dims) already exists but whose value differs, it now updates that row in place instead of dropping the new value. An identical value is still a dedup and writes nothing.
  • What a correction changes. revision goes up by 1. recorded_at and execution_id follow the correcting write. ts (the period), created_at (what the daily cap counts) and dims never move. Freshness and every read are unchanged: the read path selects explicit columns and computes freshness from ts.
  • The receipt gains corrected beside recorded and deduplicated, on both the REST 201 and the MCP record_metrics result. All contract text that taught "a correction is a new ts" is amended.

Changes

  • src/backend/db/metric_points.py
    • The insert becomes ON CONFLICT … DO UPDATE … WHERE the stored value IS DISTINCT FROM the incoming one. That comparison is null-safe.
    • RETURNING revision gives a portable three-way count, returned as a PointWriteCounts NamedTuple.
    • The store refuses two rows with one identity in a single call. Postgres raises an error on that and SQLite would apply both, so the two databases would disagree.
    • Rows are written in (ts, key) order, so two overlapping batches can't deadlock on Postgres.
  • Schema (both tracks): revision BIGINT NOT NULL DEFAULT 0 and recorded_at TEXT. recorded_at is nullable with no backfill; NULL means "written before ent#729".
    • SQLite: db/migrations.py::metric_points_restatement.
    • Postgres: Alembic 0086_metric_points_restatement, off 0085_ent720_email_identity.
    • DDL in schema.py and tables.py.
  • models.py::MetricPointsResult.corrected defaults to 0, so idempotency snapshots stored before this change still replay.
  • routers/metric_points.py
    • The counts are read outside the store try, so a shape error can't be answered as a retryable 503.
    • One INFO log line per batch that corrected anything. It logs the count only, never a value or a dimension.
  • MCP (src/mcp-server/src/tools/metrics.ts, client.ts)
    • The tool description teaches the correction rule.
    • The result carries corrected: result.corrected ?? 0, so an older backend reads as 0 (it can never correct).
  • Docs
    • Requirements: §48.2, §48.3 and §48.6, plus a new §48.9 that states the limits: last write wins by arrival order, the replaced value isn't kept, and an identical earlier batch replays over a later correction.
    • Agent guide and user docs.
    • Architecture: the database, api-endpoints and mcp-server entries.
    • The custom-metrics feature flow.
    • A learnings fragment and the /cso --diff report (0 findings).

Test Plan

  • New tests: cd tests/unit && pytest test_ent729_metric_restatement.py -v. They use real rows on SQLite. Under requires_postgres they also run on real PostgreSQL 16, including the Alembic upgrade 0085 → head over an existing row. Coverage:
    • restating a row in place, and an identical repeat that writes nothing;
    • a mixed batch with deliberately uneven counts;
    • a value that moves between the numeric and text columns;
    • execution_id following the correcting write;
    • the duplicate-identity guard and the write order;
    • the migration and rollback safety;
    • the daily cap left unmoved;
    • record, age, correct, and check that stale hasn't moved, through the real read_agent_metrics;
    • read parity: a corrected store reads exactly like a born-correct one, through get_metrics' composition and the objective join's latest_by_metric.
  • Route and MCP receipt tests: test_ent478_record_route.py and src/mcp-server/src/tools/metrics.test.ts. They cover corrected in the 201, an old snapshot replaying, the documented replay limit, and an older backend reading as 0.
  • Mutations: each of the nine lines that applies the rule turns a test red when broken. The lines are the WHERE clause, revision + 1, the sort, the duplicate guard, recorded_at, execution_id, created_at, the null-safe comparison, and the route's corrected.
  • Review round (30b73184f), from an independent backend / test-quality / contract review:
    • The migration test runs the registered MIGRATIONS entry; deleting the registration used to pass every suite.
    • Five more mutants now turn a test red.
    • A store-shape refusal or an unreadable store result is a non-retryable 500 that releases the idempotency claim.
    • The tool description now covers the in-turn replay limit.
  • Existing ent#478 store, route and validation suites updated for the three counts; the schema-parity, Alembic-parity, Alembic-heads and auth guards pass unchanged.
  • CI Postgres tier (pytest tests/unit -m requires_postgres against real PostgreSQL): 60 passed. The 6 skips are environmental and predate this branch.
  • /verify-local --skip-agent:
    • The backend image builds and import main works inside it.
    • The sibling stack boots healthy on a fresh database, with every migration applied.
    • Integration: 70 passed, 0 failed.
    • In the full unit run, the only failures were the known order-dependent ent666/ent477/retention_floor set. Those files pass on their own.
  • Manual check on a live dev stack:
    • On dev, a correction came back deduplicated: 1. On this branch it came back corrected: 1, and an identical repeat came back deduplicated: 1.
    • The row shows revision 1, with recorded_at newer than created_at and ts unchanged. The tile shows the new value.
    • Back on dev, the old code inserts fine with the new columns present.

Migration numbering: 0086 is also claimed by #3145, #3021 and #3022, and #2984 holds 0087. Whichever merges second needs to re-parent. alembic-head-watch flags a fork.

Fixes abilityai/trinity-enterprise#729

🤖 Generated with Claude Code

webmixgamer and others added 2 commits October 1, 2026 16:58
…/trinity-enterprise#729)

Tandem R45 ("Accept corrections"): a record_metrics point that repeats an
existing (metric, ts, dims) with a DIFFERENT value now updates the stored row
instead of being silently dropped as a duplicate. An identical value is still
a dedup that writes nothing.

- db/metric_points.insert_points: ON CONFLICT DO UPDATE ... WHERE the value IS
  DISTINCT FROM the stored one; value, execution_id, recorded_at and
  revision + 1 follow the correcting write; ts, created_at and dims never move.
  RETURNING revision gives a portable three-way count (PointWriteCounts:
  recorded / deduplicated / corrected). The store refuses two rows with one
  identity (PG raises, SQLite would apply both) and writes in (ts, key) order
  so overlapping batches cannot deadlock on PostgreSQL.
- Schema, both tracks: revision BIGINT NOT NULL DEFAULT 0 and recorded_at TEXT
  (nullable, no backfill: NULL = written before ent#729). SQLite migration
  metric_points_restatement + Alembic 0086_metric_points_restatement.
- The 201 MetricPointsResult and the MCP record_metrics result gain
  `corrected` (default 0 so stored idempotency snapshots still replay; `?? 0`
  against an older backend). One INFO line per correcting batch, counts only.
- Freshness, the daily cap and every read are unchanged: last_point_at is the
  newest ts, "used today" counts rows created, and get_metrics / the objective
  join read a corrected store exactly like a born-correct one (parity test).
- Contract text amended: service docstring, the tool description, the agent
  guide, user docs, requirements §48.2/§48.3/§48.6 + new §48.9 (incl. the
  stated limits: last write wins, lossy restatement, an identical earlier batch
  replays over a correction), architecture database/api/mcp entries and the
  custom-metrics flow.

Tests: tests/unit/test_ent729_metric_restatement.py (real rows, SQLite and
requires_postgres PostgreSQL, incl. the Alembic upgrade 0085 -> head over an
existing row), route + MCP receipt tests. Nine mutations of the applying lines
(WHERE, revision + 1, sort, duplicate guard, recorded_at, execution_id,
created_at, null-safety, the route's corrected) each turn a test red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…enterprise#729)

From an independent backend / test-quality / contract review of #3169:

- Route: a store-shape refusal (the store's duplicate-identity ValueError)
  and an unreadable store result both answer the non-retryable 500
  `metric_store_rejected_batch` THROUGH `_reject`, so the batch claim is
  released. Before, the first reached the generic retryable 503 and the
  second escaped `_reject` and wedged the caller's key for 24 h. Both are
  unreachable today; the documented "every non-2xx exit past the claim
  releases it" is true again. Both new route tests fail against the old route.
- Tests: the SQLite migration test now runs the REGISTERED `MIGRATIONS`
  entry (deleting the registration used to pass every suite, schema parity
  included); the dims, daily-cap, write-order and store-read-key tests are
  rebuilt so the five mutants that survived them now go red; the Alembic test
  also asserts `NOT NULL` and `DEFAULT 0`; the rollback-insert test runs on
  PostgreSQL too.
- MCP tool + agent guide: the `execution_id` description states that an
  identical earlier batch in one turn replays over a correction.
- Docs: §48.9 replay-limit wording made exact; private-planning references
  dropped from public text; registry descriptions and the cso report's
  closed coverage gap updated; learnings fragment on testing a migration
  through its registered entry.

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

@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: batch validated on train #3172

@vybe
vybe merged commit 6fead4f into dev Oct 1, 2026
28 of 29 checks passed
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