Skip to content

feat(discord): add wiki proposal decision buttons#5

Open
hamanori wants to merge 36 commits into
mainfrom
kanban/t_eb4b1d4a-wiki-save-ui
Open

feat(discord): add wiki proposal decision buttons#5
hamanori wants to merge 36 commits into
mainfrom
kanban/t_eb4b1d4a-wiki-save-ui

Conversation

@hamanori
Copy link
Copy Markdown
Owner

@hamanori hamanori commented May 17, 2026

Summary

  • Route Discord thread Wiki lifecycle clicks to proposal-only replies with Status: proposal only; no files have been changed yet.
  • Attach persistent Save/Edit/Skip decision buttons to Wiki proposal messages instead of normal lifecycle controls
  • Make Edit open a Discord modal and dispatch a revised proposal only after concrete user guidance is submitted
  • Route Save through an agent/Kanban request without synchronous Life file writes; Skip explicitly confirms no files changed
  • Rebased/rebuilt the PR branch on hamanori/hermes-agent:main so PR feat(discord): add wiki proposal decision buttons #5 is mergeable again

Test Plan

  • /Users/hamanori/.hermes/hermes-agent/venv/bin/python -m pytest tests/gateway/test_discord_news_article_buttons.py -q => 13 passed
  • git diff --check => pass
  • /Users/hamanori/.hermes/hermes-agent/venv/bin/python -m py_compile gateway/platforms/base.py gateway/platforms/discord.py => pass

hamanori and others added 30 commits May 10, 2026 00:26
* fix: make discord auto-thread names compact

* fix: defer gateway restarts and cap concurrent agents

* fix: initialize gateway agent concurrency cap

* fix: remove gateway-wide agent concurrency cap

* feat: add Discord TODO slash card controls

* feat: auto-update discord thread titles

* feat: add Discord TODO text card trigger

* fix(discord): harden gateway restart and message actions

(cherry picked from commit 1e8f240)
(cherry picked from commit 865d73f)
## 変更内容(What)
- tools/send_message_tool.py の Discord REST 送信で HTTP 429 を検出し、retry_after または Retry-After に従って再送する処理を追加した
- テキスト送信と multipart 添付送信の両方で最大4回まで retry するようにした
- 添付送信では retry ごとに FormData とファイル内容を作り直し、再送時に閉じた file handle を使わないようにした
- tests/tools/test_send_message_tool.py に retry_after JSON を解釈する単体テストを追加した

## 変更理由(Why)
- チラシ report.view.md のように複数の Markdown 表を画像添付として連続投稿すると、Discord の短時間 rate limit に当たり、最後の表画像が送信されずに終わることがあったため
- Discord API は 429 応答に retry_after を返すため、それに従って待てば送信を継続できるため

## 実装方法(How)
- Discord 専用の _discord_retry_after helper を追加し、429 以外では None、429 では待機秒数を返すようにした
- _send_discord の通常 channel 送信経路で session.post を小さな retry loop に包んだ
- multipart 添付は FormData を使い回せないため、attempt ごとにファイルを読み直して送信 payload を作る形にした

## 影響範囲・注意事項
- Discord の REST 送信経路のみが対象
- forum thread 作成経路には今回の retry は入れていない
- チラシ実データの再投稿で、最後の大きい表画像が添付として送信できることを確認済み

Co-Authored-By: OpenAI Codex <noreply@openai.com>
(cherry picked from commit e907a87)
- gateway/run.py の tool progress 送信で、metadata に関数オブジェクトではなく dict を渡すよう修正した。
- progress send / typing metadata に thread_lifecycle_buttons: False を常に含めるようにした。
- Telegram topic / Telegram DM / Slack DM の progress metadata テストを更新し、thread_id 維持と lifecycle button 抑止を同時に検証するようにした。

- Discord 通常スレッドで tool/progress 投稿にも lifecycle buttons が表示され、最後の bot 投稿にだけボタンを残す仕様に反していたため。
- 既存実装では metadata=_progress_metadata と関数そのものを渡しており、Discord adapter が非 dict metadata を無視して thread_lifecycle_buttons=False が効かなくなっていたため。

- _progress_thread_metadata という dict を作り、thread_lifecycle_buttons: False を必ず入れる形にした。
- source.thread_id または Slack DM の event_message_id fallback がある場合だけ同じ dict に thread_id を追加する。
- tool progress の send と send_typing はすべてこの dict を渡すように統一した。

- 通常の最終返信に付く lifecycle buttons の仕様は変更しない。
- news article feedback buttons の挙動は対象外。
- Hermes gateway の実運用に反映するには gateway restart が必要。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
(cherry picked from commit 903c286)
- DISCORD_ALLOW_MENTION_USERS に true/false だけでなく、カンマ区切りのDiscord user ID allowlistを指定できるようにした。
- gateway/config.py と gateway/run.py の config.yaml → env bridge で discord.allow_mentions.users のlist値をカンマ区切り文字列へ変換するようにした。
- gateway/platforms/discord.py の AllowedMentions生成で、allowlist指定時は discord.Object(id=...) のlistを users に渡すようにした。
- config bridge と Discord allowed_mentions の回帰テストを追加・修正した。

- Discord通知のメンション方針で、全ユーザーmentionを許可/禁止するだけではなく、特定ユーザーだけを安全にmention対象にする必要があったため。
- Hermesのruntimeでは gateway.run import時のbridgeと gateway.config のload時bridgeが併存しており、片方だけの変更では設定が反映されない経路が残るため。

- boolean文字列は従来どおり true/false として扱い、それ以外はカンマ区切りIDとしてparseする後方互換の分岐にした。
- IDは _clean_discord_id でmention記法も含めて正規化し、数値でないentryはwarningして無視する。
- 有効なIDが1件もないallowlist指定は、安全側としてuser pingsを無効化する。

- 既存の DISCORD_ALLOW_MENTION_USERS=true/false の挙動は維持する。
- allowlist指定はDiscord adapterのAllowedMentions usersに限定され、roles/everyoneのsafe defaultは変えない。
- 検証: UV_CACHE_DIR=/private/tmp/uv-cache uv run pytest tests/gateway/test_config_env_bridge_authority.py tests/gateway/test_discord_allowed_mentions.py が26 passed。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
(cherry picked from commit 679b064)
## 変更内容(What)
- cron scheduler の enabled_toolsets 解決で、job に enabled_toolsets が明示されている場合は空配列でも per-job override として扱うようにした。
- コメントに、明示的な空配列は tool なしを意味し、platform default への fallback ではないことを追記した。

## 変更理由(Why)
- hermes-random-mumble のような本文生成専用 job では、LLM に terminal や message tool を持たせないことで、LLM 側送信と scheduler auto-deliver の二重配送経路を根本的に避ける必要があるため。
- 旧実装では空配列が falsy 扱いになり、cron platform の default toolset に戻る可能性があったため。

## 実装方法(How)
- job dict に enabled_toolsets キーが存在し、値が None でない場合は list(job.get("enabled_toolsets") or []) を返すようにした。
- キー未指定の場合のみ、従来通り cron platform の toolset 設定へ fallback する。

