groove-dev 0.27.192 → 0.27.194
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/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/innerchat.js +9 -3
- package/node_modules/@groove-dev/daemon/src/process.js +15 -5
- package/node_modules/@groove-dev/daemon/test/innerchat.test.js +25 -3
- package/node_modules/@groove-dev/daemon/test/pending-messages.test.js +60 -0
- 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/innerchat.js +9 -3
- package/packages/daemon/src/process.js +15 -5
- package/packages/gui/package.json +1 -1
|
@@ -131,7 +131,13 @@ export class InnerChat {
|
|
|
131
131
|
};
|
|
132
132
|
thread.turns.push(turn);
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
// Only replay earlier turns when the recipient has LOST them. A running
|
|
135
|
+
// agent already has this thread's history in its live session — re-injecting
|
|
136
|
+
// it every exchange duplicates content and balloons context. A stopped agent
|
|
137
|
+
// will be resumed/rotated (fresh context), so it needs the recap.
|
|
138
|
+
const recipientHasContext =
|
|
139
|
+
this.daemon.processes.isRunning(toAgent.id) || this.daemon.processes.hasAgentLoop(toAgent.id);
|
|
140
|
+
const wrapped = this._wrap(thread, fromAgent, toAgent, message, kind, !recipientHasContext);
|
|
135
141
|
|
|
136
142
|
let result;
|
|
137
143
|
try {
|
|
@@ -322,11 +328,11 @@ export class InnerChat {
|
|
|
322
328
|
// turns to follow a continuing conversation. The closing instruction differs
|
|
323
329
|
// by kind: an ask blocks the sender (answer now), a tell does not (answer
|
|
324
330
|
// when you reach a good stopping point).
|
|
325
|
-
_wrap(thread, fromAgent, toAgent, message, kind = 'ask') {
|
|
331
|
+
_wrap(thread, fromAgent, toAgent, message, kind = 'ask', includePrior = true) {
|
|
326
332
|
const header = kind === 'tell'
|
|
327
333
|
? `[InnerChat — ${fromAgent.name} (${fromAgent.role}) sent you a message]`
|
|
328
334
|
: `[InnerChat — ${fromAgent.name} (${fromAgent.role}) is asking you a question]`;
|
|
329
|
-
const prior = thread.turns.slice(0, -1).slice(-CONTEXT_TURNS);
|
|
335
|
+
const prior = includePrior ? thread.turns.slice(0, -1).slice(-CONTEXT_TURNS) : [];
|
|
330
336
|
const lines = [header, ''];
|
|
331
337
|
|
|
332
338
|
if (prior.length) {
|
|
@@ -367,7 +367,7 @@ export class ProcessManager {
|
|
|
367
367
|
this.daemon = daemon;
|
|
368
368
|
this.handles = new Map(); // agentId -> { proc, logStream }
|
|
369
369
|
this.peakContextUsage = new Map(); // agentId -> highest contextUsage seen
|
|
370
|
-
this.pendingMessages = new Map(); // agentId -> { message, timestamp }
|
|
370
|
+
this.pendingMessages = new Map(); // agentId -> [{ message, timestamp }, …] FIFO queue
|
|
371
371
|
this._streamThrottle = new Map(); // agentId -> { timer, pending }
|
|
372
372
|
this._rotatingAgents = new Set(); // agentIds currently being rotated (rotator wrote handoff)
|
|
373
373
|
this._stalledAgents = new Set(); // agentIds already flagged as stalled (avoids duplicate broadcasts)
|
|
@@ -3222,7 +3222,13 @@ After fixing all issues, run tests (npm test) and build (npm run build) to verif
|
|
|
3222
3222
|
queueMessage(agentId, message) {
|
|
3223
3223
|
const agent = this.daemon.registry.get(agentId);
|
|
3224
3224
|
const wrapped = agent ? wrapWithRoleReminder(agent.role, message) : message;
|
|
3225
|
-
|
|
3225
|
+
// Append to a FIFO queue rather than overwriting the slot. Two messages
|
|
3226
|
+
// arriving while an agent is busy — a user chat and an InnerChat message,
|
|
3227
|
+
// or two different agents reaching out — must BOTH survive. The old
|
|
3228
|
+
// single-slot version silently dropped whichever arrived first.
|
|
3229
|
+
const queue = this.pendingMessages.get(agentId) || [];
|
|
3230
|
+
queue.push({ message: wrapped, timestamp: Date.now() });
|
|
3231
|
+
this.pendingMessages.set(agentId, queue);
|
|
3226
3232
|
if (this.daemon.rotator) {
|
|
3227
3233
|
this.daemon.rotator.recordUserMessage(agentId);
|
|
3228
3234
|
}
|
|
@@ -3230,9 +3236,13 @@ After fixing all issues, run tests (npm test) and build (npm run build) to verif
|
|
|
3230
3236
|
}
|
|
3231
3237
|
|
|
3232
3238
|
consumePendingMessage(agentId) {
|
|
3233
|
-
const
|
|
3234
|
-
if (
|
|
3235
|
-
|
|
3239
|
+
const queue = this.pendingMessages.get(agentId);
|
|
3240
|
+
if (!queue || queue.length === 0) return null;
|
|
3241
|
+
this.pendingMessages.delete(agentId);
|
|
3242
|
+
// Deliver everything queued in one turn, in arrival order, clearly
|
|
3243
|
+
// separated so the agent can tell the messages apart.
|
|
3244
|
+
const message = queue.map((q) => q.message).join('\n\n---\n\n');
|
|
3245
|
+
return { message, timestamp: queue[0].timestamp, count: queue.length };
|
|
3236
3246
|
}
|
|
3237
3247
|
|
|
3238
3248
|
async killAll() {
|
|
@@ -232,7 +232,9 @@ describe('InnerChat', () => {
|
|
|
232
232
|
|
|
233
233
|
// ── Conversation context ──────────────────────────────────
|
|
234
234
|
|
|
235
|
-
it('
|
|
235
|
+
it('does NOT replay prior turns to a running agent (it already has them)', async () => {
|
|
236
|
+
// a2 stays running throughout — re-injecting history it already holds would
|
|
237
|
+
// balloon its context.
|
|
236
238
|
const p1 = innerchat.ask('a1', 'a2', 'What shape?');
|
|
237
239
|
await tick(); innerchat.onAgentOutput('a2', result('REST'));
|
|
238
240
|
await p1;
|
|
@@ -240,12 +242,32 @@ describe('InnerChat', () => {
|
|
|
240
242
|
const p2 = innerchat.ask('a1', 'a2', 'Versioned?');
|
|
241
243
|
await tick();
|
|
242
244
|
const msg = daemon.sent.at(-1).msg;
|
|
245
|
+
assert.doesNotMatch(msg, /Earlier in this conversation/, 'no redundant recap to a live agent');
|
|
246
|
+
assert.match(msg, /Versioned\?/);
|
|
247
|
+
|
|
248
|
+
innerchat.onAgentOutput('a2', result('v2'));
|
|
249
|
+
await p2;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('DOES replay prior turns when the recipient was stopped (lost its context)', async () => {
|
|
253
|
+
const p1 = innerchat.ask('a1', 'a2', 'What shape?');
|
|
254
|
+
await tick(); innerchat.onAgentOutput('a2', result('REST'));
|
|
255
|
+
await p1;
|
|
256
|
+
|
|
257
|
+
// a2 ends its turn — a resume/rotate gives it a fresh context, so it needs
|
|
258
|
+
// the recap to follow the thread.
|
|
259
|
+
daemon.processes._loops.delete('a2');
|
|
260
|
+
daemon.processes._running.delete('a2');
|
|
261
|
+
|
|
262
|
+
const p2 = innerchat.ask('a1', 'a2', 'Versioned?');
|
|
263
|
+
await tick();
|
|
264
|
+
const newId = daemon.resumes.at(-1).newId;
|
|
265
|
+
const msg = daemon.resumes.at(-1).msg;
|
|
243
266
|
assert.match(msg, /Earlier in this conversation/);
|
|
244
267
|
assert.match(msg, /fullstack-1: What shape\?/);
|
|
245
268
|
assert.match(msg, /fullstack-14: REST/);
|
|
246
|
-
assert.match(msg, /Versioned\?/);
|
|
247
269
|
|
|
248
|
-
innerchat.onAgentOutput(
|
|
270
|
+
innerchat.onAgentOutput(newId, result('v2'));
|
|
249
271
|
await p2;
|
|
250
272
|
});
|
|
251
273
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// GROOVE — Pending message queue tests
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { ProcessManager } from '../src/process.js';
|
|
7
|
+
|
|
8
|
+
function makeDaemon() {
|
|
9
|
+
return {
|
|
10
|
+
registry: { get: (id) => ({ id, role: 'fullstack', name: id }) },
|
|
11
|
+
rotator: { recordUserMessage() {} },
|
|
12
|
+
broadcast() {},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('pending message queue', () => {
|
|
17
|
+
let pm;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => { pm = new ProcessManager(makeDaemon()); });
|
|
20
|
+
afterEach(() => { if (pm._stallWatchdog) clearInterval(pm._stallWatchdog); });
|
|
21
|
+
|
|
22
|
+
it('keeps BOTH messages when two arrive while an agent is busy', () => {
|
|
23
|
+
pm.queueMessage('a1', 'message from the user');
|
|
24
|
+
pm.queueMessage('a1', 'question from another agent');
|
|
25
|
+
|
|
26
|
+
const drained = pm.consumePendingMessage('a1');
|
|
27
|
+
assert.equal(drained.count, 2);
|
|
28
|
+
assert.match(drained.message, /message from the user/);
|
|
29
|
+
assert.match(drained.message, /question from another agent/);
|
|
30
|
+
// arrival order preserved
|
|
31
|
+
assert.ok(drained.message.indexOf('user') < drained.message.indexOf('another agent'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('drains the queue so nothing is delivered twice', () => {
|
|
35
|
+
pm.queueMessage('a1', 'one');
|
|
36
|
+
pm.consumePendingMessage('a1');
|
|
37
|
+
assert.equal(pm.consumePendingMessage('a1'), null);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns null when nothing is queued', () => {
|
|
41
|
+
assert.equal(pm.consumePendingMessage('nobody'), null);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('queues are independent per agent', () => {
|
|
45
|
+
pm.queueMessage('a1', 'for a1');
|
|
46
|
+
pm.queueMessage('a2', 'for a2');
|
|
47
|
+
assert.match(pm.consumePendingMessage('a1').message, /for a1/);
|
|
48
|
+
assert.match(pm.consumePendingMessage('a2').message, /for a2/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('preserves order across three queued messages', () => {
|
|
52
|
+
pm.queueMessage('a1', 'first');
|
|
53
|
+
pm.queueMessage('a1', 'second');
|
|
54
|
+
pm.queueMessage('a1', 'third');
|
|
55
|
+
const { message, count } = pm.consumePendingMessage('a1');
|
|
56
|
+
assert.equal(count, 3);
|
|
57
|
+
assert.ok(message.indexOf('first') < message.indexOf('second'));
|
|
58
|
+
assert.ok(message.indexOf('second') < message.indexOf('third'));
|
|
59
|
+
});
|
|
60
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.194",
|
|
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)",
|
|
@@ -131,7 +131,13 @@ export class InnerChat {
|
|
|
131
131
|
};
|
|
132
132
|
thread.turns.push(turn);
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
// Only replay earlier turns when the recipient has LOST them. A running
|
|
135
|
+
// agent already has this thread's history in its live session — re-injecting
|
|
136
|
+
// it every exchange duplicates content and balloons context. A stopped agent
|
|
137
|
+
// will be resumed/rotated (fresh context), so it needs the recap.
|
|
138
|
+
const recipientHasContext =
|
|
139
|
+
this.daemon.processes.isRunning(toAgent.id) || this.daemon.processes.hasAgentLoop(toAgent.id);
|
|
140
|
+
const wrapped = this._wrap(thread, fromAgent, toAgent, message, kind, !recipientHasContext);
|
|
135
141
|
|
|
136
142
|
let result;
|
|
137
143
|
try {
|
|
@@ -322,11 +328,11 @@ export class InnerChat {
|
|
|
322
328
|
// turns to follow a continuing conversation. The closing instruction differs
|
|
323
329
|
// by kind: an ask blocks the sender (answer now), a tell does not (answer
|
|
324
330
|
// when you reach a good stopping point).
|
|
325
|
-
_wrap(thread, fromAgent, toAgent, message, kind = 'ask') {
|
|
331
|
+
_wrap(thread, fromAgent, toAgent, message, kind = 'ask', includePrior = true) {
|
|
326
332
|
const header = kind === 'tell'
|
|
327
333
|
? `[InnerChat — ${fromAgent.name} (${fromAgent.role}) sent you a message]`
|
|
328
334
|
: `[InnerChat — ${fromAgent.name} (${fromAgent.role}) is asking you a question]`;
|
|
329
|
-
const prior = thread.turns.slice(0, -1).slice(-CONTEXT_TURNS);
|
|
335
|
+
const prior = includePrior ? thread.turns.slice(0, -1).slice(-CONTEXT_TURNS) : [];
|
|
330
336
|
const lines = [header, ''];
|
|
331
337
|
|
|
332
338
|
if (prior.length) {
|
|
@@ -367,7 +367,7 @@ export class ProcessManager {
|
|
|
367
367
|
this.daemon = daemon;
|
|
368
368
|
this.handles = new Map(); // agentId -> { proc, logStream }
|
|
369
369
|
this.peakContextUsage = new Map(); // agentId -> highest contextUsage seen
|
|
370
|
-
this.pendingMessages = new Map(); // agentId -> { message, timestamp }
|
|
370
|
+
this.pendingMessages = new Map(); // agentId -> [{ message, timestamp }, …] FIFO queue
|
|
371
371
|
this._streamThrottle = new Map(); // agentId -> { timer, pending }
|
|
372
372
|
this._rotatingAgents = new Set(); // agentIds currently being rotated (rotator wrote handoff)
|
|
373
373
|
this._stalledAgents = new Set(); // agentIds already flagged as stalled (avoids duplicate broadcasts)
|
|
@@ -3222,7 +3222,13 @@ After fixing all issues, run tests (npm test) and build (npm run build) to verif
|
|
|
3222
3222
|
queueMessage(agentId, message) {
|
|
3223
3223
|
const agent = this.daemon.registry.get(agentId);
|
|
3224
3224
|
const wrapped = agent ? wrapWithRoleReminder(agent.role, message) : message;
|
|
3225
|
-
|
|
3225
|
+
// Append to a FIFO queue rather than overwriting the slot. Two messages
|
|
3226
|
+
// arriving while an agent is busy — a user chat and an InnerChat message,
|
|
3227
|
+
// or two different agents reaching out — must BOTH survive. The old
|
|
3228
|
+
// single-slot version silently dropped whichever arrived first.
|
|
3229
|
+
const queue = this.pendingMessages.get(agentId) || [];
|
|
3230
|
+
queue.push({ message: wrapped, timestamp: Date.now() });
|
|
3231
|
+
this.pendingMessages.set(agentId, queue);
|
|
3226
3232
|
if (this.daemon.rotator) {
|
|
3227
3233
|
this.daemon.rotator.recordUserMessage(agentId);
|
|
3228
3234
|
}
|
|
@@ -3230,9 +3236,13 @@ After fixing all issues, run tests (npm test) and build (npm run build) to verif
|
|
|
3230
3236
|
}
|
|
3231
3237
|
|
|
3232
3238
|
consumePendingMessage(agentId) {
|
|
3233
|
-
const
|
|
3234
|
-
if (
|
|
3235
|
-
|
|
3239
|
+
const queue = this.pendingMessages.get(agentId);
|
|
3240
|
+
if (!queue || queue.length === 0) return null;
|
|
3241
|
+
this.pendingMessages.delete(agentId);
|
|
3242
|
+
// Deliver everything queued in one turn, in arrival order, clearly
|
|
3243
|
+
// separated so the agent can tell the messages apart.
|
|
3244
|
+
const message = queue.map((q) => q.message).join('\n\n---\n\n');
|
|
3245
|
+
return { message, timestamp: queue[0].timestamp, count: queue.length };
|
|
3236
3246
|
}
|
|
3237
3247
|
|
|
3238
3248
|
async killAll() {
|