groove-dev 0.27.135 → 0.27.137

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 (31) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +32 -0
  4. package/node_modules/@groove-dev/daemon/src/journalist.js +14 -12
  5. package/node_modules/@groove-dev/daemon/src/model-lab.js +50 -67
  6. package/node_modules/@groove-dev/daemon/src/rotator.js +49 -20
  7. package/node_modules/@groove-dev/gui/dist/assets/{index-D4JZyMWH.js → index-BrZHF7pK.js} +7 -7
  8. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  9. package/node_modules/@groove-dev/gui/package.json +1 -1
  10. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +5 -1
  11. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +1 -1
  12. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +8 -8
  13. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +1 -1
  14. package/node_modules/@groove-dev/gui/src/components/lab/system-prompt-editor.jsx +3 -3
  15. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
  16. package/package.json +1 -1
  17. package/packages/cli/package.json +1 -1
  18. package/packages/daemon/package.json +1 -1
  19. package/packages/daemon/src/api.js +32 -0
  20. package/packages/daemon/src/journalist.js +14 -12
  21. package/packages/daemon/src/model-lab.js +50 -67
  22. package/packages/daemon/src/rotator.js +49 -20
  23. package/packages/gui/dist/assets/{index-D4JZyMWH.js → index-BrZHF7pK.js} +7 -7
  24. package/packages/gui/dist/index.html +1 -1
  25. package/packages/gui/package.json +1 -1
  26. package/packages/gui/src/components/agents/agent-feed.jsx +5 -1
  27. package/packages/gui/src/components/agents/agent-file-tree.jsx +1 -1
  28. package/packages/gui/src/components/editor/code-editor.jsx +8 -8
  29. package/packages/gui/src/components/editor/file-tree.jsx +1 -1
  30. package/packages/gui/src/components/lab/system-prompt-editor.jsx +3 -3
  31. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.135",
3
+ "version": "0.27.137",
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.135",
3
+ "version": "0.27.137",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -124,6 +124,38 @@ export function createApi(app, daemon) {
124
124
  res.json({ status: 'ok', uptime: process.uptime() });
125
125
  });
126
126
 
127
+ // Debug: test fetch to llama-server from daemon runtime
128
+ app.get('/api/lab/debug-fetch', async (req, res) => {
129
+ const target = req.query.url || 'http://localhost:8081/v1/chat/completions';
130
+ const log = [];
131
+ try {
132
+ log.push(`fetch → ${target}`);
133
+ log.push(`node ${process.version}, electron ${process.versions.electron || 'N/A'}`);
134
+ const start = Date.now();
135
+ const r = await fetch(target, {
136
+ method: 'POST',
137
+ headers: { 'Content-Type': 'application/json' },
138
+ body: JSON.stringify({ model: 'Qwen3-0.6B-Q8_0.gguf', messages: [{ role: 'user', content: 'Say ok' }], stream: true, max_tokens: 10 }),
139
+ signal: AbortSignal.timeout(10000),
140
+ });
141
+ log.push(`status=${r.status} in ${Date.now() - start}ms`);
142
+ const reader = r.body.getReader();
143
+ let chunks = 0;
144
+ while (chunks < 5) {
145
+ const { done, value } = await reader.read();
146
+ if (done) break;
147
+ chunks++;
148
+ log.push(`chunk ${chunks}: ${new TextDecoder().decode(value).slice(0, 120)}`);
149
+ }
150
+ reader.cancel();
151
+ log.push(`total chunks read: ${chunks}`);
152
+ res.json({ ok: true, log });
153
+ } catch (err) {
154
+ log.push(`ERROR: ${err.message}`);
155
+ res.json({ ok: false, log, error: err.message });
156
+ }
157
+ });
158
+
127
159
  // List all agents