## 影響範囲・注意事項
- enabled_toolsets: [] を設定した cron job は、意図通り tool なしで agent を起動する。
- enabled_toolsets キーを持たない既存 job は従来通り platform 設定または default fallback を使う。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
(cherry picked from commit 0576454)
- `gateway/platforms/discord.py` の news article feedback view に adapter 参照を渡せるようにし、`Deep Dive` 押下時に記事別スレッドを作成して Hermes の詳細調査依頼を dispatch する処理を追加した。
- news article feedback の `read` / `keep` / `skip` / `deep` を既存 reaction に加えて `~/.hermes/state/news_feedback.jsonl` へ永続化する流れに整理した。
- thread lifecycle の `Resume` ボタン処理を `_resume_thread_action` に切り出し、スレッドを再オープンした後に「止まっていた続きの応答」を Hermes に依頼するようにした。
- `tests/gateway/test_discord_news_article_buttons.py` に Deep Dive スレッド作成・agent dispatch と、Resume ボタンの継続応答 dispatch の回帰テストを追加した。

- Discord の記事単位ボタンで、気になったニュースを単なる feedback ではなく、その場で詳細スレッド化して深掘りできるようにするため。
- ユーザーが thread lifecycle のリトライ/Resume アイコンを押したとき、スレッドを開き直すだけではなく、会話の続きの応答まで自動で進めたいという要求があったため。
- ボタン操作後の挙動をテストで固定し、Discord UI 側の細かい callback 変更で継続応答や Deep Dive dispatch が壊れないようにするため。

- `NewsArticleFeedbackView` に任意の `adapter` を保持させ、スレッド作成後に既存の `_dispatch_thread_session` を使って Hermes agent へ詳細調査 prompt を非同期投入する形にした。
- Deep Dive のスレッド名は記事本文の先頭行から Markdown や URL を軽く除去して `深掘り: <title>` に正規化し、Discord のスレッド名上限に合わせて 100 文字に丸めた。
- Resume は既存の thread open / auto archive 延長処理を維持しつつ、短い ephemeral 応答の後に `_dispatch_agent_request` で継続応答用 prompt を送る構成にした。
- テストでは Discord の実 button callback 表現に依存しすぎないよう、Resume の本体処理を helper として直接検証できる形にした。

- Discord gateway の news article buttons と thread lifecycle buttons の挙動が変わる。
- Deep Dive では Discord のスレッド作成権限が必要。権限不足時は followup で失敗内容を返す。
- Resume は新しい依頼を作り直すのではなく、スレッド文脈から未完了の回答・作業・確認を再開する intent として agent に渡す。
- 破壊的変更はなし。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
(cherry picked from commit b71eaab)
- gateway/markdown_table_images.py を追加し、Discord向け本文から GFM pipe table を検出して PNG 画像セグメントへ変換する共通処理を実装した
- fenced code block 内の表を除外し、separator 行、カラム数、左右中央揃えを解釈する table parser を追加した
- Pillow による表画像レンダリングを追加し、透明背景、白文字、ヘッダー下と行区切りだけの最小罫線で Discord ダークテーマに馴染む見た目にした
- tools/send_message_tool.py、cron/scheduler.py、gateway/platforms/discord.py の Discord 送信経路で、本文の table segment を text/image/text の順序で送信するようにした
- 画像生成に失敗した場合は fenced code block に包んだテキストへフォールバックし、送信自体は止めないようにした
- pyproject.toml と uv.lock に Pillow 依存を追加した
- table parser / renderer / send_message / Discord adapter の関連テストを追加した

- Discord は Markdown の pipe table を表としてレンダリングしないため、Hermes / Codex が表を投稿すると読みにくい生テキストになっていたため
- 本文全体を画像にすると通常テキストの検索性や返信操作が落ちるため、表ブロックだけを画像に差し替える必要があったため
- Discord の背景が黒系なので、白背景の表画像ではなく透明背景と白文字で自然に読める見た目が必要だったため

- 送信直前の Discord 専用処理として table segment 化を行い、非 Discord 経路には影響を出さない構成にした
- Markdown の構文全体を扱うのではなく、今回必要な GFM pipe table だけを小さく検出・parse する実装にした
- レンダリングは headless browser ではなく Pillow を使い、Hermes gateway の通常送信経路に入れても依存と失敗要因が増えにくい形にした
- 生成画像は ~/.hermes/cache/discord-table-images/ 配下に timestamp と short hash 付きで保存する
- 通常応答、send_message、cron delivery の3経路で同じ segment ルールを使い、表の前後の本文順序を保つようにした

- Discord 送信のみが対象で、他 platform の送信挙動は変更しない
- HERMES_DISCORD_TABLE_IMAGES=0 で無効化できる
- Pillow が利用できない場合やレンダリングに失敗した場合は fenced code block fallback になる
- gateway/platforms/discord.py には未コミットの Todoist 関連変更が残っているが、このコミットには表画像送信 hunk だけを含めた

Co-Authored-By: OpenAI Codex <noreply@openai.com>
(cherry picked from commit d720dc8)
- gateway/platforms/discord.py の /todo 実装を、旧 dashboard/action 型から Todoist 追加フローへ変更した。
- /todo の slash option は text だけに整理し、text が空の場合はモーダルを出さず使い方を ephemeral に案内するようにした。
- /todo text:... では Life repo の todoist_classifier.py を読み込み、タスク本文から project / section / due / note を推測して確認カードを表示するようにした。
- 確認カードにはカテゴリ・セクション・期限の select menu と Add / Cancel ボタンを配置し、select menu の変更がプレビュー本文と Add 時の作成内容に反映されるようにした。
- Todoist への実追加は Life repo の todoist_client.py を使い、確認後に resolve_project_and_section と add_task を呼ぶようにした。
- tests/gateway/test_discord_slash_commands.py に、text-only /todo 登録、空 text の案内、分類器注入、確認カード、select menu 更新、dispatch しないプレビュー送信の回帰テストを追加・更新した。

- Hermes v0.12 移行後に、以前 Discord で使っていた /todo の Todoist 追加フローが失われ、action/issue/target を持つ操作カード型に戻っていたため。
- ユーザーの希望として、モーダルは使わず /todo text:... で入力し、カテゴリや期限はまず推測しつつ確認カード上のドロップダウンで修正できる形が必要だったため。
- Discord の native slash command では /todo foo のような空白引数は扱えず、text option と message component の組み合わせが最も安定するため。

- 既存の _build_todo_prompt / _build_todo_control_view / modal 作成フローを廃止し、Todoist 専用の分類・プレビュー・作成ヘルパーに置き換えた。
- Life repo への依存は importlib.util.spec_from_file_location で scripts/todoist_classifier.py と scripts/todoist_client.py を遅延ロードする形にし、gateway 起動時の副作用を避けた。
- 確認カードは discord.ui.View に Select と Button を動的に add_item する実装にし、select callback で items を更新して edit_message で再描画するようにした。
- Add ボタンでは現在の items をそのまま Todoist 作成処理へ渡すため、ユーザーがドロップダウンで直した値が保存対象になる。
- /todo の Discord 登録定義は text のみにし、project_key/section/due/note は slash option ではなく確認カード側へ寄せた。

- Discord の /todo は Todoist 追加専用の確認フローになる。旧 action/issue/target による today/week/view/done/move 操作はこの slash command からは使わない。
- Discord クライアント側に古い slash command 定義が見える場合は、Discord の候補キャッシュ更新が必要になることがある。
- TODO分類・Todoist作成は Life repo の scripts/todoist_classifier.py と scripts/todoist_client.py に依存する。
- ドキュメント更新は不要と判断した。今回の変更は runtime の /todo 表面仕様とテストの更新に閉じている。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
(cherry picked from commit 6fde950)
## 変更内容(What)
- gateway/markdown_table_images.py で Markdown 表を複数 PNG に分割できる render_table_pngs を追加した
- 表画像のデフォルトを本文5行単位にし、短い1枚表でも空行で5行分の高さへ揃えるようにした
- 分割された同一表の各パートで列幅、画像幅、画像高さを固定し、Discord 上で列位置と表示枠が揃うようにした
- 2枚目以降の分割画像ではヘッダー行を繰り返さず、本文行だけを表示するようにした
- 画像幅のデフォルトを1100pxに抑え、フォントを30px太字寄り、白文字 stroke 付きにして読みやすくした
- 折り返しが多い表でも列境界を追えるよう、薄い縦ガイドを追加した
- 既存の単一画像 renderer 呼び出しに互換する render_table_png を残しつつ、送信経路は複数画像を扱えるようにした
- 分割、複数 MEDIA タグ、固定サイズ、短い表の高さ揃えを検証するテストを追加・更新した

