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.
- package/CHANGELOG.md +17 -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/journalist.js +29 -9
- package/node_modules/@groove-dev/daemon/src/rotator.js +31 -2
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +5 -2
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +48 -0
- 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/journalist.js +29 -9
- package/packages/daemon/src/rotator.js +31 -2
- 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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.27.1 — Seamless rotation hotfix (2026-04-12)
|
|
4
|
+
|
|
5
|
+
Fixes a severe UX regression where planner and other exploration-heavy agents auto-rotated on legitimate activity, breaking the "infinite sessions" promise.
|
|
6
|
+
|
|
7
|
+
**The bug**
|
|
8
|
+
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.
|
|
9
|
+
|
|
10
|
+
**Fixes**
|
|
11
|
+
|
|
12
|
+
- **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.
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
- **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.
|
|
16
|
+
- **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.
|
|
17
|
+
|
|
18
|
+
Tests: 218 → 221 (+3 covering planner multiplier and config overrides).
|
|
19
|
+
|
|
3
20
|
## v0.27.0 — Audit-driven release: visibility, safety, Layer 7 memory (2026-04-12)
|
|
4
21
|
|
|
5
22
|
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 -->
|
|
@@ -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,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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -168,7 +168,7 @@ describe('Journalist', () => {
|
|
|
168
168
|
});
|
|
169
169
|
|
|
170
170
|
describe('generateHandoffBrief', () => {
|
|
171
|
-
it('should generate a handoff brief for
|
|
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
|
-
|
|
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'));
|
|
@@ -256,6 +256,54 @@ describe('Rotator', () => {
|
|
|
256
256
|
assert.equal(trigger, null);
|
|
257
257
|
});
|
|
258
258
|
|
|
259
|
+
it('planner gets a 10x multiplier so normal exploration does not trigger', () => {
|
|
260
|
+
mockDaemon.config = {
|
|
261
|
+
safety: {
|
|
262
|
+
autoRotate: true,
|
|
263
|
+
tokenCeilingPerAgent: 5_000_000,
|
|
264
|
+
velocityWindowSeconds: 300,
|
|
265
|
+
velocityTokenThreshold: 1_500_000,
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
// 2M in 5min would have triggered under v0.27.0 defaults (user bug report)
|
|
269
|
+
mockDaemon.tokens.getTokensInWindow = () => 3_000_000;
|
|
270
|
+
mockDaemon.tokens.getVelocity = () => 2_053_414;
|
|
271
|
+
const trigger = rotator._checkSafetyTriggers(mkAgent({ role: 'planner' }));
|
|
272
|
+
assert.equal(trigger, null, 'planner should NOT trigger on 2M velocity');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('planner still triggers on genuinely runaway velocity (>15M per 5min)', () => {
|
|
276
|
+
mockDaemon.config = {
|
|
277
|
+
safety: {
|
|
278
|
+
autoRotate: true,
|
|
279
|
+
tokenCeilingPerAgent: 5_000_000,
|
|
280
|
+
velocityWindowSeconds: 300,
|
|
281
|
+
velocityTokenThreshold: 1_500_000,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
mockDaemon.tokens.getTokensInWindow = () => 1_000_000;
|
|
285
|
+
mockDaemon.tokens.getVelocity = () => 20_000_000;
|
|
286
|
+
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');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('role multipliers are config-overridable', () => {
|
|
292
|
+
mockDaemon.config = {
|
|
293
|
+
safety: {
|
|
294
|
+
autoRotate: true,
|
|
295
|
+
tokenCeilingPerAgent: 1_000_000,
|
|
296
|
+
velocityWindowSeconds: 300,
|
|
297
|
+
velocityTokenThreshold: 500_000,
|
|
298
|
+
roleMultipliers: { backend: 2, frontend: 2 },
|
|
299
|
+
},
|
|
300
|
+
};
|
|
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');
|
|
305
|
+
});
|
|
306
|
+
|
|
259
307
|
it('stats track safety-triggered rotations separately', async () => {
|
|
260
308
|
mockDaemon.registry.agents = [mkAgent({ tokensUsed: 1_200_000 })];
|
|
261
309
|
await rotator.rotate('a1', {
|