groove-dev 0.27.36 → 0.27.37
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/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/process.js +52 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +8 -14
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/process.js +52 -0
- package/packages/daemon/src/providers/claude-code.js +8 -14
- package/packages/gui/package.json +1 -1
- package/.groove-staging/state.json +0 -3
- package/.groove-staging/timeline.json +0 -13
|
@@ -16,6 +16,8 @@ const COMPACTION_DROP_THRESHOLD = 0.25; // 25% drop from peak = natural compacti
|
|
|
16
16
|
const COMPACTION_MIN_PEAK = 0.15; // Ignore compaction if peak was below 15%
|
|
17
17
|
const MAX_BUFFER_SIZE = 1_048_576; // 1MB — discard oldest half if exceeded
|
|
18
18
|
const STREAM_THROTTLE_MS = 250; // 4 broadcasts/sec per agent
|
|
19
|
+
const STALL_CHECK_INTERVAL_MS = 60_000; // Poll for stuck agents every 60s
|
|
20
|
+
const STALL_THRESHOLD_MS = 5 * 60_000; // 5 min of silence on a live PID = stalled stream
|
|
19
21
|
|
|
20
22
|
// Role-specific prompt prefixes — applied during spawn regardless of entry point
|
|
21
23
|
// (SpawnPanel, chat continue, CLI, API) for consistency
|
|
@@ -297,7 +299,44 @@ export class ProcessManager {
|
|
|
297
299
|
this.pendingMessages = new Map(); // agentId -> { message, timestamp }
|
|
298
300
|
this._streamThrottle = new Map(); // agentId -> { timer, pending }
|
|
299
301
|
this._rotatingAgents = new Set(); // agentIds currently being rotated (rotator wrote handoff)
|
|
302
|
+
this._stalledAgents = new Set(); // agentIds already flagged as stalled (avoids duplicate broadcasts)
|
|
300
303
|
|
|
304
|
+
this._stallWatchdog = setInterval(() => this._checkStalls(), STALL_CHECK_INTERVAL_MS);
|
|
305
|
+
if (this._stallWatchdog.unref) this._stallWatchdog.unref();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
_checkStalls() {
|
|
309
|
+
const { registry } = this.daemon;
|
|
310
|
+
const now = Date.now();
|
|
311
|
+
for (const agentId of this.handles.keys()) {
|
|
312
|
+
const agent = registry.get(agentId);
|
|
313
|
+
if (!agent || agent.status !== 'running') continue;
|
|
314
|
+
const lastActivity = agent.lastActivity ? new Date(agent.lastActivity).getTime() : now;
|
|
315
|
+
const silentMs = now - lastActivity;
|
|
316
|
+
if (silentMs < STALL_THRESHOLD_MS) {
|
|
317
|
+
if (this._stalledAgents.has(agentId)) {
|
|
318
|
+
this._stalledAgents.delete(agentId);
|
|
319
|
+
registry.update(agentId, { stalled: false });
|
|
320
|
+
}
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (this._stalledAgents.has(agentId)) continue;
|
|
324
|
+
this._stalledAgents.add(agentId);
|
|
325
|
+
registry.update(agentId, { stalled: true, stalledSince: new Date(lastActivity).toISOString() });
|
|
326
|
+
if (this.daemon.timeline) {
|
|
327
|
+
this.daemon.timeline.recordEvent('stall', {
|
|
328
|
+
agentId, agentName: agent.name, role: agent.role, silentMs,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
this.daemon.broadcast({
|
|
332
|
+
type: 'agent:stalled',
|
|
333
|
+
agentId,
|
|
334
|
+
agentName: agent.name,
|
|
335
|
+
silentMs,
|
|
336
|
+
lastActivity: agent.lastActivity,
|
|
337
|
+
});
|
|
338
|
+
console.warn(`[Groove] Agent ${agent.name} (${agentId}) silent for ${Math.round(silentMs / 1000)}s — possible stalled API stream`);
|
|
339
|
+
}
|
|
301
340
|
}
|
|
302
341
|
|
|
303
342
|
async spawn(config) {
|
|
@@ -746,6 +785,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
746
785
|
// Clean up per-agent maps to prevent unbounded growth in long sessions
|
|
747
786
|
this.peakContextUsage.delete(agent.id);
|
|
748
787
|
this.pendingMessages.delete(agent.id);
|
|
788
|
+
this._stalledAgents.delete(agent.id);
|
|
749
789
|
|
|
750
790
|
// Release file-scope locks so they don't persist after agent death
|
|
751
791
|
if (this.daemon.locks) this.daemon.locks.release(agent.id);
|
|
@@ -917,6 +957,12 @@ For normal file edits within your scope, proceed without review.
|
|
|
917
957
|
|
|
918
958
|
const updates = { lastActivity: new Date().toISOString() };
|
|
919
959
|
|
|
960
|
+
// Clear stall flag — output means the stream is alive again
|
|
961
|
+
if (this._stalledAgents.has(agentId)) {
|
|
962
|
+
this._stalledAgents.delete(agentId);
|
|
963
|
+
updates.stalled = false;
|
|
964
|
+
}
|
|
965
|
+
|
|
920
966
|
// Token tracking — feed subsystems with full breakdown
|
|
921
967
|
if (output.tokensUsed !== undefined && output.tokensUsed > 0) {
|
|
922
968
|
updates.tokensUsed = agent.tokensUsed + output.tokensUsed;
|
|
@@ -1398,6 +1444,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
1398
1444
|
logStream.write(`[${new Date().toISOString()}] Process exited: code=${code} signal=${signal}\n`);
|
|
1399
1445
|
logStream.end();
|
|
1400
1446
|
this.handles.delete(newAgent.id);
|
|
1447
|
+
this._stalledAgents.delete(newAgent.id);
|
|
1401
1448
|
const finalStatus = signal === 'SIGTERM' || signal === 'SIGKILL' ? 'killed' : code === 0 ? 'completed' : 'crashed';
|
|
1402
1449
|
registry.update(newAgent.id, { status: finalStatus, pid: null });
|
|
1403
1450
|
this.daemon.broadcast({ type: 'agent:exit', agentId: newAgent.id, code, signal, status: finalStatus });
|
|
@@ -1410,6 +1457,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
1410
1457
|
logStream.write(`[error] ${err.message}\n`);
|
|
1411
1458
|
logStream.end();
|
|
1412
1459
|
this.handles.delete(newAgent.id);
|
|
1460
|
+
this._stalledAgents.delete(newAgent.id);
|
|
1413
1461
|
registry.update(newAgent.id, { status: 'crashed', pid: null });
|
|
1414
1462
|
});
|
|
1415
1463
|
|
|
@@ -1548,6 +1596,10 @@ For normal file edits within your scope, proceed without review.
|
|
|
1548
1596
|
async killAll() {
|
|
1549
1597
|
const ids = Array.from(this.handles.keys());
|
|
1550
1598
|
await Promise.all(ids.map((id) => this.kill(id)));
|
|
1599
|
+
if (this._stallWatchdog) {
|
|
1600
|
+
clearInterval(this._stallWatchdog);
|
|
1601
|
+
this._stallWatchdog = null;
|
|
1602
|
+
}
|
|
1551
1603
|
}
|
|
1552
1604
|
|
|
1553
1605
|
isRunning(agentId) {
|
|
@@ -149,19 +149,22 @@ export class ClaudeCodeProvider extends Provider {
|
|
|
149
149
|
const cacheReadTokens = usage?.cache_read_input_tokens || 0;
|
|
150
150
|
const cacheCreationTokens = usage?.cache_creation_input_tokens || 0;
|
|
151
151
|
const outputTokens = usage?.output_tokens || 0;
|
|
152
|
+
// tokensUsed = new processing tokens only (input + output). Cache reads are
|
|
153
|
+
// the same bytes re-read every turn and must NOT be accumulated — doing so
|
|
154
|
+
// inflated agent.tokensUsed ~50× and created the phantom "freeze at 1M".
|
|
155
|
+
// totalIn still drives contextUsage because cached bytes DO occupy context.
|
|
152
156
|
const totalIn = inputTokens + cacheReadTokens + cacheCreationTokens;
|
|
153
157
|
events.push({
|
|
154
158
|
type: 'activity',
|
|
155
159
|
subtype: 'assistant',
|
|
156
160
|
data: data.message?.content || '',
|
|
157
|
-
tokensUsed:
|
|
161
|
+
tokensUsed: inputTokens + outputTokens,
|
|
158
162
|
inputTokens,
|
|
159
163
|
outputTokens,
|
|
160
164
|
cacheReadTokens,
|
|
161
165
|
cacheCreationTokens,
|
|
162
166
|
model: data.message?.model,
|
|
163
167
|
});
|
|
164
|
-
// Compute context usage from assistant message usage
|
|
165
168
|
if (totalIn > 0) {
|
|
166
169
|
const modelId = data.message?.model || '';
|
|
167
170
|
const modelMeta = ClaudeCodeProvider.models.find((m) => modelId.includes(m.id));
|
|
@@ -172,21 +175,12 @@ export class ClaudeCodeProvider extends Provider {
|
|
|
172
175
|
});
|
|
173
176
|
}
|
|
174
177
|
} else if (data.type === 'result') {
|
|
175
|
-
// Result
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const cacheReadTokens = usage?.cache_read_input_tokens || 0;
|
|
179
|
-
const cacheCreationTokens = usage?.cache_creation_input_tokens || 0;
|
|
180
|
-
const outputTokens = usage?.output_tokens || 0;
|
|
181
|
-
const totalIn = inputTokens + cacheReadTokens + cacheCreationTokens;
|
|
178
|
+
// Result carries cumulative session usage — per-turn counts were already
|
|
179
|
+
// accumulated from assistant events, so we do NOT emit tokensUsed here
|
|
180
|
+
// (that was the double-count). Only emit session-level metadata.
|
|
182
181
|
events.push({
|
|
183
182
|
type: 'result',
|
|
184
183
|
data: data.result,
|
|
185
|
-
tokensUsed: totalIn + outputTokens,
|
|
186
|
-
inputTokens,
|
|
187
|
-
outputTokens,
|
|
188
|
-
cacheReadTokens,
|
|
189
|
-
cacheCreationTokens,
|
|
190
184
|
cost: data.total_cost_usd,
|
|
191
185
|
duration: data.duration_ms,
|
|
192
186
|
turns: data.num_turns,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.37",
|
|
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)",
|
|
@@ -16,6 +16,8 @@ const COMPACTION_DROP_THRESHOLD = 0.25; // 25% drop from peak = natural compacti
|
|
|
16
16
|
const COMPACTION_MIN_PEAK = 0.15; // Ignore compaction if peak was below 15%
|
|
17
17
|
const MAX_BUFFER_SIZE = 1_048_576; // 1MB — discard oldest half if exceeded
|
|
18
18
|
const STREAM_THROTTLE_MS = 250; // 4 broadcasts/sec per agent
|
|
19
|
+
const STALL_CHECK_INTERVAL_MS = 60_000; // Poll for stuck agents every 60s
|
|
20
|
+
const STALL_THRESHOLD_MS = 5 * 60_000; // 5 min of silence on a live PID = stalled stream
|
|
19
21
|
|
|
20
22
|
// Role-specific prompt prefixes — applied during spawn regardless of entry point
|
|
21
23
|
// (SpawnPanel, chat continue, CLI, API) for consistency
|
|
@@ -297,7 +299,44 @@ export class ProcessManager {
|
|
|
297
299
|
this.pendingMessages = new Map(); // agentId -> { message, timestamp }
|
|
298
300
|
this._streamThrottle = new Map(); // agentId -> { timer, pending }
|
|
299
301
|
this._rotatingAgents = new Set(); // agentIds currently being rotated (rotator wrote handoff)
|
|
302
|
+
this._stalledAgents = new Set(); // agentIds already flagged as stalled (avoids duplicate broadcasts)
|
|
300
303
|
|
|
304
|
+
this._stallWatchdog = setInterval(() => this._checkStalls(), STALL_CHECK_INTERVAL_MS);
|
|
305
|
+
if (this._stallWatchdog.unref) this._stallWatchdog.unref();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
_checkStalls() {
|
|
309
|
+
const { registry } = this.daemon;
|
|
310
|
+
const now = Date.now();
|
|
311
|
+
for (const agentId of this.handles.keys()) {
|
|
312
|
+
const agent = registry.get(agentId);
|
|
313
|
+
if (!agent || agent.status !== 'running') continue;
|
|
314
|
+
const lastActivity = agent.lastActivity ? new Date(agent.lastActivity).getTime() : now;
|
|
315
|
+
const silentMs = now - lastActivity;
|
|
316
|
+
if (silentMs < STALL_THRESHOLD_MS) {
|
|
317
|
+
if (this._stalledAgents.has(agentId)) {
|
|
318
|
+
this._stalledAgents.delete(agentId);
|
|
319
|
+
registry.update(agentId, { stalled: false });
|
|
320
|
+
}
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (this._stalledAgents.has(agentId)) continue;
|
|
324
|
+
this._stalledAgents.add(agentId);
|
|
325
|
+
registry.update(agentId, { stalled: true, stalledSince: new Date(lastActivity).toISOString() });
|
|
326
|
+
if (this.daemon.timeline) {
|
|
327
|
+
this.daemon.timeline.recordEvent('stall', {
|
|
328
|
+
agentId, agentName: agent.name, role: agent.role, silentMs,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
this.daemon.broadcast({
|
|
332
|
+
type: 'agent:stalled',
|
|
333
|
+
agentId,
|
|
334
|
+
agentName: agent.name,
|
|
335
|
+
silentMs,
|
|
336
|
+
lastActivity: agent.lastActivity,
|
|
337
|
+
});
|
|
338
|
+
console.warn(`[Groove] Agent ${agent.name} (${agentId}) silent for ${Math.round(silentMs / 1000)}s — possible stalled API stream`);
|
|
339
|
+
}
|
|
301
340
|
}
|
|
302
341
|
|
|
303
342
|
async spawn(config) {
|
|
@@ -746,6 +785,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
746
785
|
// Clean up per-agent maps to prevent unbounded growth in long sessions
|
|
747
786
|
this.peakContextUsage.delete(agent.id);
|
|
748
787
|
this.pendingMessages.delete(agent.id);
|
|
788
|
+
this._stalledAgents.delete(agent.id);
|
|
749
789
|
|
|
750
790
|
// Release file-scope locks so they don't persist after agent death
|
|
751
791
|
if (this.daemon.locks) this.daemon.locks.release(agent.id);
|
|
@@ -917,6 +957,12 @@ For normal file edits within your scope, proceed without review.
|
|
|
917
957
|
|
|
918
958
|
const updates = { lastActivity: new Date().toISOString() };
|
|
919
959
|
|
|
960
|
+
// Clear stall flag — output means the stream is alive again
|
|
961
|
+
if (this._stalledAgents.has(agentId)) {
|
|
962
|
+
this._stalledAgents.delete(agentId);
|
|
963
|
+
updates.stalled = false;
|
|
964
|
+
}
|
|
965
|
+
|
|
920
966
|
// Token tracking — feed subsystems with full breakdown
|
|
921
967
|
if (output.tokensUsed !== undefined && output.tokensUsed > 0) {
|
|
922
968
|
updates.tokensUsed = agent.tokensUsed + output.tokensUsed;
|
|
@@ -1398,6 +1444,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
1398
1444
|
logStream.write(`[${new Date().toISOString()}] Process exited: code=${code} signal=${signal}\n`);
|
|
1399
1445
|
logStream.end();
|
|
1400
1446
|
this.handles.delete(newAgent.id);
|
|
1447
|
+
this._stalledAgents.delete(newAgent.id);
|
|
1401
1448
|
const finalStatus = signal === 'SIGTERM' || signal === 'SIGKILL' ? 'killed' : code === 0 ? 'completed' : 'crashed';
|
|
1402
1449
|
registry.update(newAgent.id, { status: finalStatus, pid: null });
|
|
1403
1450
|
this.daemon.broadcast({ type: 'agent:exit', agentId: newAgent.id, code, signal, status: finalStatus });
|
|
@@ -1410,6 +1457,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
1410
1457
|
logStream.write(`[error] ${err.message}\n`);
|
|
1411
1458
|
logStream.end();
|
|
1412
1459
|
this.handles.delete(newAgent.id);
|
|
1460
|
+
this._stalledAgents.delete(newAgent.id);
|
|
1413
1461
|
registry.update(newAgent.id, { status: 'crashed', pid: null });
|
|
1414
1462
|
});
|
|
1415
1463
|
|
|
@@ -1548,6 +1596,10 @@ For normal file edits within your scope, proceed without review.
|
|
|
1548
1596
|
async killAll() {
|
|
1549
1597
|
const ids = Array.from(this.handles.keys());
|
|
1550
1598
|
await Promise.all(ids.map((id) => this.kill(id)));
|
|
1599
|
+
if (this._stallWatchdog) {
|
|
1600
|
+
clearInterval(this._stallWatchdog);
|
|
1601
|
+
this._stallWatchdog = null;
|
|
1602
|
+
}
|
|
1551
1603
|
}
|
|
1552
1604
|
|
|
1553
1605
|
isRunning(agentId) {
|
|
@@ -149,19 +149,22 @@ export class ClaudeCodeProvider extends Provider {
|
|
|
149
149
|
const cacheReadTokens = usage?.cache_read_input_tokens || 0;
|
|
150
150
|
const cacheCreationTokens = usage?.cache_creation_input_tokens || 0;
|
|
151
151
|
const outputTokens = usage?.output_tokens || 0;
|
|
152
|
+
// tokensUsed = new processing tokens only (input + output). Cache reads are
|
|
153
|
+
// the same bytes re-read every turn and must NOT be accumulated — doing so
|
|
154
|
+
// inflated agent.tokensUsed ~50× and created the phantom "freeze at 1M".
|
|
155
|
+
// totalIn still drives contextUsage because cached bytes DO occupy context.
|
|
152
156
|
const totalIn = inputTokens + cacheReadTokens + cacheCreationTokens;
|
|
153
157
|
events.push({
|
|
154
158
|
type: 'activity',
|
|
155
159
|
subtype: 'assistant',
|
|
156
160
|
data: data.message?.content || '',
|
|
157
|
-
tokensUsed:
|
|
161
|
+
tokensUsed: inputTokens + outputTokens,
|
|
158
162
|
inputTokens,
|
|
159
163
|
outputTokens,
|
|
160
164
|
cacheReadTokens,
|
|
161
165
|
cacheCreationTokens,
|
|
162
166
|
model: data.message?.model,
|
|
163
167
|
});
|
|
164
|
-
// Compute context usage from assistant message usage
|
|
165
168
|
if (totalIn > 0) {
|
|
166
169
|
const modelId = data.message?.model || '';
|
|
167
170
|
const modelMeta = ClaudeCodeProvider.models.find((m) => modelId.includes(m.id));
|
|
@@ -172,21 +175,12 @@ export class ClaudeCodeProvider extends Provider {
|
|
|
172
175
|
});
|
|
173
176
|
}
|
|
174
177
|
} else if (data.type === 'result') {
|
|
175
|
-
// Result
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const cacheReadTokens = usage?.cache_read_input_tokens || 0;
|
|
179
|
-
const cacheCreationTokens = usage?.cache_creation_input_tokens || 0;
|
|
180
|
-
const outputTokens = usage?.output_tokens || 0;
|
|
181
|
-
const totalIn = inputTokens + cacheReadTokens + cacheCreationTokens;
|
|
178
|
+
// Result carries cumulative session usage — per-turn counts were already
|
|
179
|
+
// accumulated from assistant events, so we do NOT emit tokensUsed here
|
|
180
|
+
// (that was the double-count). Only emit session-level metadata.
|
|
182
181
|
events.push({
|
|
183
182
|
type: 'result',
|
|
184
183
|
data: data.result,
|
|
185
|
-
tokensUsed: totalIn + outputTokens,
|
|
186
|
-
inputTokens,
|
|
187
|
-
outputTokens,
|
|
188
|
-
cacheReadTokens,
|
|
189
|
-
cacheCreationTokens,
|
|
190
184
|
cost: data.total_cost_usd,
|
|
191
185
|
duration: data.duration_ms,
|
|
192
186
|
turns: data.num_turns,
|