groove-dev 0.27.113 → 0.27.116

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 (70) hide show
  1. package/CENTRAL_COMMAND_REBUILD.md +689 -0
  2. package/EMBEDDING_DIAGNOSTIC.md +197 -0
  3. package/TRAINING_DATA_v4.md +6 -0
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/cli/src/commands/team.js +59 -2
  6. package/node_modules/@groove-dev/daemon/package.json +1 -1
  7. package/node_modules/@groove-dev/daemon/src/api.js +27 -2
  8. package/node_modules/@groove-dev/daemon/src/filewatcher.js +45 -0
  9. package/node_modules/@groove-dev/daemon/src/index.js +14 -2
  10. package/node_modules/@groove-dev/daemon/src/process.js +254 -208
  11. package/node_modules/@groove-dev/daemon/src/teams.js +143 -20
  12. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +78 -45
  13. package/node_modules/@groove-dev/gui/dist/assets/index-DdN9RVnC.css +1 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/{index-BYh6iHqL.js → index-fq--PD7_.js} +1731 -1731
  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/agents/workspace-mode.jsx +0 -22
  18. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +43 -45
  19. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +2 -1
  20. package/node_modules/@groove-dev/gui/src/components/teams/team-removal-dialog.jsx +156 -0
  21. package/node_modules/@groove-dev/gui/src/stores/groove.js +57 -12
  22. package/node_modules/@groove-dev/gui/src/views/agents.jsx +23 -4
  23. package/node_modules/@groove-dev/gui/src/views/editor.jsx +1 -20
  24. package/node_modules/@groove-dev/gui/src/views/teams.jsx +84 -5
  25. package/package.json +1 -1
  26. package/packages/cli/package.json +1 -1
  27. package/packages/cli/src/commands/team.js +59 -2
  28. package/packages/daemon/package.json +1 -1
  29. package/packages/daemon/src/api.js +27 -2
  30. package/packages/daemon/src/filewatcher.js +45 -0
  31. package/packages/daemon/src/index.js +14 -2
  32. package/packages/daemon/src/process.js +254 -208
  33. package/packages/daemon/src/teams.js +143 -20
  34. package/packages/daemon/src/tunnel-manager.js +78 -45
  35. package/packages/gui/dist/assets/index-DdN9RVnC.css +1 -0
  36. package/packages/gui/dist/assets/{index-BYh6iHqL.js → index-fq--PD7_.js} +1731 -1731
  37. package/packages/gui/dist/index.html +2 -2
  38. package/packages/gui/package.json +1 -1
  39. package/packages/gui/src/components/agents/workspace-mode.jsx +0 -22
  40. package/packages/gui/src/components/layout/status-bar.jsx +43 -45
  41. package/packages/gui/src/components/settings/quick-connect.jsx +2 -1
  42. package/packages/gui/src/components/teams/team-removal-dialog.jsx +156 -0
  43. package/packages/gui/src/stores/groove.js +57 -12
  44. package/packages/gui/src/views/agents.jsx +23 -4
  45. package/packages/gui/src/views/editor.jsx +1 -20
  46. package/packages/gui/src/views/teams.jsx +84 -5
  47. package/TRAINING_DATA_v3.md +0 -11
  48. package/codex-test/offroad-nitro-racer/dist/assets/index-CuvdKK6U.js +0 -44
  49. package/codex-test/offroad-nitro-racer/dist/assets/index-DvHn2Thu.css +0 -1
  50. package/codex-test/offroad-nitro-racer/dist/index.html +0 -23
  51. package/codex-test/offroad-nitro-racer/index.html +0 -21
  52. package/codex-test/offroad-nitro-racer/package-lock.json +0 -841
  53. package/codex-test/offroad-nitro-racer/package.json +0 -15
  54. package/codex-test/offroad-nitro-racer/src/game/AI.ts +0 -28
  55. package/codex-test/offroad-nitro-racer/src/game/Audio.ts +0 -63
  56. package/codex-test/offroad-nitro-racer/src/game/Car.ts +0 -247
  57. package/codex-test/offroad-nitro-racer/src/game/Effects.ts +0 -62
  58. package/codex-test/offroad-nitro-racer/src/game/Game.ts +0 -229
  59. package/codex-test/offroad-nitro-racer/src/game/Input.ts +0 -45
  60. package/codex-test/offroad-nitro-racer/src/game/Renderer.ts +0 -224
  61. package/codex-test/offroad-nitro-racer/src/game/Track.ts +0 -158
  62. package/codex-test/offroad-nitro-racer/src/game/UI.ts +0 -96
  63. package/codex-test/offroad-nitro-racer/src/game/math.ts +0 -42
  64. package/codex-test/offroad-nitro-racer/src/main.ts +0 -24
  65. package/codex-test/offroad-nitro-racer/src/style.css +0 -291
  66. package/codex-test/offroad-nitro-racer/src/vite-env.d.ts +0 -1
  67. package/codex-test/offroad-nitro-racer/tsconfig.json +0 -18
  68. package/codex-test/offroad-nitro-racer/vite.config.ts +0 -7
  69. package/node_modules/@groove-dev/gui/dist/assets/index-DAlSbVyK.css +0 -1
  70. package/packages/gui/dist/assets/index-DAlSbVyK.css +0 -1
