groove-dev 0.27.1 → 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,23 @@
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
+
3
21
  ## v0.27.1 — Seamless rotation hotfix (2026-04-12)
4
22
 
5
23
  Fixes a severe UX regression where planner and other exploration-heavy agents auto-rotated on legitimate activity, breaking the "infinite sessions" promise.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.1",
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.1",
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
 
@@ -136,69 +136,53 @@ 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.
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.
145
144
  _getRoleMultiplier(role) {
146
145
  const safety = this.daemon.config?.safety;
147
146
  const overrides = safety?.roleMultipliers || {};
148
147
  if (overrides[role] != null) return overrides[role];
149
- // Defaults tuned from observed legitimate velocity
150
148
  const defaults = {
151
- planner: 10, // heavy exploration effectively exempt in practice
149
+ planner: 10, // heavy exploration by design
152
150
  fullstack: 4, // QC auditors read broadly
153
- analyst: 5, // research/analysis roles
154
- security: 4, // audit roles
155
- docs: 1, // focused edits
151
+ analyst: 5,
152
+ security: 4,
153
+ docs: 1,
156
154
  };
157
155
  return defaults[role] || 1;
158
156
  }
159
157
 
160
- // Safety triggers — runaway agent detection. Scoped to `spawnedAt` so
161
- // a rotation doesn't re-trigger on inherited cumulative tokens.
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.
162
165
  _checkSafetyTriggers(agent) {
163
166
  const safety = this.daemon.config?.safety;
164
167
  if (!safety || safety.autoRotate === false) return null;
165
168
  if (!this.daemon.tokens || !agent.spawnedAt) return null;
166
169
 
167
- const multiplier = this._getRoleMultiplier(agent.role);
168
- const spawnedAtMs = new Date(agent.spawnedAt).getTime();
169
-
170
170
  const baseCeiling = safety.tokenCeilingPerAgent;
171
- const ceiling = baseCeiling > 0 ? Math.round(baseCeiling * multiplier) : 0;
172
- if (ceiling > 0) {
173
- const instanceTokens = this.daemon.tokens.getTokensInWindow(agent.id, spawnedAtMs);
174
- if (instanceTokens >= ceiling) {
175
- return {
176
- reason: 'token_limit_exceeded',
177
- instanceTokens,
178
- ceiling,
179
- multiplier,
180
- };
181
- }
182
- }
171
+ if (!baseCeiling || baseCeiling <= 0) return null;
183
172
 
184
- const windowMs = (safety.velocityWindowSeconds || 300) * 1000;
185
- const baseVelocityThreshold = safety.velocityTokenThreshold;
186
- const velocityThreshold = baseVelocityThreshold > 0
187
- ? Math.round(baseVelocityThreshold * multiplier)
188
- : 0;
189
- if (velocityThreshold > 0) {
190
- const velocity = this.daemon.tokens.getVelocity(agent.id, windowMs);
191
- if (velocity >= velocityThreshold) {
192
- return {
193
- reason: 'runaway_velocity',
194
- velocity,
195
- windowMs,
196
- threshold: velocityThreshold,
197
- multiplier,
198
- };
199
- }
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
+ };
200
185
  }
201
-
202
186
  return null;
203
187
  }
204
188
 
@@ -236,10 +220,7 @@ export class Rotator extends EventEmitter {
236
220
  // Bypasses cooldown: pathological burn must be stopped immediately.
237
221
  const safety = this._checkSafetyTriggers(agent);
238
222
  if (safety) {
239
- const summary = safety.reason === 'token_limit_exceeded'
240
- ? `${safety.instanceTokens} tokens >= ${safety.ceiling} ceiling`
241
- : `${safety.velocity} tokens in ${safety.windowMs / 1000}s >= ${safety.threshold} threshold`;
242
- 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`);
243
224
  await this.rotate(agent.id, safety);
244
225
  continue;
245
226
  }
@@ -508,6 +489,8 @@ export class Rotator extends EventEmitter {
508
489
  const contextRotations = this.rotationHistory.filter((r) => r.reason === 'context_threshold').length;
509
490
  const naturalCompactions = this.rotationHistory.filter((r) => r.reason === 'natural_compaction').length;
510
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.
511
494
  const velocityRotations = this.rotationHistory.filter((r) => r.reason === 'runaway_velocity').length;
512
495
  return {
513
496
  enabled: this.enabled,
@@ -207,85 +207,34 @@ 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
- },
218
- };
219
- mockDaemon.tokens.getTokensInWindow = () => 500_000; // under ceiling
220
- mockDaemon.tokens.getVelocity = () => 1_500_000;
221
-
222
- 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);
227
- });
228
-
229
- it('ceiling check takes priority over velocity check', () => {
230
- mockDaemon.config = {
231
- safety: {
232
- autoRotate: true,
233
- tokenCeilingPerAgent: 1_000_000,
234
- velocityWindowSeconds: 300,
235
- velocityTokenThreshold: 1_000_000,
236
- },
237
- };
238
- mockDaemon.tokens.getTokensInWindow = () => 2_000_000;
239
- mockDaemon.tokens.getVelocity = () => 2_000_000;
240
- const trigger = rotator._checkSafetyTriggers(mkAgent());
241
- assert.equal(trigger.reason, 'token_limit_exceeded');
242
- });
243
-
244
- it('returns null when neither threshold hit', () => {
245
- mockDaemon.config = {
246
- safety: {
247
- autoRotate: true,
248
- tokenCeilingPerAgent: 5_000_000,
249
- velocityWindowSeconds: 300,
250
- velocityTokenThreshold: 1_500_000,
251
- },
212
+ safety: { autoRotate: true, tokenCeilingPerAgent: 5_000_000 },
252
213
  };
253
214
  mockDaemon.tokens.getTokensInWindow = () => 100_000;
254
- mockDaemon.tokens.getVelocity = () => 10_000;
255
215
  const trigger = rotator._checkSafetyTriggers(mkAgent());
256
216
  assert.equal(trigger, null);
257
217
  });
258
218
 
259
- it('planner gets a 10x multiplier so normal exploration does not trigger', () => {
219
+ it('planner gets a 10x ceiling normal heavy exploration does not trigger', () => {
260
220
  mockDaemon.config = {
261
- safety: {
262
- autoRotate: true,
263
- tokenCeilingPerAgent: 5_000_000,
264
- velocityWindowSeconds: 300,
265
- velocityTokenThreshold: 1_500_000,
266
- },
221
+ safety: { autoRotate: true, tokenCeilingPerAgent: 5_000_000 },
267
222
  };
268
- // 2M in 5min would have triggered under v0.27.0 defaults (user bug report)
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.
269
225
  mockDaemon.tokens.getTokensInWindow = () => 3_000_000;
270
- mockDaemon.tokens.getVelocity = () => 2_053_414;
271
226
  const trigger = rotator._checkSafetyTriggers(mkAgent({ role: 'planner' }));
272
- assert.equal(trigger, null, 'planner should NOT trigger on 2M velocity');
227
+ assert.equal(trigger, null, 'planner should NOT trigger at 3M when base ceiling is 5M');
273
228
  });
274
229
 
275
- it('planner still triggers on genuinely runaway velocity (>15M per 5min)', () => {
230
+ it('planner still triggers on genuinely runaway burn (>50M instance tokens)', () => {
276
231
  mockDaemon.config = {
277
- safety: {
278
- autoRotate: true,
279
- tokenCeilingPerAgent: 5_000_000,
280
- velocityWindowSeconds: 300,
281
- velocityTokenThreshold: 1_500_000,
282
- },
232
+ safety: { autoRotate: true, tokenCeilingPerAgent: 5_000_000 },
283
233
  };
284
- mockDaemon.tokens.getTokensInWindow = () => 1_000_000;
285
- mockDaemon.tokens.getVelocity = () => 20_000_000;
234
+ mockDaemon.tokens.getTokensInWindow = () => 60_000_000;
286
235
  const trigger = rotator._checkSafetyTriggers(mkAgent({ role: 'planner' }));
287
- assert.equal(trigger.reason, 'runaway_velocity');
288
- assert.equal(trigger.threshold, 15_000_000, 'planner threshold = 1.5M × 10');
236
+ assert.equal(trigger.reason, 'token_limit_exceeded');
237
+ assert.equal(trigger.ceiling, 50_000_000, 'planner ceiling = 5M × 10');
289
238
  });
290
239
 
291
240
  it('role multipliers are config-overridable', () => {
@@ -293,15 +242,23 @@ describe('Rotator', () => {
293
242
  safety: {
294
243
  autoRotate: true,
295
244
  tokenCeilingPerAgent: 1_000_000,
296
- velocityWindowSeconds: 300,
297
- velocityTokenThreshold: 500_000,
298
- roleMultipliers: { backend: 2, frontend: 2 },
245
+ roleMultipliers: { backend: 2 },
299
246
  },
300
247
  };
301
- mockDaemon.tokens.getTokensInWindow = () => 1_500_000; // above base ceiling
302
- mockDaemon.tokens.getVelocity = () => 0;
303
- const backendTrigger = rotator._checkSafetyTriggers(mkAgent({ role: 'backend' }));
304
- assert.equal(backendTrigger, null, 'backend with 2x multiplier should allow 2M ceiling');
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;
260
+ const trigger = rotator._checkSafetyTriggers(mkAgent());
261
+ assert.equal(trigger, null, 'velocity alone should never trigger a rotation');
305
262
  });
306
263
 
307
264
  it('stats track safety-triggered rotations separately', async () => {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.1",
3
+ "version": "0.27.2",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.1",
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)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.1",
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.1",
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
 
@@ -136,69 +136,53 @@ 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.
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.
145
144
  _getRoleMultiplier(role) {
146
145
  const safety = this.daemon.config?.safety;
147
146
  const overrides = safety?.roleMultipliers || {};
148
147
  if (overrides[role] != null) return overrides[role];
149
- // Defaults tuned from observed legitimate velocity
150
148
  const defaults = {
151
- planner: 10, // heavy exploration effectively exempt in practice
149
+ planner: 10, // heavy exploration by design
152
150
  fullstack: 4, // QC auditors read broadly
153
- analyst: 5, // research/analysis roles
154
- security: 4, // audit roles
155
- docs: 1, // focused edits
151
+ analyst: 5,
152
+ security: 4,
153
+ docs: 1,
156
154
  };
157
155
  return defaults[role] || 1;
158
156
  }
159
157
 
160
- // Safety triggers — runaway agent detection. Scoped to `spawnedAt` so
161
- // a rotation doesn't re-trigger on inherited cumulative tokens.
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.
162
165
  _checkSafetyTriggers(agent) {
163
166
  const safety = this.daemon.config?.safety;
164
167
  if (!safety || safety.autoRotate === false) return null;
165
168
  if (!this.daemon.tokens || !agent.spawnedAt) return null;
166
169
 
167
- const multiplier = this._getRoleMultiplier(agent.role);
168
- const spawnedAtMs = new Date(agent.spawnedAt).getTime();
169
-
170
170
  const baseCeiling = safety.tokenCeilingPerAgent;
171
- const ceiling = baseCeiling > 0 ? Math.round(baseCeiling * multiplier) : 0;
172
- if (ceiling > 0) {
173
- const instanceTokens = this.daemon.tokens.getTokensInWindow(agent.id, spawnedAtMs);
174
- if (instanceTokens >= ceiling) {
175
- return {
176
- reason: 'token_limit_exceeded',
177
- instanceTokens,
178
- ceiling,
179
- multiplier,
180
- };
181
- }
182
- }
171
+ if (!baseCeiling || baseCeiling <= 0) return null;
183
172
 
184
- const windowMs = (safety.velocityWindowSeconds || 300) * 1000;
185
- const baseVelocityThreshold = safety.velocityTokenThreshold;
186
- const velocityThreshold = baseVelocityThreshold > 0
187
- ? Math.round(baseVelocityThreshold * multiplier)
188
- : 0;
189
- if (velocityThreshold > 0) {
190
- const velocity = this.daemon.tokens.getVelocity(agent.id, windowMs);
191
- if (velocity >= velocityThreshold) {
192
- return {
193
- reason: 'runaway_velocity',
194
- velocity,
195
- windowMs,
196
- threshold: velocityThreshold,
197
- multiplier,
198
- };
199
- }
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
+ };
200
185
  }
201
-
202
186
  return null;
203
187
  }
204
188
 
@@ -236,10 +220,7 @@ export class Rotator extends EventEmitter {
236
220
  // Bypasses cooldown: pathological burn must be stopped immediately.
237
221
  const safety = this._checkSafetyTriggers(agent);
238
222
  if (safety) {
239
- const summary = safety.reason === 'token_limit_exceeded'
240
- ? `${safety.instanceTokens} tokens >= ${safety.ceiling} ceiling`
241
- : `${safety.velocity} tokens in ${safety.windowMs / 1000}s >= ${safety.threshold} threshold`;
242
- 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`);
243
224
  await this.rotate(agent.id, safety);
244
225
  continue;
245
226
  }
@@ -508,6 +489,8 @@ export class Rotator extends EventEmitter {
508
489
  const contextRotations = this.rotationHistory.filter((r) => r.reason === 'context_threshold').length;
509
490
  const naturalCompactions = this.rotationHistory.filter((r) => r.reason === 'natural_compaction').length;
510
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.
511
494
  const velocityRotations = this.rotationHistory.filter((r) => r.reason === 'runaway_velocity').length;
512
495
  return {
513
496
  enabled: this.enabled,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.1",
3
+ "version": "0.27.2",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",