groove-dev 0.27.0 → 0.27.2
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/CHANGELOG.md +35 -0
- package/CLAUDE.md +7 -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/firstrun.js +6 -6
- package/node_modules/@groove-dev/daemon/src/journalist.js +29 -9
- package/node_modules/@groove-dev/daemon/src/rotator.js +43 -31
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +5 -2
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +36 -31
- package/node_modules/@groove-dev/gui/dist/assets/{index-eCrVowF0.js → index-Bl1_J0sN.js} +13 -13
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +41 -14
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/firstrun.js +6 -6
- package/packages/daemon/src/journalist.js +29 -9
- package/packages/daemon/src/rotator.js +43 -31
- package/packages/gui/dist/assets/{index-eCrVowF0.js → index-Bl1_J0sN.js} +13 -13
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/stores/groove.js +41 -14
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>Groove GUI</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-Bl1_J0sN.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
|
|
@@ -249,25 +249,52 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
249
249
|
break;
|
|
250
250
|
|
|
251
251
|
case 'rotation:start':
|
|
252
|
-
|
|
252
|
+
// Silent — rotation must feel seamless to the user.
|
|
253
|
+
// Visibility is available in dashboard Intel panel for curious users.
|
|
253
254
|
break;
|
|
254
255
|
|
|
255
256
|
case 'rotation:complete': {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
257
|
+
// Silent toast — seamless infinite sessions are the whole promise.
|
|
258
|
+
// Migrate all agent-keyed state to the new ID ALWAYS, not just when
|
|
259
|
+
// the old agent's panel is open. Chat history, activity log, token
|
|
260
|
+
// timeline all carry forward so no state is orphaned.
|
|
261
|
+
if (!msg.newAgentId || !msg.oldAgentId) break;
|
|
262
|
+
set((s) => {
|
|
263
|
+
const chatHistory = { ...s.chatHistory };
|
|
264
|
+
const tokenTimeline = { ...s.tokenTimeline };
|
|
265
|
+
const activityLog = { ...s.activityLog };
|
|
266
|
+
const chatInputs = { ...s.chatInputs };
|
|
267
|
+
if (chatHistory[msg.oldAgentId]?.length) {
|
|
268
|
+
chatHistory[msg.newAgentId] = [...chatHistory[msg.oldAgentId]];
|
|
269
|
+
delete chatHistory[msg.oldAgentId];
|
|
270
|
+
}
|
|
271
|
+
if (tokenTimeline[msg.oldAgentId]?.length) {
|
|
272
|
+
tokenTimeline[msg.newAgentId] = [...tokenTimeline[msg.oldAgentId]];
|
|
273
|
+
delete tokenTimeline[msg.oldAgentId];
|
|
274
|
+
}
|
|
275
|
+
if (activityLog[msg.oldAgentId]?.length) {
|
|
276
|
+
activityLog[msg.newAgentId] = [...activityLog[msg.oldAgentId]];
|
|
277
|
+
delete activityLog[msg.oldAgentId];
|
|
278
|
+
}
|
|
279
|
+
if (chatInputs[msg.oldAgentId]) {
|
|
280
|
+
chatInputs[msg.newAgentId] = chatInputs[msg.oldAgentId];
|
|
281
|
+
delete chatInputs[msg.oldAgentId];
|
|
282
|
+
}
|
|
283
|
+
// Only redirect the detail panel if the user was actively viewing
|
|
284
|
+
// the old agent. Otherwise leave their current view alone.
|
|
285
|
+
const panel = s.detailPanel;
|
|
286
|
+
let detailPanel = panel;
|
|
287
|
+
let teamDetailPanels = s.teamDetailPanels;
|
|
288
|
+
if (panel?.type === 'agent' && panel.agentId === msg.oldAgentId) {
|
|
266
289
|
const newPanel = { type: 'agent', agentId: msg.newAgentId };
|
|
290
|
+
detailPanel = newPanel;
|
|
267
291
|
const tid = get().activeTeamId;
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
292
|
+
teamDetailPanels = { ...s.teamDetailPanels, [tid]: newPanel };
|
|
293
|
+
}
|
|
294
|
+
// Persist the migration to localStorage so it survives a reload
|
|
295
|
+
try { localStorage.setItem('groove:chatHistory', JSON.stringify(chatHistory)); } catch {}
|
|
296
|
+
return { chatHistory, tokenTimeline, activityLog, chatInputs, detailPanel, teamDetailPanels };
|
|
297
|
+
});
|
|
271
298
|
break;
|
|
272
299
|
}
|
|
273
300
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.2",
|
|
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)",
|
|
@@ -15,15 +15,15 @@ const DEFAULT_CONFIG = {
|
|
|
15
15
|
qcThreshold: 4,
|
|
16
16
|
maxAgents: 10,
|
|
17
17
|
defaultProvider: 'claude-code',
|
|
18
|
-
// Self-healing rotation
|
|
19
|
-
// (stuck loops,
|
|
20
|
-
//
|
|
21
|
-
//
|
|
18
|
+
// Self-healing rotation safety net. Per-instance token ceiling catches
|
|
19
|
+
// genuinely runaway agents (stuck loops, unbounded context expansion)
|
|
20
|
+
// without false-positiving legitimate heavy work. Scoped to the agent's
|
|
21
|
+
// spawnedAt so tokens from prior rotations don't count. Role multipliers
|
|
22
|
+
// scale the ceiling for exploration-heavy roles — user can override.
|
|
23
|
+
// Set autoRotate=false to disable enforcement (broadcast-only).
|
|
22
24
|
safety: {
|
|
23
25
|
autoRotate: true,
|
|
24
26
|
tokenCeilingPerAgent: 5_000_000,
|
|
25
|
-
velocityWindowSeconds: 300,
|
|
26
|
-
velocityTokenThreshold: 1_500_000,
|
|
27
27
|
},
|
|
28
28
|
};
|
|
29
29
|
|
|
@@ -689,12 +689,31 @@ export class Journalist {
|
|
|
689
689
|
// with, decided, and solved — not just what the current session did.
|
|
690
690
|
const recentChain = this.daemon.memory?.getRecentHandoffMarkdown(agent.role, 3, 3000) || '';
|
|
691
691
|
|
|
692
|
+
// Pull the user's recent messages to this agent so the new instance
|
|
693
|
+
// can continue the conversation naturally instead of restarting it.
|
|
694
|
+
// Without this, the user feels like their chat was lost.
|
|
695
|
+
const allFeedback = this.getUserFeedback() || [];
|
|
696
|
+
const agentFeedback = allFeedback
|
|
697
|
+
.filter((fb) => fb.agentId === agent.id || fb.agentName === agent.name)
|
|
698
|
+
.slice(-5);
|
|
699
|
+
const conversationSummary = agentFeedback.length > 0
|
|
700
|
+
? agentFeedback.map((fb) => `- "${fb.message}"`).join('\n')
|
|
701
|
+
: '';
|
|
702
|
+
|
|
692
703
|
return [
|
|
693
|
-
`#
|
|
704
|
+
`# Session Continuation`,
|
|
705
|
+
``,
|
|
706
|
+
`You are **${agent.name}** (role: ${agent.role}). This is an internal context refresh — `,
|
|
707
|
+
`the conversation with the user is ongoing and must feel seamless to them. They cannot see this brief.`,
|
|
708
|
+
``,
|
|
709
|
+
`## CRITICAL: Continue Seamlessly`,
|
|
694
710
|
``,
|
|
695
|
-
`
|
|
696
|
-
`
|
|
711
|
+
`Do NOT announce a restart, rotation, or resumption. Do NOT greet the user. Do NOT say `,
|
|
712
|
+
`"resuming", "continuing from where I left off", "let me check state again", or anything `,
|
|
713
|
+
`that signals a break in the conversation. The user's next message should be answered as `,
|
|
714
|
+
`if the last turn of conversation just happened — because from their side, it did.`,
|
|
697
715
|
``,
|
|
716
|
+
conversationSummary ? `## Recent User Messages (what they've been asking for)\n\n${conversationSummary}\n` : '',
|
|
698
717
|
recentChain ? `## Rotation History (recent)\n\n${recentChain}\n` : '',
|
|
699
718
|
`## Your Identity`,
|
|
700
719
|
`- Role: ${agent.role}`,
|
|
@@ -702,7 +721,7 @@ export class Journalist {
|
|
|
702
721
|
`- Provider: ${agent.provider}`,
|
|
703
722
|
agent.workingDir ? `- Working directory: ${agent.workingDir}` : '',
|
|
704
723
|
``,
|
|
705
|
-
`##
|
|
724
|
+
`## Session State`,
|
|
706
725
|
`- Tokens used: ${agent.tokensUsed}`,
|
|
707
726
|
`- Tool calls: ${entries.filter((e) => e.type === 'tool').length}`,
|
|
708
727
|
``,
|
|
@@ -713,12 +732,13 @@ export class Journalist {
|
|
|
713
732
|
``,
|
|
714
733
|
projectMap ? projectMap.slice(0, 10000) : 'No project map available yet.',
|
|
715
734
|
``,
|
|
716
|
-
`##
|
|
735
|
+
`## How to Respond`,
|
|
717
736
|
``,
|
|
718
|
-
`
|
|
719
|
-
`
|
|
720
|
-
|
|
721
|
-
agent.
|
|
737
|
+
`Wait for the user's next message, then answer it directly. If they asked a question or gave `,
|
|
738
|
+
`an instruction in the Recent User Messages section above and it's still unanswered, address `,
|
|
739
|
+
`that naturally. Match the tone and continuity of a conversation that never paused.`,
|
|
740
|
+
agent.workingDir ? `Stay within your working directory: ${agent.workingDir}.` : '',
|
|
741
|
+
agent.prompt ? `\nOriginal task context: ${agent.prompt}` : '',
|
|
722
742
|
].filter(Boolean).join('\n');
|
|
723
743
|
}
|
|
724
744
|
|
|
@@ -136,40 +136,53 @@ export class Rotator extends EventEmitter {
|
|
|
136
136
|
return result;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
//
|
|
140
|
-
//
|
|
139
|
+
// Per-role safety multiplier for the token ceiling. Exploration-heavy
|
|
140
|
+
// roles legitimately burn tokens fast on big codebases — multiplier
|
|
141
|
+
// scales their ceiling so the safety net catches truly runaway agents
|
|
142
|
+
// without false-positiving legitimate heavy work. User-overridable via
|
|
143
|
+
// config.safety.roleMultipliers.
|
|
144
|
+
_getRoleMultiplier(role) {
|
|
145
|
+
const safety = this.daemon.config?.safety;
|
|
146
|
+
const overrides = safety?.roleMultipliers || {};
|
|
147
|
+
if (overrides[role] != null) return overrides[role];
|
|
148
|
+
const defaults = {
|
|
149
|
+
planner: 10, // heavy exploration by design
|
|
150
|
+
fullstack: 4, // QC auditors read broadly
|
|
151
|
+
analyst: 5,
|
|
152
|
+
security: 4,
|
|
153
|
+
docs: 1,
|
|
154
|
+
};
|
|
155
|
+
return defaults[role] || 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Safety trigger — runaway agent detection. One check only: per-instance
|
|
159
|
+
// token ceiling scoped to `spawnedAt` so rotations don't re-trigger on
|
|
160
|
+
// inherited cumulative tokens. Velocity-based triggers were removed in
|
|
161
|
+
// v0.27.2 — they produced too many false positives on legitimate heavy
|
|
162
|
+
// exploration. If a pattern emerges from real usage that warrants an
|
|
163
|
+
// earlier-warning signal, re-add it gated on quality-degradation signals
|
|
164
|
+
// (repetitions, errors, file churn) — not velocity alone.
|
|
141
165
|
_checkSafetyTriggers(agent) {
|
|
142
166
|
const safety = this.daemon.config?.safety;
|
|
143
167
|
if (!safety || safety.autoRotate === false) return null;
|
|
144
168
|
if (!this.daemon.tokens || !agent.spawnedAt) return null;
|
|
145
169
|
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
if (ceiling > 0) {
|
|
149
|
-
const instanceTokens = this.daemon.tokens.getTokensInWindow(agent.id, spawnedAtMs);
|
|
150
|
-
if (instanceTokens >= ceiling) {
|
|
151
|
-
return {
|
|
152
|
-
reason: 'token_limit_exceeded',
|
|
153
|
-
instanceTokens,
|
|
154
|
-
ceiling,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
}
|
|
170
|
+
const baseCeiling = safety.tokenCeilingPerAgent;
|
|
171
|
+
if (!baseCeiling || baseCeiling <= 0) return null;
|
|
158
172
|
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
173
|
+
const multiplier = this._getRoleMultiplier(agent.role);
|
|
174
|
+
const ceiling = Math.round(baseCeiling * multiplier);
|
|
175
|
+
const spawnedAtMs = new Date(agent.spawnedAt).getTime();
|
|
176
|
+
const instanceTokens = this.daemon.tokens.getTokensInWindow(agent.id, spawnedAtMs);
|
|
177
|
+
|
|
178
|
+
if (instanceTokens >= ceiling) {
|
|
179
|
+
return {
|
|
180
|
+
reason: 'token_limit_exceeded',
|
|
181
|
+
instanceTokens,
|
|
182
|
+
ceiling,
|
|
183
|
+
multiplier,
|
|
184
|
+
};
|
|
171
185
|
}
|
|
172
|
-
|
|
173
186
|
return null;
|
|
174
187
|
}
|
|
175
188
|
|
|
@@ -207,10 +220,7 @@ export class Rotator extends EventEmitter {
|
|
|
207
220
|
// Bypasses cooldown: pathological burn must be stopped immediately.
|
|
208
221
|
const safety = this._checkSafetyTriggers(agent);
|
|
209
222
|
if (safety) {
|
|
210
|
-
|
|
211
|
-
? `${safety.instanceTokens} tokens >= ${safety.ceiling} ceiling`
|
|
212
|
-
: `${safety.velocity} tokens in ${safety.windowMs / 1000}s >= ${safety.threshold} threshold`;
|
|
213
|
-
console.log(` Rotator: ${agent.name} ${safety.reason} (${summary}) — auto-rotating`);
|
|
223
|
+
console.log(` Rotator: ${agent.name} ${safety.reason} (${safety.instanceTokens} tokens >= ${safety.ceiling} ceiling, ${safety.multiplier}x role mult) — auto-rotating`);
|
|
214
224
|
await this.rotate(agent.id, safety);
|
|
215
225
|
continue;
|
|
216
226
|
}
|
|
@@ -479,6 +489,8 @@ export class Rotator extends EventEmitter {
|
|
|
479
489
|
const contextRotations = this.rotationHistory.filter((r) => r.reason === 'context_threshold').length;
|
|
480
490
|
const naturalCompactions = this.rotationHistory.filter((r) => r.reason === 'natural_compaction').length;
|
|
481
491
|
const tokenLimitRotations = this.rotationHistory.filter((r) => r.reason === 'token_limit_exceeded').length;
|
|
492
|
+
// Legacy: velocity rotations are no longer triggered (removed v0.27.2)
|
|
493
|
+
// but historical entries may remain in saved history.
|
|
482
494
|
const velocityRotations = this.rotationHistory.filter((r) => r.reason === 'runaway_velocity').length;
|
|
483
495
|
return {
|
|
484
496
|
enabled: this.enabled,
|