## 変更理由(Why)
- Discord の画像プレビューでは縦長の1枚表が縮小され、商品表の文字が小さく潰れていたため
- 分割画像ごとに列幅や高さが変わると、連続した表として読んだときに列がずれて見えたため
- 1行だけの表が小さい画像になると、セクション別のチラシ閲覧版で表示サイズがばらつくため
- チラシ閲覧版のような実データでは、短い表と長い表が混在しても同じ読み心地にする必要があったため

## 実装方法(How)
- 表全体から列幅を一度だけ計算し、その列幅をすべての分割パートへ渡すようにした
- 本文行を5行単位で chunk 化し、最後の chunk は空行で5行へ揃えるようにした
- 分割後に各 PNG を最大幅・最大高さへ正規化し、透明背景のまま余白を足して同じ画像サイズにした
- render_table_pngs は複数 path を返し、segment_markdown_tables は str と path list の両方を受けられるようにした
- テストでは Pillow で生成画像の幅・高さを直接確認し、Discord 送信テストの monkeypatch 対象も複数画像 API に合わせた

## 影響範囲・注意事項
- Discord 向け Markdown 表画像の見た目と添付枚数が変わる
- HERMES_DISCORD_TABLE_IMAGE_MAX_BODY_ROWS で5行単位の分割数を調整できる
- HERMES_DISCORD_TABLE_IMAGE_MAX_WIDTH で最大幅を調整できる
- gateway/platforms/discord.py と tests/gateway/test_discord_slash_commands.py に残っている未コミット変更は今回のコミットに含めていない

Co-Authored-By: OpenAI Codex <noreply@openai.com>
(cherry picked from commit 03e4ddf)
## 変更内容(What)
- `gateway/platforms/discord.py` に Discord native slash command `/chatgpt-pro` を追加した。
- `/chatgpt-pro` は `prompt` 引数を受け取り、入力がある場合は既存の slash dispatcher に `/chatgpt-pro <prompt>` として渡す。
- `/chatgpt-pro` の `prompt` が空の場合は、Discord の ephemeral message で「何を Pro extended thinking で考えさせる?」と聞き返す。
- `gateway/platforms/discord.py` に Discord native slash command `/chatgpt-research` を追加した。
- `/chatgpt-research` は `prompt` 引数を受け取り、入力がある場合は既存の slash dispatcher に `/chatgpt-research <prompt>` として渡す。
- `/chatgpt-research` の `prompt` が空または空白だけの場合は、Discord の ephemeral message で「何を Deep Research で調べる?範囲や出力形式もあれば教えて」と聞き返す。
- `tests/gateway/test_discord_slash_commands.py` に、両コマンドの登録、prompt 付き dispatch、空 prompt 時の usage hint、dispatcher を呼ばないことを検証するテストを追加した。

## 変更理由(Why)
- Hermes から ChatGPT Web の `chatgpt-pro` / `chatgpt-research` skill を Discord で直接呼び出せるようにするため。
- `/skill chatgpt-pro ...` のような汎用 skill 起動ではなく、Discord のコマンド候補から明示的に `/chatgpt-pro` と `/chatgpt-research` を選べるようにしたかったため。
- 空 prompt のまま実行した場合に無意味な agent turn を開始せず、その場でユーザーに用途を聞き返すため。

## 実装方法(How)
- 既存の `/todo` などの native slash command と同じ `_register_slash_commands()` 内に command を定義した。
- 実行処理は新しい専用 dispatcher を作らず、既存の `_run_simple_slash()` に command text を渡す形にした。
- これにより、skill command の解決、allowlist、Discord event 化、既存 gateway dispatcher の挙動を再利用している。
- prompt の正規化は `(prompt or "").strip()` に限定し、入力本文そのものの加工や要約は既存の下流処理に任せる。
- テストは既存の fake command tree と `AsyncMock` パターンに合わせ、Discord API を呼ばない単体テストとして追加した。

## 影響範囲・注意事項
- Discord application command の実反映には gateway 起動後の command sync、または手動の `tree.sync()` が必要。
- 既に 2026-05-03 JST に手動 sync を実行し、Discord 側で `/chatgpt-pro` と `/chatgpt-research` が同期済みであることを確認した。
- Hermes 側 skill discovery では同名 skill が reserved gateway command と衝突するため `/skill` autocomplete には表示されないが、今回追加した native slash command として直接呼び出せる。
- 破壊的変更はない。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
(cherry picked from commit ac2c282)
## 変更内容(What)
- `tools/send_message_tool.py` に Discord news channel 判定、記事ブロック分割、記事フィードバック用 components 生成を追加した。
- Discord の standalone REST 送信経路でも、番号付き news digest を前置き・記事ごと・末尾に分割し、記事メッセージだけに `Read / More / Less / Deep Dive` ボタンを付けるようにした。
- `_send_discord` が Discord message components を payload に含められるようにした。
- `tests/tools/test_send_message_tool.py` に、`#business-news` 相当の standalone delivery で記事ごとに components が付く回帰テストを追加した。

## 変更理由(Why)
- `life-business-news-scout` を手動実行した際、Gateway の live adapter 経路ではなく standalone Discord REST 送信経路から投稿され、記事ごとのボタンが付かなかったため。
- news digest の記事フィードバックは、通常 cron 配送だけでなく手動 tick や fallback 配送でも同じ UX で動く必要があるため。
- 既存の live adapter 側だけに分割・ボタン付与ロジックがあり、配送経路によって挙動が分岐していたため。

## 実装方法(How)
- Discord REST 送信前に対象 channel ID と `news_article_buttons` 設定を確認し、news channel の通常テキスト digest だけを記事分割対象にした。
- 既存 Gateway 側と同じ `^\d+\. **` 形式の番号付き記事検出を使い、`### SKIP` より前の retained article だけにボタンを付ける構造にした。
- Discord API の message `components` payload を直接組み立て、既存の custom_id と同じ `news_article_read` / `news_article_keep` / `news_article_skip` / `news_article_deep` を使った。
- media 添付や thread 宛て送信は従来の経路に残し、通常の news digest text delivery だけを対象にして影響範囲を絞った。

## 影響範囲・注意事項
- 新規投稿される news digest から反映される。既存の Discord 投稿にはボタンは後付けされない。
- standalone REST delivery でもボタンが出るため、手動 `hermes cron tick` や live adapter fallback 時の news UX が Gateway 経路と揃う。
- `Deep Dive` ボタンの処理本体は既存の persistent view 側を使うため、この変更は送信 payload と分割の補完に限定している。
- 検証: `venv/bin/python -m pytest tests/tools/test_send_message_tool.py::TestSendToPlatformChunking::test_discord_news_digest_splits_articles_with_components tests/gateway/test_discord_news_article_buttons.py::test_news_digest_send_attaches_feedback_view_to_each_article -q -o addopts='' -o cache_dir=/private/tmp/hermes-pytest-cache` が成功。
- 検証: `venv/bin/python -m py_compile tools/send_message_tool.py gateway/platforms/discord.py` が成功。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
(cherry picked from commit 8bc07c0)
## 変更内容(What)
- Discord news article の Deep Dive ボタンから生成される agent dispatch prompt に、必要に応じて Life repo skills の x-browser / grok-browser を補助利用する指示を追加した。
- X/Grok 由来の情報は一次情報ではなく signal として明示し、公式情報や元記事と分けて扱う指示を追加した。
- Deep Dive ボタンの regression test で、追加した指示が dispatch prompt に含まれることを検証するようにした。

