groove-dev 0.27.110 → 0.27.112

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/EMBEDDING_SERVICE_BUILD_PLAN.md +200 -0
  2. package/TRAINING_DATA_v2.md +9 -0
  3. package/moe-training/client/consent.js +47 -55
  4. package/moe-training/client/domain-tagger.js +3 -1
  5. package/moe-training/client/trajectory-capture.js +3 -2
  6. package/moe-training/shared/constants.js +1 -0
  7. package/moe-training/test/client/consent.test.js +23 -20
  8. package/moe-training/test/client/domain-tagger.test.js +6 -4
  9. package/node_modules/@groove-dev/cli/package.json +1 -1
  10. package/node_modules/@groove-dev/daemon/package.json +1 -1
  11. package/node_modules/@groove-dev/daemon/src/api.js +24 -42
  12. package/node_modules/@groove-dev/daemon/src/index.js +8 -10
  13. package/node_modules/@groove-dev/gui/dist/assets/{index-B8JomvGM.js → index-CHu5w3i3.js} +1 -1
  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/stores/groove.js +1 -1
  17. package/node_modules/moe-training/client/consent.js +47 -55
  18. package/node_modules/moe-training/client/domain-tagger.js +3 -1
  19. package/node_modules/moe-training/client/trajectory-capture.js +3 -2
  20. package/node_modules/moe-training/shared/constants.js +1 -0
  21. package/node_modules/moe-training/test/client/consent.test.js +23 -20
  22. package/node_modules/moe-training/test/client/domain-tagger.test.js +6 -4
  23. package/package.json +1 -1
  24. package/packages/cli/package.json +1 -1
  25. package/packages/daemon/package.json +1 -1
  26. package/packages/daemon/src/api.js +24 -42
  27. package/packages/daemon/src/index.js +8 -10
  28. package/packages/gui/dist/assets/{index-B8JomvGM.js → index-CHu5w3i3.js} +1 -1
  29. package/packages/gui/dist/index.html +1 -1
  30. package/packages/gui/package.json +1 -1
  31. package/packages/gui/src/stores/groove.js +1 -1
  32. package/TRAINING_DATA.md +0 -12
  33. package/codex/browser-racing-game/README.md +0 -45
  34. package/codex/browser-racing-game/dist/assets/index-D-sGTraQ.js +0 -47
  35. package/codex/browser-racing-game/dist/assets/index-S75nJv69.css +0 -1
  36. package/codex/browser-racing-game/dist/index.html +0 -14
  37. package/codex/browser-racing-game/index.html +0 -13
  38. package/codex/browser-racing-game/package-lock.json +0 -841
  39. package/codex/browser-racing-game/package.json +0 -15
  40. package/codex/browser-racing-game/src/app.css +0 -359
  41. package/codex/browser-racing-game/src/main.ts +0 -913
  42. package/codex/browser-racing-game/tsconfig.json +0 -20
  43. package/codex/browser-racing-game/vite.config.ts +0 -12
@@ -6,7 +6,7 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <link rel="icon" type="image/png" href="/favicon.png" />
8
8
  <title>Groove GUI</title>
