groove-dev 0.27.182 → 0.27.184

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.
Files changed (57) hide show
  1. package/CLAUDE.md +0 -7
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/deliver.js +130 -0
  5. package/node_modules/@groove-dev/daemon/src/index.js +7 -3
  6. package/node_modules/@groove-dev/daemon/src/innerchat-docs.js +41 -0
  7. package/node_modules/@groove-dev/daemon/src/innerchat.js +267 -60
  8. package/node_modules/@groove-dev/daemon/src/introducer.js +30 -1
  9. package/node_modules/@groove-dev/daemon/src/process.js +22 -1
  10. package/node_modules/@groove-dev/daemon/src/registry.js +5 -1
  11. package/node_modules/@groove-dev/daemon/src/rename.js +72 -0
  12. package/node_modules/@groove-dev/daemon/src/routes/agents.js +22 -100
  13. package/node_modules/@groove-dev/daemon/src/routes/innerchat.js +63 -16
  14. package/node_modules/@groove-dev/daemon/src/routes/teams.js +11 -0
  15. package/node_modules/@groove-dev/daemon/src/teams.js +35 -1
  16. package/node_modules/@groove-dev/daemon/test/innerchat.test.js +193 -134
  17. package/node_modules/@groove-dev/daemon/test/rename.test.js +108 -0
  18. package/node_modules/@groove-dev/daemon/test/teams.test.js +53 -0
  19. package/node_modules/@groove-dev/gui/dist/assets/{index-CTer01Vg.js → index-BpOyN6Zf.js} +227 -227
  20. package/node_modules/@groove-dev/gui/dist/assets/index-DiXB7yry.css +1 -0
  21. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  22. package/node_modules/@groove-dev/gui/package.json +1 -1
  23. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +2 -95
  24. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +3 -1
  25. package/node_modules/@groove-dev/gui/src/components/fleet/fleet-agent-row.jsx +18 -2
  26. package/node_modules/@groove-dev/gui/src/components/fleet/fleet-sidebar.jsx +56 -6
  27. package/node_modules/@groove-dev/gui/src/stores/groove.js +35 -44
  28. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +27 -4
  29. package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +22 -0
  30. package/package.json +1 -1
  31. package/packages/cli/package.json +1 -1
  32. package/packages/daemon/package.json +1 -1
  33. package/packages/daemon/src/deliver.js +130 -0
  34. package/packages/daemon/src/index.js +7 -3
  35. package/packages/daemon/src/innerchat-docs.js +41 -0
  36. package/packages/daemon/src/innerchat.js +267 -60
  37. package/packages/daemon/src/introducer.js +30 -1
  38. package/packages/daemon/src/process.js +22 -1
  39. package/packages/daemon/src/registry.js +5 -1
  40. package/packages/daemon/src/rename.js +72 -0
  41. package/packages/daemon/src/routes/agents.js +22 -100
  42. package/packages/daemon/src/routes/innerchat.js +63 -16
  43. package/packages/daemon/src/routes/teams.js +11 -0
  44. package/packages/daemon/src/teams.js +35 -1
  45. package/packages/gui/dist/assets/{index-CTer01Vg.js → index-BpOyN6Zf.js} +227 -227
  46. package/packages/gui/dist/assets/index-DiXB7yry.css +1 -0
  47. package/packages/gui/dist/index.html +2 -2
  48. package/packages/gui/package.json +1 -1
  49. package/packages/gui/src/components/agents/agent-panel.jsx +2 -95
  50. package/packages/gui/src/components/chat/chat-messages.jsx +3 -1
  51. package/packages/gui/src/components/fleet/fleet-agent-row.jsx +18 -2
  52. package/packages/gui/src/components/fleet/fleet-sidebar.jsx +56 -6
  53. package/packages/gui/src/stores/groove.js +35 -44
  54. package/packages/gui/src/stores/slices/agents-slice.js +27 -4
  55. package/packages/gui/src/stores/slices/teams-slice.js +22 -0
  56. package/node_modules/@groove-dev/gui/dist/assets/index-DTFtRtkx.css +0 -1
  57. package/packages/gui/dist/assets/index-DTFtRtkx.css +0 -1
