groove-dev 0.27.165 → 0.27.168

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 (43) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/adaptive.js +37 -10
  4. package/node_modules/@groove-dev/daemon/src/classifier.js +3 -1
  5. package/node_modules/@groove-dev/daemon/src/index.js +10 -9
  6. package/node_modules/@groove-dev/daemon/src/memory.js +9 -1
  7. package/node_modules/@groove-dev/daemon/src/process.js +14 -5
  8. package/node_modules/@groove-dev/daemon/src/registry.js +18 -3
  9. package/node_modules/@groove-dev/daemon/src/rotator.js +81 -10
  10. package/node_modules/@groove-dev/daemon/src/routes/agents.js +38 -25
  11. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +28 -21
  12. package/node_modules/@groove-dev/daemon/test/rotator.test.js +1 -1
  13. package/node_modules/@groove-dev/gui/dist/assets/{index-SZBexPhJ.js → index-CSMIQsrG.js} +94 -94
  14. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  15. package/node_modules/@groove-dev/gui/package.json +1 -1
  16. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +1 -1
  17. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +32 -2
  18. package/node_modules/@groove-dev/gui/src/components/fleet/fleet-content.jsx +4 -1
  19. package/node_modules/@groove-dev/gui/src/components/fleet/fleet-pane.jsx +11 -6
  20. package/node_modules/@groove-dev/gui/src/stores/groove.js +8 -4
  21. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +9 -0
  22. package/package.json +1 -1
  23. package/packages/cli/package.json +1 -1
  24. package/packages/daemon/package.json +1 -1
  25. package/packages/daemon/src/adaptive.js +37 -10
  26. package/packages/daemon/src/classifier.js +3 -1
  27. package/packages/daemon/src/index.js +10 -9
  28. package/packages/daemon/src/memory.js +9 -1
  29. package/packages/daemon/src/process.js +14 -5
  30. package/packages/daemon/src/registry.js +18 -3
  31. package/packages/daemon/src/rotator.js +81 -10
  32. package/packages/daemon/src/routes/agents.js +38 -25
  33. package/packages/daemon/src/tunnel-manager.js +28 -21
  34. package/packages/gui/dist/assets/{index-SZBexPhJ.js → index-CSMIQsrG.js} +94 -94
  35. package/packages/gui/dist/index.html +1 -1
  36. package/packages/gui/package.json +1 -1
  37. package/packages/gui/src/components/agents/agent-panel.jsx +1 -1
  38. package/packages/gui/src/components/editor/terminal.jsx +32 -2
  39. package/packages/gui/src/components/fleet/fleet-content.jsx +4 -1
  40. package/packages/gui/src/components/fleet/fleet-pane.jsx +11 -6
  41. package/packages/gui/src/stores/groove.js +8 -4
  42. package/packages/gui/src/stores/slices/agents-slice.js +9 -0
  43. package/terminal/Screenshot_2026-05-19_at_12.20.15_PM.png +0 -0
@@ -428,6 +428,7 @@ export function registerAgentRoutes(app, daemon) {
428
428
 
429
429
  // Record user feedback so the journalist can include it in future agent context
430
430
  if (daemon.journalist) daemon.journalist.recordUserFeedback(agent, finalMessage);
431
+ if (daemon.rotator) daemon.rotator.recordUserMessage(req.params.id);
431
432
 
432
433
  // Agent loop path — send message directly to the running loop
433
434
  const wrappedMessage = wrapWithRoleReminder(agent.role, finalMessage);
@@ -449,20 +450,26 @@ export function registerAgentRoutes(app, daemon) {
449
450
  if (daemon.processes.isRunning(req.params.id)) {
450
451
  await daemon.processes.kill(req.params.id);
451
452
  }
452
- daemon.registry.remove(req.params.id);
453
+ daemon.registry.remove(req.params.id, { silent: true });
453
454
  daemon.locks.release(req.params.id);
454
455
 
455
- const newAgent = await daemon.processes.spawn({
456
- role: oldConfig.role,
457
- scope: oldConfig.scope,
458
- provider: oldConfig.provider,
459
- model: oldConfig.model,
460
- prompt: finalMessage,
461
- permission: oldConfig.permission || 'full',
462
- workingDir: oldConfig.workingDir,
463
- name: oldConfig.name,
464
- teamId: oldConfig.teamId,
465
- });
456
+ let newAgent;
457
+ try {
458
+ newAgent = await daemon.processes.spawn({
459
+ role: oldConfig.role,
460
+ scope: oldConfig.scope,
461
+ provider: oldConfig.provider,
462
+ model: oldConfig.model,
463
+ prompt: finalMessage,
464
+ permission: oldConfig.permission || 'full',
465
+ workingDir: oldConfig.workingDir,
466
+ name: oldConfig.name,
467
+ teamId: oldConfig.teamId,
468
+ });
469
+ } catch (spawnErr) {
470
+ daemon.registry.flushPendingRemovals();
471
+ throw spawnErr;
472
+ }
466
473
  daemon.audit.log('agent.instruct', { id: req.params.id, newId: newAgent.id, resumed: false });
467
474
  return res.json(newAgent);
468
475
  }
@@ -472,21 +479,27 @@ export function registerAgentRoutes(app, daemon) {
472
479
  // run one prompt per spawn and cannot resume sessions.
473
480
  if (provider?.constructor?.nonInteractive && !daemon.processes.isRunning(req.params.id)) {
474
481
  const oldConfig = { ...agent };
475
- daemon.registry.remove(req.params.id);
482
+ daemon.registry.remove(req.params.id, { silent: true });
476
483
  daemon.locks.release(req.params.id);
477
484
 
478
- const newAgent = await daemon.processes.spawn({
479
- role: oldConfig.role,
480
- scope: oldConfig.scope,
481
- provider: oldConfig.provider,
482
- model: oldConfig.model,
483
- prompt: finalMessage,
484
- introContext: oldConfig.introContext,
485
- permission: oldConfig.permission || 'full',
486
- workingDir: oldConfig.workingDir,
487
- name: oldConfig.name,
488
- teamId: oldConfig.teamId,
489
- });
485
+ let newAgent;
486
+ try {
487
+ newAgent = await daemon.processes.spawn({
488
+ role: oldConfig.role,
489
+ scope: oldConfig.scope,
490
+ provider: oldConfig.provider,
491
+ model: oldConfig.model,
492
+ prompt: finalMessage,
493
+ introContext: oldConfig.introContext,
494
+ permission: oldConfig.permission || 'full',
495
+ workingDir: oldConfig.workingDir,
496
+ name: oldConfig.name,
497
+ teamId: oldConfig.teamId,
498
+ });
499
+ } catch (spawnErr) {
500
+ daemon.registry.flushPendingRemovals();
501
+ throw spawnErr;
502
+ }
490
503
  daemon.audit.log('agent.instruct', { id: req.params.id, newId: newAgent.id, resumed: false });
491
504
  return res.json(newAgent);
492
505
  }
@@ -479,6 +479,10 @@ export class TunnelManager {
479
479
  // Remote is behind npm — upgrade
480
480
  this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'upgrading', from: remoteVer, to: npmVer } });
481
481
 
482
+ const target = `${config.user}@${config.host}`;
483
+ const keyArgs = config.sshKeyPath ? ['-i', config.sshKeyPath] : [];
484
+ const sshBase = [...keyArgs, '-p', String(config.port || 22), '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', target];
485
+
482
486
  const installCmd = npmGlobalInstall(`groove-dev@${npmVer}`, config.user);
483
487
  const cleanupCmd = 'rm -rf $(npm root -g)/.groove-dev-* $(npm root -g)/groove-dev 2>/dev/null || true';
484
488
 
@@ -496,40 +500,43 @@ export class TunnelManager {
496
500
  }
497
501
  }
498
502
 
499
- // Restart remote daemon
503
+ // Restart remote daemon — fire and forget the SSH, verify through the tunnel
500
504
  const cdPrefix = config.projectDir ? `cd "${config.projectDir}" && ` : '';
501
- const setProjectDir = config.projectDir
502
- ? `curl -sf -X POST -H 'Content-Type: application/json' --data '{"path":"${config.projectDir}"}' http://localhost:${REMOTE_PORT}/api/project-dir > /dev/null 2>&1 || true; `
503
- : '';
504
- const restartCmd = `kill $(lsof -t -i:${REMOTE_PORT}) 2>/dev/null || true; sleep 2; ${cdPrefix}GROOVE_BIN=$(which groove) && nohup "$GROOVE_BIN" start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; sleep 4; curl -sf http://localhost:${REMOTE_PORT}/api/status && (${setProjectDir}true) || true`;
505
- execFileSync('ssh', [...sshBase, sshCmd(restartCmd)], {
506
- encoding: 'utf8', timeout: 60000, stdio: ['pipe', 'pipe', 'pipe'],
507
- });
505
+ try {
506
+ execFileSync('ssh', [...sshBase, sshCmd(`kill $(lsof -t -i:${REMOTE_PORT}) 2>/dev/null || true; sleep 1; ${cdPrefix}GROOVE_BIN=$(which groove) && nohup "$GROOVE_BIN" start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown`)], {
507
+ encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'],
508
+ });
509
+ } catch { /* SSH may close before nohup finishes — that's fine */ }
508
510
 
509
- // Verify through tunnel
511
+ // Wait for daemon to come back up through the existing tunnel
512
+ this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'starting' } });
510
513
  let daemonVer = null;
511
- for (let i = 0; i < 3; i++) {
514
+ for (let i = 0; i < 8; i++) {
515
+ await new Promise(r => setTimeout(r, 2000));
512
516
  try {
513
- const check = await fetch(`http://localhost:${localPort}/api/status`, {
514
- signal: AbortSignal.timeout(3000),
515
- });
517
+ const check = await fetch(`http://localhost:${localPort}/api/status`, { signal: AbortSignal.timeout(3000) });
516
518
  if (check.ok) {
517
519
  daemonVer = (await check.json()).version || null;
518
520
  break;
519
521
  }
520
- } catch { /* retry */ }
521
- await new Promise(r => setTimeout(r, 2000));
522
+ } catch { /* not up yet */ }
522
523
  }
523
524
 
524
- const localVer = getLocalVersion();
525
- if (daemonVer) {
526
- this.daemon.broadcast({ type: 'tunnel.version-info', data: { id, localVersion: localVer, remoteVersion: daemonVer, match: daemonVer === localVer } });
527
- } else {
528
- this.daemon.broadcast({ type: 'tunnel.upgrade-failed', data: { id, error: 'Daemon did not respond after restart', from: remoteVer, attempted: npmVer } });
525
+ if (config.projectDir && daemonVer) {
526
+ try {
527
+ await fetch(`http://localhost:${localPort}/api/project-dir`, {
528
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
529
+ body: JSON.stringify({ path: config.projectDir }),
530
+ signal: AbortSignal.timeout(3000),
531
+ });
532
+ } catch { /* best effort */ }
529
533
  }
530
534
 
535
+ const localVer = getLocalVersion();
536
+ this.daemon.broadcast({ type: 'tunnel.version-info', data: { id, localVersion: localVer, remoteVersion: daemonVer || npmVer, match: (daemonVer || npmVer) === localVer } });
531
537
  this.daemon.audit.log('tunnel.upgrade', { id, from: remoteVer, to: daemonVer || npmVer });
532
538
  } catch (err) {
539
+ // Upgrade failed but tunnel may still work — check before reporting failure
533
540
  try {
534
541
  const verify = await fetch(`http://localhost:${localPort}/api/status`, { signal: AbortSignal.timeout(5000) });
535
542
  if (verify.ok) {
@@ -537,7 +544,7 @@ export class TunnelManager {
537
544
  this.daemon.broadcast({ type: 'tunnel.version-info', data: { id, localVersion: getLocalVersion(), remoteVersion: verifyData.version, match: false } });
538
545
  return;
539
546
  }
540
- } catch { /* tunnel verification failed */ }
547
+ } catch { /* tunnel down */ }
541
548
  this.daemon.broadcast({ type: 'tunnel.upgrade-failed', data: { id, error: err.message } });
542
549
  }
543
550
  }