9
- <script type="module" crossorigin src="/assets/index-B8JomvGM.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-CHu5w3i3.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-26L3JoZv.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/reactflow-DoBZjiHE.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/codemirror-CFF1Lrnz.js">
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.110",
3
+ "version": "0.27.112",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -2702,7 +2702,7 @@ export const useGrooveStore = create((set, get) => ({
2702
2702
  set({ trainingOptIn: enabled });
2703
2703
  if (!enabled) set({ trainingStats: null });
2704
2704
  } catch (e) {
2705
- get().addToast('error', 'Failed to update training preference');
2705
+ get().addToast('error', 'Failed to update training preference', e.body?.detail || e.message);
2706
2706
  }
2707
2707
  },
2708
2708
 
@@ -1,44 +1,46 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
 
3
- import Database from 'better-sqlite3';
4
3
  import { randomUUID } from 'node:crypto';
5
- import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
5
  import { join } from 'node:path';
7
6
  import { homedir } from 'node:os';
8
7
  import { CURRENT_CONSENT_VERSION } from '../shared/constants.js';
9
8
 
9
+ function ensureDir(filePath) {
10
+ const dir = filePath.replace(/[/\\][^/\\]+$/, '');
11
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
12
+ }
13
+
14
+ function readJSON(filePath) {
15
+ if (!existsSync(filePath)) return null;
16
+ try { return JSON.parse(readFileSync(filePath, 'utf-8')); } catch { return null; }
17
+ }
18
+
19
+ function writeJSON(filePath, data) {
20
+ ensureDir(filePath);
21
+ writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
22
+ }
23
+
10
24
  export class ConsentManager {
11
- constructor(dbPath) {
12
- this._dbPath = dbPath || join(homedir(), '.groove', 'consent.db');
13
- const dir = this._dbPath.replace(/[/\\][^/\\]+$/, '');
14
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
15
- this._db = new Database(this._dbPath);
16
- this._db.pragma('journal_mode = WAL');
17
- this._db.exec(`
18
- CREATE TABLE IF NOT EXISTS consent_history (
19
- id INTEGER PRIMARY KEY AUTOINCREMENT,
20
- user_id TEXT NOT NULL,
21
- opted_in INTEGER NOT NULL,
22
- consent_version TEXT NOT NULL,
23
- metadata TEXT,
24
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
25
- )
26
- `);
25
+ constructor(consentPath) {
26
+ this._path = consentPath || join(homedir(), '.groove', 'consent.json');
27
27
  }
28
28
 
29
- recordConsent(userId, optedIn, consentVersion, metadata) {
30
- this._db.prepare(
31
- 'INSERT INTO consent_history (user_id, opted_in, consent_version, metadata) VALUES (?, ?, ?, ?)'
32
- ).run(userId, optedIn ? 1 : 0, consentVersion, metadata ? JSON.stringify(metadata) : null);
29
+ recordConsent(userId, optedIn, consentVersion) {
30
+ const data = {
31
+ user_id: userId,
32
+ opted_in: !!optedIn,
33
+ consent_version: consentVersion,
34
+ updated_at: new Date().toISOString(),
35
+ };
36
+ writeJSON(this._path, data);
33
37
  }
34
38
 
35
39
  isOptedIn(userId) {
36
- const row = this._db.prepare(
37
- 'SELECT opted_in, consent_version FROM consent_history WHERE user_id = ? ORDER BY id DESC LIMIT 1'
38
- ).get(userId);
39
- if (!row) return false;
40
- if (row.consent_version !== CURRENT_CONSENT_VERSION) return false;
41
- return row.opted_in === 1;
40
+ const data = readJSON(this._path);
41
+ if (!data) return false;
42
+ if (data.consent_version !== CURRENT_CONSENT_VERSION) return false;
43
+ return data.opted_in === true;
42
44
  }
43
45
 
44
46
  revokeConsent(userId) {
@@ -46,34 +48,28 @@ export class ConsentManager {
46
48
  }
47
49
 
48
50
  getOptedInCount() {
49
- const row = this._db.prepare(`
50
- SELECT COUNT(DISTINCT user_id) as cnt FROM consent_history ch1
51
- WHERE opted_in = 1
52
- AND consent_version = ?
53
- AND id = (SELECT MAX(id) FROM consent_history ch2 WHERE ch2.user_id = ch1.user_id)
54
- `).get(CURRENT_CONSENT_VERSION);
55
- return row?.cnt || 0;
51
+ const data = readJSON(this._path);
52
+ if (!data || !data.opted_in || data.consent_version !== CURRENT_CONSENT_VERSION) return 0;
53
+ return 1;
56
54
  }
57
55
 
58
56
  getConsentHistory(userId) {
59
- const rows = this._db.prepare(
60
- 'SELECT * FROM consent_history WHERE user_id = ? ORDER BY id ASC'
61
- ).all(userId);
62
- return rows.map((r) => ({
63
- ...r,
64
- opted_in: r.opted_in === 1,
65
- metadata: r.metadata ? JSON.parse(r.metadata) : null,
66
- }));
57
+ const data = readJSON(this._path);
58
+ if (!data) return [];
59
+ return [{
60
+ user_id: data.user_id,
61
+ opted_in: data.opted_in,
62
+ consent_version: data.consent_version,
63
+ created_at: data.updated_at,
64
+ metadata: null,
65
+ }];
67
66
  }
68
67
 
69
- close() {
70
- this._db.close();
71
- }
68
+ close() {}
72
69
 
73
70
  static getOrCreateUserId(userIdPath) {
74
71
  const filePath = userIdPath || join(homedir(), '.groove', 'user_id');
75
- const dir = filePath.replace(/[/\\][^/\\]+$/, '');
76
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
72
+ ensureDir(filePath);
77
73
  if (existsSync(filePath)) {
78
74
  return readFileSync(filePath, 'utf-8').trim();
79
75
  }
@@ -82,15 +78,11 @@ export class ConsentManager {
82
78
  return uid;
83
79
  }
84
80
 
85
- static isCaptureEnabled(userIdPath, dbPath) {
81
+ static isCaptureEnabled(userIdPath, consentPath) {
86
82
  const filePath = userIdPath || join(homedir(), '.groove', 'user_id');
87
83
  if (!existsSync(filePath)) return false;
84
+ const manager = new ConsentManager(consentPath);
88
85
  const userId = readFileSync(filePath, 'utf-8').trim();
89
- const manager = new ConsentManager(dbPath);
90
- try {
91
- return manager.isOptedIn(userId);
92
- } finally {
93
- manager.close();
94
- }
86
+ return manager.isOptedIn(userId);
95
87
  }
96
88
  }
@@ -1,5 +1,7 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
 
3
+ import { EMBEDDING_SERVICE_URL } from '../shared/constants.js';
4
+
3
5
  const DEFAULT_MODEL = 'sentence-transformers/all-MiniLM-L6-v2';
4
6
  const DEFAULT_TOP_K = 3;
5
7
 
@@ -171,7 +173,7 @@ const DOMAIN_TAXONOMY = {
171
173
 
172
174
  export class DomainTagger {
173
175
  constructor(options = {}) {
174
- this._serviceUrl = options.serviceUrl || process.env.EMBEDDING_SERVICE_URL || null;
176
+ this._serviceUrl = options.serviceUrl !== undefined ? options.serviceUrl : EMBEDDING_SERVICE_URL;
175
177
  this._registryUrl = options.registryUrl || process.env.LEAF_REGISTRY_URL || null;
176
178
  this._registry = options.registry || null;
177
179
  this._model = options.model || DEFAULT_MODEL;
@@ -298,9 +298,10 @@ export class TrajectoryCapture {
298
298
 
299
299
  if (this._domainTagger) {
300
300
  const role = ctx.metadata.agent_role || '';
301
- const firstPrompt = ctx.allSteps.find((s) => s.type === 'thought')?.content || '';
302
301
  const thoughtSteps = ctx.allSteps.filter((s) => s.type === 'thought');
303
- const routingText = DomainTagger.buildRoutingText(role, firstPrompt, thoughtSteps);
302
+ const firstPrompt = thoughtSteps[0]?.content || '';
303
+ const remainingThoughts = thoughtSteps.slice(1);
304
+ const routingText = DomainTagger.buildRoutingText(role, firstPrompt, remainingThoughts);
304
305
  ctx.metadata.domain_tags = await this._domainTagger.tag(routingText);
305
306
  ctx.metadata.session_embedding = await this._domainTagger.embed(routingText);
306
307
  }
@@ -39,3 +39,4 @@ export const TRAINING_EXCLUSION_REASONS = ['too_few_steps', 'no_actions', 'no_ob
39
39
  export const USER_MESSAGE_MAX_CHARS = 2000;
40
40
 
41
41
  export const CENTRAL_COMMAND_URL = process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai';
42
+ export const EMBEDDING_SERVICE_URL = process.env.EMBEDDING_SERVICE_URL || `${CENTRAL_COMMAND_URL}/v1/embed`;
@@ -8,12 +8,12 @@ import { tmpdir } from 'node:os';
8
8
  import { ConsentManager } from '../../client/consent.js';
9
9
 
10
10
  describe('ConsentManager', () => {
11
- let tmpDir, dbPath, manager;
11
+ let tmpDir, consentPath, manager;
12
12
 
13
13
  beforeEach(() => {
14
14
  tmpDir = mkdtempSync(join(tmpdir(), 'consent-test-'));
15
- dbPath = join(tmpDir, 'consent.db');
16
- manager = new ConsentManager(dbPath);
15
+ consentPath = join(tmpDir, 'consent.json');
16
+ manager = new ConsentManager(consentPath);
17
17
  });
18
18
 
19
19
  afterEach(() => {
@@ -43,19 +43,24 @@ describe('ConsentManager', () => {
43
43
 
44
44
  it('getOptedInCount counts correctly', () => {
45
45
  manager.recordConsent('user1', true, '1.0');
46
- manager.recordConsent('user2', true, '1.0');
47
- manager.recordConsent('user3', false, '1.0');
48
- assert.equal(manager.getOptedInCount(), 2);
46
+ assert.equal(manager.getOptedInCount(), 1);
49
47
  });
50
48
 
51
- it('getConsentHistory returns all records', () => {
52
- manager.recordConsent('user1', true, '1.0', { source: 'ui' });
53
- manager.revokeConsent('user1');
49
+ it('getConsentHistory returns current state', () => {
50
+ manager.recordConsent('user1', true, '1.0');
54
51
  const history = manager.getConsentHistory('user1');
55
- assert.equal(history.length, 2);
52
+ assert.equal(history.length, 1);
56
53
  assert.equal(history[0].opted_in, true);
57
- assert.deepEqual(history[0].metadata, { source: 'ui' });
58
- assert.equal(history[1].opted_in, false);
54
+ });
55
+
56
+ it('consent.json is written with 0o600 permissions', () => {
57
+ manager.recordConsent('user1', true, '1.0');
58
+ assert.ok(existsSync(consentPath));
59
+ const data = JSON.parse(readFileSync(consentPath, 'utf-8'));
60
+ assert.equal(data.opted_in, true);
61
+ assert.equal(data.consent_version, '1.0');
62
+ assert.equal(data.user_id, 'user1');
63
+ assert.ok(data.updated_at);
59
64
  });
60
65
  });
61
66
 
@@ -96,26 +101,24 @@ describe('ConsentManager.isCaptureEnabled', () => {
96
101
  });
97
102
 
98
103
  it('returns false when no user_id file exists', () => {
99
- const result = ConsentManager.isCaptureEnabled(join(tmpDir, 'no_file'), join(tmpDir, 'c.db'));
104
+ const result = ConsentManager.isCaptureEnabled(join(tmpDir, 'no_file'), join(tmpDir, 'consent.json'));
100
105
  assert.equal(result, false);
101
106
  });
102
107
 
103
108
  it('returns false when user not opted in', () => {
104
109
  const uidPath = join(tmpDir, 'user_id');
105
- const uid = ConsentManager.getOrCreateUserId(uidPath);
106
- const dbPath = join(tmpDir, 'consent.db');
107
- const result = ConsentManager.isCaptureEnabled(uidPath, dbPath);
110
+ ConsentManager.getOrCreateUserId(uidPath);
111
+ const result = ConsentManager.isCaptureEnabled(uidPath, join(tmpDir, 'consent.json'));
108
112
  assert.equal(result, false);
109
113
  });
110
114
 
111
115
  it('returns true when user is opted in', () => {
112
116
  const uidPath = join(tmpDir, 'user_id');
113
117
  const uid = ConsentManager.getOrCreateUserId(uidPath);
114
- const dbPath = join(tmpDir, 'consent.db');
115
- const mgr = new ConsentManager(dbPath);
118
+ const consentPath = join(tmpDir, 'consent.json');
119
+ const mgr = new ConsentManager(consentPath);
116
120
  mgr.recordConsent(uid, true, '1.0');
117
- mgr.close();
118
- const result = ConsentManager.isCaptureEnabled(uidPath, dbPath);
121
+ const result = ConsentManager.isCaptureEnabled(uidPath, consentPath);
119
122
  assert.equal(result, true);
120
123
  });
121
124
  });
@@ -8,7 +8,7 @@ describe('DomainTagger', () => {
8
8
  let tagger;
9
9
 
10
10
  beforeEach(async () => {
11
- tagger = new DomainTagger();
11
+ tagger = new DomainTagger({ serviceUrl: null });
12
12
  await tagger.init();
13
13
  });
14
14
 
@@ -176,7 +176,7 @@ describe('DomainTagger', () => {
176
176
  });
177
177
 
178
178
  it('returns null when not initialized', async () => {
179
- const uninit = new DomainTagger();
179
+ const uninit = new DomainTagger({ serviceUrl: null });
180
180
  const result = await uninit.tag('Build a Python Django app');
181
181
  assert.equal(result, null);
182
182
  });
@@ -203,6 +203,7 @@ describe('DomainTagger', () => {
203
203
 
204
204
  it('accepts registry option and falls back to keyword mode without embedding service', async () => {
205
205
  const registryTagger = new DomainTagger({
206
+ serviceUrl: null,
206
207
  registry: [
207
208
  { id: 'quantum_computing', domain_description: 'Quantum computing, qubits, quantum gates' },
208
209
  { id: 'bioinformatics', domain_description: 'Biology, genomics, DNA analysis' },
@@ -215,6 +216,7 @@ describe('DomainTagger', () => {
215
216
 
216
217
  it('accepts registryUrl option and falls back to keyword mode when unavailable', async () => {
217
218
  const registryTagger = new DomainTagger({
219
+ serviceUrl: null,
218
220
  registryUrl: 'http://localhost:99999/api/leaves',
219
221
  });
220
222
  await registryTagger.init();
@@ -223,7 +225,7 @@ describe('DomainTagger', () => {
223
225
  });
224
226
 
225
227
  it('uses expanded taxonomy — has 40 domains by default', async () => {
226
- const defaultTagger = new DomainTagger();
228
+ const defaultTagger = new DomainTagger({ serviceUrl: null });
227
229
  await defaultTagger.init();
228
230
  assert.ok(defaultTagger._domains.length >= 35, `Expected 35+ domains, got ${defaultTagger._domains.length}`);
229
231
  });
@@ -239,7 +241,7 @@ describe('DomainTagger', () => {
239
241
  });
240
242
 
241
243
  it('embed() returns null when not initialized', async () => {
242
- const uninit = new DomainTagger();
244
+ const uninit = new DomainTagger({ serviceUrl: null });
243
245
  const result = await uninit.embed('Build a Python app');
244
246
  assert.equal(result, null);
245
247
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.110",
3
+ "version": "0.27.112",
4
4
  "description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.110",
3
+ "version": "0.27.112",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.110",
3
+ "version": "0.27.112",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -112,7 +112,7 @@ export function createApi(app, daemon) {
112
112
  res.setHeader('Content-Security-Policy', "default-src * 'unsafe-inline' 'unsafe-eval' data: blob:; frame-ancestors 'self'");
113
113
  } else {
114
114
  res.setHeader('X-Frame-Options', 'DENY');
115
- res.setHeader('Content-Security-Policy', "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; connect-src 'self' ws://localhost:* ws://127.0.0.1:* http://localhost:* http://127.0.0.1:*; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-src 'self'; frame-ancestors 'none'");
115
+ res.setHeader('Content-Security-Policy', "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; connect-src 'self' ws://localhost:* ws://127.0.0.1:* http://localhost:* http://127.0.0.1:*; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-src 'self' http://127.0.0.1:* http://localhost:*; frame-ancestors 'none'");
116
116
  }
117
117
  next();
118
118
  });
@@ -4855,9 +4855,13 @@ Keep responses concise. Help them think, don't lecture them about the system the
4855
4855
 
4856
4856
  app.get('/api/training/status', (req, res) => {
4857
4857
  let userId = null;
4858
- try { userId = ConsentManager.isCaptureEnabled() ? ConsentManager.getOrCreateUserId() : null; } catch (e) { /* no db yet */ }
4858
+ let optedIn = false;
4859
+ try {
4860
+ userId = ConsentManager.getOrCreateUserId();
4861
+ optedIn = ConsentManager.isCaptureEnabled();
4862
+ } catch (e) { /* */ }
4859
4863
  res.json({
4860
- optedIn: !!daemon.config.training_opt_in,
4864
+ optedIn,
4861
4865
  userId: userId ? userId.substring(0, 8) + '...' : null,
4862
4866
  captureActive: !!daemon.trajectoryCapture,
4863
4867
  sessionsCaptured: daemon.state.get('training_sessions_captured') || 0,
@@ -4869,39 +4873,23 @@ Keep responses concise. Help them think, don't lecture them about the system the
4869
4873
  const { enabled } = req.body;
4870
4874
  if (typeof enabled !== 'boolean') return res.status(400).json({ error: 'enabled must be boolean' });
4871
4875
 
4872
- daemon.config.training_opt_in = enabled;
4873
- const { saveConfig } = await import('./firstrun.js');
4874
- saveConfig(daemon.grooveDir, daemon.config);
4875
-
4876
- if (enabled) {
4877
- try {
4878
- const userId = ConsentManager.getOrCreateUserId();
4879
- const consent = new ConsentManager();
4880
- try {
4881
- consent.recordConsent(userId, true, '1.0');
4882
- } finally {
4883
- consent.close();
4884
- }
4876
+ try {
4877
+ const userId = ConsentManager.getOrCreateUserId();
4878
+ const consent = new ConsentManager();
4879
+ if (enabled) {
4880
+ consent.recordConsent(userId, true, '1.0');
4885
4881
  await daemon._initTrajectoryCapture();
4886
4882
  daemon.state.set('training_enrolled_at', new Date().toISOString());
4887
- } catch (e) {
4888
- daemon.config.training_opt_in = false;
4889
- return res.status(500).json({ error: 'Failed to enable data sharing', detail: e.message });
4890
- }
4891
- } else {
4892
- if (daemon.trajectoryCapture) {
4893
- try { await daemon.trajectoryCapture.shutdown(); } catch (e) { /* */ }
4894
- daemon.trajectoryCapture = null;
4895
- }
4896
- try {
4897
- const userId = ConsentManager.getOrCreateUserId();
4898
- const consent = new ConsentManager();
4899
- try {
4900
- consent.revokeConsent(userId);
4901
- } finally {
4902
- consent.close();
4883
+ } else {
4884
+ consent.revokeConsent(userId);
4885
+ if (daemon.trajectoryCapture) {
4886
+ try { await daemon.trajectoryCapture.shutdown(); } catch (e) { /* */ }
4887
+ daemon.trajectoryCapture = null;
4903
4888
  }
4904
- } catch (e) { /* no user_id yet */ }
4889
+ }
4890
+ } catch (e) {
4891
+ console.error('[training/opt-in] Failed to update data sharing:', e);
4892
+ return res.status(500).json({ error: 'Failed to update data sharing', detail: e.message });
4905
4893
  }
4906
4894
 
4907
4895
  daemon.broadcast({ type: 'training:status', data: { optedIn: enabled, captureActive: !!daemon.trajectoryCapture } });
@@ -4911,18 +4899,13 @@ Keep responses concise. Help them think, don't lecture them about the system the
4911
4899
 
4912
4900
  app.post('/api/training/opt-in/delete', async (req, res) => {
4913
4901
  try {
4914
- daemon.config.training_opt_in = false;
4915
- const { saveConfig } = await import('./firstrun.js');
4916
- saveConfig(daemon.grooveDir, daemon.config);
4902
+ const userId = ConsentManager.getOrCreateUserId();
4903
+ const consent = new ConsentManager();
4904
+ consent.revokeConsent(userId);
4917
4905
  if (daemon.trajectoryCapture) {
4918
4906
  try { await daemon.trajectoryCapture.shutdown(); } catch (e) { /* */ }
4919
4907
  daemon.trajectoryCapture = null;
4920
4908
  }
4921
- try {
4922
- const userId = ConsentManager.getOrCreateUserId();
4923
- const consent = new ConsentManager();
4924
- try { consent.revokeConsent(userId); } finally { consent.close(); }
4925
- } catch (e) { /* */ }
4926
4909
  daemon.broadcast({ type: 'training:status', data: { optedIn: false, captureActive: false } });
4927
4910
  if (daemon.audit) daemon.audit.log('training.delete', {});
4928
4911
  res.json({ ok: true, deleted: true });
@@ -4948,7 +4931,6 @@ Keep responses concise. Help them think, don't lecture them about the system the
4948
4931
  'port', 'journalistInterval', 'rotationThreshold', 'autoRotation',
4949
4932
  'qcThreshold', 'maxAgents', 'defaultProvider', 'defaultWorkingDir',
4950
4933
  'onboardingDismissed', 'defaultModel', 'defaultChatProvider', 'defaultChatModel',
4951
- 'training_opt_in',
4952
4934
  ];
4953
4935
  for (const key of Object.keys(req.body)) {
4954
4936
  if (!ALLOWED_KEYS.includes(key)) {
@@ -666,17 +666,15 @@ export class Daemon {
666
666
  }
667
667
 
668
668
  async _initTrajectoryCapture() {
669
- if (!this.config.training_opt_in) return;
670
669
  try {
671
- if (ConsentManager.isCaptureEnabled()) {
672
- const pkgPath = new URL('../package.json', import.meta.url);
673
- const version = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
674
- this.trajectoryCapture = new TrajectoryCapture({
675
- centralCommandUrl: process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai',
676
- grooveVersion: version,
677
- });
678
- this.trajectoryCapture.init();
679
- }
670
+ if (!ConsentManager.isCaptureEnabled()) return;
671
+ const pkgPath = new URL('../package.json', import.meta.url);
672
+ const version = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
673
+ this.trajectoryCapture = new TrajectoryCapture({
674
+ centralCommandUrl: process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai',
675
+ grooveVersion: version,
676
+ });
677
+ this.trajectoryCapture.init();
680
678
  } catch (e) {
681
679
  // Training capture is never critical
682
680
  }