## 変更理由(Why)
- News Deep Dive は単なる元記事要約ではなく、日本語圏の反応・温度感・関連論点も必要時に拾える深掘り導線として使いたいため。
- ただし X/Grok 由来の話題は一次情報ではないため、事実確認済み情報と signal を混同しないよう prompt に明示する必要があったため。
- Hermes 本体更新時に Deep Dive の依頼品質が落ちないよう、挙動をテストで固定するため。

## 実装方法(How)
- NewsArticleFeedbackView._deep_dive_prompt の bullet に x-browser / grok-browser 補助利用と X/Grok signal 扱いの2行を追加した。
- test_deep_dive_button_creates_thread_and_dispatches_agent で dispatch prompt の文言を assert する形にした。

## 影響範囲・注意事項
- News article Deep Dive ボタンの dispatch prompt のみ変更。
- Read / More / Less の feedback 記録や通常 thread lifecycle buttons には影響しない。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
(cherry picked from commit a76d41f)
- cron/scheduler.py に hermes-random-mumble 専用の final_response sanitize を追加した。
- job id 7fe4d99fd75d / job name hermes-random-mumble の場合だけ、隣接する完全重複行を配信前に除去する。
- 空白差を吸収した正規化で比較し、同じ行が連続した場合は2回目以降を落とす。

高頻度 mumble で同じ文が同一メッセージ内に2回連続して出ると、bot らしいノイズが強くなるため。prompt だけでは完全一致の重複を取り切れないので、配信直前に確実に落とすガードを追加した。

run_job で final_response を受け取った直後に _sanitize_cron_final_response を通す。対象 job 以外には何もせず、random mumble のみ _dedupe_adjacent_repeated_lines で splitlines 単位の隣接重複を削る。保存される output にも sanitize 後の response が残る。

- 対象は hermes-random-mumble のみで、他 cron job の出力には影響しない。
- 意図的に同じ行を2回連続で出したいケースは random mumble では想定しない。
- gateway は再起動済みで、この変更は live runtime に反映済み。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
(cherry picked from commit 935e413)
## 変更内容(What)
- Feishu 既存トピック内の進捗返信テストで、progress metadata に thread_lifecycle_buttons: False が付与される期待値へ更新した。
- systemd service refresh / restart routing 系テストに class-local autouse fixture を追加し、_preflight_user_systemd を no-op に monkeypatch するようにした。

## 変更理由(Why)
- Hermes v0.13.0 upstream を土台に Life 側の Discord lifecycle button 抑止仕様を復元した結果、進捗メッセージには final response 用ボタンを出さない metadata が明示的に付くため。
- systemd routing tests は unit 更新や restart 分岐を検証するのが目的であり、実行環境の user systemd preflight に依存するとローカル統合検証が不安定になるため。

## 実装方法(How)
- gateway/run.py 側の統合方針に合わせ、Feishu progress metadata の期待値を実際の送信 metadata と一致させた。
- systemd preflight そのものの単体テストは既存 class に残し、refresh / routing class だけで preflight を無効化して関心ごとを分離した。

## 影響範囲・注意事項
- テスト期待値のみの変更で、runtime code の挙動変更はない。
- 直近の関連テストは 332 passed, 2 warnings を確認済み。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
## 変更内容(What)
- Discordスレッド名のサニタイズ処理に、回答本文やMarkdownアウトラインがそのままタイトル化されたケースを検出する判定を追加した。
- 「スレッド内容の整理案を以下に示します…」のような冗長タイトルを、短い「スレッド内容整理」に圧縮する補正を追加した。
- LLM生成タイトルが初回発話由来の長い省略タイトルに近い場合、ユーザー発話から「Discordスレ名仕様」のような短いフォールバック名を作れるようにした。
- Gateway側からDiscordアダプタへ user_message を渡し、フォールバック生成に利用できるようにした。
- Discordスレ名サニタイズとフォールバック更新の回帰テストを追加した。

## 変更理由(Why)
- Discordのスレ名が初回の長い発話や、LLMの回答本文に近い冗長な文字列のまま残り、簡潔で分かりやすいタイトルになっていないため。
- 自動タイトル生成は外部LLMに依存しており、失敗時や指示逸脱時にユーザーから見えるDiscordサイドバーの品質が落ちるため。

## 実装方法(How)
- 既存の update_thread_title の流れは維持しつつ、サニタイズ後のタイトルがMarkdown/説明文らしい場合に圧縮する軽量なローカル補正を追加した。
- LLM生成タイトルが空・冗長・省略付きの初回発話風の場合は、user_message を使って deterministic な短いタイトルへ戻す経路を追加した。
- 既存の auto-thread 初期タイトル生成とは分離し、最終的に表示するスレッド名の品質補正だけを狭く変更した。

## 影響範囲・注意事項
- Discordスレッドの自動リネームにのみ影響する。
- 既存の通常タイトル更新APIは後方互換のため user_message 省略でも動く。
- Hermes Gateway実行中プロセスへ反映するには再起動が必要。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
## 変更内容(What)
- agent/title_generator.py でタイトル生成に渡すユーザー発話・アシスタント応答の切り出しを、先頭500文字から末尾1500文字へ変更した。
- タイトル生成の出力上限を 500 tokens から 64 tokens に下げ、短いスレッド名生成に必要な範囲へ絞った。
- tests/agent/test_title_generator.py に、長文入力では古い先頭文脈ではなく最近の末尾文脈が渡ること、タイトル生成の出力上限が小さいことを確認するテストを追加した。

## 変更理由(Why)
- Discordスレッド名の自動生成では、最新ターンの結論や決定事項がメッセージ末尾に出ることが多く、先頭だけを見ると古い前置きに引っ張られるため。
- ユーザーから、軽いモデルで長めに文脈を見たい一方で、タイトル生成の無駄なトークン消費は抑えたいという要求があったため。

## 実装方法(How)
- _TITLE_CONTEXT_CHARS と _TITLE_MAX_TOKENS を定数化し、タイトル生成の入力・出力予算を明示した。
- _recent_snippet() を追加し、長い文字列では末尾側を採用するようにした。
- 既存の call_llm(task="title_generation") の経路は維持し、補助モデル設定によるモデル切り替えをそのまま使えるようにした。

## 影響範囲・注意事項
- タイトル生成に渡す入力は最大でユーザー発話1500文字、アシスタント応答1500文字に増える。
- 出力上限は64 tokensへ下がるため、冗長なタイトル本文を生成しにくくなる。
- 実行中Gatewayへ反映するには、外部からのGateway再起動が必要。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
## 変更内容(What)
- `plugins/model-providers/opencode-zen/__init__.py` に `OpenCodeGoProfile` を追加し、OpenAI形式のマルチモーダルcontent partsをテキストのみのcontentへ畳み込む前処理を実装した。
- `image_url` / `input_image` パートは `[image omitted: URL]` 形式のプレースホルダーへ変換し、テキストパートはそのまま保持するようにした。
- `opencode-go` のprovider profileを通常の `ProviderProfile` から `OpenCodeGoProfile` に差し替えた。
- `tests/providers/test_e2e_wiring.py` に、DeepSeek v4系をOpenCode Go経由で呼ぶ際に画像つき履歴がtext-onlyへ変換され、元の入力リストは破壊されないことを確認するテストを追加した。

