Skip to content

fix(bot-turns): 上限を「総数」ではなく「速度」で見る — 24時間ループが1時間で止まる件 - #16

Merged
tsubouchi merged 2 commits into
mainfrom
fix/bot-turn-rate-window
Aug 2, 2026
Merged

tsubouchi merged 2 commits into
mainfrom
fix/bot-turn-rate-window

Conversation

@Bonginkan-Marial

Copy link
Copy Markdown
Contributor

結論

暴走ループ防止のカウンタが人間の発言でしかリセットされない総数だったため、意図した長時間ループも必ず止まっていた。総数ではなく時間窓あたりの速度で見る形へ変更。

実際に起きたこと(2026-08-02)

HANA / REINA / North Star に24時間分の作業を渡したが、1時間以内に3体とも無言になった。

bot turn limit reached channel_id=1514453251452309614 turns=100 max=100   02:52:20Z
bot turn limit reached channel_id=1520561711961210880 turns=100 max=100   03:16:36Z

ルーティング判定の内訳:

体 mention_required bot_turn_limit dispatched
HANA 256 42 2
REINA 269 42 1
North Star 307 9 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 が推論速度の限界で応答し合えば毎分数十通が永久に続き、作業しているループは毎分数通で前へ進みます。

  • カウンタを時間窓(既定10分)内の件数へ変更
  • 暴走は窓を埋め続けるのでトリップしたまま
  • 遅いループは通る
  • 洪水が止まれば人を待たずに自力で解除される
  • ハード上限も同じ窓で判定(既定窓なら毎分100通。soft limit の設定ミスに対する backstop であって、判断の二重化ではない)
  • 人間の発言による即時クリアは維持

窓長は 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_length 今回の障害そのもの。毎分3通を24時間続けて一度も止まらないこと
a_flood_still_stops 100通目でトリップ
a_flood_stays_stopped_while_it_continues 窓の内側で少し待っても解除されない
a_thread_recovers_once_the_flood_stops 人を待たずに戻る(元の実装に無かったもの)
a_human_still_clears_it_immediately 即時再開の道は残す
a_quiet_thread_stops_taking_up_room 窓の外の記録を捨てる

cargo test 579 / 579、cargo clippy --all-targets は本変更に対する指摘なし(既存 cron.rs の2件のみ)、cargo build --release warning 0。

適用後の運用

いま止まっているチャンネルは、この変更を反映すれば10分で自動的に解除されます。反映前でも、人間が一言発言すれば即座に再開します(discord.rs:546)。

…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
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

⚠️ This PR is missing a Discord Discussion URL in the body.

All PRs must reference a prior Discord discussion to ensure community alignment before implementation.

Please edit the PR description to include a link like:

Discord Discussion URL: https://discord.com/channels/...

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
@applego
applego requested a review from Shiratori-Maria August 2, 2026 04:37
@tsubouchi
tsubouchi merged commit d0aba68 into main Aug 2, 2026
16 checks passed
@applego
applego deleted the fix/bot-turn-rate-window branch September 14, 2026 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants