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 +18 -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/rotator.js +32 -49
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +27 -70
- 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/firstrun.js +6 -6
- package/packages/daemon/src/rotator.js +32 -49
- package/packages/gui/package.json +1 -1
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.
|
|
@@ -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
|
|
|
@@ -136,69 +136,53 @@ export class Rotator extends EventEmitter {
|
|
|
136
136
|
return result;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// Per-role safety
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
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
|
|
149
|
+
planner: 10, // heavy exploration by design
|
|
152
150
|
fullstack: 4, // QC auditors read broadly
|
|
153
|
-
analyst: 5,
|
|
154
|
-
security: 4,
|
|
155
|
-
docs: 1,
|
|
151
|
+
analyst: 5,
|
|
152
|
+
security: 4,
|
|
153
|
+
docs: 1,
|
|
156
154
|
};
|
|
157
155
|
return defaults[role] || 1;
|
|
158
156
|
}
|
|
159
157
|
|
|
160
|
-
// Safety
|
|
161
|
-
//
|
|
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
|
-
|
|
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
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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 = () =>
|
|
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, '
|
|
288
|
-
assert.equal(trigger.
|
|
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
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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 () => {
|
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
|
|
|
@@ -136,69 +136,53 @@ export class Rotator extends EventEmitter {
|
|
|
136
136
|
return result;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// Per-role safety
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
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
|
|
149
|
+
planner: 10, // heavy exploration by design
|
|
152
150
|
fullstack: 4, // QC auditors read broadly
|
|
153
|
-
analyst: 5,
|
|
154
|
-
security: 4,
|
|
155
|
-
docs: 1,
|
|
151
|
+
analyst: 5,
|
|
152
|
+
security: 4,
|
|
153
|
+
docs: 1,
|
|
156
154
|
};
|
|
157
155
|
return defaults[role] || 1;
|
|
158
156
|
}
|
|
159
157
|
|
|
160
|
-
// Safety
|
|
161
|
-
//
|
|
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
|
-
|
|
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
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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,
|