## 変更理由(Why)
- 12時の `life-youtube-taste-scout` cronで、fallback先の `deepseek-v4-pro` がOpenCode Go経由で呼ばれた際、履歴内の `image_url` パートをDeepSeek側が受け付けずHTTP 400で停止していたため。
- DeepSeek自体を完全に無効化するのではなく、DeepSeekが扱えない画像パートだけを安全にテキスト化して、ツール呼び出しを含む通常のagent workflowでは継続利用できるようにするため。

## 実装方法(How)
- ProviderProfileの既存hookである `prepare_messages()` を使い、transport層に入る前のprovider固有整形として実装した。
- 画像そのものはDeepSeekに送らず、URL付きのプレースホルダーを会話履歴に残すことで、ユーザーやagentが参照元の存在を失わない形にした。
- 変換が必要な場合だけdeepcopyして、画像パートがない通常履歴では既存通りpass-throughする。

## 影響範囲・注意事項
- 影響範囲は `opencode-go` provider profileに限定される。OpenCode Zenやnative DeepSeek providerには影響しない。
- DeepSeek経由では画像内容そのものを読めるようになるわけではなく、画像入力はテキストのプレースホルダーとして扱われる。
- 実行確認: `pytest tests/providers/test_e2e_wiring.py -q` で7件pass。
- Hermes gateway実行中プロセスへ反映するには再起動が必要。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
## 変更内容(What)
- `gateway/run.py` の `_maybe_update_discord_thread_title()` で、Discordスレ名更新パスの主要分岐を INFO ログへ出すようにした。
  - adapter未設定、入力本文不足、空の生成タイトル、DB title保存失敗、adapterがFalseを返した場合を `reason=...` 付きで記録する。
  - 生成されたタイトルとsession/thread idを短いreprで記録し、本文や長文レスポンスはログに出さない。
- `gateway/platforms/discord.py` の `DiscordAdapter.update_thread_title()` で、これまでFalseだけ返していた分岐を観測可能にした。
  - `no_client`, `missing_thread_id`, `no_title_or_user_message`, `fetch_channel_not_found`, `fetched_non_thread`, `unchanged`, `fallback_used` を INFO ログへ出す。
  - Discord API編集例外は `edit_failed` として WARNING + `exc_info=True` で記録する。
- `tests/gateway/test_discord_channel_controls.py` に、fallback利用、not found、non-thread、unchanged、edit例外のログ理由を検証するテストを追加した。
- `tests/gateway/test_title_command.py` に、gateway runner側で空タイトルとadapter Falseをログに残すことを検証するテストを追加した。

## 変更理由(Why)
Discord thread `1503781179424243774` で title_generation は走っている一方、`Renamed Discord thread ...` も失敗ログも残らず、なぜスレ名が更新されないかをログから特定できない状態だったため。
既存実装はbest-effortとして例外やFalseを握りつぶす設計だったが、スレ名更新のようなユーザーに見える副作用では、skip理由が見えないと再発時に調査が詰まるため、挙動は変えず観測性を上げた。

## 実装方法(How)
- 既存API互換を保つため、`update_thread_title()` の戻り値は bool のまま維持した。
- 大きなresult object化や自動backfillは避け、既存のbest-effort renameフローに理由付きログを追加する最小変更にした。
- 個人メッセージ本文をログに出さないよう、ログ対象はthread id、session id、短いtitle候補、reason、型名、例外メッセージに絞った。
- TDDで先にcaplogテストを追加し、silent skipが観測できない失敗を確認してから実装した。

## 影響範囲・注意事項
- Discordスレ名更新の成功/失敗判定やrename条件は基本的に既存のまま。今回の主目的はsilent skip解消と次回調査のための観測性追加。
- ログ量はDiscord thread title update時のみ増える。本文は出さず、title候補も80文字に切り詰める。
- 反映にはgateway再起動が必要。外部端末から `hermes gateway restart` を実行し、次回対象threadで1ターン流した後に `reason=...` または `Renamed Discord thread ...` を確認する。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
## 変更内容(What)
- gateway/run.py の Discord スレ名自動更新で、title_generation が空文字/Noneを返した場合に即 return せず、DiscordAdapter.update_thread_title に空タイトルと user_message を渡して既存の決定論的フォールバックを使うようにした。
- フォールバック経路のログ理由を reason=empty_generated_title_fallback として記録し、フォールバック側の adapter が false を返した場合は reason=fallback_adapter_returned_false を追加で出すようにした。
- tests/gateway/test_title_command.py の回帰テストを、空タイトル時に rename 処理を呼ばない挙動から、fallback rename を必ず呼ぶ挙動へ更新した。

## 変更理由(Why)
- title_generation を deepseek-v4-flash / OpenCode Go 経由で使うと、短い max_tokens が reasoning tokens に消費されて message.content が空になり、Discordスレ名更新が reason=empty_generated_title で止まり続けることが実測で確認されたため。
- スレ名更新は「LLMタイトルが取れたら動く」ではなく、他スレや画像混じりの会話でも最低限ユーザー発話から安定して更新される必要があるため。

## 実装方法(How)
- generate_title 自体のトークン上限を大きくしてコストを増やすのではなく、既に DiscordAdapter 側にある _fallback_thread_title_from_user_message 経路へ委譲する最小修正にした。
- これにより、LLMが空・未対応・reasoning偏重の応答を返しても、Discord rename API まで到達し、adapter側の sanitize/fallback/ログを一貫して使える。
- focused pytest で空タイトル時に update_thread_title(thread_id, "", user_message=...) が await されることを検証した。

## 影響範囲・注意事項
- Discord thread auto-title の空生成時挙動が、skip から user_message ベースのフォールバック更新へ変わる。
- title_generation のトークン消費は増やしていない。
- 本番反映には gateway 再起動が必要。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
## 変更内容(What)
- DiscordAdapter._derive_auto_thread_name の空入力フォールバックを `Hermes相談` から `相談` に変更した。
- DiscordAdapter._sanitize_thread_title の空タイトルフォールバックも `Hermes相談` から `相談` に変更した。
- mentionだけの自動スレ作成テストと、空タイトルsanitizeの回帰テストを更新・追加した。

## 変更理由(Why)
- title_generation が空になった場合や、mention等を除去して本文が空になった場合に `Hermes相談` という低情報量タイトルが増えすぎ、Discordサイドバーでスレの見分けがつきにくくなるため。
- `Hermes相談` はユーザー視点で意味の差分が少なく、既にHermesチャンネル内であることも多いため、フォールバック名として冗長だったため。

## 実装方法(How)
- 既存のカテゴリ判定やユーザー発話ベースのタイトル抽出には触れず、空入力・空sanitize時の最後のデフォルトだけを最小変更した。
- 先に `Hermes相談` を期待しない回帰テストを追加して失敗を確認し、その後に実装を変更した。
- focused gateway tests で自動スレ作成、Discord channel controls、title command 周辺の挙動を確認した。

