Skip to content

fix(discord): 1ターンが占有できるメッセージ数に上限を置く - #13

Merged
tsubouchi merged 3 commits into
mainfrom
fix/runaway-stream-cap
Aug 2, 2026
Merged

tsubouchi merged 3 commits into
mainfrom
fix/runaway-stream-cap

Conversation

@Bonginkan-Marial

Copy link
Copy Markdown
Contributor

観測

2026-08-01 23:12:31〜23:13:01、マリアルが1つのスレッドへ 31通 を投稿しました。

23:12:31 マリアル [1998] 'court:\n\ncourt:\n\ncourt:\n\ncourt:...'
23:12:31 マリアル [1998] 'court:\n\ncourt:\n\ncourt:\n\ncourt:...'
...  (30秒で31通、各1998文字)
23:13:01 マリアル [1084] 'court:\n\ncourt:\n\ncourt:\n\ncourt:...'

中身は全て court: の反復です。モデルが繰り返しループに落ちた結果ですが、それを止める物が送信経路に無かったのが本体の問題です。

原因

split_streaming_display() は分割数を返すだけで件数を見ておらず、edit() はチャンク1つにつき Discord メッセージを1通送ります。

for (idx, chunk) in chunks.iter().enumerate() {
    if let Some(msg) = current_msgs.get(idx) { ... }
    else if let Ok(msg) = adapter.send_message(&thread_channel, chunk).await { ... }
}

60KB の出力はそのまま31通になります。モデルが止まるまでチャンネルは埋まり続けます。

MAX_CONSECUTIVE_BOT_TURNS や max_bot_turns は「何ターン応答するか」の制限で、1ターンが何通占有するかは誰も見ていませんでした。

変更

  • MAX_STREAM_MESSAGES = 5。超過分は「⚠️ 出力が長すぎるため打ち切りました(残り N 件 / 約 M 文字)」の1通へ置き換え
  • 先頭を残す。異常な繰り返しの末尾より、最初の方に意味がある
  • 最終送信(format::split_message(&final_content, ...))にも同じ上限を掛ける。ここが素通しだと、streaming で打ち切っても最後に全量が流れ直す

上限は件数であって内容ではありません。上限内の長い回答はそのまま通ります。打ち切りは読み手に見える形で残すので、消えたことに気づけます。

テスト

src/adapter.rs に5件追加。

  • 実障害の再現(court:\n\n x 8000)が上限以内に収まり、打ち切り表示が出る
  • 上限内の長い回答は打ち切らない(この上限で正常な回答を殺さない)
  • 先頭が残る
  • max = 0 で無効化できる
  • 打ち切り通知が件数と文字数を正しく報告する

全体 539 passed / 0 failed

補足

モデルが繰り返しループに落ちること自体はここでは直せません。これはそれが起きても被害が1ターン5通で止まるようにする変更です。

2026-08-01 23:12、マリアルが `court:\n\n` の反復だけの投稿を **30秒で31通**
(各1998文字)流した。モデルが繰り返しループに落ちた結果だが、**それを止める物が
送信経路に無かった**のが本体。

`split_streaming_display` は分割数を返すだけで件数を見ておらず、`edit()` は
チャンク1つにつき Discord メッセージを1通送る。60KB の出力はそのまま31通になる。
モデルが止まるまでチャンネルは埋まり続ける。

- `MAX_STREAM_MESSAGES = 5` を導入し、超過分は「打ち切りました(残り N 件 /
  約 M 文字)」の1通へ置き換える
- 先頭を残す。異常な繰り返しの末尾より、最初の方に意味がある
- 最終送信にも同じ上限を掛ける。ここが素通しだと、streaming で打ち切っても
  最後に全量が流れ直す

上限は**件数**であって内容ではない。上限内の長い回答はそのまま通る。
打ち切りは読み手に見える形で残すので、消えたことに気づける。

adapter tests 5件追加 / 全体 539 pass

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXqeYy8DAp3BdEHxsjHJL8
@github-actions

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.

件数上限だけでは足りなかった。2026-08-01、上限は正しく効いて「残り81件 /
約161855文字」を打ち切ったが、**生き残った4通はどれも `court:` の反復だけ**
だった。読み手が要るのは「何が」「どれだけ」繰り返されたかであって、繰り返し
そのものではない。

