Repository navigation
fix(bot-turns): 上限を「総数」ではなく「速度」で見る — 24時間ループが1時間で止まる件 - #16
Merged
Merged
Conversation
…ve been The runaway guard counted every bot message in a thread since the last human one and never let go. That cannot tell a loop that is stuck from a loop that is working: any deliberate multi-hour run reaches a fixed total eventually and then stops until somebody happens to type something. On 2026-08-02 three bodies were given twenty-four hours of work and went silent inside the hour. The logs say what happened plainly -- `bot turn limit reached turns=100 max=100` on two channels, twenty-four minutes apart -- and the routing counts show 42, 42 and 9 messages dropped at the door against 1 to 3 dispatched. Nothing had failed. The processes were healthy, events were still arriving, and there was no path back that did not involve a person. What separates a runaway from a working loop is speed. Bots answering each other as fast as inference allows produce dozens of messages a minute forever; a loop doing work produces a few and gets somewhere. So the counter now looks at a window: sustained flooding trips it and, since the flood keeps the window full, stays tripped, while a slow loop passes and a thread recovers by itself once the flooding stops. Ten minutes by default, configurable. Long enough that a burst cannot be hidden by pausing for a moment, short enough that a genuinely throttled thread is usable again soon. The hard cap is windowed on the same terms, which at the default is a hundred messages a minute -- a backstop against a mis-set soft limit rather than a second opinion about it. A human message still clears the thread outright. The warning text changes with the behaviour. It used to say a human must reply to continue, which was true of the old code and would be a lie about this one. Timestamps outside the window are dropped as they are passed, so a quiet thread stops occupying the map -- the counters used to accumulate per thread and never be released. Tests cover the incident directly: three messages a minute for twenty-four hours never stops, a flood stops on the hundredth and stays stopped while it continues, and the thread comes back on its own afterwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LXqeYy8DAp3BdEHxsjHJL8
|
All PRs must reference a prior Discord discussion to ensure community alignment before implementation. Please edit the PR description to include a link like: This PR will be automatically closed in 3 days if the link is not added. |
CI runs clippy with -D warnings on the binary alone, where new() and DEFAULT_WINDOW were reachable only from tests and so counted as dead code. Rather than silence it, the two constructors collapse into one. The window length decides whether a thread throttles, so an entry point that supplies it silently means the same tracker behaves differently depending on which one built it -- worth removing on its own, not only to satisfy the lint. The config default now reads DEFAULT_WINDOW instead of repeating 600. Two copies of a default drift apart quietly, and the one that loses is whichever nobody looks at. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LXqeYy8DAp3BdEHxsjHJL8
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.
結論
暴走ループ防止のカウンタが人間の発言でしかリセットされない総数だったため、意図した長時間ループも必ず止まっていた。総数ではなく時間窓あたりの速度で見る形へ変更。
実際に起きたこと(2026-08-02)
HANA / REINA / North Star に24時間分の作業を渡したが、1時間以内に3体とも無言になった。
ルーティング判定の内訳:
何も壊れていませんでした。 プロセスは健全、Discord イベントも受信継続中、inflight 0件。メッセージが入口で捨てられていただけで、人間が発言する以外に戻る道がありませんでした。
原因
bot_turns.rsの設計:on_bot_message()がスレッド内の bot 発言をすべて数える(自分の発言も他botの発言も同一カウンタ。コメントに明記あり)on_human_message()のみ。呼び出し元はdiscord.rs:546の1箇所つまり、bot同士の往復が
max_bot_turns(既定100)に達した時点で、そのスレッドは人間が発言するまで恒久的に停止します。5体が同じチャンネルで往復すれば100回はすぐで、実測でも2チャンネルが24分間隔で連続到達しました。変更
暴走と正常なループを分けるのは「速さ」です。 bot が推論速度の限界で応答し合えば毎分数十通が永久に続き、作業しているループは毎分数通で前へ進みます。
窓長は
bot_turn_window_secsで設定可能(既定600秒)。10分にした理由は、短時間止めるだけで隠せない程度に長く、かつ本当に暴走したスレッドが程なく使える程度に短いこと。文言も変えました
旧: "A human must reply in this thread to continue bot-to-bot conversation."
自力で戻るようになったので、この文面はそのままだと嘘になります。「速度が落ちれば再開、人間が返信すれば即時再開」へ。
副次的な修正
窓の外の記録を通過時に捨てるため、静かなスレッドが map を占有しなくなりました。以前はスレッドごとの数値が残り続けていました。
テスト(新規6件、計579 pass)
a_long_running_loop_is_not_stopped_by_its_own_lengtha_flood_still_stopsa_flood_stays_stopped_while_it_continuesa_thread_recovers_once_the_flood_stopsa_human_still_clears_it_immediatelya_quiet_thread_stops_taking_up_roomcargo test579 / 579、cargo clippy --all-targetsは本変更に対する指摘なし(既存cron.rsの2件のみ)、cargo build --releasewarning 0。適用後の運用
いま止まっているチャンネルは、この変更を反映すれば10分で自動的に解除されます。反映前でも、人間が一言発言すれば即座に再開します(
discord.rs:546)。