Skip to content

feat(settings): metric point rows in Settings → Retention (abilityai/trinity-enterprise#671) - #3091

Open
webmixgamer wants to merge 8 commits into
devfrom
feature/671-metrics-retention-rows
Open

webmixgamer wants to merge 8 commits into
devfrom
feature/671-metrics-retention-rows

Conversation

@webmixgamer

@webmixgamer webmixgamer commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

ent#478 minted metrics_retention_days and metrics_daily_point_cap. They could be reached through the API and the env tier, but not from the Settings page.

  • Settings → Retention now shows both next to the sibling windows: Metric points (days) and Metric point quota (points / agent / day, "0 = unlimited"). They appear 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 gains quotas.metrics_daily_point_cap = {value, source}. The quota 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: an unparseable value falls back to the default, never to 0 (unlimited).
  • The field list and its rules live in utils/retentionFields.js, so vitest executes them:
    • rows are gated on the response's own edition, so nothing pops in;
    • an env-sourced row is read-only, with an env badge that names its variable and what unsetting it does, and it is never sent;
    • Save sends only the fields you changed. It used to PUT every field, which froze untouched code defaults and env values into stored rows. Values go out as typed numbers, because parseInt turned a 22-digit entry into 1.
  • A rejected save renders in an InlineError beside Save. It used to replace the whole panel with one red line and no way back.
  • Root cause found on the way (commit e5db164f0): the three backend compose files forwarded the env-backed ops keys with non-empty defaults (:-100000), and .env.example set 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:-} (the SECRET_KEY pattern) and leave them commented. A guard test keyed off config.ENV_BACKED_OPS_KEYS pins this. Effective values do not change.

Upgrade note: an existing .env copied from .env.example after 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: the quotas block.
  • 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.
  • Docs:
    • 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;
    • two learnings fragments;
    • the /cso --diff report.
  • Enterprise submodule pointer bumped f769d07 -> 29ec9e7 (commit f52e9ceda, one gitlink line). 29ec9e7 is abilityai/trinity-enterprise#725, squash-merged to private main after approval. Checked before advancing, per docs/ENTERPRISE.md:
    • 29ec9e7 is an ancestor of private main;
    • check_alembic_heads.py reports 1 head, PASS (this change adds no migration);
    • the enterprise suite against this pairing: 485 passed, 4 skipped (Postgres-only), with the panel-field guard running against the real panel.
  • Merge with 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 of dev'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-local sibling backend. Four deliberate view breaks each went red: the env lock, the inline error, the stale-"Saved" reset, and sending every field.
  • Every other new call site was mutation-checked red (quota reader parity, key guards, compose/.env.example guard).
  • /verify-local --skip-agent:
    • preflight, build + import-smoke, boot + health and integration pass;
    • the unit stage's 40 failures match a clean e87163125 base run, compared by test name (the order-dependent ent666/ent477/retention_floor set; 0 new);
    • on the sibling stack, both variables arrive empty and the quota reports code-default.
  • /cso --diff: 0 findings (docs/security-reports/cso-diff-2026-09-29-ent671-metric-rows.md).
  • Manual eyeball on localhost (below).

Eyeball (localhost; main checkout detached at e5db164f0 + enterprise a28a85a; backend re-created with --no-build)

  • Env forwarding: the re-created container has METRICS_DAILY_POINT_CAP empty. The quota's source is code-default (it was env under the old compose).
  • Settings → Retention, light and dark:
    • "metric points" appears in the description;
    • Metric points (365 days) and Metric point quota (100000, "points / agent / day · 0 = unlimited") come after Soft-deleted schedules;
    • Save is disabled until a change.
  • Save sends only changed fields: the audit row is ops_settings_change {"health_check_retention_days": "8"}, and no quota row was created.
  • Round trip: the quota at 5000 persisted across a hard reload; Metric points 2 came back as 5 (the floor). Both were audited from /api/enterprise/retention/config, the quota with retention_windows_changed: null.
  • Reject: 10000001 shows "metrics_daily_point_cap: Input should be less than or equal to 10000000". The value is kept and nothing is stored.
  • Env state:
    • METRICS_DAILY_POINT_CAP=250 shows 250, disabled, with the env badge and tooltip;
    • a sibling save omitted the quota;
    • unsetting the variable brought back 100000, editable.
  • Community (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

webmixgamer and others added 5 commits September 29, 2026 16:02
…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>
@webmixgamer webmixgamer added the ui PR touches the frontend UI — triggers Playwright e2e tests label Sep 29, 2026
…lidator rejects (Abilityai/trinity-enterprise#671)

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

Copy link
Copy Markdown

⚠️ Nightly unit-suite check skipped — merge conflict against dev.

Resolve by running git merge dev locally and pushing the result. The next nightly run will re-test once the conflict is gone.

@github-actions

Copy link
Copy Markdown

⚠️ Live-instance suite skipped — merge conflict against dev.

Resolve by merging dev locally and pushing the result; the next nightly re-tests.

webmixgamer and others added 2 commits October 1, 2026 13:01
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>
@webmixgamer
webmixgamer marked this pull request as ready for review October 1, 2026 12:02
@webmixgamer
webmixgamer requested review from dolho and vybe October 1, 2026 12:02

@dolho dolho 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.

/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.js plus 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 maps raw is None or raw == "" → None (no direct int(os.getenv(...)) anywhere in src/ or docker/). 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 :-365 made env_ops_value("metrics_retention_days") non-None, so _retention_seed_pairs skipped it. After this PR the next boot seeds a metrics_retention_days=365 row: the same number, now source: db-row and editable. The quota is not a retention window and is not seeded, so it reports code-default.
  • quotas read parity. Garbage falls back to the default, never to 0, which matches the record_metrics write boundary. windows/sources keep their meaning.
  • Frontend. It uses BaseBadge and InlineError primitives and adds no raw palette classes (the ratchet is green). Inputs get id/for/aria-describedby, plus an sr-only env 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/retention is unchanged in who can call it; it only gains the quotas block.

Informational (non-blocking)

  • [I1] Upgrade note, one more line. Besides the .env copied from the old .env.example (already noted), an upgraded install's Metric points row moves from env to db-row on the first boot after this lands, because the seeder now writes 365. That's correct and inert, but operators comparing the sources field 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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ui PR touches the frontend UI — triggers Playwright e2e tests

2 participants