← 一覧に戻る
Telegram メッセージ取りこぼし対策 v2 設計書
2026年4月7日 22:50 更新
MD から自動変換されたページです。内容について質問があれば右下の ? ボタンからどうぞ。
問題
秘書セッションがツール実行中(dispatch-job.sh等の長時間Bash)にTelegramメッセージが来ると、
Claude Codeが処理しない場合がある。message_id=2604が完全にロストした実例あり。
Phase 2A の失敗分析
設計: 3スクリプト構成
intent-hook.sh — UserPromptSubmit hookでTelegramメッセージを検知→record-intent.sh呼び出し
record-intent.sh — intentsテーブルにメッセージを記録
intents-timer.sh — cron(5分間隔)で未処理intentを検知→秘書セッションに通知
根本原因1: UserPromptSubmitはMCP notificationで発火しない
Telegramメッセージは以下の経路でClaudeに届く:
Telegram → grammY long-polling → server.ts handleInbound()
→ mcp.notification('notifications/claude/channel', ...) → Claude Codeがsystem-reminderとして注入
mcp.notification() はUserPromptSubmitのパイプラインを完全にバイパスする。
これは patch-telegram-plugin.sh のコメント(4行目)にも明記されている。
→ intent-hook.shは構造的に動作不可能
根本原因2: パッチ方式は存在するが本番で稼働していなかった
patch-telegram-plugin.sh がserver.tsに直接record-intent.sh呼び出しを注入する方式は
設計として正しいが、初適用が2026-04-07 07:50と最近。それまでの本番セッションでは未適用。
結果
- intentsテーブル: 0件(本番メッセージは1件も記録されていない)
- intents-timer.sh: 記録がないため通知も発生しない
- 取りこぼし検知・リカバリの仕組みが一切機能していない
新設計: 5層防御アーキテクチャ
Layer 1: 確実な記録 (Recording) — 既存修正
場所: server.ts パッチ (patch-telegram-plugin.sh)
仕組み: handleInbound() 内で mcp.notification() の直前に record-intent.sh を fire-and-forget で実行
状態: パッチ自体は堅牢(構文検証・配置検証・ntfyアラート付き)。run-claude.shで毎回適用。
修正点: なし(既に動作確認済み。今後はrun-claude.sh経由で常に適用される)
Layer 2: 処理追跡 (Processing Tracking) — 新規
場所: PostToolUse hook(secretary/settings.json)
対象ツール: mcp__plugin_telegram_telegram__reply のみ
仕組み:
- Claudeが reply ツールを使うとき tool_input.reply_to にメッセージIDが、tool_input.chat_id にチャットIDが入る
- PostToolUseフックで両方を抽出し、複合キー (chat_id, reply_to) で intents テーブルの該当行を status='closed' に更新
- close 条件: intent ID = "telegram-{chat_id}-{reply_to}" の完全一致のみ
※ Telegram の message_id はチャットスコープ(グローバルに一意ではない)ため、
reply_to 単独での照合は chat 間の誤閉鎖を起こす(Codex 第2回指摘)
- reply_to がない場合: close しない(pending のまま残し Layer 3/4 のリマインドに委ねる)
【Codex指摘反映】
- react は close 条件から除外: リアクションは「見た」であって「対応完了」ではない。
close すると Layer 3/4/5 が検知不能になり、未回答メッセージが静かにロストする
- reply_to 欠落時のヒューリスティック close を廃止: 誤閉鎖 = 恒久的な見逃し。
close は message_id の明示一致時のみ。曖昧ケースは pending のまま残す
スクリプト: ~/.claude/scripts/close-intent.sh
Layer 3: セッション内リマインド (In-Session Reminder) — 新規
場所: PostToolUse hook(secretary/settings.json)
対象ツール: Bash(長時間実行の主要因)
仕組み:
- Bash実行完了後、intentsテーブルでpending状態かつ3分以上経過のintentを検索
- 該当があれば additionalContext としてClaudeのコンテキストに注入
- スロットリング: 同じintent IDへのリマインドは5分間隔で制限(DB列 last_reminded_at)
スクリプト: ~/.claude/scripts/check-pending-intents.sh
Layer 4: 外部ウォッチドッグ (Orphan Watchdog) — 既存
場所: launchctl cron(com.aiharataketo.intents-timer、5分間隔)
仕組み: pending状態で10分以上経過したintentを検知→秘書セッションにtmux.py sendで通知
状態: 既に動作中。Layer 1が安定すればそのまま機能する。
Layer 5: セッション開始時チェック (Bootstrap Check) — 新規
場所: SessionStart hook(secretary/settings.json)
仕組み:
- 秘書セッション起動時にpending intentsを全件チェック
- 該当があればsystem-reminderとして注入
スクリプト: ~/.claude/scripts/bootstrap-pending-intents.sh
スキーマ変更
intentsテーブルに last_reminded_at カラムを追加(Layer 3のスロットリング用):
ALTER TABLE intents ADD COLUMN last_reminded_at TEXT;
【Codex指摘反映】record-intent.sh の CREATE TABLE / MIGRATE セクションに
last_reminded_at を追加する(「変更不要」→「migration追加が必要」に訂正)。
新規DBでも既存DBでもカラム存在を保証する。check-pending-intents.sh は
カラム不在時に fail-closed する(silent fallthrough禁止)。
フック設定(secretary/settings.json)
PostToolUse:
- matcher: mcp__plugin_telegram_telegram__reply → close-intent.sh (Layer 2)
※ react は close 条件から除外(Codex指摘: リアクション ≠ 対応完了)
- matcher: Bash → check-pending-intents.sh (Layer 3)
SessionStart:
- matcher: * → bootstrap-pending-intents.sh (Layer 5)
UserPromptSubmit:
- intent-hook.sh を削除(MCP notificationでは発火しないため無意味)
メッセージフロー(取りこぼし発生時)
- ユーザーがTelegramでメッセージ送信 (message_id=2604)
- server.ts handleInbound() が受信
- [Layer 1] record-intent.sh → intents INSERT (status=pending)
- mcp.notification() → Claude Codeにsystem-reminder注入
- Claudeは長時間Bash実行中 → メッセージを処理しない
- Bash完了 → [Layer 3] check-pending-intents.sh が 3分超のpending検知 → additionalContext で注入
- Claudeがメッセージを処理し reply → [Layer 2] で closed
もし Layer 3 でも処理されなかった場合:
8. [Layer 4] intents-timer.sh (cron 5分) が 10分超のpending検知 → tmux.py send で通知
もしセッション間で残った場合:
9. [Layer 5] 次回SessionStart時にpending intentsを提示
replyツールのパラメータ(server.ts確認済み)
reply tool:
- chat_id: string (必須)
- text: string (必須)
- reply_to: string (任意 - "Message ID to thread under. Use message_id from the inbound block.")
- files: string[] (任意)
- format: 'text' | 'markdownv2' (任意)
react tool:
- chat_id: string (必須)
- message_id: string (必須)
- emoji: string (必須)
PostToolUseフックの出力仕様(公式ドキュメント確認済み)
PostToolUse hookはstdoutにJSONを出力することでClaudeのコンテキストに注入できる:
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "ここに書いた文字列がClaudeのコンテキストに追加される"
}
}
実装ファイル一覧
| ファイル |
操作 |
説明 |
| ~/.claude/scripts/close-intent.sh |
新規 |
Layer 2: reply時にintent閉じる(reply_to明示一致のみ) |
| ~/.claude/scripts/check-pending-intents.sh |
新規 |
Layer 3: Bash後にpending intent確認 |
| ~/.claude/scripts/bootstrap-pending-intents.sh |
新規 |
Layer 5: セッション開始時チェック |
| ~/.claude/secretary/settings.json |
修正 |
フック設定更新 |
| ~/.claude/scripts/intent-hook.sh |
削除 |
UserPromptSubmitでは動作しないため |
既存で修正が必要:
- record-intent.sh — last_reminded_at カラムの CREATE/MIGRATE/verify を追加
既存で変更不要:
- intents-timer.sh — そのまま使う
- patch-telegram-plugin.sh — そのまま使う
テスト戦略
各Layerを独立してテスト可能にする:
- close-intent.sh: テスト用intentをINSERT → close-intent.shにモックJSON入力 → status確認
- check-pending-intents.sh: テスト用ancient intentをINSERT → スクリプト実行 → stdout JSON確認
- bootstrap-pending-intents.sh: テスト用intentをINSERT → スクリプト実行 → stdout確認
- 統合テスト: record-intent.sh → (時刻操作) → check-pending-intents.sh → close-intent.sh のフロー
- 誤閉鎖防止テスト: 同一chatに複数pending → reply(reply_to=特定ID) → 指定IDのみclosed、他はpending維持
- react非close検証: react後もintentがpending維持されることを確認
- schema migration テスト: last_reminded_at なしDBでrecord-intent.sh実行 → カラム追加確認
- cross-chat衝突テスト: 異なるchat_idで同じmessage_idのintent2件 → reply(chat_id=A, reply_to=X) → Aのみclosed、Bはpending維持
Codex Adversarial Review 指摘事項と対応
| # |
指摘 |
深刻度 |
対応 |
| 1 |
react を close 条件にすると未返信メッセージがロストする |
high |
react を Layer 2 対象から除外 |
| 2 |
reply_to 欠落時のヒューリスティック close は誤閉鎖を起こす |
high |
明示一致のみに限定、曖昧ケースは pending 維持 |
| 3 |
last_reminded_at カラムが record-intent.sh の migration に含まれていない |
high |
record-intent.sh に migration 追加 |
第2回レビュー指摘(修正後の再検証)
| # |
指摘 |
深刻度 |
対応 |
| 4 |
last_reminded_at migration は設計のみで未実装 |
high |
実装フェーズで record-intent.sh に追加(TDD で検証) |
| 5 |
close 条件が reply_to 単独 → chat 間の message_id 衝突で誤閉鎖 |
high |
複合キー (chat_id, reply_to) で完全一致に修正 + cross-chat テスト追加 |