期限切れの再配信 BlueBubbles Webhook をドロップする maxMessageAge フィルターの追加
OpenClaw の BlueBubbles チャンネルを設定して、設定可能なしきい値より古い受信メッセージをサイレントにドロップし、期限切れの Webhook と内部エラーレスポンスの処理を防止する方法について説明します。
🔍 症状
наблюдаемое поведение
BlueBubblesが古いメッセージのWebhookを再配信する際、以下の症状が発生します:
- 古いメッセージの処理: 数時間または数日前のタイムスタンプを持つメッセージが、新しいメッセージであるかのようにエージェントの会話フローに表示されます
- 不適切な返信: エージェントが古いメッセージに対してコンテキストに基づく返信を生成し、ユーザーを困惑させます
- レート制限エラーの返信: AIサービスが529(レート制限)レスポンスを返した場合、生のエラーテキストがiMessageの返信として送信されます:
The AI service is temporarily overloaded. Please try again in a moment.
技術的な症状
問題のあるWebhookペイロードには、現在の時刻と一致しないdateCreated値が含まれています:
json { “id”: “msg-abc123”, “dateCreated”: “2026-01-15T08:30:00Z”, “text”: “Hello from yesterday”, “guid”: “some-guid” }
現在のサーバー時刻が2026-02-24T14:00:00Zの場合、このメッセージは40日前のものになりますが、标准的な受信ハンドラパイプラインを通じて処理されます。
🧠 原因
アーキテクチャの問題
BlueBubblesの受信ハンドラには、メッセージ受付レイヤーでの時間的検証が欠けています。Webhook処理パイプラインには2つの重要なギャップがあります:
- 年齢閾値の検証がない: ハンドラは`dateCreated`タイムスタンプに関係なく、すべての受信メッセージを処理します。BlueBubblesは内部キューから古いメッセージをたまに再配信し、古くなったタイムスタンプを持つペイロードを新鮮な受信メッセージであるかのように送信します。
- 内部エラーメッセージの漏洩: 内部エラーハンドリングミドルウェアによって生成されたエラーメッセージ(特に529レート制限レスポンス)が、正当なユーザーメッセージと同じ返信パイプラインにルーティングされており、生のエラー文本がiMessageの返信として送信されています。
失敗シーケンス
- BlueBubblesがキュー内の古いメッセージを検出
- BlueBubblesがOpenClaw受信エンドポイントにWebhook POSTを送信
- ハンドラがメッセージを抽出、dateCreatedを抽出: “2026-01-15T08:30:00Z”
- 年齢チェックなし → メッセージがエージェント処理パイプラインに入る
- エージェントが古いコンテキストに基づいてレスポンスを生成
- レスポンスがBlueBubbles返信チャネル経由でiMessageとして送信
または(エラーの場合):
- AIサービスが529レート制限エラーを返す
- エラーミドルウェアが人が読めるエラーメッセージを生成
- エラーメッセージが返信チャネル経由で流れ、ログに記録されない
- ユーザーが「The AI service is temporarily overloaded…」をテキストメッセージとして受信
コードパス分析
問題はBlueBubblesハンドラ内のメッセージ受付ロジックにあります。設定可能なmaxMessageAgeSecフィールドがないため、以下のことができません:
- メッセージの`dateCreated`を現在のサーバー時刻と比較する
- 経過時間が設定された閾値を超えているかどうかを判断する
- サイレントドロップ(DEBUGレベルでログ記録)を選択して処理パイプラインを遮断する
🛠️ 解決手順
設定の追加
maxMessageAgeSecフィールドをOpenClaw設定ファイル(config.jsonまたは環境ベースの設定)に追加します:
json { “channels”: { “bluebubbles”: { “serverUrl”: “https://your-bluebubbles-server.local”, “password”: “your-password-here”, “maxMessageAgeSec”: 300 } } }
設定の前後比較
設定前(年齢フィルタリングなし):
json { “channels”: { “bluebubbles”: { “serverUrl”: “https://bb-server.local”, “password”: “secret123” } } }
設定後(年齢フィルタリングあり):
json { “channels”: { “bluebubbles”: { “serverUrl”: “https://bb-server.local”, “password”: “secret123”, “maxMessageAgeSec”: 300 } } }
ハンドラの実装手順
この修正をコードベースに実装するには、以下の変更を行います:
- チャネル設定インターフェースで検証定数を定義:
// Within channels/bluebubbles/types.ts or similar export interface BlueBubblesConfig { serverUrl: string; password: string; maxMessageAgeSec?: number; // Optional, defaults to no filtering } - 受信メッセージハンドラに年齢検証を追加:
// Within the webhook handler function async function handleInboundMessage(payload: BlueBubblesWebhookPayload): Promise<void> { const config = getBlueBubblesConfig();// Validate message age if (config.maxMessageAgeSec) { const messageAge = Date.now() - new Date(payload.dateCreated).getTime(); const maxAgeMs = config.maxMessageAgeSec * 1000;
if (messageAge > maxAgeMs) { logger.debug(`Dropping stale message ${payload.guid} (age: ${Math.floor(messageAge/60000)}m)`); return; // Silent drop }}
// Continue with normal processing… await processMessage(payload); }
- エラーメッセージが返信先にルーティングされないことを確認:
// Error middleware or handler function handleAIErrors(error: Error, context: MessageContext): void { if (error.statusCode === 529) { // Rate limit: log internally, do NOT send to user logger.warn(`AI service rate-limited for message ${context.messageId}`); return; // No reply sent }// For other errors, decide based on error visibility config if (shouldExposeErrors()) { sendReply(context, generateSafeErrorMessage(error)); } else { logger.error(error); } }
🧪 検証
メッセージ年齢フィルタリングのテスト手順
- 更新された設定をデプロイし、OpenClawサービスを再起動:
# Restart OpenClaw to load new configuration sudo systemctl restart openclawCheck service status
sudo systemctl status openclaw –no-pager
- 設定が読み込まれていることを確認:
# Check logs for config load tail -50 /var/log/openclaw/openclaw.log | grep -i "bluebubbles\|maxMessageAge"Expected output:
[INFO] BlueBubbles channel initialized with maxMessageAgeSec=300
- 古いメッセージの処理 테스트:
# Send a test webhook with old timestamp via curl curl -X POST http://localhost:3000/webhooks/bluebubbles \ -H "Content-Type: application/json" \ -H "X-BlueBubbles-Password: your-password" \ -d '{ "id": "test-stale-001", "guid": "test-stale-guid", "dateCreated": "2026-01-15T08:30:00Z", "text": "Test stale message" }'Expected: 200 OK, no reply sent, log entry for dropped message
Check logs for dropped message confirmation
tail -20 /var/log/openclaw/openclaw.log | grep “Dropping stale”
- 新鮮なメッセージの処理テスト(健全性チェック):
# Send a webhook with current timestamp curl -X POST http://localhost:3000/webhooks/bluebubbles \ -H "Content-Type: application/json" \ -H "X-BlueBubbles-Password: your-password" \ -d "{ \"id\": \"test-fresh-001\", \"guid\": \"test-fresh-guid-$(date +%s)\", \"dateCreated\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"text\": \"Test fresh message\" }"Expected: 200 OK, message processed normally, iMessage reply sent
期待される結果
- 古いメッセージ(`maxMessageAgeSec`より古い)はHTTP 200を返しますが、DEBUGレベルのログエントリを生成し、返信は送信されません
- 新鮮なメッセージは通常のパイプラインを通じて処理され、エージェントのレスポンスを受けます
- エラーメッセージ(529レート制限)はログに記録されますが、iMessageの返信として転送されることはありません
⚠️ よくある落とし穴
- タイムゾーンの不一致: OpenClawサーバーのシステムクロックがBlueBubblesと同期していることを確認してください。サーバーが異なるタイムゾーンにあり、`dateCreated`にローカル時刻を使用している場合、メッセージが正しく古すぎるまたは新鮮ものとして分類されることがあります。UTCタイムスタンプを一貫して使用してください。
- 設定を再起動していない: `config.json`の編集はOpenClawを再起動するまで有効になりません。構造的な設定変更ではホットリロードはサポートされていません。
- 競合するオーバーロード動作: macOS環境では、`launchctl`サービス管理が`systemctl`を使用しない場合があります。`systemctl restart`の代わりに`launchctl unload` / `launchctl load`を使用してください。
- 設定なしのデフォルト動作: `maxMessageAgeSec`を省略すると、すべてのメッセージ(古いもの含む)が処理されます。組み込みのデフォルトはなく、値を明示的に設定する必要があります。
- Docker環境変数のマッピング: Dockerを使用する場合、環境変数のネストされたJSONは階層を示すために二重アンダースコアを使用する必要があります:
CHANNELS__BLUEBUBBLES__MAXMESSAGEAGESEC=300 - ログレベルの詳細度: DEBUGレベルのドロップメッセージはデフォルトのログ出力に表示されない場合があります。bluebubblesチャネルのログ設定をDEBUGに設定しているか、トレースログファイルを確認してください。
🔗 関連するエラー
- 529 レート制限エラー: エラーハンドリングが返信パイプラインから分離されていない場合、iMessageテキストとして漏洩するAIサービスの一時的な過負荷レスポンス
- EAI_AGAIN / EHOSTUNREACH: BlueBubblesサーバーに到達できない場合のネットワークエラー;これらはメッセージ年齢の問題とは異なりますが、同様のログに表示されることがあります
- 重複メッセージ処理: BlueBubblesは同じWebhookを複数回送信することがあります;年齢フィルタリングが欠けている場合、重複したエージェントレスポンスが発生します
- 認証失敗 (401): 設定内のパスワードが正しくない場合、すべてのWebhookが拒否されます;年齢フィルタリングとは無関係ですが、設定エラーと誤解されることがあります
- タイムスタンプ解析エラー: BlueBubblesペイロードの不正な`dateCreated`フィールドは例外を引き起こす可能性があります;無効な日付形式の穏やかな処理を確保してください