groove-dev 0.27.128 → 0.27.131

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.
Files changed (47) hide show
  1. package/model-workspace/LAB-ASSISTANT-BUILD-PLAN.md +341 -0
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +46 -0
  5. package/node_modules/@groove-dev/daemon/src/index.js +15 -3
  6. package/node_modules/@groove-dev/daemon/src/llama-server.js +1 -1
  7. package/node_modules/@groove-dev/daemon/src/model-lab.js +46 -0
  8. package/node_modules/@groove-dev/daemon/src/process.js +6 -0
  9. package/node_modules/@groove-dev/daemon/src/state.js +18 -2
  10. package/node_modules/@groove-dev/daemon/src/teams.js +26 -7
  11. package/node_modules/@groove-dev/daemon/templates/tgi-setup.json +12 -0
  12. package/node_modules/@groove-dev/daemon/templates/vllm-setup.json +12 -0
  13. package/{packages/gui/dist/assets/index-DXIaW0aK.js → node_modules/@groove-dev/gui/dist/assets/index-BiB9oY9U.js} +1764 -1763
  14. package/node_modules/@groove-dev/gui/dist/assets/index-CeyDFVub.css +1 -0
  15. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  16. package/node_modules/@groove-dev/gui/package.json +1 -1
  17. package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +199 -0
  18. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +216 -3
  19. package/node_modules/@groove-dev/gui/src/components/ui/combobox.jsx +118 -0
  20. package/node_modules/@groove-dev/gui/src/stores/groove.js +89 -1
  21. package/node_modules/@groove-dev/gui/src/views/agents.jsx +7 -1
  22. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +56 -24
  23. package/package.json +1 -1
  24. package/packages/cli/package.json +1 -1
  25. package/packages/daemon/package.json +1 -1
  26. package/packages/daemon/src/api.js +46 -0
  27. package/packages/daemon/src/index.js +15 -3
  28. package/packages/daemon/src/llama-server.js +1 -1
  29. package/packages/daemon/src/model-lab.js +46 -0
  30. package/packages/daemon/src/process.js +6 -0
  31. package/packages/daemon/src/state.js +18 -2
  32. package/packages/daemon/src/teams.js +26 -7
  33. package/packages/daemon/templates/tgi-setup.json +12 -0
  34. package/packages/daemon/templates/vllm-setup.json +12 -0
  35. package/{node_modules/@groove-dev/gui/dist/assets/index-DXIaW0aK.js → packages/gui/dist/assets/index-BiB9oY9U.js} +1764 -1763
  36. package/packages/gui/dist/assets/index-CeyDFVub.css +1 -0
  37. package/packages/gui/dist/index.html +2 -2
  38. package/packages/gui/package.json +1 -1
  39. package/packages/gui/src/components/lab/lab-assistant.jsx +199 -0
  40. package/packages/gui/src/components/lab/runtime-config.jsx +216 -3
  41. package/packages/gui/src/components/ui/combobox.jsx +118 -0
  42. package/packages/gui/src/stores/groove.js +89 -1
  43. package/packages/gui/src/views/agents.jsx +7 -1
  44. package/packages/gui/src/views/model-lab.jsx +56 -24
  45. package/local-models/daemon-bridge.js +0 -87
  46. package/node_modules/@groove-dev/gui/dist/assets/index-CY-CITov.css +0 -1
  47. package/packages/gui/dist/assets/index-CY-CITov.css +0 -1
