fix(gateway): expire stale dangerous-confirmation text on replay (#59607) - #60110
Merged
kshitijk4poor merged 3 commits intoJul 7, 2026
Merged
Conversation
NousResearch#59607) When a high-risk side effect (e.g. host restart via shutdown.exe) runs, the user's plain-text confirmation phrase is persisted in the conversation transcript. If the host restart killed the gateway process before the assistant's tool result was written, the transcript tail ends on the assistant's text response - and the dangerous confirmation text remains in the user role. On the next inbound message - possibly a casual 'are you there?' from the user minutes later - the LLM sees the stale confirmation and may interpret the new turn as a fresh re-confirmation, re-executing the destructive action. This is the failure mode reported in NousResearch#59607. Fix: - Add strip_stale_dangerous_confirmations() in agent/replay_cleanup.py that removes user messages whose content matches a known dangerous confirmation pattern AND whose timestamp is older than 60 seconds. - Add is_dangerous_confirmation() helper with the matched patterns (i18n-aware: covers 確認強制重開機 from the original incident). - Wire the stripper into _build_gateway_agent_history() right after the existing 75ed07a strippers, so the strip chain is: strip_interrupted_tool_tails -> strip_dangling_tool_call_tail -> strip_stale_dangerous_confirmations. - Update _build_replay_entry() to preserve the timestamp on user messages (it was previously dropped), since the new stripper needs it. Complements 75ed07a (which strips the assistant side of the broken tail) by handling the user side: a stale plain-text confirmation that the assistant has not yet responded to in a way the resume logic recognises. Failing-test-first discipline: the bug-detection test test_stale_confirmation_text_is_stripped_on_resume fails on unfixed code (proves the test catches the bug) and passes after the fix. Five additional safety tests confirm no regression on: - fresh confirmations (within expiry) are preserved - non-confirmation text is preserved - non-matching histories are untouched - dangerous-pattern detection works in all cases (case, i18n, None) - direct unit test of the strip helper Refs: NousResearch#59607
Deleting the matched user message breaks the strict role-alternation
invariant on the exact incident tail this fix targets — user(confirm) →
assistant('OK, restarting') becomes two consecutive assistant messages,
which strict providers reject and which the alternation-repair passes
upstream don't cover. Replace the message content with an explicit
'confirmation EXPIRED, re-confirm before any destructive action'
sentinel instead: the trigger text is still neutralized, the model gets
an affirmative instruction not to act, and the message sequence stays
valid. Adds an alternation-preservation regression test.
Follow-up to the salvage of NousResearch#59640 by @knoal.
Gateway user replay entries now carry a timestamp (read by the stale-confirmation expiry check). The transports already sanitize it (NousResearch#47868), but handle_max_iterations hand-builds api_messages and calls chat.completions.create() directly, bypassing the transport — a strict provider would 400 on the foreign key. Mirror the transport's pop here, alongside the existing tool_name/codex_* sanitization.
kshitijk4poor
enabled auto-merge (rebase)
July 7, 2026 09:07
Collaborator
Related: salvage/cherry-pick of #59640 (@knoal, authorship preserved) with a redaction-in-place follow-up that keeps user/assistant alternation valid on the #59607 incident tail. This is the active superseding version. Also fixes #59607 (second, user-side failure mode) and relates to #49201 (distinct assistant-tail replay mechanism). Maintainer to pick between #59640 and this. |
This was referenced Jul 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A stale high-risk confirmation phrase ("confirm forced restart" / 確認強制重開機) left in the transcript tail by a host reboot can no longer trigger the destructive action a second time when the user later sends an unrelated message (#59607, second failure mode).
Root cause: the host reboot kills the gateway before the tool result is persisted, so the transcript tail keeps the user's confirmation text. On resume, the model reads it as a live confirmation and re-executes (observed: second
shutdown.exereboot 50 minutes later, triggered by "are you there?").Changes
agent/replay_cleanup.py:is_dangerous_confirmation()+strip_stale_dangerous_confirmations()— user messages matching known dangerous-confirmation patterns (incl. the i18n variants from the incident) whose timestamp is older than 60s are expired on gateway history replay — @knoal's fix, cherry-picked from fix(gateway): strip stale dangerous-confirmation text in user messages (#59607) #59640gateway/run.py: wire the stripper into_build_gateway_agent_history()after the existing tail strippers;_build_replay_entry()preservestimestampon user messages so the stripper can read it (transports already droptimestampbefore the API — Strict chat-completions providers reject leaked messages[].timestamp metadata #47868) — @knoaltests/gateway/test_stale_confirmation_expiry.py: 6 tests incl. a bug-detection test that fails on unfixed code — @knoaluser(confirm) → assistant("OK, restarting")→ two consecutive assistant messages). Redaction neutralizes the trigger, keeps the sequence valid, and gives the model an affirmative do-not-act instruction. + alternation-preservation regression test.Note: the pattern list is a bounded mitigation for the observed incident class, not a general solution — a tracked-approval-with-TTL mechanism at the approval layer would cover arbitrary phrasing. Filing that as a follow-up design issue.
Validation
Credit
Salvaged from #59640 by @knoal (commit cherry-picked; re-authored to their GitHub noreply identity since the original commit carried an anonymized agent email). Closes #59640. Refs #59607.