@@ -329,6 +329,7 @@ export class ProcessManager {
329
329
  this._streamThrottle = new Map(); // agentId -> { timer, pending }
330
330
  this._rotatingAgents = new Set(); // agentIds currently being rotated (rotator wrote handoff)
331
331
  this._stalledAgents = new Set(); // agentIds already flagged as stalled (avoids duplicate broadcasts)
332
+ this._exitHandled = new Set();
332
333
 
333
334
  this._stallWatchdog = setInterval(() => this._checkStalls(), STALL_CHECK_INTERVAL_MS);
334
335
  if (this._stallWatchdog.unref) this._stallWatchdog.unref();
@@ -366,6 +367,242 @@ export class ProcessManager {
366
367
  });
367
368
  console.warn(`[Groove] Agent ${agent.name} (${agentId}) silent for ${Math.round(silentMs / 1000)}s — possible stalled API stream`);
368
369
  }
370
+
371
+ // Defense in depth: detect zombie handles where PID is no longer alive
372
+ const ZOMBIE_THRESHOLD_MS = 10 * 60_000;
373
+ for (const [agentId, handle] of this.handles.entries()) {
374
+ const agent = registry.get(agentId);
375
+ if (!agent) continue;
376
+ const lastActivity = agent.lastActivity ? new Date(agent.lastActivity).getTime() : now;
377
+ if (now - lastActivity < ZOMBIE_THRESHOLD_MS) continue;
378
+ const pid = handle.proc?.pid;
379
+ if (!pid) continue;
380
+ try {
381
+ process.kill(pid, 0);
382
+ } catch {
383
+ console.warn(`[Groove] Agent ${agent.name} (${agentId}) PID ${pid} no longer alive — force-cleaning handle`);
384
+ if (handle.logStream && !handle.logStream.destroyed) {
385
+ handle.logStream.write(`[${new Date().toISOString()}] Force-cleaned: PID ${pid} no longer alive\n`);
386
+ handle.logStream.end();
387
+ }
388
+ this.handles.delete(agentId);
389
+ this._exitHandled.add(agentId);
390
+ setTimeout(() => this._exitHandled.delete(agentId), 30_000);
391
+ this._stalledAgents.delete(agentId);
392
+ const throttle = this._streamThrottle.get(agentId);
393
+ if (throttle?.timer) clearTimeout(throttle.timer);
394
+ this._streamThrottle.delete(agentId);
395
+ this.peakContextUsage.delete(agentId);
396
+ this.pendingMessages.delete(agentId);
397
+ if (this.daemon.locks) this.daemon.locks.release(agentId);
398
+ registry.update(agentId, { status: 'completed', pid: null });
399
+ this.daemon.broadcast({ type: 'agent:exit', agentId, code: 0, signal: null, status: 'completed' });
400
+ }
401
+ }
402
+ }
403
+
404
+ _handleProcessExit(agent, code, signal, logStream, stderrBuf, logPath) {
405
+ if (this._exitHandled.has(agent.id)) return;
406
+ this._exitHandled.add(agent.id);
407
+ setTimeout(() => this._exitHandled.delete(agent.id), 30_000);
408
+
409
+ const { registry } = this.daemon;
410
+
411
+ if (!logStream.destroyed) {
412
+ logStream.write(`[${new Date().toISOString()}] Process exited: code=${code} signal=${signal}\n`);
413
+ logStream.end();
414
+ }
415
+
416
+ this.handles.delete(agent.id);
417
+
418
+ const throttle = this._streamThrottle.get(agent.id);
419
+ if (throttle?.timer) clearTimeout(throttle.timer);
420
+ this._streamThrottle.delete(agent.id);
421
+
422
+ this.peakContextUsage.delete(agent.id);
423
+ this.pendingMessages.delete(agent.id);
424
+ this._stalledAgents.delete(agent.id);
425
+
426
+ if (this.daemon.locks) this.daemon.locks.release(agent.id);
427
+
428
+ const finalStatus = signal === 'SIGTERM' || signal === 'SIGKILL'
429
+ ? 'killed'
430
+ : code === 0
431
+ ? 'completed'
432
+ : 'crashed';
433
+
434
+ const crashError = finalStatus === 'crashed' ? stderrBuf.join('').trim().slice(-500) : null;
435
+
436
+ registry.update(agent.id, { status: finalStatus, pid: null });
437
+
438
+ if (this.daemon.timeline) {
439
+ const agentData = registry.get(agent.id);
440
+ this.daemon.timeline.recordEvent(finalStatus === 'completed' ? 'complete' : finalStatus === 'crashed' ? 'crash' : 'kill', {
441
+ agentId: agent.id, agentName: agent.name, role: agent.role,
442
+ finalTokens: agentData?.tokensUsed || 0, costUsd: agentData?.costUsd || 0,
443
+ exitCode: code,
444
+ });
445
+ }
446
+
447
+ if (this.daemon.trajectoryCapture) {
448
+ try {
449
+ if (finalStatus === 'completed') {
450
+ this.daemon.trajectoryCapture.onAgentComplete(agent.id, {
451
+ status: 'SUCCESS', exit_code: code, signal,
452
+ });
453
+ } else {
454
+ this.daemon.trajectoryCapture.onAgentCrash(agent.id,
455
+ signal ? 'Killed by signal ' + signal : 'Exit code ' + code
456
+ );
457
+ }
458
+ const count = (this.daemon.state.get('training_sessions_captured') || 0) + 1;
459
+ this.daemon.state.set('training_sessions_captured', count);
460
+ } catch (e) { /* fail silent */ }
461
+ }
462
+
463
+ this.daemon.broadcast({
464
+ type: 'agent:exit',
465
+ agentId: agent.id,
466
+ code,
467
+ signal,
468
+ status: finalStatus,
469
+ error: crashError || undefined,
470
+ });
471
+
472
+ if (this.daemon.integrations) {
473
+ this.daemon.integrations.refreshMcpJson();
474
+ }
475
+
476
+ if (finalStatus === 'completed' && agent.role === 'planner') {
477
+ this._extractRecommendedTeam(agent, logPath);
478
+ }
479
+
480
+ if (finalStatus === 'completed') {
481
+ const pending = this.consumePendingMessage(agent.id);
482
+ if (pending) {
483
+ const agentData = registry.get(agent.id);
484
+ if (agentData?.sessionId) {
485
+ this.resume(agent.id, pending.message).catch((err) => {
486
+ console.error(`[Groove] Auto-resume with queued message failed for ${agent.name}: ${err.message}`);
487
+ });
488
+ return;
489
+ }
490
+ }
491
+ }
492
+
493
+ if (finalStatus === 'completed' && this.daemon.journalist) {
494
+ const a = registry.get(agent.id);
495
+ const turns = a?.turns || 0;
496
+ const tok = a?.tokensUsed || 0;
497
+ if (turns > 1 || tok >= 100) {
498
+ this.daemon.journalist.requestSynthesis('completion');
499
+ }
500
+ }
501
+
502
+ this._checkPhase2(agent.id);
503
+
504
+ if (agent.teamId) {
505
+ this._checkPreviewReady(agent.teamId);
506
+ }
507
+
508
+ if (finalStatus === 'completed') {
509
+ const files = this.daemon.journalist?.getAgentFiles(agent) || [];
510
+ if (files.length > 0) this._triggerIdleQC(agent);
511
+ this._processHandoffs(agent);
512
+ if (this._rotatingAgents.has(agent.id)) {
513
+ this._rotatingAgents.delete(agent.id);
514
+ } else {
515
+ this._writeCompletionHandoff(agent).catch(err => console.error(`[Groove] Completion handoff failed for ${agent.name}:`, err.message));
516
+ }
517
+ }
518
+
519
+ if (this.daemon.memory && (finalStatus === 'completed' || finalStatus === 'crashed')) {
520
+ try {
521
+ const events = this.daemon.classifier?.agentWindows?.[agent.id] || [];
522
+ const signals = events.length >= 6
523
+ ? this.daemon.adaptive.extractSignals(events, agent.scope)
524
+ : null;
525
+ const score = signals ? this.daemon.adaptive.scoreSession(signals) : null;
526
+ const files = this.daemon.journalist?.getAgentFiles(agent) || [];
527
+ this.daemon.memory.updateSpecialization(agent.id, {
528
+ role: agent.role,
529
+ qualityScore: score,
530
+ filesTouched: files,
531
+ signals,
532
+ threshold: this.daemon.adaptive?.getThreshold(agent.provider, agent.role),
533
+ });
534
+ } catch { /* best-effort */ }
535
+ }
536
+ }
537
+
538
+ _handleResumeProcessExit(agent, code, signal, logStream) {
539
+ if (this._exitHandled.has(agent.id)) return;
540
+ this._exitHandled.add(agent.id);
541
+ setTimeout(() => this._exitHandled.delete(agent.id), 30_000);
542
+
543
+ const { registry } = this.daemon;
544
+
545
+ if (!logStream.destroyed) {
546
+ logStream.write(`[${new Date().toISOString()}] Process exited: code=${code} signal=${signal}\n`);
547
+ logStream.end();
548
+ }
549
+
550
+ this.handles.delete(agent.id);
551
+ this._stalledAgents.delete(agent.id);
552
+
553
+ if (this.daemon.locks) this.daemon.locks.release(agent.id);
554
+
555
+ const finalStatus = signal === 'SIGTERM' || signal === 'SIGKILL' ? 'killed' : code === 0 ? 'completed' : 'crashed';
556
+ registry.update(agent.id, { status: finalStatus, pid: null });
557
+
558
+ if (this.daemon.trajectoryCapture) {
559
+ try {
560
+ if (finalStatus === 'completed') {
561
+ this.daemon.trajectoryCapture.onAgentComplete(agent.id, {
562
+ status: 'SUCCESS', exit_code: code, signal,
563
+ });
564
+ } else {
565
+ this.daemon.trajectoryCapture.onAgentCrash(agent.id,
566
+ signal ? 'Killed by signal ' + signal : 'Exit code ' + code
567
+ );
568
+ }
569
+ const count = (this.daemon.state.get('training_sessions_captured') || 0) + 1;
570
+ this.daemon.state.set('training_sessions_captured', count);
571
+ } catch (e) { /* fail silent */ }
572
+ }
573
+
574
+ this.daemon.broadcast({ type: 'agent:exit', agentId: agent.id, code, signal, status: finalStatus });
575
+ if (finalStatus === 'completed' && this.daemon.journalist) {
576
+ const a = registry.get(agent.id);
577
+ const turns = a?.turns || 0;
578
+ const tok = a?.tokensUsed || 0;
579
+ if (turns > 1 || tok >= 100) this.daemon.journalist.requestSynthesis('completion');
580
+ }
581
+
582
+ if (finalStatus === 'completed' && !this._rotatingAgents.has(agent.id)) {
583
+ this._writeCompletionHandoff(agent).catch(err =>
584
+ console.error(`[Groove] Completion handoff failed for ${agent.name}:`, err.message));
585
+ }
586
+ if (this._rotatingAgents.has(agent.id)) {
587
+ this._rotatingAgents.delete(agent.id);
588
+ }
589
+ if (this.daemon.memory && (finalStatus === 'completed' || finalStatus === 'crashed')) {
590
+ try {
591
+ const events = this.daemon.classifier?.agentWindows?.[agent.id] || [];
592
+ const signals = events.length >= 6
593
+ ? this.daemon.adaptive.extractSignals(events, agent.scope)
594
+ : null;
595
+ const score = signals ? this.daemon.adaptive.scoreSession(signals) : null;
596
+ const files = this.daemon.journalist?.getAgentFiles(agent) || [];
597
+ this.daemon.memory.updateSpecialization(agent.id, {
598
+ role: agent.role,
599
+ qualityScore: score,
600
+ filesTouched: files,
601
+ signals,
602
+ threshold: this.daemon.adaptive?.getThreshold(agent.provider, agent.role),
603
+ });
604
+ } catch { /* best-effort */ }
605
+ }
369
606
  }