@@ -369,54 +369,39 @@ export const useGrooveStore = create((set, get) => ({
369
369
  break;
370
370
  }
371
371
 
372
- case 'innerchat:sent': {
373
- const ic = msg.data;
374
- get().addChatMessage(ic.to.id, 'innerchat', ic.message, false);
375
- set((s) => {
376
- const icMsgs = { ...s.innerchatMessages };
377
- icMsgs[ic.id] = ic;
378
- persistJSON('groove:innerchatMessages', icMsgs);
379
- return { innerchatMessages: icMsgs };
380
- });
381
- // Tag the chat message with innerchat metadata
382
- set((s) => {
383
- const history = { ...s.chatHistory };
384
- const arr = history[ic.to.id] || [];
385
- if (arr.length > 0) {
386
- const last = arr[arr.length - 1];
387
- if (last.from === 'innerchat' && last.text === ic.message) {
388
- arr[arr.length - 1] = { ...last, innerchat: { messageId: ic.id, fromAgent: ic.from } };
389
- history[ic.to.id] = arr;
390
- persistJSON('groove:chatHistory', history);
391
- return { chatHistory: history };
392
- }
393
- }
394
- return {};
395
- });
396
- break;
397
- }
372
+ case 'innerchat:turn': {
373
+ const { thread, turn } = msg.data;
374
+ if (turn.status === 'failed') {
375
+ get().addToast('error', `Relay to ${turn.to.name} failed`, turn.error);
376
+ }
398
377
 
399
- case 'innerchat:response': {
400
- const ic = msg.data;
401
- get().addChatMessage(ic.from.id, 'innerchat', ic.response, false);
378
+ // The turn lands in the recipient's chat, badged with who sent it.
402
379
  set((s) => {
403
- const icMsgs = { ...s.innerchatMessages };
404
- icMsgs[ic.id] = ic;
405
- persistJSON('groove:innerchatMessages', icMsgs);
406
- // Tag the response chat message with innerchat metadata
407
380
  const history = { ...s.chatHistory };
408
- const arr = history[ic.from.id] || [];
409
- if (arr.length > 0) {
410
- const last = arr[arr.length - 1];
411
- if (last.from === 'innerchat' && last.text === ic.response) {
412
- arr[arr.length - 1] = { ...last, innerchat: { messageId: ic.id, fromAgent: ic.to } };
413
- history[ic.from.id] = arr;
414
- persistJSON('groove:chatHistory', history);
415
- }
416
- }
417
- return { innerchatMessages: icMsgs, chatHistory: history };
381
+ const arr = [...(history[turn.to.id] || [])];
382
+ arr.push({
383
+ from: 'innerchat',
384
+ text: turn.text,
385
+ timestamp: turn.timestamp,
386
+ isQuery: false,
387
+ innerchat: {
388
+ turnId: turn.id,
389
+ threadId: thread.id,
390
+ kind: turn.kind,
391
+ fromAgent: turn.from,
392
+ },
393
+ });
394
+ history[turn.to.id] = arr.slice(-100);
395
+ persistJSON('groove:chatHistory', history);
396
+
397
+ const threads = { ...s.innerchatThreads, [thread.id]: thread };
398
+ persistJSON('groove:innerchatThreads', threads);
399
+ return { chatHistory: history, innerchatThreads: threads };
418
400
  });
419
- get().addToast('info', `${ic.to.name} replied to ${ic.from.name}`, 'InnerChat response received');
401
+
402
+ if (turn.kind === 'reply' && turn.status !== 'failed') {
403
+ get().addToast('info', `${turn.from.name} replied`, `Forwarded to ${turn.to.name}`);
404
+ }
420
405
  break;
421
406
  }
422
407
 
@@ -620,6 +605,12 @@ export const useGrooveStore = create((set, get) => ({
620
605
  get().fetchTeams();
621
606
  break;
622
607
 
608
+ // Reordering already applied optimistically on the client that did it;
609
+ // this keeps every other window in sync.
610
+ case 'teams:reordered':
611
+ if (msg.teams) set({ teams: msg.teams });
612
+ break;
613
+
623
614
  case 'approval:request':
624
615
  set((s) => ({ pendingApprovals: [...s.pendingApprovals, msg.data] }));
625
616
  break;
@@ -104,20 +104,43 @@ export const createAgentsSlice = (set, get) => ({
104
104
  }
105
105
  },
106
106
 
107
+ // Move an agent to another team. Teams are an organizational grouping —
108
+ // this re-labels the agent and does NOT relocate its working directory, so a
109
+ // running process keeps working where it is.
110
+ async moveAgentToTeam(agentId, teamId) {
111
+ const agent = get().agents.find((a) => a.id === agentId);
112
+ if (!agent || agent.teamId === teamId) return;
113
+ const team = get().teams.find((t) => t.id === teamId);
114
+ try {
115
+ await api.patch(`/agents/${encodeURIComponent(agentId)}`, { teamId });
116
+ get().addToast('success', `Moved ${agent.name} → ${team?.name || 'team'}`);
117
+ } catch (err) {
118
+ get().addToast('error', 'Move failed', err.message);
119
+ }
120
+ },
121
+
107
122
  // ── InnerChat ─────────────────────────────────────────────
108
123
 
109
- innerchatMessages: loadJSON('groove:innerchatMessages') || {},
124
+ innerchatThreads: loadJSON('groove:innerchatThreads') || {},
110
125
 
111
- async sendInnerChat(fromId, toId, message) {
126
+ // Relay a message from one agent to another. Pass threadId to continue an
127
+ // existing exchange rather than opening a new one.
128
+ async sendInnerChat(fromId, toId, message, threadId = null) {
112
129
  try {
113
- const msg = await api.post('/innerchat/send', { from: fromId, to: toId, message });
114
- return msg;
130
+ return await api.post('/innerchat/send', { from: fromId, to: toId, message, threadId });
115
131
  } catch (err) {
116
132
  get().addToast('error', 'InnerChat failed', err.message);
117
133
  throw err;
118
134
  }
119
135
  },
120
136
 
137
+ // Threads this agent is a participant in, most recent first.
138
+ innerchatThreadsFor(agentId) {
139
+ return Object.values(get().innerchatThreads)
140
+ .filter((t) => t.participants.some((p) => p.id === agentId))
141
+ .sort((a, b) => b.updatedAt - a.updatedAt);
142
+ },
143
+
121
144
  // ── Chat ──────────────────────────────────────────────────
122
145
 
123
146
  addChatMessage(agentId, from, text, isQuery = false, attachments = undefined) {
@@ -173,6 +173,28 @@ export const createTeamsSlice = (set, get) => ({
173
173
  }
174
174
  },
175
175
 
176
+ // Move a team to a new index in the sidebar. Applied optimistically so the
177
+ // drop lands instantly, then reconciled with whatever the daemon persisted.
178
+ async reorderTeams(draggedId, targetId) {
179
+ const current = get().teams;
180
+ const from = current.findIndex((t) => t.id === draggedId);
181
+ const to = current.findIndex((t) => t.id === targetId);
182
+ if (from === -1 || to === -1 || from === to) return;
183
+
184
+ const next = [...current];
185
+ const [moved] = next.splice(from, 1);
186
+ next.splice(to, 0, moved);
187
+ set({ teams: next });
188
+
189
+ try {
190
+ const { teams } = await api.post('/teams/reorder', { orderedIds: next.map((t) => t.id) });
191
+ if (teams) set({ teams });
192
+ } catch (err) {
193
+ set({ teams: current });
194
+ get().addToast('error', 'Failed to reorder teams', err.message);
195
+ }
196
+ },
197
+
176
198
  async promoteTeam(id) {
177
199
  try {
178
200
  const team = await api.post(`/teams/${encodeURIComponent(id)}/promote`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.182",
3
+ "version": "0.27.184",
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)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.182",
3
+ "version": "0.27.184",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.182",
3
+ "version": "0.27.184",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -0,0 +1,130 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { wrapWithRoleReminder } from './process.js';
4
+ import { getProvider } from './providers/index.js';
5
+
6
+ // Reviving a >5M-token claude session has crashed the CLI mid-HTTP-parse
7
+ // (V8 fatal in JsonStringifier) — past this ceiling the rotator's handoff
8
+ // brief sidesteps the resume entirely.
9
+ const SESSION_RESUME_CEILING = 5_000_000;
10
+
11
+ /**
12
+ * Deliver a message to an agent, whatever state it's in.
13
+ *
14
+ * This is the single pipe every inbound message travels — user chat from the
15
+ * GUI and InnerChat relays alike. Handles agent-loop delivery, one-shot and
16
+ * non-interactive providers, queueing behind a running task, session resume,
17
+ * and full rotation when the session is too large to revive.
18
+ *
19
+ * Respawn paths mint a NEW agent id. Callers that track the agent afterwards
20
+ * (InnerChat keys response capture on it) must use the returned `agentId`,
21
+ * not the one they passed in.
22
+ *
23
+ * @returns {Promise<{agentId: string, status: string, resumed: boolean, agent: object|null}>}
24
+ */
25
+ export async function deliverInstruction(daemon, agentId, message, opts = {}) {
26
+ const { recordFeedback = true } = opts;
27
+
28
+ const agent = daemon.registry.get(agentId);
29
+ if (!agent) throw new Error('Agent not found');
30
+
31
+ const finalMessage = message.trim();
32
+ if (!finalMessage) throw new Error('message is required');
33
+
34
+ // Journalist/rotator treat this as a human steering signal — InnerChat
35
+ // relays opt out so agent-to-agent chatter doesn't pollute user feedback.
36
+ if (recordFeedback) {
37
+ if (daemon.journalist) daemon.journalist.recordUserFeedback(agent, finalMessage);
38
+ if (daemon.rotator) daemon.rotator.recordUserMessage(agentId);
39
+ }
40
+
41
+ const wrappedMessage = wrapWithRoleReminder(agent.role, finalMessage);
42
+
43
+ // Agent loop path — send straight to the running loop.
44
+ if (daemon.processes.hasAgentLoop(agentId)) {
45
+ const sent = await daemon.processes.sendMessage(agentId, wrappedMessage);
46
+ if (sent) {
47
+ daemon.audit.log('agent.chat', { id: agentId });
48
+ return { agentId, status: 'message_sent', resumed: false, agent };
49
+ }
50
+ // Loop exists but isn't running — fall through to resume/rotate.
51
+ }
52
+
53
+ const provider = getProvider(agent.provider);
54
+
55
+ // One-shot providers (groove-network): kill any running instance and respawn
56
+ // with the message as --prompt. No handoff brief, no resume, no queue.
57
+ if (provider?.constructor?.isOneShot) {
58
+ const oldConfig = { ...agent };
59
+ if (daemon.processes.isRunning(agentId)) {
60
+ await daemon.processes.kill(agentId);
61
+ }
62
+ daemon.registry.remove(agentId, { silent: true });
63
+ daemon.locks.release(agentId);
64
+
65
+ const newAgent = await respawn(daemon, {
66
+ role: oldConfig.role,
67
+ scope: oldConfig.scope,
68
+ provider: oldConfig.provider,
69
+ model: oldConfig.model,
70
+ prompt: finalMessage,
71
+ permission: oldConfig.permission || 'full',
72
+ workingDir: oldConfig.workingDir,
73
+ name: oldConfig.name,
74
+ teamId: oldConfig.teamId,
75
+ });
76
+ daemon.audit.log('agent.instruct', { id: agentId, newId: newAgent.id, resumed: false });
77
+ return { agentId: newAgent.id, status: 'respawned', resumed: false, agent: newAgent };
78
+ }
79
+
80
+ // Non-interactive CLI providers (e.g. Gemini): one prompt per spawn, cannot
81
+ // resume — respawn preserving the original introContext.
82
+ if (provider?.constructor?.nonInteractive && !daemon.processes.isRunning(agentId)) {
83
+ const oldConfig = { ...agent };
84
+ daemon.registry.remove(agentId, { silent: true });
85
+ daemon.locks.release(agentId);
86
+
87
+ const newAgent = await respawn(daemon, {
88
+ role: oldConfig.role,
89
+ scope: oldConfig.scope,
90
+ provider: oldConfig.provider,
91
+ model: oldConfig.model,
92
+ prompt: finalMessage,
93
+ introContext: oldConfig.introContext,
94
+ permission: oldConfig.permission || 'full',
95
+ workingDir: oldConfig.workingDir,
96
+ name: oldConfig.name,
97
+ teamId: oldConfig.teamId,
98
+ });
99
+ daemon.audit.log('agent.instruct', { id: agentId, newId: newAgent.id, resumed: false });
100
+ return { agentId: newAgent.id, status: 'respawned', resumed: false, agent: newAgent };
101
+ }
102
+
103
+ // Running CLI agent (no loop) — queue behind the current task rather than
104
+ // killing and respawning mid-work.
105
+ if (daemon.processes.isRunning(agentId)) {
106
+ daemon.processes.queueMessage(agentId, wrappedMessage);
107
+ daemon.audit.log('agent.chat.queued', { id: agentId });
108
+ return { agentId, status: 'message_queued', resumed: false, agent };
109
+ }
110
+
111
+ // Stopped CLI agent — resume the session, or rotate past the ceiling.
112
+ const resumed = !!agent.sessionId && (agent.tokensUsed || 0) < SESSION_RESUME_CEILING;
113
+ const newAgent = resumed
114
+ ? await daemon.processes.resume(agentId, wrappedMessage)
115
+ : await daemon.rotator.rotate(agentId, { additionalPrompt: wrappedMessage });
116
+
117
+ daemon.audit.log('agent.instruct', { id: agentId, newId: newAgent.id, resumed });
118
+ return { agentId: newAgent.id, status: resumed ? 'resumed' : 'rotated', resumed, agent: newAgent };
119
+ }
120
+
121
+ // Spawn, flushing the registry's pending removals if it fails so a failed
122
+ // respawn doesn't silently erase the agent it was replacing.
123
+ async function respawn(daemon, config) {
124
+ try {
125
+ return await daemon.processes.spawn(config);
126
+ } catch (spawnErr) {
127
+ daemon.registry.flushPendingRemovals();
128
+ throw spawnErr;
129
+ }
130
+ }
@@ -10,7 +10,7 @@ import express from 'express';
10
10
  import { WebSocketServer } from 'ws';
11
11
  import { Registry } from './registry.js';
12
12
  import { createApi } from './api.js';
13
- import { ProcessManager } from './process.js';
13
+ import { ProcessManager, sanitizeFilename } from './process.js';
14
14
  import { StateManager } from './state.js';
15
15
  import { Introducer } from './introducer.js';
16
16
  import { LockManager } from './lockmanager.js';
@@ -750,7 +750,11 @@ export class Daemon {
750
750
  try {
751
751
  // Build set of agent names still in the registry — never remove their logs
752
752
  const allAgents = this.registry.getAll();
753
- const activeNames = new Set(allAgents.map((a) => a.name));
753
+ // Raw logs are keyed by sanitized name; GROOVE_AGENT_LOGS/ subdirectories
754
+ // are keyed by agent id (journalist.js). Comparing either against the
755
+ // wrong set deletes live agents' history.
756
+ const activeNames = new Set(allAgents.map((a) => sanitizeFilename(a.name)));
757
+ const activeIds = new Set(allAgents.map((a) => a.id));
754
758
 
755
759
  // Safety: if registry is empty but log files exist, state may have been
756
760
  // lost (corrupt JSON, partial write). Skip log cleanup to prevent
@@ -778,7 +782,7 @@ export class Daemon {
778
782
  if (existsSync(agentLogsDir)) {
779
783
  for (const dir of readdirSync(agentLogsDir, { withFileTypes: true })) {
780
784
  if (!dir.isDirectory()) continue;
781
- if (activeNames.has(dir.name)) continue;
785
+ if (activeIds.has(dir.name)) continue;
782
786
  try { rmSync(resolve(agentLogsDir, dir.name), { recursive: true }); cleaned++; } catch { /* skip */ }
783
787
  }
784
788
  // Remove GROOVE_AGENT_LOGS/ itself if empty
@@ -0,0 +1,41 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ // The agent-facing InnerChat instructions, in one place.
4
+ //
5
+ // These are injected into a new agent's spawn prompt AND written into every
6
+ // AGENTS_REGISTRY.md. Both are needed: the prompt reaches the agent on turn
7
+ // one, and the file survives context compaction — an agent that has forgotten
8
+ // the prompt can re-read the registry it already knows to consult.
9
+ //
10
+ // No imports on purpose — process.js and introducer.js both pull from here.
11
+
12
+ export function innerChatInstructions(port = 31415, agentName = 'YOUR_NAME') {
13
+ return [
14
+ '## Consulting Other Agents (InnerChat)',
15
+ '',
16
+ 'Other GROOVE agents may be working alongside you, including on other teams.',
17
+ 'When the user asks you to reach out to, coordinate with, or get input from',
18
+ 'another agent, use this — it sends them a message and waits for their reply:',
19
+ '',
20
+ '```bash',
21
+ `curl -s http://localhost:${port}/api/innerchat/ask -X POST -H 'Content-Type: application/json' \\`,
22
+ ` -d '{"from":"${agentName}","to":"AGENT_NAME","message":"YOUR_QUESTION"}'`,
23
+ '```',
24
+ '',
25
+ 'The command BLOCKS until they answer, then returns:',
26
+ '`{"from":"...","reply":"...","exchangesUsed":N,"exchangesRemaining":N}`',
27
+ '',
28
+ 'Call it again to continue — prior turns are included automatically, so just say',
29
+ 'the next thing. Keep going until you reach a conclusion together, then report',
30
+ 'that conclusion to the user.',
31
+ '',
32
+ '**Rules:**',
33
+ '- Only start a conversation when the user asks you to. Never consult other agents on your own initiative.',
34
+ `- \`from\` must be your own agent name${agentName === 'YOUR_NAME' ? ' (it is in your $GROOVE_AGENT_NAME environment variable)' : ''}.`,
35
+ '- Agent names are listed in `AGENTS_REGISTRY.md`, including agents on other teams.',
36
+ '- One question per call. Wait for the answer before deciding what to ask next.',
37
+ '- Read any error you get back — it tells you what to do (unknown name, cap reached, or they are waiting on you).',
38
+ '- `exchangesRemaining` counts down. At 0 the conversation is over: stop and report what you have.',
39
+ '- If another agent asks YOU something, answer it directly in your next message. They are blocked until you do.',
40
+ ];
41
+ }