Skip to content

flush_intermediates bypasses the deliverability gate, so intermediates ship unconditionally #838

Description

@adolfousier

Symptom

An audit reply arrived twice, two minutes apart: once at 16:49 as a numbered list, once at 16:50 as the same content in a table. Same meaning, different formatting, two separate Telegram messages.

Log for the turn:

16:50:27 Telegram: agent call completed — delivering final response
16:50:27 Telegram dedup: response.content len=976, sent_intermediates count=1

sent_intermediates count=1 proves an intermediate was sent as its own message. The final response was the 16:50 table; the 16:49 list was the intermediate.

Cause

There are TWO paths that deliver intermediates, and only one is gated.

Streaming loop (handler.rs) checks is_deliverable_rich_report before promoting an intermediate to its own message — the #582 rule that it must carry a table and clear a length floor.

flush_intermediates (intermediates.rs:17) sends every queued intermediate unconditionally. No classifier check anywhere in it. It pushes each one to sent_intermediates, which is the counter above.

Its own doc says it is "called from resume + follow-up paths", and follow_up_question.rs:117 is one caller. So any turn passing through those surfaces dumps its intermediates verbatim, table or not.

So "intermediates only surface when they carry a table" was only ever true for one of the two ways they reach a chat.

Why it was missed

A revert of the streaming classifier (64edc85d) was expected to stop this and did not, because it addressed the gated path. The binary was rebuilt and the duplicate persisted, which is what exposed the second path.

Worth recording: the conclusion "the streaming classifier promoted it" was drawn from sent_intermediates count=1 without first checking WHICH code path increments that counter. Both do.

Fix

Apply the same deliverability decision in flush_intermediates that the streaming path applies, so both agree on what earns its own message. An intermediate that does not qualify should fold rather than send.

Care

  • The dedup in delivery.rs will not save this. It strips a final response that overlaps a sent intermediate, but compares normalized TEXT — a bullet list and a table carrying the same meaning share almost no bytes, so nothing is stripped.
  • flush_intermediates runs where no user message is in scope (resume, follow-up), so anything it gains must not assume a live turn.
  • Whatever gate is used must be shared rather than duplicated, or the two paths will drift again exactly as they did here.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions