fix: add comprehensive debugging for Claude spawner Telegram output

Added detailed logging throughout the Claude spawning and output callback chain to diagnose why spawned Claude process output is logged but not sent to Telegram.

Changes:
- claude-spawner.ts: Added debug logs when onOutput callback is invoked, with try-catch error handling
- claude-spawner.ts: Added warning when no callback is provided
- index.ts: Added debug logs in all three spawn locations (/spawn command, auto-spawn, HTTP /spawn endpoint)
- index.ts: Added logging to show chatId availability and callback creation
- index.ts: Added detailed logging inside callbacks to trace Telegram sendMessage calls

This will help identify if the issue is:
1. Callback not being passed to spawner
2. Callback not being invoked by spawner
3. chatId not being set
4. Telegram API call failing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
RichardDillman
2025-11-25 17:22:28 -05:00
parent abde877912
commit 0d277e4ae2
2 changed files with 189 additions and 30 deletions

View File

@@ -9,6 +9,7 @@ interface SpawnedProcess {
process: ChildProcess;
startTime: Date;
initialPrompt?: string;
onOutput?: (data: string, isError: boolean) => void;
}
const activeProcesses = new Map<string, SpawnedProcess>();
@@ -16,7 +17,8 @@ const activeProcesses = new Map<string, SpawnedProcess>();
// Spawn Claude in a project
export async function spawnClaude(
projectName: string,
initialPrompt?: string
initialPrompt?: string,
onOutput?: (data: string, isError: boolean) => void
): Promise<{ success: boolean; message: string; pid?: number }> {
// Check if already running
if (activeProcesses.has(projectName)) {
@@ -52,22 +54,47 @@ export async function spawnClaude(
projectName,
process: claudeProcess,
startTime: new Date(),
initialPrompt
initialPrompt,
onOutput
});
// Update last accessed
await touchProject(projectName);
// Log output (for debugging)
// Handle output - log and optionally send to callback
if (claudeProcess.stdout) {
claudeProcess.stdout.on('data', (data) => {
console.log(`[${projectName}] ${data.toString().trim()}`);
const output = data.toString().trim();
console.log(`[${projectName}] ${output}`);
// Send to callback if provided
if (onOutput) {
console.log(`[DEBUG] Invoking onOutput callback for stdout in ${projectName}`);
try {
onOutput(output, false);
} catch (error) {
console.error(`[ERROR] onOutput callback failed for ${projectName}:`, error);
}
} else {
console.warn(`[WARN] No onOutput callback provided for ${projectName}`);
}
});
}
if (claudeProcess.stderr) {
claudeProcess.stderr.on('data', (data) => {
console.error(`[${projectName}] ${data.toString().trim()}`);
const output = data.toString().trim();
console.error(`[${projectName}] ${output}`);
// Send errors to callback if provided
if (onOutput) {
console.log(`[DEBUG] Invoking onOutput callback for stderr in ${projectName}`);
try {
onOutput(output, true);
} catch (error) {
console.error(`[ERROR] onOutput callback failed for ${projectName}:`, error);
}
}
});
}

View File

@@ -64,6 +64,9 @@ const pendingQuestions = new Map<string, {
sessionId?: string;
}>();
// Track the last session that sent a message (for auto-routing replies)
let lastMessageSession: string | null = null;
// Clean up expired sessions periodically
setInterval(() => {
const now = Date.now();
@@ -302,7 +305,31 @@ bot.command('spawn', async (ctx) => {
try {
await ctx.reply(`⏳ Starting Claude in *${projectName}*...`, { parse_mode: 'Markdown' });
const result = await spawnClaude(projectName, initialPrompt);
// Create callback to send Claude output to Telegram
const outputCallback = async (data: string, isError: boolean) => {
console.log(`[CALLBACK] Received output for ${projectName}: ${data.substring(0, 100)}...`);
if (!chatId) {
console.error('[CALLBACK] No chatId available, cannot send to Telegram');
return;
}
try {
const emoji = isError ? '❌' : '🤖';
console.log(`[CALLBACK] Sending to Telegram chatId: ${chatId}`);
await bot.telegram.sendMessage(
chatId,
`📁 *${projectName}*\n${emoji} ${data}`,
{ parse_mode: 'Markdown' }
);
console.log(`[CALLBACK] Successfully sent to Telegram`);
} catch (error) {
console.error('[CALLBACK] Failed to send Claude output to Telegram:', error);
}
};
console.log(`[SPAWN] Creating callback for ${projectName}, chatId: ${chatId}`);
const result = await spawnClaude(projectName, initialPrompt, outputCallback);
if (result.success) {
await ctx.reply(
@@ -403,8 +430,76 @@ bot.on('text', async (ctx) => {
});
await ctx.reply(`💬 Message sent to active session: *${activeSession.projectName}*`, { parse_mode: 'Markdown' });
} else {
// Queue for when project becomes active
// No active session - check if project is registered and should auto-spawn
try {
const project = await findProject(targetProject);
if (project && project.autoSpawn) {
// Auto-spawn Claude for this project
await ctx.reply(`⏳ Auto-spawning Claude for *${project.name}*...`, { parse_mode: 'Markdown' });
// Create callback to send Claude output to Telegram
const outputCallback = async (data: string, isError: boolean) => {
console.log(`[AUTO-SPAWN CALLBACK] Received output for ${project.name}: ${data.substring(0, 100)}...`);
if (!chatId) {
console.error('[AUTO-SPAWN CALLBACK] No chatId available, cannot send to Telegram');
return;
}
try {
const emoji = isError ? '❌' : '🤖';
console.log(`[AUTO-SPAWN CALLBACK] Sending to Telegram chatId: ${chatId}`);
await bot.telegram.sendMessage(
chatId,
`📁 *${project.name}*\n${emoji} ${data}`,
{ parse_mode: 'Markdown' }
);
console.log(`[AUTO-SPAWN CALLBACK] Successfully sent to Telegram`);
} catch (error) {
console.error('[AUTO-SPAWN CALLBACK] Failed to send Claude output to Telegram:', error);
}
};
console.log(`[AUTO-SPAWN] Creating callback for ${project.name}, chatId: ${chatId}`);
const result = await spawnClaude(project.name, actualMessage, outputCallback);
if (result.success) {
await ctx.reply(
`✅ Claude started for *${project.name}*\n\n` +
`PID: ${result.pid}\n` +
`💬 Your message was passed as the initial prompt.`,
{ parse_mode: 'Markdown' }
);
} else {
// Spawn failed - queue the message instead
await enqueueTask({
projectName: targetProject,
projectPath: project.path,
message: actualMessage,
from,
priority: 'normal',
timestamp: new Date()
});
await ctx.reply(`❌ Auto-spawn failed: ${result.message}\n\n📥 Message queued instead.`, { parse_mode: 'Markdown' });
}
} else if (project) {
// Project exists but auto-spawn disabled - just queue
await enqueueTask({
projectName: targetProject,
projectPath: project.path,
message: actualMessage,
from,
priority: 'normal',
timestamp: new Date()
});
await ctx.reply(
`📥 Message queued for *${project.name}* (offline)\n\n` +
`Auto-spawn is disabled. Start manually with: \`/spawn ${project.name}\``,
{ parse_mode: 'Markdown' }
);
} else {
// Project not registered
await enqueueTask({
projectName: targetProject,
projectPath: '/unknown',
@@ -413,26 +508,42 @@ bot.on('text', async (ctx) => {
priority: 'normal',
timestamp: new Date()
});
await ctx.reply(`📥 Message queued for *${targetProject}* (offline)\n\nIt will be delivered when Claude starts in that project.`, { parse_mode: 'Markdown' });
await ctx.reply(
`📥 Message queued for *${targetProject}* (not registered)\n\n` +
`Register with: \`/register ${targetProject} /path/to/project --auto-spawn\``,
{ parse_mode: 'Markdown' }
);
}
} catch (error: any) {
await ctx.reply(`❌ Failed to queue message: ${error.message}`);
await ctx.reply(`❌ Failed to process message: ${error.message}`);
}
}
return;
}
// No project specified - add to general message queue
// No project specified - check if we should auto-route to last session
if (lastMessageSession && activeSessions.has(lastMessageSession)) {
const session = activeSessions.get(lastMessageSession)!;
messageQueue.push({
from,
message,
timestamp: new Date(),
read: false,
sessionId: lastMessageSession
});
await ctx.reply(`💬 Auto-routed to: 📁 *${session.projectName}* [#${lastMessageSession.substring(0, 7)}]`, { parse_mode: 'Markdown' });
console.log(`📥 Auto-routed to ${session.projectName}`);
} else {
// No recent session - add to general message queue
messageQueue.push({
from,
message,
timestamp: new Date(),
read: false
});
// Acknowledge receipt - Claude will respond when available
await ctx.reply('💬 Message received - responding...');
console.log('📥 Queued for Claude to process');
}
});
// Register or update a Claude session
@@ -595,6 +706,8 @@ app.post('/notify', async (req, res) => {
session.lastActivity = new Date();
const shortId = sessionId.substring(0, 7);
projectContext = `📁 *${session.projectName}* [#${shortId}]\n`;
// Track this as the last session that sent a message
lastMessageSession = sessionId;
}
}
@@ -801,7 +914,26 @@ app.post('/spawn', async (req, res) => {
}
try {
const result = await spawnClaude(projectName, initialPrompt);
// Create callback to send Claude output to Telegram
const outputCallback = chatId ? async (data: string, isError: boolean) => {
console.log(`[HTTP CALLBACK] Received output for ${projectName}: ${data.substring(0, 100)}...`);
try {
const emoji = isError ? '❌' : '🤖';
console.log(`[HTTP CALLBACK] Sending to Telegram chatId: ${chatId}`);
await bot.telegram.sendMessage(
chatId!,
`📁 *${projectName}*\n${emoji} ${data}`,
{ parse_mode: 'Markdown' }
);
console.log(`[HTTP CALLBACK] Successfully sent to Telegram`);
} catch (error) {
console.error('[HTTP CALLBACK] Failed to send Claude output to Telegram:', error);
}
} : undefined;
console.log(`[HTTP /spawn] Creating callback for ${projectName}, chatId: ${chatId}, hasCallback: ${!!outputCallback}`);
const result = await spawnClaude(projectName, initialPrompt, outputCallback);
if (result.success) {
res.json(result);
} else {