370
607
 
371
608
  async spawn(config) {
@@ -732,6 +969,7 @@ For normal file edits within your scope, proceed without review.
732
969
  logStream.write(`[${new Date().toISOString()}] Agent loop exited: status=${status}\n`);
733
970
  logStream.end();
734
971
  this.handles.delete(agent.id);
972
+ this._stalledAgents.delete(agent.id);
735
973
 
736
974
  // Clean up stream throttle so pending timers don't fire for dead agents
737
975
  const throttle = this._streamThrottle.get(agent.id);
@@ -775,8 +1013,9 @@ For normal file edits within your scope, proceed without review.
775
1013
  this.daemon.broadcast({ type: 'agent:exit', agentId: agent.id, code: code || 0, signal, status });
776
1014
  if (this.daemon.integrations) this.daemon.integrations.refreshMcpJson();
777
1015
  if (status === 'completed' && this.daemon.journalist) {
778
- const turns = agentData?.turns || 0;
779
- const tok = agentData?.tokensUsed || 0;
1016
+ const a = registry.get(agent.id);
1017
+ const turns = a?.turns || 0;
1018
+ const tok = a?.tokensUsed || 0;
780
1019
  if (turns > 1 || tok >= 100) this.daemon.journalist.requestSynthesis('completion');
781
1020
  }
782
1021
  this._checkPhase2(agent.id);
@@ -862,6 +1101,7 @@ For normal file edits within your scope, proceed without review.
862
1101
  if (!logStream.destroyed) logStream.write(`[${new Date().toISOString()}] Spawn error: ${err.message}\n`);
863
1102
  if (!logStream.destroyed) logStream.end();
864
1103
  this.handles.delete(agent.id);
1104
+ this._exitHandled.add(agent.id);
865
1105
  registry.update(agent.id, { status: 'crashed', pid: null });
866
1106
  this.daemon.broadcast({ type: 'agent:exit', agentId: agent.id, code: null, signal: null, status: 'crashed', error: err.message });
867
1107
  });
