R8 Legacy Retirement — Design¶
Status: picked up by owner instruction (2026-09-29); scope authorized, slices S0–S5 ordered. Date: 2026-09-28 Branch: chore/r8-legacy-retirement (off main at 51f635e5) Supersedes: nothing. This is 12-postponed-roadmap.md R8 moving from the deferred index into its own spec, which is that file's own convention at :10.
Evidence tags follow the repo convention (AGENTS.md): [V] verified this session by source read at the named tip; [C] computed; [O] owner statement; [A] agent-reported, not independently verified. Keep every marker when editing.
0. Why this is open now¶
R8's trigger in the index is "Go-live holds 14 days". That trigger is reversed by owner instruction [O] — the owner picked it up on 2026-09-29 with two rulings that set the shape of the work:
- "we do not have to support backwards compatability" and "we should hard raise" [O].
PIPELINE_MODE=legacymust RAISE at boot. No deprecation window, no alias, no compatibility shim, no one-release warning. The precedent is ledger item 20 on the re-ID swap, whose wording is nearly identical. - No A5500. Repo + GB300 only. S1's 9,722 MiB and S4's 16.2 s belong to that box (ledger items 40–41) and are neither re-measured nor restated as repo claims here.
The reason the deletion matters functionally rather than cosmetically: the legacy path is reachable by environment variable today. backend/core/config.py:1084 accepts "legacy" and :1086 only logs a warning [V]. A deployment with a stale .env boots the unsupported pipeline.
The owner's stated motive is agent-experience, not code hygiene: "so we dont confuse future agents."
1. What goes¶
Everything below verified [V] at main 51f635e5, which is the tip this spec was written against. The measured detail — every line number, importer and trap — lives in docs/plans/2026-09-28-r8-legacy-retirement-scope.md.
| target | size | note |
|---|---|---|
PIPELINE_MODE=legacy as a value | — | becomes a raise; 13 branch sites delete |
ai-llm service + ai/nemotron build context | — | prod.yml profile legacy |
backend/services/nemotron_analyzer.py | 5,429 L | minus the hoist in §2 |
enrichment_pipeline.py / enrichment_client.py | 7,662 / 3,334 L | legacy-side |
nemotron_streaming analyzer_facade prompt_auto_tuner nemotron_latency_optimizer | 575/266/205/650 L | legacy-side |
skeleton_action_service.py | — | only importer is enrichment_pipeline |
pose_analysis_service.py | — | zero non-test importers already |
19 of 22 *_loader.py | — | see §3 |
| Florence-2 (loader, extractor, rows, Triton dir) | — | legacy-reachable, not VLM-reachable |
legacy FULL_MODEL_SET Triton models (11) | — | §4 |
pose / demographics / action / reid_embeddings tables + models.yml rows | — | §5 |
| R8's named frontend panels | — | §6 |
2. What stays — and the one extraction that must happen first¶
nemotron_analyzer.py is ~98% legacy, but :294-411 is imported by the shipped VLM path [V]: vlm_client.py:65-69, vlm_analyzer.py:74, evaluation/vlm_replay.py:56, and main.py:773 inside the live constrained-decoding startup gate. Symbols: ConstrainedDecodingNotEnforced, _is_length_truncated, build_probe_schema (and VerificationRowOutcome, PROBE_PROMPT, _TRUNCATED_STOPS, _probe_completion). They move to backend/services/constrained_decoding.py before the file is deleted; deleting whole breaks vlm at import.
The three VLM-reachable loaders stay, with model_loader_base.py: face_recognizer_loader, osnet_loader, fast_alpr_loader [V]. The other 19 are reachable only through enrichment_pipeline, model_zoo._LOADER_MAP, or prompts.py under if TYPE_CHECKING: — which erases at runtime and is therefore not evidence of life.
This is not a judgement call about capability. core/container.py:508 builds the enrichment collaborators only when pipeline_mode == "legacy", and no API route constructs EnrichmentPipeline [V]. Retiring legacy makes the tier unreachable by construction.
Stays for a different reason than "it works": the specialists are lookups against a store the pixels cannot reach (face→known_persons, plate→registered vehicles, re-ID→PersonEmbedding via household_matcher.load_person_gallery). The VLM receives the raw key frames as base64 (vlm_client.py:499) and perceives the scene itself, which is why Florence and the attribute zoo are substitutions rather than additions. If more signal is wanted later, the category worth adding is detection/lookup — the sanctioned optional threat specialist (GATEWAY_ENABLE_THREAT, residency.py:69) — not a second perception model.
Other must-not-break [V]:
notification_filter.py:35 should_notify— implemented, unwired. M1's exit; a separate job.threat_detections(enrichment.py:100) — read byalert_engine.py,api/schemas/alerts.py.household.py:155 PersonEmbedding— LIVE, whileenrichment.py:218 reid_embeddingsis legacy.- The
llama-cachenamed volume (prod.yml:1621) — mounted by ai-vlm at:262. GPU_LLM, and theCTX_SIZE/PARALLELpair — shared with vlm's slot budget (32768/2).- The
models.ymlrow carryingtriton_name: reid— one row feeds the CPU OSNet loader and the Triton repository. Keep the whole row. - Triton
yolo26,reid, and the opt-inthreat— S1's PASS was measured against exactlyyolo26+reid(residency.py:65, threat off by default).
3. Slices¶
One slice, one PR, TDD red-first. Order matters where noted.
| slice | content | why here |
|---|---|---|
| S0 | currency banners + this spec + the doc-12 mark | ships alone, before any deletion |
| S1 | hard raise on non-vlm; delete the 13 branch sites | makes legacy unreachable at boot |
| S2 | hoist :294-411, then delete the analyzer + enrichment tier | extract before delete |
| S3 | ai_contract providers, one per slice; prune 11 Triton models | PR-gated tier |
| S4 | table retirements (model class + dated DROP SQL) | see ordering below |
| S5 | frontend panels + docs a future agent reads | last |
Ordering argument for S4 early [V reasoning]: select_preload_candidates (main.py:644) preloads by enabled AND preload and is not keyed to mode, so smoke-fire-yolov8n's 350 MB preloads in shipped vlm mode today. S1's PASS was measured with that dead weight inside the budget, so retiring it moves S1 down — headroom, never risk. Table retirements are also smaller and more reversible than deleting a module the DI graph still names.
4. Triton residency¶
FULL_MODEL_SET (residency.py:46-60) holds 14; VLM_MODEL_BASE is ("yolo26","reid") (:65) with threat opt-in (:69). The 11 others are legacy-only, so pruning them cannot move S1's number. Face (SCRFD + w600k_r50) is CPU-side in the backend and is not in Triton at all. Tests that pin the pair move with the change: test_gateway_model_set_compose.py, ai/gateway/tests/test_residency.py, test_entrypoint_residency.py.
5. Schema¶
There is no Alembic in this repo. Schema comes from backend/scripts/init_schema.py + core/database.py, with hand-authored dated SQL in docs/api/migrations/. So a table retires as: delete the model class, add a dated DROP file. Precedents: 2026-09-26-face-vector-provenance-model-id.sql, NEM-5051-convert-alert-json-to-jsonb.sql.
6. Frontend¶
R8 :108 names "their frontend panels" as in-scope, and they are real [V]: components/enrichment/EnrichmentViewer.tsx, components/events/EnrichmentBadges.tsx, EnrichmentPanel.tsx, EventEnrichmentSummary.tsx, utils/poseVisualization.ts. The tree is frontend/ — not src/frontend/, a path several scoping agents asserted and none exists.
7. Minefield¶
- Clear stale
__pycache__before trusting a deletion guard (R8:118: a stalebackend/api/helpers/__pycache__false-reddened one on 2026-09-23). - Coverage gates are the merged floors — 84 unit (
ci.yml:613), 37 integration (ci.yml:1215), notpyproject.toml's 85. Report the direction; never lower a floor. - Deleting test files moves the suppression census (
ci.yml:101-104vs.github/suppression-baseline.json). Regenerate viascripts/suppression-registry-gen.py; never hand-edit a generated file. - ~96 legacy-only test files go with their module; ~61 more merely mention legacy and are edited, not deleted.
- 66 of those 96 error at import in this sandbox (no
libxcb.so.1/cv2). Environmental — judge by CI. - Never rewrite frozen research prose, and never edit a spec's Status/rev line: owner-only.
docker-compose.ghcr.ymlhas noai-vlmservice at all. Report it; do not fix it inside a retirement slice.
8. Open, owner's call¶
models.yml: delete legacy rows, or keepenabled: falseas provenance?- Does R8 reach the
ai/enrichment/ai/enrichment-lightproviders, or backend only? - Keep
fullas a Triton dev affordance? (Default: no — hard-raiseGATEWAY_MODEL_SET.) nemotron_urlhas live, non-mode-gated callers [V]:summary_generator.py:84(its scheduler atmain.py:1092is gated on Redis only, not mode) andprompt_service.py:938(POSTs{_llm_url}/completion; router registered atmain.py:1649). Both already degrade to warnings becauseai-llmis profile-legacy— so deleting makes an existing breakage permanent. Re-home them on the VLM, or retire the features explicitly.- Whether keeping the threat detector switched-on is wanted now (costs S1 VRAM — re-measure).