コンテンツにスキップ

OpenClaw Discord連携:非同期ワーカーでタイムアウトを解消

開発者の皆さん、Discordボットを運用していて、処理に時間がかかりすぎてタイムアウトエラーに悩まされたことはありませんか?ユーザーからのリクエストを受け取ったものの、バックエンドの処理が終わる前に接続が切れてしまうのは、非常にもどかしい問題ですよね。

今回は、Discordのインバウンド処理を非同期化することで、このタイムアウト問題を根本から解決する「Discord Async Inbound Worker」の計画について共有します。

インバウンドのDiscordターンを非同期化することで、ユーザー側で発生するDiscordリスナーのタイムアウト失敗を解消します。

  1. Gatewayリスナーがインバウンドイベントを迅速に受け取り、正規化します。
  2. Discord実行キューが、現在使用しているものと同じ順序境界でキー指定されたシリアライズ済みジョブを保存します。
  3. ワーカーが、Carbonリスナーの生存期間外で実際のエージェントターンを実行します。
  4. 実行完了後、返信が元のチャンネルまたはスレッドに配信されます。

これは、エージェントの実行自体は進行中であるにもかかわらず、キューに入ったDiscordの実行が channels.discord.eventQueue.listenerTimeout でタイムアウトしてしまう問題に対する長期的な解決策です。

この計画は部分的に実装されています。

完了済みの事項:

  • DiscordリスナーのタイムアウトとDiscord実行のタイムアウトが別々の設定になりました。
  • 受理されたインバウンドのDiscordターンが src/discord/monitor/inbound-worker.ts にエンキューされるようになりました。
  • Carbonリスナーではなく、ワーカーが長時間実行されるターンを管理するようになりました。
  • ルートごとの既存の順序制御がキューキーによって維持されています。
  • Discordワーカーパスのタイムアウト回帰テストが存在します。

これを分かりやすく言うと:

  • 本番環境でのタイムアウトバグは修正されました。
  • Discordリスナーの制限時間が切れても、長時間実行されるターンが停止しなくなりました。
  • ただし、ワーカーのアーキテクチャ自体はまだ完成していません。

不足している事項:

  • DiscordInboundJob がまだ部分的にしか正規化されておらず、ライブなランタイム参照を保持しています。
  • コマンドセマンティクス(stop, new, reset、および将来のセッション制御)がまだ完全にワーカーネイティブになっていません。
  • ワーカーのオブザーバビリティ(観測容易性)とオペレーター向けのステータス表示が最小限です。
  • 再起動時の耐久性がまだありません。

現在の動作では、エージェントのターン全体がリスナーの生存期間に紐付けられています。

  • src/discord/monitor/listeners.ts がタイムアウトと中断の境界を適用しています。
  • src/discord/monitor/message-handler.ts が、キューに入れられた実行をその境界内に保持しています。
  • src/discord/monitor/message-handler.process.ts が、メディアの読み込み、ルーティング、ディスパッチ、タイピング表示、ドラフトストリーミング、そして最終的な返信配信をインラインで実行しています。

このアーキテクチャには2つの問題があります。

  • 正常に動作していても、処理が長いターンはリスナーのウォッチドッグによって中断される可能性があります。
  • 下流のランタイムが応答を生成できたはずの場合でも、ユーザーに返信が表示されないことがあります。

タイムアウト設定を引き上げることは一時的な助けにはなりますが、失敗のモード自体を変えることはできません。

  • このフェーズでDiscord以外のチャンネルを再設計しない。
  • 最初の実装で、これを全チャンネル共通の汎用ワーカーフレームワークに広げない。
  • チャンネル横断的なインバウンドワーカーの抽象化をまだ抽出しない。重複が明らかな場合にのみ、低レベルのプリミティブを共有する。
  • 安全にリリースするために必要な場合を除き、最初のパスで耐久性のあるクラッシュリカバリを追加しない。
  • この計画でルート選択、バインディングセマンティクス、またはACPポリシーを変更しない。

現在のDiscord処理パスは、長期的なジョブペイロードに含めるべきではない、いくつかのライブなランタイムオブジェクトに依然として依存しています。

  • Carbon Client
  • 生のDiscordイベント形状
  • メモリ内のギルド履歴マップ
  • スレッドバインディングマネージャーのコールバック
  • ライブタイピングおよびドラフトストリームの状態

実行をワーカーキューに移動させましたが、正規化の境界はまだ不完全です。現時点でのワーカーは「同じプロセス内で、いくつかの同じライブオブジェクトを使用して後で実行する」という状態であり、完全にデータのみに基づいたジョブ境界にはなっていません。

DiscordMessageListener は引き続き入り口となりますが、その役割は以下のようになります。

  • プリフライトチェックとポリシーチェックの実行
  • 受理された入力をシリアライズ可能な DiscordInboundJob に正規化
  • ジョブをセッションごと、またはチャンネルごとの非同期キューにエンキュー
  • エンキューが成功したら、すぐにCarbonに制御を戻す

リスナーは、エンドツーエンドのLLMターンの生存期間を管理すべきではありません。

