feat(metrics): a corrected value restates its metric point (abilityai/trinity-enterprise#729) - #3169
Merged
Merged
Conversation
…/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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
record_metricsgets 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.revisiongoes up by 1.recorded_atandexecution_idfollow 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 fromts.correctedbesiderecordedanddeduplicated, on both the REST 201 and the MCPrecord_metricsresult. All contract text that taught "a correction is a newts" is amended.Changes
src/backend/db/metric_points.pyON CONFLICT … DO UPDATE … WHEREthe stored valueIS DISTINCT FROMthe incoming one. That comparison is null-safe.RETURNING revisiongives a portable three-way count, returned as aPointWriteCountsNamedTuple.(ts, key)order, so two overlapping batches can't deadlock on Postgres.revision BIGINT NOT NULL DEFAULT 0andrecorded_at TEXT.recorded_atis nullable with no backfill; NULL means "written before ent#729".db/migrations.py::metric_points_restatement.0086_metric_points_restatement, off0085_ent720_email_identity.schema.pyandtables.py.models.py::MetricPointsResult.correcteddefaults to 0, so idempotency snapshots stored before this change still replay.routers/metric_points.pytry, so a shape error can't be answered as a retryable 503.src/mcp-server/src/tools/metrics.ts,client.ts)corrected: result.corrected ?? 0, so an older backend reads as 0 (it can never correct).database,api-endpointsandmcp-serverentries./cso --diffreport (0 findings).Test Plan
cd tests/unit && pytest test_ent729_metric_restatement.py -v. They use real rows on SQLite. Underrequires_postgresthey also run on real PostgreSQL 16, including the Alembic upgrade 0085 → head over an existing row. Coverage:execution_idfollowing the correcting write;stalehasn't moved, through the realread_agent_metrics;get_metrics' composition and the objective join'slatest_by_metric.test_ent478_record_route.pyandsrc/mcp-server/src/tools/metrics.test.ts. They covercorrectedin the 201, an old snapshot replaying, the documented replay limit, and an older backend reading as 0.revision + 1, the sort, the duplicate guard,recorded_at,execution_id,created_at, the null-safe comparison, and the route'scorrected.30b73184f), from an independent backend / test-quality / contract review:MIGRATIONSentry; deleting the registration used to pass every suite.pytest tests/unit -m requires_postgresagainst real PostgreSQL): 60 passed. The 6 skips are environmental and predate this branch./verify-local --skip-agent:import mainworks inside it.dev, a correction came backdeduplicated: 1. On this branch it came backcorrected: 1, and an identical repeat came backdeduplicated: 1.revision 1, withrecorded_atnewer thancreated_atandtsunchanged. The tile shows the new value.dev, the old code inserts fine with the new columns present.Migration numbering:
0086is also claimed by #3145, #3021 and #3022, and #2984 holds0087. Whichever merges second needs to re-parent.alembic-head-watchflags a fork.Fixes abilityai/trinity-enterprise#729
🤖 Generated with Claude Code