## 影響範囲・注意事項
- Discordスレ名自動生成で、本文が実質空のときのデフォルト名が `相談` になる。
- 既存の具体的なカテゴリタイトルや、LLM/ユーザー発話から生成できるタイトルには影響しない。
- 本番反映には gateway 再起動が必要。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
## 変更内容(What)
- Discord thread title auto-update で title_generation が空を返した場合、ユーザー発話ベースのフォールバック rename を呼ばず、reason=empty_generated_title で skip する挙動へ戻した。
- DiscordAdapter.update_thread_title から bad generated title / 省略記号 / 「どんな感じ」系タイトルの user_message fallback を廃止し、候補が不適切な場合は rename せず reason=bad_generated_title で止めるようにした。
- 空タイトル時に adapter.update_thread_title が呼ばれないこと、bad generated title がフォールバック更新されないことを回帰テストで固定した。

## 変更理由(Why)
- 空生成時に決定論フォールバックへ落とすと `Hermes相談` や `相談` のような低情報量タイトルが量産され、ユーザーが意図した「良いタイトルが作れたときだけ更新する」挙動から外れるため。
- 画像付き/メディア付きメッセージなどで title_generation が十分な材料を得られない場合は、推測タイトルで上書きするより、更新を保留してログで原因を追えるほうが安全なため。

## 実装方法(How)
- GatewayRunner._maybe_update_discord_thread_title の empty title 分岐から adapter fallback 呼び出しを削除した。
- DiscordAdapter.update_thread_title では title が空、または sanitization 後に不適切な候補と判定された場合に即 False を返すようにし、既存の fetch/edit 処理へ進まないようにした。
- focused gateway tests と diff check を実行して、空生成・bad title・既存Discord title controlsの挙動を確認した。

## 影響範囲・注意事項
- Discordスレ名自動更新は、LLM title_generation が有効なタイトルを返した場合だけ rename する。
- 既に作成済みの汎用名スレッドを自動で戻す処理は含めていない。
- 本番反映には gateway 再起動が必要。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
## 変更内容(What)
- Discord thread title auto-update で title_generation が空を返して skip するログに `media_policy=text_only_no_image_analysis` を追加した。
- 空生成時の回帰テストで、画像解析を使わないテキスト専用方針がログに出ることを検証するようにした。

## 変更理由(Why)
- Discordスレ名生成は画像内容を使わず、テキストから良いタイトルが生成できた場合だけ更新する方針にしたため。
- 画像付き投稿で空タイトルになった場合にも、画像解析フォールバックを試していないことを運用ログから判断できるようにするため。

## 実装方法(How)
- GatewayRunner._maybe_update_discord_thread_title の empty title skip ログ文言だけを変更し、挙動は変えない最小修正にした。
- focused gateway tests と diff check で確認した。

## 影響範囲・注意事項
- 動作変更はなく、ログの観測性だけの変更。
- 本番反映には gateway 再起動が必要。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
## 変更内容(What)
- `agent/title_generator.py` のタイトル生成出力上限を 64 tokens から 4096 tokens に引き上げた。
- `gateway/run.py` の Discord スレ名自動更新時のタイトル生成 timeout を 20 秒から 60 秒に引き上げた。
- `tests/agent/test_title_generator.py` の期待値とテスト名を、reasoning モデルでも最終タイトル本文を出せる出力予算を確認する内容へ更新した。

## 変更理由(Why)
- 実運用の `title_generation` が `deepseek-v4-flash` を使っており、64 tokens では `reasoning_content` だけで `message.content` が空のまま `finish_reason=length` になっていたため。
- その結果、Discord スレ名更新は毎回 `empty_generated_title` で skip され、生成タイトルから動的にスレ名を変更する仕様が実際には動いていなかったため。
- 4096 tokens まで許可し、timeout も 60 秒へ伸ばすことで、reasoning-capable な補助モデルでも短い可視タイトルを最後まで返せるようにするため。

## 実装方法(How)
- タイトル生成の入力コンテキストサイズは維持し、出力予算だけを拡大した。
- Discord スレ名更新の呼び出し側 timeout も、拡大した出力予算に合わせて 60 秒へ変更した。
- 実 Discord 上に E2E 検証用スレッドを作成し、`generate_title()` が生成したタイトルを `DiscordAdapter.update_thread_title()` 経由で実際に `thread.edit(name=...)` できることを確認した。

## 影響範囲・注意事項
- タイトル生成 1 回あたりの補助モデル出力上限が増えるため、reasoning モデル利用時のタイトル生成コスト・時間は増える可能性がある。
- ただし Discord スレ名更新は good title が出た場合のみ rename し、空タイトルや bad title では引き続き更新しない。
- 実 Discord E2E の検証スレッド ID は `1504277399754506392`、最終スレ名は `スレ名自動更新のE2E検証`。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
## 変更内容(What)
- tests/gateway/test_title_command.py に、Discordスレ名自動更新でタイトル生成が成功した場合の成功経路テストを追加した。
- generate_title が有効なタイトルを返したときに、session title が保存され、adapter.update_thread_title(thread_id, title, user_message=...) が呼ばれることを検証するようにした。
- 成功時ログに generated が出て、adapter_returned_false には落ちないことも確認するようにした。

## 変更理由(Why)
- 空タイトル時にフォールバックしないことだけでなく、良いタイトルが生成された場合には動的にDiscordスレ名更新へ進むことをテストで明確に保証するため。
- 実装意図として「低品質タイトルはskipするが、良いタイトルならrenameする」挙動を将来の変更で壊さないため。

## 実装方法(How)
- GatewayRunner._maybe_update_discord_thread_title を object.__new__ で最小構成にし、Discord SessionSource と AsyncMock adapter を差し込んだ。
- agent.title_generator.generate_title を monkeypatch して安定した良いタイトルを返すようにし、外部モデルやDiscord APIに依存しない単体テストとして成功経路を固定した。

## 影響範囲・注意事項
- テスト追加のみでランタイム挙動の変更はない。
- 実Discord APIへのライブrename確認ではなく、gateway内のタイトル生成成功経路からadapter呼び出しまでを検証する回帰テストである。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
## 変更内容(What)
- gateway/run.py の Discord thread title 自動更新で、(platform, thread_id, session_id) ごとに asyncio.Lock を保持するようにした。
- セッションDBタイトル保存から adapter.update_thread_title() までの適用区間を同じ lock 内で実行し、lock 取得後も既存の stale_generation チェックを維持した。
- tests/gateway/test_title_command.py に、古い rename が await 中のまま新しい title task が走る race の regression test を追加した。
- regression test では、新しい rename が古い rename の完了前に finish しないことと、最終DBタイトルが新しい title になることを検証する。

## 変更理由(Why)
- 既存の generation stale guard は adapter.update_thread_title() を await する直前までしか効かず、古い Discord HTTP rename が in-flight の間に新しい rename が完了すると、古い rename が最後に完了して可視スレ名を巻き戻す可能性があったため。
- Discord API への visible rename 適用順を session/thread 単位で保証し、最終的に最新 generation の title が勝つ状態へ収束させるため。

## 実装方法(How)
- title_key と同じ (platform, thread_id, session_id) をキーにした _discord_thread_title_apply_locks dict を GatewayRunner に遅延生成する。
- title 生成と低情報 title rejection は従来どおり apply 前に実行し、DB set + Discord rename の副作用だけを async with title_apply_lock 内へ移した。
- lock 内で stale_generation_before_apply / stale_generation_before_rename の既存ログと skip 動作を残し、低情報 title rejection と background task exception/cancel observation の挙動を変えないようにした。
- test では最初の adapter.update_thread_title を Event で止め、新しい title update が古い rename 完了前に finish できないことを確認してから両 task を完了させる。

