diff --git a/src/discord-bridge.py b/src/discord-bridge.py index a8690f0..972076a 100644 --- a/src/discord-bridge.py +++ b/src/discord-bridge.py @@ -10,7 +10,6 @@ import json import os import shlex -import subprocess import sys import time from pathlib import Path @@ -138,7 +137,6 @@ async def on_ready(): client.loop.create_task(poll_results()) client.loop.create_task(poll_approved()) client.loop.create_task(poll_proactive()) - client.loop.create_task(poll_dm_fallback()) def _message_mentions_bot(message): @@ -700,68 +698,6 @@ async def poll_proactive(): await asyncio.sleep(3) -async def poll_dm_fallback(): - """Fallback path for task/question/briefing results that no other - consumer is going to handle. - - These are voice-originated or cron-originated results (not Discord or - Telegram, which have their own pending-reply paths). When the voice - client is disconnected — or the file has been sitting long enough that - it's clearly stale — the result would otherwise be silently lost. This - loop shells out to `src/dm-result.py`, which contains the - voiceConnected-check + Discord-DM-send logic shipped in PR #347. - - Grace period: 90s. Discord-bound files are skipped via `pending_replies` - so we don't race with `poll_results()`. Proactive files are handled by - `poll_proactive()` already, so we don't touch those either. - """ - GRACE_SECONDS = 90 - FALLBACK_PREFIXES = ("task-", "question-", "briefing-", "insight-", "friction-") - while True: - try: - now = time.time() - for f in RESULTS_DIR.iterdir(): - if f.suffix != ".txt": - continue - if not any(f.name.startswith(p) for p in FALLBACK_PREFIXES): - continue - # Skip anything Discord is already tracking for reply. - task_id = f.stem # e.g. "task-1776286725412" - if task_id in pending_replies: - continue - # Grace window so voice-agent / telegram-bridge get first dibs. - try: - age = now - f.stat().st_mtime - except FileNotFoundError: - continue - if age < GRACE_SECONDS: - continue - # Subprocess out to the shared CLI tool so there's only one - # code path for the voiceConnected check + DM send. - try: - result = subprocess.run( - ["python3", str(REPO / "src" / "dm-result.py"), "--file", str(f)], - capture_output=True, text=True, timeout=15, - ) - except Exception as e: - print(f" [dm-fallback] subprocess failed on {f.name}: {e}") - continue - if result.returncode == 0: - stdout = (result.stdout or "").strip() - # dm-result.py prints "voice connected, skipping" when voice is up. - # In that case we leave the file alone for voice-agent to pick up. - if "skipping DM" in stdout: - continue - print(f" [dm-fallback] sent {f.name} via dm-result.py") - f.unlink(missing_ok=True) - else: - stderr = (result.stderr or "").strip()[:200] - print(f" [dm-fallback] dm-result.py failed on {f.name}: {stderr}") - except Exception as e: - print(f" [dm-fallback] poll error: {e}") - await asyncio.sleep(30) - - def _send_via_rest(channel_id: str, message: str): """Send a message via Discord REST API (no gateway connection). Exits after sending.""" import urllib.request diff --git a/src/task-bridge.ts b/src/task-bridge.ts index 491e4b7..7b760e7 100644 --- a/src/task-bridge.ts +++ b/src/task-bridge.ts @@ -9,6 +9,7 @@ */ import { writeFileSync, readFileSync, existsSync, unlinkSync, mkdirSync, readdirSync, appendFileSync } from 'node:fs'; +import { execSync } from 'node:child_process'; import { join } from 'node:path'; import { z } from 'zod'; import type { ToolDefinition } from 'bodhi-realtime-agent'; @@ -318,8 +319,37 @@ export function startResultWatcher(onResult: (result: string) => void, isClientC const files = readdirSync(RESULT_DIR).filter(f => f.endsWith('.txt')).sort(); if (files.length === 0) return; - // Only deliver if a client is connected — otherwise keep files queued + // If no voice client connected, fall back to Discord DM via + // dm-result.py. That script resolves the owner and DM channel + // on demand via the Discord API (no hardcoded channel ID). + // Handles the existing bookkeeping inline — mirrors the + // connected path below — so delivered files aren't re-processed. if (!isClientConnected()) { + for (const file of files) { + if (_deliveredResults.has(file)) continue; + const path = join(RESULT_DIR, file); + const result = readFileSync(path, 'utf-8').trim(); + if (!result) continue; + const taskId = file.replace('.txt', ''); + console.log(`${ts()} [TaskBridge] Voice offline — sending ${file} via Discord DM`); + try { + execSync(`python3 "${join(REPO_DIR, 'src', 'dm-result.py')}" --file "${path}"`, { timeout: 15_000 }); + } catch (e: any) { + console.error(`${ts()} [TaskBridge] DM fallback failed for ${file}: ${e.message}`); + } + _sendTaskStatus?.(taskId, 'done', result.slice(0, 60), result); + _deliveredResults.add(file); + _pendingTasks.delete(taskId); + logConversation('core-agent', `[task:${taskId}] ${result.slice(0, 200)}`); + try { + fetch('http://localhost:7843/task-done', { + method: 'POST', + headers: _apiHeaders(), + body: JSON.stringify({ taskId, result }), + }).catch(() => {}); + } catch {} + setTimeout(() => { try { unlinkSync(path); } catch {} }, 10_000); + } return; }