128
160
  app.get('/api/agents', (req, res) => {
129
161
  res.json(daemon.registry.getAll());
@@ -853,9 +853,8 @@ export class Journalist {
853
853
  const agentLog = filteredLogs[agent.id];
854
854
  const entries = agentLog?.entries || [];
855
855
 
856
- // Layer 7 memory: discoveries (pointer only), constraints, specializations
857
- const hasDiscoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 1, 100);
858
- const discoveryPointer = hasDiscoveries ? 'See .groove/memory/agent-discoveries.jsonl for error→fix pairs.' : '';
856
+ // Layer 7 memory: discoveries (inline, not pointer — agents lose context with pointers), constraints, specializations
857
+ const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 10, 1500) || '';
859
858
  const constraints = this.daemon.memory?.getConstraintsMarkdown(2000) || '';
860
859
  const specialization = this.daemon.memory?.getSpecialization(agent.id);
861
860
  const specLine = specialization?.avgQualityScore != null
@@ -872,7 +871,7 @@ export class Journalist {
872
871
  const recentTools = entries
873
872
  .filter((e) => e.type === 'tool' || e.type === 'error')
874
873
  .slice(-5)
875
- .map((e) => `- ${e.type === 'error' ? 'ERROR ' : ''}${e.tool}: ${(e.input || e.text || '').slice(0, 80)}`)
874
+ .map((e) => `- ${e.type === 'error' ? 'ERROR ' : ''}${e.tool}: ${(e.input || e.text || '').slice(0, 200)}`)
876
875
  .join('\n');
877
876
 
878
877
  // Try AI-synthesized session summary
@@ -909,7 +908,7 @@ export class Journalist {
909
908
  const fallbackRecentTools = entries
910
909
  .filter((e) => e.type === 'tool' || e.type === 'error')
911
910
  .slice(-5)
912
- .map((e) => `- ${e.type === 'error' ? 'ERROR ' : ''}${e.tool}: ${(e.input || '').slice(0, 80)}`)
911
+ .map((e) => `- ${e.type === 'error' ? 'ERROR ' : ''}${e.tool}: ${(e.input || '').slice(0, 200)}`)
913
912
  .join('\n');
914
913
 
915
914
  const fallbackParts = [];
@@ -923,8 +922,8 @@ export class Journalist {
923
922
  // For quality_degradation rotations, drop user messages (already in session summary)
924
923
  const includeUserMessages = options.reason !== 'quality_degradation';
925
924
 
926
- // Cap Original Task to 500 chars
927
- const originalTask = agent.prompt ? agent.prompt.slice(0, 500) + (agent.prompt.length > 500 ? '…' : '') : '';
925
+ // Cap Original Task to 1000 chars — task descriptions for debugging can be long
926
+ const originalTask = agent.prompt ? agent.prompt.slice(0, 1000) + (agent.prompt.length > 1000 ? '…' : '') : '';
928
927
 
929
928
  let brief = [
930
929
  `# Handoff Brief — ${agent.name} (${agent.role})`,
@@ -934,10 +933,13 @@ export class Journalist {
934
933
  `Rotation: ${options.reason || 'manual'}${options.qualityScore ? ` (quality: ${options.qualityScore}/100)` : ''} | Tokens: ${agent.tokensUsed}`,
935
934
  specLine,
936
935
  ``,
937
- discoveryPointer ? `## Known Issues & Fixes\n\n${discoveryPointer}\n` : '',
936
+ // Priority order: session summary (contains unresolved errors) first,
937
+ // then constraints, then discoveries, then tools — so the most critical
938
+ // debugging context survives even if the brief hits the hard cap.
939
+ sessionSummary ? `## Session Summary\n\n${sessionSummary}\n` : '',
938
940
  constraints ? `## Project Constraints (must follow)\n\n${constraints}\n` : '',
941
+ discoveries ? `## Known Issues & Fixes\n\n${discoveries}\n` : '',
939
942
  recentTools ? `## Last 5 Tool Calls\n\n${recentTools}\n` : '',
940
- sessionSummary ? `## Session Summary\n\n${sessionSummary}\n` : '',
941
943
  includeUserMessages && conversationSummary ? `## Recent User Messages\n\n${conversationSummary}\n` : '',
942
944
  recentChain ? `## Rotation History\n\n${recentChain}\n` : '',
943
945
  originalTask ? `## Original Task\n\n${originalTask}\n` : '',
@@ -946,9 +948,9 @@ export class Journalist {
946
948
  `Continue seamlessly — finish the work and deliver the output.`,
947
949
  ].filter(Boolean).join('\n');
948
950
 
949
- // Hard cap: total brief must not exceed 5000 chars
950
- if (brief.length > 5000) {
951
- brief = brief.slice(0, 4950) + '\n\n[Brief truncated — see session logs for full context]';
951
+ // Hard cap: 8000 chars enough for debugging context without overwhelming the new agent
952
+ if (brief.length > 8000) {
953
+ brief = brief.slice(0, 7950) + '\n\n[Brief truncated — see session logs for full context]';
952
954
  }
953
955
 
954
956
  return brief;
@@ -4,6 +4,7 @@
4
4
  import { resolve } from 'path';
5
5
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'fs';
6
6
  import { randomUUID } from 'crypto';
7
+ import { Readable } from 'stream';
7
8
 
8
9
  const RUNTIME_TYPES = ['ollama', 'vllm', 'llama-cpp', 'tgi', 'openai-compatible'];
9
10
  const DEFAULT_OLLAMA_ENDPOINT = 'http://localhost:11434';
@@ -223,10 +224,9 @@ export class ModelLab {
223
224
  ...this._buildParameterBody(parameters || {}),
224
225
  };
225
226
 
226
- const endpoint = `${rt.endpoint}/v1/chat/completions`;
227
-
228
- const headers = { 'Content-Type': 'application/json' };
229
- if (rt.apiKey) headers['Authorization'] = `Bearer ${rt.apiKey}`;
227
+ const endpoint = rt.endpoint.replace('localhost', '127.0.0.1');
228
+ const reqHeaders = { 'Content-Type': 'application/json' };
229
+ if (rt.apiKey) reqHeaders['Authorization'] = `Bearer ${rt.apiKey}`;
230
230
 
231
231
  const requestStart = Date.now();
232
232
  let ttft = null;
@@ -236,85 +236,61 @@ export class ModelLab {
236
236
  let generationStart = null;
237
237
  let fullContent = '';
238
238
 
239
- const resp = await fetch(endpoint, {
239
+ const resp = await fetch(`${endpoint}/v1/chat/completions`, {
240
240
  method: 'POST',
241
- headers,
241
+ headers: reqHeaders,
242
242
  body: JSON.stringify(body),
243
243
  signal: AbortSignal.timeout(300000),
244
244
  });
245
245
 
246
246
  if (!resp.ok) {
247
- let errorMsg;
248
- try { errorMsg = (await resp.json()).error?.message || `HTTP ${resp.status}`; } catch { errorMsg = `HTTP ${resp.status}`; }
249
- throw new Error(errorMsg);
247
+ let errMsg = `HTTP ${resp.status}`;
248
+ try { const e = await resp.json(); errMsg = e.error?.message || errMsg; } catch { /* ignore */ }
249
+ throw new Error(errMsg);
250
250
  }
251
251
 
252
- const reader = resp.body.getReader();
253
- const decoder = new TextDecoder();
252
+ const nodeStream = Readable.fromWeb(resp.body);
254
253
  let buffer = '';
255
254
 
256
- try {
257
- while (true) {
258
- const { done, value } = await reader.read();
259
- if (done) break;
260
-
261
- buffer += decoder.decode(value, { stream: true });
262
- const lines = buffer.split('\n');
263
- buffer = lines.pop() || '';
264
-
265
- for (const line of lines) {
266
- const trimmed = line.trim();
267
- if (!trimmed || !trimmed.startsWith('data: ')) continue;
268
- const payload = trimmed.slice(6);
269
- if (payload === '[DONE]') continue;
270
-
271
- try {
272
- const chunk = JSON.parse(payload);
273
- const delta = chunk.choices?.[0]?.delta;
274
- if (delta?.reasoning_content) {
275
- if (ttft === null) {
276
- ttft = Date.now() - requestStart;
277
- generationStart = Date.now();
278
- }
279
- completionTokens++;
280
- onEvent({ type: 'reasoning', content: delta.reasoning_content });
281
- }
282
- if (delta?.content) {
283
- if (ttft === null) {
284
- ttft = Date.now() - requestStart;
285
- generationStart = Date.now();
286
- }
287
- fullContent += delta.content;
288
- completionTokens++;
289
- onEvent({ type: 'token', content: delta.content });
290
- }
291
- if (chunk.usage) {
292
- promptTokens = chunk.usage.prompt_tokens || 0;
293
- totalTokens = chunk.usage.total_tokens || 0;
294
- if (chunk.usage.completion_tokens) {
295
- completionTokens = chunk.usage.completion_tokens;
296
- }
297
- }
298
- } catch { /* skip malformed chunk */ }
299
- }
255
+ for await (const chunk of nodeStream) {
256
+ buffer += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
257
+ const lines = buffer.split('\n');
258
+ buffer = lines.pop() || '';
259
+
260
+ for (const line of lines) {
261
+ const trimmed = line.trim();
262
+ if (!trimmed || !trimmed.startsWith('data: ')) continue;
263
+ const data = trimmed.slice(6);
264
+ if (data === '[DONE]') continue;
265
+
266
+ try {
267
+ const parsed = JSON.parse(data);
268
+ const delta = parsed.choices?.[0]?.delta;
269
+ if (delta?.reasoning_content) {
270
+ if (ttft === null) { ttft = Date.now() - requestStart; generationStart = Date.now(); }
271
+ completionTokens++;
272
+ onEvent({ type: 'reasoning', content: delta.reasoning_content });
273
+ }
274
+ if (delta?.content) {
275
+ if (ttft === null) { ttft = Date.now() - requestStart; generationStart = Date.now(); }
276
+ fullContent += delta.content;
277
+ completionTokens++;
278
+ onEvent({ type: 'token', content: delta.content });
279
+ }
280
+ if (parsed.usage) {
281
+ promptTokens = parsed.usage.prompt_tokens || 0;
282
+ totalTokens = parsed.usage.total_tokens || 0;
283
+ if (parsed.usage.completion_tokens) completionTokens = parsed.usage.completion_tokens;
284
+ }
285
+ } catch { /* skip malformed chunk */ }
300
286
  }
301
- } finally {
302
- reader.releaseLock();
303
287
  }
304
288
 
305
289
  const generationTime = generationStart ? Date.now() - generationStart : Date.now() - requestStart;
306
290
  const tokensPerSec = generationTime > 0 ? (completionTokens / (generationTime / 1000)) : 0;
307
291
 
308
- let memoryUsage = null;
309
- if (rt.type === 'ollama') {
310
- memoryUsage = await this.getOllamaMemoryUsage(rt.endpoint);
311
- }
312
-
313
292
  if (sessionId) {
314
- this._appendToSession(sessionId, messages, {
315
- role: 'assistant',
316
- content: fullContent,
317
- });
293
+ this._appendToSession(sessionId, messages, { role: 'assistant', content: fullContent });
318
294
  }
319
295
 
320
296
  onEvent({
@@ -326,9 +302,16 @@ export class ModelLab {
326
302
  promptTokens,
327
303
  completionTokens,
328
304
  generationTime,
329
- memoryUsage,
305
+ memoryUsage: null,
330
306
  },
331
307
  });
308
+
309
+ if (rt.type === 'ollama') {
310
+ try {
311
+ const mem = await this.getOllamaMemoryUsage(rt.endpoint);
312
+ if (mem) onEvent({ type: 'memory', usage: mem });
313
+ } catch { /* ignore */ }
314
+ }
332
315
  }
333
316
 
334
317
  _buildParameterBody(params) {
@@ -147,11 +147,19 @@ export class Rotator extends EventEmitter {
147
147
  const signals = signalsEarly;
148
148
  let score = this.daemon.adaptive.scoreSession(signals);
149
149
 
150
- if (ageSec > 1800) score -= 5;
151
- if (ageSec > 3600) score -= 10;
152
- if (ageSec > 7200) score -= 15;
153
- if (ageSec > 14400) score -= 20;
154
- if (ageSec > 28800) score -= 25;
150
+ // Age penalties: only for providers that don't manage their own context.
151
+ // Claude Code handles context internally via compaction — long sessions
152
+ // are normal and productive. Penalizing age causes premature rotation
153
+ // that destroys active debugging context and creates restart loops.
154
+ const providerForAge = getProvider(agent.provider);
155
+ const selfManagesForAge = providerForAge?.constructor?.managesOwnContext ?? false;
156
+ if (!selfManagesForAge) {
157
+ if (ageSec > 1800) score -= 5;
158
+ if (ageSec > 3600) score -= 10;
159
+ if (ageSec > 7200) score -= 15;
160
+ if (ageSec > 14400) score -= 20;
161
+ if (ageSec > 28800) score -= 25;
162
+ }
155
163
 
156
164
  score = Math.max(0, Math.min(100, score));
157
165
 
@@ -240,16 +248,23 @@ export class Rotator extends EventEmitter {
240
248
  }
241
249
  }
242
250
 
243
- // --- Change 4: Truncation-triggered immediate rotation (all providers) ---
244
- if (agent.consecutiveTruncations >= 2) {
251
+ // --- Change 4: Truncation-triggered rotation ---
252
+ // Self-managing providers need more consecutive truncations — single
253
+ // truncations can be transient API issues, not session degradation.
254
+ const truncationThreshold = selfManagesContext ? 4 : 2;
255
+ if (agent.consecutiveTruncations >= truncationThreshold) {
245
256
  console.log(` Rotator: ${agent.name} consecutiveTruncations=${agent.consecutiveTruncations} — FORCE rotating (incomplete_response)`);
246
257
  await this.rotate(agent.id, { reason: 'incomplete_response', qualityScore: 0 });
247
258
  continue;
248
259
  }
249
260
 
250
- // --- Change 3: Compaction-aware rotation for self-managing providers ---
261
+ // --- Change 3: Compaction-aware rotation ---
262
+ // Only for non-self-managing providers. Claude Code compacts internally
263
+ // as part of normal operation — it's healthy, not degradation. Counting
264
+ // compactions toward a ceiling causes premature rotation that destroys
265
+ // active debugging sessions and creates restart loops.
251
266
  const compactions = this.compactionCounts.get(agent.id) || 0;
252
- if (compactions >= 5) {
267
+ if (!selfManagesContext && compactions >= 5) {
253
268
  console.log(` Rotator: ${agent.name} compactions=${compactions} — FORCE rotating (compaction_ceiling)`);
254
269
  await this.rotate(agent.id, { reason: 'compaction_ceiling' });
255
270
  continue;
@@ -259,18 +274,30 @@ export class Rotator extends EventEmitter {
259
274
  // agents don't persist producing bad output for 8-10 minutes
260
275
  if (this._isOnCooldown(agent.id, QUALITY_COOLDOWN_MS)) continue;
261
276
 
262
- // Effective quality threshold: lower for agents showing degradation signals
263
- let effectiveQualityThreshold = QUALITY_THRESHOLD;
264
- if (compactions >= 3 || agent.truncationSuspected || agent.cacheResetDetected) {
265
- effectiveQualityThreshold = 55;
277
+ // Effective quality threshold depends on provider type.
278
+ // Self-managing providers (Claude Code): threshold = 15. Only rotate on
279
+ // truly catastrophic degradation. Normal debugging naturally produces
280
+ // errors, retries, and bash repetitions — the scoring model treats these
281
+ // as degradation but they're expected behavior during investigation.
282
+ // A threshold of 40 (the default) kills debugging sessions at ~8 minutes.
283
+ let effectiveQualityThreshold;
284
+ if (selfManagesContext) {
285
+ effectiveQualityThreshold = 15;
286
+ } else {
287
+ effectiveQualityThreshold = QUALITY_THRESHOLD;
288
+ if (compactions >= 3) effectiveQualityThreshold = 55;
289
+ if (agent.truncationSuspected || agent.cacheResetDetected) {
290
+ effectiveQualityThreshold = Math.max(effectiveQualityThreshold, 55);
291
+ }
266
292
  }
267
293
 
268
- // All providers: quality-based rotation — detects degradation before tokens are wasted
269
294
  const quality = this.scoreLiveSession(agent);
270
295
  if (quality.hasEnoughData && quality.score < effectiveQualityThreshold) {
271
- // Severe degradation (score < 25): rotate immediately regardless of idle state.
272
- // The agent is producing bad output waiting for idle is counterproductive.
273
- if (quality.score < 25) {
296
+ // For self-managing providers, effectiveQualityThreshold IS the severe
297
+ // threshold (15) any score below it is catastrophic, rotate immediately.
298
+ // For others, severe = < 25, moderate = 25-40/55.
299
+ const severeThreshold = selfManagesContext ? effectiveQualityThreshold : 25;
300
+ if (quality.score < severeThreshold) {
274
301
  console.log(` Rotator: ${agent.name} quality=${quality.score} — FORCE rotating (severe degradation)`);
275
302
  await this.rotate(agent.id, {
276
303
  reason: 'quality_degradation',
@@ -279,8 +306,10 @@ export class Rotator extends EventEmitter {
279
306
  });
280
307
  continue;
281
308
  }
282
- // Moderate degradation (25-40): rotate when idle
283
- if (this._idleMs(agent) > 10_000) {
309
+ // Moderate degradation (25-40): only for non-self-managing providers.
310
+ // Claude Code sessions should never be killed for "moderate" quality —
311
+ // errors during debugging are expected, not degradation.
312
+ if (!selfManagesContext && this._idleMs(agent) > 10_000) {
284
313
  console.log(` Rotator: ${agent.name} quality=${quality.score} — rotating (quality)`);
285
314
  await this.rotate(agent.id, {
286
315
  reason: 'quality_degradation',
@@ -343,7 +372,7 @@ export class Rotator extends EventEmitter {
343
372
  reason: options.reason || 'manual',
344
373
  oldTokens: agent.tokensUsed,
345
374
  contextUsage: agent.contextUsage,
346
- brief: brief.slice(0, 4000),
375
+ brief: brief.slice(0, 6000),
347
376
  }, agent.workingDir, agent.teamId);
348
377
  }
349
378