## 影響範囲・注意事項
- Discord thread title の自動更新で、同一 Discord thread/session の DB保存とrenameが直列化されるため、同一threadで連続応答した場合はrename適用が順番待ちになる。
- 異なる thread/session key の title update は別 lock なので並行性を維持する。
- 追加した lock dict は GatewayRunner のプロセス内状態であり、既存 generation dict と同じ寿命で保持される。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
@hamanori hamanori force-pushed the kanban/t_eb4b1d4a-wiki-save-ui branch from d8b5de1 to 51d8086 Compare May 17, 2026 17:15
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 17, 2026

🔎 Lint report: kanban/t_eb4b1d4a-wiki-save-ui vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 7849 on HEAD, 7755 on base (🆕 +94)

🆕 New issues (74):

Rule Count
invalid-argument-type 33
unresolved-attribute 31
invalid-assignment 9
unsupported-operator 1
First entries
gateway/platforms/base.py:2804: [invalid-argument-type] invalid-argument-type: Argument to bound method `BasePlatformAdapter._keep_typing` is incorrect: Expected `int | float`, found `dict[str, str] | None | dict[str | Unknown, str | Unknown]`
tests/run_agent/test_compressor_fallback_update.py:71: [unresolved-attribute] unresolved-attribute: Attribute `context_length` is not defined on `None` in union `None | Unknown | ContextCompressor`
run_agent.py:8164: [invalid-argument-type] invalid-argument-type: Argument to function `get_provider_request_timeout` is incorrect: Expected `str | None`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | ... omitted 3 union elements`
run_agent.py:12893: [unresolved-attribute] unresolved-attribute: Attribute `context_length` is not defined on `None` in union `None | Unknown | ContextCompressor`
run_agent.py:7091: [invalid-argument-type] invalid-argument-type: Argument to function `get_provider_request_timeout` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
run_agent.py:12970: [unresolved-attribute] unresolved-attribute: Attribute `update_model` is not defined on `None` in union `None | Unknown | ContextCompressor`
run_agent.py:7842: [unresolved-attribute] unresolved-attribute: Attribute `strip` is not defined on `dict[Unknown | str, Unknown | str | dict[str, str]] & ~AlwaysFalsy`, `int & ~AlwaysFalsy`, `dict[Unknown, Unknown] & ~AlwaysFalsy` in union `(str & ~AlwaysFalsy) | (Unknown & ~AlwaysFalsy) | (dict[Unknown | str, Unknown | str | dict[str, str]] & ~AlwaysFalsy) | ... omitted 4 union elements`
run_agent.py:10623: [invalid-argument-type] invalid-argument-type: Argument to function `_fixed_temperature_for_model` is incorrect: Expected `str | None`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | ... omitted 3 union elements`
run_agent.py:2766: [invalid-assignment] invalid-assignment: Object of type `int` is not assignable to attribute `threshold_tokens` on type `None | Unknown | ContextCompressor`
run_agent.py:3694: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | ... omitted 3 union elements`
run_agent.py:12755: [invalid-argument-type] invalid-argument-type: Argument to function `_pool_may_recover_from_rate_limit` is incorrect: Expected `str | None`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
cli.py:7999: [unresolved-attribute] unresolved-attribute: Attribute `last_prompt_tokens` is not defined on `None` in union `None | Unknown | ContextCompressor`
run_agent.py:8348: [invalid-argument-type] invalid-argument-type: Argument to function `get_transport` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | ... omitted 3 union elements`
run_agent.py:12083: [invalid-argument-type] invalid-argument-type: Argument to function `save_context_length` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | ... omitted 3 union elements`
run_agent.py:12110: [invalid-argument-type] invalid-argument-type: Argument to function `estimate_usage_cost` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | ... omitted 3 union elements`
run_agent.py:12075: [unresolved-attribute] unresolved-attribute: Attribute `update_from_response` is not defined on `None` in union `None | Unknown | ContextCompressor`
run_agent.py:8759: [invalid-argument-type] invalid-argument-type: Argument to function `get_provider_profile` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
run_agent.py:9183: [unresolved-attribute] unresolved-attribute: Attribute `lower` is not defined on `dict[Unknown, Unknown] & ~AlwaysFalsy`, `int & ~AlwaysFalsy`, `dict[Unknown | str, Unknown | str | dict[str, str]] & ~AlwaysFalsy` in union `(str & ~AlwaysFalsy) | (Unknown & ~AlwaysFalsy) | (dict[Unknown, Unknown] & ~AlwaysFalsy) | ... omitted 3 union elements`
gateway/platforms/base.py:2804: [invalid-argument-type] invalid-argument-type: Argument to bound method `BasePlatformAdapter._keep_typing` is incorrect: Expected `Event | None`, found `dict[str, str] | None | dict[str | Unknown, str | Unknown]`
run_agent.py:8080: [invalid-argument-type] invalid-argument-type: Argument to bound method `ContextCompressor.update_model` is incorrect: Expected `int`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | ... omitted 3 union elements`
run_agent.py:6645: [invalid-argument-type] invalid-argument-type: Argument to function `build_anthropic_client` is incorrect: Expected `str`, found `str | dict[Unknown | str, Unknown | str | dict[str, str]] | Any | ... omitted 4 union elements`
gateway/platforms/discord.py:5665: [unresolved-attribute] unresolved-attribute: Attribute `TextStyle` is not defined on `None` in union `Unknown | None`
run_agent.py:8164: [invalid-argument-type] invalid-argument-type: Argument to function `get_provider_request_timeout` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | ... omitted 3 union elements`
run_agent.py:10683: [unresolved-attribute] unresolved-attribute: Attribute `strip` is not defined on `dict[Unknown, Unknown] & ~AlwaysFalsy`, `int & ~AlwaysFalsy`, `dict[Unknown | str, Unknown | str | dict[str, str]] & ~AlwaysFalsy` in union `(str & ~AlwaysFalsy) | (Unknown & ~AlwaysFalsy) | (dict[Unknown, Unknown] & ~AlwaysFalsy) | ... omitted 3 union elements`
run_agent.py:13779: [unresolved-attribute] unresolved-attribute: Attribute `should_compress` is not defined on `None` in union `None | Unknown | ContextCompressor`
... and 49 more

✅ Fixed issues (35):

