groove-dev 0.26.31 → 0.26.33
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/node_modules/@groove-dev/daemon/src/api.js +27 -3
- package/node_modules/@groove-dev/daemon/src/introducer.js +13 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +146 -28
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +3 -3
- package/node_modules/@groove-dev/gui/dist/assets/{index-vxioP1y2.js → index-CPF9iasK.js} +12 -12
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +6 -5
- package/package.json +1 -1
- package/packages/daemon/src/api.js +27 -3
- package/packages/daemon/src/introducer.js +13 -0
- package/packages/daemon/src/journalist.js +146 -28
- package/packages/gui/dist/assets/{index-vxioP1y2.js → index-CPF9iasK.js} +12 -12
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +6 -5
|
@@ -484,6 +484,9 @@ export function createApi(app, daemon) {
|
|
|
484
484
|
const agent = daemon.registry.get(req.params.id);
|
|
485
485
|
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
486
486
|
|
|
487
|
+
// Record user feedback so the journalist can include it in future agent context
|
|
488
|
+
if (daemon.journalist) daemon.journalist.recordUserFeedback(agent, message.trim());
|
|
489
|
+
|
|
487
490
|
// Agent loop path — send message directly to the running loop
|
|
488
491
|
if (daemon.processes.hasAgentLoop(req.params.id)) {
|
|
489
492
|
const sent = await daemon.processes.sendMessage(req.params.id, message.trim());
|
|
@@ -1822,14 +1825,35 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1822
1825
|
);
|
|
1823
1826
|
|
|
1824
1827
|
if (existing && prompt) {
|
|
1825
|
-
//
|
|
1828
|
+
// Reuse existing agent: kill the old process and spawn fresh with full context.
|
|
1829
|
+
// This ensures the agent gets intro context, project map, and design system —
|
|
1830
|
+
// resume() bypasses all of that and the agent spawns blind.
|
|
1826
1831
|
try {
|
|
1827
|
-
|
|
1832
|
+
// Kill old process if running
|
|
1833
|
+
if (existing.status === 'running' || existing.status === 'starting') {
|
|
1834
|
+
try { await daemon.processes.kill(existing.id); } catch { /* already dead */ }
|
|
1835
|
+
}
|
|
1836
|
+
// Remove old entry
|
|
1837
|
+
daemon.registry.remove(existing.id);
|
|
1838
|
+
daemon.locks.release(existing.id);
|
|
1839
|
+
|
|
1840
|
+
// Spawn fresh with the same name/team but new prompt + full context
|
|
1841
|
+
const validated = validateAgentConfig({
|
|
1842
|
+
role: existing.role,
|
|
1843
|
+
scope: config.scope || existing.scope || [],
|
|
1844
|
+
prompt,
|
|
1845
|
+
provider: config.provider || existing.provider || undefined,
|
|
1846
|
+
model: config.model || existing.model || 'auto',
|
|
1847
|
+
permission: config.permission || existing.permission || 'auto',
|
|
1848
|
+
workingDir: existing.workingDir || projectWorkingDir,
|
|
1849
|
+
name: existing.name,
|
|
1850
|
+
});
|
|
1851
|
+
validated.teamId = defaultTeamId;
|
|
1852
|
+
const newAgent = await daemon.processes.spawn(validated);
|
|
1828
1853
|
reused.push({ id: newAgent.id, name: newAgent.name, role: newAgent.role, reusedFrom: existing.name });
|
|
1829
1854
|
phase1Ids.push(newAgent.id);
|
|
1830
1855
|
daemon.audit.log('team.reuse', { oldId: existing.id, newId: newAgent.id, role: config.role });
|
|
1831
1856
|
} catch (err) {
|
|
1832
|
-
// Reuse failed — fall through to spawn
|
|
1833
1857
|
failed.push({ role: config.role, error: `reuse failed: ${err.message}` });
|
|
1834
1858
|
}
|
|
1835
1859
|
} else {
|
|
@@ -85,6 +85,19 @@ export class Introducer {
|
|
|
85
85
|
lines.push(` GROOVE will automatically wake the target agent and deliver your request.`);
|
|
86
86
|
lines.push(`- Check AGENTS_REGISTRY.md for the latest team state.`);
|
|
87
87
|
|
|
88
|
+
// User feedback from previous tasks — critical context about what the user
|
|
89
|
+
// observed and what needs to change. Prevents agents from repeating mistakes.
|
|
90
|
+
const feedback = this.daemon.journalist?.getUserFeedback() || [];
|
|
91
|
+
if (feedback.length > 0) {
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push(`## User Feedback (from previous tasks)`);
|
|
94
|
+
lines.push('');
|
|
95
|
+
lines.push(`The user sent these messages about previous agents' work. Pay close attention — these indicate issues that previous agents missed:`);
|
|
96
|
+
for (const fb of feedback.slice(-10)) {
|
|
97
|
+
lines.push(`- **${fb.agentName}** (${fb.role}): "${fb.message}"`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
88
101
|
// Project files section — tell the new agent what exists and what to read
|
|
89
102
|
if (allTeamFiles.length > 0) {
|
|
90
103
|
lines.push('');
|
|
@@ -57,9 +57,18 @@ export class Journalist {
|
|
|
57
57
|
if (this.synthesizing) return; // Don't overlap
|
|
58
58
|
|
|
59
59
|
const agents = this.daemon.registry.getAll();
|
|
60
|
-
const
|
|
60
|
+
const running = agents.filter((a) => a.status === 'running');
|
|
61
|
+
|
|
62
|
+
// Include recently completed agents (last 30 min) so their work persists
|
|
63
|
+
// in the project map instead of vanishing the moment they finish
|
|
64
|
+
const thirtyMinAgo = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
65
|
+
const recentlyCompleted = agents.filter((a) =>
|
|
66
|
+
a.status === 'completed' && a.lastActivity && a.lastActivity > thirtyMinAgo
|
|
67
|
+
&& !running.some((r) => r.id === a.id)
|
|
68
|
+
);
|
|
69
|
+
const activeAgents = [...running, ...recentlyCompleted];
|
|
61
70
|
|
|
62
|
-
// Skip if no
|
|
71
|
+
// Skip if no agents to synthesize
|
|
63
72
|
if (activeAgents.length === 0) return;
|
|
64
73
|
|
|
65
74
|
// Smart scheduling: skip if no new log output since last cycle
|
|
@@ -149,33 +158,75 @@ export class Journalist {
|
|
|
149
158
|
}
|
|
150
159
|
|
|
151
160
|
filterLog(rawLog, agent) {
|
|
152
|
-
// Parse stream-json lines and extract meaningful events
|
|
161
|
+
// Parse stream-json lines and extract meaningful events.
|
|
162
|
+
// Focus on PROGRESS (writes, edits, commands with results) not EXPLORATION (reads, greps).
|
|
153
163
|
const entries = [];
|
|
154
164
|
const lines = rawLog.split('\n');
|
|
165
|
+
const toolResults = new Map(); // tool_use_id -> result text
|
|
155
166
|
|
|
167
|
+
// First pass: collect tool results so we can attach them to tool calls
|
|
168
|
+
for (const line of lines) {
|
|
169
|
+
if (!line.trim() || line.startsWith('[')) continue;
|
|
170
|
+
try {
|
|
171
|
+
const data = JSON.parse(line);
|
|
172
|
+
if (data.type === 'user' && data.message?.content) {
|
|
173
|
+
const content = Array.isArray(data.message.content) ? data.message.content : [];
|
|
174
|
+
for (const block of content) {
|
|
175
|
+
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
176
|
+
const text = typeof block.content === 'string' ? block.content
|
|
177
|
+
: Array.isArray(block.content) ? block.content.map((c) => c.text || '').join('').slice(0, 300)
|
|
178
|
+
: '';
|
|
179
|
+
if (text) toolResults.set(block.tool_use_id, text);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} catch { /* skip */ }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Second pass: extract meaningful events
|
|
156
187
|
for (const line of lines) {
|
|
157
|
-
// Skip empty lines and GROOVE spawn headers
|
|
158
188
|
if (!line.trim() || line.startsWith('[')) continue;
|
|
159
189
|
|
|
160
190
|
try {
|
|
161
191
|
const data = JSON.parse(line);
|
|
162
192
|
|
|
163
|
-
// Tool use
|
|
193
|
+
// Tool use — only keep WRITES, EDITS, and COMMANDS (progress, not exploration)
|
|
164
194
|
if (data.type === 'assistant' && data.message?.content) {
|
|
165
195
|
const content = data.message.content;
|
|
166
196
|
if (Array.isArray(content)) {
|
|
167
197
|
for (const block of content) {
|
|
168
198
|
if (block.type === 'tool_use') {
|
|
169
|
-
|
|
199
|
+
const tool = block.name;
|
|
200
|
+
|
|
201
|
+
// Skip exploration tools — reads/searches are noise for synthesis
|
|
202
|
+
if (tool === 'Read' || tool === 'Glob' || tool === 'Grep') continue;
|
|
203
|
+
|
|
204
|
+
const entry = {
|
|
170
205
|
type: 'tool',
|
|
171
|
-
tool
|
|
172
|
-
input: this.summarizeToolInput(
|
|
206
|
+
tool,
|
|
207
|
+
input: this.summarizeToolInput(tool, block.input),
|
|
173
208
|
timestamp: data.timestamp,
|
|
174
|
-
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Capture what was actually changed for Edit operations
|
|
212
|
+
if (tool === 'Edit' && block.input) {
|
|
213
|
+
const old = (block.input.old_string || '').slice(0, 150);
|
|
214
|
+
const nw = (block.input.new_string || '').slice(0, 150);
|
|
215
|
+
if (old && nw) entry.diff = `"${old}" → "${nw}"`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Capture Bash command output (exit code, first line of result)
|
|
219
|
+
if (tool === 'Bash' && block.id) {
|
|
220
|
+
const result = toolResults.get(block.id);
|
|
221
|
+
if (result) entry.output = result.slice(0, 200);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
entries.push(entry);
|
|
175
225
|
} else if (block.type === 'text' && block.text) {
|
|
176
|
-
// Only keep substantial
|
|
226
|
+
// Only keep substantial reasoning (decisions, conclusions)
|
|
227
|
+
// Short fragments like "Let me check..." are noise
|
|
177
228
|
const text = block.text.trim();
|
|
178
|
-
if (text.length >
|
|
229
|
+
if (text.length > 200) {
|
|
179
230
|
entries.push({ type: 'thinking', text: text.slice(0, 2000), timestamp: data.timestamp });
|
|
180
231
|
}
|
|
181
232
|
}
|
|
@@ -194,10 +245,8 @@ export class Journalist {
|
|
|
194
245
|
});
|
|
195
246
|
}
|
|
196
247
|
} catch {
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
entries.push({ type: 'error', text: line.trim().slice(0, 200) });
|
|
200
|
-
}
|
|
248
|
+
// Skip non-JSON lines entirely — transient stderr like ENOENT
|
|
249
|
+
// causes degradation when agents try to "fix" phantom errors
|
|
201
250
|
}
|
|
202
251
|
}
|
|
203
252
|
|
|
@@ -208,15 +257,11 @@ export class Journalist {
|
|
|
208
257
|
if (!input) return '';
|
|
209
258
|
switch (toolName) {
|
|
210
259
|
case 'Write':
|
|
211
|
-
case 'Edit':
|
|
212
260
|
return input.file_path || input.path || '';
|
|
213
|
-
case '
|
|
261
|
+
case 'Edit':
|
|
214
262
|
return input.file_path || input.path || '';
|
|
215
263
|
case 'Bash':
|
|
216
|
-
return (input.command || '').slice(0,
|
|
217
|
-
case 'Glob':
|
|
218
|
-
case 'Grep':
|
|
219
|
-
return input.pattern || '';
|
|
264
|
+
return (input.command || '').slice(0, 150);
|
|
220
265
|
default:
|
|
221
266
|
return JSON.stringify(input).slice(0, 100);
|
|
222
267
|
}
|
|
@@ -295,14 +340,16 @@ export class Journalist {
|
|
|
295
340
|
|
|
296
341
|
formatEntry(entry) {
|
|
297
342
|
switch (entry.type) {
|
|
298
|
-
case 'tool':
|
|
299
|
-
|
|
343
|
+
case 'tool': {
|
|
344
|
+
let line = `- [${entry.tool}] ${entry.input}`;
|
|
345
|
+
if (entry.diff) line += ` — changed: ${entry.diff}`;
|
|
346
|
+
if (entry.output) line += ` → ${entry.output.split('\n')[0]}`;
|
|
347
|
+
return line;
|
|
348
|
+
}
|
|
300
349
|
case 'thinking':
|
|
301
|
-
return `- [thought] ${entry.text.slice(0,
|
|
350
|
+
return `- [thought] ${entry.text.slice(0, 300)}`;
|
|
302
351
|
case 'result':
|
|
303
|
-
return `- [result] ${entry.text.slice(0,
|
|
304
|
-
case 'error':
|
|
305
|
-
return `- [error] ${entry.text}`;
|
|
352
|
+
return `- [result] ${entry.text.slice(0, 300)} (${entry.turns} turns, ${entry.duration}ms)`;
|
|
306
353
|
default:
|
|
307
354
|
return `- [${entry.type}] ${entry.text || ''}`;
|
|
308
355
|
}
|
|
@@ -515,8 +562,25 @@ export class Journalist {
|
|
|
515
562
|
existing = readFileSync(path, 'utf8').replace(/^# GROOVE Decisions Log[\s\S]*?\n\n/, '');
|
|
516
563
|
}
|
|
517
564
|
|
|
565
|
+
// Deduplicate — skip if the new decisions are substantially similar to the most recent entry.
|
|
566
|
+
// Extract the last cycle's decisions text for comparison.
|
|
567
|
+
if (existing) {
|
|
568
|
+
const lastEntry = existing.match(/^## Cycle[\s\S]*?\n\n([\s\S]*?)(?=\n---|\n\n\*Auto|\n## Cycle|$)/);
|
|
569
|
+
if (lastEntry) {
|
|
570
|
+
const lastText = lastEntry[1].trim().toLowerCase().replace(/\s+/g, ' ');
|
|
571
|
+
const newText = decisions.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
572
|
+
// If >60% of the new text appears in the last entry, skip (near-duplicate)
|
|
573
|
+
const newWords = newText.split(' ').filter((w) => w.length > 3);
|
|
574
|
+
const overlap = newWords.filter((w) => lastText.includes(w)).length;
|
|
575
|
+
if (newWords.length > 0 && overlap / newWords.length > 0.6) return;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
518
579
|
const entry = `## Cycle ${this.cycleCount} — ${new Date().toISOString()}\n\n${decisions}\n\n`;
|
|
519
|
-
|
|
580
|
+
|
|
581
|
+
// Keep last 20 entries max to prevent unbounded growth
|
|
582
|
+
const entries = (entry + existing).split(/(?=^## Cycle)/m).slice(0, 20).join('');
|
|
583
|
+
writeFileSync(path, header + entries);
|
|
520
584
|
}
|
|
521
585
|
|
|
522
586
|
writeAgentSessionLogs(agents, filteredLogs) {
|
|
@@ -724,6 +788,60 @@ export class Journalist {
|
|
|
724
788
|
}
|
|
725
789
|
}
|
|
726
790
|
|
|
791
|
+
// --- User Feedback Tracking ---
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Record user feedback/messages sent to agents.
|
|
795
|
+
* This gets included in the project map and handoff briefs so future
|
|
796
|
+
* agents know what the user said about previous work.
|
|
797
|
+
*/
|
|
798
|
+
recordUserFeedback(agent, message) {
|
|
799
|
+
if (!this._userFeedback) this._loadFeedback();
|
|
800
|
+
if (!this._userFeedback[agent.id]) this._userFeedback[agent.id] = [];
|
|
801
|
+
this._userFeedback[agent.id].push({
|
|
802
|
+
agentName: agent.name,
|
|
803
|
+
role: agent.role,
|
|
804
|
+
message: message.slice(0, 500),
|
|
805
|
+
timestamp: new Date().toISOString(),
|
|
806
|
+
});
|
|
807
|
+
// Keep last 20 per agent
|
|
808
|
+
if (this._userFeedback[agent.id].length > 20) {
|
|
809
|
+
this._userFeedback[agent.id] = this._userFeedback[agent.id].slice(-20);
|
|
810
|
+
}
|
|
811
|
+
this._saveFeedback();
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
_loadFeedback() {
|
|
815
|
+
const p = resolve(this.daemon.grooveDir, 'user-feedback.json');
|
|
816
|
+
try {
|
|
817
|
+
if (existsSync(p)) this._userFeedback = JSON.parse(readFileSync(p, 'utf8'));
|
|
818
|
+
else this._userFeedback = {};
|
|
819
|
+
} catch { this._userFeedback = {}; }
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
_saveFeedback() {
|
|
823
|
+
try {
|
|
824
|
+
writeFileSync(resolve(this.daemon.grooveDir, 'user-feedback.json'), JSON.stringify(this._userFeedback, null, 2));
|
|
825
|
+
} catch { /* non-fatal */ }
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Get user feedback for an agent (or all agents in a team).
|
|
830
|
+
* Used by the introducer to give agents context about previous attempts.
|
|
831
|
+
*/
|
|
832
|
+
getUserFeedback(agentOrTeamId) {
|
|
833
|
+
if (!this._userFeedback) this._loadFeedback();
|
|
834
|
+
if (!this._userFeedback) return [];
|
|
835
|
+
// Check if it's an agent ID
|
|
836
|
+
if (this._userFeedback[agentOrTeamId]) return this._userFeedback[agentOrTeamId];
|
|
837
|
+
// Check by team — collect feedback for all agents in the team
|
|
838
|
+
const all = [];
|
|
839
|
+
for (const [, entries] of Object.entries(this._userFeedback)) {
|
|
840
|
+
all.push(...entries);
|
|
841
|
+
}
|
|
842
|
+
return all.sort((a, b) => a.timestamp.localeCompare(b.timestamp)).slice(-30);
|
|
843
|
+
}
|
|
844
|
+
|
|
727
845
|
// --- Accessors ---
|
|
728
846
|
|
|
729
847
|
getLastSynthesis() {
|
|
@@ -52,13 +52,13 @@ describe('Journalist', () => {
|
|
|
52
52
|
assert.equal(entries[0].input, 'src/api/auth.js');
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
it('should
|
|
55
|
+
it('should skip non-JSON lines (transient errors are noise)', () => {
|
|
56
56
|
const { daemon } = createMockDaemon();
|
|
57
57
|
const journalist = new Journalist(daemon);
|
|
58
58
|
|
|
59
|
+
// Non-JSON error lines are dropped to prevent context degradation
|
|
59
60
|
const entries = journalist.filterLog('Error: something broke\nTypeError: undefined is not a function', {});
|
|
60
|
-
assert.equal(entries.length,
|
|
61
|
-
assert.equal(entries[0].type, 'error');
|
|
61
|
+
assert.equal(entries.length, 0);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
it('should extract result events', () => {
|