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 CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.27.2 — Drop velocity trigger (2026-04-12)
4
+
5
+ Following up on v0.27.1: the role-multiplier fix addressed the planner but left the same false-positive class live for any agent doing heavy exploration on a large codebase. Dropping velocity-based rotation entirely rather than papering over it further.
6
+
7
+ **What changed**
8
+ - Removed `runaway_velocity` as a rotation trigger. The logic is gone — not disabled, gone.
9
+ - `safety.velocityWindowSeconds` and `safety.velocityTokenThreshold` config keys removed.
10
+ - Token ceiling remains as the single safety net. Per-instance ceiling + role multipliers (planner 10×, fullstack/security 4×, analyst 5×) catch genuinely runaway agents without tripping on fast legitimate work.
11
+ - Stats still expose `velocityRotations` count for historical rotations already in `rotation-history.json`.
12
+ - GUI intel-panel's `V:` badge handling preserved for viewing historical events.
13
+
14
+ **Why**
15
+ Velocity alone is a bad stuck-loop signal — legitimate heavy work is fast. The only signal that actually distinguishes a runaway from real work is speed combined with non-productivity (repetitions, errors, file churn). Rather than build the gate now, we're waiting for real usage data to see if earlier-warning is actually needed. The ceiling catches real runaways. Adaptive context rotation catches degradation. If a pattern emerges from heavy real-world use that needs an earlier safety net, it'll be re-added gated on quality signals — not velocity.
16
+
17
+ **Measurement still works.** Pre/post-rotation velocity is still captured in rotation history for savings measurement. We just don't trigger on it.
18
+
19
+ Tests: 221 → 220 (−1 net; added ceiling-only coverage, removed velocity-specific tests).
20
+
21
+ ## v0.27.1 — Seamless rotation hotfix (2026-04-12)
22
+
23
+ Fixes a severe UX regression where planner and other exploration-heavy agents auto-rotated on legitimate activity, breaking the "infinite sessions" promise.
24
+
25
+ **The bug**
26
+ Running a planner on a new project triggered `runaway_velocity` rotation at 2M tokens / 5min — which is normal behavior for a planner reading a codebase. The rotation killed the chat panel, reset the agent, and made the session feel broken. The whole point of context rotation is that it should be invisible to the user.
27
+
28
+ **Fixes**
29
+
30
+ - **Role-based safety multipliers.** Planners, fullstack auditors, analysts, and security roles now have much higher velocity/ceiling thresholds by default. Planner gets 10× (effective 15M/5min), fullstack/security get 4×, analyst 5×. Implementers (backend, frontend) stay at 1×. User-overridable via `safety.roleMultipliers` config.
31
+ - **Handoff brief rewritten for seamless continuation.** The agent is no longer told "this is a context rotation, the previous session is being replaced." It's now instructed to continue the conversation naturally — no greetings, no "resuming", no announcement. From the user's side, the conversation never paused.
32
+ - **User's recent messages included in the brief.** Previously the new agent had no memory of what the user was asking for. Now the last 5 user messages to this agent are carried forward so the conversation resumes with full context.
33
+ - **Chat history always migrates on rotation.** Previously only migrated when the user had the old agent's detail panel open — if they'd navigated away, chat was orphaned forever. Now migrates unconditionally for all agent-keyed state (chat, activity log, token timeline, draft input). Persisted to localStorage immediately.
34
+ - **Rotation toasts silenced.** "Rotating..." and "Rotated, saved X tokens" toasts removed from the default flow. Rotations still fully visible in the dashboard Intel panel for users who want to see them.
35
+
36
+ Tests: 218 → 221 (+3 covering planner multiplier and config overrides).
37
+
3
38
  ## v0.27.0 — Audit-driven release: visibility, safety, Layer 7 memory (2026-04-12)
4
39
 
5
40
  Backend audit after a 275M-token stress test revealed three classes of gaps: blind spots (invisible tokens), unvalidated claims (hardcoded coefficients posing as measurements), and missing memory (every new agent started from scratch). This release closes all three.
package/CLAUDE.md CHANGED
@@ -263,3 +263,10 @@ Audit-driven release. Multi-agent orchestration system with 7 coordination layer
263
263
  - Dashboard: routing donut, cache panel, context health gauges