Rule Count
invalid-argument-type 29
unresolved-attribute 3
invalid-assignment 2
unsupported-operator 1
First entries
run_agent.py:8164: [invalid-argument-type] invalid-argument-type: Argument to function `get_provider_request_timeout` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
tests/gateway/test_discord_news_article_buttons.py:241: [invalid-assignment] invalid-assignment: Object of type `def dispatch_agent_request(interaction, text) -> CoroutineType[Any, Any, Unknown]` is not assignable to attribute `_dispatch_agent_request` of type `def _dispatch_agent_request(self, interaction: Unknown, text: str) -> CoroutineType[Any, Any, None]`
run_agent.py:12975: [invalid-argument-type] invalid-argument-type: Argument to bound method `ContextCompressor.update_model` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:12755: [invalid-argument-type] invalid-argument-type: Argument to function `_pool_may_recover_from_rate_limit` is incorrect: Expected `str | None`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:4269: [invalid-argument-type] invalid-argument-type: Argument to function `save_trajectory` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:8163: [invalid-argument-type] invalid-argument-type: Argument to function `build_anthropic_client` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:12521: [invalid-argument-type] invalid-argument-type: Argument to function `_is_oauth_token` is incorrect: Expected `str`, found `str | dict[Unknown, Unknown] | Any | ... omitted 3 union elements`
gateway/platforms/base.py:2793: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["stop_event"]` and value of type `Event` on object of type `dict[str, dict[str, str] | None]`
run_agent.py:8348: [invalid-argument-type] invalid-argument-type: Argument to function `get_transport` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:12065: [invalid-argument-type] invalid-argument-type: Argument to function `normalize_usage` is incorrect: Expected `str | None`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:10683: [unresolved-attribute] unresolved-attribute: Attribute `strip` is not defined on `dict[Unknown | str, Unknown | str | dict[str, str]] & ~AlwaysFalsy`, `int & ~AlwaysFalsy`, `dict[Unknown, Unknown] & ~AlwaysFalsy` in union `(str & ~AlwaysFalsy) | (Unknown & ~AlwaysFalsy) | (dict[Unknown | str, Unknown | str | dict[str, str]] & ~AlwaysFalsy) | ... omitted 3 union elements`
run_agent.py:8905: [invalid-argument-type] invalid-argument-type: Argument to function `lmstudio_model_reasoning_options` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:3698: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:5262: [unresolved-attribute] unresolved-attribute: Attribute `split` is not defined on `dict[Unknown | str, Unknown | str | dict[str, str]]`, `int`, `dict[Unknown, Unknown]` in union `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:8759: [invalid-argument-type] invalid-argument-type: Argument to function `get_provider_profile` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:12112: [invalid-argument-type] invalid-argument-type: Argument to function `estimate_usage_cost` is incorrect: Expected `str | None`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
gateway/platforms/base.py:2797: [invalid-argument-type] invalid-argument-type: Argument to bound method `BasePlatformAdapter._keep_typing` is incorrect: Expected `Event | None`, found `dict[str, str] | None`
run_agent.py:8080: [invalid-argument-type] invalid-argument-type: Argument to bound method `ContextCompressor.update_model` is incorrect: Expected `int`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:12110: [invalid-argument-type] invalid-argument-type: Argument to function `estimate_usage_cost` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:9184: [unresolved-attribute] unresolved-attribute: Attribute `lower` is not defined on `dict[Unknown | str, Unknown | str | dict[str, str]] & ~AlwaysFalsy`, `int & ~AlwaysFalsy`, `dict[Unknown, Unknown] & ~AlwaysFalsy` in union `(str & ~AlwaysFalsy) | (Unknown & ~AlwaysFalsy) | (dict[Unknown | str, Unknown | str | dict[str, str]] & ~AlwaysFalsy) | ... omitted 3 union elements`
run_agent.py:12083: [invalid-argument-type] invalid-argument-type: Argument to function `save_context_length` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:2330: [invalid-argument-type] invalid-argument-type: Argument to function `ensure_lmstudio_model_loaded` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:8742: [invalid-argument-type] invalid-argument-type: Argument to function `_get_anthropic_max_output` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
cli.py:8006: [invalid-argument-type] invalid-argument-type: Argument to function `estimate_usage_cost` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:2926: [invalid-argument-type] invalid-argument-type: Argument to function `get_provider_stale_timeout` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
... and 10 more

Unchanged: 3993 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

hamanori and others added 6 commits May 18, 2026 02:16
## 変更内容(What)
- gateway/platforms/discord.py の Wiki lifecycle button 用 ack 文言を llm-wiki 保存提案であり、まだ書き込まないことが分かる表現に更新した。
- Wiki proposal prompt に proposal-only と「絶対にファイルを書き込まない」制約を明記した。
- Save / Edit / Skip の次操作に加えて、Save 時は Kanban タスクとして Life repo knowledge/ 反映、index 更新、log 記録、lint、commit、push を行う方針を説明するようにした。

## 変更理由(Why)
- Discord の Wiki ボタンが直接 knowledge/ を書き込む操作だと誤解されないようにするため。
- ユーザーが Wiki を押した時点では保存候補だけを作り、実際の反映は確認後の Kanban 作業に分離する運用を runtime に反映するため。

## 実装方法(How)
- 既存の live gateway main 側の定数 THREAD_LIFECYCLE_WIKI_EPHEMERAL_MESSAGE と THREAD_LIFECYCLE_WIKI_PROPOSAL_PROMPT を最小変更した。
- callback 経路やボタン ID は変更せず、既存の thread_lifecycle_wiki runtime path が同じまま新しい proposal-only prompt を dispatch する形にした。

## 影響範囲・注意事項
- 影響範囲は Discord thread lifecycle の Wiki ボタン文言と agent request のみ。
- live gateway へ反映するには gateway restart が必要。
- 既存の targeted pytest には current main の auto-thread 関連で既知の失敗があるため、今回変更の runtime path は別途安全な Python smoke で検証した。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
## 変更内容(What)
- `gateway/platforms/discord.py` の Wiki ボタン用 ephemeral 文言を、Wiki候補を作るだけでまだ書き込まないことが分かる表現へ更新しました。
- `THREAD_LIFECYCLE_WIKI_PROPOSAL_PROMPT` を補強し、保存不要時は理由だけを返すこと、保存候補がある場合だけ Save/Edit/Skip 前提の候補項目を出すことを明記しました。
- 推奨保存先の分類を `raw/articles`, `concepts`, `entities`, `comparisons`, `queries`, `preferences`, `保存しない` に整理し、それぞれの判断基準をプロンプト内に追加しました。
- `tests/gateway/test_discord_news_article_buttons.py` の Wiki 候補フローテストを更新し、proposal-only ステータス、保存不要分岐、分類一覧、次の操作案が含まれることを検証するようにしました。

## 変更理由(Why)
Discord thread lifecycle の Wiki ボタンは Phase 1 としてファイルを直接書き換えず、Life repo の knowledge/ に保存する候補だけを生成する運用へ固定する必要があるためです。ユーザーにも「まだ書き込まない」ことが即時の ephemeral 応答で伝わるようにし、後続の Save/Edit/Skip UI 実装前でもエージェント出力が同じ候補フローに揃うようにしました。

## 実装方法(How)
定数化済みの ephemeral メッセージと proposal prompt を更新し、`ThreadLifecycleView._wiki_thread_action` の既存 dispatch 経路はそのまま利用しました。one-word lifecycle labels や他ボタンの custom_id には触れず、既存のボタン構造を壊さない範囲でプロンプト内容だけを拡張しています。近接テストは既存の Wiki ボタンテストに assertion を追加し、実装と文言が同期する形にしました。

## 影響範囲・注意事項
- Discord Wiki ボタン押下時の表示文言とエージェントへの依頼文だけが変わります。
- Save/Edit/Skip の専用 UI は今回も未実装で、プロンプト上の次操作案としてのみ扱います。
- 対象テスト `python -m pytest tests/gateway/test_discord_news_article_buttons.py -q` は 8 件 pass、`python -m py_compile gateway/platforms/discord.py` も成功しています。

Co-Authored-By: OpenAI Codex <noreply@openai.com>
@hamanori hamanori force-pushed the kanban/t_eb4b1d4a-wiki-save-ui branch from 51d8086 to 021af34 Compare May 17, 2026 17:17
@hamanori
Copy link
Copy Markdown
Owner Author

APPROVED (Kanban reviewer): Discord Wiki proposal flow reviewed. Wiki creates proposal-only responses with no file writes until Save; Edit now opens a modal and dispatches only after user guidance is submitted; Save/Skip are safe; targeted gateway tests pass locally (13/13). I could not submit a GitHub approval because GitHub rejects approving this user's own PR. Full CI test job remains red from broad pre-existing/unrelated failures observed on origin/main as well; PR is mergeable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant