W-007/ADR-0149 wired the consumer side of the recognizer registry (first_admitted_recognizer → graph derivation, opt-in via recognition_grounded_graph). The producer side — capturing (tokens, bundle) from admitted turns so derive_recognizer at checkpoint can anti-unify them — had no production caller. record_recognition_example existed but was only invoked by tests, so _pending_recognizer_examples stayed empty in live sessions and the registry could never grow from traffic. Observed: 103-turn session wrote recognizers.jsonl empty even with recognition running. - CognitiveTurnPipeline.run calls runtime.record_recognition_example at the admitted-recognition boundary - Producer fires unconditionally; consumer (derive_recognizer at checkpoint) stays opt-in behind the same flag — flipping it later is no longer a cold start - hasattr guard keeps the pipeline tolerant of non-ChatRuntime runtimes Validated: tests/test_adr_0154_recognizer_producer_wiring.py (5 tests covering admit/refuse, flag-off producer, end-to-end loop, accumulation); core test --suite cognition/smoke + recognition phase 1/2/refusal-propagation all green. Out of scope: bootstrap of the first recognizer from operator review (substrate-liveness audit scope); bounded growth of the producer queue when consumer flag stays off (future LRU cap).
3.9 KiB
ADR-0154 — DerivedRecognizer producer wiring (W-020b)
Status: accepted Date: 2026-05-25
Context
ADR-0149 (W-007) wired the DerivedRecognizer registry's consumer
side: runtime.first_admitted_recognizer() is read by
CognitiveTurnPipeline.__init__ and feeds the optional
recognition-grounded graph at pipeline.py ~line 217 (gated by
recognition_grounded_graph, default off).
The producer side — capturing (tokens, bundle) from admitted
turns so derive_recognizer at checkpoint can anti-unify them into
tighter recognizers — was never connected in production code.
runtime.record_recognition_example had zero non-test callers:
$ grep -rn record_recognition_example --include="*.py" | grep -v test
chat/runtime.py:703: def record_recognition_example(
Consequence: _pending_recognizer_examples stayed permanently empty,
so the conditional at chat/runtime.py:684-691 —
if (
self.config.recognition_grounded_graph
and self._pending_recognizer_examples
):
recognizer = derive_recognizer(...)
...
— never fired, even with the flag enabled. The registry could only
grow via tests calling record_recognition_example directly.
Observed symptom: a 103-turn session wrote recognizers.jsonl as
empty even though recognition was running.
Decision
In CognitiveTurnPipeline.run, at the admitted-recognition boundary
(directly after EpistemicGraph construction), call
runtime.record_recognition_example(raw_tokens, _rec_outcome.proposition).
- Producer fires unconditionally when a turn is admitted — the
bucket is filled regardless of
recognition_grounded_graph. This means flipping the consumer flag later is not a cold start. - Consumer stays opt-in behind the same flag — no change to
checkpoint_engine_state'sderive_recognizergate. hasattrguard onruntime.record_recognition_examplekeeps the pipeline tolerant of non-ChatRuntimeruntimes (test doubles, alternative shells).
Invariants
- Refused recognition: no producer call (gated inside
if _rec_outcome.admitted:). - No attached recognizer: no recognition runs at all, no producer call.
- Per-turn FeatureBundle is the validated proposition emitted by
recognize— no shape massaging in the pipeline. recognizeis unchanged;derive_recognizeris unchanged; trace hash bytes are unchanged for any given turn.
Out of scope
- Bootstrap of the very first recognizer. This ADR closes the loop given a recognizer is attached. No path in production code seeds the first recognizer from operator review or reviewed teaching examples; that is a substrate-liveness concern tracked separately under the ADR-0143 / substrate-liveness audit family.
- Unbounded growth of
_pending_recognizer_exampleswhen the consumer flag stays off. With flag=False, the producer accumulates forever. Acceptable for short sessions; a future bound (LRU or cap) should ship before long-running operators enable the producer with the consumer off.
Validation
tests/test_adr_0154_recognizer_producer_wiring.py:
- admitted turn appends
(tokens, bundle)to the producer queue (flag=False so the queue is not drained at checkpoint) - producer fires when consumer flag is off
- refused turn does not populate the queue
- end-to-end loop: with flag=True, an admitted turn feeds the
producer queue, then
checkpoint_engine_statedrains it viaderive_recognizerand registers the result - multiple admitted turns accumulate in order
CLI lanes: core test --suite cognition (120 + 1 skipped),
core test --suite smoke (67), recognition phase 1/2 + refusal
propagation (25) all green.
Closure
After this ADR, the DerivedRecognizer registry can grow from live traffic. The remaining gap is bootstrap — getting the first recognizer into the registry without test-only injection. That is a substrate-liveness scope concern, not W-020b.