groove-dev 0.27.205 → 0.27.207
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/chatstore.js +112 -21
- package/node_modules/@groove-dev/daemon/src/index.js +13 -0
- package/node_modules/@groove-dev/daemon/src/routes/agents.js +4 -0
- package/node_modules/@groove-dev/daemon/src/routes/chat-history.js +6 -3
- package/node_modules/@groove-dev/daemon/test/chatstore.test.js +111 -19
- package/node_modules/@groove-dev/gui/dist/assets/{index-vrFP3bO0.js → index-OzNfp6-Y.js} +36 -36
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- 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/chatstore.js +112 -21
- package/packages/daemon/src/index.js +13 -0
- package/packages/daemon/src/routes/agents.js +4 -0
- package/packages/daemon/src/routes/chat-history.js +6 -3
- package/packages/gui/dist/assets/{index-vrFP3bO0.js → index-OzNfp6-Y.js} +36 -36
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
8
8
|
<title>Groove GUI</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-OzNfp6-Y.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-26L3JoZv.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-DoBZjiHE.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BYKpdS2W.js">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.207",
|
|
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)",
|
|
@@ -9,14 +9,21 @@ const MAX_PER_AGENT = 200;
|
|
|
9
9
|
const SAVE_DEBOUNCE_MS = 1500;
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Server-side chat history.
|
|
12
|
+
* Server-side chat history, keyed by AGENT NAME.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* Two hard-won identity lessons live here:
|
|
15
|
+
*
|
|
16
|
+
* 1. localStorage is per-origin (scheme://host:PORT), and a tunnelled GUI's
|
|
17
|
+
* port changes across reconnects — so history kept only in the browser gets
|
|
18
|
+
* stranded on dead origins. Hence a server-side store at all.
|
|
19
|
+
*
|
|
20
|
+
* 2. Agent IDs are NOT stable: every rotation/resume mints a new id. A store
|
|
21
|
+
* keyed by id fragments on every rotation — the full history stays under
|
|
22
|
+
* the dead id while the new id starts near-empty, and a reconnect then
|
|
23
|
+
* "restores" that near-empty stub over the user's real history. The agent
|
|
24
|
+
* NAME survives rotation (the Watch system keys by name for the same
|
|
25
|
+
* reason), so name is the identity here. IDs are accepted at the API edge
|
|
26
|
+
* and resolved immediately.
|
|
20
27
|
*/
|
|
21
28
|
export class ChatStore {
|
|
22
29
|
constructor(daemon) {
|
|
@@ -36,6 +43,50 @@ export class ChatStore {
|
|
|
36
43
|
return {};
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
// Resolve an id-or-name to the stable storage key (the agent's name).
|
|
47
|
+
// Unknown refs are used verbatim so nothing is ever dropped — an orphaned
|
|
48
|
+
// id key re-merges at boot once its agent is known, or stays parked.
|
|
49
|
+
_keyFor(ref) {
|
|
50
|
+
if (!ref) return null;
|
|
51
|
+
const byId = this.daemon.registry?.get?.(ref);
|
|
52
|
+
if (byId?.name) return byId.name;
|
|
53
|
+
return String(ref);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Fold any id-keyed entries into their agent's name key. Runs at boot (after
|
|
58
|
+
* registry restore) so stores written by the old id-keyed code reattach to
|
|
59
|
+
* the agent wherever the id is still resolvable.
|
|
60
|
+
*/
|
|
61
|
+
migrate() {
|
|
62
|
+
const agents = this.daemon.registry?.getAll?.() || [];
|
|
63
|
+
const byId = new Map(agents.map((a) => [a.id, a]));
|
|
64
|
+
let moved = 0;
|
|
65
|
+
for (const key of Object.keys(this.history)) {
|
|
66
|
+
const agent = byId.get(key);
|
|
67
|
+
if (!agent || agent.name === key) continue;
|
|
68
|
+
this.history[agent.name] = mergeMessages(this.history[agent.name], this.history[key]);
|
|
69
|
+
delete this.history[key];
|
|
70
|
+
moved += 1;
|
|
71
|
+
}
|
|
72
|
+
if (moved) this._scheduleSave();
|
|
73
|
+
return moved;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Rotation-time hook: an entry parked under a dead id (written by the old
|
|
78
|
+
* id-keyed code, or by an old GUI posting under ids) follows the agent to
|
|
79
|
+
* its new identity.
|
|
80
|
+
*/
|
|
81
|
+
remap(oldRef, newRef) {
|
|
82
|
+
const oldKey = String(oldRef);
|
|
83
|
+
const newKey = this._keyFor(newRef);
|
|
84
|
+
if (!newKey || oldKey === newKey || !this.history[oldKey]) return;
|
|
85
|
+
this.history[newKey] = mergeMessages(this.history[newKey], this.history[oldKey]);
|
|
86
|
+
delete this.history[oldKey];
|
|
87
|
+
this._scheduleSave();
|
|
88
|
+
}
|
|
89
|
+
|
|
39
90
|
_scheduleSave() {
|
|
40
91
|
if (this._saveTimer) return;
|
|
41
92
|
this._saveTimer = setTimeout(() => {
|
|
@@ -64,35 +115,59 @@ export class ChatStore {
|
|
|
64
115
|
return out;
|
|
65
116
|
}
|
|
66
117
|
|
|
67
|
-
append(
|
|
68
|
-
|
|
118
|
+
append(ref, message) {
|
|
119
|
+
const key = this._keyFor(ref);
|
|
120
|
+
if (!key) return;
|
|
69
121
|
const clean = this._clean(message);
|
|
70
122
|
if (!clean) return;
|
|
71
|
-
const arr = this.history[
|
|
123
|
+
const arr = this.history[key] || [];
|
|
72
124
|
arr.push(clean);
|
|
73
|
-
this.history[
|
|
125
|
+
this.history[key] = arr.slice(-MAX_PER_AGENT);
|
|
74
126
|
this._scheduleSave();
|
|
75
127
|
}
|
|
76
128
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Merge a batch from a client. This is a UNION by (timestamp, from, text) —
|
|
131
|
+
* never a replace — so a browser holding messages the server missed adds
|
|
132
|
+
* them, and a browser with less than the server can never truncate it.
|
|
133
|
+
* A "restore" must be incapable of destroying what it restores from.
|
|
134
|
+
*/
|
|
135
|
+
merge(ref, messages) {
|
|
136
|
+
const key = this._keyFor(ref);
|
|
137
|
+
if (!key || !Array.isArray(messages)) return;
|
|
138
|
+
const incoming = messages.map((m) => this._clean(m)).filter(Boolean);
|
|
139
|
+
this.history[key] = mergeMessages(this.history[key], incoming);
|
|
82
140
|
this._scheduleSave();
|
|
83
141
|
}
|
|
84
142
|
|
|
143
|
+
/**
|
|
144
|
+
* History for the GUI: live agents keyed by their CURRENT id (what the GUI
|
|
145
|
+
* looks up by), everything else under its stored name key so an agent
|
|
146
|
+
* respawned under the same name picks its history back up.
|
|
147
|
+
*/
|
|
148
|
+
view() {
|
|
149
|
+
const out = {};
|
|
150
|
+
const agents = this.daemon.registry?.getAll?.() || [];
|
|
151
|
+
const liveNames = new Map(agents.map((a) => [a.name, a.id]));
|
|
152
|
+
for (const [key, msgs] of Object.entries(this.history)) {
|
|
153
|
+
if (!Array.isArray(msgs) || !msgs.length) continue;
|
|
154
|
+
out[liveNames.get(key) || key] = msgs;
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
85
159
|
getAll() {
|
|
86
160
|
return this.history;
|
|
87
161
|
}
|
|
88
162
|
|
|
89
|
-
get(
|
|
90
|
-
return this.history[
|
|
163
|
+
get(ref) {
|
|
164
|
+
return this.history[this._keyFor(ref)] || [];
|
|
91
165
|
}
|
|
92
166
|
|
|
93
|
-
remove(
|
|
94
|
-
|
|
95
|
-
|
|
167
|
+
remove(ref) {
|
|
168
|
+
const key = this._keyFor(ref);
|
|
169
|
+
if (key && this.history[key]) {
|
|
170
|
+
delete this.history[key];
|
|
96
171
|
this._scheduleSave();
|
|
97
172
|
}
|
|
98
173
|
}
|
|
@@ -102,3 +177,19 @@ export class ChatStore {
|
|
|
102
177
|
this._saveNow();
|
|
103
178
|
}
|
|
104
179
|
}
|
|
180
|
+
|
|
181
|
+
// Union of two message arrays, deduped on (timestamp, from, text), time-sorted,
|
|
182
|
+
// capped. Exported for tests and the migration path.
|
|
183
|
+
export function mergeMessages(a, b) {
|
|
184
|
+
const seen = new Set();
|
|
185
|
+
const out = [];
|
|
186
|
+
for (const m of [...(a || []), ...(b || [])]) {
|
|
187
|
+
if (!m || typeof m !== 'object') continue;
|
|
188
|
+
const sig = `${m.timestamp}:${m.from}:${typeof m.text === 'string' ? m.text.slice(0, 200) : ''}`;
|
|
189
|
+
if (seen.has(sig)) continue;
|
|
190
|
+
seen.add(sig);
|
|
191
|
+
out.push(m);
|
|
192
|
+
}
|
|
193
|
+
out.sort((x, y) => (x.timestamp || 0) - (y.timestamp || 0));
|
|
194
|
+
return out.slice(-MAX_PER_AGENT);
|
|
195
|
+
}
|
|
@@ -438,6 +438,12 @@ export class Daemon {
|
|
|
438
438
|
}
|
|
439
439
|
|
|
440
440
|
broadcast(message) {
|
|
441
|
+
// Every rotation/resume path announces itself here — the one chokepoint
|
|
442
|
+
// for "this agent has a new id". Chat history parked under the dead id
|
|
443
|
+
// (stale client posts, pre-migration data) follows the agent.
|
|
444
|
+
if (message.type === 'rotation:complete' && message.oldAgentId && message.agentId) {
|
|
445
|
+
try { this.chatStore.remap(message.oldAgentId, message.agentId); } catch { /* best effort */ }
|
|
446
|
+
}
|
|
441
447
|
if (!this.wss) return;
|
|
442
448
|
const payload = JSON.stringify(message);
|
|
443
449
|
for (const client of this.wss.clients) {
|
|
@@ -640,6 +646,13 @@ export class Daemon {
|
|
|
640
646
|
this.federation.initialize();
|
|
641
647
|
this._startGarbageCollector();
|
|
642
648
|
|
|
649
|
+
// Fold id-keyed chat history into name keys now that the registry is
|
|
650
|
+
// restored — reattaches histories written by the old id-keyed store.
|
|
651
|
+
try {
|
|
652
|
+
const moved = this.chatStore.migrate();
|
|
653
|
+
if (moved) console.log(`[chat] migrated ${moved} id-keyed histories to agent names`);
|
|
654
|
+
} catch { /* best effort */ }
|
|
655
|
+
|
|
643
656
|
// Regenerate the on-disk registry files once on boot. They otherwise
|
|
644
657
|
// only refresh on a registry change, so a daemon upgraded with new
|
|
645
658
|
// agent-facing docs (InnerChat, Watch) would serve stale AGENTS_REGISTRY.md
|
|
@@ -89,6 +89,10 @@ export function registerAgentRoutes(app, daemon) {
|
|
|
89
89
|
// Killed/completed agents stay visible so the user can review output.
|
|
90
90
|
const purge = req.query.purge === 'true';
|
|
91
91
|
if (purge) {
|
|
92
|
+
// Clear chat history while the id still resolves to a name — the
|
|
93
|
+
// store is name-keyed, and a purged name's history must not haunt a
|
|
94
|
+
// future agent spawned under the same name.
|
|
95
|
+
try { daemon.chatStore.remove(agent.name); } catch { /* best effort */ }
|
|
92
96
|
daemon.registry.remove(req.params.id);
|
|
93
97
|
}
|
|
94
98
|
|
|
@@ -4,7 +4,9 @@ export function registerChatHistoryRoutes(app, daemon) {
|
|
|
4
4
|
// Full history for all agents — the GUI loads this on connect so chats are
|
|
5
5
|
// present regardless of which origin/port the tunnel came up on.
|
|
6
6
|
app.get('/api/chat-history', (req, res) => {
|
|
7
|
-
|
|
7
|
+
// view() keys live agents by their CURRENT id — ids churn on rotation, so
|
|
8
|
+
// the store itself is name-keyed and this translates at the edge.
|
|
9
|
+
res.json({ history: daemon.chatStore.view() });
|
|
8
10
|
});
|
|
9
11
|
|
|
10
12
|
// Append a single message for an agent.
|
|
@@ -17,13 +19,14 @@ export function registerChatHistoryRoutes(app, daemon) {
|
|
|
17
19
|
res.json({ ok: true });
|
|
18
20
|
});
|
|
19
21
|
|
|
20
|
-
//
|
|
22
|
+
// Batch sync from a client. A UNION, deliberately not a replace — a client
|
|
23
|
+
// can add messages the server missed but can never truncate server history.
|
|
21
24
|
app.put('/api/chat-history/:agentId', (req, res) => {
|
|
22
25
|
const { messages } = req.body || {};
|
|
23
26
|
if (!Array.isArray(messages)) {
|
|
24
27
|
return res.status(400).json({ error: 'messages array required' });
|
|
25
28
|
}
|
|
26
|
-
daemon.chatStore.
|
|
29
|
+
daemon.chatStore.merge(req.params.agentId, messages);
|
|
27
30
|
res.json({ ok: true });
|
|
28
31
|
});
|
|
29
32
|
|