// ❌ 再起動で失われるもの
- setTimeout/setInterval のタイマー
- WebSocket接続状態
- 進行中のPromise/コールバック
- メモリキャッシュ
- イベントリスナーの登録状態// ✅ 再起動後も残るもの
- セッション基本情報
- Discord Thread ID ↔ Devin Session ID マッピング
- セッション状態(ただし不正確な可能性)
- ユーザー情報| 戦略 | Redis | ログ | 追加DB | 復旧速度 | 実装複雑度 | 推奨度 |
|---|---|---|---|---|---|---|
| 起動時復旧 | ✅ | - | - | 高 | 中 | ⭐⭐⭐⭐⭐ |
| 定期チェック | ✅ | - | - | 中 | 低 | ⭐⭐⭐⭐ |
| 手動復旧 | ✅ | - | - | 低 | 低 | ⭐⭐⭐ |
| 完全永続化 | ✅ | ✅ | ✅ | 高 | 高 | ⭐⭐ |
class DevinDiscordBot {
async startup(): Promise<void> {
console.log('🚀 Bot starting up...');
// 1. Discord 接続
await this.connectDiscord();
// 2. Redis 接続
await this.connectRedis();
// 3. セッション復旧
await this.recoverSessions();
console.log('✅ Bot ready!');
}
private async recoverSessions(): Promise<void> {
const sessionKeys = await this.redis.keys('sessions:*');
console.log(`🔄 Found ${sessionKeys.length} sessions to check`);
const recoveryTasks = sessionKeys.map(key => this.recoverSession(key));
await Promise.allSettled(recoveryTasks);
}
private async recoverSession(sessionKey: string): Promise<void> {
try {
const sessionData = await this.redis.get(sessionKey);
if (!sessionData) return;
const session: SessionData = JSON.parse(sessionData);
// 実行中のセッションのみ処理
if (session.status !== 'running') return;
console.log(`🔍 Checking session: ${session.id}`);
// Devin API で現在の状態を確認
const devinStatus = await this.devinClient.getStatus(
session.devinSessionId
);
if (devinStatus.completed) {
await this.handleCompletion(session, devinStatus.result);
} else if (devinStatus.failed) {
await this.handleError(session, devinStatus.error);
} else {
// まだ実行中 → ポーリング再開
await this.resumePolling(session);
}
} catch (error) {
console.error(`❌ Failed to recover session ${sessionKey}:`, error);
}
}
private async resumePolling(session: SessionData): Promise<void> {
// Discord に復旧通知
const thread = this.discord.channels.cache.get(session.discordThreadId);
await thread?.send({
embeds: [{
color: 0x00ff00,
title: '🔄 監視を再開しました',
description: 'Botが再起動したため、セッションの監視を再開します。',
timestamp: new Date().toISOString()
}]
});
// ポーリング開始
this.startPolling(session.id);
}
}interface SessionData {
id: string;
discordThreadId: string;
devinSessionId: string;
userId: string;
status: SessionStatus;
createdAt: number;
updatedAt: number; // ← 追加:最終更新時刻
lastPolled: number; // ← 追加:最終ポーリング時刻
taskDescription: string;
metadata?: {
retryCount?: number; // ← 追加:リトライ回数
errorHistory?: string[]; // ← 追加:エラー履歴
};
}
// 孤児セッション検出用のハートビート
interface HeartbeatData {
sessionId: string;
processId: string; // プロセス識別子
lastHeartbeat: number;
}- セッション状態の保持: 基本情報は保存される
- DevinAPI連携: セッションIDがあれば状態確認可能
- Discord連携: ThreadIDがあれば通知送信可能
- 自動復旧: 起動時に状態同期できる
- 詳細ログなし: エラーの詳細分析は困難
- 長期統計なし: 使用状況の分析不可
- 完全性保証なし: Redis障害時はデータ消失
// 最低限のログ出力でカバー
class SimpleLogger {
static logSessionEvent(event: string, sessionId: string, data?: any): void {
const logEntry = {
timestamp: new Date().toISOString(),
event,
sessionId,
data
};
// 標準出力(Docker ログで収集可能)
console.log(JSON.stringify(logEntry));
}
}
// 使用例
SimpleLogger.logSessionEvent('session.created', sessionId, { task });
SimpleLogger.logSessionEvent('session.completed', sessionId, { duration });
SimpleLogger.logSessionEvent('process.recovered', sessionId, { devinStatus });- Redis セッション管理
- 起動時のセッション復旧
- 基本的なエラーハンドリング
- ハートビート機能
- 孤児セッション検出
- 詳細ログ出力
- メトリクス収集
- アラート設定
- ダッシュボード構築
Redisとログだけで十分です。
- 起動時復旧でセッション連続性を保証
- DevinAPIで状態同期可能
- シンプルなログでトラブルシューティング対応
- 必要に応じて後から PostgreSQL 追加可能
追加の永続化は、運用してみて実際に必要性を感じてからでも遅くありません。