@@ -0,0 +1,341 @@
1
+ # Lab Assistant — Build Plan
2
+
3
+ ## Overview
4
+
5
+ Add an embedded AI assistant to the Model Lab that helps users set up inference runtimes (vLLM, TGI) without leaving the Lab. One agent, one chat, inline in the center panel. The agent handles system recon, installation, configuration, server startup, and auto-creates the Lab runtime when done.
6
+
7
+ ## User Flow
8
+
9
+ 1. User selects a model + picks vLLM or TGI backend in the Launch Model section
10
+ 2. Clicks **"Setup vLLM with Assistant"** button
11
+ 3. Center panel switches from Playground to Assistant tab — agent chat appears
12
+ 4. Agent: "Let me check your system..." → runs nvidia-smi, checks CUDA, Python, Docker, VRAM
13
+ 5. Agent: "You have an RTX 4090 with 24GB VRAM. I'll set up vLLM via Docker with Qwen3-8B."
14
+ 6. Agent installs, configures, starts the server, validates with a health check
15
+ 7. Agent calls `POST http://localhost:31415/api/lab/runtimes` to register the runtime
16
+ 8. Runtime appears in the Lab's left panel automatically (via WebSocket event)
17
+ 9. Agent: "Done! Switch to the Playground tab to start chatting."
18
+ 10. User clicks "Switch to Playground" — model is ready to use
19
+
20
+ ## Architecture
21
+
22
+ ```
23
+ User clicks "Setup vLLM"
24
+ → store.launchLabAssistant('vllm')
25
+ → POST /api/lab/assistant { backend: 'vllm' }
26
+ → daemon reads templates/vllm-setup.json
27
+ → daemon.processes.spawn({ role: 'lab-assistant', prompt: ... })
28
+ → returns { agentId }
29
+ → store sets labAssistantAgentId, switches to Assistant tab
30
+ → agent output streams via WebSocket → chatHistory[agentId]
31
+
32
+ Agent runs system commands, installs vLLM, starts server
33
+ → agent calls: curl POST /api/lab/runtimes (on localhost)
34
+ → daemon creates runtime, broadcasts lab:runtime:added
35
+ → store.fetchLabRuntimes() fires → left panel updates
36
+ ```
37
+
38
+ ## Implementation
39
+
40
+ ### 1. Team Templates (new files)
41
+
42
+ **`packages/daemon/templates/vllm-setup.json`**
43
+
44
+ Single-agent template. Role is `lab-assistant` (avoids the `planner` role which is restricted to planning-only in `ROLE_PROMPTS`).
45
+
46
+ ```json
47
+ {
48
+ "name": "vllm-setup",
49
+ "description": "Lab Assistant for vLLM installation and configuration",
50
+ "agents": [
51
+ {
52
+ "role": "lab-assistant",
53
+ "scope": [],
54
+ "provider": "claude-code",
55
+ "prompt": "SEE PROMPT SPEC BELOW"
56
+ }
57
+ ]
58
+ }
59
+ ```
60
+
61
+ **`packages/daemon/templates/tgi-setup.json`** — same structure, TGI-specific prompt.
62
+
63
+ #### Planner Prompt Spec (vLLM)
64
+
65
+ The prompt is the hardest part. It must cover:
66
+
67
+ **Identity**: "You are a GROOVE Lab Assistant. You help the user set up a vLLM inference server. Be conversational, report progress clearly."
68
+
69
+ **System Recon** (run these commands, report findings):
70
+ - `nvidia-smi` — GPU model, VRAM, driver version
71
+ - `nvcc --version` — CUDA toolkit version
72
+ - `python3 --version` and `pip3 --version`
73
+ - `docker --version`
74
+ - `free -h` — available RAM
75
+ - `df -h /` — disk space
76
+
77
+ **Decision Matrix**:
78
+ - Docker available + NVIDIA GPU → Docker path (simplest)
79
+ - No Docker, Python 3.8+ and CUDA → pip path
80
+ - No GPU → warn user, suggest llama.cpp/Ollama instead
81
+ - VRAM sizing: <8GB → 1-3B models, 8-16GB → 7B, 16-24GB → 13B, 24-48GB → 30-70B quantized, 48GB+ → 70B+
82
+
83
+ **Installation**:
84
+ - Docker: `docker run --runtime nvidia --gpus all -v ~/.cache/huggingface:/root/.cache/huggingface -p 8000:8000 --ipc=host vllm/vllm-openai:latest --model <model>`
85
+ - Pip: `pip install vllm && vllm serve <model> --host 0.0.0.0 --port 8000`
86
+ - Must use `nohup` or `docker run -d` so server persists after agent exits
87
+
88
+ **Validation**: `curl http://localhost:8000/v1/models` — confirm JSON response
89
+
90
+ **Runtime Registration**:
91
+ ```bash
92
+ PORT=$(cat ~/.groove/daemon.port 2>/dev/null || echo 31415)
93
+ curl -s -X POST http://localhost:$PORT/api/lab/runtimes \
94
+ -H 'Content-Type: application/json' \
95
+ -d '{"name":"vLLM - <model>","type":"vllm","endpoint":"http://localhost:8000"}'
96
+ ```
97
+
98
+ **Completion**: Tell user to switch to Playground tab.
99
+
100
+ **Error Handling**: Explain errors clearly, suggest fixes, offer retry. Common issues: CUDA mismatch, insufficient VRAM, Docker not running, missing nvidia-container-toolkit.
101
+
102
+ #### TGI Prompt Spec
103
+
104
+ Same structure, but:
105
+ - Docker: `ghcr.io/huggingface/text-generation-inference`
106
+ - Default port: 8080
107
+ - Model loading with `--model-id`
108
+ - Runtime type: `"tgi"`
109
+
110
+ ---
111
+
112
+ ### 2. Daemon Endpoint
113
+
114
+ **File**: `packages/daemon/src/api.js`
115
+ **Location**: After the existing lab endpoints (after `GET /api/lab/sessions/:id`, around line 6760)
116
+
117
+ **`POST /api/lab/assistant`**
118
+
119
+ ```
120
+ Request: { "backend": "vllm" | "tgi" }
121
+ Response: { "agentId": "abc123", "backend": "vllm" }
122
+ ```
123
+
124
+ Implementation:
125
+ 1. Validate `backend` is "vllm" or "tgi"
126
+ 2. Read template: `readFileSync(resolve(__dirname, '../templates/${backend}-setup.json'), 'utf8')`
127
+ 3. Parse JSON, extract the agent config (first entry in `agents` array)
128
+ 4. Spawn agent via `daemon.processes.spawn()` with:
129
+ - `role`: "lab-assistant"
130
+ - `provider`: agent config provider or daemon default
131
+ - `prompt`: agent config prompt
132
+ - `teamId`: default team ID
133
+ - `metadata`: `{ labAssistant: true, backend }`
134
+ 5. Return `201` with `{ agentId: agent.id, backend }`
135
+
136
+ **Import needed**: `readFileSync` from `fs`, `resolve` from `path` (likely already imported in api.js)
137
+
138
+ ---
139
+
140
+ ### 3. Store State & Actions
141
+
142
+ **File**: `packages/gui/src/stores/groove.js`
143
+
144
+ **New state** (add after `labLaunchError`):
145
+ ```js
146
+ labAssistantAgentId: null,
147
+ labAssistantMode: false,
148
+ labAssistantBackend: null,
149
+ ```
150
+
151
+ **New actions** (add after the existing lab actions block):
152
+
153
+ ```js
154
+ async launchLabAssistant(backend) {
155
+ const existing = get().labAssistantAgentId;
156
+ if (existing) {
157
+ // If assistant already running, just switch to its tab
158
+ const agent = get().agents.find((a) => a.id === existing);
159
+ if (agent && agent.status === 'running') {
160
+ set({ labAssistantMode: true });
161
+ return;
162
+ }
163
+ }
164
+ try {
165
+ const data = await api.post('/lab/assistant', { backend });
166
+ set({ labAssistantAgentId: data.agentId, labAssistantMode: true, labAssistantBackend: backend });
167
+ get().addToast('info', `Lab Assistant started for ${backend}`);
168
+ } catch (err) {
169
+ get().addToast('error', 'Failed to start assistant', err.message);
170
+ }
171
+ },
172
+
173
+ dismissLabAssistant() {
174
+ set({ labAssistantMode: false });
175
+ },
176
+
177
+ clearLabAssistant() {
178
+ const id = get().labAssistantAgentId;
179
+ if (id) api.delete(`/agents/${id}`).catch(() => {});
180
+ set({ labAssistantAgentId: null, labAssistantMode: false, labAssistantBackend: null });
181
+ },
182
+ ```
183
+
184
+ **No WebSocket changes needed** — `agent:output` already populates `chatHistory[agentId]`, and `lab:runtime:added` already triggers `fetchLabRuntimes()`.
185
+
186
+ ---
187
+
188
+ ### 4. Lab Assistant Component (new file)
189
+
190
+ **File**: `packages/gui/src/components/lab/lab-assistant.jsx`
191
+
192
+ A focused chat component for the lab assistant agent. Reads from `chatHistory[labAssistantAgentId]`, sends via the existing agent instruction mechanism.
193
+
194
+ **Structure**:
195
+ ```
196
+ LabAssistant
197
+ ├── Header: backend badge, agent status, dismiss "X" button
198
+ ├── ScrollArea: message list
199
+ │ ├── Agent messages (left-aligned, border-l accent)
200
+ │ └── User messages (right-aligned, accent/10 bg)
201
+ ├── Thinking indicator (when agent is processing)
202
+ ├── Completion banner: "Setup complete — Switch to Playground" button
203
+ └── Input: textarea + send button (Enter to send, Shift+Enter newline)
204
+ ```
205
+
206
+ **Key store selectors**:
207
+ - `labAssistantAgentId` — which agent to display
208
+ - `chatHistory[agentId]` — message history
209
+ - `agents.find(a => a.id === agentId)` — agent status (running/completed/error)
210
+ - `dismissLabAssistant` — switch back to playground
211
+
212
+ **Sending messages**: Use the existing `instructAgent(agentId, message)` store action (check if this exists — it should be the mechanism used by AgentChat). If not, use `api.post('/agents/${id}/instruct', { message })`.
213
+
214
+ **Styling**: Match `chat-playground.jsx` patterns exactly — same message bubble styles, same input area, same auto-scroll logic. License header on line 1.
215
+
216
+ **Completion detection**: When `agent.status !== 'running'` and the last message exists, show the "Switch to Playground" button.
217
+
218
+ ---
219
+
220
+ ### 5. Center Panel Mode Switch
221
+
222
+ **File**: `packages/gui/src/views/model-lab.jsx`
223
+
224
+ **New import**: `import { LabAssistant } from '../components/lab/lab-assistant';`
225
+
226
+ **New store selectors** in `ModelLabView`:
227
+ ```js
228
+ const labAssistantAgentId = useGrooveStore((s) => s.labAssistantAgentId);
229
+ const labAssistantMode = useGrooveStore((s) => s.labAssistantMode);
230
+ ```
231
+
232
+ **Replace the center panel** (currently `<div className="flex-1 min-w-0 p-3"><ChatPlayground /></div>`) with:
233
+
234
+ ```jsx
235
+ <div className="flex-1 min-w-0 flex flex-col">
236
+ {/* Tab bar — only visible when assistant exists */}
237
+ {labAssistantAgentId && (
238
+ <div className="flex-shrink-0 flex items-center gap-1 px-3 pt-2">
239
+ <button
240
+ onClick={() => set({ labAssistantMode: false })}
241
+ className={cn(
242
+ 'px-3 py-1.5 text-xs font-sans font-medium rounded-t-md transition-colors cursor-pointer',
243
+ !labAssistantMode ? 'text-accent bg-accent/10' : 'text-text-3 hover:text-text-1',
244
+ )}
245
+ >
246
+ Playground
247
+ </button>
248
+ <button
249
+ onClick={() => set({ labAssistantMode: true })}
250
+ className={cn(
251
+ 'px-3 py-1.5 text-xs font-sans font-medium rounded-t-md transition-colors cursor-pointer',
252
+ labAssistantMode ? 'text-accent bg-accent/10' : 'text-text-3 hover:text-text-1',
253
+ )}
254
+ >
255
+ Assistant
256
+ </button>
257
+ </div>
258
+ )}
259
+ <div className="flex-1 min-h-0 p-3">
260
+ {labAssistantMode && labAssistantAgentId ? <LabAssistant /> : <ChatPlayground />}
261
+ </div>
262
+ </div>
263
+ ```
264
+
265
+ Note: The tab buttons need to call store's `set` function — either expose `setLabAssistantMode(bool)` as a store action, or use `dismissLabAssistant()` and `set({ labAssistantMode: true })`. Cleanest approach: add a `setLabAssistantMode(mode)` action to the store.
266
+
267
+ ---
268
+
269
+ ### 6. LaunchModel Button
270
+
271
+ **File**: `packages/gui/src/components/lab/runtime-config.jsx`
272
+
273
+ **In the `LaunchModel` component**, replace the static guidance box for vLLM/TGI:
274
+
275
+ Change from:
276
+ ```jsx
277
+ {!currentBackend?.autoLaunch && (
278
+ <div>...manual setup guidance...</div>
279
+ )}
280
+ ```
281
+
282
+ To:
283
+ ```jsx
284
+ {!currentBackend?.autoLaunch && (
285
+ <div className="space-y-2">
286
+ <Button variant="primary" size="sm" className="w-full" onClick={handleLaunchAssistant} disabled={assistantLaunching}>
287
+ {assistantLaunching
288
+ ? <><Loader2 size={12} className="animate-spin mr-1.5" /> Starting Assistant...</>
289
+ : <><Wrench size={12} className="mr-1.5" /> Setup {currentBackend?.label} with Assistant</>
290
+ }
291
+ </Button>
292
+ <p className="text-2xs text-text-4 font-sans px-1">
293
+ An AI assistant will check your system and handle the installation, or start your server manually and add it as a Runtime below.
294
+ </p>
295
+ </div>
296
+ )}
297
+ ```
298
+
299
+ **New imports**: `Wrench` from lucide-react, `launchLabAssistant` from store.
300
+
301
+ **Update BACKENDS array** subtitle for vLLM/TGI:
302
+ ```js
303
+ { id: 'vllm', label: 'vLLM', subtitle: 'GPU-optimized, guided setup', autoLaunch: false },
304
+ { id: 'tgi', label: 'TGI', subtitle: 'HuggingFace, guided setup', autoLaunch: false },
305
+ ```
306
+
307
+ ---
308
+
309
+ ## File Summary
310
+
311
+ | File | Action | Description |
312
+ |------|--------|-------------|
313
+ | `packages/daemon/templates/vllm-setup.json` | CREATE | vLLM setup agent template + prompt |
314
+ | `packages/daemon/templates/tgi-setup.json` | CREATE | TGI setup agent template + prompt |
315
+ | `packages/daemon/src/api.js` | MODIFY | Add `POST /api/lab/assistant` endpoint |
316
+ | `packages/gui/src/stores/groove.js` | MODIFY | Add labAssistant state + actions |
317
+ | `packages/gui/src/components/lab/lab-assistant.jsx` | CREATE | Assistant chat component |
318
+ | `packages/gui/src/views/model-lab.jsx` | MODIFY | Center panel tab switch |
319
+ | `packages/gui/src/components/lab/runtime-config.jsx` | MODIFY | "Setup with Assistant" button |
320
+
321
+ ## Build Order
322
+
323
+ 1. Team templates (`vllm-setup.json`, `tgi-setup.json`)
324
+ 2. Daemon endpoint (`POST /api/lab/assistant` in `api.js`)
325
+ 3. Store state and actions (`groove.js`)
326
+ 4. Lab Assistant component (`lab-assistant.jsx`)
327
+ 5. Center panel mode switch (`model-lab.jsx`)
328
+ 6. LaunchModel button (`runtime-config.jsx`)
329
+ 7. `npm run build` from `packages/gui/`
330
+ 8. End-to-end test
331
+
332
+ ## Key Constraints
333
+
334
+ - **Role must be `lab-assistant`** — not `planner` (which is restricted to planning-only in `ROLE_PROMPTS`)
335
+ - **Agent sends messages via WebSocket** — the `chatHistory[agentId]` pattern already handles this
336
+ - **Runtime creation via curl** — the agent calls the daemon REST API directly. The daemon is on localhost:31415 (or read from `~/.groove/daemon.port`)
337
+ - **Server must persist** — use `nohup`, `docker run -d`, or background processes so the inference server outlives the agent
338
+ - **No inline styles** — Tailwind CSS only
339
+ - **License header** — `// FSL-1.1-Apache-2.0 — see LICENSE` on every new source file
340
+ - **ESM imports** — all files use `import`/`export`
341
+ - **Do NOT restart the daemon** — verify with `npm run build` only
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.128",
3
+ "version": "0.27.131",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.128",
3
+ "version": "0.27.131",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -6669,6 +6669,27 @@ Keep responses concise. Help them think, don't lecture them about the system the
6669
6669
  }
6670
6670
  });
6671
6671
 
6672
+ app.get('/api/lab/local-models', (req, res) => {
6673
+ try {
6674
+ res.json(daemon.modelLab.listLocalModels());
6675
+ } catch (err) {
6676
+ res.status(500).json({ error: err.message });
6677
+ }
6678
+ });
6679
+
6680
+ app.post('/api/lab/launch-local', async (req, res) => {
6681
+ try {
6682
+ const { modelId } = req.body;
6683
+ if (!modelId || typeof modelId !== 'string') {
6684
+ return res.status(400).json({ error: 'modelId is required' });
6685
+ }
6686
+ const result = await daemon.modelLab.launchLocalModel(modelId);
6687
+ res.json(result);
6688
+ } catch (err) {
6689
+ res.status(400).json({ error: err.message });
6690
+ }
6691
+ });
6692
+
6672
6693
  app.post('/api/lab/inference', async (req, res) => {
6673
6694
  try {
6674
6695
  const params = validateLabInferenceParams(req.body);
@@ -6743,6 +6764,31 @@ Keep responses concise. Help them think, don't lecture them about the system the
6743
6764
  res.json(session);
6744
6765
  });
6745
6766
 
6767
+ app.post('/api/lab/assistant', async (req, res) => {
6768
+ try {
6769
+ const { backend } = req.body || {};
6770
+ if (!backend || !['vllm', 'tgi'].includes(backend)) {
6771
+ return res.status(400).json({ error: 'backend must be "vllm" or "tgi"' });
6772
+ }
6773
+ const templatePath = resolve(__dirname, `../templates/${backend}-setup.json`);
6774
+ const template = JSON.parse(readFileSync(templatePath, 'utf8'));
6775
+ const agentConfig = template.agents[0];
6776
+ const config = {
6777
+ role: 'lab-assistant',
6778
+ scope: agentConfig.scope || [],
6779
+ provider: agentConfig.provider || daemon.config.defaultProvider,
6780
+ prompt: agentConfig.prompt,
6781
+ metadata: { labAssistant: true, backend },
6782
+ };
6783
+ if (!config.provider) config.provider = daemon.config.defaultProvider;
6784
+ const agent = await daemon.processes.spawn(config);
6785
+ daemon.audit.log('lab.assistant.spawn', { id: agent.id, backend });
6786
+ res.status(201).json({ agentId: agent.id, backend });
6787
+ } catch (err) {
6788
+ res.status(400).json({ error: err.message });
6789
+ }
6790
+ });
6791
+
6746
6792
  // --- Wallet & earnings stubs (Base L2 — wired to real data post-mainnet) ---
6747
6793
 
6748
6794
  app.get('/api/network/wallet', networkGate, (req, res) => {
@@ -719,10 +719,23 @@ export class Daemon {
719
719
 
720
720
  try {
721
721
  // Build set of agent names still in the registry — never remove their logs
722
- const activeNames = new Set(this.registry.getAll().map((a) => a.name));
722
+ const allAgents = this.registry.getAll();
723
+ const activeNames = new Set(allAgents.map((a) => a.name));
723
724
 
724
- // 1. Clean raw log files for agents no longer in the registry
725
+ // Safety: if registry is empty but log files exist, state may have been
726
+ // lost (corrupt JSON, partial write). Skip log cleanup to prevent
727
+ // destroying agent history that could still be recovered.
725
728
  const logsDir = resolve(grooveDir, 'logs');
729
+ const agentLogsDir = resolve(this.projectDir, 'GROOVE_AGENT_LOGS');
730
+ const hasOrphanedLogs = (existsSync(logsDir) && readdirSync(logsDir).length > 0) ||
731
+ (existsSync(agentLogsDir) && readdirSync(agentLogsDir).length > 0);
732
+
733
+ if (allAgents.length === 0 && hasOrphanedLogs) {
734
+ console.log('[Groove:GC] Registry empty but log files exist — skipping cleanup to prevent data loss');
735
+ return;
736
+ }
737
+
738
+ // 1. Clean raw log files for agents no longer in the registry
726
739
  if (existsSync(logsDir)) {
727
740
  for (const file of readdirSync(logsDir)) {
728
741
  const agentName = file.replace(/\.log$/, '');
@@ -732,7 +745,6 @@ export class Daemon {
732
745
  }
733
746
 
734
747
  // 2. Clean GROOVE_AGENT_LOGS/ for agents no longer in the registry
735
- const agentLogsDir = resolve(this.projectDir, 'GROOVE_AGENT_LOGS');
736
748
  if (existsSync(agentLogsDir)) {
737
749
  for (const dir of readdirSync(agentLogsDir, { withFileTypes: true })) {
738
750
  if (!dir.isDirectory()) continue;
@@ -71,7 +71,7 @@ export class LlamaServerManager {
71
71
 
72
72
  // Flash attention for better memory efficiency (if supported)
73
73
  if (options.flashAttention !== false) {
74
- args.push('--flash-attn');
74
+ args.push('--flash-attn', 'auto');
75
75
  }
76
76
 
77
77
  const proc = spawn('llama-server', args, {
@@ -442,6 +442,52 @@ export class ModelLab {
442
442
  this._saveSession(session);
443
443
  }
444
444
 
445
+ // ─── Launch Local GGUF ───────────────────────────────────────
446
+
447
+ async launchLocalModel(modelId) {
448
+ const mm = this.daemon.modelManager;
449
+ const ls = this.daemon.llamaServer;
450
+ if (!mm || !ls) throw new Error('Local model serving not available');
451
+
452
+ const model = mm.getModel(modelId);
453
+ if (!model) throw new Error('Model not found in local store');
454
+
455
+ const modelPath = mm.getModelPath(modelId);
456
+ if (!modelPath) throw new Error('Model file not found on disk');
457
+
458
+ const endpoint = await ls.ensureServer(modelPath);
459
+ if (!endpoint) throw new Error('Failed to start inference server');
460
+
461
+ const existing = [...this.runtimes.values()].find(
462
+ (r) => r._localModelId === modelId,
463
+ );
464
+ if (existing) {
465
+ existing.endpoint = endpoint;
466
+ existing.models = [{ id: model.filename, name: model.filename, size: model.sizeBytes }];
467
+ this._saveRuntimes();
468
+ this.daemon.broadcast({ type: 'lab:runtime:updated', data: existing });
469
+ return { runtime: existing, model: model.filename };
470
+ }
471
+
472
+ const label = model.filename.replace(/\.gguf$/i, '');
473
+ const runtime = await this.addRuntime({
474
+ name: `${label} (local)`,
475
+ type: 'llama-cpp',
476
+ endpoint,
477
+ models: [{ id: model.filename, name: model.filename, size: model.sizeBytes }],
478
+ });
479
+ runtime._localModelId = modelId;
480
+ this._saveRuntimes();
481
+
482
+ return { runtime, model: model.filename };
483
+ }
484
+
485
+ listLocalModels() {
486
+ const mm = this.daemon.modelManager;
487
+ if (!mm) return [];
488
+ return mm.getInstalled().filter((m) => m.exists);
489
+ }
490
+
445
491
  // ─── Auto-detect Ollama ─────────────────────────────────────
446
492
 
447
493
  async autoDetectOllama() {
@@ -1953,6 +1953,9 @@ For normal file edits within your scope, proceed without review.
1953
1953
  this.daemon.trajectoryCapture.onAgentSpawn(
1954
1954
  newAgent.id, config.provider, config.model || null, config.role, teamSize, config.prompt
1955
1955
  ).catch(() => {});
1956
+ if (message && typeof message === 'string' && message.trim()) {
1957
+ this.daemon.trajectoryCapture.onUserMessage(newAgent.id, message, 'user');
1958
+ }
1956
1959
  } catch (e) { /* fail silent */ }
1957
1960
  }
1958
1961
 
@@ -2095,6 +2098,9 @@ For normal file edits within your scope, proceed without review.
2095
2098
  this.daemon.trajectoryCapture.onAgentSpawn(
2096
2099
  newAgent.id, config.provider, loopConfig.model || config.model || null, config.role, teamSize, config.prompt
2097
2100
  ).catch(() => {});
2101
+ if (message && typeof message === 'string' && message.trim()) {
2102
+ this.daemon.trajectoryCapture.onUserMessage(newAgent.id, message, 'user');
2103
+ }
2098
2104
  } catch (e) { /* fail silent */ }
2099
2105
  }
2100
2106
 
@@ -1,7 +1,7 @@
1
1
  // GROOVE — State Persistence
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
- import { readFileSync, existsSync, readdirSync, unlinkSync } from 'fs';
4
+ import { readFileSync, writeFileSync, existsSync, readdirSync, unlinkSync, renameSync, copyFileSync } from 'fs';
5
5
  import { writeFile } from 'node:fs/promises';
6
6
  import { resolve } from 'path';
7
7
 
@@ -9,6 +9,7 @@ export class StateManager {
9
9
  constructor(grooveDir) {
10
10
  this.grooveDir = grooveDir;
11
11
  this.path = resolve(grooveDir, 'state.json');
12
+ this.backupPath = resolve(grooveDir, 'state.json.bak');
12
13
  this.data = {};
13
14
  }
14
15
 
@@ -16,13 +17,28 @@ export class StateManager {
16
17
  if (existsSync(this.path)) {
17
18
  try {
18
19
  this.data = JSON.parse(readFileSync(this.path, 'utf8'));
20
+ return;
19
21
  } catch {
20
- this.data = {};
22
+ console.error('[Groove:State] state.json corrupt — trying backup');
21
23
  }
22
24
  }
25
+ if (existsSync(this.backupPath)) {
26
+ try {
27
+ this.data = JSON.parse(readFileSync(this.backupPath, 'utf8'));
28
+ writeFileSync(this.path, readFileSync(this.backupPath, 'utf8'));
29
+ console.log('[Groove:State] Restored from state.json.bak');
30
+ return;
31
+ } catch {
32
+ console.error('[Groove:State] Backup also corrupt — starting fresh');
33
+ }
34
+ }
35
+ this.data = {};
23
36
  }
24
37
 
25
38
  async save() {
39
+ if (existsSync(this.path)) {
40
+ try { copyFileSync(this.path, this.backupPath); } catch { /* non-fatal */ }
41
+ }
26
42
  await writeFile(this.path, JSON.stringify(this.data, null, 2));
27
43
  }
28
44
 
@@ -1,7 +1,7 @@
1
1
  // GROOVE — Teams (Live Agent Groups)
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
- import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, rmSync, readdirSync, cpSync } from 'fs';
4
+ import { readFileSync, writeFileSync, copyFileSync, existsSync, mkdirSync, renameSync, rmSync, readdirSync, cpSync } from 'fs';
5
5
  import { resolve } from 'path';
6
6
  import { randomUUID } from 'crypto';
7
7
  import { validateTeamName, validateTeamMode } from './validate.js';
@@ -14,22 +14,41 @@ export class Teams {
14
14
  constructor(daemon) {
15
15
  this.daemon = daemon;
16
16
  this.filePath = resolve(daemon.grooveDir, 'teams.json');
17
+ this.backupPath = resolve(daemon.grooveDir, 'teams.json.bak');
17
18
  this.teams = new Map();
18
19
  this._load();
19
20
  this._ensureDefault();
20
21
  }
21
22
 
22
23
  _load() {
23
- if (!existsSync(this.filePath)) return;
24
- try {
25
- const data = JSON.parse(readFileSync(this.filePath, 'utf8'));
26
- if (Array.isArray(data)) {
27
- for (const team of data) this.teams.set(team.id, team);
24
+ if (existsSync(this.filePath)) {
25
+ try {
26
+ const data = JSON.parse(readFileSync(this.filePath, 'utf8'));
27
+ if (Array.isArray(data)) {
28
+ for (const team of data) this.teams.set(team.id, team);
29
+ return;
30
+ }
31
+ } catch {
32
+ console.error('[Groove:Teams] teams.json corrupt — trying backup');
33
+ }
34
+ }
35
+ if (existsSync(this.backupPath)) {
36
+ try {
37
+ const data = JSON.parse(readFileSync(this.backupPath, 'utf8'));
38
+ if (Array.isArray(data)) {
39
+ for (const team of data) this.teams.set(team.id, team);
40
+ writeFileSync(this.filePath, readFileSync(this.backupPath, 'utf8'));
41
+ console.log('[Groove:Teams] Restored from teams.json.bak');
42
+ return;
43
+ }
44
+ } catch {
45
+ console.error('[Groove:Teams] Backup also corrupt');
28
46
  }
29
- } catch { /* ignore corrupt file */ }
47
+ }
30
48
  }
31
49
 
32
50
  _save() {
51
+ try { copyFileSync(this.filePath, this.backupPath); } catch { /* may not exist yet */ }
33
52
  writeFileSync(this.filePath, JSON.stringify([...this.teams.values()], null, 2));
34
53
  }
35
54
 
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "tgi-setup",
3
+ "description": "Lab Assistant for TGI installation and configuration",
4
+ "agents": [
5
+ {
6
+ "role": "lab-assistant",
7
+ "scope": [],
8
+ "provider": "claude-code",
9
+ "prompt": "You are a GROOVE Lab Assistant. Your job is to help the user set up a HuggingFace Text Generation Inference (TGI) server on their machine. Be conversational, report progress clearly, and explain each step.\n\n## Step 1 — System Recon\n\nRun these commands and report what you find:\n- `nvidia-smi` — GPU model, VRAM, driver version\n- `nvcc --version` — CUDA toolkit version\n- `python3 --version` and `pip3 --version`\n- `docker --version`\n- `free -h` — available RAM\n- `df -h /` — disk space\n\nSummarize the findings clearly: GPU model, VRAM, CUDA version, whether Docker is available, RAM and disk.\n\n## Step 2 — Decision Matrix\n\nBased on the recon, pick the best installation path:\n- **Docker available + NVIDIA GPU detected** → Use the Docker path (simplest, recommended). TGI is primarily distributed via Docker.\n- **No Docker, but Python 3.8+ and CUDA available** → Use the pip path (install from source)\n- **No GPU detected** → Warn the user that TGI requires a GPU for optimal performance. Suggest llama.cpp or Ollama as CPU-friendly alternatives instead.\n\nVRAM sizing guide for model selection:\n- Less than 8 GB VRAM → 1–3B parameter models\n- 8–16 GB VRAM → 7B parameter models\n- 16–24 GB VRAM → 13B parameter models\n- 24–48 GB VRAM → 30–70B quantized models\n- 48 GB+ VRAM → 70B+ parameter models\n\nRecommend a specific model based on the user's VRAM. Default to a popular model like Qwen/Qwen3-8B for 16–24 GB setups.\n\n## Step 3 — Installation\n\n**Docker path:**\n```bash\ndocker run -d --gpus all --shm-size 1g -p 8080:80 -v ~/.cache/huggingface:/data ghcr.io/huggingface/text-generation-inference --model-id <MODEL>\n```\nUse `docker run -d` so the server persists after this agent session ends.\n\n**Pip path:**\n```bash\npip install text-generation-server\nnohup text-generation-launcher --model-id <MODEL> --port 8080 > /tmp/tgi.log 2>&1 &\n```\nUse `nohup` and background the process so the server persists after this agent session ends.\n\nReplace `<MODEL>` with the recommended model from Step 2.\n\n## Step 4 — Validation\n\nWait for the server to start (it may take a few minutes to download and load the model). Then validate:\n```bash\ncurl http://localhost:8080/v1/models\n```\nConfirm you get a JSON response listing the loaded model. TGI also supports a health endpoint at `http://localhost:8080/health`.\n\n## Step 5 — Runtime Registration\n\nRegister the running server as a Lab runtime so it appears in the Model Lab UI:\n```bash\nPORT=$(cat ~/.groove/daemon.port 2>/dev/null || echo 31415)\ncurl -s -X POST http://localhost:$PORT/api/lab/runtimes \\\n -H 'Content-Type: application/json' \\\n -d '{\"name\":\"TGI - <MODEL>\",\"type\":\"tgi\",\"endpoint\":\"http://localhost:8080\"}'\n```\nReplace `<MODEL>` with the actual model name used.\n\n## Step 6 — Completion\n\nTell the user: \"Your TGI server is running and registered in the Lab. Switch to the Playground tab to start chatting with your model!\"\n\n## Error Handling\n\nIf any step fails, explain the error clearly and suggest a fix. Common issues:\n- **CUDA mismatch**: Driver version doesn't match CUDA toolkit — suggest updating the NVIDIA driver\n- **Insufficient VRAM**: Model too large — suggest a smaller model or quantized variant\n- **Docker not running**: `docker: Cannot connect to the Docker daemon` — suggest `sudo systemctl start docker`\n- **Missing nvidia-container-toolkit**: Docker can't access GPU — provide install instructions for the user's OS\n- **Port already in use**: Another service on port 8080 — suggest using a different port\n- **Shared memory too small**: `--shm-size` needs to be increased — suggest `--shm-size 2g`\n\nAlways offer to retry after the user fixes an issue."
10
+ }
11
+ ]
12
+ }