Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 0 additions & 64 deletions src/discord-bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import json
import os
import shlex
import subprocess
import sys
import time
from pathlib import Path
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
32 changes: 31 additions & 1 deletion src/task-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}

Expand Down
Loading