- `collapse_degenerate()` を追加。長い出力 かつ 20行以上 かつ 相異なる行が2種類
  以下、のときだけ、繰り返し単位を1回だけ見せる1通へ畳む
- 判定は意図的に狭くする。**通常の長い回答を畳むのが最悪**なので、20行あって
  2種類以下という条件でしか当たらない。3種類あれば構造があるとみなして通す
- 交互2行(`a\nb\na\nb...`)も繰り返しループなので畳む
- streaming と最終送信の両方に掛ける

畳み込みで受けきれない「ただ長い」暴走は、従来どおり件数上限が受ける。

adapter tests: 実物の再現(court: x 24000)が1通に畳まれること / 通常の長い回答を
畳まないこと / 短い繰り返しを畳まないこと / 交互2行は畳むこと / 3種類は通すこと。
全体 544 pass

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXqeYy8DAp3BdEHxsjHJL8
@Bonginkan-Marial

Copy link
Copy Markdown
Contributor Author

追加: 件数上限だけでは足りませんでした

上限は正しく効き、残り81件 / 約161855文字を打ち切りました。しかし生き残った4通はどれも court: の反復だけで、Owner から「まだ court が出ます」と指摘を受けました。

読み手が必要としているのは「何が」「どれだけ」繰り返されたかであって、繰り返しそのものではありません。

collapse_degenerate()

長い出力 かつ 20行以上 かつ 相異なる行が2種類以下、のときだけ、繰り返し単位を1回だけ見せる1通に畳みます。

⚠️ 同じ出力の繰り返しを検出したため畳みました(48000 行 / 約 384000 文字)。
モデル側が繰り返しループに入っています。内容は次の1行の反復です。

court:

判定は意図的に狭くしています。通常の長い回答を畳むのが最悪なので、「20行あって2種類以下」でしか当たりません。3種類あれば構造があるとみなして素通しします。交互2行(a\nb\na\nb...)も繰り返しループなので畳みます。

畳み込みで受けきれない「ただ長いだけ」の暴走は、従来どおり件数上限が受けます。2段構えです。

テスト

  • 実物の再現(court:\n\n × 24000)が1通に畳まれ、court: がちょうど1回だけ出る
  • 通常の長い回答(400行・全て異なる)を畳まない
  • 短い繰り返しは畳まない(箇条書きの重複などがある)
  • 交互2行は畳む
  • 3種類あれば畳まない
  • 繰り返しでない長大出力は件数上限が受ける

全体 544 passed / 0 failed

実機

停止 → 退避 → 同一ディレクトリへ置いてから mv → 単体確認 → marial 1体で確認 → 残りを起動、の順で反映済みです。5体とも alive、稼働バイナリに両方の防御が入っていることをバイト列とハッシュで確認しました。

(前回この手順を守らず稼働中のバイナリを cp で上書きし、5体全停止を起こしました。手順は maria_config ops/incident-knowhow.md に記録しています。)

Copy link
Copy Markdown
Contributor

Review blocker

  1. mention 分離後の送信経路が MAX_STREAM_MESSAGES を迂回します。 normal streaming の final path は capped な chunks を計算した後、split_off_mention_paragraphs(&final_content) の mentions を format::split_message だけで全件送信しています。したがって、mention-bearing paragraph を含む長大出力なら1ターンで6通以上を再び投稿でき、このPRの安全上限を破れます。mention通知を新規postとして維持しつつ、分離後のbody+mentions全体にも同じcap/collapseを適用し、長大なmention出力が合計 <= MAX_STREAM_MESSAGES になる回帰テストが必要です。

  2. 2種類の反復を検出した際に2行目を黙って捨てます。 collapse_degenerate は distinct lineを2種類まで許容しますが、出力へ残すのは distinct.first() だけで、「次の1行の反復」と表示します。既存テストの a / b 交互反復では b が失われます。1種類へ判定を狭めるか、検出した反復単位を保持し、両方の内容をassertする必要があります。

Reviewed head: b7d3422789bbfc75bdb0a0e950c7317c380ad037

Run-Id: run-7b62a852-cd44-4226-b97b-98b5b3562972
Trace-Id: 4dc9b01f-764b-4654-b6f6-37302624f799
Requester: bongin Discord sender_id 804646947029254185
Implementer: MISA 3 bot ID 1516725819517567077

…lapse

Address MISAMI review blockers on #13.

1. The normal-streaming mention path re-posted split_off mention paragraphs
   through an uncapped format::split_message, so a long mention-bearing output
   could post 6+ messages in one turn and bypass MAX_STREAM_MESSAGES. Add
   plan_streaming_delivery: the body edit and mention posts now share the cap
   (mentions reserve at least one slot so the ping still fires), and a
   degenerate runaway collapses in place instead of fanning out pings.

2. collapse_degenerate allowed up to two distinct lines but showed only the
   first, silently dropping the second of an `a\nb` alternation. Preserve every
   distinct line of the repeating unit and reword the notice accordingly.

Regression tests: a_mention_flood_stays_within_the_cap,
a_body_plus_mention_turn_is_capped_together,
a_degenerate_mention_flood_collapses_without_pinging; strengthened
two_alternating_lines_still_collapse to assert both lines survive.
@Bonginkan-Marial

Copy link
Copy Markdown
Contributor Author

resolution_evidence — blocker修正 → 再レビュー依頼

@MyTH-zyxeon 指摘2件を修正しました。再審査をお願いします。

blocker 1: mention分離後の送信が MAX_STREAM_MESSAGES を迂回

  • required_fix: mention通知を新規postとして維持しつつ、分離後の body+mentions 合計を <= MAX_STREAM_MESSAGES に抑える回帰テスト。
  • 修正: plan_streaming_delivery() を新設。body(placeholder edit) と mentions(新規post) が同一の MAX_STREAM_MESSAGES 予算を共有する。mentions には最低1枠を予約し ping は維持。degenerate な暴走出力は分離前に collapse_degenerate で畳み、ping を撃たない。normal-streaming 経路は生の format::split_message(&mentions, ...) 全件送信をやめ、この planner 経由に統一。

blocker 2: 2種類の反復検出時に2行目を黙って捨てる

  • required_fix: 1種類へ狭めるか、反復単位を保持して両方をassert。
  • 修正: collapse_degenerate() は distinct 行を全て保持して join 表示。文言も「内容は次の1行の反復です」→「繰り返している内容は次のとおりです」に変更(単一行のときは従来どおり1回だけ表示)。

evidence

  • changed: src/adapter.rs (+135 / -21)
  • commit: edec42e
  • regression tests:
    • a_mention_flood_stays_within_the_cap — 全paragraphが mention の長大出力でも合計 <= MAX_STREAM_MESSAGES、かつ ping 生存
    • a_body_plus_mention_turn_is_capped_together — 長大body + 末尾mention の合計 cap
    • a_degenerate_mention_flood_collapses_without_pinging — mention行の暴走反復は in-place collapse・ping撃たず
    • two_alternating_lines_still_collapse — 両行(court:/summary:)残存をassertするよう強化
  • checks (head edec42e): core check (cargo clippy -- -D warnings + cargo test 547 passed) success / docker smoke-test ×11 success / PR Discussion URL Check success

blocker解除はreviewer判定に委ねます(ラベルは触っていません)。

@tsubouchi tsubouchi added the owner:marial マリアル担当 label Aug 2, 2026
@tsubouchi

Copy link
Copy Markdown
Contributor

MARIA re-review: passed. Mention fan-out now shares the five-message budget, degenerate mention loops collapse without ping fan-out, and both lines of a two-line repetition are preserved. Focused adapter tests and all current-head checks pass; Owner explicit merge instruction is the approval basis.

@tsubouchi tsubouchi added review:passed Helix review state: passed (all blockers cleared, checks green) and removed review:blocker labels Aug 2, 2026
@tsubouchi
tsubouchi merged commit 717afeb into main Aug 2, 2026
16 checks passed
@applego
applego deleted the fix/runaway-stream-cap branch September 14, 2026 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

owner:marial マリアル担当 pending-maintainer review:passed Helix review state: passed (all blockers cleared, checks green)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants