groove-dev 0.9.2 → 0.10.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/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/api.js +10 -5
- package/node_modules/@groove-dev/daemon/src/process.js +133 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +14 -0
- package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/{index-iBqV1c12.js → index-C9USf6W_.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/views/CommandCenter.jsx +23 -36
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +10 -5
- package/packages/daemon/src/process.js +133 -0
- package/packages/daemon/src/providers/claude-code.js +14 -0
- package/packages/daemon/src/registry.js +1 -1
- package/packages/gui/dist/assets/{index-iBqV1c12.js → index-C9USf6W_.js} +13 -13
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/views/CommandCenter.jsx +23 -36
|
@@ -254,8 +254,9 @@ export function createApi(app, daemon) {
|
|
|
254
254
|
}
|
|
255
255
|
});
|
|
256
256
|
|
|
257
|
-
// Instruct an agent
|
|
258
|
-
//
|
|
257
|
+
// Instruct an agent — resumes session if possible, falls back to rotation
|
|
258
|
+
// Resume = zero cold-start (uses --resume SESSION_ID)
|
|
259
|
+
// Rotation = full handoff brief (only for degradation or no session)
|
|
259
260
|
app.post('/api/agents/:id/instruct', async (req, res) => {
|
|
260
261
|
try {
|
|
261
262
|
const { message } = req.body;
|
|
@@ -264,9 +265,13 @@ export function createApi(app, daemon) {
|
|
|
264
265
|
}
|
|
265
266
|
const agent = daemon.registry.get(req.params.id);
|
|
266
267
|
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
|
|
269
|
+
// Try session resume first (zero cold-start)
|
|
270
|
+
// Falls back to rotation if no session ID or provider doesn't support resume
|
|
271
|
+
const newAgent = agent.sessionId
|
|
272
|
+
? await daemon.processes.resume(req.params.id, message.trim())
|
|
273
|
+
: await daemon.rotator.rotate(req.params.id, { additionalPrompt: message.trim() });
|
|
274
|
+
|
|
270
275
|
res.json(newAgent);
|
|
271
276
|
} catch (err) {
|
|
272
277
|
res.status(400).json({ error: err.message });
|
|
@@ -194,6 +194,10 @@ For normal file edits within your scope, proceed without review.
|
|
|
194
194
|
this.daemon.classifier.addEvent(agent.id, output);
|
|
195
195
|
|
|
196
196
|
const updates = { lastActivity: new Date().toISOString() };
|
|
197
|
+
// Capture session_id for --resume support (zero cold-start continuation)
|
|
198
|
+
if (output.sessionId) {
|
|
199
|
+
updates.sessionId = output.sessionId;
|
|
200
|
+
}
|
|
197
201
|
if (output.tokensUsed !== undefined && output.tokensUsed > 0) {
|
|
198
202
|
const current = registry.get(agent.id);
|
|
199
203
|
if (current) {
|
|
@@ -262,6 +266,135 @@ For normal file edits within your scope, proceed without review.
|
|
|
262
266
|
return agent;
|
|
263
267
|
}
|
|
264
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Resume a completed agent's session with a new message.
|
|
271
|
+
* Uses --resume SESSION_ID for zero cold-start continuation.
|
|
272
|
+
* Falls back to full spawn if no session ID available.
|
|
273
|
+
*/
|
|
274
|
+
async resume(agentId, message) {
|
|
275
|
+
const { registry, locks } = this.daemon;
|
|
276
|
+
const agent = registry.get(agentId);
|
|
277
|
+
if (!agent) throw new Error(`Agent ${agentId} not found`);
|
|
278
|
+
|
|
279
|
+
// If no session ID, fall back to rotation (handoff brief)
|
|
280
|
+
if (!agent.sessionId) {
|
|
281
|
+
return this.daemon.rotator.rotate(agentId, { additionalPrompt: message });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const provider = getProvider(agent.provider || 'claude-code');
|
|
285
|
+
if (!provider?.buildResumeCommand) {
|
|
286
|
+
return this.daemon.rotator.rotate(agentId, { additionalPrompt: message });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Clean up old agent entry but keep the data we need
|
|
290
|
+
const config = { ...agent };
|
|
291
|
+
const sessionId = agent.sessionId;
|
|
292
|
+
|
|
293
|
+
// Kill if still running, or remove if dead
|
|
294
|
+
if (this.handles.has(agentId)) {
|
|
295
|
+
await this.kill(agentId);
|
|
296
|
+
} else {
|
|
297
|
+
locks.release(agentId);
|
|
298
|
+
registry.remove(agentId);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Build resume command
|
|
302
|
+
const { command, args, env } = provider.buildResumeCommand(sessionId, message, config.model);
|
|
303
|
+
|
|
304
|
+
// Set up log capture
|
|
305
|
+
const logDir = resolve(this.daemon.grooveDir, 'logs');
|
|
306
|
+
mkdirSync(logDir, { recursive: true });
|
|
307
|
+
const logPath = resolve(logDir, `${sanitizeFilename(config.name)}.log`);
|
|
308
|
+
const logStream = createWriteStream(logPath, { flags: 'a', mode: 0o600 });
|
|
309
|
+
|
|
310
|
+
const resumeLine = `[${new Date().toISOString()}] GROOVE resuming session: ${command} --resume ${sessionId}\n`;
|
|
311
|
+
logStream.write(resumeLine);
|
|
312
|
+
|
|
313
|
+
// Re-register in registry with same name
|
|
314
|
+
const newAgent = registry.add({
|
|
315
|
+
role: config.role,
|
|
316
|
+
scope: config.scope,
|
|
317
|
+
provider: config.provider,
|
|
318
|
+
model: config.model,
|
|
319
|
+
prompt: config.prompt,
|
|
320
|
+
permission: config.permission,
|
|
321
|
+
workingDir: config.workingDir,
|
|
322
|
+
name: config.name,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Carry cumulative tokens
|
|
326
|
+
if (config.tokensUsed > 0) {
|
|
327
|
+
registry.update(newAgent.id, { tokensUsed: config.tokensUsed });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Re-register locks
|
|
331
|
+
if (newAgent.scope && newAgent.scope.length > 0) {
|
|
332
|
+
locks.register(newAgent.id, newAgent.scope);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Spawn the resumed process
|
|
336
|
+
const proc = cpSpawn(command, args, {
|
|
337
|
+
cwd: config.workingDir || this.daemon.projectDir,
|
|
338
|
+
env: { ...process.env, ...env, GROOVE_AGENT_ID: newAgent.id, GROOVE_AGENT_NAME: newAgent.name },
|
|
339
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
340
|
+
detached: false,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (!proc.pid) {
|
|
344
|
+
registry.remove(newAgent.id);
|
|
345
|
+
locks.release(newAgent.id);
|
|
346
|
+
logStream.end();
|
|
347
|
+
throw new Error(`Failed to resume — process has no PID`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
this.handles.set(newAgent.id, { proc, logStream });
|
|
351
|
+
registry.update(newAgent.id, { status: 'running', pid: proc.pid });
|
|
352
|
+
|
|
353
|
+
// Same stdout/stderr/exit handling as spawn
|
|
354
|
+
proc.stdout.on('data', (chunk) => {
|
|
355
|
+
logStream.write(chunk);
|
|
356
|
+
const output = provider.parseOutput(chunk.toString());
|
|
357
|
+
if (output) {
|
|
358
|
+
this.daemon.classifier.addEvent(newAgent.id, output);
|
|
359
|
+
const updates = { lastActivity: new Date().toISOString() };
|
|
360
|
+
if (output.sessionId) updates.sessionId = output.sessionId;
|
|
361
|
+
if (output.tokensUsed !== undefined && output.tokensUsed > 0) {
|
|
362
|
+
const current = registry.get(newAgent.id);
|
|
363
|
+
if (current) {
|
|
364
|
+
updates.tokensUsed = current.tokensUsed + output.tokensUsed;
|
|
365
|
+
this.daemon.tokens.record(newAgent.id, output.tokensUsed);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (output.contextUsage !== undefined) updates.contextUsage = output.contextUsage;
|
|
369
|
+
registry.update(newAgent.id, updates);
|
|
370
|
+
this.daemon.broadcast({ type: 'agent:output', agentId: newAgent.id, data: output });
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
proc.stderr.on('data', (chunk) => { logStream.write(`[stderr] ${chunk}`); });
|
|
375
|
+
|
|
376
|
+
proc.on('exit', (code, signal) => {
|
|
377
|
+
logStream.write(`[${new Date().toISOString()}] Process exited: code=${code} signal=${signal}\n`);
|
|
378
|
+
logStream.end();
|
|
379
|
+
this.handles.delete(newAgent.id);
|
|
380
|
+
const finalStatus = signal === 'SIGTERM' || signal === 'SIGKILL' ? 'killed' : code === 0 ? 'completed' : 'crashed';
|
|
381
|
+
registry.update(newAgent.id, { status: finalStatus, pid: null });
|
|
382
|
+
this.daemon.broadcast({ type: 'agent:exit', agentId: newAgent.id, code, signal, status: finalStatus });
|
|
383
|
+
if (finalStatus === 'completed' && this.daemon.journalist) {
|
|
384
|
+
this.daemon.journalist.cycle().catch(() => {});
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
proc.on('error', (err) => {
|
|
389
|
+
logStream.write(`[error] ${err.message}\n`);
|
|
390
|
+
logStream.end();
|
|
391
|
+
this.handles.delete(newAgent.id);
|
|
392
|
+
registry.update(newAgent.id, { status: 'crashed', pid: null });
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
return newAgent;
|
|
396
|
+
}
|
|
397
|
+
|
|
265
398
|
async kill(agentId) {
|
|
266
399
|
const handle = this.handles.get(agentId);
|
|
267
400
|
|
|
@@ -61,6 +61,15 @@ export class ClaudeCodeProvider extends Provider {
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
buildResumeCommand(sessionId, prompt, model) {
|
|
65
|
+
// Resume a previous session — preserves full conversation history
|
|
66
|
+
// No cold start, no handoff brief needed
|
|
67
|
+
const args = ['--resume', sessionId, '--output-format', 'stream-json', '--verbose', '--dangerously-skip-permissions'];
|
|
68
|
+
if (model) args.push('--model', model);
|
|
69
|
+
if (prompt) args.push(prompt);
|
|
70
|
+
return { command: 'claude', args, env: {} };
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
buildHeadlessCommand(prompt, model) {
|
|
65
74
|
const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose'];
|
|
66
75
|
if (model) args.push('--model', model);
|
|
@@ -109,6 +118,11 @@ export class ClaudeCodeProvider extends Provider {
|
|
|
109
118
|
try {
|
|
110
119
|
const data = JSON.parse(l);
|
|
111
120
|
|
|
121
|
+
// Capture session_id for --resume support
|
|
122
|
+
if (data.session_id) {
|
|
123
|
+
events.push({ type: 'session', sessionId: data.session_id });
|
|
124
|
+
}
|
|
125
|
+
|
|
112
126
|
if (data.type === 'assistant') {
|
|
113
127
|
events.push({
|
|
114
128
|
type: 'activity',
|
|
@@ -48,7 +48,7 @@ export class Registry extends EventEmitter {
|
|
|
48
48
|
if (!agent) return null;
|
|
49
49
|
|
|
50
50
|
// Only allow known fields to prevent prototype pollution
|
|
51
|
-
const SAFE_FIELDS = ['status', 'pid', 'tokensUsed', 'contextUsage', 'lastActivity', 'model', 'name', 'routingMode', 'routingReason'];
|
|
51
|
+
const SAFE_FIELDS = ['status', 'pid', 'tokensUsed', 'contextUsage', 'lastActivity', 'model', 'name', 'routingMode', 'routingReason', 'sessionId'];
|
|
52
52
|
for (const key of Object.keys(updates)) {
|
|
53
53
|
if (SAFE_FIELDS.includes(key)) {
|
|
54
54
|
agent[key] = updates[key];
|