2. 正規化されたジョブペイロード

Section titled “2. 正規化されたジョブペイロード”

後でターンを実行するために必要なデータのみを含む、シリアライズ可能なジョブ記述子を導入します。

最小限の形状:

  • ルート識別情報
    • agentId
    • sessionKey
    • accountId
    • channel
  • 配信識別情報
    • 送信先チャンネルID
    • 返信対象のメッセージID
    • スレッドID(存在する場合)
  • 送信者識別情報
    • 送信者ID、ラベル、ユーザー名、タグ
  • チャンネルコンテキスト
    • ギルドID
    • チャンネル名またはスラッグ
    • スレッドのメタデータ
    • 解決済みのシステムプロンプトオーバーライド
  • 正規化されたメッセージ本文
    • ベーステキスト
    • 有効なメッセージテキスト
    • 添付ファイルの記述子または解決済みのメディア参照
  • 判定結果
    • メンション要件の結果
    • コマンド権限の結果
    • 適用可能な場合はバインドされたセッションまたはエージェントのメタデータ

ジョブペイロードには、ライブなCarbonオブジェクトやミュータブルなクロージャを含めてはいけません。

現在の実装状況:

  • 部分的に完了
  • src/discord/monitor/inbound-job.ts が存在し、ワーカーへの受け渡しを定義している
  • ペイロードにはまだライブなDiscordランタイムコンテキストが含まれており、さらに削減する必要がある

以下の責任を持つDiscord専用のワーカーランナーを追加します。

  • DiscordInboundJob からターンのコンテキストを再構築
  • メディアおよび実行に必要な追加のチャンネルメタデータの読み込み
  • エージェントターンのディスパッチ
  • 最終的な返信ペイロードの配信
  • ステータスと診断情報の更新

推奨される場所:

  • src/discord/monitor/inbound-worker.ts
  • src/discord/monitor/inbound-job.ts

特定のルート境界において、順序は現在と同等に保たれる必要があります。

推奨されるキー:

  • resolveDiscordRunQueueKey(...) と同じキューキーロジックを使用する

これにより、既存の動作が維持されます。

  • 1つのバインドされたエージェントとの会話が、それ自体とインターリーブ(交差)しない
  • 異なるDiscordチャンネルは、引き続き独立して進行できる

切り替え後は、2つの異なるタイムアウトクラスが存在することになります。

  • リスナータイムアウト
    • 正規化とエンキューのみをカバー
    • 短時間であるべき
  • 実行タイムアウト
    • オプションであり、ワーカーが管理し、明示的でユーザーに見えるもの
    • Carbonリスナーの設定から誤って継承されるべきではない

これにより、「Discord Gatewayリスナーが生存していること」と「エージェントの実行が正常であること」の間の偶発的な結合が解消されます。

  • ステータス:部分的に実装済み
  • 完了:
    • buildDiscordInboundJob(...) の抽出
    • ワーカー受け渡しテストの追加
  • 残り:
    • DiscordInboundJob をプレーンなデータのみにする
    • ライブなランタイム依存関係を、ジョブごとのペイロードではなくワーカー所有のサービスに移動する
    • ライブなリスナー参照をジョブに繋ぎ戻してプロセスコンテキストを再構築するのをやめる

フェーズ 2: メモリ内ワーカーキュー

Section titled “フェーズ 2: メモリ内ワーカーキュー”
  • ステータス:実装済み
  • 完了:
    • 解決された実行キューキーでキー指定された DiscordInboundWorkerQueue を追加
    • リスナーが processDiscordMessage(...) を直接待機するのではなく、ジョブをエンキューするように変更
    • ワーカーがプロセス内、メモリ内のみでジョブを実行

これが最初の機能的な切り替えとなります。

  • ステータス:未着手
  • 配信、タイピング表示、ドラフトストリーミングの管理をワーカー向けのアダプターに移動する。
  • ライブなプリフライトコンテキストの直接使用を、ワーカーコンテキストの再構築に置き換える。
  • 必要に応じて processDiscordMessage(...) を一時的にファサードとして残し、その後分割する。

フェーズ 4: コマンドセマンティクス

Section titled “フェーズ 4: コマンドセマンティクス”
  • ステータス:未着手 ワークがキューに入っているときでも、ネイティブのDiscordコマンドが正しく動作することを確認します。

  • stop

  • new

  • reset

  • 将来のセッション制御コマンド

ワーカーキューは、コマンドがアクティブなターンまたはキューに入ったターンをターゲットにできるだけの実行状態を公開する必要があります。

フェーズ 5: オブザーバビリティとオペレーターUX

Section titled “フェーズ 5: オブザーバビリティとオペレーターUX”
  • ステータス:未着手
  • キューの深さとアクティブなワーカー数をモニターステータスに出力する。
  • エンキュー時刻、開始時刻、終了時刻、およびタイムアウトやキャンセルの理由を記録する。
  • ワーカー管理のタイムアウトや配信の失敗をログに明確に表示する。

フェーズ 6: オプションの耐久性フォローアップ

