groove-dev 0.27.111 → 0.27.113
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.
- package/TRAINING_DATA_v3.md +11 -0
- package/codex-test/offroad-nitro-racer/dist/assets/index-CuvdKK6U.js +44 -0
- package/codex-test/offroad-nitro-racer/dist/assets/index-DvHn2Thu.css +1 -0
- package/codex-test/offroad-nitro-racer/dist/index.html +23 -0
- package/codex-test/offroad-nitro-racer/index.html +21 -0
- package/codex-test/offroad-nitro-racer/package-lock.json +841 -0
- package/codex-test/offroad-nitro-racer/package.json +15 -0
- package/codex-test/offroad-nitro-racer/src/game/AI.ts +28 -0
- package/codex-test/offroad-nitro-racer/src/game/Audio.ts +63 -0
- package/codex-test/offroad-nitro-racer/src/game/Car.ts +247 -0
- package/codex-test/offroad-nitro-racer/src/game/Effects.ts +62 -0
- package/codex-test/offroad-nitro-racer/src/game/Game.ts +229 -0
- package/codex-test/offroad-nitro-racer/src/game/Input.ts +45 -0
- package/codex-test/offroad-nitro-racer/src/game/Renderer.ts +224 -0
- package/codex-test/offroad-nitro-racer/src/game/Track.ts +158 -0
- package/codex-test/offroad-nitro-racer/src/game/UI.ts +96 -0
- package/codex-test/offroad-nitro-racer/src/game/math.ts +42 -0
- package/codex-test/offroad-nitro-racer/src/main.ts +24 -0
- package/codex-test/offroad-nitro-racer/src/style.css +291 -0
- package/codex-test/offroad-nitro-racer/src/vite-env.d.ts +1 -0
- package/codex-test/offroad-nitro-racer/tsconfig.json +18 -0
- package/codex-test/offroad-nitro-racer/vite.config.ts +7 -0
- package/moe-training/client/consent.js +47 -55
- package/moe-training/client/parsers/codex.js +3 -3
- package/moe-training/client/parsers/gemini.js +2 -2
- package/moe-training/client/step-classifier.js +2 -2
- package/moe-training/test/client/consent.test.js +23 -20
- package/moe-training/test/client/step-classifier.test.js +63 -7
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +74 -69
- package/node_modules/@groove-dev/daemon/src/index.js +30 -18
- package/node_modules/@groove-dev/gui/dist/assets/{index-CHu5w3i3.js → index-BYh6iHqL.js} +3 -3
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +3 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +15 -0
- package/node_modules/moe-training/client/consent.js +47 -55
- package/node_modules/moe-training/client/parsers/codex.js +3 -3
- package/node_modules/moe-training/client/parsers/gemini.js +2 -2
- package/node_modules/moe-training/client/step-classifier.js +2 -2
- package/node_modules/moe-training/test/client/consent.test.js +23 -20
- package/node_modules/moe-training/test/client/step-classifier.test.js +63 -7
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +74 -69
- package/packages/daemon/src/index.js +30 -18
- package/packages/gui/dist/assets/{index-CHu5w3i3.js → index-BYh6iHqL.js} +3 -3
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/preview/preview-workspace.jsx +3 -1
- package/packages/gui/src/stores/groove.js +15 -0
- package/TRAINING_DATA_v2.md +0 -9
|
@@ -132,11 +132,11 @@ describe('StepClassifier', () => {
|
|
|
132
132
|
assert.equal(StepClassifier.countUserInterventions(steps), 0);
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
-
it('reclassifies action
|
|
135
|
+
it('never reclassifies action to error', () => {
|
|
136
136
|
const classifier = new StepClassifier();
|
|
137
137
|
const step = { type: 'action', content: 'Command failed with exit code 1' };
|
|
138
138
|
const result = classifier.onStep(step);
|
|
139
|
-
assert.equal(result.type, '
|
|
139
|
+
assert.equal(result.type, 'action');
|
|
140
140
|
});
|
|
141
141
|
|
|
142
142
|
it('reclassifies observation with error content to error', () => {
|
|
@@ -215,19 +215,75 @@ describe('StepClassifier', () => {
|
|
|
215
215
|
assert.equal(result.type, 'error');
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
-
it('
|
|
218
|
+
it('does not reclassify observation containing bare word "error" in source code', () => {
|
|
219
219
|
const classifier = new StepClassifier();
|
|
220
|
-
const step = { type: '
|
|
220
|
+
const step = { type: 'observation', content: 'function handleError(err) { console.error(err); }' };
|
|
221
221
|
const result = classifier.onStep(step);
|
|
222
|
-
assert.equal(result.type, '
|
|
222
|
+
assert.equal(result.type, 'observation');
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
-
it('
|
|
225
|
+
it('does not reclassify observation with "0 errors" or "found 0 vulnerabilities"', () => {
|
|
226
226
|
const classifier = new StepClassifier();
|
|
227
|
-
const step = { type: '
|
|
227
|
+
const step = { type: 'observation', content: 'Build succeeded\n0 errors, 0 warnings\nfound 0 vulnerabilities' };
|
|
228
|
+
const result = classifier.onStep(step);
|
|
229
|
+
assert.equal(result.type, 'observation');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('does not reclassify observation reading a file that mentions exceptions', () => {
|
|
233
|
+
const classifier = new StepClassifier();
|
|
234
|
+
const step = { type: 'observation', content: '{"scripts": {"build": "tsc && vite build"}, "name": "my-app"}' };
|
|
235
|
+
const result = classifier.onStep(step);
|
|
236
|
+
assert.equal(result.type, 'observation');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('reclassifies observation with real TypeScript build error', () => {
|
|
240
|
+
const classifier = new StepClassifier();
|
|
241
|
+
const step = { type: 'observation', content: 'src/main.ts(1,8): error TS2882: Cannot find module' };
|
|
242
|
+
const result = classifier.onStep(step);
|
|
243
|
+
assert.equal(result.type, 'error');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('reclassifies observation with Python traceback', () => {
|
|
247
|
+
const classifier = new StepClassifier();
|
|
248
|
+
const step = { type: 'observation', content: 'Traceback (most recent call last):\n File "main.py", line 5' };
|
|
249
|
+
const result = classifier.onStep(step);
|
|
250
|
+
assert.equal(result.type, 'error');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('reclassifies observation with actual TypeError message', () => {
|
|
254
|
+
const classifier = new StepClassifier();
|
|
255
|
+
const step = { type: 'observation', content: 'TypeError: Cannot read properties of undefined (reading "map")' };
|
|
256
|
+
const result = classifier.onStep(step);
|
|
257
|
+
assert.equal(result.type, 'error');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('reclassifies observation with exit code failure', () => {
|
|
261
|
+
const classifier = new StepClassifier();
|
|
262
|
+
const step = { type: 'observation', content: 'Process exited with exit code 1' };
|
|
263
|
+
const result = classifier.onStep(step);
|
|
264
|
+
assert.equal(result.type, 'error');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('reclassifies observation with ModuleNotFoundError', () => {
|
|
268
|
+
const classifier = new StepClassifier();
|
|
269
|
+
const step = { type: 'observation', content: 'ModuleNotFoundError: No module named requests' };
|
|
228
270
|
const result = classifier.onStep(step);
|
|
229
271
|
assert.equal(result.type, 'error');
|
|
230
272
|
});
|
|
273
|
+
|
|
274
|
+
it('preserves action type even with error keywords', () => {
|
|
275
|
+
const classifier = new StepClassifier();
|
|
276
|
+
const step = { type: 'action', content: 'Command failed with exit code 1' };
|
|
277
|
+
const result = classifier.onStep(step);
|
|
278
|
+
assert.equal(result.type, 'action');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('preserves action type regardless of is_error flag', () => {
|
|
282
|
+
const classifier = new StepClassifier();
|
|
283
|
+
const step = { type: 'action', content: 'ENOENT: no such file', is_error: true };
|
|
284
|
+
const result = classifier.onStep(step);
|
|
285
|
+
assert.equal(result.type, 'action');
|
|
286
|
+
});
|
|
231
287
|
});
|
|
232
288
|
|
|
233
289
|
describe('StepClassifier.classifyIntent', () => {
|
|
@@ -3855,6 +3855,48 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3855
3855
|
// --- Preview Proxy (same-origin iframe support) ---
|
|
3856
3856
|
// Forwards HTTP requests to the dev server so the GUI can iframe the preview
|
|
3857
3857
|
// without cross-origin issues. WebSocket upgrade for HMR is handled in index.js.
|
|
3858
|
+
|
|
3859
|
+
function rewriteAbsoluteUrls(body, proxyBase) {
|
|
3860
|
+
let out = body;
|
|
3861
|
+
// HTML attributes: src, href, action, poster
|
|
3862
|
+
out = out.replace(/((?:src|href|action|poster)\s*=\s*(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3863
|
+
// JS imports: from '/' and import('/')
|
|
3864
|
+
out = out.replace(/(from\s+(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3865
|
+
out = out.replace(/(import\s*\(\s*(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3866
|
+
// CSS url()
|
|
3867
|
+
out = out.replace(/(url\s*\(\s*(["']?))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3868
|
+
return out;
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
const REWRITABLE_TYPES = ['text/html', 'application/javascript', 'text/javascript', 'text/css'];
|
|
3872
|
+
|
|
3873
|
+
function handleProxyResponse(proxyRes, res, proxyBase) {
|
|
3874
|
+
const fwdHeaders = { ...proxyRes.headers };
|
|
3875
|
+
delete fwdHeaders['content-security-policy'];
|
|
3876
|
+
delete fwdHeaders['x-frame-options'];
|
|
3877
|
+
|
|
3878
|
+
const ct = (fwdHeaders['content-type'] || '').toLowerCase();
|
|
3879
|
+
const shouldRewrite = REWRITABLE_TYPES.some((t) => ct.includes(t));
|
|
3880
|
+
|
|
3881
|
+
if (!shouldRewrite) {
|
|
3882
|
+
res.writeHead(proxyRes.statusCode, fwdHeaders);
|
|
3883
|
+
proxyRes.pipe(res);
|
|
3884
|
+
return;
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
const chunks = [];
|
|
3888
|
+
proxyRes.on('data', (c) => chunks.push(c));
|
|
3889
|
+
proxyRes.on('end', () => {
|
|
3890
|
+
let body = Buffer.concat(chunks).toString('utf8');
|
|
3891
|
+
body = rewriteAbsoluteUrls(body, proxyBase);
|
|
3892
|
+
const buf = Buffer.from(body, 'utf8');
|
|
3893
|
+
fwdHeaders['content-length'] = buf.length;
|
|
3894
|
+
delete fwdHeaders['transfer-encoding'];
|
|
3895
|
+
res.writeHead(proxyRes.statusCode, fwdHeaders);
|
|
3896
|
+
res.end(buf);
|
|
3897
|
+
});
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3858
3900
|
app.all('/api/preview/:teamId/proxy/*', (req, res) => {
|
|
3859
3901
|
const entry = daemon.preview?.get(req.params.teamId);
|
|
3860
3902
|
if (!entry || !entry.url) return res.status(404).json({ error: 'No active preview for this team' });
|
|
@@ -3865,9 +3907,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3865
3907
|
const proxyPath = req.params[0] || '';
|
|
3866
3908
|
const search = req.url.includes('?') ? '?' + req.url.split('?').slice(1).join('?') : '';
|
|
3867
3909
|
const fullPath = '/' + proxyPath + search;
|
|
3910
|
+
const proxyBase = `/api/preview/${req.params.teamId}/proxy`;
|
|
3868
3911
|
|
|
3869
3912
|
const headers = { ...req.headers };
|
|
3870
3913
|
delete headers.host;
|
|
3914
|
+
delete headers['accept-encoding'];
|
|
3871
3915
|
headers.host = targetUrl.host;
|
|
3872
3916
|
|
|
3873
3917
|
const proxyReq = httpRequest({
|
|
@@ -3876,13 +3920,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3876
3920
|
path: fullPath,
|
|
3877
3921
|
method: req.method,
|
|
3878
3922
|
headers,
|
|
3879
|
-
}, (proxyRes) =>
|
|
3880
|
-
const fwdHeaders = { ...proxyRes.headers };
|
|
3881
|
-
delete fwdHeaders['content-security-policy'];
|
|
3882
|
-
delete fwdHeaders['x-frame-options'];
|
|
3883
|
-
res.writeHead(proxyRes.statusCode, fwdHeaders);
|
|
3884
|
-
proxyRes.pipe(res);
|
|
3885
|
-
});
|
|
3923
|
+
}, (proxyRes) => handleProxyResponse(proxyRes, res, proxyBase));
|
|
3886
3924
|
|
|
3887
3925
|
proxyReq.on('error', (err) => {
|
|
3888
3926
|
if (!res.headersSent) res.status(502).json({ error: `Proxy error: ${err.message}` });
|
|
@@ -3899,9 +3937,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3899
3937
|
try { targetUrl = new URL(entry.devUrl || entry.url); } catch { return res.status(500).json({ error: 'Invalid preview URL' }); }
|
|
3900
3938
|
|
|
3901
3939
|
const search = req.url.includes('?') ? '?' + req.url.split('?').slice(1).join('?') : '';
|
|
3940
|
+
const proxyBase = `/api/preview/${req.params.teamId}/proxy`;
|
|
3902
3941
|
|
|
3903
3942
|
const headers = { ...req.headers };
|
|
3904
3943
|
delete headers.host;
|
|
3944
|
+
delete headers['accept-encoding'];
|
|
3905
3945
|
headers.host = targetUrl.host;
|
|
3906
3946
|
|
|
3907
3947
|
const proxyReq = httpRequest({
|
|
@@ -3910,13 +3950,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3910
3950
|
path: '/' + search,
|
|
3911
3951
|
method: req.method,
|
|
3912
3952
|
headers,
|
|
3913
|
-
}, (proxyRes) =>
|
|
3914
|
-
const fwdHeaders = { ...proxyRes.headers };
|
|
3915
|
-
delete fwdHeaders['content-security-policy'];
|
|
3916
|
-
delete fwdHeaders['x-frame-options'];
|
|
3917
|
-
res.writeHead(proxyRes.statusCode, fwdHeaders);
|
|
3918
|
-
proxyRes.pipe(res);
|
|
3919
|
-
});
|
|
3953
|
+
}, (proxyRes) => handleProxyResponse(proxyRes, res, proxyBase));
|
|
3920
3954
|
|
|
3921
3955
|
proxyReq.on('error', (err) => {
|
|
3922
3956
|
if (!res.headersSent) res.status(502).json({ error: `Proxy error: ${err.message}` });
|
|
@@ -4855,9 +4889,13 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4855
4889
|
|
|
4856
4890
|
app.get('/api/training/status', (req, res) => {
|
|
4857
4891
|
let userId = null;
|
|
4858
|
-
|
|
4892
|
+
let optedIn = false;
|
|
4893
|
+
try {
|
|
4894
|
+
userId = ConsentManager.getOrCreateUserId();
|
|
4895
|
+
optedIn = ConsentManager.isCaptureEnabled();
|
|
4896
|
+
} catch (e) { /* */ }
|
|
4859
4897
|
res.json({
|
|
4860
|
-
optedIn
|
|
4898
|
+
optedIn,
|
|
4861
4899
|
userId: userId ? userId.substring(0, 8) + '...' : null,
|
|
4862
4900
|
captureActive: !!daemon.trajectoryCapture,
|
|
4863
4901
|
sessionsCaptured: daemon.state.get('training_sessions_captured') || 0,
|
|
@@ -4869,52 +4907,23 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4869
4907
|
const { enabled } = req.body;
|
|
4870
4908
|
if (typeof enabled !== 'boolean') return res.status(400).json({ error: 'enabled must be boolean' });
|
|
4871
4909
|
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
} catch (modErr) {
|
|
4878
|
-
console.error('[training/opt-in] Native module load failed:', modErr);
|
|
4879
|
-
return res.status(500).json({
|
|
4880
|
-
error: 'Failed to enable data sharing',
|
|
4881
|
-
detail: 'Native SQLite module (better-sqlite3) is not available. On remote instances, ensure build tools are installed (gcc, g++, make, python3) and run: npm rebuild better-sqlite3',
|
|
4882
|
-
});
|
|
4883
|
-
}
|
|
4884
|
-
try {
|
|
4885
|
-
const userId = ConsentManager.getOrCreateUserId();
|
|
4886
|
-
const consent = new ConsentManager();
|
|
4887
|
-
try {
|
|
4888
|
-
consent.recordConsent(userId, true, '1.0');
|
|
4889
|
-
} finally {
|
|
4890
|
-
consent.close();
|
|
4891
|
-
}
|
|
4892
|
-
daemon.config.training_opt_in = true;
|
|
4893
|
-
saveConfig(daemon.grooveDir, daemon.config);
|
|
4910
|
+
try {
|
|
4911
|
+
const userId = ConsentManager.getOrCreateUserId();
|
|
4912
|
+
const consent = new ConsentManager();
|
|
4913
|
+
if (enabled) {
|
|
4914
|
+
consent.recordConsent(userId, true, '1.0');
|
|
4894
4915
|
await daemon._initTrajectoryCapture();
|
|
4895
4916
|
daemon.state.set('training_enrolled_at', new Date().toISOString());
|
|
4896
|
-
}
|
|
4897
|
-
|
|
4898
|
-
daemon.
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
}
|
|
4902
|
-
} else {
|
|
4903
|
-
daemon.config.training_opt_in = false;
|
|
4904
|
-
saveConfig(daemon.grooveDir, daemon.config);
|
|
4905
|
-
if (daemon.trajectoryCapture) {
|
|
4906
|
-
try { await daemon.trajectoryCapture.shutdown(); } catch (e) { /* */ }
|
|
4907
|
-
daemon.trajectoryCapture = null;
|
|
4908
|
-
}
|
|
4909
|
-
try {
|
|
4910
|
-
const userId = ConsentManager.getOrCreateUserId();
|
|
4911
|
-
const consent = new ConsentManager();
|
|
4912
|
-
try {
|
|
4913
|
-
consent.revokeConsent(userId);
|
|
4914
|
-
} finally {
|
|
4915
|
-
consent.close();
|
|
4917
|
+
} else {
|
|
4918
|
+
consent.revokeConsent(userId);
|
|
4919
|
+
if (daemon.trajectoryCapture) {
|
|
4920
|
+
try { await daemon.trajectoryCapture.shutdown(); } catch (e) { /* */ }
|
|
4921
|
+
daemon.trajectoryCapture = null;
|
|
4916
4922
|
}
|
|
4917
|
-
}
|
|
4923
|
+
}
|
|
4924
|
+
} catch (e) {
|
|
4925
|
+
console.error('[training/opt-in] Failed to update data sharing:', e);
|
|
4926
|
+
return res.status(500).json({ error: 'Failed to update data sharing', detail: e.message });
|
|
4918
4927
|
}
|
|
4919
4928
|
|
|
4920
4929
|
daemon.broadcast({ type: 'training:status', data: { optedIn: enabled, captureActive: !!daemon.trajectoryCapture } });
|
|
@@ -4924,18 +4933,13 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4924
4933
|
|
|
4925
4934
|
app.post('/api/training/opt-in/delete', async (req, res) => {
|
|
4926
4935
|
try {
|
|
4927
|
-
|
|
4928
|
-
const
|
|
4929
|
-
|
|
4936
|
+
const userId = ConsentManager.getOrCreateUserId();
|
|
4937
|
+
const consent = new ConsentManager();
|
|
4938
|
+
consent.revokeConsent(userId);
|
|
4930
4939
|
if (daemon.trajectoryCapture) {
|
|
4931
4940
|
try { await daemon.trajectoryCapture.shutdown(); } catch (e) { /* */ }
|
|
4932
4941
|
daemon.trajectoryCapture = null;
|
|
4933
4942
|
}
|
|
4934
|
-
try {
|
|
4935
|
-
const userId = ConsentManager.getOrCreateUserId();
|
|
4936
|
-
const consent = new ConsentManager();
|
|
4937
|
-
try { consent.revokeConsent(userId); } finally { consent.close(); }
|
|
4938
|
-
} catch (e) { /* */ }
|
|
4939
4943
|
daemon.broadcast({ type: 'training:status', data: { optedIn: false, captureActive: false } });
|
|
4940
4944
|
if (daemon.audit) daemon.audit.log('training.delete', {});
|
|
4941
4945
|
res.json({ ok: true, deleted: true });
|
|
@@ -4961,7 +4965,6 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4961
4965
|
'port', 'journalistInterval', 'rotationThreshold', 'autoRotation',
|
|
4962
4966
|
'qcThreshold', 'maxAgents', 'defaultProvider', 'defaultWorkingDir',
|
|
4963
4967
|
'onboardingDismissed', 'defaultModel', 'defaultChatProvider', 'defaultChatModel',
|
|
4964
|
-
'training_opt_in',
|
|
4965
4968
|
];
|
|
4966
4969
|
for (const key of Object.keys(req.body)) {
|
|
4967
4970
|
if (!ALLOWED_KEYS.includes(key)) {
|
|
@@ -6214,13 +6217,15 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
6214
6217
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
6215
6218
|
env: tagEnv,
|
|
6216
6219
|
});
|
|
6220
|
+
daemon._networkCheckProc = proc;
|
|
6217
6221
|
let stdout = '';
|
|
6218
6222
|
let stderr = '';
|
|
6219
6223
|
proc.stdout.on('data', (c) => { stdout += c.toString(); });
|
|
6220
6224
|
proc.stderr.on('data', (c) => { stderr += c.toString(); });
|
|
6221
6225
|
const timeout = setTimeout(() => { safeKill(proc, 'SIGTERM'); }, 10_000);
|
|
6222
|
-
proc.on('error', () => { clearTimeout(timeout); resolvePromise(null); });
|
|
6226
|
+
proc.on('error', () => { clearTimeout(timeout); daemon._networkCheckProc = null; resolvePromise(null); });
|
|
6223
6227
|
proc.on('close', (code) => {
|
|
6228
|
+
daemon._networkCheckProc = null;
|
|
6224
6229
|
clearTimeout(timeout);
|
|
6225
6230
|
if (code !== 0) return resolvePromise(null);
|
|
6226
6231
|
const tags = [];
|
|
@@ -290,11 +290,11 @@ export class Daemon {
|
|
|
290
290
|
});
|
|
291
291
|
|
|
292
292
|
// Debounced file I/O for registry changes (at most once per 2s)
|
|
293
|
-
|
|
293
|
+
this._registryIoTimer = null;
|
|
294
294
|
const _debouncedRegistryIo = () => {
|
|
295
|
-
if (_registryIoTimer) return;
|
|
296
|
-
_registryIoTimer = setTimeout(() => {
|
|
297
|
-
_registryIoTimer = null;
|
|
295
|
+
if (this._registryIoTimer) return;
|
|
296
|
+
this._registryIoTimer = setTimeout(() => {
|
|
297
|
+
this._registryIoTimer = null;
|
|
298
298
|
this.introducer.writeRegistryFile(this.projectDir);
|
|
299
299
|
this.introducer.injectGrooveSection(this.projectDir);
|
|
300
300
|
}, 2000);
|
|
@@ -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
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
}
|
|
@@ -781,6 +779,11 @@ export class Daemon {
|
|
|
781
779
|
if (this._stateSaveInterval) clearInterval(this._stateSaveInterval);
|
|
782
780
|
if (this._classifierInterval) clearInterval(this._classifierInterval);
|
|
783
781
|
if (this._subscriptionPollInterval) clearInterval(this._subscriptionPollInterval);
|
|
782
|
+
if (this._registryIoTimer) clearTimeout(this._registryIoTimer);
|
|
783
|
+
if (this._networkCheckProc) {
|
|
784
|
+
try { this._networkCheckProc.kill(); } catch { /* already exited */ }
|
|
785
|
+
this._networkCheckProc = null;
|
|
786
|
+
}
|
|
784
787
|
|
|
785
788
|
// Clean up file watchers and terminal sessions
|
|
786
789
|
this.fileWatcher.unwatchAll();
|
|
@@ -825,10 +828,19 @@ export class Daemon {
|
|
|
825
828
|
|
|
826
829
|
// Close server
|
|
827
830
|
return new Promise((resolvePromise) => {
|
|
828
|
-
this.
|
|
829
|
-
this.
|
|
830
|
-
|
|
831
|
-
|
|
831
|
+
this.federationWss.close(() => {
|
|
832
|
+
this.wss.close(() => {
|
|
833
|
+
this.server.close(() => {
|
|
834
|
+
// Unref lingering handles (idle fetch/undici TLS pool connections,
|
|
835
|
+
// closed servers) so they don't prevent process exit in tests.
|
|
836
|
+
for (const h of process._getActiveHandles()) {
|
|
837
|
+
if (typeof h.unref === 'function' && h !== process.stdout && h !== process.stderr) {
|
|
838
|
+
h.unref();
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
console.log('GROOVE daemon stopped.');
|
|
842
|
+
resolvePromise();
|
|
843
|
+
});
|
|
832
844
|
});
|
|
833
845
|
});
|
|
834
846
|
});
|