agentgui 1.0.837 → 1.0.839
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -0
- package/lib/message-queue.js +62 -0
- package/lib/process-message-rate-limit.js +18 -0
- package/lib/process-message.js +126 -0
- package/lib/routes-agent-actions.js +117 -0
- package/lib/routes-auth-config.js +30 -0
- package/lib/routes-messages.js +139 -0
- package/lib/routes-runs.js +156 -0
- package/lib/routes-scripts.js +135 -0
- package/lib/routes-sessions.js +144 -0
- package/lib/stream-event-handler.js +115 -0
- package/package.json +1 -1
- package/scripts/patch-fsbrowse.js +1 -1
- package/server.js +45 -1706
package/server.js
CHANGED
|
@@ -27,6 +27,12 @@ import { register as registerThreadRoutes } from './lib/routes-threads.js';
|
|
|
27
27
|
import { register as registerDebugRoutes } from './lib/routes-debug.js';
|
|
28
28
|
import { register as registerConvRoutes } from './lib/routes-conversations.js';
|
|
29
29
|
import { register as registerAgentRoutes } from './lib/routes-agents.js';
|
|
30
|
+
import { register as registerMessagesRoutes } from './lib/routes-messages.js';
|
|
31
|
+
import { register as registerSessionsRoutes } from './lib/routes-sessions.js';
|
|
32
|
+
import { register as registerRunsRoutes } from './lib/routes-runs.js';
|
|
33
|
+
import { register as registerScriptsRoutes } from './lib/routes-scripts.js';
|
|
34
|
+
import { register as registerAgentActionsRoutes } from './lib/routes-agent-actions.js';
|
|
35
|
+
import { register as registerAuthConfigRoutes } from './lib/routes-auth-config.js';
|
|
30
36
|
import { startCodexOAuth, exchangeCodexOAuthCode, handleCodexOAuthCallback, getCodexOAuthStatus, getCodexOAuthState, CODEX_HOME, CODEX_AUTH_FILE } from './lib/oauth-codex.js';
|
|
31
37
|
import { WSOptimizer } from './lib/ws-optimizer.js';
|
|
32
38
|
import { WsRouter } from './lib/ws-protocol.js';
|
|
@@ -53,6 +59,10 @@ import CheckpointManager from './lib/checkpoint-manager.js';
|
|
|
53
59
|
import { JsonlWatcher } from './lib/jsonl-watcher.js';
|
|
54
60
|
import { createBroadcast } from './lib/broadcast.js';
|
|
55
61
|
import { createRecovery } from './lib/recovery.js';
|
|
62
|
+
import { parseRateLimitResetTime } from './lib/process-message-rate-limit.js';
|
|
63
|
+
import { createEventHandler } from './lib/stream-event-handler.js';
|
|
64
|
+
import { createMessageQueue } from './lib/message-queue.js';
|
|
65
|
+
import { createProcessMessage } from './lib/process-message.js';
|
|
56
66
|
|
|
57
67
|
|
|
58
68
|
process.on('uncaughtException', (err, origin) => {
|
|
@@ -546,1049 +556,29 @@ const server = http.createServer(async (req, res) => {
|
|
|
546
556
|
const convHandler = _convRoutes._match(req.method, pathOnly);
|
|
547
557
|
if (convHandler) { await convHandler(req, res); return; }
|
|
548
558
|
|
|
549
|
-
const
|
|
550
|
-
if (
|
|
551
|
-
if (req.method === 'GET') {
|
|
552
|
-
const url = new URL(req.url, 'http://localhost');
|
|
553
|
-
const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 500);
|
|
554
|
-
const offset = Math.max(parseInt(url.searchParams.get('offset') || '0'), 0);
|
|
555
|
-
const result = queries.getPaginatedMessages(messagesMatch[1], limit, offset);
|
|
556
|
-
sendJSON(req, res, 200, result);
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
if (req.method === 'POST') {
|
|
561
|
-
const conversationId = messagesMatch[1];
|
|
562
|
-
const conv = queries.getConversation(conversationId);
|
|
563
|
-
if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
|
|
564
|
-
const body = await parseBody(req);
|
|
565
|
-
const agentId = body.agentId || conv.agentType || conv.agentId || 'claude-code';
|
|
566
|
-
const model = body.model || conv.model || null;
|
|
567
|
-
const subAgent = body.subAgent || conv.subAgent || null;
|
|
568
|
-
const idempotencyKey = body.idempotencyKey || null;
|
|
569
|
-
const message = queries.createMessage(conversationId, 'user', body.content, idempotencyKey);
|
|
570
|
-
queries.createEvent('message.created', { role: 'user', messageId: message.id }, conversationId);
|
|
571
|
-
broadcastSync({ type: 'message_created', conversationId, message, timestamp: Date.now() });
|
|
572
|
-
|
|
573
|
-
if (activeExecutions.has(conversationId)) {
|
|
574
|
-
if (!messageQueues.has(conversationId)) messageQueues.set(conversationId, []);
|
|
575
|
-
messageQueues.get(conversationId).push({ content: body.content, agentId, model, messageId: message.id, subAgent });
|
|
576
|
-
const queueLength = messageQueues.get(conversationId).length;
|
|
577
|
-
broadcastSync({ type: 'queue_status', conversationId, queueLength, messageId: message.id, timestamp: Date.now() });
|
|
578
|
-
sendJSON(req, res, 200, { message, queued: true, queuePosition: queueLength, idempotencyKey });
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
const session = queries.createSession(conversationId);
|
|
583
|
-
queries.createEvent('session.created', { messageId: message.id, sessionId: session.id }, conversationId, session.id);
|
|
584
|
-
|
|
585
|
-
activeExecutions.set(conversationId, { pid: null, startTime: Date.now(), sessionId: session.id, lastActivity: Date.now() });
|
|
586
|
-
queries.setIsStreaming(conversationId, true);
|
|
587
|
-
|
|
588
|
-
broadcastSync({
|
|
589
|
-
type: 'streaming_start',
|
|
590
|
-
sessionId: session.id,
|
|
591
|
-
conversationId,
|
|
592
|
-
messageId: message.id,
|
|
593
|
-
agentId,
|
|
594
|
-
timestamp: Date.now()
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
sendJSON(req, res, 201, { message, session, idempotencyKey });
|
|
598
|
-
|
|
599
|
-
processMessageWithStreaming(conversationId, message.id, session.id, body.content, agentId, model, subAgent)
|
|
600
|
-
.catch(err => {
|
|
601
|
-
console.error(`[messages] Uncaught error for conv ${conversationId}:`, err.message);
|
|
602
|
-
debugLog(`[messages] Uncaught error: ${err.message}`);
|
|
603
|
-
logError('processMessageWithStreaming', err, { convId: conversationId });
|
|
604
|
-
});
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
const streamMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/stream$/);
|
|
610
|
-
if (streamMatch && req.method === 'POST') {
|
|
611
|
-
const conversationId = streamMatch[1];
|
|
612
|
-
const body = await parseBody(req);
|
|
613
|
-
const conv = queries.getConversation(conversationId);
|
|
614
|
-
if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
|
|
615
|
-
|
|
616
|
-
const prompt = body.content || body.message || '';
|
|
617
|
-
const agentId = body.agentId || conv.agentType || conv.agentId || 'claude-code';
|
|
618
|
-
const model = body.model || conv.model || null;
|
|
619
|
-
const subAgent = body.subAgent || conv.subAgent || null;
|
|
620
|
-
|
|
621
|
-
const userMessage = queries.createMessage(conversationId, 'user', prompt);
|
|
622
|
-
queries.createEvent('message.created', { role: 'user', messageId: userMessage.id }, conversationId);
|
|
623
|
-
|
|
624
|
-
broadcastSync({ type: 'message_created', conversationId, message: userMessage, timestamp: Date.now() });
|
|
625
|
-
|
|
626
|
-
if (activeExecutions.has(conversationId)) {
|
|
627
|
-
debugLog(`[stream] Conversation ${conversationId} is busy, queuing message`);
|
|
628
|
-
if (!messageQueues.has(conversationId)) messageQueues.set(conversationId, []);
|
|
629
|
-
messageQueues.get(conversationId).push({ content: prompt, agentId, model, messageId: userMessage.id, subAgent });
|
|
630
|
-
|
|
631
|
-
const queueLength = messageQueues.get(conversationId).length;
|
|
632
|
-
broadcastSync({ type: 'queue_status', conversationId, queueLength, messageId: userMessage.id, timestamp: Date.now() });
|
|
633
|
-
|
|
634
|
-
sendJSON(req, res, 200, { message: userMessage, queued: true, queuePosition: queueLength });
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const session = queries.createSession(conversationId);
|
|
639
|
-
queries.createEvent('session.created', { messageId: userMessage.id, sessionId: session.id }, conversationId, session.id);
|
|
640
|
-
|
|
641
|
-
activeExecutions.set(conversationId, { pid: null, startTime: Date.now(), sessionId: session.id, lastActivity: Date.now() });
|
|
642
|
-
queries.setIsStreaming(conversationId, true);
|
|
643
|
-
|
|
644
|
-
broadcastSync({
|
|
645
|
-
type: 'streaming_start',
|
|
646
|
-
sessionId: session.id,
|
|
647
|
-
conversationId,
|
|
648
|
-
messageId: userMessage.id,
|
|
649
|
-
agentId,
|
|
650
|
-
timestamp: Date.now()
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
sendJSON(req, res, 200, { message: userMessage, session, streamId: session.id });
|
|
654
|
-
|
|
655
|
-
processMessageWithStreaming(conversationId, userMessage.id, session.id, prompt, agentId, model, subAgent)
|
|
656
|
-
.catch(err => debugLog(`[stream] Uncaught error: ${err.stack || err.message}`));
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
const queueMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/queue$/);
|
|
661
|
-
if (queueMatch && req.method === 'GET') {
|
|
662
|
-
const conversationId = queueMatch[1];
|
|
663
|
-
const conv = queries.getConversation(conversationId);
|
|
664
|
-
if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
|
|
665
|
-
const queue = messageQueues.get(conversationId) || [];
|
|
666
|
-
sendJSON(req, res, 200, { queue });
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
const queueItemMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/queue\/([^/]+)$/);
|
|
671
|
-
if (queueItemMatch && req.method === 'DELETE') {
|
|
672
|
-
const conversationId = queueItemMatch[1];
|
|
673
|
-
const messageId = queueItemMatch[2];
|
|
674
|
-
const queue = messageQueues.get(conversationId);
|
|
675
|
-
if (!queue) { sendJSON(req, res, 404, { error: 'Queue not found' }); return; }
|
|
676
|
-
const index = queue.findIndex(q => q.messageId === messageId);
|
|
677
|
-
if (index === -1) { sendJSON(req, res, 404, { error: 'Queued message not found' }); return; }
|
|
678
|
-
queue.splice(index, 1);
|
|
679
|
-
if (queue.length === 0) messageQueues.delete(conversationId);
|
|
680
|
-
broadcastSync({ type: 'queue_status', conversationId, queueLength: queue?.length || 0, timestamp: Date.now() });
|
|
681
|
-
sendJSON(req, res, 200, { deleted: true });
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
if (queueItemMatch && req.method === 'PATCH') {
|
|
686
|
-
const conversationId = queueItemMatch[1];
|
|
687
|
-
const messageId = queueItemMatch[2];
|
|
688
|
-
const body = await parseBody(req);
|
|
689
|
-
const queue = messageQueues.get(conversationId);
|
|
690
|
-
if (!queue) { sendJSON(req, res, 404, { error: 'Queue not found' }); return; }
|
|
691
|
-
const item = queue.find(q => q.messageId === messageId);
|
|
692
|
-
if (!item) { sendJSON(req, res, 404, { error: 'Queued message not found' }); return; }
|
|
693
|
-
if (body.content !== undefined) item.content = body.content;
|
|
694
|
-
if (body.agentId !== undefined) item.agentId = body.agentId;
|
|
695
|
-
broadcastSync({ type: 'queue_updated', conversationId, messageId, content: item.content, agentId: item.agentId, timestamp: Date.now() });
|
|
696
|
-
sendJSON(req, res, 200, { updated: true, item });
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const messageMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/messages\/([^/]+)$/);
|
|
701
|
-
if (messageMatch && req.method === 'GET') {
|
|
702
|
-
const msg = queries.getMessage(messageMatch[2]);
|
|
703
|
-
if (!msg || msg.conversationId !== messageMatch[1]) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
704
|
-
sendJSON(req, res, 200, { message: msg });
|
|
705
|
-
return;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
const sessionMatch = pathOnly.match(/^\/api\/sessions\/([^/]+)$/);
|
|
709
|
-
if (sessionMatch && req.method === 'GET') {
|
|
710
|
-
const sess = queries.getSession(sessionMatch[1]);
|
|
711
|
-
if (!sess) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
712
|
-
const events = queries.getSessionEvents(sessionMatch[1]);
|
|
713
|
-
sendJSON(req, res, 200, { session: sess, events });
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const fullLoadMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/full$/);
|
|
718
|
-
if (fullLoadMatch && req.method === 'GET') {
|
|
719
|
-
const conversationId = fullLoadMatch[1];
|
|
720
|
-
const conv = queries.getConversation(conversationId);
|
|
721
|
-
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
722
|
-
const latestSession = queries.getLatestSession(conversationId);
|
|
723
|
-
const isActivelyStreaming = activeExecutions.has(conversationId);
|
|
724
|
-
|
|
725
|
-
const url = new URL(req.url, 'http://localhost');
|
|
726
|
-
const chunkLimit = Math.min(parseInt(url.searchParams.get('chunkLimit') || '500'), 5000);
|
|
727
|
-
const allChunks = url.searchParams.get('allChunks') === '1';
|
|
728
|
-
|
|
729
|
-
const totalChunks = queries.getConversationChunkCount(conversationId);
|
|
730
|
-
let chunks;
|
|
731
|
-
if (allChunks || totalChunks <= chunkLimit) {
|
|
732
|
-
chunks = queries.getConversationChunks(conversationId);
|
|
733
|
-
} else {
|
|
734
|
-
chunks = queries.getRecentConversationChunks(conversationId, chunkLimit);
|
|
735
|
-
}
|
|
736
|
-
const msgResult = queries.getPaginatedMessages(conversationId, 100, 0);
|
|
737
|
-
const rateLimitInfo = rateLimitState.get(conversationId) || null;
|
|
738
|
-
sendJSON(req, res, 200, {
|
|
739
|
-
conversation: conv,
|
|
740
|
-
isActivelyStreaming,
|
|
741
|
-
latestSession,
|
|
742
|
-
chunks,
|
|
743
|
-
totalChunks,
|
|
744
|
-
messages: msgResult.messages,
|
|
745
|
-
rateLimitInfo
|
|
746
|
-
});
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
const conversationChunksMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/chunks$/);
|
|
751
|
-
if (conversationChunksMatch && req.method === 'GET') {
|
|
752
|
-
const conversationId = conversationChunksMatch[1];
|
|
753
|
-
const conv = queries.getConversation(conversationId);
|
|
754
|
-
if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
|
|
755
|
-
|
|
756
|
-
const url = new URL(req.url, 'http://localhost');
|
|
757
|
-
const since = parseInt(url.searchParams.get('since') || '0');
|
|
758
|
-
const all = url.searchParams.get('all') === 'true';
|
|
759
|
-
const totalChunks = queries.getConversationChunkCount(conversationId);
|
|
760
|
-
let chunks;
|
|
761
|
-
if (since > 0) {
|
|
762
|
-
chunks = queries.getConversationChunksSince(conversationId, since);
|
|
763
|
-
} else if (all) {
|
|
764
|
-
chunks = queries.getConversationChunks(conversationId);
|
|
765
|
-
} else {
|
|
766
|
-
chunks = queries.getRecentConversationChunks(conversationId, 500);
|
|
767
|
-
}
|
|
768
|
-
debugLog(`[chunks] Conv ${conversationId}: ${chunks.length} chunks (total: ${totalChunks})`);
|
|
769
|
-
sendJSON(req, res, 200, { ok: true, chunks, totalChunks });
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
const sessionChunksMatch = pathOnly.match(/^\/api\/sessions\/([^/]+)\/chunks$/);
|
|
774
|
-
if (sessionChunksMatch && req.method === 'GET') {
|
|
775
|
-
const sessionId = sessionChunksMatch[1];
|
|
776
|
-
const sess = queries.getSession(sessionId);
|
|
777
|
-
if (!sess) { sendJSON(req, res, 404, { error: 'Session not found' }); return; }
|
|
778
|
-
|
|
779
|
-
const url = new URL(req.url, 'http://localhost');
|
|
780
|
-
const sinceSeq = parseInt(url.searchParams.get('sinceSeq') || '-1');
|
|
781
|
-
const since = parseInt(url.searchParams.get('since') || '0');
|
|
782
|
-
|
|
783
|
-
let chunks;
|
|
784
|
-
if (sinceSeq >= 0) {
|
|
785
|
-
chunks = queries.getChunksSinceSeq(sessionId, sinceSeq);
|
|
786
|
-
} else {
|
|
787
|
-
chunks = queries.getChunksSince(sessionId, since);
|
|
788
|
-
}
|
|
789
|
-
sendJSON(req, res, 200, { ok: true, chunks });
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
if (pathOnly.match(/^\/api\/conversations\/([^/]+)\/sessions\/latest$/) && req.method === 'GET') {
|
|
794
|
-
const convId = pathOnly.match(/^\/api\/conversations\/([^/]+)\/sessions\/latest$/)[1];
|
|
795
|
-
const latestSession = queries.getLatestSession(convId);
|
|
796
|
-
if (!latestSession) {
|
|
797
|
-
sendJSON(req, res, 200, { session: null });
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
const events = queries.getSessionEvents(latestSession.id);
|
|
801
|
-
sendJSON(req, res, 200, { session: latestSession, events });
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
const executionMatch = pathOnly.match(/^\/api\/sessions\/([^/]+)\/execution$/);
|
|
806
|
-
if (executionMatch && req.method === 'GET') {
|
|
807
|
-
const sessionId = executionMatch[1];
|
|
808
|
-
const url = new URL(req.url, 'http://localhost');
|
|
809
|
-
const limit = Math.min(parseInt(url.searchParams.get('limit') || '1000'), 5000);
|
|
810
|
-
const offset = Math.max(parseInt(url.searchParams.get('offset') || '0'), 0);
|
|
811
|
-
const filterType = url.searchParams.get('filterType');
|
|
812
|
-
|
|
813
|
-
try {
|
|
814
|
-
const session = queries.getSession(sessionId);
|
|
815
|
-
const allChunks = session ? (queries.getChunksSince(sessionId, 0) || []) : [];
|
|
816
|
-
const filtered = filterType ? allChunks.filter(e => e.type === filterType) : allChunks;
|
|
817
|
-
const executionData = {
|
|
818
|
-
sessionId,
|
|
819
|
-
events: filtered.slice(offset, offset + limit),
|
|
820
|
-
total: filtered.length,
|
|
821
|
-
limit,
|
|
822
|
-
offset,
|
|
823
|
-
hasMore: offset + limit < filtered.length,
|
|
824
|
-
metadata: {
|
|
825
|
-
status: session?.status || 'unknown',
|
|
826
|
-
startTime: session?.created_at || null,
|
|
827
|
-
duration: session?.completed_at && session?.created_at ? session.completed_at - session.created_at : 0,
|
|
828
|
-
eventCount: filtered.length
|
|
829
|
-
}
|
|
830
|
-
};
|
|
831
|
-
|
|
832
|
-
sendJSON(req, res, 200, executionData);
|
|
833
|
-
} catch (err) {
|
|
834
|
-
sendJSON(req, res, 400, { error: err.message });
|
|
835
|
-
}
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
const runsMatch = pathOnly.match(/^\/api\/runs$/);
|
|
840
|
-
if (runsMatch && req.method === 'POST') {
|
|
841
|
-
let body = '';
|
|
842
|
-
for await (const chunk of req) { body += chunk; }
|
|
843
|
-
let parsed = {};
|
|
844
|
-
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
845
|
-
|
|
846
|
-
const { input, agentId, webhook } = parsed;
|
|
847
|
-
if (!input) {
|
|
848
|
-
sendJSON(req, res, 400, { error: 'Missing input in request body' });
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
const resolvedAgentId = agentId || 'claude-code';
|
|
853
|
-
const resolvedModel = parsed.model || null;
|
|
854
|
-
const cwd = parsed.workingDirectory || STARTUP_CWD;
|
|
855
|
-
|
|
856
|
-
const thread = queries.createConversation(resolvedAgentId, 'Stateless Run', cwd);
|
|
857
|
-
const session = queries.createSession(thread.id, resolvedAgentId, 'pending');
|
|
858
|
-
const message = queries.createMessage(thread.id, 'user', typeof input === 'string' ? input : JSON.stringify(input));
|
|
859
|
-
|
|
860
|
-
processMessageWithStreaming(thread.id, message.id, session.id, typeof input === 'string' ? input : JSON.stringify(input), resolvedAgentId, resolvedModel);
|
|
861
|
-
|
|
862
|
-
sendJSON(req, res, 200, {
|
|
863
|
-
id: session.id,
|
|
864
|
-
status: 'pending',
|
|
865
|
-
started_at: session.started_at,
|
|
866
|
-
agentId: resolvedAgentId
|
|
867
|
-
});
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
const runsSearchMatch = pathOnly.match(/^\/api\/runs\/search$/);
|
|
872
|
-
if (runsSearchMatch && req.method === 'POST') {
|
|
873
|
-
const sessions = queries.getAllSessions();
|
|
874
|
-
const runs = sessions.slice(0, 50).map(s => ({
|
|
875
|
-
id: s.id,
|
|
876
|
-
status: s.status,
|
|
877
|
-
started_at: s.started_at,
|
|
878
|
-
completed_at: s.completed_at,
|
|
879
|
-
agentId: s.agentId,
|
|
880
|
-
input: null,
|
|
881
|
-
output: null
|
|
882
|
-
})).reverse();
|
|
883
|
-
sendJSON(req, res, 200, runs);
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// POST /runs/stream - SSE removed, use WebSocket
|
|
888
|
-
if (pathOnly === '/api/runs/stream' && req.method === 'POST') {
|
|
889
|
-
res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' }));
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
const scriptsMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/scripts$/);
|
|
894
|
-
if (scriptsMatch && req.method === 'GET') {
|
|
895
|
-
const conv = queries.getConversation(scriptsMatch[1]);
|
|
896
|
-
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
897
|
-
const wd = conv.workingDirectory || STARTUP_CWD;
|
|
898
|
-
let hasStart = false, hasDev = false;
|
|
899
|
-
try {
|
|
900
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(wd, 'package.json'), 'utf-8'));
|
|
901
|
-
const scripts = pkg.scripts || {};
|
|
902
|
-
hasStart = !!scripts.start;
|
|
903
|
-
hasDev = !!scripts.dev;
|
|
904
|
-
} catch {}
|
|
905
|
-
const running = activeScripts.has(scriptsMatch[1]);
|
|
906
|
-
const runningScript = running ? activeScripts.get(scriptsMatch[1]).script : null;
|
|
907
|
-
sendJSON(req, res, 200, { hasStart, hasDev, running, runningScript });
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
const runScriptMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/run-script$/);
|
|
912
|
-
if (runScriptMatch && req.method === 'POST') {
|
|
913
|
-
const conversationId = runScriptMatch[1];
|
|
914
|
-
const conv = queries.getConversation(conversationId);
|
|
915
|
-
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
916
|
-
if (activeScripts.has(conversationId)) { sendJSON(req, res, 409, { error: 'Script already running' }); return; }
|
|
917
|
-
const body = await parseBody(req);
|
|
918
|
-
const script = body.script;
|
|
919
|
-
if (script !== 'start' && script !== 'dev') { sendJSON(req, res, 400, { error: 'Invalid script' }); return; }
|
|
920
|
-
const wd = conv.workingDirectory || STARTUP_CWD;
|
|
921
|
-
try {
|
|
922
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(wd, 'package.json'), 'utf-8'));
|
|
923
|
-
if (!pkg.scripts || !pkg.scripts[script]) { sendJSON(req, res, 400, { error: `Script "${script}" not found` }); return; }
|
|
924
|
-
} catch { sendJSON(req, res, 400, { error: 'No package.json' }); return; }
|
|
925
|
-
|
|
926
|
-
const childEnv = { ...process.env, FORCE_COLOR: '1' };
|
|
927
|
-
delete childEnv.PORT;
|
|
928
|
-
delete childEnv.BASE_URL;
|
|
929
|
-
delete childEnv.HOT_RELOAD;
|
|
930
|
-
const isWindows = os.platform() === 'win32';
|
|
931
|
-
const child = spawn('npm', ['run', script], { cwd: wd, stdio: ['ignore', 'pipe', 'pipe'], detached: true, env: childEnv, shell: isWindows });
|
|
932
|
-
activeScripts.set(conversationId, { process: child, script, startTime: Date.now() });
|
|
933
|
-
broadcastSync({ type: 'script_started', conversationId, script, timestamp: Date.now() });
|
|
934
|
-
|
|
935
|
-
const onData = (stream) => (chunk) => {
|
|
936
|
-
broadcastSync({ type: 'script_output', conversationId, data: chunk.toString(), stream, timestamp: Date.now() });
|
|
937
|
-
};
|
|
938
|
-
child.stdout.on('data', onData('stdout'));
|
|
939
|
-
child.stderr.on('data', onData('stderr'));
|
|
940
|
-
child.stdout.on('error', () => {});
|
|
941
|
-
child.stderr.on('error', () => {});
|
|
942
|
-
child.on('error', (err) => {
|
|
943
|
-
activeScripts.delete(conversationId);
|
|
944
|
-
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: err.message, timestamp: Date.now() });
|
|
945
|
-
});
|
|
946
|
-
child.on('close', (code) => {
|
|
947
|
-
activeScripts.delete(conversationId);
|
|
948
|
-
broadcastSync({ type: 'script_stopped', conversationId, code: code || 0, timestamp: Date.now() });
|
|
949
|
-
});
|
|
950
|
-
sendJSON(req, res, 200, { ok: true, script, pid: child.pid });
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
const stopScriptMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/stop-script$/);
|
|
955
|
-
if (stopScriptMatch && req.method === 'POST') {
|
|
956
|
-
const conversationId = stopScriptMatch[1];
|
|
957
|
-
const entry = activeScripts.get(conversationId);
|
|
958
|
-
if (!entry) { sendJSON(req, res, 404, { error: 'No running script' }); return; }
|
|
959
|
-
try { process.kill(-entry.process.pid, 'SIGTERM'); } catch { try { entry.process.kill('SIGTERM'); } catch {} }
|
|
960
|
-
sendJSON(req, res, 200, { ok: true });
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
const scriptStatusMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/script-status$/);
|
|
965
|
-
if (scriptStatusMatch && req.method === 'GET') {
|
|
966
|
-
const entry = activeScripts.get(scriptStatusMatch[1]);
|
|
967
|
-
sendJSON(req, res, 200, { running: !!entry, script: entry?.script || null });
|
|
968
|
-
return;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
const cancelRunMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/cancel$/);
|
|
972
|
-
if (cancelRunMatch && req.method === 'POST') {
|
|
973
|
-
const conversationId = cancelRunMatch[1];
|
|
974
|
-
const entry = activeExecutions.get(conversationId);
|
|
975
|
-
|
|
976
|
-
if (!entry) {
|
|
977
|
-
sendJSON(req, res, 404, { error: 'No active execution to cancel' });
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
const { pid, sessionId } = entry;
|
|
982
|
-
|
|
983
|
-
if (pid) {
|
|
984
|
-
try {
|
|
985
|
-
process.kill(-pid, 'SIGKILL');
|
|
986
|
-
} catch {
|
|
987
|
-
try {
|
|
988
|
-
process.kill(pid, 'SIGKILL');
|
|
989
|
-
} catch (e) {}
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
if (sessionId) {
|
|
994
|
-
queries.updateSession(sessionId, {
|
|
995
|
-
status: 'interrupted',
|
|
996
|
-
completed_at: Date.now()
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
queries.setIsStreaming(conversationId, false);
|
|
1001
|
-
activeExecutions.delete(conversationId);
|
|
1002
|
-
|
|
1003
|
-
broadcastSync({
|
|
1004
|
-
type: 'streaming_complete',
|
|
1005
|
-
sessionId,
|
|
1006
|
-
conversationId,
|
|
1007
|
-
interrupted: true,
|
|
1008
|
-
timestamp: Date.now()
|
|
1009
|
-
});
|
|
559
|
+
const messagesHandler = _messagesRoutes._match(req.method, pathOnly);
|
|
560
|
+
if (messagesHandler) { await messagesHandler(req, res); return; }
|
|
1010
561
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
}
|
|
562
|
+
const sessionsHandler = _sessionsRoutes._match(req.method, pathOnly);
|
|
563
|
+
if (sessionsHandler) { await sessionsHandler(req, res); return; }
|
|
1014
564
|
|
|
1015
|
-
const
|
|
1016
|
-
if (
|
|
1017
|
-
const conversationId = resumeRunMatch[1];
|
|
1018
|
-
const conv = queries.getConversation(conversationId);
|
|
1019
|
-
|
|
1020
|
-
if (!conv) {
|
|
1021
|
-
sendJSON(req, res, 404, { error: 'Conversation not found' });
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
565
|
+
const scriptsHandler = _scriptsRoutes._match(req.method, pathOnly);
|
|
566
|
+
if (scriptsHandler) { await scriptsHandler(req, res); return; }
|
|
1024
567
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
sendJSON(req, res, 409, { error: 'Conversation already has an active execution' });
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
let body = '';
|
|
1032
|
-
for await (const chunk of req) { body += chunk; }
|
|
1033
|
-
let parsed = {};
|
|
1034
|
-
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
1035
|
-
|
|
1036
|
-
const { content, agentId } = parsed;
|
|
1037
|
-
if (!content) {
|
|
1038
|
-
sendJSON(req, res, 400, { error: 'Missing content in request body' });
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
const resolvedAgentId = agentId || conv.agentId || 'claude-code';
|
|
1043
|
-
const resolvedModel = parsed.model || conv.model || null;
|
|
1044
|
-
const cwd = conv.workingDirectory || STARTUP_CWD;
|
|
1045
|
-
|
|
1046
|
-
const session = queries.createSession(conversationId, resolvedAgentId, 'pending');
|
|
1047
|
-
|
|
1048
|
-
const message = queries.createMessage(conversationId, 'user', content);
|
|
1049
|
-
|
|
1050
|
-
processMessageWithStreaming(conversationId, message.id, session.id, content, resolvedAgentId, resolvedModel);
|
|
1051
|
-
|
|
1052
|
-
sendJSON(req, res, 200, {
|
|
1053
|
-
ok: true,
|
|
1054
|
-
conversationId,
|
|
1055
|
-
sessionId: session.id,
|
|
1056
|
-
messageId: message.id,
|
|
1057
|
-
resumed: true
|
|
1058
|
-
});
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
const injectMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/inject$/);
|
|
1063
|
-
if (injectMatch && req.method === 'POST') {
|
|
1064
|
-
const conversationId = injectMatch[1];
|
|
1065
|
-
const conv = queries.getConversation(conversationId);
|
|
1066
|
-
|
|
1067
|
-
if (!conv) {
|
|
1068
|
-
sendJSON(req, res, 404, { error: 'Conversation not found' });
|
|
1069
|
-
return;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
let body = '';
|
|
1073
|
-
for await (const chunk of req) { body += chunk; }
|
|
1074
|
-
let parsed = {};
|
|
1075
|
-
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
1076
|
-
|
|
1077
|
-
const { content, eager } = parsed;
|
|
1078
|
-
if (!content) {
|
|
1079
|
-
sendJSON(req, res, 400, { error: 'Missing content in request body' });
|
|
1080
|
-
return;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
const entry = activeExecutions.get(conversationId);
|
|
1084
|
-
|
|
1085
|
-
if (entry && eager) {
|
|
1086
|
-
sendJSON(req, res, 409, { error: 'Cannot eagerly inject while execution is running - message queued' });
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
const message = queries.createMessage(conversationId, 'user', '[INJECTED] ' + content);
|
|
1091
|
-
|
|
1092
|
-
if (!entry) {
|
|
1093
|
-
const resolvedAgentId = conv.agentId || 'claude-code';
|
|
1094
|
-
const resolvedModel = conv.model || null;
|
|
1095
|
-
const cwd = conv.workingDirectory || STARTUP_CWD;
|
|
1096
|
-
const session = queries.createSession(conversationId, resolvedAgentId, 'pending');
|
|
1097
|
-
processMessageWithStreaming(conversationId, message.id, session.id, message.content, resolvedAgentId, resolvedModel);
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
sendJSON(req, res, 200, { ok: true, injected: true, conversationId, messageId: message.id });
|
|
1101
|
-
return;
|
|
1102
|
-
}
|
|
568
|
+
const runsHandlerA = _runsRoutes._match(req.method, pathOnly);
|
|
569
|
+
if (runsHandlerA) { await runsHandlerA(req, res); return; }
|
|
1103
570
|
|
|
1104
571
|
const agentHandler = _agentRoutes._match(req.method, pathOnly);
|
|
1105
572
|
if (agentHandler) { await agentHandler(req, res); return; }
|
|
1106
573
|
|
|
1107
|
-
if (pathOnly === '/api/runs' && req.method === 'POST') {
|
|
1108
|
-
const body = await parseBody(req);
|
|
1109
|
-
const { agent_id, input, config, webhook_url } = body;
|
|
1110
|
-
if (!agent_id) {
|
|
1111
|
-
sendJSON(req, res, 422, { error: 'agent_id is required' });
|
|
1112
|
-
return;
|
|
1113
|
-
}
|
|
1114
|
-
const agent = discoveredAgents.find(a => a.id === agent_id);
|
|
1115
|
-
if (!agent) {
|
|
1116
|
-
sendJSON(req, res, 404, { error: 'Agent not found' });
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
const run = queries.createRun(agent_id, null, input, config, webhook_url);
|
|
1120
|
-
sendJSON(req, res, 201, run);
|
|
1121
|
-
return;
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
if (pathOnly === '/api/runs/search' && req.method === 'POST') {
|
|
1125
|
-
const body = await parseBody(req);
|
|
1126
|
-
const result = queries.searchRuns(body);
|
|
1127
|
-
sendJSON(req, res, 200, result);
|
|
1128
|
-
return;
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
if (pathOnly === '/api/runs/wait' && req.method === 'POST') {
|
|
1132
|
-
const body = await parseBody(req);
|
|
1133
|
-
const { agent_id, input, config } = body;
|
|
1134
|
-
if (!agent_id) {
|
|
1135
|
-
sendJSON(req, res, 422, { error: 'agent_id is required' });
|
|
1136
|
-
return;
|
|
1137
|
-
}
|
|
1138
|
-
const agent = discoveredAgents.find(a => a.id === agent_id);
|
|
1139
|
-
if (!agent) {
|
|
1140
|
-
sendJSON(req, res, 404, { error: 'Agent not found' });
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
const run = queries.createRun(agent_id, null, input, config);
|
|
1144
|
-
const statelessThreadId = queries.getRun(run.run_id)?.thread_id;
|
|
1145
|
-
if (statelessThreadId && input?.content) {
|
|
1146
|
-
try {
|
|
1147
|
-
await runClaudeWithStreaming(agent_id, statelessThreadId, input.content, config?.model || null);
|
|
1148
|
-
const finalRun = queries.getRun(run.run_id);
|
|
1149
|
-
sendJSON(req, res, 200, finalRun);
|
|
1150
|
-
} catch (err) {
|
|
1151
|
-
queries.updateRunStatus(run.run_id, 'error');
|
|
1152
|
-
sendJSON(req, res, 500, { error: err.message });
|
|
1153
|
-
}
|
|
1154
|
-
} else {
|
|
1155
|
-
sendJSON(req, res, 200, run);
|
|
1156
|
-
}
|
|
1157
|
-
return;
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
const oldRunByIdMatch1 = pathOnly.match(/^\/api\/runs\/([^/]+)$/);
|
|
1161
|
-
if (oldRunByIdMatch1) {
|
|
1162
|
-
const runId = oldRunByIdMatch1[1];
|
|
1163
|
-
|
|
1164
|
-
if (req.method === 'GET') {
|
|
1165
|
-
const run = queries.getRun(runId);
|
|
1166
|
-
if (!run) {
|
|
1167
|
-
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
sendJSON(req, res, 200, run);
|
|
1171
|
-
return;
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
if (req.method === 'POST') {
|
|
1175
|
-
const body = await parseBody(req);
|
|
1176
|
-
const run = queries.getRun(runId);
|
|
1177
|
-
if (!run) {
|
|
1178
|
-
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
1179
|
-
return;
|
|
1180
|
-
}
|
|
1181
|
-
if (run.status !== 'pending') {
|
|
1182
|
-
sendJSON(req, res, 409, { error: 'Run is not resumable' });
|
|
1183
|
-
return;
|
|
1184
|
-
}
|
|
1185
|
-
if (body.input?.content && run.thread_id) {
|
|
1186
|
-
runClaudeWithStreaming(run.agent_id, run.thread_id, body.input.content, null).catch(() => {});
|
|
1187
|
-
}
|
|
1188
|
-
sendJSON(req, res, 200, run);
|
|
1189
|
-
return;
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
if (req.method === 'DELETE') {
|
|
1193
|
-
try {
|
|
1194
|
-
queries.deleteRun(runId);
|
|
1195
|
-
res.writeHead(204);
|
|
1196
|
-
res.end();
|
|
1197
|
-
} catch (err) {
|
|
1198
|
-
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
1199
|
-
}
|
|
1200
|
-
return;
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
const runWaitMatch = pathOnly.match(/^\/api\/runs\/([^/]+)\/wait$/);
|
|
1205
|
-
if (runWaitMatch && req.method === 'GET') {
|
|
1206
|
-
const runId = runWaitMatch[1];
|
|
1207
|
-
const run = queries.getRun(runId);
|
|
1208
|
-
if (!run) {
|
|
1209
|
-
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
1210
|
-
return;
|
|
1211
|
-
}
|
|
1212
|
-
const startTime = Date.now();
|
|
1213
|
-
const pollInterval = setInterval(() => {
|
|
1214
|
-
const currentRun = queries.getRun(runId);
|
|
1215
|
-
const elapsed = Date.now() - startTime;
|
|
1216
|
-
const done = currentRun && ['success', 'error', 'cancelled'].includes(currentRun.status);
|
|
1217
|
-
if (done) {
|
|
1218
|
-
clearInterval(pollInterval);
|
|
1219
|
-
sendJSON(req, res, 200, currentRun);
|
|
1220
|
-
} else if (elapsed > 30000) {
|
|
1221
|
-
clearInterval(pollInterval);
|
|
1222
|
-
sendJSON(req, res, 408, { error: 'Run still pending after 30s', run_id: runId, status: currentRun?.status || run.status });
|
|
1223
|
-
}
|
|
1224
|
-
}, 500);
|
|
1225
|
-
req.on('close', () => clearInterval(pollInterval));
|
|
1226
|
-
return;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
const runStreamMatch = pathOnly.match(/^\/api\/runs\/([^/]+)\/stream$/);
|
|
1230
|
-
if (runStreamMatch && req.method === 'GET') {
|
|
1231
|
-
res.writeHead(410); res.end(JSON.stringify({ error: 'SSE removed, use WebSocket' }));
|
|
1232
|
-
return;
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
const oldRunCancelMatch1 = pathOnly.match(/^\/api\/runs\/([^/]+)\/cancel$/);
|
|
1236
|
-
if (oldRunCancelMatch1 && req.method === 'POST') {
|
|
1237
|
-
const runId = oldRunCancelMatch1[1];
|
|
1238
|
-
try {
|
|
1239
|
-
const run = queries.getRun(runId);
|
|
1240
|
-
if (!run) {
|
|
1241
|
-
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
if (['success', 'error', 'cancelled'].includes(run.status)) {
|
|
1246
|
-
sendJSON(req, res, 409, { error: 'Run already completed or cancelled' });
|
|
1247
|
-
return;
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
const cancelledRun = queries.cancelRun(runId);
|
|
1251
|
-
|
|
1252
|
-
const threadId = run.thread_id;
|
|
1253
|
-
if (threadId) {
|
|
1254
|
-
const execution = activeExecutions.get(threadId);
|
|
1255
|
-
if (execution?.pid) {
|
|
1256
|
-
try {
|
|
1257
|
-
process.kill(-execution.pid, 'SIGTERM');
|
|
1258
|
-
} catch {
|
|
1259
|
-
try {
|
|
1260
|
-
process.kill(execution.pid, 'SIGTERM');
|
|
1261
|
-
} catch (e) {
|
|
1262
|
-
console.error(`[cancel] Failed to SIGTERM PID ${execution.pid}:`, e.message);
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
setTimeout(() => {
|
|
1267
|
-
try {
|
|
1268
|
-
process.kill(-execution.pid, 'SIGKILL');
|
|
1269
|
-
} catch {
|
|
1270
|
-
try {
|
|
1271
|
-
process.kill(execution.pid, 'SIGKILL');
|
|
1272
|
-
} catch (e) {}
|
|
1273
|
-
}
|
|
1274
|
-
}, 3000);
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
if (execution?.sessionId) {
|
|
1278
|
-
queries.updateSession(execution.sessionId, {
|
|
1279
|
-
status: 'error',
|
|
1280
|
-
error: 'Cancelled by user',
|
|
1281
|
-
completed_at: Date.now()
|
|
1282
|
-
});
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
activeExecutions.delete(threadId);
|
|
1286
|
-
queries.setIsStreaming(threadId, false);
|
|
1287
|
-
|
|
1288
|
-
broadcastSync({
|
|
1289
|
-
type: 'streaming_cancelled',
|
|
1290
|
-
sessionId: execution?.sessionId || runId,
|
|
1291
|
-
conversationId: threadId,
|
|
1292
|
-
runId: runId,
|
|
1293
|
-
timestamp: Date.now()
|
|
1294
|
-
});
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
sendJSON(req, res, 200, cancelledRun);
|
|
1298
|
-
} catch (err) {
|
|
1299
|
-
if (err.message === 'Run not found') {
|
|
1300
|
-
sendJSON(req, res, 404, { error: err.message });
|
|
1301
|
-
} else if (err.message.includes('already completed')) {
|
|
1302
|
-
sendJSON(req, res, 409, { error: err.message });
|
|
1303
|
-
} else {
|
|
1304
|
-
sendJSON(req, res, 500, { error: err.message });
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
return;
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
const threadRunCancelMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/runs\/([^/]+)\/cancel$/);
|
|
1311
|
-
if (threadRunCancelMatch && req.method === 'POST') {
|
|
1312
|
-
const threadId = threadRunCancelMatch[1];
|
|
1313
|
-
const runId = threadRunCancelMatch[2];
|
|
1314
|
-
|
|
1315
|
-
try {
|
|
1316
|
-
const run = queries.getRun(runId);
|
|
1317
|
-
if (!run) {
|
|
1318
|
-
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
1319
|
-
return;
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
if (run.thread_id !== threadId) {
|
|
1323
|
-
sendJSON(req, res, 400, { error: 'Run does not belong to specified thread' });
|
|
1324
|
-
return;
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
if (['success', 'error', 'cancelled'].includes(run.status)) {
|
|
1328
|
-
sendJSON(req, res, 409, { error: 'Run already completed or cancelled' });
|
|
1329
|
-
return;
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
const cancelledRun = queries.cancelRun(runId);
|
|
1333
|
-
|
|
1334
|
-
const execution = activeExecutions.get(threadId);
|
|
1335
|
-
if (execution?.pid) {
|
|
1336
|
-
try {
|
|
1337
|
-
process.kill(-execution.pid, 'SIGTERM');
|
|
1338
|
-
} catch {
|
|
1339
|
-
try {
|
|
1340
|
-
process.kill(execution.pid, 'SIGTERM');
|
|
1341
|
-
} catch (e) {
|
|
1342
|
-
console.error(`[cancel] Failed to SIGTERM PID ${execution.pid}:`, e.message);
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
setTimeout(() => {
|
|
1347
|
-
try {
|
|
1348
|
-
process.kill(-execution.pid, 'SIGKILL');
|
|
1349
|
-
} catch {
|
|
1350
|
-
try {
|
|
1351
|
-
process.kill(execution.pid, 'SIGKILL');
|
|
1352
|
-
} catch (e) {}
|
|
1353
|
-
}
|
|
1354
|
-
}, 3000);
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
if (execution?.sessionId) {
|
|
1358
|
-
queries.updateSession(execution.sessionId, {
|
|
1359
|
-
status: 'error',
|
|
1360
|
-
error: 'Cancelled by user',
|
|
1361
|
-
completed_at: Date.now()
|
|
1362
|
-
});
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
activeExecutions.delete(threadId);
|
|
1366
|
-
activeProcessesByRunId.delete(runId);
|
|
1367
|
-
queries.setIsStreaming(threadId, false);
|
|
1368
|
-
|
|
1369
|
-
broadcastSync({ type: 'run_cancelled', runId, threadId, sessionId: execution?.sessionId, timestamp: Date.now() });
|
|
1370
|
-
broadcastSync({ type: 'streaming_cancelled', sessionId: execution?.sessionId || runId, conversationId: threadId, runId, timestamp: Date.now() });
|
|
1371
|
-
|
|
1372
|
-
sendJSON(req, res, 200, cancelledRun);
|
|
1373
|
-
} catch (err) {
|
|
1374
|
-
if (err.message === 'Run not found') {
|
|
1375
|
-
sendJSON(req, res, 404, { error: err.message });
|
|
1376
|
-
} else if (err.message.includes('already completed')) {
|
|
1377
|
-
sendJSON(req, res, 409, { error: err.message });
|
|
1378
|
-
} else {
|
|
1379
|
-
sendJSON(req, res, 500, { error: err.message });
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
return;
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
const threadRunWaitMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/runs\/([^/]+)\/wait$/);
|
|
1386
|
-
if (threadRunWaitMatch && req.method === 'GET') {
|
|
1387
|
-
const threadId = threadRunWaitMatch[1];
|
|
1388
|
-
const runId = threadRunWaitMatch[2];
|
|
1389
|
-
|
|
1390
|
-
const run = queries.getRun(runId);
|
|
1391
|
-
if (!run) {
|
|
1392
|
-
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
1393
|
-
return;
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
if (run.thread_id !== threadId) {
|
|
1397
|
-
sendJSON(req, res, 400, { error: 'Run does not belong to specified thread' });
|
|
1398
|
-
return;
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
const startTime = Date.now();
|
|
1402
|
-
const pollInterval = setInterval(() => {
|
|
1403
|
-
const currentRun = queries.getRun(runId);
|
|
1404
|
-
const elapsed = Date.now() - startTime;
|
|
1405
|
-
const done = currentRun && ['success', 'error', 'cancelled'].includes(currentRun.status);
|
|
1406
|
-
if (done) {
|
|
1407
|
-
clearInterval(pollInterval);
|
|
1408
|
-
sendJSON(req, res, 200, currentRun);
|
|
1409
|
-
} else if (elapsed > 30000) {
|
|
1410
|
-
clearInterval(pollInterval);
|
|
1411
|
-
sendJSON(req, res, 408, { error: 'Run still pending after 30s', run_id: runId, status: currentRun?.status || run.status });
|
|
1412
|
-
}
|
|
1413
|
-
}, 500);
|
|
1414
|
-
req.on('close', () => clearInterval(pollInterval));
|
|
1415
|
-
return;
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
574
|
const oauthHandler = _oauthRoutes._match(req.method, pathOnly);
|
|
1419
575
|
if (oauthHandler) { await oauthHandler(req, res); return; }
|
|
1420
576
|
|
|
1421
|
-
const
|
|
1422
|
-
if (
|
|
1423
|
-
const agentId = agentAuthMatch[1];
|
|
1424
|
-
const agent = discoveredAgents.find(a => a.id === agentId);
|
|
1425
|
-
if (!agent) { sendJSON(req, res, 404, { error: 'Agent not found' }); return; }
|
|
577
|
+
const agentActionsHandler = _agentActionsRoutes._match(req.method, pathOnly);
|
|
578
|
+
if (agentActionsHandler) { await agentActionsHandler(req, res); return; }
|
|
1426
579
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
const result = await startCodexOAuth(req, { PORT, BASE_URL });
|
|
1430
|
-
const conversationId = '__agent_auth__';
|
|
1431
|
-
broadcastSync({ type: 'script_started', conversationId, script: 'auth-codex', agentId: 'codex', timestamp: Date.now() });
|
|
1432
|
-
broadcastSync({ type: 'script_output', conversationId, data: `\x1b[36mOpening OpenAI OAuth in your browser...\x1b[0m\r\n\r\nIf it doesn't open automatically, visit:\r\n${result.authUrl}\r\n`, stream: 'stdout', timestamp: Date.now() });
|
|
1433
|
-
|
|
1434
|
-
const pollId = setInterval(() => {
|
|
1435
|
-
if (getCodexOAuthState().status === 'success') {
|
|
1436
|
-
clearInterval(pollId);
|
|
1437
|
-
const email = getCodexOAuthState().email || '';
|
|
1438
|
-
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
|
|
1439
|
-
broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
|
|
1440
|
-
} else if (getCodexOAuthState().status === 'error') {
|
|
1441
|
-
clearInterval(pollId);
|
|
1442
|
-
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${getCodexOAuthState().error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
|
|
1443
|
-
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: getCodexOAuthState().error, timestamp: Date.now() });
|
|
1444
|
-
}
|
|
1445
|
-
}, 1000);
|
|
1446
|
-
|
|
1447
|
-
setTimeout(() => clearInterval(pollId), 5 * 60 * 1000);
|
|
1448
|
-
|
|
1449
|
-
sendJSON(req, res, 200, { ok: true, agentId, authUrl: result.authUrl, mode: result.mode });
|
|
1450
|
-
return;
|
|
1451
|
-
} catch (e) {
|
|
1452
|
-
console.error('[codex-oauth] /api/agents/codex/auth failed:', e);
|
|
1453
|
-
sendJSON(req, res, 500, { error: e.message });
|
|
1454
|
-
return;
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
if (agentId === 'gemini') {
|
|
1459
|
-
try {
|
|
1460
|
-
const result = await startGeminiOAuth(req, { PORT, BASE_URL, rootDir });
|
|
1461
|
-
const conversationId = '__agent_auth__';
|
|
1462
|
-
broadcastSync({ type: 'script_started', conversationId, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
|
|
1463
|
-
broadcastSync({ type: 'script_output', conversationId, data: `\x1b[36mOpening Google OAuth in your browser...\x1b[0m\r\n\r\nIf it doesn't open automatically, visit:\r\n${result.authUrl}\r\n`, stream: 'stdout', timestamp: Date.now() });
|
|
1464
|
-
|
|
1465
|
-
const pollId = setInterval(() => {
|
|
1466
|
-
if (getGeminiOAuthState().status === 'success') {
|
|
1467
|
-
clearInterval(pollId);
|
|
1468
|
-
const email = getGeminiOAuthState().email || '';
|
|
1469
|
-
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
|
|
1470
|
-
broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
|
|
1471
|
-
} else if (getGeminiOAuthState().status === 'error') {
|
|
1472
|
-
clearInterval(pollId);
|
|
1473
|
-
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${getGeminiOAuthState().error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
|
|
1474
|
-
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: getGeminiOAuthState().error, timestamp: Date.now() });
|
|
1475
|
-
}
|
|
1476
|
-
}, 1000);
|
|
1477
|
-
|
|
1478
|
-
setTimeout(() => clearInterval(pollId), 5 * 60 * 1000);
|
|
1479
|
-
|
|
1480
|
-
sendJSON(req, res, 200, { ok: true, agentId, authUrl: result.authUrl, mode: result.mode });
|
|
1481
|
-
return;
|
|
1482
|
-
} catch (e) {
|
|
1483
|
-
console.error('[gemini-oauth] /api/agents/gemini/auth failed:', e);
|
|
1484
|
-
sendJSON(req, res, 500, { error: e.message });
|
|
1485
|
-
return;
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
const authCommands = {
|
|
1490
|
-
'claude-code': { cmd: 'claude', args: ['setup-token'] },
|
|
1491
|
-
'opencode': { cmd: 'opencode', args: ['auth', 'login'] },
|
|
1492
|
-
};
|
|
1493
|
-
const authCmd = authCommands[agentId];
|
|
1494
|
-
if (!authCmd) { sendJSON(req, res, 400, { error: 'No auth command for this agent' }); return; }
|
|
1495
|
-
|
|
1496
|
-
const conversationId = '__agent_auth__';
|
|
1497
|
-
if (activeScripts.has(conversationId)) {
|
|
1498
|
-
sendJSON(req, res, 409, { error: 'Auth process already running' });
|
|
1499
|
-
return;
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
const child = spawn(authCmd.cmd, authCmd.args, {
|
|
1503
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1504
|
-
env: { ...process.env, FORCE_COLOR: '1' },
|
|
1505
|
-
shell: os.platform() === 'win32'
|
|
1506
|
-
});
|
|
1507
|
-
activeScripts.set(conversationId, { process: child, script: 'auth-' + agentId, startTime: Date.now() });
|
|
1508
|
-
broadcastSync({ type: 'script_started', conversationId, script: 'auth-' + agentId, agentId, timestamp: Date.now() });
|
|
1509
|
-
|
|
1510
|
-
const onData = (stream) => (chunk) => {
|
|
1511
|
-
broadcastSync({ type: 'script_output', conversationId, data: chunk.toString(), stream, timestamp: Date.now() });
|
|
1512
|
-
};
|
|
1513
|
-
child.stdout.on('data', onData('stdout'));
|
|
1514
|
-
child.stderr.on('data', onData('stderr'));
|
|
1515
|
-
child.stdout.on('error', () => {});
|
|
1516
|
-
child.stderr.on('error', () => {});
|
|
1517
|
-
child.on('error', (err) => {
|
|
1518
|
-
activeScripts.delete(conversationId);
|
|
1519
|
-
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: err.message, timestamp: Date.now() });
|
|
1520
|
-
});
|
|
1521
|
-
child.on('close', (code) => {
|
|
1522
|
-
activeScripts.delete(conversationId);
|
|
1523
|
-
broadcastSync({ type: 'script_stopped', conversationId, code: code || 0, timestamp: Date.now() });
|
|
1524
|
-
});
|
|
1525
|
-
sendJSON(req, res, 200, { ok: true, agentId, pid: child.pid });
|
|
1526
|
-
return;
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
const agentUpdateMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/update$/);
|
|
1530
|
-
if (agentUpdateMatch && req.method === 'POST') {
|
|
1531
|
-
const agentId = agentUpdateMatch[1];
|
|
1532
|
-
const updateCommands = {
|
|
1533
|
-
'claude-code': { cmd: 'claude', args: ['update', '--yes'] },
|
|
1534
|
-
};
|
|
1535
|
-
const updateCmd = updateCommands[agentId];
|
|
1536
|
-
if (!updateCmd) { sendJSON(req, res, 400, { error: 'No update command for this agent' }); return; }
|
|
1537
|
-
const conversationId = '__agent_update__';
|
|
1538
|
-
if (activeScripts.has(conversationId)) { sendJSON(req, res, 409, { error: 'Update already running' }); return; }
|
|
1539
|
-
const child = spawn(updateCmd.cmd, updateCmd.args, {
|
|
1540
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1541
|
-
env: { ...process.env, FORCE_COLOR: '1' },
|
|
1542
|
-
shell: os.platform() === 'win32'
|
|
1543
|
-
});
|
|
1544
|
-
activeScripts.set(conversationId, { process: child, script: 'update-' + agentId, startTime: Date.now() });
|
|
1545
|
-
broadcastSync({ type: 'script_started', conversationId, script: 'update-' + agentId, agentId, timestamp: Date.now() });
|
|
1546
|
-
const onData = (stream) => (chunk) => {
|
|
1547
|
-
broadcastSync({ type: 'script_output', conversationId, data: chunk.toString(), stream, timestamp: Date.now() });
|
|
1548
|
-
};
|
|
1549
|
-
child.stdout.on('data', onData('stdout'));
|
|
1550
|
-
child.stderr.on('data', onData('stderr'));
|
|
1551
|
-
child.stdout.on('error', () => {});
|
|
1552
|
-
child.stderr.on('error', () => {});
|
|
1553
|
-
child.on('error', (err) => {
|
|
1554
|
-
activeScripts.delete(conversationId);
|
|
1555
|
-
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: err.message, timestamp: Date.now() });
|
|
1556
|
-
});
|
|
1557
|
-
child.on('close', (code) => {
|
|
1558
|
-
activeScripts.delete(conversationId);
|
|
1559
|
-
modelCache.delete(agentId);
|
|
1560
|
-
broadcastSync({ type: 'script_stopped', conversationId, code: code || 0, timestamp: Date.now() });
|
|
1561
|
-
});
|
|
1562
|
-
sendJSON(req, res, 200, { ok: true, agentId, pid: child.pid });
|
|
1563
|
-
return;
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
if (pathOnly === '/api/auth/configs' && req.method === 'GET') {
|
|
1567
|
-
const configs = getProviderConfigs();
|
|
1568
|
-
sendJSON(req, res, 200, configs);
|
|
1569
|
-
return;
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
if (pathOnly === '/api/auth/save-config' && req.method === 'POST') {
|
|
1573
|
-
try {
|
|
1574
|
-
const body = await parseBody(req);
|
|
1575
|
-
const { providerId, apiKey, defaultModel } = body || {};
|
|
1576
|
-
if (typeof providerId !== 'string' || !providerId.length || providerId.length > 100) {
|
|
1577
|
-
sendJSON(req, res, 400, { error: 'Invalid providerId' }); return;
|
|
1578
|
-
}
|
|
1579
|
-
if (typeof apiKey !== 'string' || !apiKey.length || apiKey.length > 10000) {
|
|
1580
|
-
sendJSON(req, res, 400, { error: 'Invalid apiKey' }); return;
|
|
1581
|
-
}
|
|
1582
|
-
if (defaultModel !== undefined && (typeof defaultModel !== 'string' || defaultModel.length > 200)) {
|
|
1583
|
-
sendJSON(req, res, 400, { error: 'Invalid defaultModel' }); return;
|
|
1584
|
-
}
|
|
1585
|
-
const configPath = saveProviderConfig(providerId, apiKey, defaultModel || '');
|
|
1586
|
-
sendJSON(req, res, 200, { success: true, path: configPath });
|
|
1587
|
-
} catch (err) {
|
|
1588
|
-
sendJSON(req, res, 400, { error: err.message });
|
|
1589
|
-
}
|
|
1590
|
-
return;
|
|
1591
|
-
}
|
|
580
|
+
const authConfigHandler = _authConfigRoutes._match(req.method, pathOnly);
|
|
581
|
+
if (authConfigHandler) { await authConfigHandler(req, res); return; }
|
|
1592
582
|
|
|
1593
583
|
const speechHandler = _speechRoutes._match(req.method, pathOnly);
|
|
1594
584
|
if (speechHandler) { await speechHandler(req, res, pathOnly); return; }
|
|
@@ -1659,680 +649,6 @@ function serveFile(filePath, res, req) { return _serveFile(filePath, res, req, _
|
|
|
1659
649
|
|
|
1660
650
|
let broadcastSeq = 0;
|
|
1661
651
|
|
|
1662
|
-
function parseRateLimitResetTime(text) {
|
|
1663
|
-
const match = text.match(/resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(?(UTC|[A-Z]{2,4})\)?/i);
|
|
1664
|
-
if (!match) return 300;
|
|
1665
|
-
let hours = parseInt(match[1], 10);
|
|
1666
|
-
const minutes = match[2] ? parseInt(match[2], 10) : 0;
|
|
1667
|
-
const period = match[3]?.toLowerCase();
|
|
1668
|
-
if (period === 'pm' && hours !== 12) hours += 12;
|
|
1669
|
-
if (period === 'am' && hours === 12) hours = 0;
|
|
1670
|
-
const now = new Date();
|
|
1671
|
-
const resetTime = new Date(now);
|
|
1672
|
-
resetTime.setUTCHours(hours, minutes, 0, 0);
|
|
1673
|
-
if (resetTime <= now) resetTime.setUTCDate(resetTime.getUTCDate() + 1);
|
|
1674
|
-
return Math.max(60, Math.ceil((resetTime.getTime() - now.getTime()) / 1000));
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
async function processMessageWithStreaming(conversationId, messageId, sessionId, content, agentId, model, subAgent) {
|
|
1678
|
-
const startTime = Date.now();
|
|
1679
|
-
touchACP(agentId);
|
|
1680
|
-
|
|
1681
|
-
const conv = queries.getConversation(conversationId);
|
|
1682
|
-
if (!conv) {
|
|
1683
|
-
console.error(`[stream] Conversation ${conversationId} not found, aborting`);
|
|
1684
|
-
queries.updateSession(sessionId, { status: 'error', error: 'Conversation not found' });
|
|
1685
|
-
queries.setIsStreaming(conversationId, false);
|
|
1686
|
-
return;
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
if (activeExecutions.has(conversationId)) {
|
|
1690
|
-
const existing = activeExecutions.get(conversationId);
|
|
1691
|
-
if (existing.sessionId !== sessionId) {
|
|
1692
|
-
debugLog(`[stream] Conversation ${conversationId} already has active execution (different session), aborting duplicate`);
|
|
1693
|
-
return;
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
if (rateLimitState.has(conversationId)) {
|
|
1698
|
-
const rlState = rateLimitState.get(conversationId);
|
|
1699
|
-
if (rlState.retryAt > Date.now()) {
|
|
1700
|
-
debugLog(`[stream] Conversation ${conversationId} is in rate limit cooldown, aborting`);
|
|
1701
|
-
return;
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
activeExecutions.set(conversationId, { pid: null, startTime, sessionId, lastActivity: startTime });
|
|
1706
|
-
execMachine.send(conversationId, { type: 'START', sessionId });
|
|
1707
|
-
queries.setIsStreaming(conversationId, true);
|
|
1708
|
-
queries.updateSession(sessionId, { status: 'active' });
|
|
1709
|
-
const batcher = createChunkBatcher(queries, debugLog);
|
|
1710
|
-
|
|
1711
|
-
try {
|
|
1712
|
-
debugLog(`[stream] Starting: conversationId=${conversationId}, sessionId=${sessionId}`);
|
|
1713
|
-
|
|
1714
|
-
const cwd = conv?.workingDirectory || STARTUP_CWD;
|
|
1715
|
-
let resumeSessionId = conv?.claudeSessionId || null;
|
|
1716
|
-
|
|
1717
|
-
let allBlocks = [];
|
|
1718
|
-
let eventCount = 0;
|
|
1719
|
-
let currentSequence = queries.getMaxSequence(sessionId) ?? -1;
|
|
1720
|
-
|
|
1721
|
-
const onEvent = (parsed) => {
|
|
1722
|
-
eventCount++;
|
|
1723
|
-
const entry = activeExecutions.get(conversationId);
|
|
1724
|
-
if (entry) entry.lastActivity = Date.now();
|
|
1725
|
-
if (parsed.session_id) {
|
|
1726
|
-
ownedSessionIds.add(parsed.session_id);
|
|
1727
|
-
if (!resumeSessionId || resumeSessionId !== parsed.session_id) {
|
|
1728
|
-
resumeSessionId = parsed.session_id;
|
|
1729
|
-
queries.setClaudeSessionId(conversationId, parsed.session_id, sessionId);
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
debugLog(`[stream] Event ${eventCount}: type=${parsed.type}`);
|
|
1733
|
-
|
|
1734
|
-
if (parsed.type === 'system') {
|
|
1735
|
-
if (parsed.subtype === 'task_notification') return;
|
|
1736
|
-
if (!parsed.model && !parsed.cwd && !parsed.tools) return;
|
|
1737
|
-
|
|
1738
|
-
const systemBlock = {
|
|
1739
|
-
type: 'system',
|
|
1740
|
-
subtype: parsed.subtype,
|
|
1741
|
-
model: parsed.model,
|
|
1742
|
-
cwd: parsed.cwd,
|
|
1743
|
-
tools: parsed.tools,
|
|
1744
|
-
session_id: parsed.session_id
|
|
1745
|
-
};
|
|
1746
|
-
|
|
1747
|
-
currentSequence++;
|
|
1748
|
-
batcher.add(sessionId, conversationId, currentSequence, 'system', systemBlock);
|
|
1749
|
-
|
|
1750
|
-
broadcastSync({
|
|
1751
|
-
type: 'streaming_progress',
|
|
1752
|
-
sessionId,
|
|
1753
|
-
conversationId,
|
|
1754
|
-
block: systemBlock,
|
|
1755
|
-
blockRole: 'system',
|
|
1756
|
-
blockIndex: allBlocks.length,
|
|
1757
|
-
seq: currentSequence,
|
|
1758
|
-
timestamp: Date.now()
|
|
1759
|
-
});
|
|
1760
|
-
} else if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
1761
|
-
for (const block of parsed.message.content) {
|
|
1762
|
-
allBlocks.push(block);
|
|
1763
|
-
|
|
1764
|
-
currentSequence++;
|
|
1765
|
-
batcher.add(sessionId, conversationId, currentSequence, block.type || 'assistant', block);
|
|
1766
|
-
|
|
1767
|
-
broadcastSync({
|
|
1768
|
-
type: 'streaming_progress',
|
|
1769
|
-
sessionId,
|
|
1770
|
-
conversationId,
|
|
1771
|
-
block,
|
|
1772
|
-
blockRole: 'assistant',
|
|
1773
|
-
blockIndex: allBlocks.length - 1,
|
|
1774
|
-
seq: currentSequence,
|
|
1775
|
-
timestamp: Date.now()
|
|
1776
|
-
});
|
|
1777
|
-
|
|
1778
|
-
if (block.type === 'text' && block.text) {
|
|
1779
|
-
// Check for rate limit message in text content
|
|
1780
|
-
const rateLimitTextMatch = block.text.match(/you'?ve hit your limit|rate limit exceeded/i);
|
|
1781
|
-
if (rateLimitTextMatch) {
|
|
1782
|
-
debugLog(`[rate-limit] Detected rate limit message in stream for conv ${conversationId}`);
|
|
1783
|
-
|
|
1784
|
-
const retryAfterSec = parseRateLimitResetTime(block.text);
|
|
1785
|
-
debugLog(`[rate-limit] Parsed reset time, retry in ${retryAfterSec}s`);
|
|
1786
|
-
|
|
1787
|
-
// Kill the running process
|
|
1788
|
-
const entry = activeExecutions.get(conversationId);
|
|
1789
|
-
if (entry && entry.pid) {
|
|
1790
|
-
try {
|
|
1791
|
-
process.kill(entry.pid);
|
|
1792
|
-
debugLog(`[rate-limit] Killed process ${entry.pid} for conv ${conversationId}`);
|
|
1793
|
-
} catch (e) {
|
|
1794
|
-
debugLog(`[rate-limit] Failed to kill process: ${e.message}`);
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
// Set flag to stop processing and trigger retry
|
|
1799
|
-
const existingRetryCount = rateLimitState.get(conversationId)?.retryCount || 0;
|
|
1800
|
-
if (existingRetryCount >= 3) {
|
|
1801
|
-
debugLog(`[rate-limit] Conv ${conversationId} stream rate limit hit ${existingRetryCount + 1} times, giving up`);
|
|
1802
|
-
batcher.drain();
|
|
1803
|
-
activeExecutions.delete(conversationId);
|
|
1804
|
-
queries.setIsStreaming(conversationId, false);
|
|
1805
|
-
const errorMessage = queries.createMessage(conversationId, 'assistant', `Error: Rate limit exceeded after ${existingRetryCount + 1} attempts. Please try again later.`);
|
|
1806
|
-
broadcastSync({ type: 'message_created', conversationId, message: errorMessage, timestamp: Date.now() });
|
|
1807
|
-
broadcastSync({ type: 'streaming_complete', sessionId, conversationId, interrupted: true, timestamp: Date.now() });
|
|
1808
|
-
return;
|
|
1809
|
-
}
|
|
1810
|
-
rateLimitState.set(conversationId, {
|
|
1811
|
-
retryAt: Date.now() + (retryAfterSec * 1000),
|
|
1812
|
-
cooldownMs: retryAfterSec * 1000,
|
|
1813
|
-
retryCount: existingRetryCount + 1,
|
|
1814
|
-
isStreamDetected: true
|
|
1815
|
-
});
|
|
1816
|
-
|
|
1817
|
-
// Broadcast rate limit event
|
|
1818
|
-
broadcastSync({
|
|
1819
|
-
type: 'rate_limit_hit',
|
|
1820
|
-
sessionId,
|
|
1821
|
-
conversationId,
|
|
1822
|
-
retryAfterMs: retryAfterSec * 1000,
|
|
1823
|
-
retryAt: Date.now() + (retryAfterSec * 1000),
|
|
1824
|
-
retryCount: 1,
|
|
1825
|
-
timestamp: Date.now()
|
|
1826
|
-
});
|
|
1827
|
-
|
|
1828
|
-
batcher.drain();
|
|
1829
|
-
activeExecutions.delete(conversationId);
|
|
1830
|
-
queries.setIsStreaming(conversationId, false);
|
|
1831
|
-
|
|
1832
|
-
// Schedule retry
|
|
1833
|
-
setTimeout(() => {
|
|
1834
|
-
rateLimitState.delete(conversationId);
|
|
1835
|
-
broadcastSync({
|
|
1836
|
-
type: 'rate_limit_clear',
|
|
1837
|
-
conversationId,
|
|
1838
|
-
timestamp: Date.now()
|
|
1839
|
-
});
|
|
1840
|
-
scheduleRetry(conversationId, messageId, content, agentId, model, subAgent);
|
|
1841
|
-
}, retryAfterSec * 1000);
|
|
1842
|
-
|
|
1843
|
-
return; // Stop processing events
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
eagerTTS(block.text, conversationId, sessionId);
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
} else if (parsed.type === 'user' && parsed.message?.content) {
|
|
1850
|
-
for (const block of parsed.message.content) {
|
|
1851
|
-
if (block.type === 'tool_result') {
|
|
1852
|
-
const toolResultBlock = {
|
|
1853
|
-
type: 'tool_result',
|
|
1854
|
-
tool_use_id: block.tool_use_id,
|
|
1855
|
-
content: typeof block.content === 'string' ? block.content : JSON.stringify(block.content),
|
|
1856
|
-
is_error: block.is_error || false
|
|
1857
|
-
};
|
|
1858
|
-
|
|
1859
|
-
currentSequence++;
|
|
1860
|
-
batcher.add(sessionId, conversationId, currentSequence, 'tool_result', toolResultBlock);
|
|
1861
|
-
|
|
1862
|
-
broadcastSync({
|
|
1863
|
-
type: 'streaming_progress',
|
|
1864
|
-
sessionId,
|
|
1865
|
-
conversationId,
|
|
1866
|
-
block: toolResultBlock,
|
|
1867
|
-
blockRole: 'tool_result',
|
|
1868
|
-
blockIndex: allBlocks.length,
|
|
1869
|
-
seq: currentSequence,
|
|
1870
|
-
timestamp: Date.now()
|
|
1871
|
-
});
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
} else if (parsed.type === 'result') {
|
|
1875
|
-
const resultBlock = {
|
|
1876
|
-
type: 'result',
|
|
1877
|
-
subtype: parsed.subtype,
|
|
1878
|
-
duration_ms: parsed.duration_ms,
|
|
1879
|
-
total_cost_usd: parsed.total_cost_usd,
|
|
1880
|
-
num_turns: parsed.num_turns,
|
|
1881
|
-
is_error: parsed.is_error || false,
|
|
1882
|
-
result: parsed.result
|
|
1883
|
-
};
|
|
1884
|
-
|
|
1885
|
-
currentSequence++;
|
|
1886
|
-
batcher.add(sessionId, conversationId, currentSequence, 'result', resultBlock);
|
|
1887
|
-
|
|
1888
|
-
broadcastSync({
|
|
1889
|
-
type: 'streaming_progress',
|
|
1890
|
-
sessionId,
|
|
1891
|
-
conversationId,
|
|
1892
|
-
block: resultBlock,
|
|
1893
|
-
blockRole: 'result',
|
|
1894
|
-
blockIndex: allBlocks.length,
|
|
1895
|
-
isResult: true,
|
|
1896
|
-
seq: currentSequence,
|
|
1897
|
-
timestamp: Date.now()
|
|
1898
|
-
});
|
|
1899
|
-
|
|
1900
|
-
if (parsed.result) {
|
|
1901
|
-
const resultText = typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
|
|
1902
|
-
|
|
1903
|
-
// Check for rate limit message in result
|
|
1904
|
-
const rateLimitResultMatch = resultText.match(/you'?ve hit your limit|rate limit exceeded/i);
|
|
1905
|
-
if (rateLimitResultMatch) {
|
|
1906
|
-
debugLog(`[rate-limit] Detected rate limit in result for conv ${conversationId}`);
|
|
1907
|
-
|
|
1908
|
-
const retryAfterSec = parseRateLimitResetTime(resultText);
|
|
1909
|
-
|
|
1910
|
-
const entry = activeExecutions.get(conversationId);
|
|
1911
|
-
if (entry && entry.pid) {
|
|
1912
|
-
try {
|
|
1913
|
-
process.kill(entry.pid);
|
|
1914
|
-
} catch (e) {}
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
const existingRetryCount2 = rateLimitState.get(conversationId)?.retryCount || 0;
|
|
1918
|
-
if (existingRetryCount2 >= 3) {
|
|
1919
|
-
debugLog(`[rate-limit] Conv ${conversationId} result rate limit hit ${existingRetryCount2 + 1} times, giving up`);
|
|
1920
|
-
batcher.drain();
|
|
1921
|
-
activeExecutions.delete(conversationId);
|
|
1922
|
-
queries.setIsStreaming(conversationId, false);
|
|
1923
|
-
const errorMessage = queries.createMessage(conversationId, 'assistant', `Error: Rate limit exceeded after ${existingRetryCount2 + 1} attempts. Please try again later.`);
|
|
1924
|
-
broadcastSync({ type: 'message_created', conversationId, message: errorMessage, timestamp: Date.now() });
|
|
1925
|
-
broadcastSync({ type: 'streaming_complete', sessionId, conversationId, interrupted: true, timestamp: Date.now() });
|
|
1926
|
-
return;
|
|
1927
|
-
}
|
|
1928
|
-
rateLimitState.set(conversationId, {
|
|
1929
|
-
retryAt: Date.now() + (retryAfterSec * 1000),
|
|
1930
|
-
cooldownMs: retryAfterSec * 1000,
|
|
1931
|
-
retryCount: existingRetryCount2 + 1,
|
|
1932
|
-
isStreamDetected: true
|
|
1933
|
-
});
|
|
1934
|
-
|
|
1935
|
-
broadcastSync({
|
|
1936
|
-
type: 'rate_limit_hit',
|
|
1937
|
-
sessionId,
|
|
1938
|
-
conversationId,
|
|
1939
|
-
retryAfterMs: retryAfterSec * 1000,
|
|
1940
|
-
retryAt: Date.now() + (retryAfterSec * 1000),
|
|
1941
|
-
retryCount: existingRetryCount2 + 1,
|
|
1942
|
-
timestamp: Date.now()
|
|
1943
|
-
});
|
|
1944
|
-
|
|
1945
|
-
batcher.drain();
|
|
1946
|
-
activeExecutions.delete(conversationId);
|
|
1947
|
-
queries.setIsStreaming(conversationId, false);
|
|
1948
|
-
|
|
1949
|
-
setTimeout(() => {
|
|
1950
|
-
rateLimitState.delete(conversationId);
|
|
1951
|
-
broadcastSync({
|
|
1952
|
-
type: 'rate_limit_clear',
|
|
1953
|
-
conversationId,
|
|
1954
|
-
timestamp: Date.now()
|
|
1955
|
-
});
|
|
1956
|
-
scheduleRetry(conversationId, messageId, content, agentId, model, subAgent);
|
|
1957
|
-
}, retryAfterSec * 1000);
|
|
1958
|
-
|
|
1959
|
-
return;
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
if (resultText) eagerTTS(resultText, conversationId, sessionId);
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
if (parsed.result && allBlocks.length === 0) {
|
|
1966
|
-
allBlocks.push({ type: 'text', text: String(parsed.result) });
|
|
1967
|
-
}
|
|
1968
|
-
} else if (parsed.type === 'tool_status') {
|
|
1969
|
-
// Handle ACP tool status updates (in_progress, pending)
|
|
1970
|
-
broadcastSync({
|
|
1971
|
-
type: 'streaming_progress',
|
|
1972
|
-
sessionId,
|
|
1973
|
-
conversationId,
|
|
1974
|
-
block: {
|
|
1975
|
-
type: 'tool_status',
|
|
1976
|
-
tool_use_id: parsed.tool_use_id,
|
|
1977
|
-
status: parsed.status
|
|
1978
|
-
},
|
|
1979
|
-
seq: currentSequence,
|
|
1980
|
-
timestamp: Date.now()
|
|
1981
|
-
});
|
|
1982
|
-
} else if (parsed.type === 'usage') {
|
|
1983
|
-
// Handle ACP usage updates
|
|
1984
|
-
broadcastSync({
|
|
1985
|
-
type: 'streaming_progress',
|
|
1986
|
-
sessionId,
|
|
1987
|
-
conversationId,
|
|
1988
|
-
block: {
|
|
1989
|
-
type: 'usage',
|
|
1990
|
-
usage: parsed.usage
|
|
1991
|
-
},
|
|
1992
|
-
seq: currentSequence,
|
|
1993
|
-
timestamp: Date.now()
|
|
1994
|
-
});
|
|
1995
|
-
} else if (parsed.type === 'plan') {
|
|
1996
|
-
// Handle ACP plan updates
|
|
1997
|
-
broadcastSync({
|
|
1998
|
-
type: 'streaming_progress',
|
|
1999
|
-
sessionId,
|
|
2000
|
-
conversationId,
|
|
2001
|
-
block: {
|
|
2002
|
-
type: 'plan',
|
|
2003
|
-
entries: parsed.entries
|
|
2004
|
-
},
|
|
2005
|
-
seq: currentSequence,
|
|
2006
|
-
timestamp: Date.now()
|
|
2007
|
-
});
|
|
2008
|
-
}
|
|
2009
|
-
};
|
|
2010
|
-
|
|
2011
|
-
const resolvedModel = model || conv?.model || null;
|
|
2012
|
-
const resolvedSubAgent = subAgent || conv?.subAgent || null;
|
|
2013
|
-
const unifiedSystemPrompt = buildSystemPrompt(agentId, resolvedModel, resolvedSubAgent);
|
|
2014
|
-
const config = {
|
|
2015
|
-
verbose: true,
|
|
2016
|
-
outputFormat: 'stream-json',
|
|
2017
|
-
timeout: 1800000,
|
|
2018
|
-
print: true,
|
|
2019
|
-
resumeSessionId,
|
|
2020
|
-
systemPrompt: unifiedSystemPrompt,
|
|
2021
|
-
model: resolvedModel || undefined,
|
|
2022
|
-
subAgent: resolvedSubAgent || undefined,
|
|
2023
|
-
onEvent,
|
|
2024
|
-
onPid: (pid) => {
|
|
2025
|
-
const entry = activeExecutions.get(conversationId);
|
|
2026
|
-
if (entry) entry.pid = pid;
|
|
2027
|
-
execMachine.send(conversationId, { type: 'SET_PID', pid });
|
|
2028
|
-
},
|
|
2029
|
-
onProcess: (proc) => {
|
|
2030
|
-
const entry = activeExecutions.get(conversationId);
|
|
2031
|
-
if (entry) entry.proc = proc;
|
|
2032
|
-
execMachine.send(conversationId, { type: 'SET_PROC', proc });
|
|
2033
|
-
}
|
|
2034
|
-
};
|
|
2035
|
-
|
|
2036
|
-
// Resolve cli-wrapper agent IDs (e.g. cli-kilo → kilo) to their underlying registered agent
|
|
2037
|
-
let resolvedAgentId = agentId || 'claude-code';
|
|
2038
|
-
const wrapperAgent = discoveredAgents.find(a => a.id === resolvedAgentId && a.protocol === 'cli-wrapper' && a.acpId);
|
|
2039
|
-
if (wrapperAgent) resolvedAgentId = wrapperAgent.acpId;
|
|
2040
|
-
|
|
2041
|
-
const { outputs, sessionId: claudeSessionId } = await runClaudeWithStreaming(content, cwd, resolvedAgentId, config);
|
|
2042
|
-
|
|
2043
|
-
// Check if rate limit was already handled in stream detection
|
|
2044
|
-
if (rateLimitState.get(conversationId)?.isStreamDetected) {
|
|
2045
|
-
debugLog(`[rate-limit] Rate limit already handled in stream for conv ${conversationId}, skipping success handler`);
|
|
2046
|
-
return;
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
activeExecutions.delete(conversationId);
|
|
2050
|
-
execMachine.send(conversationId, { type: 'COMPLETE' });
|
|
2051
|
-
batcher.drain();
|
|
2052
|
-
if (claudeSessionId) ownedSessionIds.delete(claudeSessionId);
|
|
2053
|
-
debugLog(`[stream] Claude returned ${outputs.length} outputs, sessionId=${claudeSessionId}`);
|
|
2054
|
-
|
|
2055
|
-
debugLog(`[stream] Keeping claudeSessionId=${claudeSessionId} for session continuity`);
|
|
2056
|
-
|
|
2057
|
-
// Mark session as complete
|
|
2058
|
-
queries.updateSession(sessionId, {
|
|
2059
|
-
status: 'complete',
|
|
2060
|
-
response: JSON.stringify({ outputs, eventCount }),
|
|
2061
|
-
completed_at: Date.now()
|
|
2062
|
-
});
|
|
2063
|
-
|
|
2064
|
-
broadcastSync({
|
|
2065
|
-
type: 'streaming_complete',
|
|
2066
|
-
sessionId,
|
|
2067
|
-
conversationId,
|
|
2068
|
-
agentId,
|
|
2069
|
-
eventCount,
|
|
2070
|
-
seq: currentSequence,
|
|
2071
|
-
timestamp: Date.now()
|
|
2072
|
-
});
|
|
2073
|
-
|
|
2074
|
-
debugLog(`[stream] Completed: ${outputs.length} outputs, ${eventCount} events`);
|
|
2075
|
-
} catch (error) {
|
|
2076
|
-
const elapsed = Date.now() - startTime;
|
|
2077
|
-
debugLog(`[stream] Error after ${elapsed}ms: ${error.message}`);
|
|
2078
|
-
const conv2 = queries.getConversation(conversationId);
|
|
2079
|
-
if (conv2?.claudeSessionId) ownedSessionIds.delete(conv2.claudeSessionId);
|
|
2080
|
-
|
|
2081
|
-
// Check if rate limit was already handled in stream detection
|
|
2082
|
-
const existingState = rateLimitState.get(conversationId);
|
|
2083
|
-
if (existingState?.isStreamDetected) {
|
|
2084
|
-
debugLog(`[rate-limit] Rate limit already handled in stream for conv ${conversationId}, skipping catch handler`);
|
|
2085
|
-
return;
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
const isAuthError = error.authError || error.nonRetryable ||
|
|
2089
|
-
/401|unauthorized|invalid.*auth|invalid.*token|auth.*failed|permission denied|access denied/i.test(error.message);
|
|
2090
|
-
|
|
2091
|
-
const isRateLimit = error.rateLimited ||
|
|
2092
|
-
/rate.?limit|429|too many requests|overloaded|throttl/i.test(error.message);
|
|
2093
|
-
|
|
2094
|
-
queries.updateSession(sessionId, {
|
|
2095
|
-
status: 'error',
|
|
2096
|
-
error: error.message,
|
|
2097
|
-
completed_at: Date.now()
|
|
2098
|
-
});
|
|
2099
|
-
|
|
2100
|
-
if (isAuthError) {
|
|
2101
|
-
debugLog(`[auth-error] Auth error for conv ${conversationId}: ${error.message}`);
|
|
2102
|
-
broadcastSync({
|
|
2103
|
-
type: 'streaming_error',
|
|
2104
|
-
sessionId,
|
|
2105
|
-
conversationId,
|
|
2106
|
-
error: `Authentication failed: ${error.message}. Please check your API credentials.`,
|
|
2107
|
-
recoverable: false,
|
|
2108
|
-
isAuthError: true,
|
|
2109
|
-
timestamp: Date.now()
|
|
2110
|
-
});
|
|
2111
|
-
const errorMessage = queries.createMessage(conversationId, 'assistant', `Error: Authentication failed. ${error.message}. Please update your credentials and try again.`);
|
|
2112
|
-
broadcastSync({
|
|
2113
|
-
type: 'message_created',
|
|
2114
|
-
conversationId,
|
|
2115
|
-
message: errorMessage,
|
|
2116
|
-
timestamp: Date.now()
|
|
2117
|
-
});
|
|
2118
|
-
queries.setIsStreaming(conversationId, false);
|
|
2119
|
-
batcher.drain();
|
|
2120
|
-
activeExecutions.delete(conversationId);
|
|
2121
|
-
return;
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
if (isRateLimit) {
|
|
2125
|
-
const existingState = rateLimitState.get(conversationId) || {};
|
|
2126
|
-
const retryCount = (existingState.retryCount || 0) + 1;
|
|
2127
|
-
const maxRateLimitRetries = 3;
|
|
2128
|
-
|
|
2129
|
-
if (retryCount > maxRateLimitRetries) {
|
|
2130
|
-
debugLog(`[rate-limit] Conv ${conversationId} hit rate limit ${retryCount} times, giving up`);
|
|
2131
|
-
broadcastSync({
|
|
2132
|
-
type: 'streaming_error',
|
|
2133
|
-
sessionId,
|
|
2134
|
-
conversationId,
|
|
2135
|
-
error: `Rate limit exceeded after ${retryCount} attempts. Please try again later.`,
|
|
2136
|
-
recoverable: false,
|
|
2137
|
-
timestamp: Date.now()
|
|
2138
|
-
});
|
|
2139
|
-
const errorMessage = queries.createMessage(conversationId, 'assistant', `Error: Rate limit exceeded after ${retryCount} attempts. Please try again later.`);
|
|
2140
|
-
broadcastSync({
|
|
2141
|
-
type: 'message_created',
|
|
2142
|
-
conversationId,
|
|
2143
|
-
message: errorMessage,
|
|
2144
|
-
timestamp: Date.now()
|
|
2145
|
-
});
|
|
2146
|
-
queries.setIsStreaming(conversationId, false);
|
|
2147
|
-
return;
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
const cooldownMs = (error.retryAfterSec || 60) * 1000;
|
|
2151
|
-
const retryAt = Date.now() + cooldownMs;
|
|
2152
|
-
rateLimitState.set(conversationId, { retryAt, cooldownMs, retryCount });
|
|
2153
|
-
debugLog(`[rate-limit] Conv ${conversationId} hit rate limit (attempt ${retryCount}/${maxRateLimitRetries}), retry in ${cooldownMs}ms`);
|
|
2154
|
-
|
|
2155
|
-
broadcastSync({
|
|
2156
|
-
type: 'rate_limit_hit',
|
|
2157
|
-
sessionId,
|
|
2158
|
-
conversationId,
|
|
2159
|
-
retryAfterMs: cooldownMs,
|
|
2160
|
-
retryAt,
|
|
2161
|
-
retryCount,
|
|
2162
|
-
timestamp: Date.now()
|
|
2163
|
-
});
|
|
2164
|
-
|
|
2165
|
-
batcher.drain();
|
|
2166
|
-
|
|
2167
|
-
debugLog(`[rate-limit] Scheduling retry for conv ${conversationId} in ${cooldownMs}ms (attempt ${retryCount + 1})`);
|
|
2168
|
-
|
|
2169
|
-
setTimeout(() => {
|
|
2170
|
-
debugLog(`[rate-limit] Timeout fired for conv ${conversationId}, calling scheduleRetry`);
|
|
2171
|
-
rateLimitState.delete(conversationId);
|
|
2172
|
-
debugLog(`[rate-limit] Conv ${conversationId} cooldown expired, restarting (attempt ${retryCount + 1})`);
|
|
2173
|
-
broadcastSync({
|
|
2174
|
-
type: 'rate_limit_clear',
|
|
2175
|
-
conversationId,
|
|
2176
|
-
timestamp: Date.now()
|
|
2177
|
-
});
|
|
2178
|
-
scheduleRetry(conversationId, messageId, content, agentId, model, subAgent);
|
|
2179
|
-
}, cooldownMs);
|
|
2180
|
-
return;
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
const isSessionConflict = error.exitCode === null && eventCount === 0;
|
|
2184
|
-
|
|
2185
|
-
broadcastSync({
|
|
2186
|
-
type: 'streaming_error',
|
|
2187
|
-
sessionId,
|
|
2188
|
-
conversationId,
|
|
2189
|
-
error: error.message,
|
|
2190
|
-
isPrematureEnd: error.isPrematureEnd || false,
|
|
2191
|
-
exitCode: error.exitCode,
|
|
2192
|
-
stderrText: error.stderrText,
|
|
2193
|
-
recoverable: elapsed < 60000,
|
|
2194
|
-
isSessionConflict,
|
|
2195
|
-
timestamp: Date.now()
|
|
2196
|
-
});
|
|
2197
|
-
|
|
2198
|
-
if (!isSessionConflict) {
|
|
2199
|
-
const errorMessage = queries.createMessage(conversationId, 'assistant', `Error: ${error.message}`);
|
|
2200
|
-
broadcastSync({
|
|
2201
|
-
type: 'message_created',
|
|
2202
|
-
conversationId,
|
|
2203
|
-
message: errorMessage,
|
|
2204
|
-
timestamp: Date.now()
|
|
2205
|
-
});
|
|
2206
|
-
}
|
|
2207
|
-
} finally {
|
|
2208
|
-
batcher.drain();
|
|
2209
|
-
// Use atomic cleanup but only if not in rate limit recovery
|
|
2210
|
-
if (!rateLimitState.has(conversationId)) {
|
|
2211
|
-
cleanupExecution(conversationId);
|
|
2212
|
-
drainMessageQueue(conversationId);
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
function scheduleRetry(conversationId, messageId, content, agentId, model, subAgent) {
|
|
2218
|
-
debugLog(`[rate-limit] scheduleRetry called for conv ${conversationId}, messageId=${messageId}`);
|
|
2219
|
-
|
|
2220
|
-
if (!content) {
|
|
2221
|
-
const conv = queries.getConversation(conversationId);
|
|
2222
|
-
const lastMsg = queries.getLastUserMessage(conversationId);
|
|
2223
|
-
content = lastMsg?.content || 'continue';
|
|
2224
|
-
debugLog(`[rate-limit] Recovered content from last message: ${content?.substring?.(0, 50)}...`);
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
const newSession = queries.createSession(conversationId);
|
|
2228
|
-
queries.createEvent('session.created', { messageId, sessionId: newSession.id, retryReason: 'rate_limit' }, conversationId, newSession.id);
|
|
2229
|
-
|
|
2230
|
-
debugLog(`[rate-limit] Broadcasting streaming_start for retry session ${newSession.id}`);
|
|
2231
|
-
broadcastSync({
|
|
2232
|
-
type: 'streaming_start',
|
|
2233
|
-
sessionId: newSession.id,
|
|
2234
|
-
conversationId,
|
|
2235
|
-
messageId,
|
|
2236
|
-
agentId,
|
|
2237
|
-
queueLength: messageQueues.get(conversationId)?.length || 0,
|
|
2238
|
-
timestamp: Date.now()
|
|
2239
|
-
});
|
|
2240
|
-
|
|
2241
|
-
const startTime = Date.now();
|
|
2242
|
-
activeExecutions.set(conversationId, { pid: null, startTime, sessionId: newSession.id, lastActivity: startTime });
|
|
2243
|
-
|
|
2244
|
-
debugLog(`[rate-limit] Calling processMessageWithStreaming for retry`);
|
|
2245
|
-
processMessageWithStreaming(conversationId, messageId, newSession.id, content, agentId, model, subAgent)
|
|
2246
|
-
.catch(err => {
|
|
2247
|
-
debugLog(`[rate-limit] Retry failed: ${err.message}`);
|
|
2248
|
-
console.error(`[rate-limit] Retry error for conv ${conversationId}:`, err);
|
|
2249
|
-
// Clean up state on retry failure
|
|
2250
|
-
cleanupExecution(conversationId);
|
|
2251
|
-
broadcastSync({
|
|
2252
|
-
type: 'streaming_error',
|
|
2253
|
-
sessionId: newSession.id,
|
|
2254
|
-
conversationId,
|
|
2255
|
-
error: `Rate limit retry failed: ${err.message}`,
|
|
2256
|
-
recoverable: false,
|
|
2257
|
-
timestamp: Date.now()
|
|
2258
|
-
});
|
|
2259
|
-
});
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
function drainMessageQueue(conversationId) {
|
|
2263
|
-
// Machine context queue is authoritative; fall back to legacy Map
|
|
2264
|
-
const machineQueue = execMachine.getQueue(conversationId);
|
|
2265
|
-
const mapQueue = messageQueues.get(conversationId);
|
|
2266
|
-
if (machineQueue.length === 0 && (!mapQueue || mapQueue.length === 0)) return;
|
|
2267
|
-
|
|
2268
|
-
let next;
|
|
2269
|
-
if (machineQueue.length > 0) {
|
|
2270
|
-
// Consume from machine via COMPLETE transition (draining state pops nextItem)
|
|
2271
|
-
execMachine.send(conversationId, { type: 'COMPLETE' });
|
|
2272
|
-
const ctx = execMachine.getContext(conversationId);
|
|
2273
|
-
next = ctx?.nextItem;
|
|
2274
|
-
// Also keep Map in sync
|
|
2275
|
-
if (mapQueue && mapQueue.length > 0) mapQueue.shift();
|
|
2276
|
-
if (mapQueue && mapQueue.length === 0) messageQueues.delete(conversationId);
|
|
2277
|
-
} else {
|
|
2278
|
-
next = mapQueue.shift();
|
|
2279
|
-
if (mapQueue.length === 0) messageQueues.delete(conversationId);
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
|
-
if (!next) return;
|
|
2283
|
-
|
|
2284
|
-
debugLog(`[queue] Draining next message for ${conversationId}, messageId=${next.messageId}`);
|
|
2285
|
-
|
|
2286
|
-
const remainingQueueLength = execMachine.getQueue(conversationId).length || messageQueues.get(conversationId)?.length || 0;
|
|
2287
|
-
|
|
2288
|
-
broadcastSync({
|
|
2289
|
-
type: 'queue_item_dequeued',
|
|
2290
|
-
conversationId,
|
|
2291
|
-
messageId: next.messageId,
|
|
2292
|
-
queueLength: remainingQueueLength,
|
|
2293
|
-
timestamp: Date.now()
|
|
2294
|
-
});
|
|
2295
|
-
|
|
2296
|
-
const session = queries.createSession(conversationId);
|
|
2297
|
-
queries.createEvent('session.created', { messageId: next.messageId, sessionId: session.id }, conversationId, session.id);
|
|
2298
|
-
|
|
2299
|
-
broadcastSync({
|
|
2300
|
-
type: 'streaming_start',
|
|
2301
|
-
sessionId: session.id,
|
|
2302
|
-
conversationId,
|
|
2303
|
-
messageId: next.messageId,
|
|
2304
|
-
agentId: next.agentId,
|
|
2305
|
-
queueLength: remainingQueueLength,
|
|
2306
|
-
timestamp: Date.now()
|
|
2307
|
-
});
|
|
2308
|
-
|
|
2309
|
-
broadcastSync({
|
|
2310
|
-
type: 'queue_status',
|
|
2311
|
-
conversationId,
|
|
2312
|
-
queueLength: remainingQueueLength,
|
|
2313
|
-
timestamp: Date.now()
|
|
2314
|
-
});
|
|
2315
|
-
|
|
2316
|
-
const startTime = Date.now();
|
|
2317
|
-
// Machine START event makes machine authoritative for this execution
|
|
2318
|
-
execMachine.send(conversationId, { type: 'START', sessionId: session.id });
|
|
2319
|
-
activeExecutions.set(conversationId, { pid: null, startTime, sessionId: session.id, lastActivity: startTime });
|
|
2320
|
-
|
|
2321
|
-
processMessageWithStreaming(conversationId, next.messageId, session.id, next.content, next.agentId, next.model, next.subAgent)
|
|
2322
|
-
.catch(err => {
|
|
2323
|
-
debugLog(`[queue] Error processing queued message: ${err.message}`);
|
|
2324
|
-
cleanupExecution(conversationId);
|
|
2325
|
-
broadcastSync({
|
|
2326
|
-
type: 'streaming_error',
|
|
2327
|
-
sessionId: session.id,
|
|
2328
|
-
conversationId,
|
|
2329
|
-
error: `Queue processing failed: ${err.message}`,
|
|
2330
|
-
recoverable: true,
|
|
2331
|
-
timestamp: Date.now()
|
|
2332
|
-
});
|
|
2333
|
-
setTimeout(() => drainMessageQueue(conversationId), 100);
|
|
2334
|
-
});
|
|
2335
|
-
}
|
|
2336
652
|
|
|
2337
653
|
|
|
2338
654
|
const wss = new WebSocketServer({
|
|
@@ -2425,6 +741,23 @@ const broadcastSync = createBroadcast({
|
|
|
2425
741
|
getSeq: () => ++broadcastSeq
|
|
2426
742
|
});
|
|
2427
743
|
|
|
744
|
+
// Wire up process-message factories now that broadcastSync and all deps are available
|
|
745
|
+
const _mqDeps = {
|
|
746
|
+
queries, messageQueues, activeExecutions, rateLimitState, execMachine,
|
|
747
|
+
broadcastSync, cleanupExecution, debugLog,
|
|
748
|
+
getProcessMessageWithStreaming: () => processMessageWithStreaming
|
|
749
|
+
};
|
|
750
|
+
const { scheduleRetry, drainMessageQueue } = createMessageQueue(_mqDeps);
|
|
751
|
+
|
|
752
|
+
const { processMessageWithStreaming } = createProcessMessage({
|
|
753
|
+
queries, activeExecutions, rateLimitState, execMachine,
|
|
754
|
+
broadcastSync, runClaudeWithStreaming, cleanupExecution, checkpointManager,
|
|
755
|
+
discoveredAgents, ownedSessionIds, STARTUP_CWD, buildSystemPrompt,
|
|
756
|
+
parseRateLimitResetTime, eagerTTS, touchACP, createChunkBatcher,
|
|
757
|
+
debugLog, logError,
|
|
758
|
+
scheduleRetry, drainMessageQueue, createEventHandler
|
|
759
|
+
});
|
|
760
|
+
|
|
2428
761
|
// WebSocket protocol router
|
|
2429
762
|
const wsRouter = new WsRouter();
|
|
2430
763
|
|
|
@@ -2437,6 +770,12 @@ const _threadRoutes = registerThreadRoutes({ sendJSON, parseBody, queries });
|
|
|
2437
770
|
const _debugRoutes = registerDebugRoutes({ sendJSON, queries, activeExecutions, messageQueues, syncClients, wsOptimizer, _errLogPath });
|
|
2438
771
|
const _convRoutes = registerConvRoutes({ sendJSON, parseBody, queries, activeExecutions, broadcastSync });
|
|
2439
772
|
const _agentRoutes = registerAgentRoutes({ sendJSON, parseBody, queries, discoveredAgents, getACPStatus, modelCache, getModelsForAgent, debugLog });
|
|
773
|
+
const _messagesRoutes = registerMessagesRoutes({ queries, sendJSON, parseBody, broadcastSync, processMessageWithStreaming, activeExecutions, messageQueues, debugLog, logError });
|
|
774
|
+
const _sessionsRoutes = registerSessionsRoutes({ queries, sendJSON, activeExecutions, rateLimitState, debugLog });
|
|
775
|
+
const _runsRoutes = registerRunsRoutes({ sendJSON, parseBody, queries, broadcastSync, processMessageWithStreaming, activeExecutions, activeProcessesByRunId, discoveredAgents, STARTUP_CWD });
|
|
776
|
+
const _scriptsRoutes = registerScriptsRoutes({ sendJSON, parseBody, queries, broadcastSync, activeScripts, activeExecutions, processMessageWithStreaming, STARTUP_CWD });
|
|
777
|
+
const _agentActionsRoutes = registerAgentActionsRoutes({ sendJSON, queries, broadcastSync, discoveredAgents, activeScripts, startGeminiOAuth, startCodexOAuth, getGeminiOAuthState, getCodexOAuthState, modelCache, PORT, BASE_URL, rootDir });
|
|
778
|
+
const _authConfigRoutes = registerAuthConfigRoutes({ sendJSON, parseBody, getProviderConfigs, saveProviderConfig });
|
|
2440
779
|
|
|
2441
780
|
registerConvHandlers(wsRouter, {
|
|
2442
781
|
queries, activeExecutions, rateLimitState,
|