groove-dev 0.27.206 → 0.27.208

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 (41) 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 +301 -0
  6. package/node_modules/@groove-dev/daemon/src/axom-server.js +23 -4
  7. package/node_modules/@groove-dev/daemon/src/chatstore.js +112 -21
  8. package/node_modules/@groove-dev/daemon/src/index.js +16 -1
  9. package/node_modules/@groove-dev/daemon/src/routes/agents.js +4 -0
  10. package/node_modules/@groove-dev/daemon/src/routes/axom.js +73 -0
  11. package/node_modules/@groove-dev/daemon/src/routes/chat-history.js +6 -3
  12. package/node_modules/@groove-dev/daemon/src/routes/watch.js +11 -0
  13. package/node_modules/@groove-dev/daemon/src/watcher.js +55 -2
  14. package/node_modules/@groove-dev/daemon/test/axom-connector.test.js +44 -1
  15. package/node_modules/@groove-dev/daemon/test/axom-runtimes.test.js +198 -0
  16. package/node_modules/@groove-dev/daemon/test/chatstore.test.js +111 -19
  17. package/node_modules/@groove-dev/daemon/test/watcher.test.js +68 -2
  18. package/node_modules/@groove-dev/gui/dist/assets/{index-BYQ4qIHh.js → index-3Lzv2-To.js} +235 -235
  19. package/node_modules/@groove-dev/gui/dist/assets/index-Bh_HF8ed.css +1 -0
  20. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  21. package/node_modules/@groove-dev/gui/package.json +1 -1
  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/axom-connector.js +50 -2
  26. package/packages/daemon/src/axom-remote.js +16 -14
  27. package/packages/daemon/src/axom-runtimes.js +301 -0
  28. package/packages/daemon/src/axom-server.js +23 -4
  29. package/packages/daemon/src/chatstore.js +112 -21
  30. package/packages/daemon/src/index.js +16 -1
  31. package/packages/daemon/src/routes/agents.js +4 -0
  32. package/packages/daemon/src/routes/axom.js +73 -0
  33. package/packages/daemon/src/routes/chat-history.js +6 -3
  34. package/packages/daemon/src/routes/watch.js +11 -0
  35. package/packages/daemon/src/watcher.js +55 -2
  36. package/packages/gui/dist/assets/{index-BYQ4qIHh.js → index-3Lzv2-To.js} +235 -235
  37. package/packages/gui/dist/assets/index-Bh_HF8ed.css +1 -0
  38. package/packages/gui/dist/index.html +2 -2
  39. package/packages/gui/package.json +1 -1
  40. package/node_modules/@groove-dev/gui/dist/assets/index-DG6yq4dB.css +0 -1
  41. package/packages/gui/dist/assets/index-DG6yq4dB.css +0 -1
@@ -4,7 +4,9 @@ export function registerChatHistoryRoutes(app, daemon) {
4
4
  // Full history for all agents — the GUI loads this on connect so chats are
5
5
  // present regardless of which origin/port the tunnel came up on.
6
6
  app.get('/api/chat-history', (req, res) => {
7
- res.json({ history: daemon.chatStore.getAll() });
7
+ // view() keys live agents by their CURRENT id — ids churn on rotation, so
8
+ // the store itself is name-keyed and this translates at the edge.
9
+ res.json({ history: daemon.chatStore.view() });
8
10
  });
9
11
 
10
12
  // Append a single message for an agent.
@@ -17,13 +19,14 @@ export function registerChatHistoryRoutes(app, daemon) {
17
19
  res.json({ ok: true });
18
20
  });
19
21
 
20
- // Replace an agent's whole history (batch sync).
22
+ // Batch sync from a client. A UNION, deliberately not a replace — a client
23
+ // can add messages the server missed but can never truncate server history.
21
24
  app.put('/api/chat-history/:agentId', (req, res) => {
22
25
  const { messages } = req.body || {};
23
26
  if (!Array.isArray(messages)) {
24
27
  return res.status(400).json({ error: 'messages array required' });
25
28
  }
26
- daemon.chatStore.replace(req.params.agentId, messages);
29
+ daemon.chatStore.merge(req.params.agentId, messages);
27
30
  res.json({ ok: true });
28
31
  });
29
32
 
