groove-dev 0.27.193 → 0.27.195
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/deliver.js +5 -1
- package/node_modules/@groove-dev/daemon/src/process.js +60 -5
- 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/deliver.js +5 -1
- package/packages/daemon/src/process.js +60 -5
- package/packages/gui/package.json +1 -1
|
@@ -38,7 +38,11 @@ export async function deliverInstruction(daemon, agentId, message, opts = {}) {
|
|
|
38
38
|
if (daemon.rotator) daemon.rotator.recordUserMessage(agentId);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
// Prepend a fresh wall-clock anchor so the agent gauges progress by real time
|
|
42
|
+
// and the user's direction, not by how much context has piled up.
|
|
43
|
+
const clock = daemon.processes.sessionClock?.(agent);
|
|
44
|
+
const timedMessage = clock ? `${clock}\n\n${finalMessage}` : finalMessage;
|
|
45
|
+
const wrappedMessage = wrapWithRoleReminder(agent.role, timedMessage);
|
|
42
46
|
|
|
43
47
|
// Agent loop path — send straight to the running loop.
|
|
44
48
|
if (daemon.processes.hasAgentLoop(agentId)) {
|
|
@@ -362,12 +362,37 @@ export function wrapWithRoleReminder(role, message) {
|
|
|
362
362
|
return message;
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
// A fresh wall-clock anchor, regenerated for every message an agent receives.
|
|
366
|
+
// Agents have no innate sense of elapsed time — they infer "how far along are
|
|
367
|
+
// we" from how much context has piled up, so 15 minutes of dense work (and the
|
|
368
|
+
// session-feel that rotations add) reads as a long day and they start winding
|
|
369
|
+
// down. This gives them the real time and duration each turn, plus an explicit
|
|
370
|
+
// frame that the session is live and open-ended.
|
|
371
|
+
export function sessionClockLine(startISO) {
|
|
372
|
+
const now = new Date();
|
|
373
|
+
const when = now.toLocaleString('en-US', {
|
|
374
|
+
weekday: 'short', month: 'short', day: 'numeric',
|
|
375
|
+
hour: 'numeric', minute: '2-digit', hour12: true,
|
|
376
|
+
});
|
|
377
|
+
let dur = '';
|
|
378
|
+
if (startISO) {
|
|
379
|
+
const mins = Math.max(0, Math.round((now - new Date(startISO)) / 60000));
|
|
380
|
+
const human = mins < 60 ? `${mins} min` : `${Math.floor(mins / 60)}h ${mins % 60}m`;
|
|
381
|
+
dur = ` You've been in this work session about ${human}.`;
|
|
382
|
+
}
|
|
383
|
+
return `[Clock — ${when}.${dur} This is a live session with the user present and engaged. `
|
|
384
|
+
+ 'Judge how far along you are by the task and the user\'s direction, not by how much has '
|
|
385
|
+
+ 'been built or a felt sense of time. Do not wind down, "call it a day," or trim scope on '
|
|
386
|
+
+ 'your own — keep working until the user says to stop.]';
|
|
387
|
+
}
|
|
388
|
+
|
|
365
389
|
export class ProcessManager {
|
|
366
390
|
constructor(daemon) {
|
|
367
391
|
this.daemon = daemon;
|
|
368
392
|
this.handles = new Map(); // agentId -> { proc, logStream }
|
|
369
393
|
this.peakContextUsage = new Map(); // agentId -> highest contextUsage seen
|
|
370
|
-
this.pendingMessages = new Map(); // agentId -> { message, timestamp }
|
|
394
|
+
this.pendingMessages = new Map(); // agentId -> [{ message, timestamp }, …] FIFO queue
|
|
395
|
+
this.sessionStarts = new Map(); // agentName -> ISO start; keyed by name so it survives rotation
|
|
371
396
|
this._streamThrottle = new Map(); // agentId -> { timer, pending }
|
|
372
397
|
this._rotatingAgents = new Set(); // agentIds currently being rotated (rotator wrote handoff)
|
|
373
398
|
this._stalledAgents = new Set(); // agentIds already flagged as stalled (avoids duplicate broadcasts)
|
|
@@ -1184,6 +1209,12 @@ For normal file edits within your scope, proceed without review.
|
|
|
1184
1209
|
}
|
|
1185
1210
|
}
|
|
1186
1211
|
|
|
1212
|
+
// Give the agent a wall-clock anchor on its very first turn. sessionClock
|
|
1213
|
+
// records the session start (by name, so a later rotation keeps it).
|
|
1214
|
+
if (!isOneShotProvider) {
|
|
1215
|
+
spawnConfig.prompt = `${this.sessionClock(agent)}\n\n${spawnConfig.prompt}`;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1187
1218
|
// Set up log capture (shared between CLI and agent loop paths)
|
|
1188
1219
|
const logDir = resolve(this.daemon.grooveDir, 'logs');
|
|
1189
1220
|
mkdirSync(logDir, { recursive: true });
|
|
@@ -3219,10 +3250,30 @@ After fixing all issues, run tests (npm test) and build (npm run build) to verif
|
|
|
3219
3250
|
return !!(handle?.loop);
|
|
3220
3251
|
}
|
|
3221
3252
|
|
|
3253
|
+
// The fresh clock line for an agent, anchored to when its work session first
|
|
3254
|
+
// began. Keyed by name, so a rotation (new agent id, same name) keeps the
|
|
3255
|
+
// original start rather than resetting the clock to "just now".
|
|
3256
|
+
sessionClock(agent) {
|
|
3257
|
+
if (!agent) return sessionClockLine(null);
|
|
3258
|
+
const key = agent.name || agent.id;
|
|
3259
|
+
let start = this.sessionStarts.get(key);
|
|
3260
|
+
if (!start) {
|
|
3261
|
+
start = agent.metadata?.sessionStartedAt || agent.spawnedAt || agent.createdAt || new Date().toISOString();
|
|
3262
|
+
this.sessionStarts.set(key, start);
|
|
3263
|
+
}
|
|
3264
|
+
return sessionClockLine(start);
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3222
3267
|
queueMessage(agentId, message) {
|
|
3223
3268
|
const agent = this.daemon.registry.get(agentId);
|
|
3224
3269
|
const wrapped = agent ? wrapWithRoleReminder(agent.role, message) : message;
|
|
3225
|
-
|
|
3270
|
+
// Append to a FIFO queue rather than overwriting the slot. Two messages
|
|
3271
|
+
// arriving while an agent is busy — a user chat and an InnerChat message,
|
|
3272
|
+
// or two different agents reaching out — must BOTH survive. The old
|
|
3273
|
+
// single-slot version silently dropped whichever arrived first.
|
|
3274
|
+
const queue = this.pendingMessages.get(agentId) || [];
|
|
3275
|
+
queue.push({ message: wrapped, timestamp: Date.now() });
|
|
3276
|
+
this.pendingMessages.set(agentId, queue);
|
|
3226
3277
|
if (this.daemon.rotator) {
|
|
3227
3278
|
this.daemon.rotator.recordUserMessage(agentId);
|
|
3228
3279
|
}
|
|
@@ -3230,9 +3281,13 @@ After fixing all issues, run tests (npm test) and build (npm run build) to verif
|
|
|
3230
3281
|
}
|
|
3231
3282
|
|
|
3232
3283
|
consumePendingMessage(agentId) {
|
|
3233
|
-
const
|
|
3234
|
-
if (
|
|
3235
|
-
|
|
3284
|
+
const queue = this.pendingMessages.get(agentId);
|
|
3285
|
+
if (!queue || queue.length === 0) return null;
|
|
3286
|
+
this.pendingMessages.delete(agentId);
|
|
3287
|
+
// Deliver everything queued in one turn, in arrival order, clearly
|
|
3288
|
+
// separated so the agent can tell the messages apart.
|
|
3289
|
+
const message = queue.map((q) => q.message).join('\n\n---\n\n');
|
|
3290
|
+
return { message, timestamp: queue[0].timestamp, count: queue.length };
|
|
3236
3291
|
}
|
|
3237
3292
|
|
|
3238
3293
|
async killAll() {
|
|
@@ -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.195",
|
|
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)",
|
|
@@ -38,7 +38,11 @@ export async function deliverInstruction(daemon, agentId, message, opts = {}) {
|
|
|
38
38
|
if (daemon.rotator) daemon.rotator.recordUserMessage(agentId);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
// Prepend a fresh wall-clock anchor so the agent gauges progress by real time
|
|
42
|
+
// and the user's direction, not by how much context has piled up.
|
|
43
|
+
const clock = daemon.processes.sessionClock?.(agent);
|
|
44
|
+
const timedMessage = clock ? `${clock}\n\n${finalMessage}` : finalMessage;
|
|
45
|
+
const wrappedMessage = wrapWithRoleReminder(agent.role, timedMessage);
|
|
42
46
|
|
|
43
47
|
// Agent loop path — send straight to the running loop.
|
|
44
48
|
if (daemon.processes.hasAgentLoop(agentId)) {
|
|
@@ -362,12 +362,37 @@ export function wrapWithRoleReminder(role, message) {
|
|
|
362
362
|
return message;
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
// A fresh wall-clock anchor, regenerated for every message an agent receives.
|
|
366
|
+
// Agents have no innate sense of elapsed time — they infer "how far along are
|
|
367
|
+
// we" from how much context has piled up, so 15 minutes of dense work (and the
|
|
368
|
+
// session-feel that rotations add) reads as a long day and they start winding
|
|
369
|
+
// down. This gives them the real time and duration each turn, plus an explicit
|
|
370
|
+
// frame that the session is live and open-ended.
|
|
371
|
+
export function sessionClockLine(startISO) {
|
|
372
|
+
const now = new Date();
|
|
373
|
+
const when = now.toLocaleString('en-US', {
|
|
374
|
+
weekday: 'short', month: 'short', day: 'numeric',
|
|
375
|
+
hour: 'numeric', minute: '2-digit', hour12: true,
|
|
376
|
+
});
|
|
377
|
+
let dur = '';
|
|
378
|
+
if (startISO) {
|
|
379
|
+
const mins = Math.max(0, Math.round((now - new Date(startISO)) / 60000));
|
|
380
|
+
const human = mins < 60 ? `${mins} min` : `${Math.floor(mins / 60)}h ${mins % 60}m`;
|
|
381
|
+
dur = ` You've been in this work session about ${human}.`;
|
|
382
|
+
}
|
|
383
|
+
return `[Clock — ${when}.${dur} This is a live session with the user present and engaged. `
|
|
384
|
+
+ 'Judge how far along you are by the task and the user\'s direction, not by how much has '
|
|
385
|
+
+ 'been built or a felt sense of time. Do not wind down, "call it a day," or trim scope on '
|
|
386
|
+
+ 'your own — keep working until the user says to stop.]';
|
|
387
|
+
}
|
|
388
|
+
|
|
365
389
|
export class ProcessManager {
|
|
366
390
|
constructor(daemon) {
|
|
367
391
|
this.daemon = daemon;
|
|
368
392
|
this.handles = new Map(); // agentId -> { proc, logStream }
|
|
369
393
|
this.peakContextUsage = new Map(); // agentId -> highest contextUsage seen
|
|
370
|
-
this.pendingMessages = new Map(); // agentId -> { message, timestamp }
|
|
394
|
+
this.pendingMessages = new Map(); // agentId -> [{ message, timestamp }, …] FIFO queue
|
|
395
|
+
this.sessionStarts = new Map(); // agentName -> ISO start; keyed by name so it survives rotation
|
|
371
396
|
this._streamThrottle = new Map(); // agentId -> { timer, pending }
|
|
372
397
|
this._rotatingAgents = new Set(); // agentIds currently being rotated (rotator wrote handoff)
|
|
373
398
|
this._stalledAgents = new Set(); // agentIds already flagged as stalled (avoids duplicate broadcasts)
|
|
@@ -1184,6 +1209,12 @@ For normal file edits within your scope, proceed without review.
|
|
|
1184
1209
|
}
|
|
1185
1210
|
}
|
|
1186
1211
|
|
|
1212
|
+
// Give the agent a wall-clock anchor on its very first turn. sessionClock
|
|
1213
|
+
// records the session start (by name, so a later rotation keeps it).
|
|
1214
|
+
if (!isOneShotProvider) {
|
|
1215
|
+
spawnConfig.prompt = `${this.sessionClock(agent)}\n\n${spawnConfig.prompt}`;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1187
1218
|
// Set up log capture (shared between CLI and agent loop paths)
|
|
1188
1219
|
const logDir = resolve(this.daemon.grooveDir, 'logs');
|
|
1189
1220
|
mkdirSync(logDir, { recursive: true });
|
|
@@ -3219,10 +3250,30 @@ After fixing all issues, run tests (npm test) and build (npm run build) to verif
|
|
|
3219
3250
|
return !!(handle?.loop);
|
|
3220
3251
|
}
|
|
3221
3252
|
|
|
3253
|
+
// The fresh clock line for an agent, anchored to when its work session first
|
|
3254
|
+
// began. Keyed by name, so a rotation (new agent id, same name) keeps the
|
|
3255
|
+
// original start rather than resetting the clock to "just now".
|
|
3256
|
+
sessionClock(agent) {
|
|
3257
|
+
if (!agent) return sessionClockLine(null);
|
|
3258
|
+
const key = agent.name || agent.id;
|
|
3259
|
+
let start = this.sessionStarts.get(key);
|
|
3260
|
+
if (!start) {
|
|
3261
|
+
start = agent.metadata?.sessionStartedAt || agent.spawnedAt || agent.createdAt || new Date().toISOString();
|
|
3262
|
+
this.sessionStarts.set(key, start);
|
|
3263
|
+
}
|
|
3264
|
+
return sessionClockLine(start);
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3222
3267
|
queueMessage(agentId, message) {
|
|
3223
3268
|
const agent = this.daemon.registry.get(agentId);
|
|
3224
3269
|
const wrapped = agent ? wrapWithRoleReminder(agent.role, message) : message;
|
|
3225
|
-
|
|
3270
|
+
// Append to a FIFO queue rather than overwriting the slot. Two messages
|
|
3271
|
+
// arriving while an agent is busy — a user chat and an InnerChat message,
|
|
3272
|
+
// or two different agents reaching out — must BOTH survive. The old
|
|
3273
|
+
// single-slot version silently dropped whichever arrived first.
|
|
3274
|
+
const queue = this.pendingMessages.get(agentId) || [];
|
|
3275
|
+
queue.push({ message: wrapped, timestamp: Date.now() });
|
|
3276
|
+
this.pendingMessages.set(agentId, queue);
|
|
3226
3277
|
if (this.daemon.rotator) {
|
|
3227
3278
|
this.daemon.rotator.recordUserMessage(agentId);
|
|
3228
3279
|
}
|
|
@@ -3230,9 +3281,13 @@ After fixing all issues, run tests (npm test) and build (npm run build) to verif
|
|
|
3230
3281
|
}
|
|
3231
3282
|
|
|
3232
3283
|
consumePendingMessage(agentId) {
|
|
3233
|
-
const
|
|
3234
|
-
if (
|
|
3235
|
-
|
|
3284
|
+
const queue = this.pendingMessages.get(agentId);
|
|
3285
|
+
if (!queue || queue.length === 0) return null;
|
|
3286
|
+
this.pendingMessages.delete(agentId);
|
|
3287
|
+
// Deliver everything queued in one turn, in arrival order, clearly
|
|
3288
|
+
// separated so the agent can tell the messages apart.
|
|
3289
|
+
const message = queue.map((q) => q.message).join('\n\n---\n\n');
|
|
3290
|
+
return { message, timestamp: queue[0].timestamp, count: queue.length };
|
|
3236
3291
|
}
|
|
3237
3292
|
|
|
3238
3293
|
async killAll() {
|