groove-dev 0.27.0 → 0.27.1

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.
@@ -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-eCrVowF0.js"></script>
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">
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.0",
3
+ "version": "0.27.1",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -249,25 +249,52 @@ export const useGrooveStore = create((set, get) => ({
249
249
  break;
250
250
 
251
251
  case 'rotation:start':
252
- get().addToast('info', `Rotating ${msg.agentName}...`);
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
- get().addToast('success', `Rotated ${msg.agentName}`, `Saved ${msg.tokensSaved} tokens`);
257
- const panel = get().detailPanel;
258
- if (panel?.type === 'agent' && panel.agentId === msg.oldAgentId && msg.newAgentId) {
259
- set((s) => {
260
- const chatHistory = { ...s.chatHistory };
261
- const tokenTimeline = { ...s.tokenTimeline };
262
- const activityLog = { ...s.activityLog };
263
- if (chatHistory[msg.oldAgentId]?.length) chatHistory[msg.newAgentId] = [...chatHistory[msg.oldAgentId]];
264
- if (tokenTimeline[msg.oldAgentId]?.length) tokenTimeline[msg.newAgentId] = [...tokenTimeline[msg.oldAgentId]];
265
- if (activityLog[msg.oldAgentId]?.length) activityLog[msg.newAgentId] = [...activityLog[msg.oldAgentId]];
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
- return { chatHistory, tokenTimeline, activityLog, detailPanel: newPanel, teamDetailPanels: { ...s.teamDetailPanels, [tid]: newPanel } };
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.0",
3
+ "version": "0.27.1",
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.0",
3
+ "version": "0.27.1",
4
4
  "description": "GROOVE CLI \u2014 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.0",
3
+ "version": "0.27.1",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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
- `# Agent Handoff Brief`,
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
- `You are continuing the work of **${agent.name}** (role: ${agent.role}).`,
696
- `This is a context rotation the previous session is being replaced to keep context fresh.`,
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
- `## Previous Session`,
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
- `## Instructions`,
735
+ `## How to Respond`,
717
736
  ``,
718
- `Continue the work from where the previous session left off.`,
719
- `Review AGENTS_REGISTRY.md for team awareness.`,
720
- agent.workingDir ? `Stay within your working directory: ${agent.workingDir}` : '',
721
- agent.prompt ? `\nOriginal task: ${agent.prompt}` : '',
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,6 +136,27 @@ export class Rotator extends EventEmitter {
136
136
  return result;
137
137
  }
138
138
 
139
+ // Per-role safety multipliers. Exploration-heavy roles burn tokens fast
140
+ // by design (reading codebases, searching files) — a planner running
141
+ // normally can hit 2M+ tokens in 5 min just reading. One-size-fits-all
142
+ // thresholds produce false positives on exactly the roles that need to
143
+ // read fast. Multipliers scale both the velocity threshold and the
144
+ // instance ceiling. User can override via config.safety.roleMultipliers.
145
+ _getRoleMultiplier(role) {
146
+ const safety = this.daemon.config?.safety;
147
+ const overrides = safety?.roleMultipliers || {};
148
+ if (overrides[role] != null) return overrides[role];
149
+ // Defaults tuned from observed legitimate velocity
150
+ const defaults = {
151
+ planner: 10, // heavy exploration — effectively exempt in practice
152
+ fullstack: 4, // QC auditors read broadly
153
+ analyst: 5, // research/analysis roles
154
+ security: 4, // audit roles
155
+ docs: 1, // focused edits
156
+ };
157
+ return defaults[role] || 1;
158
+ }
159
+
139
160
  // Safety triggers — runaway agent detection. Scoped to `spawnedAt` so
140
161
  // a rotation doesn't re-trigger on inherited cumulative tokens.
141
162
  _checkSafetyTriggers(agent) {
@@ -143,8 +164,11 @@ export class Rotator extends EventEmitter {
143
164
  if (!safety || safety.autoRotate === false) return null;
144
165
  if (!this.daemon.tokens || !agent.spawnedAt) return null;
145
166
 
167
+ const multiplier = this._getRoleMultiplier(agent.role);
146
168
  const spawnedAtMs = new Date(agent.spawnedAt).getTime();
147
- const ceiling = safety.tokenCeilingPerAgent;
169
+
170
+ const baseCeiling = safety.tokenCeilingPerAgent;
171
+ const ceiling = baseCeiling > 0 ? Math.round(baseCeiling * multiplier) : 0;
148
172
  if (ceiling > 0) {
149
173
  const instanceTokens = this.daemon.tokens.getTokensInWindow(agent.id, spawnedAtMs);
150
174
  if (instanceTokens >= ceiling) {
@@ -152,12 +176,16 @@ export class Rotator extends EventEmitter {
152
176
  reason: 'token_limit_exceeded',
153
177
  instanceTokens,
154
178
  ceiling,
179
+ multiplier,
155
180
  };
156
181
  }
157
182
  }
158
183
 
159
184
  const windowMs = (safety.velocityWindowSeconds || 300) * 1000;
160
- const velocityThreshold = safety.velocityTokenThreshold;
185
+ const baseVelocityThreshold = safety.velocityTokenThreshold;
186
+ const velocityThreshold = baseVelocityThreshold > 0
187
+ ? Math.round(baseVelocityThreshold * multiplier)
188
+ : 0;
161
189
  if (velocityThreshold > 0) {
162
190
  const velocity = this.daemon.tokens.getVelocity(agent.id, windowMs);
163
191
  if (velocity >= velocityThreshold) {
@@ -166,6 +194,7 @@ export class Rotator extends EventEmitter {
166
194
  velocity,
167
195
  windowMs,
168
196
  threshold: velocityThreshold,
197
+ multiplier,
169
198
  };
170
199
  }
171
200
  }