@@ -906,154 +1146,13 @@ For normal file edits within your scope, proceed without review.
906
1146
  while (stderrBuf.join('').length > 2048) stderrBuf.shift();
907
1147
  });
908
1148
 
909
- // Handle process exit
1149
+ // Handle process exit — cleanup extracted to _handleProcessExit with dedup
910
1150
  proc.on('exit', (code, signal) => {
911
- const exitLine = `[${new Date().toISOString()}] Process exited: code=${code} signal=${signal}\n`;
912
- logStream.write(exitLine);
913
- logStream.end();
914
-
915
- this.handles.delete(agent.id);
916
-
917
- // Clean up stream throttle so pending timers don't fire for dead agents
918
- const throttle = this._streamThrottle.get(agent.id);
919
- if (throttle?.timer) clearTimeout(throttle.timer);
920
- this._streamThrottle.delete(agent.id);
921
-
922
- // Clean up per-agent maps to prevent unbounded growth in long sessions
923
- this.peakContextUsage.delete(agent.id);
924
- this.pendingMessages.delete(agent.id);
925
- this._stalledAgents.delete(agent.id);
926
-
927
- // Release file-scope locks so they don't persist after agent death
928
- if (this.daemon.locks) this.daemon.locks.release(agent.id);
929
-
930
- const finalStatus = signal === 'SIGTERM' || signal === 'SIGKILL'
931
- ? 'killed'
932
- : code === 0
933
- ? 'completed'
934
- : 'crashed';
935
-
936
- // Capture crash error from stderr for UI display
937
- const crashError = finalStatus === 'crashed' ? stderrBuf.join('').trim().slice(-500) : null;
938
-
939
- registry.update(agent.id, { status: finalStatus, pid: null });
940
-
941
- // Record lifecycle event for timeline
942
- if (this.daemon.timeline) {
943
- const agentData = registry.get(agent.id);
944
- this.daemon.timeline.recordEvent(finalStatus === 'completed' ? 'complete' : finalStatus === 'crashed' ? 'crash' : 'kill', {
945
- agentId: agent.id, agentName: agent.name, role: agent.role,
946
- finalTokens: agentData?.tokensUsed || 0, costUsd: agentData?.costUsd || 0,
947
- exitCode: code,
948
- });
949
- }
950
-
951
- if (this.daemon.trajectoryCapture) {
952
- try {
953
- if (finalStatus === 'completed') {
954
- this.daemon.trajectoryCapture.onAgentComplete(agent.id, {
955
- status: 'SUCCESS', exit_code: code, signal,
956
- });
957
- } else {
958
- this.daemon.trajectoryCapture.onAgentCrash(agent.id,
959
- signal ? 'Killed by signal ' + signal : 'Exit code ' + code
960
- );
961
- }
962
- const count = (this.daemon.state.get('training_sessions_captured') || 0) + 1;
963
- this.daemon.state.set('training_sessions_captured', count);
964
- } catch (e) { /* fail silent */ }
965
- }
966
-
967
- this.daemon.broadcast({
968
- type: 'agent:exit',
969
- agentId: agent.id,
970
- code,
971
- signal,
972
- status: finalStatus,
973
- error: crashError || undefined,
974
- });
975
-
976
- // Refresh MCP config — remove integrations no longer needed by running agents
977
- if (this.daemon.integrations) {
978
- this.daemon.integrations.refreshMcpJson();
979
- }
980
-
981
- // Extract recommended-team.json from planner text output if it wasn't written to disk.
982
- // Non-Claude providers (Codex, Gemini) may embed the JSON in text rather than using Write.
983
- if (finalStatus === 'completed' && agent.role === 'planner') {
984
- this._extractRecommendedTeam(agent, logPath);
985
- }
986
-
987
- // Auto-resume with queued message: if the user sent a message while this
988
- // CLI agent was still running, resume the session now that it's done.
989
- if (finalStatus === 'completed') {
990
- const pending = this.consumePendingMessage(agent.id);
991
- if (pending) {
992
- const agentData = registry.get(agent.id);
993
- if (agentData?.sessionId) {
994
- this.resume(agent.id, pending.message).catch((err) => {
995
- console.error(`[Groove] Auto-resume with queued message failed for ${agent.name}: ${err.message}`);
996
- });
997
- return;
998
- }
999
- }
1000
- }
1001
-
1002
- // Trigger journalist synthesis on completion (event-driven, debounced).
1003
- // Skip trivial sessions — a greeting-only completion (user never gave a task)
1004
- // has nothing worth synthesizing and wastes a $0.04+ headless claude call.
1005
- if (finalStatus === 'completed' && this.daemon.journalist) {
1006
- const a = registry.get(agent.id);
1007
- const turns = a?.turns || 0;
1008
- const tok = a?.tokensUsed || 0;
1009
- if (turns > 1 || tok >= 100) {
1010
- this.daemon.journalist.requestSynthesis('completion');
1011
- }
1012
- }
1013
-
1014
- // Phase 2 auto-spawn: check if all phase 1 agents for a team are done
1015
- this._checkPhase2(agent.id);
1016
-
1017
- // Preview launch: when every agent in this team is in a terminal state,
1018
- // kick off the one-click preview (dev server or static serve) the planner
1019
- // staged in the team plan. Fires once per team launch.
1020
- // Fire on any terminal status so crashed QC agents don't block preview
1021
- // when builders completed successfully.
1022
- if (agent.teamId) {
1023
- this._checkPreviewReady(agent.teamId);
1024
- }
1025
-
1026
- // Auto-trigger idle QC: if this agent modified files and there's an idle QC
1027
- // in the same team, activate it to verify the changes
1028
- if (finalStatus === 'completed') {
1029
- const files = this.daemon.journalist?.getAgentFiles(agent) || [];
1030
- if (files.length > 0) this._triggerIdleQC(agent);
1031
- this._processHandoffs(agent);
1032
- if (this._rotatingAgents.has(agent.id)) {
1033
- this._rotatingAgents.delete(agent.id);
1034
- } else {
1035
- this._writeCompletionHandoff(agent).catch(err => console.error(`[Groove] Completion handoff failed for ${agent.name}:`, err.message));
1036
- }
1037
- }
1151
+ this._handleProcessExit(agent, code, signal, logStream, stderrBuf, logPath);
1152
+ });
1038
1153
 
