Skip to content

Gateway follow-ups queued during a busy/compression window no longer vanish; overflow survives shutdown (#99882, salvage #99912) - #100972

Merged
teknium1 merged 4 commits into
mainfrom
p1/comp-recovery-fifo
Sep 2, 2026
Merged

teknium1 merged 4 commits into
mainfrom
p1/comp-recovery-fifo

Conversation

@teknium1

@teknium1 teknium1 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Gateway follow-ups that were queued into the FIFO overflow while a turn was busy (e.g. demoted interrupt → queue during in-flight compression) are no longer silently lost when the busy window ends without the post-turn promotion running: the next idle arrival rescues the orphaned chain and runs it first, in arrival order, exactly once — and the overflow tail now survives gateway shutdown alongside the slot.

Root cause: the overflow list (SessionState.conversation.queued_events) is only drained by _promote_queued_event at the _run_agent post-turn site. Any exit that skips that site (/stop, generation bump, turn exception, drain) left the tail populated on an idle session, and the fresh _process_message path never looked at it.

Changes

  • gateway/run.py — _rescue_orphaned_overflow (new, from fix(gateway): rescue orphaned FIFO overflow when session goes idle (#99882) #99912) + rescue at the idle claim site in _handle_message before _claim_active_session_slot. Follow-up on top of the salvaged commits: the helper now pops the oldest orphan and returns it as the current turn (the original left it in the slot too, so the post-turn _dequeue_pending_event ran it a second time), stages the next orphan in the slot so the drain continues the chain, and the call site parks the incoming event via _enqueue_fifo (slot when free, overflow otherwise). The rescued event's own source drives the turn.
  • gateway/run.py _stop_impl + gateway/shutdown_flush.py — sibling site of the same loss class: shutdown flushed only the adapter slot ([Bug] Gateway _pending_messages.clear() discards unrecoverable messages on shutdown - FTS corruption data loss #72680); flush_overflow_to_file now writes each overflow event in the slot-flush payload shape (+seq), so the existing recover_pending_to_db replays them on restart with no new reader.
  • tests/gateway/test_fifo_overflow_rescue.py (from fix(gateway): rescue orphaned FIFO overflow when session goes idle (#99882) #99912, contract updated) — single-orphan removal from both stores, 2-orphan chain staging, occupied-slot / empty no-ops, FIFO across rescue + new arrival, single-orphan-then-new-message lands in slot.
  • tests/gateway/test_shutdown_flush.py — overflow flush ordering/skip cases, round-trip through recover_pending_to_db, empty no-op.

Validation

Before (origin/main) After
Live repro /tmp/c1b_repro_fifo.py (real GatewayRunner, real BasePlatformAdapter.handle_message, real SessionStore+SessionDB in temp HERMES_HOME, real compression lock; only the LLM scripted) ORPHAN STATE ... True; FINAL turns=['Sent', 'new idle message D', 'follow-up C (re-sent)'] → C ran only after D's full turn (FIFO violated; any boundary in that window drops it) Rescued orphaned FIFO overflow event ... (#99882) WARNING; FINAL turns=['Sent', 'follow-up C (re-sent)', 'new idle message D']; CLEAN: dispatched exactly once, before D
Original #99912 shape under the same repro — TURNS=['Sent','C','C','D'] (double dispatch) → fixed by the follow-up commit
Sabotage (restore original helper shape) — 4/6 rescue tests fail
pytest tests/gateway/test_fifo_overflow_rescue.py test_shutdown_flush.py test_pending_queue_spool.py test_queue_consumption.py test_queue_command.py test_steer_fifo_overwrite.py test_priority_path_compression_demotion_56391.py test_session_race_guard.py test_restart_drain.py test_goal_continuation_drain.py test_session_stall_watchdog.py test_busy_session_ack.py test_max_concurrent_sessions.py — 93 passed, 2 skipped
ruff check on touched files — clean

Live repro: real gateway runner + adapter dispatch, compression lock held during the busy window, /stop ends the window — before: follow-up C orphaned in queued_events on an idle session, dispatched only after unrelated message D; after: C rescued on D's arrival, runs exactly once before D, overflow empty.

Closes #99882
Salvages #99912 — commits by @salch-cred cherry-picked with authorship preserved.

Infographic

fifo-orphan-rescue

@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/gateway Gateway runner, session dispatch, delivery sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Sep 2, 2026
salch-cred and others added 4 commits September 2, 2026 00:24
…99882)

When a follow-up is demoted to /queue during compression-in-flight,
it lands in SessionState.conversation.queued_events (overflow) with
the slot event in adapter._pending_messages.  After the slot's turn
completes, _promote_queued_event should move the overflow head into
the slot for the recursive drain.  When that drain never runs — the
#99882 shape: busy window ended through an exit that skipped the
promotion site — the overflow is silently orphaned: never dispatched,
never persisted, never logged.  A 170-char Telegram follow-up vanished
without a trace; its re-send also vanished for the same reason.

Fix: _rescue_orphaned_overflow stages one orphan into the empty slot
on the next idle arrival, and the new message is enqueued behind it
so FIFO order (#28503) holds — oldest orphan runs as this turn, the
rest drain in order, the new message last.  The helper is best-effort
(slot occupied or no overflow → no-op) and logs at WARNING when it
fires so a future drain regression is visible.

Tests (tests/gateway/test_fifo_overflow_rescue.py, 4 cases on the real
GatewayRunner FIFO):
- moves overflow head to empty slot
- no-op when slot occupied
- no-op when no overflow
- FIFO preserved: orphan-1, orphan-2, new-msg in exact arrival order

Existing queue suites pass unchanged (test_queue_consumption — 5 passed).

Fixes #99882
Review note on #99912: rescued = 1 followed by if rescued: is a constant
conditional — the log block runs unconditionally now that staging is
single-orphan by design.
…rder (#99882)

Follow-up to the salvaged #99912 rescue. The original helper left the
rescued orphan IN the adapter slot while the caller also swapped it in as
the current turn, so the post-turn _dequeue_pending_event ran the same
follow-up a second time (live repro: TURNS=['Sent','C','C','D']). The
helper now pops the oldest orphan and returns it to run as this turn,
stages the NEXT orphan in the slot so the drain continues the chain in
arrival order, and the call site parks the incoming message behind the
chain via _enqueue_fifo (slot when free, overflow otherwise) instead of
always appending to overflow. The rescued event's own source drives the
turn so reply anchors point at the message actually being answered.

Tests: contract updated for the new return type; added the 2-orphan chain
case and the single-orphan-then-new-message slot case (both fail against
the original helper shape).
…99882)

Sibling site of the same loss class. The #72680 shutdown flush only
serialised the adapter slot (_pending_messages); the FIFO tail parked in
SessionState.conversation.queued_events was discarded with the process,
so every follow-up queued behind the head at restart time vanished the
same way the idle-orphan did. flush_overflow_to_file writes one payload
per overflow event in the slot-flush shape (plus seq for arrival order),
so the existing recover_pending_to_db startup replay inserts them with no
new reader. Wired into _stop_impl beside the slot flush.
@teknium1
teknium1 force-pushed the p1/comp-recovery-fifo branch from 24d4f2c to cffd44d Compare September 2, 2026 07:24
@github-actions

github-actions Bot commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on cffd44d — fix(gateway): flush the FIFO overflow tail to disk at shutdo

⚠️ Warnings

OSV vulnerability scan · View job

13 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


debug info

CI timings

CI timings · View report · View job

Wall time 4m14s vs 5m18s (-20.1%). 1 job(s) slower, 13 faster,

  • OS-specific tests / Windows-only tests: -62.0s
  • Python lints / ruff enforcement (blocking): -53.0s
  • Check no committed infographics / check-no-committed-infographics: -47.0s
  • Python tests / Run tests: -36.0s
  • Check contributors / check-attribution: -25.0s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P1 High — major feature broken, no workaround sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Queued follow-ups (demoted to queue during context compression) are silently orphaned — messages lost, no log

3 participants