Skip to content

feat(status): 待ち時間中の表示を、実イベントに裏づけられた状態表示にする - #14

Merged
tsubouchi merged 1 commit into
mainfrom
feat/truthful-live-status
Aug 2, 2026
Merged

tsubouchi merged 1 commit into
mainfrom
feat/truthful-live-status

Conversation

@Bonginkan-Marial

Copy link
Copy Markdown
Contributor

結論

streaming 中の placeholder が … だけで、ローカルLLMの待ち時間(実測 53〜198秒)ずっとそれが表示され、故障に見えていた。演出は残したまま、表示する状態を全て実イベント由来にした。

Owner報告のスクショで、HANAが … のまま数十秒。回答自体は後から正常に届いており、壊れていたのは表示だけ。

事実確認

streaming は config ではなく DiscordAdapter::use_streaming()=!other_bot_present(discord.rs:194)で決まる。Discord では既定でON。加えて adapter.rs:511-514 に既知のレースがあり、スレッド初回はbotが同席していても streaming になる。スクショはこれ。

実測(2026-08-02、3体が1つの推論サーバを共有):

体 モデル 応答
REINA qwen3-coder-30b-chat 1.03s
North Star qwen3.6-35b-a3b 15.7s
HANA hana-coder-ff711 14.6s(実計算1.2s、待ち13.4s)

リアクション(🤔)は生存signalにはなるが、どのツールが動いているか・何秒経ったか・そもそも何か届いたかは表せない。さらにローカル3体は tool_display = "none" のため、最初の本文が来るまで streaming 表示は空のまま。

変更

placeholder の中身を状態表示にする。メッセージは増やさない(編集ループが既に読んでいる watch チャンネルへ書くだけ)。最初の本文が来た時点で書き込みをやめ、以降は回答がメッセージを占有する。

⏳ 受け取りました · 4秒
🤔 考えています · 6秒
🔧 実行中: bash: uv run python -m unittest · 21秒
… 反応がありません(直前: 考えています) · 48秒 ツール2件(失敗1)

「嘘をつかない」の担保

装飾は字面とレイアウトだけ。主張部分は次の規律で縛り、テストで固定した。

規律 意味 テスト
phase はそれを証明するイベントでしか遷移しない 何も届いていないのに「考えています」と言わない。初期行は「受け取りました」=こちら側の事実だけを主張 it_does_not_claim_thinking_before_anything_is_heard / every_rendered_phase_was_caused_by_its_event
tick() は Silent への降格しかしない 時間は作業の証拠ではない time_alone_never_promotes_a_phase
Silent は無signalを無signalとして言い、直前に実際に聞いた状態を添える 待ちと詰まりは外から同じに見え、経過時間だけが手がかり silence_is_reported_as_silence / recovering_from_silence_requires_an_event
実行中ツールは沈黙ではない 開始は聞いた、終了は聞いていない。長いbuild/testが誤って「無反応」にならない a_running_tool_is_not_silence
ツール名は verbatim、失敗は数える 表示と実際の動作を食い違わせない/失敗を消さない the_tool_name_is_shown_verbatim / tool_failures_are_not_hidden
経過時間は巻き戻らない elapsed_time_never_goes_backwards

ハンドオーバーは send() 内で同期的に立てる。チャンネル経由だと tick が本文を上書きする窓が残る(text_hands_the_message_over_immediately)。

状態行には zero-width space の marker を付け、is_placeholder_display() が「回答ではなく足場」と判別できるようにした。steer 時に状態行を残さないため。文言一致だと表現を変えた瞬間に壊れる。

設計上の削除

当初は「別メッセージを遅延投稿して完了時に削除」で作り、placeholder 自体を置き換える形へ変えた際に不要になった delay_ms・本文文字数カウント・Phase::Writing を削除。本文が届いた時点で本文そのものが表示されるので、文字数を出す機会が無い。動かないコードは残さない。

既定

enabled = false。全メッセージの見た目が変わるので、デプロイ側が明示的に選ぶべき変更。有効化は [live_status] enabled = true(tick_ms 既定2000、silence_after_ms 既定10000)。

検証

結果
cargo test 551 / 551
cargo clippy --all-targets 指摘なし
cargo build --release warning 0

うち新規18件が live_status。

未対応(別issue相当)

adapter.rs:511-514 の multibot レース自体は直していない。bot同席スレッドの初回が streaming になる件で、本PRの範囲外。

A streaming turn opens by posting a message containing "…" and edits it as
output arrives. On Discord that path is the normal one -- use_streaming()
is `!other_bot_present` (discord.rs:194), not a config flag -- so almost
every turn starts this way.

Against a hosted model the placeholder is gone in under a second. Against
a local one it is not: measured today, three bodies share one inference
server and a two-token reply took 14.6s wall for 1.2s of compute, with
turns of 53s, 86s and 198s in the logs. For all of that time the channel
shows a message whose entire content is "…", which is what a broken bot
looks like. Reported from a real thread where the answer did arrive, and
only the wait looked wrong.

The reaction machine already signals liveness, but a single emoji cannot
say which tool is running, how long it has been, or whether anything has
been heard at all -- and with tool_display = "none" (every local body)
the streaming display stays empty until the first text, so the emoji is
the only signal there is.

So the placeholder now carries a status line instead of an ellipsis. No
new message: it writes into the same watch channel the edit loop already
reads, and stops on the first text chunk so the answer owns the message
from then on. The handover flag is set synchronously in send() rather
than through the channel, or a tick could overwrite real output.

    ⏳ 受け取りました · 4秒
    🤔 考えています · 6秒
    🔧 実行中: bash: uv run python -m unittest · 21秒
    … 反応がありません(直前: 考えています) · 48秒 ツール2件(失敗1)

The constraint the module is built around is that none of this may be
decoration pretending to be information:

  - a phase is entered only by the event that proves it, so there is no
    "thinking" before anything has been heard -- the opening line claims
    only that the request was accepted, which is a fact about us
  - tick() can demote to Silent and nothing else; time is not evidence of
    work
  - Silent names the absence of signal and recalls what was last actually
    heard, instead of continuing to display stale activity as current.
    Waiting and wedged look identical from outside, and the elapsed time
    is the only thing that separates them
  - a running tool is not silence: we were told it started and not told
    it ended
  - tool titles are shown verbatim; failures are counted, not dropped

Tests assert those properties directly, including that no label appears
without its event and that ticking never advances the phase.

Off by default. It changes what every message looks like, which is not
something a deployment should get without asking.

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 1, 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.

@tsubouchi
tsubouchi merged commit 0e45ee9 into main Aug 2, 2026
16 checks passed
@applego
applego deleted the feat/truthful-live-status 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