[Backend] Emit structured, timestamped beats with evidence references #332
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: CRITICAL. Found in the August 2026 session lifecycle review (#319). The core of the accuracy re-architecture.
Why
The summary is currently free prose with no timestamps, no citations, and no required evidence — nothing lets a reader or a downstream check verify a claimed action against the transcript. That is why a total corruption of the input produced output that read fine to a human for months.
The codebase already knows how to do this properly:
extract_highlightsvalidates every quote by normalised substring match against the transcript and carries atimestamp_ref(audio_service.py:954-1101). The summary path has no equivalent. This issue generalises that proven pattern.Proposed fix
The map phase over each time window emits grammar-enforced JSON:
Constrain the shape at the decoder, not by asking politely — llama.cpp supports
json_schemato GBNF grammar (see the constrained-decoding spike, #281), and Anthropic/OpenAI support structured outputs.response_format: {"type":"json_object"}alone is advisory on the current deployment and is not sufficient.Beats become reusable structure, not a throwaway intermediate: a timeline UI, the input to highlights, the input to the lore extract phase (replacing today's second full-transcript pass — a net token saving across the pipeline), and an indexable event store for
/ask.Acceptance criteria
Verified; gaps fixed in
152048e.Criteria
EXTRACT_SCHEMA, shaped per provider by_apply_json_schema, request shapes tested per provider.minItemsbelow."Flagged as degraded" had never existed
grep -rn degradedacross the backend returned only unrelated docstrings. The tolerant fallback was there; nothing recorded that it had been used.The observable signature is that the response needed structural repair — on llama.cpp and Ollama the schema compiles to a decoding grammar, so a conforming server cannot emit malformed JSON. If repair was necessary, the grammar was not applied and the beats came from the tolerant parser rather than a guarantee.
This matters concretely because of #281: llama.cpp's own documented
response_formatshape is silently ignored by real servers, so everyjson_schemacall in production was doing nothing at all — with green unit tests throughout. That is exactly the condition this criterion asks to be made visible, and it still would not have been.Recorded on
summarisation_runs.schema_degradedand exposed through the API. Implemented as a callback rather than a changed return type, because a dozen tests monkeypatchextract_beatsand the signal was worth having without churning all of them.extract_beats was never invoked by any test
Every test in the suite patches it out, and the doubles return pre-parsed dicts — so the parsing it exists to do had zero coverage. "Tests cover a well-formed response, a malformed one, and a truncated one" was met one layer down at the transport, never here. Now covered directly: well-formed, unparseable, no
beatskey, repaired, and clean.The best-effort promise was not actually kept
_persist_summarisation_runflushed without a savepoint, inside the transaction that later commits the transcript and summary. Catching the exception does not clear a session needing rollback, so the nextcommit()would die ofPendingRollbackError— meaning a failure to record provenance would destroy the artifact the GM actually asked for. Precisely backwards, and the attendance block a few hundred lines away already had this right.The guarding test could not catch it:
test_a_failure_to_record_never_costs_the_summarypassesidentities=[object()], which raises inside the legend comprehension before any DB call, so it exercises the one failure mode that structurally cannot poison the transaction. There is now one that writes a NUL byte — Postgres rejects\x00in a text column, and a model can emit one — and asserts the session is still committable afterwards. Verified by mutation: it fails without the savepoint, and the original test passes either way.One criterion deliberately not met
minItems: 1on the evidence array. That constraint compiles into a decoding grammar, so a model that genuinely cannot cite a line would be forced to emit one — converting a visible"cites no transcript line"flag into an invisible fabricated citation. In a milestone built on not trusting the model, that trade is backwards. The validator already flags empty evidence, and_coerce_beataccepts it so the beat is recorded rather than dropped.Also
unsupported_sentenceswas computed on every run and dropped on the floor, despite #334's commit message claiming it landed on the record. Now persisted alongsideused_beatsandschema_degraded.Migration
f7a8b0c1d2e3. Closing.