Section titled “フェーズ 6: オプションの耐久性フォローアップ”
  • ステータス:未着手 メモリ内バージョンが安定した後にのみ実施:

  • キューに入れられたDiscordジョブがGatewayの再起動後も存続すべきかどうかを決定する。

  • 存続させる場合は、ジョブ記述子と配信チェックポイントを永続化する。

  • させない場合は、明示的なメモリ内境界をドキュメント化する。

これは、再起動からの復旧がリリースに必須でない限り、別のフォローアップとして行うべきです。

現在の主要なファイル:

  • src/discord/monitor/listeners.ts
  • src/discord/monitor/message-handler.ts
  • src/discord/monitor/message-handler.preflight.ts
  • src/discord/monitor/message-handler.process.ts
  • src/discord/monitor/status.ts

現在のワーカー関連ファイル:

  • src/discord/monitor/inbound-job.ts
  • src/discord/monitor/inbound-worker.ts
  • src/discord/monitor/inbound-job.test.ts
  • src/discord/monitor/message-handler.queue.test.ts

次に修正が必要になる可能性が高い箇所:

  • src/auto-reply/dispatch.ts
  • src/discord/monitor/reply-delivery.ts
  • src/discord/monitor/thread-bindings.ts
  • src/discord/monitor/native-command.ts

次のステップは、ワーカーの境界を部分的ではなく、完全なものにすることです。

具体的には以下を行います:

  1. DiscordInboundJob からライブなランタイム依存関係を削除する。
  2. それらの依存関係を代わりにDiscordワーカーインスタンスに保持させる。
  3. キューに入れられるジョブを、Discord固有のプレーンなデータに削減する:
    • ルート識別情報
    • 配信ターゲット
    • 送信者情報
    • 正規化されたメッセージのスナップショット
    • ゲーティングおよびバインディングの判定結果
  4. ワーカー内部で、そのプレーンなデータからワーカー実行コンテキストを再構築する。

実務的には、以下の要素が各ジョブに保持されるのをやめ、ワーカー自体またはワーカー所有のアダプターに保持されるようにすることを意味します:

  • client
  • threadBindings
  • guildHistories
  • discordRestFetch
  • その他ミュータブルなランタイム専用ハンドル

これが完了したら、次のフォローアップとして stop, new, reset のコマンド状態のクリーンアップを行うべきです。

以下のファイルにある既存のタイムアウト再現テストを維持します:

  • src/discord/monitor/message-handler.queue.test.ts

以下の新しいテストを追加します:

  1. リスナーがターンの完了を待たずに、エンキュー後に即座にリターンすること。
  2. ルートごとの順序が維持されていること。
  3. 異なるチャンネルが引き続き並行して動作すること。
  4. 返信が元のメッセージの送信先に配信されること。
  5. stop がアクティブなワーカー管理の実行をキャンセルすること。
  6. ワーカーの失敗が、後続のジョブをブロックすることなく、可視化された診断情報を生成すること。
  7. ACPにバインドされたDiscordチャンネルが、ワーカー実行下でも正しくルーティングされること。
  • リスク:コマンドセマンティクスが現在の同期的な動作から乖離する。 緩和策:コマンド状態の配管を、後回しにせず同じ切り替えタイミングで実装する。

  • リスク:返信配信でスレッドや返信先のコンテキストが失われる。 緩和策:DiscordInboundJob において配信識別情報を第一級の要素として扱う。

  • リスク:リトライやキューの再起動中に二重送信が発生する。 緩和策:最初のパスはメモリ内のみに留めるか、永続化の前に明示的な配信のべき等性を追加する。

  • リスク:移行中に message-handler.process.ts の見通しが悪くなる。 緩和策:ワーカーの切り替え前または切り替え中に、正規化、実行、配信のヘルパーに分割する。

以下の条件を満たしたときに計画完了とみなします:

  1. Discordリスナーのタイムアウトによって、正常に動作している長時間実行ターンが中断されなくなること。
  2. コード内で、リスナーの生存期間とエージェントターンの生存期間が別々の概念になっていること。
  3. 既存のセッションごとの順序が維持されていること。
  4. ACPにバインドされたDiscordチャンネルが、同じワーカーパスを通じて動作すること。
  5. stop が、古いリスナー所有のコールスタックではなく、ワーカー所有の実行をターゲットにすること。
  6. タイムアウトや配信の失敗が、リスナーによるサイレントなドロップではなく、明示的なワーカーの結果になること。

以下のフォローアップPRで完成させます:

  1. DiscordInboundJob をプレーンデータのみにし、ライブなランタイム参照をワーカーに移動する。
  2. stop, new, reset のコマンド状態の所有権を整理する。
  3. ワーカーのオブザーバビリティとオペレーターステータスを追加する。
  4. 耐久性が必要かどうかを決定するか、メモリ内境界を明示的にドキュメント化する。

Discord専用に限定し、時期尚早なチャンネル横断的ワーカー抽象化を避けることで、これは管理可能な範囲のフォローアップとして進めることができます。

OpenClaw

OpenClaw Expert

まだ解決しませんか?

このページで解決しない場合は、OpenClaw Expertに直接質問してください。