Skip to content

fix(gateway): skip queued-follow-up re-send when stream consumer already delivered - #172

Open
hashbender wants to merge 1 commit into
mainfrom
mirror/pr-56092
Open

hashbender wants to merge 1 commit into
mainfrom
mirror/pr-56092

Conversation

@hashbender

Copy link
Copy Markdown
Owner

What does this PR do?

Fixes a race condition where the gateway's queued-follow-up path re-sends a response that was already delivered to the user by the stream consumer.

When a response includes image attachments, the stream consumer sends the text, then _deliver_media_from_response() sends the images separately. If background processes finish around the same time and inject notifications, the queued-follow-up path checks _stream_confirmed_final_delivery() which only looks at final_response_sent. If the stream task was cancelled after the send (e.g. image delivery or cleanup exceeded the 5-second wait), final_response_sent stays False and the response is re-sent as a duplicate.

The fix adds a fallback check: if the stream consumer's already_sent property is True, treat delivery as confirmed. This only applies to the queued-follow-up path — the normal send path is unaffected.

Related Issue

Fixes NousResearch#55806

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • gateway/run.py: Added fallback check in the queued-follow-up path — when _stream_confirmed_final_delivery() returns False but the stream consumer's already_sent is True, skip the re-send. This prevents duplicate responses when the stream task is cancelled after delivering the message.
  • tests/gateway/test_duplicate_reply_suppression.py: Added 2 regression tests covering the new fallback (consumer already sent → skip, consumer never sent → re-send).

How to Test

  1. Run pytest tests/gateway/test_duplicate_reply_suppression.py -q — all 28 tests should pass (26 existing + 2 new).
  2. The bug requires a specific timing scenario (stream consumer sends text, image delivery takes >5 seconds, background process notification arrives during the gap). The regression tests verify the logic without requiring the timing race.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 26.4.1

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A (logic-only change, no platform-specific behavior)
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Screenshots / Logs

From the issue reporter's gateway logs:

17:59:06  [Discord] Sending response (672 chars)    ← stream consumer sent the text
17:59:06  [Discord] Sending 1 image(s)              ← image delivered separately
17:59:11  Process finished — injecting agent notification
17:59:14  Queued follow-up: final stream delivery not confirmed; sending first response  ← BUG: duplicate

With the fix, the queued follow-up path sees already_sent=True on the stream consumer and skips the re-send.


Mirror-of: NousResearch#56092
NousResearch#56092

@tenki-reviewer

tenki-reviewer Bot commented Jul 1, 2026 •

Copy link
Copy Markdown

Review Complete

Files Reviewed: 2
Findings: 1

By Severity:

  • 🟠 High: 1

PR adds duplicate-reply suppression to the gateway's queued-follow-up path, but the fallback check uses already_sent (true for any partial output) instead of final_content_delivered (true only when the final response reached the user), risking dropped answers when the stream task is cancelled after partial delivery.

Files Reviewed (2 files)
gateway/run.py
tests/gateway/test_duplicate_reply_suppression.py

@tenki-reviewer tenki-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: 🟠 High (68/100) — 1 high finding · 58 LOC across 2 files


Single High-Severity Finding

This PR modifies gateway/run.py to suppress duplicate reply delivery in the queued-follow-up path by checking whether the stream consumer already sent content. However, the fallback check at line 18067 uses _sc.already_sent rather than _sc.final_content_delivered.

The Bug

  • _sc.already_sent (defined in stream_consumer.py line 238-240) is True whenever any content reached the platform — tool-progress commentary, partial fallback chunks, interim edits. It is deliberately not reset at tool boundaries (line 344-345).
  • _sc.final_content_delivered (line 253-256) is the dedicated flag for "the final response content reached the user, even if cosmetic finalization failed."
  • The normal delivery path at line 18252-18253 already correctly checks final_content_delivered.
  • The queued-follow-up fallback added by this PR checks already_sent instead.

Impact

If the stream task is cancelled after sending tool-progress commentary (already_sent=True) but before the final answer is delivered (final_content_delivered=False), the fallback incorrectly skips the re-send and the user receives no answer. This is a real risk because the stream consumer explicitly documents (line 593-600) that already_sent can be True without the final response having been delivered.

Fix

Change the fallback check from already_sent to final_content_delivered, update the surrounding comments, and update the corresponding test at test_duplicate_reply_suppression.py line 387 to set the correct flag.

Comment thread gateway/run.py
Comment on lines +18067 to +18076
if (
not _already_streamed
and _sc is not None
and getattr(_sc, "already_sent", False)
):
_already_streamed = True
logger.debug(
"Queued follow-up for session %s: stream consumer already_sent=True but final_response_sent=False; skipping re-send.",
session_key or "?",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Queued-follow-up fallback uses already_sent instead of final_content_delivered, risking dropped final responses (bug)

In gateway/run.py lines 18067–18076, the PR adds a fallback that treats _sc.already_sent=True as confirmation the final response was delivered, skipping the re-send. However, already_sent is set whenever any content (tool-progress commentary, partial fallback chunks, interim edits) reaches the platform — it does NOT guarantee the final answer was delivered. The stream consumer explicitly documents this at line 593-600: '_already_sent may be True from prior tool-progress edits or fallback-mode promotion — that doesn't mean the final answer reached the user.' The _reset_segment_state() method (line 344-345) resets _final_response_sent and _final_content_delivered at tool boundaries but intentionally does NOT reset _already_sent, so the flag can be stale from earlier segments. The flag designed for this exact purpose — final_content_delivered (line 253-256: 'True when the final response content reached the user, even if the subsequent cosmetic edit failed') — is used correctly by the normal delivery path at line 18252-18253 but omitted from this queued-follow-up path. If the stream task is cancelled after sending tool-progress or partial text (already_sent=True) but before the final response is delivered (final_content_delivered=False), the fallback incorrectly suppresses the re-send and the user receives no answer.

💡 Suggestion: Replace the already_sent fallback check with final_content_delivered, which is the stream consumer's dedicated flag for 'final response content reached the user even if finalization/cursor-removal later failed.' This is the flag the normal delivery path already uses at line 18252-18253.

Suggested change
if (
not _already_streamed
and _sc is not None
and getattr(_sc, "already_sent", False)
):
_already_streamed = True
logger.debug(
"Queued follow-up for session %s: stream consumer already_sent=True but final_response_sent=False; skipping re-send.",
session_key or "?",
)
if (
not _already_streamed
and _sc is not None
and getattr(_sc, "final_content_delivered", False)
):
_already_streamed = True
logger.debug(
"Queued follow-up for session %s: stream consumer final_content_delivered=True but final_response_sent=False; skipping re-send.",
session_key or "?",
)
📋 Prompt for AI Agents

In gateway/run.py at lines 18067–18076, change the fallback check from getattr(_sc, 'already_sent', False) to getattr(_sc, 'final_content_delivered', False). Also update the comment block at lines 18060–18065 to reference final_content_delivered instead of already_sent, and update the corresponding test at tests/gateway/test_duplicate_reply_suppression.py line 387 to set final_content_delivered=True instead of already_sent=True so the test validates the correct signal.

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.

Gateway: queued follow-up resends previous response when stream delivery confirmation fails (image + background process race)

1 participant