Skip to content

refactor(config): give feature flags provenance, typed ids, and an honest boot log - #551

Open
RonenMars wants to merge 1 commit into
feat/live-activity-push-flagfrom
refactor/feature-flag-provenance
Open

refactor(config): give feature flags provenance, typed ids, and an honest boot log#551
RonenMars wants to merge 1 commit into
feat/live-activity-push-flagfrom
refactor/feature-flag-provenance

Conversation

@RonenMars

Copy link
Copy Markdown
Owner

Stacked on #545 — its branch is the base, so this diff shows only the refactor.
Merge #545 first; GitHub will retarget this to main automatically.

The bug this starts from

The boot log stated the opposite of the truth for any flag whose default is ON.

const enabledFlags = nonDefaultFeatureFlags(this.featureFlags);
this.log.info(`Feature flags active: ${enabledFlags.join(", ")}`, )

nonDefaultFeatureFlags returns ids that differ from their default, under a heading that says active.
sessionRehydration defaults on, so disabling it printed Feature flags active: sessionRehydration — naming a flag as active at the moment it was turned off.
Three of the four flags default off, which is why the message read correctly often enough to survive.

It also logged names only, and went silent on a stock boot, so the log could never answer "what was this process actually running with".

What changed

Provenance. The resolver walked env ?? cli ?? yaml ?? default and discarded which rung won.
It now returns { values, sources }, both total, where sources[id] is "override" | "env" | "cli" | "yaml" | "default".
GET /api/config/feature-flags carries sources as an additive field, so "why is this on?" is answerable over HTTP instead of by hand-reading the environment, the argv and server.yaml.

Boot log. Every flag, every boot, as id=value(source) under event: "config.feature_flags".

Typed ids. FeatureFlagId is derived from the registry, replacing Record<string, boolean>.
A typo'd consumer was previously undefined read as falsy — silently disabling the feature it was meant to gate.
It is now a compile error, and it found a real consumer I had missed: cli/prod.ts reads the resolver's result directly, outside every src/-only grep.

The legacy override is a real rung. codexSystemPromptEnabled was applied by mutating the resolved map afterwards, so the resolver was not actually the single source of truth and an override was invisible to any reporting.
CLAUDE.md already documented it as precedence rung 1; the code now matches.

A flag that gates nothing now fails a test. feature-flags.test.ts scans src/ and cli/ for each registry id.
Without it a flag can be declared, validated, persisted to server.yaml and served over HTTP while affecting nothing, with every layer reporting success — the same bug that shipped once in claude-flags.

Not done, deliberately

No remote flag service.
Resolution stays boot-time only: ptyHost decides whether live PTYs run in a separate host process, and re-evaluating that mid-flight would strand sessions across two regimes with no migration path.

Verified state

npm run lint clean (tsc + biome).
119/119 across the five suites that touch flags: feature-flags, live-activity-flag, session-rehydration, prod-commands, session-settings.

The new unread-flag guard was given a positive control rather than trusted: adding a temporaryProbeFlag to the registry failed it with expected [ 'temporaryProbeFlag' ] to deeply equal [], and it was removed again.

Note for reviewers: tsconfig.json excludes __tests__, so npm run lint type-checks neither this PR's test changes nor any others.
A test passing the whole resolution object where a values map was expected got through lint and surfaced only when the suite ran.

…nest boot log

The resolver collapsed env → CLI → yaml → default into a bare boolean with a `??` chain, so nothing could answer "why is this flag on?".
It now walks the rungs as an ordered list and returns `{ values, sources }`, both total, where `sources[id]` names the rung that decided each flag.
`GET /api/config/feature-flags` gains an additive `sources` field so that question is answerable over HTTP rather than by reading the environment, the argv and server.yaml by hand.

The boot log stated the opposite of the truth for a flag defaulting ON.
It printed the ids differing from their defaults under the heading "Feature flags active", so disabling `sessionRehydration` listed it as active, and it went silent on a stock boot.
It now prints every flag as `id=value(source)` under `event: "config.feature_flags"`.

`FeatureFlagId` is derived from the registry instead of `Record<string, boolean>`, so a typo'd consumer is a compile error rather than `undefined` read as falsy.
That found a real one: `cli/prod.ts` reads the resolver's result directly and was never covered by the src-only greps.
The legacy `codexSystemPromptEnabled` override becomes a real rung inside the resolver rather than a mutation applied to the finished map, which is what CLAUDE.md already documented.

Adds a test that scans src/ and cli/ for every registry id, so a flag cannot be declared, validated, persisted and served while gating nothing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant