ADR — Findings Runtime Disabled Mode
Formerly
docs/architecture/ADR-FINDINGS-RUNTIME-DISABLED-MODE.md.One of two ADRs numbered 013; the other is the unrelated .NET callgraph extractor dual-mode. This is the canonical “ADR-013” for the Findings Runtime cluster (ADR-013 → ADR-017).
| Field | Value |
|---|---|
| Status | Accepted (signed off 2026-04-29 — Architecture lead via user approval) |
| Date | 2026-04-28 |
| Authors | Findings Ledger Runtime sub-team |
| Module | src/Findings/StellaOps.Findings.Ledger.WebService/Endpoints/RuntimeTracesEndpoints.cs, src/Findings/StellaOps.Findings.Ledger/Infrastructure/Postgres/Runtime/ |
| Related | SPRINT_20260429_011..014, SPRINT_20260429_015_FindingsLedger_runtime_documentation, SPRINT_20260429_023_Findings_runtime_contract_corrections |
In one line: runtime instrumentation in the Stella Ops Findings Ledger is off by default; when off, a startup-bound null-repository pattern guarantees no runtime data is persisted, ingest endpoints accept-and-no-op (
202), and read endpoints return404— wire-indistinguishable from “enabled but no data yet” so clients need only one code path.Audience: Findings Ledger engineers and operators planning a safe, opt-in runtime rollout.
Context
Runtime instrumentation is opt-in. Existing deployments that upgrade to the runtime-aware Findings Ledger build must not start receiving trace ingest, growing storage, or surfacing runtime data on findings until the operator explicitly opts in. This is captured as a sprint decision:
*Decision: Runtime instrumentation is disabled by default in new
- upgraded deployments. Operators opt in. Avoids surprise data ingestion + disk growth on upgrade.*
—SPRINT_20260429_015_FindingsLedger_runtime_documentationDecisions & Risks
The toggle is a single config key: findings:ledger:runtime:enabled, default false.
Sprint 023 corrected the DI binding to match this contract: an absent config key now resolves the NullRuntime*Repository implementations. Only an explicit true value wires Postgres runtime persistence.
When the toggle is off, three things must happen:
- No persistence is touched. Even if a misconfigured agent posts trace data, the WebService must not write to the runtime tables.
- Read endpoints behave like “feature has no data yet”, not “feature is broken”. Clients (UI / CLI / SDK) should differentiate “you turned off the feature” from “you turned on the feature but nothing has been ingested yet” — but the wire contract should be identical, so client logic is uniform.
- Ingest endpoints don’t fail noisily. Agents should be able to keep posting; the server simply no-ops the request. This is the familiar “204-style sink” pattern that makes opt-in rollout safe.
Decision
Disabled mode is implemented with two cooperating mechanisms:
1. Null-pattern repository implementations
When the toggle is off, the DI registration binds the runtime repositories to no-op (null-pattern) implementations:
IRuntimeTraceRepository→NullRuntimeTraceRepository—Save*is a no-op,Get*/List*return empty.IRuntimeScoreRepository→NullRuntimeScoreRepository—Upsertis a no-op,Getreturnsnull.IRuntimeTimelineRepository→NullRuntimeTimelineRepository—Appendis a no-op,Queryreturns empty.
The application code (RuntimeInstrumentationService, RuntimeTracesEndpoints, RuntimeTimelineEndpoints) is unchanged between enabled and disabled modes. It calls the repository interface either way. The repository selection happens once at startup.
2. The 202 / 404 wire contract
| Endpoint | Disabled mode response | Enabled-but-empty response | Enabled-with-data response |
|---|---|---|---|
POST .../runtime/traces | 202 Accepted, frameCount=0 | 202 Accepted, frameCount=N | 202 Accepted, frameCount=N |
GET .../runtime/traces | 404 NotFound | 404 NotFound | 200 OK + body |
GET .../runtime/score | 404 NotFound | 404 NotFound | 200 OK + body |
GET .../runtime-timeline | 404 NotFound | 404 NotFound | 200 OK + body |
Disabled and “enabled but no data” are wire-indistinguishable. This is intentional — clients should not branch on the toggle state. The toggle is an operator concept, not a client concept.
3. Empty-result reads return 404 (not 200 + empty)
Returning 200 OK with [] would force every client to inspect the body to decide whether to render an empty state. Returning 404 makes the empty-state path a status-code branch, which is the standard HTTP idiom for “no resource here yet”. This is asserted by contract tests:
RuntimeTraces_Endpoint_WithAuth_Returns_NotFound_For_Default_ServiceRuntimeTimeline_Endpoint_WithAuth_Returns_NotFound_For_Default_Service
Architecture-lead sign-off
- Author (Implementer): 2026-04-28.
- Architecture lead: Accepted 2026-04-29 (drafted autonomously per
SPRINT_20260429_015_FindingsLedger_runtime_documentationFL-RUNTIME-DOC-003; see the Sign-off Window below for how acceptance was reached). - Corrective implementation:
SPRINT_20260429_023aligned the default binding with this ADR before sign-off.
Rationale
Why null-pattern instead of a config check at every call site? A config check at every call site is impossible to audit — one missed branch and a disabled deployment writes data. The null-pattern collapses the safety check to one binding decision at startup, which is trivially auditable and trivially testable.
Why 202 on ingest in disabled mode (not 404 / 503)? Two reasons:
- Operational rollout safety. Operators can deploy the agent side first, then flip the server toggle, with zero downtime and zero error spam in agent logs. If we returned 404, every agent would log an error indefinitely.
- Symmetric contract. “Enabled with no data yet” and “disabled” should look the same on the wire so clients have one code path.
Why 404 on read in disabled mode (not 200 + empty body)? Three reasons:
- Standard HTTP idiom.
404means “no resource”;200 + []means “resource exists, contains nothing”. They’re different. - Client UX. UI components can short-circuit to an empty state on a 404 without parsing the body. Less code in every client.
- Backward compatibility. Older clients that don’t understand the runtime feature will simply skip a 404 and continue.
Why one toggle and not per-endpoint toggles? Splitting the toggle forces operators to understand the feature’s internal structure to configure it sensibly. One toggle keeps the decision space small. Sub-toggles (e.g. “disable timeline retention but keep traces”) can land later if there’s demand.
Alternatives considered
- Per-call-site config check.
if (options.RuntimeEnabled) { ... }inline. Rejected — impossible to audit, easy to miss a branch. - Whole-endpoint feature gate (return 503 when disabled). Rejected — agents posting to a 503 endpoint will retry forever and spam logs.
- Skip endpoint registration entirely when disabled. Rejected — routing changes between enabled and disabled deployments, which breaks proxies / load balancers that expect a stable surface.
- Rate-limit-to-zero in disabled mode. Rejected — same problem as 503, agents will retry.
- Auto-enable on first ingest. Rejected — explicitly violates the “no surprise data ingestion” sprint decision.
Consequences
- Upgrade is safe. Existing deployments add the runtime tables via auto-migration but don’t accept any data until the operator flips the toggle.
- One config key gates everything. Easy to document, easy to flip, easy to reason about in incidents.
- Wire contract is identical between disabled and enabled-but-empty. Clients have a uniform “no data” code path.
- Null repositories require their own tests (one suite per interface, asserting the no-op behavior). These exist under
src/Findings/__Tests/StellaOps.Findings.Ledger.Runtime.Persistence.Tests/. - Operators can flip the toggle off again. The data stays in Postgres (we don’t drop it), but ingest stops. Re-enabling resumes ingestion against the existing rows. This is the documented operational behavior.
Test coverage
RuntimeTraces_Endpoint_WithAuth_Returns_NotFound_For_Default_ServiceRuntimeTimeline_Endpoint_WithAuth_Returns_NotFound_For_Default_ServiceNullRuntimeRepositoriesTestsassert null repository behavior, default-off DI, explicit-disabled DI, and explicit-enabled Postgres registration.- WebService composition root tests assert disabled-mode endpoint behavior when the toggle is off.
References
- ADR-014 Findings Runtime Persistence
- ADR-015 Findings Runtime Privacy Filter
- ADR-016 Findings Runtime Score Formula
docs/modules/findings-ledger/architecture.mddocs/modules/findings-ledger/enabling-runtime-instrumentation.mddocs/modules/findings-ledger/runtime-instrumentation-api.md§5src/Findings/StellaOps.Findings.Ledger.WebService/Endpoints/RuntimeTracesEndpoints.cs
Sign-off Window
Accepted on 2026-04-29 by Architecture lead. The 14-day default-accept window from sprint 024 (deadline 2026-05-13) closed early via explicit approval rather than the timeout path. Original window text retained below for history.
Following the project’s RFC default-accept process: if this ADR remains
Proposedwith no objection logged in this document by 2026-05-13 (14 days from sprint 024 commit), it is automatically promoted toAccepted. Architecture-lead may flip status earlier on review. Objections should be added under “Open Issues” below with name + date.
Open Issues
No objections logged during the sign-off window.
