Skip to content

fix: stop regenerating answers over quality guesses - #167

Merged
devhims merged 8 commits into
mainfrom
fix/reliable-answer-validation
Oct 7, 2026
Merged

devhims merged 8 commits into
mainfrom
fix/reliable-answer-validation

Conversation

@devhims

@devhims devhims commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Problem / Motivation

Final answers fail in production because validation treats quality guesses as hard failures. Session ba0c904a (run 3809e44c) produced two complete answers that were both rejected with UNGROUNDED_ANSWER, then fell back to a partial summary and an error.

The most likely cause is the cross-video name rule. Four Manali videos had been analyzed. Findings about Rohtang Pass came from Hindi-script transcripts, while only one other video recorded the English name, so any paragraph that mentioned "Rohtang Pass" and cited the video those facts came from would be rejected. The numerical rule could not plausibly fire, because the evidence held no mass or percentage figures. This is an inference from the logged error code, the saved analyses and the validator code: the rejected drafts were not saved, so the exact failing paragraph cannot be confirmed. The rejected-answer traces included here make the next case directly readable.

Other rules had the same shape: a regex guess that, when wrong, discards a complete answer and restarts generation, adding 10 to 20 seconds and resetting the streamed draft.

Rule

An answer is regenerated (once) only when it is unusable:

  • output that fails the transmitted schema
  • an answer cut off at the output limit with no complete block
  • a research answer with no resolvable citation
  • an answer made only of filler or a promise of future work

When the application knows the correct result, it corrects the answer. When a specific claim is likely unsupported, the claim is qualified where it appears, not only in a note. Everything else is recorded for review.

What changed

Check Before Now
Name cited from a different video Regenerate Removed
Figure whose cited source has the same value with a different unit (54.2 g vs 54.2%) Regenerate Marked 54.2 g (unverified) in the answer, plus UNVERIFIED_FIGURES note
Other mass/percentage figure absent from the block's citations Regenerate UNVERIFIED_FIGURES note listing the figures
Citation matching no saved excerpt, or several different ones Fail the save Shown as [source unavailable] where it appeared (as deleted sources show [source deleted]), CITATIONS_UNAVAILABLE note
Inline reference to real evidence its block did not declare Regenerate Kept where the model placed it
Inline marker with no usable reference Regenerate Shown as [source unavailable]
Output-token limit reached Regenerate Blocks that closed are kept and a block cut off mid-way is dropped, ANSWER_TRUNCATED note. JSON that closes cleanly despite the limit is kept whole, and the review trace records which case occurred. Regenerate only if no complete block remains
First-message answer missing the exact text Regenerate Whole answer replaced by the stored message
Comparison subject not cited Regenerate PARTIAL_EVIDENCE note naming the subject
Unsolicited needsEvidence field Regenerate Ignored
Numbered-item count mismatch Regenerate Logged for review only
Sentence split across blocks, repeated warning words Regenerate Removed
Filler-only answer Regenerate Unchanged: one repair, then fail

The transcript analyst keeps a finding whose figures cannot be matched, marked unverified, instead of discarding it. When some figures in a finding do match, the finding is still rewritten from those figures only. Grounding problems no longer trigger an analyst retry or fail an analysis.

Diagnostics: this PR includes the two trace commits from #165. Rejected answers are saved to private traces as final_answer_rejection. Accepted answers that received notes, corrections or review signals record an answer_review trace and an agent_answer_review log event, without answer text, so each check can be measured in production.

The Durable Object now hashes the accepted answer synchronously after building it, because marking citations unavailable can change the saved text. This also removes the only await before commit, so cancellation, deletion or a concurrent finalize can no longer interleave with acceptance.

Tests

All 1,592 unit tests and 348 runtime/session integration tests pass. Type checking passes. Tests that encoded the old rejections now assert the new outcome. Tests that used a quality rejection to exercise the repair path now use a schema-invalid or filler answer, so the repair path keeps its coverage. New tests cover:

  • a regression built from session ba0c904a (Hindi transcripts, Rohtang Pass cited from another video)
  • truncated-answer salvage, including complete blocks with only the outer JSON missing, and text containing quotes, braces and the word blocks
  • replacing a contradictory first-message answer
  • inline marking of likely unit errors, and leaving other unverified figures unmarked
  • [source unavailable] at save time and for unusable inline markers
  • the figure check never throwing on arbitrary input
  • the answer-review trace

Scope

This PR changes validation only. It does not implement synthesis-only finalization; the existing context-gathering reads remain. The research-handoff commit from #165 is not included. Failover timers that restart long streaming answers are a separate issue.

Compatibility and deployment

No migration. Warning codes are free-form strings, so the new note codes need no contract change. Existing traces and answers are unaffected.

Validation now blocks an answer only when the application cannot render
or persist it: output that fails the transmitted schema, an answer cut
off with no complete block, or a research answer with no resolvable
citation. Every other check repairs the answer or attaches a note under
Source notes and limitations, and never triggers regeneration.

- Unmatched or ambiguous citations are removed with a CITATIONS_REMOVED
  note instead of failing the save.
- Inline markers for undeclared references are stripped.
- Answers that reach the output limit keep their complete blocks with an
  ANSWER_TRUNCATED note.
- Mass and percentage figures missing from a block's own citations add
  an UNVERIFIED_FIGURES note.
- Uncited comparison subjects add a PARTIAL_EVIDENCE note.
- First-message requests quote the stored text directly.
- Numbered-item, filler, sentence-split and repeated-warning heuristics
  no longer reject answers; the first two are logged for review.
- The transcript analyst keeps findings with unverified figures instead
  of discarding them, so grounding problems never fail an analysis.
- Accepted answers with notes record a private answer_review trace.
- Answer acceptance hashes the saved text synchronously, which removes
  the async gap before commit.
@vercel

vercel Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
video2ctx-web Ready Ready Preview Oct 7, 2026 5:27pm UTC

…nswers

Truncation recovery dropped the last block of every cut-off answer, even
when that block had closed and only the outer JSON was missing. A
complete two-item comparison lost its second item, and a complete
single-block answer was regenerated. Recovery now keeps every block
whose object closed in the raw text and drops only a block cut off
mid-way. JSON that closes cleanly despite the limit still loses its last
block, which may have been closed early.

A first-message answer without the exact stored text was prefixed with
the correct quote, leaving the model's wrong statement underneath. The
application knows the answer, so it now replaces the whole answer.
Notes alone left unsupported claims confidently worded in the answer.
Following review, specific problems are now marked in place:

- A figure whose cited source has the same value with a different unit,
  a likely unit error, is marked "(unverified)" next to the figure.
- A citation that matches no saved evidence shows "[source unavailable]"
  where it appeared, as deleted sources already show "[source deleted]".
- An inline reference to real evidence that its block did not declare
  stays where the model placed it instead of being dropped.
- A filler-only answer is not an answer, so it gets one repair again.
@devhims devhims changed the title fix: reject final answers only when they cannot be rendered fix: stop regenerating answers over quality guesses Oct 7, 2026
The renderer keeps inline citations, but the figure check read only a
block's declared evidenceIds. A correct figure cited inline from source
B was marked unverified because only source A was declared, and an
answer citing only inline skipped the check entirely. Rendering, the
figure check and comparison coverage now share one reference set per
block: declared plus inline references after alias mapping.

Detection and inline marking also disagreed on Hindi digits: the checker
normalized ५४.२ ग्राम to 54.2g, but the marker searched the original text
with an ASCII-only pattern and never placed "(unverified)". Both now use
one matcher on the original text that reads ASCII, Devanagari and
fullwidth digits, so the mark lands on the exact figure that was flagged.
An answer that reached the output limit but whose JSON closed cleanly
lost its last block, on the theory that the provider might have closed
it early. That hid the case instead of showing whether it happens. Such
answers are now kept whole with the ANSWER_TRUNCATED note, and the
answer-review trace records how each length-limited answer ended:
dropped_block, complete_blocks or closed_cleanly.
@devhims
devhims merged commit 3b11b6e into main Oct 7, 2026
10 checks passed
@devhims
devhims deleted the fix/reliable-answer-validation branch October 7, 2026 17:55

This branch was successfully deployed

1 active deployment
Preview — 1677851b Deployed Oct 7, 2026 by vercel[bot]
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