groove-dev 0.27.207 → 0.27.209

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 (34) 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/axom-connector.js +50 -2
  4. package/node_modules/@groove-dev/daemon/src/axom-remote.js +16 -14
  5. package/node_modules/@groove-dev/daemon/src/axom-runtimes.js +498 -0
  6. package/node_modules/@groove-dev/daemon/src/axom-server.js +25 -4
  7. package/node_modules/@groove-dev/daemon/src/index.js +3 -1
  8. package/node_modules/@groove-dev/daemon/src/routes/axom.js +113 -0
  9. package/node_modules/@groove-dev/daemon/src/routes/watch.js +11 -0
  10. package/node_modules/@groove-dev/daemon/src/watcher.js +55 -2
  11. package/node_modules/@groove-dev/daemon/test/axom-connector.test.js +44 -1
  12. package/node_modules/@groove-dev/daemon/test/axom-runtimes.test.js +367 -0
  13. package/node_modules/@groove-dev/daemon/test/watcher.test.js +68 -2
  14. package/node_modules/@groove-dev/gui/dist/assets/{index-OzNfp6-Y.js → index-217ZVIOc.js} +242 -237
  15. package/node_modules/@groove-dev/gui/dist/assets/index-DdCadtGL.css +1 -0
  16. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  17. package/node_modules/@groove-dev/gui/package.json +1 -1
  18. package/package.json +1 -1
  19. package/packages/cli/package.json +1 -1
  20. package/packages/daemon/package.json +1 -1
  21. package/packages/daemon/src/axom-connector.js +50 -2
  22. package/packages/daemon/src/axom-remote.js +16 -14
  23. package/packages/daemon/src/axom-runtimes.js +498 -0
  24. package/packages/daemon/src/axom-server.js +25 -4
  25. package/packages/daemon/src/index.js +3 -1
  26. package/packages/daemon/src/routes/axom.js +113 -0
  27. package/packages/daemon/src/routes/watch.js +11 -0
  28. package/packages/daemon/src/watcher.js +55 -2
  29. package/packages/gui/dist/assets/{index-OzNfp6-Y.js → index-217ZVIOc.js} +242 -237
  30. package/packages/gui/dist/assets/index-DdCadtGL.css +1 -0
  31. package/packages/gui/dist/index.html +2 -2
  32. package/packages/gui/package.json +1 -1
  33. package/node_modules/@groove-dev/gui/dist/assets/index-DG6yq4dB.css +0 -1
  34. package/packages/gui/dist/assets/index-DG6yq4dB.css +0 -1
@@ -143,9 +143,11 @@ describe('Watcher', () => {
143
143
  assert.throws(() => watcher.create('nope', { command: 'x' }), /Agent not found/);
144
144
  });
145
145
 
146
+ // Distinct commands — identical ones now re-attach to the running job rather
147
+ // than stacking up, so they would never reach the cap.
146
148
  it('caps active watches per agent', () => {
147
- for (let i = 0; i < 5; i++) watcher.create('a1', { command: 'sleep 5', label: `w${i}` });
148
- assert.throws(() => watcher.create('a1', { command: 'sleep 5' }), /already have 5/);
149
+ for (let i = 0; i < 5; i++) watcher.create('a1', { command: `sleep 5 # ${i}`, label: `w${i}` });
150
+ assert.throws(() => watcher.create('a1', { command: 'sleep 5 # 6' }), /already have 5/);
149
151
  });
150
152
 
151
153
  it('cancels a watch and stops its process', async () => {
@@ -206,6 +208,70 @@ describe('Watcher', () => {
206
208
  watcher2.stop();
207
209
  });
208
210
 
211
+ // ── no double-launch (regression: daemon rebuild re-ran a live job) ──
212
+ //
213
+ // A daemon restart resumes the agent mid-turn, so it re-issues the watch it
214
+ // believes never completed. Launching a second copy of a long job corrupts
215
+ // the first one's output and competes for the same hardware.
216
+
217
+ it('re-attaches instead of launching a second copy of a running command', async () => {
218
+ const marker = resolve(grooveDir, 'runs.txt');
219
+ const cmd = `echo run >> ${marker}; sleep 3`;
220
+
221
+ const first = watcher.create('a1', { command: cmd, label: 'champion v11' });
222
+ await settle(300);
223
+
224
+ const second = watcher.create('a1', { command: cmd, label: 'champion v11' });
225
+ assert.equal(second.id, first.id, 'the same watch is returned, not a new one');
226
+ assert.equal(second.reattached, true, 'the caller is told it re-attached');
227
+ assert.equal(
228
+ watcher.list().filter((w) => w.status === 'active').length, 1,
229
+ 'only one active watch exists',
230
+ );
231
+
232
+ await settle(400);
233
+ const runs = readFileSync(marker, 'utf8').trim().split('\n').filter(Boolean);
234
+ assert.equal(runs.length, 1, 'the command executed exactly once');
235
+ });
236
+
237
+ it('does not re-attach once the original job has exited', async () => {
238
+ const cmd = 'exit 0';
239
+ const first = watcher.create('a1', { command: cmd, label: 'short' });
240
+ await settle(500); // job finishes and the watch fires
241
+
242
+ const second = watcher.create('a1', { command: cmd, label: 'short again' });
243
+ assert.notEqual(second.id, first.id, 'a finished job may legitimately be re-run');
244
+ assert.ok(!second.reattached);
245
+ });
246
+
247
+ it('reports a job that vanished while the daemon was down instead of polling to timeout', async () => {
248
+ const w = watcher.create('a1', { command: 'sleep 30', label: 'doomed' });
249
+ await settle(200);
250
+ watcher._killJob(watcher.watches.get(w.id)); // job dies during the outage
251
+ watcher.stop();
252
+ await settle(200);
253
+
254
+ const daemon2 = makeDaemon(grooveDir);
255
+ wireDelivery(daemon2);
256
+ const watcher2 = new Watcher(daemon2);
257
+ daemon2.watcher = watcher2;
258
+ daemon2.registry.add({ id: 'a1', name: 'fullstack-1', role: 'fullstack', provider: 'claude-code' });
259
+
260
+ watcher2.restore();
261
+ await settle(120);
262
+ assert.equal(daemon2.delivered.length, 1, 'the agent is told the job was lost');
263
+ assert.match(daemon2.delivered[0].message, /lost while the daemon was down/);
264
+ watcher2.stop();
265
+ });
266
+
267
+ it('appends to the run log so a re-run cannot erase the record', () => {
268
+ const w = watcher.create('a1', { command: 'echo hi', label: 'log' });
269
+ const script = readFileSync(resolve(grooveDir, 'watch-runs', w.id, 'run.sh'), 'utf8');
270
+ const outLine = script.split('\n').find((l) => l.includes('out.log'));
271
+ assert.ok(outLine.includes('>>'), 'the output redirect appends');
272
+ assert.ok(!/[^>]>\s*'[^']*out\.log/.test(outLine), 'the output log is never truncated');
273
+ });
274
+
209
275
  it('persists watches to disk', () => {
210
276
  watcher.create('a1', { command: 'sleep 5', label: 'persisted' });
211
277
  const raw = JSON.parse(readFileSync(resolve(grooveDir, 'watches.json'), 'utf8'));