groove-dev 0.27.55 → 0.27.57
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/ai-chat/CHAT_MASTER_PLAN.md +184 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +169 -0
- package/node_modules/@groove-dev/daemon/src/conversations.js +423 -0
- package/node_modules/@groove-dev/daemon/src/index.js +2 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-C5WTeZO4.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-De-OWmBX.js → index-X58BAjGp.js} +1752 -1745
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +138 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +112 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +347 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +165 -0
- package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +154 -0
- package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +143 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +220 -0
- package/node_modules/@groove-dev/gui/src/views/chat.jsx +6 -0
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +169 -0
- package/packages/daemon/src/conversations.js +423 -0
- package/packages/daemon/src/index.js +2 -0
- package/packages/gui/dist/assets/index-C5WTeZO4.css +1 -0
- package/packages/gui/dist/assets/{index-De-OWmBX.js → index-X58BAjGp.js} +1752 -1745
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.jsx +2 -0
- package/packages/gui/src/components/chat/chat-header.jsx +138 -0
- package/packages/gui/src/components/chat/chat-input.jsx +112 -0
- package/packages/gui/src/components/chat/chat-messages.jsx +347 -0
- package/packages/gui/src/components/chat/chat-view.jsx +165 -0
- package/packages/gui/src/components/chat/conversation-list.jsx +154 -0
- package/packages/gui/src/components/chat/model-picker.jsx +143 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +2 -1
- package/packages/gui/src/stores/groove.js +220 -0
- package/packages/gui/src/views/chat.jsx +6 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CyVj0fHl.css +0 -1
- package/packages/gui/dist/assets/index-CyVj0fHl.css +0 -1
|
@@ -75,6 +75,13 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
75
75
|
chatInputs: {}, // Per-agent draft input text — persists across tab switches
|
|
76
76
|
tokenTimeline: {},
|
|
77
77
|
|
|
78
|
+
// ── Conversations (Chat view) ────────────────────────────
|
|
79
|
+
conversations: [],
|
|
80
|
+
activeConversationId: localStorage.getItem('groove:activeConversationId') || null,
|
|
81
|
+
conversationMessages: loadJSON('groove:conversationMessages'),
|
|
82
|
+
sendingMessage: false,
|
|
83
|
+
streamingConversationId: null,
|
|
84
|
+
|
|
78
85
|
// ── Approvals ─────────────────────────────────────────────
|
|
79
86
|
pendingApprovals: [],
|
|
80
87
|
resolvedApprovals: [],
|
|
@@ -163,6 +170,7 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
163
170
|
if (isTunneled) get().fetchProjectDir();
|
|
164
171
|
}).catch(() => {});
|
|
165
172
|
get().fetchTeams();
|
|
173
|
+
get().fetchConversations();
|
|
166
174
|
get().fetchApprovals();
|
|
167
175
|
get().checkMarketplaceAuth();
|
|
168
176
|
get().fetchTunnels();
|
|
@@ -342,6 +350,28 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
342
350
|
set({ chatHistory: history });
|
|
343
351
|
persistJSON('groove:chatHistory', history);
|
|
344
352
|
}
|
|
353
|
+
|
|
354
|
+
// Mirror to conversation messages if this agent belongs to a conversation
|
|
355
|
+
const conv = get().conversations.find((c) => c.agentId === agentId);
|
|
356
|
+
if (conv) {
|
|
357
|
+
const convMsgs = { ...get().conversationMessages };
|
|
358
|
+
if (!convMsgs[conv.id]) convMsgs[conv.id] = [];
|
|
359
|
+
const convArr = [...convMsgs[conv.id]];
|
|
360
|
+
const lastConv = convArr[convArr.length - 1];
|
|
361
|
+
const isRecentConv = lastConv && lastConv.from === 'assistant' && (Date.now() - lastConv.timestamp) < 8000;
|
|
362
|
+
const isConvDupe = isRecentConv && (lastConv.text === trimmed || lastConv.text.endsWith(trimmed));
|
|
363
|
+
if (!isConvDupe) {
|
|
364
|
+
if (isRecentConv) {
|
|
365
|
+
const sep = data.subtype === 'assistant' ? '\n\n' : ' ';
|
|
366
|
+
convArr[convArr.length - 1] = { ...lastConv, text: lastConv.text + sep + trimmed, timestamp: Date.now() };
|
|
367
|
+
} else {
|
|
368
|
+
convArr.push({ from: 'assistant', text: trimmed, timestamp: Date.now() });
|
|
369
|
+
}
|
|
370
|
+
convMsgs[conv.id] = convArr.slice(-200);
|
|
371
|
+
set({ conversationMessages: convMsgs, streamingConversationId: conv.id });
|
|
372
|
+
persistJSON('groove:conversationMessages', convMsgs);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
345
375
|
}
|
|
346
376
|
|
|
347
377
|
// Tool calls → activity log (shown in streaming bar, not as chat bubbles)
|
|
@@ -379,6 +409,12 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
379
409
|
});
|
|
380
410
|
}
|
|
381
411
|
|
|
412
|
+
// Clear conversation streaming state
|
|
413
|
+
const exitConv = get().conversations.find((c) => c.agentId === msg.agentId);
|
|
414
|
+
if (exitConv && get().streamingConversationId === exitConv.id) {
|
|
415
|
+
set({ sendingMessage: false, streamingConversationId: null });
|
|
416
|
+
}
|
|
417
|
+
|
|
382
418
|
// Log crash error to agent chat so user can see what happened
|
|
383
419
|
if (msg.error && msg.agentId) {
|
|
384
420
|
get().addChatMessage(msg.agentId, 'system', `Crashed: ${msg.error}`);
|
|
@@ -736,6 +772,76 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
736
772
|
get().fetchBetaStatus();
|
|
737
773
|
get().fetchNetworkInstallStatus();
|
|
738
774
|
break;
|
|
775
|
+
|
|
776
|
+
case 'conversation:created': {
|
|
777
|
+
const conv = msg.data;
|
|
778
|
+
if (conv) set((s) => ({ conversations: [conv, ...s.conversations.filter((c) => c.id !== conv.id)] }));
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
case 'conversation:updated': {
|
|
783
|
+
const conv = msg.data;
|
|
784
|
+
if (conv) set((s) => ({ conversations: s.conversations.map((c) => c.id === conv.id ? { ...c, ...conv } : c) }));
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
case 'conversation:deleted': {
|
|
789
|
+
const id = msg.data?.id || msg.id;
|
|
790
|
+
if (id) {
|
|
791
|
+
set((s) => {
|
|
792
|
+
const conversations = s.conversations.filter((c) => c.id !== id);
|
|
793
|
+
const conversationMessages = { ...s.conversationMessages };
|
|
794
|
+
delete conversationMessages[id];
|
|
795
|
+
const activeConversationId = s.activeConversationId === id ? null : s.activeConversationId;
|
|
796
|
+
if (activeConversationId !== s.activeConversationId) localStorage.setItem('groove:activeConversationId', '');
|
|
797
|
+
return { conversations, conversationMessages, activeConversationId };
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
break;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
case 'conversation:chunk': {
|
|
804
|
+
const { conversationId, text } = msg.data || msg;
|
|
805
|
+
if (!conversationId || !text) break;
|
|
806
|
+
set((s) => {
|
|
807
|
+
const msgs = { ...s.conversationMessages };
|
|
808
|
+
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
809
|
+
const arr = [...msgs[conversationId]];
|
|
810
|
+
const last = arr[arr.length - 1];
|
|
811
|
+
if (last && last.from === 'assistant' && (Date.now() - last.timestamp) < 30000) {
|
|
812
|
+
arr[arr.length - 1] = { ...last, text: last.text + text, timestamp: Date.now() };
|
|
813
|
+
} else {
|
|
814
|
+
arr.push({ from: 'assistant', text, timestamp: Date.now() });
|
|
815
|
+
}
|
|
816
|
+
msgs[conversationId] = arr.slice(-200);
|
|
817
|
+
persistJSON('groove:conversationMessages', msgs);
|
|
818
|
+
return { conversationMessages: msgs, streamingConversationId: conversationId };
|
|
819
|
+
});
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
case 'conversation:complete': {
|
|
824
|
+
const { conversationId } = msg.data || msg;
|
|
825
|
+
if (conversationId) {
|
|
826
|
+
set({ sendingMessage: false, streamingConversationId: null });
|
|
827
|
+
persistJSON('groove:conversationMessages', get().conversationMessages);
|
|
828
|
+
}
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
case 'conversation:error': {
|
|
833
|
+
const { conversationId, error } = msg.data || msg;
|
|
834
|
+
if (conversationId) {
|
|
835
|
+
set((s) => {
|
|
836
|
+
const msgs = { ...s.conversationMessages };
|
|
837
|
+
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
838
|
+
msgs[conversationId] = [...msgs[conversationId], { from: 'system', text: `Error: ${error || 'Unknown error'}`, timestamp: Date.now() }];
|
|
839
|
+
persistJSON('groove:conversationMessages', msgs);
|
|
840
|
+
return { conversationMessages: msgs, sendingMessage: false, streamingConversationId: null };
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
739
845
|
}
|
|
740
846
|
};
|
|
741
847
|
|
|
@@ -1532,6 +1638,120 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1532
1638
|
}
|
|
1533
1639
|
},
|
|
1534
1640
|
|
|
1641
|
+
// ── Conversations (Chat view) ────────────────────────────
|
|
1642
|
+
|
|
1643
|
+
async fetchConversations() {
|
|
1644
|
+
try {
|
|
1645
|
+
const data = await api.get('/conversations');
|
|
1646
|
+
set({ conversations: data.conversations || data || [] });
|
|
1647
|
+
} catch { /* endpoint may not exist yet */ }
|
|
1648
|
+
},
|
|
1649
|
+
|
|
1650
|
+
async createConversation(provider, model, mode = 'api') {
|
|
1651
|
+
try {
|
|
1652
|
+
const conv = await api.post('/conversations', { provider, model, mode });
|
|
1653
|
+
set((s) => ({
|
|
1654
|
+
conversations: [conv, ...s.conversations.filter((c) => c.id !== conv.id)],
|
|
1655
|
+
activeConversationId: conv.id,
|
|
1656
|
+
}));
|
|
1657
|
+
localStorage.setItem('groove:activeConversationId', conv.id);
|
|
1658
|
+
return conv;
|
|
1659
|
+
} catch (err) {
|
|
1660
|
+
get().addToast('error', 'Failed to create conversation', err.message);
|
|
1661
|
+
throw err;
|
|
1662
|
+
}
|
|
1663
|
+
},
|
|
1664
|
+
|
|
1665
|
+
async setConversationMode(id, mode) {
|
|
1666
|
+
try {
|
|
1667
|
+
const conv = await api.patch(`/conversations/${encodeURIComponent(id)}`, { mode });
|
|
1668
|
+
set((s) => ({ conversations: s.conversations.map((c) => c.id === id ? { ...c, ...conv } : c) }));
|
|
1669
|
+
} catch (err) {
|
|
1670
|
+
get().addToast('error', 'Mode change failed', err.message);
|
|
1671
|
+
}
|
|
1672
|
+
},
|
|
1673
|
+
|
|
1674
|
+
async stopChatStreaming(conversationId) {
|
|
1675
|
+
try {
|
|
1676
|
+
await api.post(`/conversations/${encodeURIComponent(conversationId)}/stop`);
|
|
1677
|
+
set({ sendingMessage: false, streamingConversationId: null });
|
|
1678
|
+
} catch { /* ignore */ }
|
|
1679
|
+
},
|
|
1680
|
+
|
|
1681
|
+
async deleteConversation(id) {
|
|
1682
|
+
try {
|
|
1683
|
+
await api.delete(`/conversations/${encodeURIComponent(id)}`);
|
|
1684
|
+
set((s) => {
|
|
1685
|
+
const conversations = s.conversations.filter((c) => c.id !== id);
|
|
1686
|
+
const conversationMessages = { ...s.conversationMessages };
|
|
1687
|
+
delete conversationMessages[id];
|
|
1688
|
+
persistJSON('groove:conversationMessages', conversationMessages);
|
|
1689
|
+
const activeConversationId = s.activeConversationId === id
|
|
1690
|
+
? (conversations[0]?.id || null)
|
|
1691
|
+
: s.activeConversationId;
|
|
1692
|
+
localStorage.setItem('groove:activeConversationId', activeConversationId || '');
|
|
1693
|
+
return { conversations, conversationMessages, activeConversationId };
|
|
1694
|
+
});
|
|
1695
|
+
} catch (err) {
|
|
1696
|
+
get().addToast('error', 'Delete failed', err.message);
|
|
1697
|
+
}
|
|
1698
|
+
},
|
|
1699
|
+
|
|
1700
|
+
async renameConversation(id, title) {
|
|
1701
|
+
try {
|
|
1702
|
+
const conv = await api.patch(`/conversations/${encodeURIComponent(id)}`, { title });
|
|
1703
|
+
set((s) => ({ conversations: s.conversations.map((c) => c.id === id ? { ...c, ...conv } : c) }));
|
|
1704
|
+
} catch (err) {
|
|
1705
|
+
get().addToast('error', 'Rename failed', err.message);
|
|
1706
|
+
}
|
|
1707
|
+
},
|
|
1708
|
+
|
|
1709
|
+
async pinConversation(id, pinned) {
|
|
1710
|
+
try {
|
|
1711
|
+
const conv = await api.patch(`/conversations/${encodeURIComponent(id)}`, { pinned });
|
|
1712
|
+
set((s) => ({ conversations: s.conversations.map((c) => c.id === id ? { ...c, ...conv } : c) }));
|
|
1713
|
+
} catch (err) {
|
|
1714
|
+
get().addToast('error', 'Pin failed', err.message);
|
|
1715
|
+
}
|
|
1716
|
+
},
|
|
1717
|
+
|
|
1718
|
+
setActiveConversation(id) {
|
|
1719
|
+
set({ activeConversationId: id });
|
|
1720
|
+
localStorage.setItem('groove:activeConversationId', id || '');
|
|
1721
|
+
},
|
|
1722
|
+
|
|
1723
|
+
async sendChatMessage(conversationId, message) {
|
|
1724
|
+
const conv = get().conversations.find((c) => c.id === conversationId);
|
|
1725
|
+
if (!conv) return;
|
|
1726
|
+
|
|
1727
|
+
// Add user message to local state immediately
|
|
1728
|
+
set((s) => {
|
|
1729
|
+
const msgs = { ...s.conversationMessages };
|
|
1730
|
+
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
1731
|
+
msgs[conversationId] = [...msgs[conversationId], { from: 'user', text: message, timestamp: Date.now() }];
|
|
1732
|
+
persistJSON('groove:conversationMessages', msgs);
|
|
1733
|
+
return { conversationMessages: msgs, sendingMessage: true, streamingConversationId: conversationId };
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
try {
|
|
1737
|
+
const body = { message };
|
|
1738
|
+
if (conv.mode === 'api' || !conv.mode) {
|
|
1739
|
+
const history = get().conversationMessages[conversationId] || [];
|
|
1740
|
+
body.history = history.slice(0, -1);
|
|
1741
|
+
}
|
|
1742
|
+
await api.post(`/conversations/${encodeURIComponent(conversationId)}/message`, body);
|
|
1743
|
+
} catch (err) {
|
|
1744
|
+
set((s) => {
|
|
1745
|
+
const msgs = { ...s.conversationMessages };
|
|
1746
|
+
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
1747
|
+
msgs[conversationId] = [...msgs[conversationId], { from: 'system', text: `Failed: ${err.message}`, timestamp: Date.now() }];
|
|
1748
|
+
persistJSON('groove:conversationMessages', msgs);
|
|
1749
|
+
return { conversationMessages: msgs, sendingMessage: false, streamingConversationId: null };
|
|
1750
|
+
});
|
|
1751
|
+
get().addToast('error', 'Message failed', err.message);
|
|
1752
|
+
}
|
|
1753
|
+
},
|
|
1754
|
+
|
|
1535
1755
|
// ── Editor ────────────────────────────────────────────────
|
|
1536
1756
|
|
|
1537
1757
|
async openFile(path) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.57",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -810,6 +810,175 @@ export function createApi(app, daemon) {
|
|
|
810
810
|
}
|
|
811
811
|
});
|
|
812
812
|
|
|
813
|
+
// --- Conversations ---
|
|
814
|
+
|
|
815
|
+
app.get('/api/conversations', (req, res) => {
|
|
816
|
+
res.json({ conversations: daemon.conversations.list() });
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
app.post('/api/conversations', async (req, res) => {
|
|
820
|
+
try {
|
|
821
|
+
const { provider, model, title, mode } = req.body;
|
|
822
|
+
if (!provider || typeof provider !== 'string') {
|
|
823
|
+
return res.status(400).json({ error: 'provider is required' });
|
|
824
|
+
}
|
|
825
|
+
if (mode && mode !== 'api' && mode !== 'agent') {
|
|
826
|
+
return res.status(400).json({ error: 'mode must be "api" or "agent"' });
|
|
827
|
+
}
|
|
828
|
+
const conversation = await daemon.conversations.create(provider, model, title, mode || 'api');
|
|
829
|
+
daemon.audit.log('conversation.create', { id: conversation.id, provider, model, mode: conversation.mode });
|
|
830
|
+
res.status(201).json(conversation);
|
|
831
|
+
} catch (err) {
|
|
832
|
+
res.status(400).json({ error: err.message });
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
app.get('/api/conversations/:id', (req, res) => {
|
|
837
|
+
const conversation = daemon.conversations.get(req.params.id);
|
|
838
|
+
if (!conversation) return res.status(404).json({ error: 'Conversation not found' });
|
|
839
|
+
res.json(conversation);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
app.patch('/api/conversations/:id', async (req, res) => {
|
|
843
|
+
try {
|
|
844
|
+
const conv = daemon.conversations.get(req.params.id);
|
|
845
|
+
if (!conv) return res.status(404).json({ error: 'Conversation not found' });
|
|
846
|
+
if (req.body.title !== undefined) daemon.conversations.rename(req.params.id, req.body.title);
|
|
847
|
+
if (req.body.pinned !== undefined) daemon.conversations.pin(req.params.id, req.body.pinned);
|
|
848
|
+
if (req.body.archived !== undefined) daemon.conversations.archive(req.params.id, req.body.archived);
|
|
849
|
+
if (req.body.mode !== undefined) {
|
|
850
|
+
if (req.body.mode !== 'api' && req.body.mode !== 'agent') {
|
|
851
|
+
return res.status(400).json({ error: 'mode must be "api" or "agent"' });
|
|
852
|
+
}
|
|
853
|
+
await daemon.conversations.setMode(req.params.id, req.body.mode);
|
|
854
|
+
}
|
|
855
|
+
daemon.audit.log('conversation.update', { id: req.params.id, mode: req.body.mode });
|
|
856
|
+
res.json(daemon.conversations.get(req.params.id));
|
|
857
|
+
} catch (err) {
|
|
858
|
+
res.status(400).json({ error: err.message });
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
app.delete('/api/conversations/:id', async (req, res) => {
|
|
863
|
+
try {
|
|
864
|
+
const conv = daemon.conversations.get(req.params.id);
|
|
865
|
+
if (!conv) return res.status(404).json({ error: 'Conversation not found' });
|
|
866
|
+
await daemon.conversations.delete(req.params.id);
|
|
867
|
+
daemon.audit.log('conversation.delete', { id: req.params.id });
|
|
868
|
+
res.json({ ok: true });
|
|
869
|
+
} catch (err) {
|
|
870
|
+
res.status(400).json({ error: err.message });
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
app.post('/api/conversations/:id/message', async (req, res) => {
|
|
875
|
+
try {
|
|
876
|
+
const { message, history } = req.body;
|
|
877
|
+
if (!message || typeof message !== 'string' || !message.trim()) {
|
|
878
|
+
return res.status(400).json({ error: 'message is required' });
|
|
879
|
+
}
|
|
880
|
+
const conv = daemon.conversations.get(req.params.id);
|
|
881
|
+
if (!conv) return res.status(404).json({ error: 'Conversation not found' });
|
|
882
|
+
|
|
883
|
+
daemon.conversations.autoTitle(req.params.id, message.trim());
|
|
884
|
+
daemon.conversations.touchUpdatedAt(req.params.id);
|
|
885
|
+
|
|
886
|
+
// API mode — lightweight headless streaming, no agent spawned
|
|
887
|
+
if (conv.mode === 'api' || !conv.agentId) {
|
|
888
|
+
await daemon.conversations.sendMessage(req.params.id, message.trim(), history || []);
|
|
889
|
+
daemon.audit.log('conversation.message', { id: req.params.id, mode: 'api' });
|
|
890
|
+
return res.json({ status: 'streaming', mode: 'api' });
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Agent mode — existing behavior
|
|
894
|
+
const agent = daemon.registry.get(conv.agentId);
|
|
895
|
+
if (!agent) return res.status(400).json({ error: 'Agent no longer exists' });
|
|
896
|
+
|
|
897
|
+
// Record user feedback for journalist context
|
|
898
|
+
if (daemon.journalist) daemon.journalist.recordUserFeedback(agent, message.trim());
|
|
899
|
+
|
|
900
|
+
// Agent loop path — send message directly to the running loop
|
|
901
|
+
if (daemon.processes.hasAgentLoop(conv.agentId)) {
|
|
902
|
+
const sent = await daemon.processes.sendMessage(conv.agentId, message.trim());
|
|
903
|
+
if (sent) {
|
|
904
|
+
daemon.audit.log('conversation.message', { id: req.params.id, agentId: conv.agentId });
|
|
905
|
+
return res.json({ id: conv.agentId, status: 'message_sent' });
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// One-shot providers: kill and respawn with the message as prompt
|
|
910
|
+
const provider = getProvider(agent.provider);
|
|
911
|
+
if (provider?.constructor?.isOneShot) {
|
|
912
|
+
const oldConfig = { ...agent };
|
|
913
|
+
if (daemon.processes.isRunning(conv.agentId)) {
|
|
914
|
+
await daemon.processes.kill(conv.agentId);
|
|
915
|
+
}
|
|
916
|
+
daemon.registry.remove(conv.agentId);
|
|
917
|
+
daemon.locks.release(conv.agentId);
|
|
918
|
+
|
|
919
|
+
const newAgent = await daemon.processes.spawn({
|
|
920
|
+
role: 'chat',
|
|
921
|
+
scope: oldConfig.scope,
|
|
922
|
+
provider: oldConfig.provider,
|
|
923
|
+
model: oldConfig.model,
|
|
924
|
+
prompt: message.trim(),
|
|
925
|
+
permission: oldConfig.permission || 'full',
|
|
926
|
+
workingDir: oldConfig.workingDir,
|
|
927
|
+
name: oldConfig.name,
|
|
928
|
+
teamId: oldConfig.teamId,
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
// Update conversation to point to new agent
|
|
932
|
+
const convObj = daemon.conversations.conversations.get(req.params.id);
|
|
933
|
+
if (convObj) {
|
|
934
|
+
convObj.agentId = newAgent.id;
|
|
935
|
+
daemon.conversations._save();
|
|
936
|
+
}
|
|
937
|
+
daemon.audit.log('conversation.message', { id: req.params.id, agentId: newAgent.id, oneShot: true });
|
|
938
|
+
return res.json({ id: newAgent.id, status: 'respawned' });
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Running CLI agent — queue the message
|
|
942
|
+
if (daemon.processes.isRunning(conv.agentId)) {
|
|
943
|
+
daemon.processes.queueMessage(conv.agentId, message.trim());
|
|
944
|
+
daemon.audit.log('conversation.message', { id: req.params.id, agentId: conv.agentId, queued: true });
|
|
945
|
+
return res.json({ id: conv.agentId, status: 'message_queued' });
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// CLI agent — session resume or rotation
|
|
949
|
+
const SESSION_RESUME_CEILING = 5_000_000;
|
|
950
|
+
const resumed = !!agent.sessionId && (agent.tokensUsed || 0) < SESSION_RESUME_CEILING;
|
|
951
|
+
const newAgent = resumed
|
|
952
|
+
? await daemon.processes.resume(conv.agentId, message.trim())
|
|
953
|
+
: await daemon.rotator.rotate(conv.agentId, { additionalPrompt: message.trim() });
|
|
954
|
+
|
|
955
|
+
// Update conversation to point to new agent if rotated
|
|
956
|
+
if (newAgent.id !== conv.agentId) {
|
|
957
|
+
const convObj = daemon.conversations.conversations.get(req.params.id);
|
|
958
|
+
if (convObj) {
|
|
959
|
+
convObj.agentId = newAgent.id;
|
|
960
|
+
daemon.conversations._save();
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
daemon.audit.log('conversation.message', { id: req.params.id, agentId: newAgent.id, resumed });
|
|
965
|
+
res.json({ id: newAgent.id, status: resumed ? 'resumed' : 'rotated' });
|
|
966
|
+
} catch (err) {
|
|
967
|
+
res.status(400).json({ error: err.message });
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
app.post('/api/conversations/:id/stop', (req, res) => {
|
|
972
|
+
try {
|
|
973
|
+
const conv = daemon.conversations.get(req.params.id);
|
|
974
|
+
if (!conv) return res.status(404).json({ error: 'Conversation not found' });
|
|
975
|
+
daemon.conversations.stopStreaming(req.params.id);
|
|
976
|
+
res.json({ ok: true });
|
|
977
|
+
} catch (err) {
|
|
978
|
+
res.status(400).json({ error: err.message });
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
|
|
813
982
|
// --- Approvals ---
|
|
814
983
|
|
|
815
984
|
app.get('/api/approvals', (req, res) => {
|