@@ -20,6 +20,17 @@ export function registerWatchRoutes(app, daemon) {
20
20
  if (!who) return res.status(404).json({ error: `Unknown agent: ${agent}` });
21
21
 
22
22
  const watch = daemon.watcher.create(who.id, { command, until, label, timeoutMs, intervalMs });
23
+ if (watch.reattached) {
24
+ return res.json({
25
+ ok: true,
26
+ watchId: watch.id,
27
+ reattached: true,
28
+ message: `That command is ALREADY RUNNING under watch ${watch.id} ("${watch.label}") — `
29
+ + 'this request re-attached to it instead of starting a second copy. '
30
+ + 'Do NOT launch it again by hand. You will be resumed with the result when it finishes; '
31
+ + 'you can end your turn now.',
32
+ });
33
+ }
23
34
  res.json({
24
35
  ok: true,
25
36
  watchId: watch.id,
@@ -59,6 +59,28 @@ export class Watcher {
59
59
  if (!command && !until) throw new Error('Provide either "command" (run it) or "until" (poll it)');
60
60
  if (command && until) throw new Error('Provide only one of "command" or "until"');
61
61
 
62
+ // Re-attach, never re-execute. A daemon restart resumes the agent mid-turn,
63
+ // so it re-issues the watch it thinks never completed — and a second copy of
64
+ // a long job (training, benchmark) launched against the same working dir
65
+ // corrupts the first one's output and competes for the GPU. If an identical
66
+ // command is still running for this agent, hand back the existing watch.
67
+ // This is the daemon-side equivalent of an flock, and it has to live here
68
+ // because the agent cannot know a duplicate already exists.
69
+ if (command) {
70
+ const dup = [...this.watches.values()].find(
71
+ (w) => w.status === 'active'
72
+ && w.mode === 'command'
73
+ && w.command === command
74
+ && (w.agentName === agent.name || w.agentId === agentId)
75
+ && this._jobAlive(w),
76
+ );
77
+ if (dup) {
78
+ this.daemon.audit?.log('watch.duplicate', { id: dup.id, agent: agentId, label: dup.label });
79
+ console.log(`[Groove:Watcher] Re-attached to running watch ${dup.id} instead of re-running: ${dup.label}`);
80
+ return { ...this._public(dup), reattached: true };
81
+ }
82
+ }
83
+
62
84
  const active = [...this.watches.values()].filter((w) => w.agentId === agentId && w.status === 'active');
63
85
  if (active.length >= MAX_WATCHES_PER_AGENT) {
64
86
  throw new Error(`You already have ${MAX_WATCHES_PER_AGENT} active watches — cancel one before adding another`);
@@ -118,11 +140,14 @@ export class Watcher {
118
140
  // Run the command in a SUBSHELL so its own `exit N` can't kill the wrapper
119
141
  // before the sentinel is written; tee output to a file, then atomically
120
142
  // publish the exit code so the poller never sees a half-written value.
143
+ // Append rather than truncate: if this script is ever run twice, `>` would
144
+ // erase the first run's record — exactly the evidence you need to work out
145
+ // what happened. Appending keeps both.
121
146
  const script = [
122
147
  '#!/bin/sh',
123
148
  '(',
124
149
  watch.command,
125
- `) > ${shq(watch.outFile)} 2>&1`,
150
+ `) >> ${shq(watch.outFile)} 2>&1`,
126
151
  `echo $? > ${shq(watch.statusFile)}.tmp && mv ${shq(watch.statusFile)}.tmp ${shq(watch.statusFile)}`,
127
152
  '',
128
153
  ].join('\n');
@@ -143,6 +168,16 @@ export class Watcher {
143
168
  }
144
169
  }
145
170
 
171
+ // Is this watch's detached job still running? Signal 0 tests for existence
172
+ // without delivering anything. A finished job (sentinel present) is not alive
173
+ // even if the pid happens to have been recycled by another process.
174
+ _jobAlive(watch) {
175
+ if (watch.mode !== 'command') return false;
176
+ if (watch.statusFile && existsSync(watch.statusFile)) return false; // already exited
177
+ if (!watch.pid) return false;
178
+ try { process.kill(watch.pid, 0); return true; } catch { return false; }
179
+ }
180
+
146
181
  // One poll iteration — checks for completion (sentinel for command mode, the
147
182
  // `until` command for until mode) and wakes the agent when it's met.
148
183
  _tick(watch) {
@@ -257,6 +292,7 @@ export class Watcher {
257
292
  if (!Array.isArray(data)) return 0;
258
293
 
259
294
  let rearmed = 0;
295
+ let lost = 0;
260
296
  const now = Date.now();
261
297
  for (const w of data) {
262
298
  // Drop stale finished watches; keep recent ones for history.
@@ -268,10 +304,27 @@ export class Watcher {
268
304
  }
269
305
  const watch = { ...w, _poll: null, _deadline: null };
270
306
  this.watches.set(watch.id, watch);
307
+
308
+ // A command watch whose job is gone with no exit sentinel died while the
309
+ // daemon was down. Polling it would just burn until the timeout and then
310
+ // report "may still be running" — say what actually happened instead.
311
+ if (watch.mode === 'command' && watch.pid
312
+ && !existsSync(watch.statusFile || '') && !this._jobAlive(watch)) {
313
+ lost++;
314
+ this._wake(watch, {
315
+ outcome: 'error',
316
+ summary: `The job for "${watch.label}" is no longer running and never recorded an exit code — `
317
+ + 'it was lost while the daemon was down. Check its output before assuming it finished.',
318
+ output: tailFile(watch.outFile, OUTPUT_TAIL),
319
+ });
320
+ continue;
321
+ }
322
+
271
323
  this._arm(watch);
272
324
  rearmed++;
273
325
  }
274
- if (rearmed > 0) console.log(`[Groove:Watcher] Restored ${rearmed} active watch(es) after restart`);
326
+ if (rearmed > 0) console.log(`[Groove:Watcher] Re-attached to ${rearmed} running watch(es) after restart`);
327
+ if (lost > 0) console.log(`[Groove:Watcher] ${lost} watch(es) lost their job while the daemon was down`);
275
328
  this._persist();
276
329
  return rearmed;
277
330
  }