feat(settings): metric point rows in Settings → Retention (abilityai/trinity-enterprise#671) - #3091
webmixgamer wants to merge 8 commits into
Conversation
…trinity-enterprise#671) ent#478 minted metrics_retention_days and metrics_daily_point_cap, reachable through the API and the env tier but not on the Settings page. The panel now shows them beside the sibling windows as "Metric points" (days) and "Metric point quota" (points / agent / day, "0 = unlimited"), in an edition whose managed retention endpoint can save them; in Community the rows are absent and PUT /api/settings/ops/config stays the write path. - GET /api/settings/retention carries quotas.metrics_daily_point_cap = {value, source}. The cap is a write budget, not a window, so windows and sources are unchanged; its value is read the way the record_metrics write boundary enforces it (unparseable -> the default, never 0 = unlimited). - utils/retentionFields.js holds the field list and the rules, so vitest executes them: rows gated on the response's own edition (no pop-in); an env-sourced row is read-only with an "env" badge naming its variable and what unsetting it does, and is never sent; Save sends only the fields the operator changed (it used to PUT every field, freezing untouched code defaults and env values into stored rows), as typed numbers — parseInt turned a 22-digit entry into 1. - A rejected save now renders in an InlineError beside Save; it used to replace the whole panel with one red line and no way back. - Tests: tests/unit/test_ent671_retention_metric_rows.py (quotas in both editions, env/row precedence, parity with the enforcing reader, a call-site pin that the view uses the field module), retentionFields.spec.js, and a route-mocked @smoke e2e (save round-trip, out-of-bounds 422 shown inline, env row read-only and omitted, Community rows absent). The enterprise submodule pointer is NOT bumped here; it moves after the private retention-module change merges. Refs Abilityai/trinity-enterprise#671 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eans an operator set them (Abilityai/trinity-enterprise#671) The Settings → Retention metric rows are read-only when their source is `env`. On a real install the source was ALWAYS env: every backend compose file forwarded `${METRICS_DAILY_POINT_CAP:-100000}` / `:-365` (a non-empty default), and .env.example — which start.sh copies to .env on a fresh install — set both outright. The live backend reports both variables set with no .env line. So both rows would have shipped locked on essentially every install, the #2085 seeder skipped the window for the same reason, and the "unset the variable and restart" advice could not work: removing the line brought back the compose default. - docker-compose.yml / .prod.yml / .hosted.yml forward the three ENV_BACKED_OPS_KEYS (METRICS_RETENTION_DAYS, METRICS_DAILY_POINT_CAP, INTER_AGENT_MAX_CHAIN_DEPTH) as ${VAR:-} — the SECRET_KEY pattern. An empty value is unset to config.env_ops_value; the code default (365 / 100000 / 8, unchanged) is the fallback. Effective values do not move. - .env.example leaves the three commented, documented, with the reason. - tests/unit/test_ent671_env_backed_ops_forwarding.py pins it, keyed off config.ENV_BACKED_OPS_KEYS (a fourth key is covered on the day it lands); red with a compose default restored or an example line uncommented. Upgrade note: an existing .env copied from .env.example after ent#478 still sets the variables, so those rows stay read-only (correctly — the value IS pinned by the environment) until the lines are removed and the backend is recreated. Refs Abilityai/trinity-enterprise#671 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Abilityai/trinity-enterprise#671) 0 findings at the daily gate. No new endpoint; the read gains an admin-only quotas block; the managed write gains two bounded fields, rejects unknown keys and now writes an audit row; compose forwards the env-backed keys empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Abilityai/trinity-enterprise#671) reliability.md: the env tier only means "an operator set it" because compose now forwards the env-backed keys empty and .env.example leaves them commented. platform-settings.md: the first new edit clears "Saved". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lidator rejects (Abilityai/trinity-enterprise#671) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Resolve by running |
|
Resolve by merging |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # docs/memory/feature-flows/agent-custom-metrics.md # tests/registry.json
…c9e7) 29ec9e7 is Abilityai/trinity-enterprise#725 (squash-merged to private main): the managed retention module learns metrics_retention_days and metrics_daily_point_cap, rejects unknown keys and booleans, and audits the managed PUT. This PR's Settings rows save through it; on the old pointer the old module accepted the new keys and silently dropped them. Checked before advancing (docs/ENTERPRISE.md): 29ec9e7 is an ancestor of private main; check_alembic_heads.py -> 18 revisions, 1 head (0017_credential_vault), PASS (no migration in this change); the enterprise suite against this pairing -> 485 passed, 4 skipped (Postgres-only), the panel-field guard running against the real panel. Refs Abilityai/trinity-enterprise#671 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dolho
left a comment
There was a problem hiding this comment.
/review: PR #3091 (feature/671-metrics-retention-rows → dev), head f52e9ced
Scope: CLEAN against abilityai/trinity-enterprise#671. The diff adds the two metric rows, the quotas read field and changed-fields-only Save. It also fixes the compose/.env.example empty default, which is the root cause that would have shipped these rows read-only everywhere. INTER_AGENT_MAX_CHAIN_DEPTH is included because it is the third ENV_BACKED_OPS_KEYS member with the same defect, so it is not scope drift.
Pointer bump: f769d07 -> 29ec9e7, one gitlink line. I checked that 29ec9e7 is an ancestor of private origin/main; it is the squash merge of trinity-enterprise#725, which I reviewed and approved.
Verification (isolated worktree at the PR head)
tests/unit/test_ent671_retention_metric_rows.py+test_ent671_env_backed_ops_forwarding.py: 27 passed- vitest
retentionFields.spec.jsplus the raw-colour, loading-gate and source-text ratchets: 45 passed - Enterprise-docs guard pattern over the added
docs/lines: no hits - CI at the time of review: 15 pass, 6 pending, 0 failing
Execution coverage: mutations (each reverted afterwards)
| mutation | red |
|---|---|
| Save sends unchanged fields | 4 retentionSaveBody cases, incl. "an untouched code default never becomes a stored row" |
| Save sends an env-sourced field | "never sends an env-sourced field…" |
Number(raw) → parseInt(raw, 10) |
"sends a huge entry as itself", "sends a fractional entry as itself" |
drop the edition === 'enterprise' gate |
4 cases incl. "leaves the metric rows out in Community (AC3)" |
garbage quota reads 0 (unlimited) in GET /retention |
test_quota_value_is_what_the_write_boundary_enforces[resolved3] |
docker-compose.prod.yml back to :-100000 |
test_backend_compose_forwards_the_variable_with_an_empty_default[…prod], test_no_other_compose_file_supplies_a_default_either |
.env.example sets METRICS_RETENTION_DAYS again |
test_env_example_leaves_the_variable_commented |
The rules live in utils/retentionFields.js and are executed by vitest rather than pinned by source text, and the view wiring is covered by the 4 @smoke e2e cases.
Checked and clean
- Empty-default forwarding is safe for every reader. The only consumer of the three variables is
config.env_ops_value, which mapsraw is None or raw == ""→None(no directint(os.getenv(...))anywhere insrc/ordocker/). An unset variable therefore reaches the code default exactly as before. Effective values don't change. - Boot seeder interaction is inert. On an existing install, the old
:-365madeenv_ops_value("metrics_retention_days")non-None, so_retention_seed_pairsskipped it. After this PR the next boot seeds ametrics_retention_days=365row: the same number, nowsource: db-rowand editable. The quota is not a retention window and is not seeded, so it reportscode-default. quotasread parity. Garbage falls back to the default, never to0, which matches therecord_metricswrite boundary.windows/sourceskeep their meaning.- Frontend. It uses
BaseBadgeandInlineErrorprimitives and adds no raw palette classes (the ratchet is green). Inputs getid/for/aria-describedby, plus ansr-onlyenv explanation. Save is disabled while clean, and a stale "Saved" clears on the next edit. A rejected save no longer replaces the panel. - Auth. No new write route.
GET /api/settings/retentionis unchanged in who can call it; it only gains thequotasblock.
Informational (non-blocking)
- [I1] Upgrade note, one more line. Besides the
.envcopied from the old.env.example(already noted), an upgraded install's Metric points row moves fromenvtodb-rowon the first boot after this lands, because the seeder now writes 365. That's correct and inert, but operators comparing thesourcesfield before and after may ask, so it may be worth a sentence in the release notes. - [I2] A negative stored quota reads as unlimited; already filed as #3089. The prune approval ignoring the env tier is #3088. Both are fine as follow-ups.
Verdict: approve. Please let the pending checks (e2e, build, prod-image-smoke, CodeQL) finish green before merging.
🤖 Generated with Claude Code
Summary
ent#478 minted
metrics_retention_daysandmetrics_daily_point_cap. They could be reached through the API and the env tier, but not from the Settings page.PUT /api/settings/ops/configstays the write path.GET /api/settings/retentiongainsquotas.metrics_daily_point_cap={value, source}. The quota is a write budget, not a window, sowindowsandsourcesare unchanged. Its value is read the way therecord_metricswrite boundary enforces it: an unparseable value falls back to the default, never to0(unlimited).utils/retentionFields.js, so vitest executes them:edition, so nothing pops in;envbadge that names its variable and what unsetting it does, and it is never sent;parseIntturned a 22-digit entry into1.InlineErrorbeside Save. It used to replace the whole panel with one red line and no way back.e5db164f0): the three backend compose files forwarded the env-backed ops keys with non-empty defaults (:-100000), and.env.exampleset them outright. So "source: env" held on every install, and the new rows would have shipped read-only everywhere. The files now forward them as${VAR:-}(theSECRET_KEYpattern) and leave them commented. A guard test keyed offconfig.ENV_BACKED_OPS_KEYSpins this. Effective values do not change.Upgrade note: an existing
.envcopied from.env.exampleafter ent#478 still sets the variables. On such an install the rows stay read-only (correctly, since the value is pinned by env) until those lines are removed and the backend container is recreated.Changes
src/backend/routers/settings/retention.py: thequotasblock.src/frontend/src/utils/retentionFields.js(new),src/frontend/src/views/Settings.vue: the rows, badge, changed-fields-only Save and inline error.docker-compose.yml,docker-compose.prod.yml,docker-compose.hosted.yml,.env.example: empty-default forwarding.requirements/lifecycle-observability.md§47.8;architecture/api-endpoints.md,architecture/reliability.md;feature-flows/platform-settings.md(new Retention tab section),feature-flows/agent-custom-metrics.md;/cso --diffreport.f769d07 -> 29ec9e7(commitf52e9ceda, one gitlink line).29ec9e7is abilityai/trinity-enterprise#725, squash-merged to privatemainafter approval. Checked before advancing, perdocs/ENTERPRISE.md:29ec9e7is an ancestor of privatemain;check_alembic_heads.pyreports 1 head, PASS (this change adds no migration);dev(2026-10-01): two add/add conflicts (tests/registry.json, the flow's revision table), resolved by keeping both sides. The registry stays valid and all ofdev's entries are preserved in order. Re-run on the merged tree: 312 OSS unit and 4,475 vitest pass, with only the two known host-only failures.Test plan
cd tests && pytest unit/test_ent671_retention_metric_rows.py unit/test_ent671_env_backed_ops_forwarding.py -v: 27 passed. Neighbouring retention, settings and compose suites: 277 passed on the dev-merged tree.src/frontend/tests/unit/retentionFields.spec.js: 17 passed. The full vitest run passes, apart from the two known host-only failures (roomComposerChain,roomStopWork). The raw-colour, loading-gate and source-text ratchets are unchanged.e2e/settings-retention-metric-rows.spec.js(4 ×@smoke, route-mocked at feature-flags, the retention GET and the managed PUT): 4/4 against a/verify-localsibling backend. Four deliberate view breaks each went red: the env lock, the inline error, the stale-"Saved" reset, and sending every field..env.exampleguard)./verify-local --skip-agent:e87163125base run, compared by test name (the order-dependent ent666/ent477/retention_floor set; 0 new);code-default./cso --diff: 0 findings (docs/security-reports/cso-diff-2026-09-29-ent671-metric-rows.md).Eyeball (localhost; main checkout detached at
e5db164f0+ enterprisea28a85a; backend re-created with--no-build)METRICS_DAILY_POINT_CAPempty. The quota's source iscode-default(it wasenvunder the old compose).ops_settings_change{"health_check_retention_days": "8"}, and no quota row was created./api/enterprise/retention/config, the quota withretention_windows_changed: null.METRICS_DAILY_POINT_CAP=250shows 250, disabled, with the env badge and tooltip;TRINITY_OSS_ONLY=1): both rows absent and no Save. The enterprise route answers 403, and the platform settings API still answers.Follow-ups filed: #3088 (the prune approval ignores the env tier, split out of this PR), #3089 (a negative stored quota reads as unlimited).
Fixes abilityai/trinity-enterprise#671
🤖 Generated with Claude Code