Skip to content

Commit 8c55a01

Browse files
Stewart86claude
andcommitted
fix: implement proper JSON buffering for Claude streaming responses
Bobby was failing to parse multi-line JSON objects from Claude Code's streaming output, causing valid response data to be skipped. This fix implements proper JSON buffering with state-based parsing that handles JSON objects split across multiple chunks. • Added JSON buffer to accumulate partial JSON objects • Implemented brace counting and string context tracking • Added proper escape sequence handling for JSON parsing • Fixed buffer cleanup to prevent memory buildup 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent fc0ed14 commit 8c55a01

File tree

1 file changed

+131
-79
lines changed

1 file changed

+131
-79
lines changed

index.js

Lines changed: 131 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,25 @@ if (!process.env.GH_TOKEN) {
3232
process.exit(1);
3333
}
3434

35-
// Initialize Discord client
35+
// Self-healing configuration
36+
const RECONNECT_DELAY = 5000; // 5 seconds
37+
const MAX_RECONNECT_ATTEMPTS = 10;
38+
let reconnectAttempts = 0;
39+
let isReconnecting = false;
40+
41+
// Initialize Discord client with error handling
3642
const client = new Client({
3743
intents: [
3844
GatewayIntentBits.Guilds,
3945
GatewayIntentBits.GuildMessages,
4046
GatewayIntentBits.MessageContent,
4147
],
48+
// Add WebSocket options for better error handling
49+
ws: {
50+
properties: {
51+
browser: 'Discord.js'
52+
}
53+
}
4254
});
4355

4456

@@ -183,101 +195,141 @@ Be precise, actionable, and concise. Users value speed and accuracy over verbose
183195
let stderrBuffer = "";
184196
let extractedSessionId = sessionId; // Keep track of session ID
185197
let threadTitle = null;
198+
let jsonBuffer = ""; // Buffer to accumulate partial JSON
186199

187200
// Process stdout stream in real-time
188201
try {
189202
for await (const chunk of proc.stdout) {
190203
const text = new TextDecoder().decode(chunk);
191-
192-
// Parse each line as separate JSON objects
193-
const lines = text.split('\n').filter(line => line.trim());
194-
195-
for (const line of lines) {
196-
try {
197-
const jsonData = JSON.parse(line);
198-
199-
// Extract session ID from metadata
200-
if (jsonData.type === 'metadata' && jsonData.session_id) {
201-
extractedSessionId = jsonData.session_id;
202-
console.log(`Captured session ID: ${extractedSessionId}`);
203-
}
204-
205-
// Send assistant messages immediately as they arrive
206-
if (jsonData.type === 'assistant' && jsonData.message?.content) {
207-
const content = Array.isArray(jsonData.message.content)
208-
? jsonData.message.content.map(block => {
209-
if (typeof block === 'string') return block;
210-
if (block.text) return block.text;
211-
// Skip non-text blocks (like tool_use blocks)
212-
return '';
213-
}).join('')
214-
: jsonData.message.content;
215-
216-
if (content) {
217-
responseContent += content;
218-
219-
// Extract thread title if present (but don't show to user)
220-
const titleMatch = content.match(/\[THREAD_TITLE:\s*([^\]]+)\]/);
221-
if (titleMatch && !threadTitle) {
222-
threadTitle = titleMatch[1].trim();
223-
console.log(`Extracted thread title: ${threadTitle}`);
224-
}
225-
226-
// Remove thread title from content before sending to user
227-
const userContent = content.replace(/\[THREAD_TITLE:\s*[^\]]+\]/g, '').trim();
228-
229-
// Send each chunk as a new message instead of editing
204+
jsonBuffer += text;
205+
206+
// Try to extract complete JSON objects from buffer
207+
let startIndex = 0;
208+
let braceCount = 0;
209+
let inString = false;
210+
let escapeNext = false;
211+
212+
for (let i = 0; i < jsonBuffer.length; i++) {
213+
const char = jsonBuffer[i];
214+
215+
if (escapeNext) {
216+
escapeNext = false;
217+
continue;
218+
}
219+
220+
if (char === '\\' && inString) {
221+
escapeNext = true;
222+
continue;
223+
}
224+
225+
if (char === '"') {
226+
inString = !inString;
227+
continue;
228+
}
229+
230+
if (!inString) {
231+
if (char === '{') {
232+
braceCount++;
233+
} else if (char === '}') {
234+
braceCount--;
235+
236+
// Complete JSON object found
237+
if (braceCount === 0) {
238+
const jsonStr = jsonBuffer.substring(startIndex, i + 1);
230239
try {
231-
if (userContent) {
232-
await channel.send(userContent);
233-
lastMessageRef = true; // Just track that we've sent something
240+
const jsonData = JSON.parse(jsonStr);
241+
242+
// Extract session ID from metadata
243+
if (jsonData.type === 'metadata' && jsonData.session_id) {
244+
extractedSessionId = jsonData.session_id;
245+
console.log(`Captured session ID: ${extractedSessionId}`);
234246
}
235-
} catch (discordError) {
236-
console.error("Discord update error:", discordError);
237-
}
238-
}
239-
}
240247

241-
// Handle final result
242-
if (jsonData.type === 'result' && jsonData.subtype === 'success') {
243-
if (jsonData.result) {
244-
responseContent = jsonData.result;
248+
// Send assistant messages immediately as they arrive
249+
if (jsonData.type === 'assistant' && jsonData.message?.content) {
250+
const content = Array.isArray(jsonData.message.content)
251+
? jsonData.message.content.map(block => {
252+
if (typeof block === 'string') return block;
253+
if (block.text) return block.text;
254+
// Skip non-text blocks (like tool_use blocks)
255+
return '';
256+
}).join('')
257+
: jsonData.message.content;
258+
259+
if (content) {
260+
responseContent += content;
261+
262+
// Extract thread title if present (but don't show to user)
263+
const titleMatch = content.match(/\[THREAD_TITLE:\s*([^\]]+)\]/);
264+
if (titleMatch && !threadTitle) {
265+
threadTitle = titleMatch[1].trim();
266+
console.log(`Extracted thread title: ${threadTitle}`);
267+
}
268+
269+
// Remove thread title from content before sending to user
270+
const userContent = content.replace(/\[THREAD_TITLE:\s*[^\]]+\]/g, '').trim();
271+
272+
// Send each chunk as a new message instead of editing
273+
try {
274+
if (userContent) {
275+
await channel.send(userContent);
276+
lastMessageRef = true; // Just track that we've sent something
277+
}
278+
} catch (discordError) {
279+
console.error("Discord update error:", discordError);
280+
}
281+
}
282+
}
245283

246-
// Extract thread title from final result if not already found
247-
const titleMatch = responseContent.match(/\[THREAD_TITLE:\s*([^\]]+)\]/);
248-
if (titleMatch && !threadTitle) {
249-
threadTitle = titleMatch[1].trim();
250-
console.log(`Extracted thread title from result: ${threadTitle}`);
251-
}
284+
// Handle final result
285+
if (jsonData.type === 'result' && jsonData.subtype === 'success') {
286+
if (jsonData.result) {
287+
responseContent = jsonData.result;
288+
289+
// Extract thread title from final result if not already found
290+
const titleMatch = responseContent.match(/\[THREAD_TITLE:\s*([^\]]+)\]/);
291+
if (titleMatch && !threadTitle) {
292+
threadTitle = titleMatch[1].trim();
293+
console.log(`Extracted thread title from result: ${threadTitle}`);
294+
}
295+
296+
// Only send final result if we haven't sent streaming messages
297+
try {
298+
if (!lastMessageRef) {
299+
// Remove thread title from final response before sending to user
300+
const userResponse = responseContent.replace(/\[THREAD_TITLE:\s*[^\]]+\]/g, '').trim();
301+
if (userResponse) {
302+
await channel.send(userResponse);
303+
lastMessageRef = true;
304+
}
305+
}
306+
// If we were streaming, the final result is already incorporated
307+
} catch (discordError) {
308+
console.error("Discord final update error:", discordError);
309+
}
310+
}
252311

253-
// Only send final result if we haven't sent streaming messages
254-
try {
255-
if (!lastMessageRef) {
256-
// Remove thread title from final response before sending to user
257-
const userResponse = responseContent.replace(/\[THREAD_TITLE:\s*[^\]]+\]/g, '').trim();
258-
if (userResponse) {
259-
await channel.send(userResponse);
260-
lastMessageRef = true;
312+
// Also capture session ID from result if available
313+
if (jsonData.session_id && !extractedSessionId) {
314+
extractedSessionId = jsonData.session_id;
315+
console.log(`Captured session ID from result: ${extractedSessionId}`);
261316
}
262317
}
263-
// If we were streaming, the final result is already incorporated
264-
} catch (discordError) {
265-
console.error("Discord final update error:", discordError);
266-
}
267-
}
268318

269-
// Also capture session ID from result if available
270-
if (jsonData.session_id && !extractedSessionId) {
271-
extractedSessionId = jsonData.session_id;
272-
console.log(`Captured session ID from result: ${extractedSessionId}`);
319+
} catch (parseError) {
320+
console.log("Failed to parse JSON object:", parseError.message);
321+
}
322+
323+
// Move to next potential JSON object
324+
startIndex = i + 1;
325+
braceCount = 0;
273326
}
274327
}
275-
276-
} catch (parseError) {
277-
// Skip invalid JSON lines
278-
console.log("Skipping non-JSON line:", line.substring(0, 100));
279328
}
280329
}
330+
331+
// Remove processed JSON objects from buffer
332+
jsonBuffer = jsonBuffer.substring(startIndex);
281333
}
282334
} catch (streamError) {
283335
console.error("Error processing stdout stream:", streamError);

0 commit comments

Comments
 (0)