1039
- // Update Layer 7 specialization profile for this agent's session
1040
- if (this.daemon.memory && (finalStatus === 'completed' || finalStatus === 'crashed')) {
1041
- try {
1042
- const events = this.daemon.classifier?.agentWindows?.[agent.id] || [];
1043
- const signals = events.length >= 6
1044
- ? this.daemon.adaptive.extractSignals(events, agent.scope)
1045
- : null;
1046
- const score = signals ? this.daemon.adaptive.scoreSession(signals) : null;
1047
- const files = this.daemon.journalist?.getAgentFiles(agent) || [];
1048
- this.daemon.memory.updateSpecialization(agent.id, {
1049
- role: agent.role,
1050
- qualityScore: score,
1051
- filesTouched: files,
1052
- signals,
1053
- threshold: this.daemon.adaptive?.getThreshold(agent.provider, agent.role),
1054
- });
1055
- } catch { /* best-effort */ }
1056
- }
1154
+ proc.on('close', (code, signal) => {
1155
+ this._handleProcessExit(agent, code, signal, logStream, stderrBuf, logPath);
1057
1156
  });
1058
1157
 
1059
1158
  proc.on('error', (err) => {
@@ -1061,6 +1160,7 @@ For normal file edits within your scope, proceed without review.
1061
1160
  logStream.end();
1062
1161
 
1063
1162
  this.handles.delete(agent.id);
1163
+ this._exitHandled.add(agent.id);
1064
1164
  if (this.daemon.locks) this.daemon.locks.release(agent.id);
1065
1165
  registry.update(agent.id, { status: 'crashed', pid: null });
1066
1166
  this.daemon.broadcast({
@@ -1763,6 +1863,7 @@ For normal file edits within your scope, proceed without review.
1763
1863
  if (!logStream.destroyed) logStream.write(`[${new Date().toISOString()}] Resume spawn error: ${err.message}\n`);
1764
1864
  if (!logStream.destroyed) logStream.end();
1765
1865
  this.handles.delete(newAgent.id);
1866
+ this._exitHandled.add(newAgent.id);
1766
1867
  registry.update(newAgent.id, { status: 'crashed', pid: null });
1767
1868
  this.daemon.broadcast({ type: 'agent:exit', agentId: newAgent.id, code: null, signal: null, status: 'crashed', error: err.message });
1768
1869
  });
@@ -1795,73 +1896,18 @@ For normal file edits within your scope, proceed without review.
1795
1896
  proc.stderr.on('data', (chunk) => { logStream.write(`[stderr] ${chunk}`); });
1796
1897
 
1797
1898
  proc.on('exit', (code, signal) => {
1798
- logStream.write(`[${new Date().toISOString()}] Process exited: code=${code} signal=${signal}\n`);
1799
- logStream.end();
1800
- this.handles.delete(newAgent.id);
1801
- this._stalledAgents.delete(newAgent.id);
1802
-
1803
- // Release file-scope locks so they don't persist after agent death
1804
- if (this.daemon.locks) this.daemon.locks.release(newAgent.id);
1805
-
1806
- const finalStatus = signal === 'SIGTERM' || signal === 'SIGKILL' ? 'killed' : code === 0 ? 'completed' : 'crashed';
1807
- registry.update(newAgent.id, { status: finalStatus, pid: null });
1808
-
1809
- if (this.daemon.trajectoryCapture) {
1810
- try {
1811
- if (finalStatus === 'completed') {
1812
- this.daemon.trajectoryCapture.onAgentComplete(newAgent.id, {
1813
- status: 'SUCCESS', exit_code: code, signal,
1814
- });
1815
- } else {
1816
- this.daemon.trajectoryCapture.onAgentCrash(newAgent.id,
1817
- signal ? 'Killed by signal ' + signal : 'Exit code ' + code
1818
- );
1819
- }
1820
- const count = (this.daemon.state.get('training_sessions_captured') || 0) + 1;
1821
- this.daemon.state.set('training_sessions_captured', count);
1822
- } catch (e) { /* fail silent */ }
1823
- }
1824
-
1825
- this.daemon.broadcast({ type: 'agent:exit', agentId: newAgent.id, code, signal, status: finalStatus });
1826
- if (finalStatus === 'completed' && this.daemon.journalist) {
1827
- const a = registry.get(newAgent.id);
1828
- const turns = a?.turns || 0;
1829
- const tok = a?.tokensUsed || 0;
1830
- if (turns > 1 || tok >= 100) this.daemon.journalist.requestSynthesis('completion');
1831
- }
1899
+ this._handleResumeProcessExit(newAgent, code, signal, logStream);
1900
+ });
1832
1901
 
1833
- // Persist Layer 7 state for resumed-session completions too, not just fresh spawns.
1834
- // Without this, every resume after the first loses its work from the handoff chain.
1835
- if (finalStatus === 'completed' && !this._rotatingAgents.has(newAgent.id)) {
1836
- this._writeCompletionHandoff(newAgent).catch(err =>
1837
- console.error(`[Groove] Completion handoff failed for ${newAgent.name}:`, err.message));
1838
- }
1839
- if (this._rotatingAgents.has(newAgent.id)) {
1840
- this._rotatingAgents.delete(newAgent.id);
1841
- }
1842
- if (this.daemon.memory && (finalStatus === 'completed' || finalStatus === 'crashed')) {
1843
- try {
1844
- const events = this.daemon.classifier?.agentWindows?.[newAgent.id] || [];
1845
- const signals = events.length >= 6
1846
- ? this.daemon.adaptive.extractSignals(events, newAgent.scope)
1847
- : null;
1848
- const score = signals ? this.daemon.adaptive.scoreSession(signals) : null;
1849
- const files = this.daemon.journalist?.getAgentFiles(newAgent) || [];
1850
- this.daemon.memory.updateSpecialization(newAgent.id, {
1851
- role: newAgent.role,
1852
- qualityScore: score,
1853
- filesTouched: files,
1854
- signals,
1855
- threshold: this.daemon.adaptive?.getThreshold(newAgent.provider, newAgent.role),
1856
- });
1857
- } catch { /* best-effort */ }
1858
- }
1902
+ proc.on('close', (code, signal) => {
1903
+ this._handleResumeProcessExit(newAgent, code, signal, logStream);
1859
1904
  });
1860
1905
 
1861
1906
  proc.on('error', (err) => {
1862
1907
  logStream.write(`[error] ${err.message}\n`);
1863
1908
  logStream.end();
1864
1909
  this.handles.delete(newAgent.id);
1910
+ this._exitHandled.add(newAgent.id);
1865
1911
  this._stalledAgents.delete(newAgent.id);
1866
1912
  registry.update(newAgent.id, { status: 'crashed', pid: null });
1867
1913
  });