264
264
  - Monitor/QC agent mode (stay active, loop)
265
265
  - Distribution: demo video, HN launch, Twitter content
266
+
267
+ <!-- GROOVE:START -->
268
+ ## GROOVE Orchestration (auto-injected)
269
+ Active agents: 0
270
+ See AGENTS_REGISTRY.md for full agent state.
271
+ **Memory policy:** Ignore auto-memory. Do not read or write MEMORY.md. GROOVE manages all context.
272
+ <!-- GROOVE:END -->
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.0",
3
+ "version": "0.27.2",
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.2",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -15,15 +15,15 @@ const DEFAULT_CONFIG = {
15
15
  qcThreshold: 4,
16
16
  maxAgents: 10,
17
17
  defaultProvider: 'claude-code',
18
- // Self-healing rotation triggers. Catch pathological agent behavior
19
- // (stuck loops, runaway tool-call cycles) and auto-rotate with fresh
20
- // context. Tokens carry forward; journalist generates handoff brief.
21
- // Set autoRotate=false to disable and get broadcast-only notifications.
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
- `# 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,40 +136,53 @@ export class Rotator extends EventEmitter {
136
136
  return result;
137
137
  }
138
138
 
139
- // Safety triggers runaway agent detection. Scoped to `spawnedAt` so
140
- // a rotation doesn't re-trigger on inherited cumulative tokens.
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 spawnedAtMs = new Date(agent.spawnedAt).getTime();
147
- const ceiling = safety.tokenCeilingPerAgent;
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 windowMs = (safety.velocityWindowSeconds || 300) * 1000;
160
- const velocityThreshold = safety.velocityTokenThreshold;
161
- if (velocityThreshold > 0) {
162
- const velocity = this.daemon.tokens.getVelocity(agent.id, windowMs);
163
- if (velocity >= velocityThreshold) {
164
- return {
165
- reason: 'runaway_velocity',
166
- velocity,
167
- windowMs,
168
- threshold: velocityThreshold,
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
- const summary = safety.reason === 'token_limit_exceeded'
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,
@@ -168,7 +168,7 @@ describe('Journalist', () => {
168
168
  });
169
169
 
170
170
  describe('generateHandoffBrief', () => {
171
- it('should generate a handoff brief for context rotation', async () => {
171
+ it('should generate a handoff brief for seamless session continuation', async () => {
172
172
  const { daemon, grooveDir } = createMockDaemon();
173
173
  const journalist = new Journalist(daemon);
174
174
 
@@ -192,7 +192,10 @@ describe('Journalist', () => {
192
192
  const brief = await journalist.generateHandoffBrief(agent);
193
193
 
194
194
  assert.ok(brief.includes('backend-1'));
195
- assert.ok(brief.includes('context rotation'));
195
+ // The brief must NOT tell the agent to announce a rotation/restart —
196
+ // seamless infinite sessions require the agent to continue naturally.
197
+ assert.ok(brief.includes('seamless') || brief.includes('Continue'));
198
+ assert.ok(!brief.includes('previous session is being replaced'));
196
199
  assert.ok(brief.includes('src/api/**'));
197
200
  assert.ok(brief.includes('5000'));
198
201
  assert.ok(brief.includes('Build the auth API'));
@@ -207,53 +207,58 @@ describe('Rotator', () => {
207
207
  assert.equal(trigger.ceiling, 1_000_000);
208
208
  });
209
209
 
210
- it('fires runaway_velocity when recent burn exceeds threshold', () => {
210
+ it('returns null when ceiling not hit', () => {
211
211
  mockDaemon.config = {
212
- safety: {
213
- autoRotate: true,
214
- tokenCeilingPerAgent: 10_000_000,
215
- velocityWindowSeconds: 300,
216
- velocityTokenThreshold: 1_000_000,
217
- },
212
+ safety: { autoRotate: true, tokenCeilingPerAgent: 5_000_000 },
218
213
  };
219
- mockDaemon.tokens.getTokensInWindow = () => 500_000; // under ceiling
220
- mockDaemon.tokens.getVelocity = () => 1_500_000;
221
-
214
+ mockDaemon.tokens.getTokensInWindow = () => 100_000;
222
215
  const trigger = rotator._checkSafetyTriggers(mkAgent());
223
- assert.equal(trigger.reason, 'runaway_velocity');
224
- assert.equal(trigger.velocity, 1_500_000);
225
- assert.equal(trigger.threshold, 1_000_000);
226
- assert.equal(trigger.windowMs, 300_000);
216
+ assert.equal(trigger, null);
227
217
  });
228
218
 
229
- it('ceiling check takes priority over velocity check', () => {
219
+ it('planner gets a 10x ceiling normal heavy exploration does not trigger', () => {
230
220
  mockDaemon.config = {
231
- safety: {
232
- autoRotate: true,
233
- tokenCeilingPerAgent: 1_000_000,
234
- velocityWindowSeconds: 300,
235
- velocityTokenThreshold: 1_000_000,
236
- },
221
+ safety: { autoRotate: true, tokenCeilingPerAgent: 5_000_000 },
237
222
  };
238
- mockDaemon.tokens.getTokensInWindow = () => 2_000_000;
239
- mockDaemon.tokens.getVelocity = () => 2_000_000;
240
- const trigger = rotator._checkSafetyTriggers(mkAgent());
223
+ // A planner reading a big codebase at 3M tokens would have tripped
224
+ // the old 5M ceiling but has 50M headroom under the role multiplier.
225
+ mockDaemon.tokens.getTokensInWindow = () => 3_000_000;
226
+ const trigger = rotator._checkSafetyTriggers(mkAgent({ role: 'planner' }));
227
+ assert.equal(trigger, null, 'planner should NOT trigger at 3M when base ceiling is 5M');
228
+ });
229
+
230
+ it('planner still triggers on genuinely runaway burn (>50M instance tokens)', () => {
231
+ mockDaemon.config = {
232
+ safety: { autoRotate: true, tokenCeilingPerAgent: 5_000_000 },
233
+ };
234
+ mockDaemon.tokens.getTokensInWindow = () => 60_000_000;
235
+ const trigger = rotator._checkSafetyTriggers(mkAgent({ role: 'planner' }));
241
236
  assert.equal(trigger.reason, 'token_limit_exceeded');
237
+ assert.equal(trigger.ceiling, 50_000_000, 'planner ceiling = 5M × 10');
242
238
  });
243
239
 
244
- it('returns null when neither threshold hit', () => {
240
+ it('role multipliers are config-overridable', () => {
245
241
  mockDaemon.config = {
246
242
  safety: {
247
243
  autoRotate: true,
248
- tokenCeilingPerAgent: 5_000_000,
249
- velocityWindowSeconds: 300,
250
- velocityTokenThreshold: 1_500_000,
244
+ tokenCeilingPerAgent: 1_000_000,
245
+ roleMultipliers: { backend: 2 },
251
246
  },
252
247
  };
253
- mockDaemon.tokens.getTokensInWindow = () => 100_000;
254
- mockDaemon.tokens.getVelocity = () => 10_000;
248
+ mockDaemon.tokens.getTokensInWindow = () => 1_500_000; // above base ceiling, under 2x
249
+ const trigger = rotator._checkSafetyTriggers(mkAgent({ role: 'backend' }));
250
+ assert.equal(trigger, null, 'backend with 2x multiplier should allow 2M ceiling');
251
+ });
252
+
253
+ it('does not trigger on velocity (velocity rotation removed in v0.27.2)', () => {
254
+ mockDaemon.config = {
255
+ safety: { autoRotate: true, tokenCeilingPerAgent: 10_000_000 },
256
+ };
257
+ // Even with huge velocity, no rotation if under ceiling
258
+ mockDaemon.tokens.getTokensInWindow = () => 500_000;
259
+ mockDaemon.tokens.getVelocity = () => 99_999_999;
255
260
  const trigger = rotator._checkSafetyTriggers(mkAgent());
256
- assert.equal(trigger, null);
261
+ assert.equal(trigger, null, 'velocity alone should never trigger a rotation');
257
262
  });
258
263
 
259
264
  it('stats track safety-triggered rotations separately', async () => {